feat(transactions): filter /transactions by rakenskapsar and kvartal (#1545)

* feat(transactions): filter /transactions by rakenskapsar and kvartal

User request: booking a specific period (including brutet rakenskapsar,
e.g. July-June) meant scrolling past every other year's transactions.

- New FyPicker chip in the toolbar scopes both the inbox and history views
  to a fiscal year; quarter chips (Q1-Q4, fiscal-year aligned) appear once
  a year is selected. Clicking the active quarter widens back to the year.
- Bounds are pushed into the Supabase queries (window, pending backlog,
  badge count, load-more) so pagination and the Att bokfora count stay
  consistent with the visible list; skattekonto rows are bounded client-side.
- Scope persists under a page-local localStorage key, deliberately separate
  from the shared report scope so a year picked on a report page never
  silently hides pending inbox rows.
- lib/transactions/period-filter.ts derives quarter bounds from fiscal
  period dates (handles brutet, shortened and extended years); unit tested.
- FyPicker gains an optional storageKeyPrefix prop; default unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(transactions): never hide pending rows behind the period filter

Swedish accounting review on PR #1545: scoping the pending-backlog fetch
and badge count to the period made unbooked rows outside the selected
year vanish from the inbox worklist (BFL 5 kap: pending affarshandelser
must stay visible until booked).

- Pending-backlog fetch and the DB pending count are unscoped again;
  only the history window pages server-side within the period.
- The inbox applies the period client-side over the complete backlog;
  the tab badge counts pending rows inside the scope.
- When pending rows (bank or skattekonto) fall outside the scope, the
  footer says how many and offers Visa alla, which clears the filter
  and its persisted value.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(transactions): guard list fetches against stale cross-scope responses

CodeRabbit on PR #1545:
- fetchTransactions/loadMoreTransactions now carry a fetch generation;
  a response applies only if no newer fetch (scope change, realtime
  refresh, load-more) started meanwhile, so a slow pre-filter request
  can no longer overwrite the active period scope's window, paging
  offsets, or loading skeleton.
- FyPicker restore effect includes storageKeyPrefix in its deps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(transactions): label quarter chips as fiscal-year quarters

Swedish accounting review note on PR #1545: Q1-Q4 follow the company's
rakenskapsar, which on a brutet rakenskapsar differs from the calendar
quarters that momsdeklaration periods use. Say so in the group's
aria-label and hover title so the chips are not mistaken for VAT
periods.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-08-13 01:21:31 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent a07dcf4a55
commit b9bf60234d
6 changed files with 434 additions and 32 deletions
+11 -3
View File
@@ -35,6 +35,13 @@ interface FyPickerProps {
/** Server-loaded periods for the first render, scoped to initialCompanyId. */
initialPeriods?: FiscalPeriod[]
initialCompanyId?: string | null
/**
* localStorage prefix for the persisted selection (companyId is appended).
* Defaults to the report-wide shared scope; pass a page-specific prefix
* when the page's scope must not follow (or steer) the shared one, e.g.
* the transactions inbox, where a narrowed scope hides pending rows.
*/
storageKeyPrefix?: string
className?: string
}
@@ -61,6 +68,7 @@ export function FyPicker({
onReady,
initialPeriods,
initialCompanyId,
storageKeyPrefix = STORAGE_KEY_PREFIX,
className,
}: FyPickerProps) {
const { company } = useCompany()
@@ -108,7 +116,7 @@ export function FyPicker({
const pick = fetched.find((p) => p.period_end < today) ?? fetched[0]
if (pick) onChange(pick.id, pick)
} else {
const stored = window.localStorage.getItem(STORAGE_KEY_PREFIX + company.id)
const stored = window.localStorage.getItem(storageKeyPrefix + company.id)
if (stored === ALL_YEARS_VALUE) {
if (includeAllOption) onChange(null, null)
else if (fetched.length > 0) onChange(fetched[0].id, fetched[0])
@@ -128,12 +136,12 @@ export function FyPicker({
// onReady is a lifecycle callback: fire once per load, not on parent
// re-renders that re-create it.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [company?.id, hideFuturePeriods, includeAllOption, preferLatestEnded, initialCompanyId, initialPeriods])
}, [company?.id, hideFuturePeriods, includeAllOption, preferLatestEnded, initialCompanyId, initialPeriods, storageKeyPrefix])
const handleChange = (id: string) => {
const nextId = id === ALL_YEARS_VALUE ? null : id
if (company?.id && typeof window !== 'undefined') {
window.localStorage.setItem(STORAGE_KEY_PREFIX + company.id, nextId ?? ALL_YEARS_VALUE)
window.localStorage.setItem(storageKeyPrefix + company.id, nextId ?? ALL_YEARS_VALUE)
}
onChange(nextId, nextId ? periods.find((p) => p.id === nextId) ?? null : null)
}