a8980a3a41
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>
113 lines
4.3 KiB
TypeScript
113 lines
4.3 KiB
TypeScript
'use client'
|
|
|
|
import { useState, type ReactNode } from 'react'
|
|
import { useTranslations } from 'next-intl'
|
|
import { useCompany } from '@/contexts/CompanyContext'
|
|
import { SkatteverketPromoCard } from '@/components/dashboard/SkatteverketPromoCard'
|
|
import { AgentPromo } from '@/components/dashboard/AgentPromo'
|
|
import type { InitialSetupState } from '@/types'
|
|
|
|
interface DashboardContentProps {
|
|
companyId: string
|
|
/** Signed-in user's first name for the greeting; null falls back to a
|
|
* nameless greeting. */
|
|
userFirstName?: string | null
|
|
initialSetup: InitialSetupState
|
|
/**
|
|
* False until the company has a verified agent_profile. When false the hero
|
|
* slot shows a build-assistant prompt instead of the next-best-action card,
|
|
* so existing/migrated users are nudged to build the assistant without a
|
|
* full-screen onboarding takeover.
|
|
*/
|
|
agentBuilt?: boolean
|
|
hasSkatteverketConnected?: boolean
|
|
/**
|
|
* Streamed sections (server components behind Suspense in
|
|
* app/(dashboard)/page.tsx): the notice line, the setup checklist and the
|
|
* Att göra + Fortsätt panes. The shell renders the greeting immediately
|
|
* and each slot fills in as its queries resolve, instead of the whole page
|
|
* waiting for the slowest of ~30 queries.
|
|
*/
|
|
notices: ReactNode
|
|
checklist: ReactNode
|
|
panes: ReactNode
|
|
}
|
|
|
|
/**
|
|
* Hem (concept scene 14): greeting, then the two panes side by side:
|
|
* Att göra (obligations, lib/worklist) and Fortsätt (in-progress work,
|
|
* lib/worklist/resume). KPI tiles, revenue/expense cards and the deadline/tax
|
|
* widgets left the page (founder direction, dev_docs/last_session_resume.md
|
|
* §8): the numbers live at /kpi and /reports, deadlines render as Bevaka rows.
|
|
*/
|
|
export default function DashboardContent({
|
|
companyId,
|
|
userFirstName,
|
|
initialSetup,
|
|
agentBuilt = true,
|
|
hasSkatteverketConnected = false,
|
|
notices,
|
|
checklist,
|
|
panes,
|
|
}: DashboardContentProps) {
|
|
const t = useTranslations('dashboard')
|
|
const { company } = useCompany()
|
|
|
|
// Time-of-day greeting (concept: "God morgon, Jakob."). Client-side clock
|
|
// on purpose (the user's local morning, not the server's), captured once
|
|
// so render stays pure.
|
|
const [greetingNow] = useState(() => new Date())
|
|
const hour = greetingNow.getHours()
|
|
const greeting =
|
|
hour < 10 ? t('greeting_morning') : hour < 17 ? t('greeting_day') : t('greeting_evening')
|
|
const dateLine = new Intl.DateTimeFormat('sv-SE', {
|
|
weekday: 'long',
|
|
day: 'numeric',
|
|
month: 'long',
|
|
}).format(greetingNow)
|
|
|
|
return (
|
|
<div className="stagger-enter space-y-8">
|
|
{/* Degraded-state notices (lib/notices): one attn line, highest
|
|
priority first, quiet "+N till" expander. The wrong-account hint
|
|
participates in the same priority list instead of rendering its own
|
|
unconditional line, and the old boxed BackupHealthBanner card lives
|
|
on as the backup_failing category. */}
|
|
{notices}
|
|
|
|
{/* Greeting hero (concept scene 14) */}
|
|
<section>
|
|
<h1 className="font-display text-2xl leading-8 tracking-tight">
|
|
{userFirstName ? `${greeting}, ${userFirstName}.` : `${greeting}.`}
|
|
</h1>
|
|
<p className="mt-1.5 text-[13px] text-muted-foreground">
|
|
{dateLine}
|
|
{company?.name ? ` · ${company.name}` : ''}
|
|
</p>
|
|
</section>
|
|
|
|
{checklist}
|
|
|
|
{/* Build-assistant nudge: shown only until the company has a verified
|
|
agent_profile, so existing/migrated users get a clear prompt instead
|
|
of a full-screen onboarding takeover. While the stepped first-run
|
|
checklist is visible it already carries the assistant as its last
|
|
step, so the promo waits until that block is dismissed or completed. */}
|
|
{!agentBuilt && (initialSetup.dismissedAt || initialSetup.completedAt) && (
|
|
<AgentPromo companyId={companyId} />
|
|
)}
|
|
|
|
{/* The two panes (concept hem-grid). When nothing is in progress the
|
|
right pane renders null and Att göra takes the full width. */}
|
|
{panes}
|
|
|
|
{/* Connect-Skatteverket nudge for existing companies. Gated on
|
|
agentBuilt so it never stacks under the build-assistant hero:
|
|
one CTA surface at a time. */}
|
|
{agentBuilt && (
|
|
<SkatteverketPromoCard companyId={companyId} connected={hasSkatteverketConnected} />
|
|
)}
|
|
</div>
|
|
)
|
|
}
|