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>
112 lines
4.2 KiB
TypeScript
112 lines
4.2 KiB
TypeScript
import { Suspense } from 'react'
|
|
import { redirect } from 'next/navigation'
|
|
import DashboardContent from '@/components/dashboard/DashboardContent'
|
|
import { ChecklistSkeleton, PanesSkeleton } from '@/components/dashboard/HemSkeletons'
|
|
import {
|
|
getDashboardAuthContext,
|
|
getDashboardCompanyId,
|
|
getDashboardSettings,
|
|
getResolvedDashboardAgentProfile,
|
|
} from './request-context'
|
|
import { HemChecklistSection, HemNoticesSection, HemPanesSection } from './hem-sections'
|
|
|
|
export const dynamic = 'force-dynamic'
|
|
|
|
// Home route = Hem (concept scene 14): greeting + Att göra + Fortsätt.
|
|
// The KPI/revenue/deadline widgets left the page (founder direction,
|
|
// dev_docs/last_session_resume.md §8), which also pruned their fetches:
|
|
// the journal-line YTD aggregation, unpaid-invoice totals and deadline
|
|
// queries are gone and the page got faster.
|
|
//
|
|
// Streaming: the page itself 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),
|
|
// so ~30 queries fill three blocks in as they land instead of holding the
|
|
// whole page behind the slowest one. RSC streaming applies to client
|
|
// navigations too, not only hard loads.
|
|
|
|
export default async function DashboardPage() {
|
|
const [{ supabase, user }, companyId] = await Promise.all([
|
|
getDashboardAuthContext(),
|
|
getDashboardCompanyId(),
|
|
])
|
|
|
|
if (!user) {
|
|
redirect('/login')
|
|
}
|
|
|
|
if (!companyId) {
|
|
redirect('/onboarding')
|
|
}
|
|
|
|
const now = new Date()
|
|
|
|
const [settingsRes, { data: profile }, agentProfile, { count: skatteverketTokenCount }] =
|
|
await Promise.all([
|
|
getDashboardSettings(),
|
|
// First name for the greeting.
|
|
supabase.from('profiles').select('full_name').eq('id', user.id).maybeSingle(),
|
|
getResolvedDashboardAgentProfile(),
|
|
// The Skatteverket promo below the panes needs this flag in the shell;
|
|
// the checklist section reads it again for its own step (cheap head count).
|
|
supabase.from('skatteverket_tokens').select('*', { count: 'exact', head: true }).eq('user_id', user.id).eq('company_id', companyId),
|
|
])
|
|
|
|
// A FAILED settings read must not masquerade as "onboarding not done":
|
|
// that sent fully onboarded users back to the wizard on a transient query
|
|
// failure (issue #1053). Throw to the error boundary (retryable) and only
|
|
// redirect on a genuinely incomplete or missing settings row.
|
|
const { data: settings, error: settingsError } = settingsRes
|
|
if (settingsError) {
|
|
throw new Error(`company_settings fetch failed: ${settingsError.message}`)
|
|
}
|
|
|
|
// If onboarding is not complete, redirect to onboarding
|
|
if (!settings?.onboarding_complete) {
|
|
redirect('/onboarding')
|
|
}
|
|
|
|
const agentBuilt = Boolean(agentProfile?.verified_at)
|
|
const userFirstName = profile?.full_name?.trim().split(/\s+/)[0] ?? null
|
|
const initialSetup = {
|
|
path: settings.initial_setup_path ?? null,
|
|
completedAt: settings.initial_setup_completed_at ?? null,
|
|
dismissedAt: settings.initial_setup_dismissed_at ?? null,
|
|
}
|
|
const setupOpen = !settings.initial_setup_completed_at && !settings.initial_setup_dismissed_at
|
|
|
|
return (
|
|
<DashboardContent
|
|
companyId={companyId}
|
|
agentBuilt={agentBuilt}
|
|
userFirstName={userFirstName}
|
|
initialSetup={initialSetup}
|
|
hasSkatteverketConnected={(skatteverketTokenCount || 0) > 0}
|
|
notices={
|
|
<Suspense fallback={null}>
|
|
<HemNoticesSection companyId={companyId} userId={user.id} now={now} />
|
|
</Suspense>
|
|
}
|
|
checklist={
|
|
<Suspense fallback={<ChecklistSkeleton />}>
|
|
<HemChecklistSection
|
|
companyId={companyId}
|
|
userId={user.id}
|
|
now={now}
|
|
initialSetup={initialSetup}
|
|
agentBuilt={agentBuilt}
|
|
vatRegistered={settings.vat_registered}
|
|
momsPeriod={settings.moms_period ?? null}
|
|
/>
|
|
</Suspense>
|
|
}
|
|
panes={
|
|
<Suspense fallback={<PanesSkeleton />}>
|
|
<HemPanesSection companyId={companyId} now={now} setupOpen={setupOpen} />
|
|
</Suspense>
|
|
}
|
|
/>
|
|
)
|
|
}
|