* feat(home): concept scene 14: greeting + Att gora/Fortsatt panes Hem becomes the founder-approved two-panel layout: serif time-of-day greeting with date and company, the Att gora worklist restyled to the concept pane (eyebrow header, h-rows with count chips, hover chevrons) and a new Fortsatt pane listing in-progress work derived purely from draft state (lib/worklist/resume: journal drafts, invoice drafts/unsent, mid-lifecycle salary runs; deadline boost, cap 3, tested). A completed flow can never render as a resume row by construction: only draft-state rows are fetched. KPI tiles, revenue/expense cards and the deadline/tax widgets leave the page per dev_docs/last_session_resume.md section 8, which also prunes their fetches (journal-line YTD aggregation, unpaid totals, deadlines): the page got faster. Banners, checklist, build-assistant hero and the Skatteverket nudge survive. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(home): serif pane titles for Att gora and Fortsatt Founder feedback: the uppercase eyebrow headers read as a stray font. Both pane titles are now the Hedvig display serif (text-lg) over the hairline, matching the page's heading language; the band headers inside Att gora keep their small uppercase form as grouping devices. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(home): Geist pane titles for Att gora and Fortsatt Founder call: the pane titles use the body sans (14px medium), not the display serif. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): Geist section headers + drop stale-transactions chip Founder feedback: pane/section headers (Att gora, Fortsatt, the reports groups Lopande/Bokslut/Skatt & moms etc) render in Geist sentence case instead of uppercase eyebrows or serif. The global h1-h3 display-font rule moves into @layer base so utility classes like font-sans can actually override it (unlayered element rules beat Tailwind's layered utilities: this was silently eating the override). Also removes the 'N aldre an 14 dagar' chip from the Bokfora transaktioner row and its stale-count plumbing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(ui): continuous nav crossfade + floating slide-over entrance The rail/full nav states now stay mounted and crossfade past each other (the inactive layer absolute, faded, nudged sideways, inert) while the aside width animates: the switch reads as one continuous motion instead of a DOM swap. The detail slide-over floats in from the right edge (slide-in-from-right-full, 300ms decelerating curve) per the concept, with a quicker ease-in exit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): make transitions and enter/exit animations actually run Two silent app-wide animation killers found while chasing 'the nav still is not smooth': 1. The codebase uses the shadcn animate-in/out vocabulary everywhere but no animate plugin was ever installed: Tailwind v4 silently dropped every such class, so popovers, dialogs, menus and the slide-over all appeared instantly. globals.css now defines the exact subset in use (accEnter/accExit keyframes + var-driven utilities), plugin-free, composing with duration/ease via --tw-duration/--tw-ease and collapsing under prefers-reduced-motion. Dialog drops its bracket-variant slide classes (zoom+fade carries the entrance). 2. The scrollbar auto-hide block's universal '* { transition: scrollbar-color ... }' was unlayered, and unlayered rules beat Tailwind's layered transition-* utilities regardless of specificity: every width/margin/color transition in the app was dead. The rule now lives in @layer base. Verified: the aside animates 248->64 over 300ms and the slide-over runs accEnter at 0.3s with the decelerating curve. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): finish the rr-mask session-replay masking sweep Main's #1105 switched one amount cell from the no-op sensitive-field class to rr-mask (rrweb's built-in text-masking class). The reskinned tables introduced more sensitive-field cells; all 12 occurrences now use rr-mask so financial amounts are masked in session replays. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
134 lines
5.0 KiB
TypeScript
134 lines
5.0 KiB
TypeScript
import { redirect } from 'next/navigation'
|
|
import DashboardContent from '@/components/dashboard/DashboardContent'
|
|
import { getWorklistCounts, listSuggestedMatches } from '@/lib/worklist'
|
|
import { listResumeItems } from '@/lib/worklist/resume'
|
|
import type { OnboardingProgress } from '@/types'
|
|
import {
|
|
getDashboardAuthContext,
|
|
getDashboardCompanyId,
|
|
getDashboardSettings,
|
|
getResolvedDashboardAgentProfile,
|
|
} from './request-context'
|
|
|
|
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.
|
|
|
|
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()
|
|
|
|
// Fetch all data in parallel
|
|
const [
|
|
settingsRes,
|
|
{ count: customerCount },
|
|
{ count: invoiceCount },
|
|
{ count: transactionCount },
|
|
{ data: bankConnections },
|
|
{ count: sieImportCount },
|
|
{ count: skatteverketTokenCount },
|
|
{ data: profile },
|
|
agentProfile,
|
|
worklist,
|
|
suggestedMatches,
|
|
resumeItems,
|
|
] = await Promise.all([
|
|
getDashboardSettings(),
|
|
supabase.from('customers').select('*', { count: 'exact', head: true }).eq('company_id', companyId),
|
|
supabase.from('invoices').select('*', { count: 'exact', head: true }).eq('company_id', companyId),
|
|
supabase.from('transactions').select('*', { count: 'exact', head: true }).eq('company_id', companyId),
|
|
supabase.from('bank_connections').select('id, status, consent_expires, bank_name').eq('company_id', companyId).eq('status', 'active'),
|
|
supabase.from('sie_imports').select('*', { count: 'exact', head: true }).eq('company_id', companyId).eq('status', 'completed'),
|
|
// Skatteverket tokens are user-scoped (one BankID identity per user) but
|
|
// carry the active company_id; either filter would work: we use user_id
|
|
// because that's what the token-store reads/writes against.
|
|
supabase.from('skatteverket_tokens').select('*', { count: 'exact', head: true }).eq('user_id', user.id),
|
|
// First name for the greeting.
|
|
supabase.from('profiles').select('full_name').eq('id', user.id).maybeSingle(),
|
|
getResolvedDashboardAgentProfile(),
|
|
// Pending-work counts + suggested matches come from lib/worklist: the
|
|
// same source as the sidebar badges, so the numbers can never diverge.
|
|
getWorklistCounts(supabase, companyId),
|
|
listSuggestedMatches(supabase, companyId, 5),
|
|
// In-progress work for the Fortsätt pane: pure draft-state derivation.
|
|
listResumeItems(supabase, companyId, now),
|
|
])
|
|
|
|
// 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 onboardingProgress: OnboardingProgress = {
|
|
hasCustomers: (customerCount || 0) > 0,
|
|
hasInvoices: (invoiceCount || 0) > 0,
|
|
hasBankConnected: (bankConnections?.length || 0) > 0 || (transactionCount || 0) > 0,
|
|
hasSIEImport: (sieImportCount || 0) > 0,
|
|
hasSkatteverketConnected: (skatteverketTokenCount || 0) > 0,
|
|
}
|
|
|
|
const nowMs = now.getTime()
|
|
const expiringBankConnections = (bankConnections || [])
|
|
.filter(conn => {
|
|
if (!conn.consent_expires) return false
|
|
const daysLeft = Math.ceil(
|
|
(new Date(conn.consent_expires).getTime() - nowMs) / (1000 * 60 * 60 * 24)
|
|
)
|
|
return daysLeft > 0 && daysLeft <= 14
|
|
})
|
|
.map(conn => ({
|
|
id: conn.id as string,
|
|
bank_name: conn.bank_name as string,
|
|
days_left: Math.ceil(
|
|
(new Date(conn.consent_expires!).getTime() - nowMs) / (1000 * 60 * 60 * 24)
|
|
),
|
|
}))
|
|
|
|
const userFirstName = profile?.full_name?.trim().split(/\s+/)[0] ?? null
|
|
|
|
return (
|
|
<DashboardContent
|
|
companyId={companyId}
|
|
agentBuilt={agentBuilt}
|
|
userFirstName={userFirstName}
|
|
expiringBankConnections={expiringBankConnections}
|
|
worklist={worklist}
|
|
suggestedMatches={suggestedMatches}
|
|
resumeItems={resumeItems}
|
|
onboardingProgress={onboardingProgress}
|
|
initialSetup={{
|
|
path: settings.initial_setup_path ?? null,
|
|
completedAt: settings.initial_setup_completed_at ?? null,
|
|
dismissedAt: settings.initial_setup_dismissed_at ?? null,
|
|
}}
|
|
/>
|
|
)
|
|
}
|