Files
accounted/lib/worklist/aggregate.ts
T
Jakob Wennberg a8980a3a41 perf(hem): stream the home page in three sections and trim its query plan (#1945)
Hem was one ~33-query render behind a single fallback: the greeting waited
for the slowest worklist scan, and the request also paid a sequential
bank_file_imports read after the batch, a second scan of the suggested
matches (getWorklistCounts counted the same 200 rows the pane listed) and
an awaited stale-dismissal delete on the read path.

- page.tsx awaits only what the greeting shell and the redirects need
  (settings, profile, agent profile, the Skatteverket flag); the notice
  line, the setup checklist and the Att göra + Fortsätt panes are async
  server components behind their own Suspense (hem-sections.tsx). RSC
  streaming applies to client navigations too, so the greeting paints
  first on every visit and each block fills in as its queries land.
- DashboardContent becomes the shell with three slots; HemNotices keeps
  the one client-side action (the wrong-account sign-out); HemSkeletons
  are the two fallbacks.
- getWorklistCounts accepts the suggested matches the caller is already
  fetching (a promise, so it stays parallel); listSuggestedMatches runs
  once at the scan cap and the pane shows the first five.
- countInboxDocuments runs its id chunks in one wave instead of N
  sequential round trips.
- getCompanyNotices takes deferReap; Hem passes Next's after() so the
  stale-dismissal delete runs after the response.
- bank_file_imports joins the checklist section's batch.

Tests: worklist aggregate (precomputed matches skip the rescan), notices
aggregate (deferReap receives the reap; the delete does not run inline
and runs when the task does).

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 14:02:09 +02:00

88 lines
2.9 KiB
TypeScript

import type { SupabaseClient } from '@supabase/supabase-js'
import type { SuggestedMatch } from './types'
import type { WorklistCounts } from './types'
import {
countDeadlinesNeedingAction,
countInboxDocuments,
countOverdueInvoices,
countPendingOperations,
countReconciliationDue,
countSuggestedMatches,
countSupplierInvoicesAwaitingApproval,
countUnbookedTransactions,
countVerifikatMissingDocument,
} from './categories'
/**
* All worklist counts in one round-trip burst. Each count is a bounded query
* (mostly head-only; suggested_match revalidates its candidates, see
* categories.ts) and individually soft-fails to 0, so this is safe to call
* from layouts and server components on every render.
*
* `total` is the number of distinct actionable items: suggested_match is a
* fast path over transactions already counted in book_transaction, so it is
* excluded to avoid double-counting (see lib/worklist/types.ts).
*/
export interface GetWorklistCountsOptions {
/**
* Suggested matches the caller is already fetching (Hem renders them in
* the Att göra pane): the count is taken from this list instead of a
* second scan of the same rows. A promise is accepted so it can run in
* parallel with the other counts.
*/
suggestedMatches?: SuggestedMatch[] | Promise<SuggestedMatch[]>
}
export async function getWorklistCounts(
supabase: SupabaseClient,
companyId: string,
options: GetWorklistCountsOptions = {},
): Promise<WorklistCounts> {
const [
bookTransaction,
inboxDocument,
suggestedMatch,
supplierInvoiceApproval,
verifikatMissingDocument,
overdueInvoice,
deadlineAction,
pendingOperations,
reconciliationDue,
] = await Promise.all([
countUnbookedTransactions(supabase, companyId),
countInboxDocuments(supabase, companyId),
options.suggestedMatches
? Promise.resolve(options.suggestedMatches).then((m) => m.length)
: countSuggestedMatches(supabase, companyId),
countSupplierInvoicesAwaitingApproval(supabase, companyId),
countVerifikatMissingDocument(supabase, companyId),
countOverdueInvoices(supabase, companyId),
countDeadlinesNeedingAction(supabase, companyId),
countPendingOperations(supabase, companyId),
countReconciliationDue(supabase, companyId),
])
return {
counts: {
book_transaction: bookTransaction,
inbox_document: inboxDocument,
suggested_match: suggestedMatch,
supplier_invoice_approval: supplierInvoiceApproval,
verifikat_missing_document: verifikatMissingDocument,
overdue_invoice: overdueInvoice,
deadline_action: deadlineAction,
pending_operations: pendingOperations,
reconciliation_due: reconciliationDue,
},
total:
bookTransaction +
inboxDocument +
supplierInvoiceApproval +
verifikatMissingDocument +
overdueInvoice +
deadlineAction +
pendingOperations +
reconciliationDue,
}
}