From 8c5b8eaa42be9ba300e7fc36f7363c0421feb3b1 Mon Sep 17 00:00:00 2001 From: Mattsson <111893710+mattssonn@users.noreply.github.com> Date: Mon, 30 Mar 2026 00:31:33 +0200 Subject: [PATCH] fix: improve date filter UX with partial dates and Apply button (GNU-18) (#149) Co-authored-by: Claude Opus 4.6 (1M context) --- components/bookkeeping/JournalEntryList.tsx | 71 ++++++++++++++++++--- 1 file changed, 62 insertions(+), 9 deletions(-) diff --git a/components/bookkeeping/JournalEntryList.tsx b/components/bookkeeping/JournalEntryList.tsx index 57f00bf4..cfd6ba86 100644 --- a/components/bookkeeping/JournalEntryList.tsx +++ b/components/bookkeeping/JournalEntryList.tsx @@ -44,7 +44,46 @@ export default function JournalEntryList({ periodId }: Props) { const [dateToInput, setDateToInput] = useState('') const pageSize = 20 - const isValidDate = (v: string) => /^\d{4}-\d{2}-\d{2}$/.test(v) && !isNaN(Date.parse(v)) + const normalizeDate = (v: string): string | null => { + const trimmed = v.trim() + if (!trimmed) return null + // YYYY + if (/^\d{4}$/.test(trimmed)) { + const y = parseInt(trimmed, 10) + if (y < 1900 || y > 2100) return null + return `${trimmed}-01-01` + } + // YYYY-MM + if (/^\d{4}-\d{2}$/.test(trimmed)) { + const [y, m] = trimmed.split('-').map(Number) + if (y < 1900 || y > 2100 || m < 1 || m > 12) return null + return `${trimmed}-01` + } + // YYYY-MM-DD + if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) { + const d = new Date(trimmed + 'T00:00:00') + if (isNaN(d.getTime())) return null + // Verify the date didn't roll over (e.g. 2024-02-31 → March) + const [y, m, day] = trimmed.split('-').map(Number) + if (d.getFullYear() !== y || d.getMonth() + 1 !== m || d.getDate() !== day) return null + return trimmed + } + return null + } + + const applyDateFilter = () => { + const fromVal = dateFromInput.trim() + const toVal = dateToInput.trim() + const nextFrom = fromVal === '' ? '' : normalizeDate(fromVal) ?? dateFrom + const nextTo = toVal === '' ? '' : normalizeDate(toVal) ?? dateTo + setDateFromInput(nextFrom) + setDateToInput(nextTo) + if (nextFrom !== dateFrom || nextTo !== dateTo) { + setDateFrom(nextFrom) + setDateTo(nextTo) + setPage(0) + } + } const fetchAttachmentCounts = useCallback(async (entryIds: string[]) => { if (entryIds.length === 0) return @@ -174,12 +213,15 @@ export default function JournalEntryList({ periodId }: Props) { onChange={(e) => setDateFromInput(e.target.value)} onBlur={() => { const v = dateFromInput.trim() - const next = v === '' ? '' : isValidDate(v) ? v : dateFrom - setDateFromInput(next) - if (next !== dateFrom) { setDateFrom(next); setPage(0) } + if (v === '') return + const normalized = normalizeDate(v) + if (normalized) setDateFromInput(normalized) }} onKeyDown={(e) => { - if (e.key === 'Enter') (e.target as HTMLInputElement).blur() + if (e.key === 'Enter') { + e.preventDefault() + applyDateFilter() + } }} className="h-8 w-[145px] text-xs" /> @@ -190,15 +232,26 @@ export default function JournalEntryList({ periodId }: Props) { onChange={(e) => setDateToInput(e.target.value)} onBlur={() => { const v = dateToInput.trim() - const next = v === '' ? '' : isValidDate(v) ? v : dateTo - setDateToInput(next) - if (next !== dateTo) { setDateTo(next); setPage(0) } + if (v === '') return + const normalized = normalizeDate(v) + if (normalized) setDateToInput(normalized) }} onKeyDown={(e) => { - if (e.key === 'Enter') (e.target as HTMLInputElement).blur() + if (e.key === 'Enter') { + e.preventDefault() + applyDateFilter() + } }} className="h-8 w-[145px] text-xs" /> + {(dateFrom || dateTo) && (