Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
63027e0
feat(web): the audit log filters by a date range, and says which wind…
mforce Sep 3, 2026
f13858f
feat(web): expenses filters by a date range like its sibling screens …
mforce Sep 3, 2026
43dec5e
fix(web): the stock lot date range sits in the bounded toolbar (#653)
mforce Sep 3, 2026
c3275b7
docs(web): the help text describes the date-range filters that shippe…
mforce Sep 3, 2026
f9ae138
test(web): pin the audit and expenses date range to the bounded toolb…
mforce Sep 3, 2026
8d51846
fix(web): the audit date filter rejects impossible dates, and its his…
mforce Sep 3, 2026
41bbfe1
fix(web): a scoped audit view filtered to nothing names both the reco…
mforce Sep 3, 2026
af52d25
fix(web): the audit date filter accepts low-numbered years, and its e…
mforce Sep 3, 2026
0e93dac
docs: name the scoped filtered-empty key and state the #653 relations…
mforce Sep 3, 2026
215f830
fix(web): date validation gets one boundary table instead of one case…
mforce Sep 3, 2026
77d6670
test(web): pin whitespace against a lenient future date validator, an…
mforce Sep 3, 2026
7e01864
fix(web): the expense range bounds are not capped at today, which the…
mforce Sep 3, 2026
49654cd
docs(web): the help text stops describing a cap the filters no longer…
mforce Sep 4, 2026
e2f63d1
docs(web): the help text claims only what is true of recording, and s…
mforce Sep 4, 2026
70a53d8
docs: item 7 names each screen's actual initial filter value (#666)
mforce Sep 4, 2026
c8433c5
docs(web): the date-cap help text covers every stocked item, not only…
mforce Sep 4, 2026
2fd1f3c
fix(web): the help text calls the expiry field what the field calls i…
mforce Sep 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
366 changes: 366 additions & 0 deletions docs/designs/666-667-spa-date-range-filters.md

Large diffs are not rendered by default.

35 changes: 24 additions & 11 deletions web/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1693,7 +1693,8 @@ export const en = {
enterAmountGreaterThanZero: "Enter an amount greater than zero.",

// Filters
monthLabel: "Month",
fromLabel: "From",
toLabel: "To",
// Shared by the filter select, the add-form select, and the edit-form
// select — all three already carry this identical label in source.
categoryLabel: "Category",
Expand All @@ -1702,7 +1703,7 @@ export const en = {
manageCategoriesButton: "manage categories",
// {{amount}} is formatMoney's already-formatted total — farm-locale DATA,
// never routed through i18n.language.
monthTotalLabel: "Month total: {{amount}}",
periodTotalLabel: "Total for this period: {{amount}}",

// Category-management panel
categoriesHeading: "Expense categories",
Expand Down Expand Up @@ -1742,7 +1743,8 @@ export const en = {
saveCorrectionButton: "Save correction",

// Expenses table
noExpensesMessage: "No expenses for this month.",
noExpensesMessage: "No expenses recorded yet.",
noExpensesMatch: "No expenses match these filters.",
dateHeader: "Date",
categoryHeader: "Category",
descriptionHeader: "Description",
Expand Down Expand Up @@ -2052,8 +2054,16 @@ export const en = {
actionHeader: "Action",
entityHeader: "Entity",
reasonHeader: "Reason",
// #666 — the date window. Inclusive calendar days over the UTC timestamp,
// matching this screen's own "When (UTC)" column.
fromLabel: "From",
toLabel: "To",
emptyMessage: "No audit events yet.",
scopedEmptyMessage: "No audit events for this record yet.",
// Distinct from emptyMessage on purpose: under an active date range the
// log is not empty, the window is (INV-4).
filteredEmptyMessage: "No audit events match these filters.",
scopedFilteredEmptyMessage: "No audit events for this record match these filters.",
loadMoreButton: "load more",
},
// Task 30 (B5, #182) — ExportPage: the manual-backup screen (#95,
Expand Down Expand Up @@ -2818,7 +2828,8 @@ export const en = {
expensesHeading: "Expenses (admin)",
expensesRecording:
"Record money going out: date, category, description, and amount (in the farm's currency), optionally "
+ "tied to a flock. The month picker shows a running total; categories are managed on the same screen "
+ "tied to a flock. The date range — the farm's own dates, not a browser clock elsewhere — shows a running "
+ "total for the period you pick; categories are managed on the same screen "
+ "(deactivating one hides it from new expenses — recorded ones keep it).",
expensesCorrections:
"Corrections edit the expense in place (<strong>correct</strong> on the row). If someone else "
Expand Down Expand Up @@ -2854,7 +2865,9 @@ export const en = {
"The Audit log's \"Record type\" dropdown does not filter the rows itself — it narrows the "
+ "\"Action\" dropdown next to it down to only the actions that happen on the type you pick "
+ "(Flock, Sales order, and so on), so you are not scanning one long list of every action the "
+ "farm can log. Pick the action from that narrowed list to actually filter the rows.",
+ "farm can log. Pick the action from that narrowed list to actually filter the rows. "
+ "The From and To dates beside them DO filter the rows, over the UTC day shown in the "
+ "\"When (UTC)\" column.",
auditRecordHistory:
"Flocks, Egg grades, Daily entry history, Sales and Expenses each carry a History column showing who "
+ "created the record and when, plus who last changed it if anyone has. It is read from the same audit "
Expand Down Expand Up @@ -2918,11 +2931,11 @@ export const en = {
+ "once that formatting lands.",
farmSettingsTimezone:
"The <strong>timezone</strong> is the farm's day. Every field that records <em>when something "
+ "happened</em> — daily entry, flocks, water, feed usage and purchases, expenses, orders and payments "
+ "— opens on it and refuses to go past it, whatever day the phone or laptop in your hand is on, so a "
+ "device travelling ahead of the farm can no longer offer a date the save then refuses. Dates that are "
+ "meant to be in the future are not capped: a feed batch's <strong>expiry</strong>, and the date "
+ "ranges you filter History and Water by.",
+ "happened</em> — daily entry, flocks, water, feed usage, stock purchases, expenses, orders and payments "
+ "— opens on it and refuses to go past it for what you record, whatever day the phone or laptop in "
+ "your hand is on, so a device travelling ahead of the farm can no longer offer a date the save then "
+ "refuses. Dates that are meant to be in the future are not capped: a "
+ "<strong>stock item's expiry date</strong>.",
farmSettingsCurrency:
"The <strong>currency</strong> locks the moment the farm records its first amount — a sale, a payment, "
+ "an expense, a priced product, or money spent on feed. The field shows as locked with the reason "
Expand Down Expand Up @@ -3123,7 +3136,7 @@ export const en = {
+ "work, when eggs leave a withdrawal period, which eggs a sale can take, the day a flock is depleted "
+ "or archived on, and the range reports open on. Every field that records WHEN SOMETHING HAPPENED "
+ "opens on it and will not go past it, whatever day the device in your hand is on. Dates meant to "
+ "fall in the future are not capped — a feed batch's expiry, and the History and Water filters.",
+ "fall in the future are not capped — a stock item's expiry date.",

glossaryInstallToHomeScreenTerm: "Install to home screen",
glossaryInstallToHomeScreenDef:
Expand Down
33 changes: 20 additions & 13 deletions web/src/i18n/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1324,12 +1324,13 @@ export const es = {
enterAmountGreaterThanZero: "Ingrese un monto mayor que cero.",

// Filters
monthLabel: "Mes",
fromLabel: "Desde",
toLabel: "Hasta",
categoryLabel: "Categoría",
allCategoriesOption: "Todas las categorías",
hideCategoriesButton: "ocultar categorías",
manageCategoriesButton: "gestionar categorías",
monthTotalLabel: "Total del mes: {{amount}}",
periodTotalLabel: "Total de este período: {{amount}}",

// Category-management panel
categoriesHeading: "Categorías de gastos",
Expand Down Expand Up @@ -1363,7 +1364,8 @@ export const es = {
saveCorrectionButton: "Guardar corrección",

// Expenses table
noExpensesMessage: "No hay gastos este mes.",
noExpensesMessage: "Aún no se han registrado gastos.",
noExpensesMatch: "Ningún gasto coincide con estos filtros.",
dateHeader: "Fecha",
categoryHeader: "Categoría",
descriptionHeader: "Descripción",
Expand Down Expand Up @@ -1562,8 +1564,12 @@ export const es = {
actionHeader: "Acción",
entityHeader: "Entidad",
reasonHeader: "Motivo",
fromLabel: "Desde",
toLabel: "Hasta",
emptyMessage: "Aún no hay eventos de auditoría.",
scopedEmptyMessage: "Aún no hay eventos de auditoría para este registro.",
filteredEmptyMessage: "Ningún evento de auditoría coincide con estos filtros.",
scopedFilteredEmptyMessage: "Ningún evento de auditoría de este registro coincide con estos filtros.",
loadMoreButton: "cargar más",
},

Expand Down Expand Up @@ -2299,7 +2305,8 @@ export const es = {
expensesHeading: "Gastos (admin)",
expensesRecording:
"Registre el dinero que sale: fecha, categoría, descripción y monto (en la moneda de la granja), "
+ "opcionalmente vinculado a un lote. El selector de mes muestra un total acumulado; las categorías "
+ "opcionalmente vinculado a un lote. El rango de fechas — las fechas propias de la granja, no un "
+ "reloj del navegador en otro lugar — muestra un total acumulado para el período elegido; las categorías "
+ "se administran en la misma pantalla (desactivar una la oculta de los gastos nuevos — los ya "
+ "registrados la conservan).",
expensesCorrections:
Expand Down Expand Up @@ -2335,7 +2342,9 @@ export const es = {
"El menú \"Tipo de registro\" del registro de auditoría no filtra las filas por sí solo — reduce el "
+ "menú \"Acción\" de al lado a solo las acciones que ocurren en el tipo elegido (Lote, Pedido de "
+ "venta, etc.), para que no tengas que revisar una lista larga con cada acción que la granja puede "
+ "registrar. Elige la acción de esa lista reducida para filtrar las filas realmente.",
+ "registrar. Elige la acción de esa lista reducida para filtrar las filas realmente. "
+ "Las fechas \"Desde\" y \"Hasta\" junto a ellos SÍ filtran las filas, sobre el día UTC que "
+ "muestra la columna \"Cuándo (UTC)\".",
auditRecordHistory:
"Lotes, Grados de huevo, Historial de registro diario, Ventas y Gastos tienen una columna Historial "
+ "que muestra quién creó el registro y cuándo, más quién lo modificó por última vez si alguien lo "
Expand Down Expand Up @@ -2400,12 +2409,11 @@ export const es = {
+ "medidas una vez que ese formato esté disponible.",
farmSettingsTimezone:
"La <strong>zona horaria</strong> es el día de la granja. Cada campo que registra <em>cuándo sucedió "
+ "algo</em> — entrada diaria, lotes, agua, uso y compras de alimento, gastos, pedidos y pagos — se "
+ "abre en ella y se niega a ir más allá, sea cual sea el día en el teléfono o la laptop que tiene en "
+ "la mano, para que un dispositivo adelantado a la granja ya no pueda ofrecer una fecha que el "
+ "guardado luego rechace. Las fechas que están destinadas a estar en el futuro no tienen tope: la "
+ "<strong>caducidad</strong> de un lote de alimento, y los rangos de fechas por los que filtra "
+ "Historial y Agua.",
+ "algo</em> — entrada diaria, lotes, agua, uso de alimento, compras de existencias, gastos, pedidos y pagos — se "
+ "abre en ella y se niega a ir más allá para lo que usted registra, sea cual sea el día en el "
+ "teléfono o la laptop que tiene en la mano, para que un dispositivo adelantado a la granja ya no "
+ "pueda ofrecer una fecha que el guardado luego rechace. Las fechas que están destinadas a estar en "
+ "el futuro no tienen tope — la <strong>fecha de vencimiento</strong> de un artículo en existencia.",
farmSettingsCurrency:
"La <strong>moneda</strong> se bloquea en el momento en que la granja registra su primer monto — una "
+ "venta, un pago, un gasto, un producto con precio, o dinero gastado en alimento. El campo se "
Expand Down Expand Up @@ -2607,8 +2615,7 @@ export const es = {
+ "puede tomar una venta, el día en que se agota o archiva un lote, y el rango en que se abren los "
+ "informes. Todo campo que registra CUÁNDO OCURRIÓ ALGO se abre en ese día y no permite ir más "
+ "allá, sin importar en qué día esté el dispositivo que tiene en la mano. Las fechas que deben caer "
+ "en el futuro no tienen ese límite — el vencimiento de un lote de alimento, y los filtros de "
+ "Historial y Agua.",
+ "en el futuro no tienen ese límite — la fecha de vencimiento de un artículo en existencia.",

glossaryInstallToHomeScreenTerm: "Instalar en la pantalla de inicio",
glossaryInstallToHomeScreenDef:
Expand Down
32 changes: 19 additions & 13 deletions web/src/i18n/tl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1373,12 +1373,13 @@ export const tl = {
enterAmountGreaterThanZero: "Maglagay ng halagang higit sa zero.",

// Filters
monthLabel: "Buwan",
fromLabel: "Mula",
toLabel: "Hanggang",
categoryLabel: "Kategorya",
allCategoriesOption: "Lahat ng kategorya",
hideCategoriesButton: "itago ang mga kategorya",
manageCategoriesButton: "pamahalaan ang mga kategorya",
monthTotalLabel: "Kabuuan ng buwan: {{amount}}",
periodTotalLabel: "Kabuuan sa panahong ito: {{amount}}",

// Category-management panel
categoriesHeading: "Mga kategorya ng gastos",
Expand Down Expand Up @@ -1412,7 +1413,8 @@ export const tl = {
saveCorrectionButton: "I-save ang pagtatama",

// Expenses table
noExpensesMessage: "Walang gastos ngayong buwan.",
noExpensesMessage: "Wala pang naitalang gastos.",
noExpensesMatch: "Walang gastos na tumutugma sa mga filter na ito.",
dateHeader: "Petsa",
categoryHeader: "Kategorya",
descriptionHeader: "Deskripsyon",
Expand Down Expand Up @@ -1620,8 +1622,12 @@ export const tl = {
actionHeader: "Aksyon",
entityHeader: "Entidad",
reasonHeader: "Dahilan",
fromLabel: "Mula",
toLabel: "Hanggang",
emptyMessage: "Wala pang audit event.",
scopedEmptyMessage: "Wala pang audit event para sa record na ito.",
filteredEmptyMessage: "Walang audit event na tumutugma sa mga filter na ito.",
scopedFilteredEmptyMessage: "Walang audit event para sa record na ito na tumutugma sa mga filter na ito.",
loadMoreButton: "mag-load pa",
},

Expand Down Expand Up @@ -2387,7 +2393,8 @@ export const tl = {
expensesHeading: "Mga Gastos (admin)",
expensesRecording:
"Itala ang lumalabas na pera: petsa, kategorya, deskripsyon, at halaga (sa currency ng bukid), "
+ "opsyonal na naka-link sa isang kawan. Ipinapakita ng month picker ang tumatakbong total; "
+ "opsyonal na naka-link sa isang kawan. Ipinapakita ng hanay ng petsa — ang sariling petsa ng bukid, "
+ "hindi ng orasan ng browser sa ibang lugar — ang tumatakbong total para sa panahong pinili mo; "
+ "pinapamahalaan ang mga kategorya sa parehong screen (ang pag-deactivate ng isa ay nagtatago nito "
+ "mula sa mga bagong gastos — pinapanatili ito ng mga naitala na).",
expensesCorrections:
Expand Down Expand Up @@ -2425,7 +2432,8 @@ export const tl = {
+ "lamang nito ang dropdown na \"Aksyon\" sa tabi nito para lang sa mga aksyong nangyayari sa uring "
+ "pinili mo (Kawan, Order ng benta, atbp.), para hindi mo na kailangang mag-scan sa isang mahabang "
+ "listahan ng bawat aksyong maaaring itala ng bukid. Piliin ang aksyon sa napaliit na listahang iyon "
+ "para talagang masalà ang mga row.",
+ "para talagang masalà ang mga row. Ang mga petsang \"Mula\" at \"Hanggang\" sa tabi nila ay "
+ "TALAGANG nagsasalà sa mga row, batay sa araw na UTC na ipinapakita sa column na \"Kailan (UTC)\".",
auditRecordHistory:
"Ang Mga kawan, Grado ng itlog, Kasaysayan ng pang-araw-araw na entry, Benta at Gastos ay may "
+ "kolum na Kasaysayan na nagpapakita kung sino ang gumawa ng record at kailan, at kung sino ang "
Expand Down Expand Up @@ -2493,12 +2501,11 @@ export const tl = {
+ "formatting na iyon.",
farmSettingsTimezone:
"Ang <strong>time zone</strong> ang araw ng bukid. Ang bawat field na nagtatala ng <em>kung kailan "
+ "nangyari ang isang bagay</em> — araw-araw na entry, kawan, tubig, paggamit at pagbili ng feed, "
+ "gastos, order at bayad — ay bumubukas dito at tumatanggi lumampas dito, anuman ang araw sa "
+ "telepono o laptop na hawak mo, para hindi na makapag-alok ang isang device na nauuna sa bukid ng "
+ "petsang tatanggihan din naman ng pag-save. Ang mga petsang nakatakdang mangyari sa hinaharap ay "
+ "walang cap: ang <strong>expiry</strong> ng isang lote ng feed, at ang mga date range na ginagamit "
+ "mo para i-filter ang History at Tubig.",
+ "nangyari ang isang bagay</em> — araw-araw na entry, kawan, tubig, paggamit ng feed, pagbili ng stock, "
+ "gastos, order at bayad — ay bumubukas dito at tumatanggi lumampas dito para sa itinatala mo, "
+ "anuman ang araw sa telepono o laptop na hawak mo, para hindi na makapag-alok ang isang device na "
+ "nauuna sa bukid ng petsang tatanggihan din naman ng pag-save. Ang mga petsang nakatakdang "
+ "mangyari sa hinaharap ay walang cap — ang <strong>petsa ng expiry</strong> ng isang item sa stock.",
farmSettingsCurrency:
"Naka-lock ang <strong>currency</strong> sa sandaling itala ng bukid ang una nitong halaga — isang "
+ "benta, isang bayad, isang gastos, isang produktong may presyo, o perang ginastos sa feed. "
Expand Down Expand Up @@ -2706,8 +2713,7 @@ export const tl = {
+ "itlog ang puwedeng kunin ng isang benta, ang araw na na-deplete o na-archive ang isang kawan, at "
+ "ang range na binubuksan ng mga report. Bawat field na nagtatala kung KAILAN NANGYARI ANG ISANG "
+ "BAGAY ay nagbubukas dito at hindi ito lalagpasan, kahit anong araw ang nasa device mo. Hindi "
+ "limitado ang mga petsang dapat mahulog sa hinaharap — ang expiry ng isang batch ng feed, at ang "
+ "mga filter ng History at Tubig.",
+ "limitado ang mga petsang dapat mahulog sa hinaharap — ang petsa ng expiry ng isang item sa stock.",

glossaryInstallToHomeScreenTerm: "Pag-install sa home screen",
glossaryInstallToHomeScreenDef:
Expand Down
63 changes: 62 additions & 1 deletion web/src/lib/dates.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, afterEach, vi } from "vitest";
import { todayIso, ageWeeks, daysBefore, isKnownTimeZone } from "./dates";
import { todayIso, ageWeeks, daysBefore, isKnownTimeZone, isIsoCalendarDate } from "./dates";

describe("todayIso", () => {
afterEach(() => vi.useRealTimers());
Expand Down Expand Up @@ -156,3 +156,64 @@ describe("ageWeeks", () => {
}
});
});

// #666 — the accepted set is stated ONCE, as the whole space, because the
// case-by-case method got this wrong three rounds running (impossible dates
// accepted, then years 1-99 rejected, then year 0000 accepted). The set is
// exactly what BOTH ends already require: HTML's valid-date-string production
// for <input type="date"> needs year >= 1, and the endpoint binds DateOnly,
// whose MinValue is 0001-01-01. Anything outside that is a value the control
// cannot display and the server cannot bind — a filter the user can see the
// effect of but not clear.
const ACCEPTED = [
"0001-01-01", // DateOnly.MinValue, and HTML's lowest valid year
"0050-01-01", // two-digit year: Date.UTC would remap this to 1950
"0099-12-31", // top of the remap range
"0100-01-01", // first year the remap never touched
"1970-01-01", // epoch, in case the probe is ever built from it
"2024-02-29", // real leap day
"2026-08-01", // ordinary date
"9999-12-31", // highest the four-digit regex admits
];

const REJECTED = [
"0000-01-01", // HTML requires year >= 1; DateOnly cannot bind it -> 400
"0000-02-29", // year 0 IS a proleptic leap year, so the round-trip alone accepts it
"2026-02-29", // not a leap year
"2026-02-31", // never a real day
"2026-00-01", // month 0
"2026-13-01", // month 13
"2026-01-00", // day 0
"2026-01-32", // day 32
"", // empty
"2026-8-1", // unpadded
"2026-08-1", // half-padded
"20260801", // no separators
"2026-08-01T00:00:00Z", // datetime, not a date
"abcd-ef-gh", // non-numeric
"10000-01-01", // five-digit year
// Whitespace is URL-reachable: searchParams.get() decodes "+" and "%20" to a
// space, so ?from=+2026-08-01 arrives as " 2026-08-01" and ?from=2026+-08-01
// as "2026 -08-01". The leading- and trailing-space rows exist to fail the
// tempting future edit — trim the input, or allow \s* at the anchors — which
// every other row in this table survives. The interior-space row is not a
// control for that edit (either form still rejects it, since \s* at the
// anchors does not admit an interior space); it is here because this exact
// input path can produce it. What the server would do with a padded value is
// not asserted here: this function's contract is that the client sends only
// what the CONTROL can display, and <input type="date"> cannot display any
// of these three.
" 2026-08-01", // leading space, the "+" decoding
"2026-08-01 ", // trailing space
"2026 -08-01", // interior space
];

describe("isIsoCalendarDate", () => {
it.each(ACCEPTED)("accepts %s", (v) => {
expect(isIsoCalendarDate(v)).toBe(true);
});

it.each(REJECTED)("rejects %s", (v) => {
expect(isIsoCalendarDate(v)).toBe(false);
});
});
Loading
Loading