(null)
const [periodHydrated, setPeriodHydrated] = useState(false)
- const [periods, setPeriods] = useState([])
const [filterOpen, setFilterOpen] = useState(false)
const [dateFrom, setDateFrom] = useState('')
const [dateTo, setDateTo] = useState('')
@@ -164,13 +169,31 @@ export default function JournalEntryList() {
}
const fetchAttachmentCounts = useCallback(async (entryIds: string[]) => {
- if (entryIds.length === 0) return
+ if (entryIds.length === 0) {
+ setAttachmentCounts({})
+ return
+ }
+ // The counts route caps each request at 50 IDs, so a large page ("Alla", or
+ // 100/page) must be split into chunks and merged. Without this the whole
+ // request 400s and every document-requiring row falsely shows the
+ // missing-underlag warning until it's expanded.
+ const COUNTS_BATCH_SIZE = 50
+ const batches: string[][] = []
+ for (let i = 0; i < entryIds.length; i += COUNTS_BATCH_SIZE) {
+ batches.push(entryIds.slice(i, i + COUNTS_BATCH_SIZE))
+ }
try {
- const res = await fetch(
- `/api/documents/counts?journal_entry_ids=${entryIds.join(',')}`
+ const results = await Promise.all(
+ batches.map(async (batch) => {
+ const res = await fetch(
+ `/api/documents/counts?journal_entry_ids=${batch.join(',')}`
+ )
+ if (!res.ok) return {} as Record
+ const { data } = await res.json()
+ return (data || {}) as Record
+ })
)
- const { data } = await res.json()
- setAttachmentCounts(data || {})
+ setAttachmentCounts(Object.assign({}, ...results))
} catch {
// Non-critical — silently ignore
}
@@ -230,7 +253,6 @@ export default function JournalEntryList() {
// gates that first fetch so the list loads already scoped to the resolved year.
useEffect(() => {
if (!company?.id) {
- setPeriods([])
setPeriodId(null)
setPeriodHydrated(true)
return
@@ -248,7 +270,6 @@ export default function JournalEntryList() {
// Non-critical — fall through with an empty list (scope stays "all years").
}
if (cancelled) return
- setPeriods(fetched)
const stored =
typeof window !== 'undefined'
@@ -544,28 +565,25 @@ export default function JournalEntryList() {
// Count of active dialog filters, shown as a badge on the Filtrera button so
// the user can tell the list is scoped without opening the dialog. Sort order
// is a view preference (always set), not a filter, so it is excluded.
- const activeFilterCount =
- (periodId ? 1 : 0) +
+ // Filters that live inside the Filtrera dialog. The fiscal-year scope is its
+ // own control now, so it no longer counts toward the dialog badge (it would
+ // otherwise always read "1" for the default year).
+ const dialogFilterCount =
(seriesFilter !== 'all' ? 1 : 0) +
(dateFrom || dateTo ? 1 : 0) +
(showMissingOnly ? 1 : 0)
+ // Year scope included — drives empty-state messaging + keeping the bar mounted.
+ const activeFilterCount = (periodId ? 1 : 0) + dialogFilterCount
// When any filter or search is active we keep the filter bar mounted even
// with zero results, so the user can edit or clear their query. The pristine
// "no entries yet" state below only applies to an untouched, empty ledger.
const hasActiveFilters = Boolean(search) || activeFilterCount > 0
- // Resolve the active fiscal-year scope for the bar chip. "All years" (periodId
- // null) renders immediately; a specific period waits until its name resolves
- // from the fetched list (scopeLabel stays null meanwhile, so the chip never
- // flashes the wrong scope). Surfacing this keeps the period visible per BFL
- // without the user having to open the filter dialog.
- const activePeriod = periodId ? periods.find((p) => p.id === periodId) ?? null : null
- const scopeLabel = periodId ? activePeriod?.name ?? null : t('scope_all_years')
-
- // Apply a fiscal-year selection from the dialog. The FiscalYearSelector
- // persists the choice to localStorage itself; here we only mirror it into
- // local state and reset pagination.
+ // Apply a fiscal-year selection. The FiscalYearSelector (now an inline control
+ // in the toolbar, not buried in the filter dialog) persists the choice to
+ // localStorage itself; here we only mirror it into local state and reset
+ // pagination.
const handlePeriodChange = (next: string | null) => {
setPeriodId(next)
setPage(0)
@@ -620,24 +638,44 @@ export default function JournalEntryList() {
// Verifikat/Utkast toggle stays reachable.
if (!loading && entries.length === 0 && !hasActiveFilters && listMode === 'committed' && draftCount === 0) {
return (
-
-
-
+ {/* Control bar: view toggle + search + filters + active fiscal-year scope
+ on one aligned row (wraps on narrow screens) rather than four stacked rows. */}
+
+ {/* Verifikat vs Utkast. Drafts live in their own view with a count badge so
+ they don't sink to the last page of the committed list. */}
+
@@ -840,152 +867,121 @@ export default function JournalEntryList() {
-
-
- {/* Verifikat vs Utkast. Drafts live in their own view with a count badge so
- they don't sink to the last page of the committed list. */}
-
-
-
-
-
-
-
- {/* Active fiscal-year scope — visible without opening the filter dialog so
- the user always sees which räkenskapsår the ledger is scoped to (BFL
- period-correctness). Clicking it opens the dialog to change the scope. */}
- {listMode === 'committed' && periodHydrated && scopeLabel && (
-
- ) : (
- // Filter-scoped: mark every missing-doc verifikat matching the active
- // filters across all pages — scales to a post-import flood.
-
- )}
-
- )}
+ )}
+
{loading ? (
-
-
-
-
{t('loading')}
-
-
+
) : filteredEntries.length === 0 ? (
// Empty placeholder, scoped to the situation: an empty drafts view, a
// filtered committed view with no matches, or a committed view with no
// posted entries yet (but drafts exist — hence we got here, not the
// pristine early return above).
-
-
-