import { useEffect, useMemo, useState } from "react"; import { Link, useNavigate } from "react-router-dom"; import { useDashboard, type DashboardResponse } from "../hooks/useDashboard"; import { Money } from "../components/ui"; import PercentGuard from "../components/PercentGuard"; import MonthlyTrendChart from "../components/charts/MonthlyTrendChart"; import FixedFundingBars, { type FixedItem, } from "../components/charts/FixedFundingBars"; import VariableAllocationDonut, { type VariableSlice, } from "../components/charts/VariableAllocationDonut"; import { useAuthSession } from "../hooks/useAuthSession"; import { fixedPlansApi } from "../api/fixedPlans"; import { useToast } from "../components/Toast"; import PaydayOverlay from "../components/PaydayOverlay"; import PaymentReconciliationModal from "../components/PaymentReconciliationModal"; import FundingConfirmationModal from "../components/FundingConfirmationModal"; import EarlyPaymentPromptModal from "../components/EarlyPaymentPromptModal"; import { isoToDateString, dateStringToUTCMidnight, getTodayInTimezone, getBrowserTimezone, formatDateInTimezone } from "../utils/timezone"; export default function DashboardPage() { const session = useAuthSession(); const shouldLoadDashboard = !!session.data?.userId; const trendMonths = 6; const [trendPage, setTrendPage] = useState(0); const { data, isLoading, isError, isFetching, error, refetch } = useDashboard( shouldLoadDashboard, { trendPage, trendMonths, } ); const navigate = useNavigate(); const { push } = useToast(); const [activeTab, setActiveTab] = useState< "overview" | "variable" | "fixed" | "trend" >("overview"); const [isDesktop, setIsDesktop] = useState(() => { if (typeof window === "undefined") return true; return window.matchMedia("(min-width: 1024px)").matches; }); // Get user timezone from dashboard data const userTimezone = data?.user?.timezone || getBrowserTimezone(); const incomeType = data?.user?.incomeType ?? "regular"; const incomeFrequency = data?.user?.incomeFrequency; const firstIncomeDate = data?.user?.firstIncomeDate ?? null; const variableSlices: VariableSlice[] = useMemo(() => { if (!data) return []; return data.variableCategories.map((c) => ({ name: c.name, value: c.percent, isSavings: !!c.isSavings, })); }, [data]); const fixedChartData: FixedItem[] = useMemo(() => { if (!data) return []; return data.fixedPlans.map((plan) => { const total = plan.totalCents ?? 0; const funded = Math.min(plan.fundedCents ?? 0, total); const remaining = Math.max(total - funded, 0); const aheadCents = getFundingAhead(plan) ?? 0; // Calculate percentages for the chart (0-100 scale) const fundedPercent = total > 0 ? (funded / total) * 100 : 0; const remainingPercent = total > 0 ? (remaining / total) * 100 : 0; return { name: plan.name, funded: fundedPercent, remaining: remainingPercent, fundedCents: funded, remainingCents: remaining, aheadCents, isOverdue: (plan as { isOverdue?: boolean }).isOverdue ?? false, dueOn: plan.dueOn, }; }); }, [data]); // Due-today overlay state const [dueTodayPlan, setDueTodayPlan] = useState< | { id: string; name: string; dueOn: string; remainingCents: number } | null >(null); const [overrideDueISO, setOverrideDueISO] = useState(""); useEffect(() => { if (typeof window === "undefined") return; const media = window.matchMedia("(min-width: 1024px)"); const update = () => setIsDesktop(media.matches); update(); media.addEventListener("change", update); return () => { media.removeEventListener("change", update); }; }, []); useEffect(() => { if (!isDesktop && activeTab === "overview") { setActiveTab("variable"); } }, [isDesktop, activeTab]); const [showDueOverlay, setShowDueOverlay] = useState(false); const [isSubmittingDue, setIsSubmittingDue] = useState(false); const [dueQueue, setDueQueue] = useState< Array<{ id: string; name: string; dueOn: string; remainingCents: number }> >([]); const [queueIndex, setQueueIndex] = useState(0); const savingsCategories = useMemo( () => (data?.variableCategories || []).filter((c) => c.isSavings), [data] ); const savingsReserveById = useMemo(() => { if (!data) return new Map(); const reserveTotal = data.fixedPlans.reduce( (sum, plan) => sum + (plan.fundedCents || 0), 0 ); const savingsTotalPercent = savingsCategories.reduce( (sum, c) => sum + (c.percent || 0), 0 ); const shares = savingsCategories.map((c) => { const raw = savingsTotalPercent ? (reserveTotal * (c.percent || 0)) / savingsTotalPercent : 0; const floored = Math.floor(raw); return { id: c.id, share: floored, frac: raw - floored }; }); let remainder = reserveTotal - shares.reduce((sum, item) => sum + item.share, 0); shares .slice() .sort((a, b) => b.frac - a.frac) .forEach((item) => { if (remainder > 0) { item.share += 1; remainder -= 1; } }); return new Map(shares.map((item) => [item.id, item.share])); }, [data, savingsCategories]); const [fundingSource, setFundingSource] = useState<"savings" | "deficit">( "savings" ); const [savingsCategoryId, setSavingsCategoryId] = useState( null ); const [hasCheckedDueToday, setHasCheckedDueToday] = useState(false); // Funding confirmation modal state const [showFundingConfirmation, setShowFundingConfirmation] = useState(false); const [fundingData, setFundingData] = useState<{ planId: string; planName: string; totalCents: number; fundedCents: number; availableBudget: number; } | null>(null); // Payment reconciliation modal state const [showPaymentReconciliation, setShowPaymentReconciliation] = useState(false); const [reconciliationData, setReconciliationData] = useState<{ planId: string; planName: string; totalCents: number; fundedCents: number; isOverdue: boolean; overdueAmount?: number; message: string; nextDueDate: string; } | null>(null); const [earlyPaymentPrompt, setEarlyPaymentPrompt] = useState<{ planId: string; planName: string; dueOn: string; } | null>(null); const openDueItem = async (item: { id: string; name: string; dueOn: string }) => { const planDetail = data?.fixedPlans.find((p) => p.id === item.id); const totalCents = planDetail?.totalCents ?? 0; const fundedCents = planDetail?.fundedCents ?? 0; const isFullyFunded = fundedCents >= totalCents; if (!isFullyFunded) { const availableBudget = data?.totals.variableBalanceCents ?? 0; setFundingData({ planId: item.id, planName: item.name, totalCents, fundedCents, availableBudget, }); setShowFundingConfirmation(true); return; } try { const fundingResult = await fixedPlansApi.attemptFinalFunding(item.id); const freq = (planDetail as any)?.frequency; const nextDueISO = computeNextDueDateISO(item.dueOn, freq); const nextDueDate = nextDueISO ? isoToDateString(nextDueISO, userTimezone) : item.dueOn.slice(0, 10); setReconciliationData({ planId: item.id, planName: item.name, totalCents: fundingResult.totalCents, fundedCents: fundingResult.fundedCents, isOverdue: !!fundingResult.isOverdue, overdueAmount: fundingResult.overdueAmount, message: fundingResult.message ?? "", nextDueDate: nextDueDate ?? getTodayInTimezone(userTimezone), }); setShowPaymentReconciliation(true); } catch (fundingError) { console.error("[DueOverlay] attempt-final-funding failed", fundingError); } }; const advanceDueQueue = async () => { const nextIdx = queueIndex + 1; if (nextIdx < dueQueue.length) { const nextItem = dueQueue[nextIdx]; setQueueIndex(nextIdx); setDueTodayPlan(nextItem); setOverrideDueISO(nextItem.dueOn.slice(0, 10)); const initialSavings = savingsCategories[0]?.id || null; setSavingsCategoryId(initialSavings); setFundingSource("savings"); await openDueItem(nextItem); } else { setDueTodayPlan(null); setDueQueue([]); setQueueIndex(0); } }; function toUserMidnight(iso: string, timezone: string) { const dateStr = isoToDateString(iso, timezone); return new Date(dateStringToUTCMidnight(dateStr, timezone)); } function countPayPeriodsBetween( startIso: string, endIso: string, firstIncomeIso: string, frequency: NonNullable, timezone: string ) { let count = 0; let nextPayDate = toUserMidnight(firstIncomeIso, timezone); const normalizedStart = toUserMidnight(startIso, timezone); const normalizedEnd = toUserMidnight(endIso, timezone); const targetDay = Number(isoToDateString(firstIncomeIso, timezone).split("-")[2] || "1"); const advanceByPeriod = () => { if (frequency === "monthly") { const year = nextPayDate.getUTCFullYear(); const month = nextPayDate.getUTCMonth() + 1; const daysInMonth = new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); nextPayDate = new Date(Date.UTC(year, month, Math.min(targetDay, daysInMonth))); } else { const days = frequency === "biweekly" ? 14 : 7; nextPayDate = new Date(nextPayDate.getTime() + days * 86_400_000); } }; while (nextPayDate < normalizedStart) { advanceByPeriod(); } while (nextPayDate < normalizedEnd) { count++; advanceByPeriod(); } return Math.max(1, count); } function getFundingAhead(plan: any) { if ( incomeType !== "regular" || !incomeFrequency || !firstIncomeDate || !plan.cycleStart || !plan.dueOn ) { return null; } const nowOverride = new URLSearchParams(window.location.search).get("debugNow"); const now = nowOverride ? new Date(`${nowOverride}T00:00:00.000Z`).toISOString() : new Date().toISOString(); let cycleStart = plan.cycleStart as string; const dueOn = plan.dueOn as string; let cycleStartDate: Date; let dueDate: Date; let nowDate: Date; try { cycleStartDate = toUserMidnight(cycleStart, userTimezone); dueDate = toUserMidnight(dueOn, userTimezone); nowDate = toUserMidnight(now, userTimezone); } catch { return null; } if (cycleStartDate >= dueDate || cycleStartDate > nowDate) { cycleStart = now; } const totalPeriods = countPayPeriodsBetween( cycleStart, dueOn, firstIncomeDate, incomeFrequency, userTimezone ); const elapsedPeriods = countPayPeriodsBetween( cycleStart, now, firstIncomeDate, incomeFrequency, userTimezone ); const targetFunded = Math.min( plan.totalCents ?? 0, Math.ceil(((plan.totalCents ?? 0) * elapsedPeriods) / totalPeriods) ); const funded = plan.fundedCents ?? 0; const aheadBy = Math.max(0, funded - targetFunded); return aheadBy > 0 ? aheadBy : null; } // Helper: compute next due date from current due using user's timezone function computeNextDueDateISO(dateISO: string, frequency?: string): string | null { if (!frequency || frequency === "one-time") return null; // Convert ISO to date string in user's timezone const dateStr = isoToDateString(dateISO, userTimezone); const [year, month, day] = dateStr.split('-').map(Number); const date = new Date(Date.UTC(year, month - 1, day)); if (frequency === "weekly") { date.setUTCDate(date.getUTCDate() + 7); } else if (frequency === "biweekly") { date.setUTCDate(date.getUTCDate() + 14); } else if (frequency === "monthly") { date.setUTCMonth(date.getUTCMonth() + 1); } // Convert back to ISO with timezone handling const nextDateStr = `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, '0')}-${String(date.getUTCDate()).padStart(2, '0')}`; return dateStringToUTCMidnight(nextDateStr, userTimezone); } // Detect if any fixed plan is due/overdue and needs user payment confirmation. // Include fully funded overdue plans so missed-day confirmations still surface. useEffect(() => { let cancelled = false; async function checkDueToday() { if (!data || !shouldLoadDashboard || hasCheckedDueToday) return; try { const res = await fixedPlansApi.due({ daysAhead: 0 }); const items = (res.items || []) .filter((x) => x.isDue && (!x.isOverdue || x.remainingCents <= 0)) .filter((x) => { const dismissedUntilStr = localStorage.getItem(`overdue-dismissed-${x.id}`); if (!dismissedUntilStr) return true; const dismissedUntil = parseInt(dismissedUntilStr, 10); if (!Number.isFinite(dismissedUntil) || Date.now() >= dismissedUntil) { localStorage.removeItem(`overdue-dismissed-${x.id}`); return true; } return false; }); if (!cancelled && items.length > 0) { setHasCheckedDueToday(true); const first = items[0]; await openDueItem(first); // Store queue for potential future bills setDueQueue(items.map((i) => ({ id: i.id, name: i.name, dueOn: i.dueOn, remainingCents: i.remainingCents, }))); setQueueIndex(0); } } catch (e: any) { // Non-blocking: just ignore failures try { console.warn("[DueOverlay] due check failed", e); } catch {} } } checkDueToday(); return () => { cancelled = true; }; }, [data, shouldLoadDashboard, hasCheckedDueToday, savingsCategories, userTimezone]); useEffect(() => { if ( !data || showDueOverlay || showFundingConfirmation || showPaymentReconciliation || earlyPaymentPrompt ) { return; } const eligible = data.fixedPlans.find((plan: any) => { const total = plan.totalCents ?? 0; const funded = plan.fundedCents ?? 0; if (total <= 0 || funded < total) return false; if (plan.isOverdue) return false; if (plan.needsFundingThisPeriod !== false) return false; const ahead = getFundingAhead(plan); if (!ahead || ahead <= 0) return false; const key = `early-paid-prompt-${plan.id}-${plan.dueOn}`; if (localStorage.getItem(key)) return false; return true; }); if (eligible) { setEarlyPaymentPrompt({ planId: eligible.id, planName: eligible.name, dueOn: eligible.dueOn, }); } }, [ data, showDueOverlay, showFundingConfirmation, showPaymentReconciliation, earlyPaymentPrompt, ]); useEffect(() => { if (data && !data.hasBudgetSetup) { navigate("/onboarding", { replace: true }); } }, [data?.hasBudgetSetup, navigate]); if (isLoading) { return (
{Array.from({ length: 3 }).map((_, idx) => (
))}
); } if (isError || !data) { return (

{(error as any)?.message || "Failed to load dashboard"}

); } const currency = (cents: number) => (cents / 100).toFixed(2); // CSV export removed per UI simplification request. const hasCategories = data.variableCategories.length > 0; const hasPlans = data.fixedPlans.length > 0; const hasTx = data.recentTransactions.length > 0; const hasTransactionsOlderThanSixMonths = data.trendWindow?.hasTransactionsOlderThanSixMonths ?? false; const useTrendWindowPagination = hasTransactionsOlderThanSixMonths; const trendWindowLabel = data.trendWindow?.label ?? "Last 6 months"; const canTrendNewer = data.trendWindow?.canGoNewer ?? trendPage > 0; const canTrendOlder = data.trendWindow?.canGoOlder ?? true; const greetingName = data.user.displayName || session.data?.displayName || session.data?.email?.split("@")[0] || "friend"; // Chart data for analytics section return (
{/* Payday Overlay */} {/* Due Today Overlay */} {showDueOverlay && dueTodayPlan && (

{dueTodayPlan.name} is due today

Confirm the due date and record the payment. We'll roll the plan forward automatically.

{(() => { const planFull = data?.fixedPlans?.find((p) => p.id === dueTodayPlan.id); // Extract frequency from plan.frequency or paymentSchedule.frequency let freq = (planFull as any)?.frequency; if (!freq && (planFull as any)?.paymentSchedule?.frequency) { freq = (planFull as any).paymentSchedule.frequency; } const suggestedISO = planFull ? computeNextDueDateISO(dueTodayPlan.dueOn, freq) : null; const suggestedDateOnly = suggestedISO ? suggestedISO.slice(0, 10) : ""; return (
Calculated next due date
{suggestedDateOnly || "n/a"}
If this looks correct, press Confirm. Or adjust below.
); })()}
Remaining
{queueIndex + 1} of {dueQueue.length}
{dueTodayPlan.remainingCents > 0 && (
Funding source
{fundingSource === "savings" && ( )}
)}
)} {/* Funding Confirmation Modal */} {showFundingConfirmation && fundingData && ( { setShowFundingConfirmation(false); setFundingData(null); }} onFundingComplete={async (fundingResult) => { setShowFundingConfirmation(false); try { const planDetail = data?.fixedPlans.find(p => p.id === fundingData.planId); const freq = (planDetail as any)?.frequency; const fallbackDueISO = dateStringToUTCMidnight(getTodayInTimezone(userTimezone), userTimezone); const nextDueISO = computeNextDueDateISO(planDetail?.dueOn ?? fallbackDueISO, freq); const nextDueDate = nextDueISO ? isoToDateString(nextDueISO, userTimezone) : getTodayInTimezone(userTimezone); setReconciliationData({ planId: fundingData.planId, planName: fundingData.planName, totalCents: fundingResult.totalCents, fundedCents: fundingResult.fundedCents, isOverdue: !!fundingResult.isOverdue, overdueAmount: fundingResult.overdueAmount, message: fundingResult.message ?? "", nextDueDate: nextDueDate ?? getTodayInTimezone(userTimezone), }); setFundingData(null); setShowPaymentReconciliation(true); await refetch(); } catch (err) { console.error("Failed to proceed to payment reconciliation", err); push("err", "Failed to proceed with payment"); setFundingData(null); } }} /> )} {/* Payment Reconciliation Modal */} {showPaymentReconciliation && reconciliationData && ( { setShowPaymentReconciliation(false); setReconciliationData(null); }} onSuccess={async () => { setShowPaymentReconciliation(false); setReconciliationData(null); // Clear any "remind me later" dismissal since they've handled it if (reconciliationData?.planId) { localStorage.removeItem(`overdue-dismissed-${reconciliationData.planId}`); } push("ok", "Payment recorded successfully"); await refetch(); if (dueQueue.length > 0) { await advanceDueQueue(); } }} /> )} {earlyPaymentPrompt && ( { const key = `early-paid-prompt-${earlyPaymentPrompt.planId}-${earlyPaymentPrompt.dueOn}`; localStorage.setItem(key, "dismissed"); setEarlyPaymentPrompt(null); }} onConfirmed={() => { const key = `early-paid-prompt-${earlyPaymentPrompt.planId}-${earlyPaymentPrompt.dueOn}`; localStorage.setItem(key, "confirmed"); refetch(); }} /> )} {/* HEADER */}

Hello, {greetingName}!

Snapshot of balances, plans, and recent activity.

{/* OVERDUE BILLS WARNING BANNER */} {(() => { const overduePlans = data.fixedPlans.filter((p: any) => p.isOverdue && (p.overdueAmount ?? 0) > 0); if (overduePlans.length === 0) return null; const totalOverdue = overduePlans.reduce((sum: number, p: any) => sum + (p.overdueAmount ?? 0), 0); return (
!
{overduePlans.length === 1 ? `${overduePlans[0].name} is overdue` : `${overduePlans.length} bills are overdue`}
Total outstanding: · Post income to auto-fund
+ Add Income
{overduePlans.length > 1 && (
Priority order: {overduePlans .sort((a: any, b: any) => { const aTime = a.overdueSince ? new Date(a.overdueSince).getTime() : 0; const bTime = b.overdueSince ? new Date(b.overdueSince).getTime() : 0; return aTime - bTime; }) .map((p: any) => `${p.name} ($${((p.overdueAmount ?? 0) / 100).toFixed(0)})`) .join(" → ")}
)}
); })()} {/* BUDGET OVERVIEW – key metrics */}
sum + (plan.fundedCents || 0), 0)} />
{/* ANALYTICS SECTION - Desktop: Show early with tabs */} {isDesktop && (

Analytics

setActiveTab("overview")} /> setActiveTab("variable")} /> setActiveTab("fixed")} /> setActiveTab("trend")} />
{activeTab === "overview" && (
)} {activeTab === "variable" && ( )} {activeTab === "fixed" && ( )} {activeTab === "trend" && (
{useTrendWindowPagination ? ( setTrendPage((p) => p + 1)} onNewer={() => setTrendPage((p) => Math.max(0, p - 1))} /> ) : null}
)}
)} {/* UPCOMING PLAN ALERTS */} {data.upcomingPlans.length > 0 && ( )} {/* VARIABLE / FIXED LISTS */}

Variable Categories

{data.percentTotal}%
{!hasCategories ? ( ) : (
{data.variableCategories.map((c) => { return (
{c.name}
{c.isSavings ? "Savings • " : ""} Priority {c.priority}
{c.isSavings && (c.savingsTargetCents ?? 0) > 0 && (
Goal ${currency(c.savingsTargetCents ?? 0)}
)}
{c.isSavings ? ( <>
Available · Reserved{" "}
{c.percent}%
) : ( <>
{c.percent}%
)}
); })}
)}

Fixed plans

{!hasPlans ? ( ) : (
{data.fixedPlans.map((p) => { const total = p.totalCents ?? 0; const funded = Math.min(p.fundedCents ?? 0, total); const remaining = Math.max(total - funded, 0); const pct = total ? Math.round((funded / total) * 100) : 0; const isOverdue = (p as any).isOverdue; const overdueAmount = (p as any).overdueAmount ?? 0; const aheadCents = getFundingAhead(p); return (
{p.name}
{isOverdue && ( OVERDUE )} {aheadCents !== null && ( Ahead {new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0, }).format((aheadCents ?? 0) / 100)} )}
Due{" "} {formatDateInTimezone(p.dueOn, userTimezone)} · Priority {p.priority}
{isOverdue && overdueAmount > 0 && (
Outstanding:
)}
Remaining
); })}
)}
{/* SAVINGS GOALS */} {data.savingsTargets.length > 0 && ( )} {/* Mobile / tablet: tabbed analytics */}

Analytics

setActiveTab("variable")} /> setActiveTab("fixed")} /> setActiveTab("trend")} />
{activeTab === "variable" && ( )} {activeTab === "fixed" && ( )} {activeTab === "trend" && (
{useTrendWindowPagination ? ( setTrendPage((p) => p + 1)} onNewer={() => setTrendPage((p) => Math.max(0, p - 1))} /> ) : null}
)}
); } /* ─────────────────────────── * Small components * ───────────────────────────*/ function AnalyticsTabButton({ label, active, onClick, }: { label: string; active: boolean; onClick: () => void; }) { return ( ); } function TrendWindowControls({ label, canGoOlder, canGoNewer, busy, onOlder, onNewer, }: { label: string; canGoOlder: boolean; canGoNewer: boolean; busy: boolean; onOlder: () => void; onNewer: () => void; }) { return (
{label}
); } function UpcomingPlanAlerts({ plans, userTimezone, }: { plans: { id: string; name: string; dueOn: string; remainingCents: number }[]; userTimezone: string; }) { if (plans.length === 0) return null; return (
Upcoming plan alerts (next 14 days)
    {plans.map((plan) => (
  • {plan.name}
    Due{" "} {formatDateInTimezone(plan.dueOn, userTimezone, { month: "short", day: "numeric", })}
    Remaining
  • ))}
); } function SavingsGoalsPanel({ goals, }: { goals: Array<{ id: string; name: string; balanceCents: number; targetCents: number; percent: number; }>; }) { return (

Savings goals

    {goals.map((goal) => (
  • {goal.name} {goal.percent}%
    saved Goal
  • ))}
); } function RecentTransactionsPanel({ transactions, hasData, rangeLabel, userTimezone, }: { transactions: DashboardResponse["recentTransactions"]; hasData: boolean; rangeLabel?: string; userTimezone: string; }) { const title = rangeLabel ? "Transactions in window" : "Recent transactions"; const visibleTransactions = transactions.slice(0, 10); const currentMonthLabel = new Intl.DateTimeFormat("en-US", { month: "long", year: "numeric", timeZone: userTimezone, }).format(new Date()); if (!hasData) { return (

{title}

{rangeLabel ?
{rangeLabel}
: null}
Tip: Records defaults to This month ({currentMonthLabel}).
); } return (

{title}

{rangeLabel ?
{rangeLabel}
: null}
Tip: Records defaults to This month ({currentMonthLabel}).
{/* Desktop table view */}
{visibleTransactions.map((tx) => ( ))}
Date Kind Amount
{new Date(tx.occurredAt).toLocaleString()} {tx.kind.replace("_", " ")}
{/* Mobile card view */}
{visibleTransactions.map((tx) => (
{tx.kind.replace("_", " ")}
{new Date(tx.occurredAt).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' })}
))}
View more
); } // Modernized KPI card function Card({ label, children, }: { label: string; children: React.ReactNode; }) { return (
{label}
{children}
); } function ProgressBar({ value, max, label, variant = "default", }: { value: number; max: number; label?: string; variant?: "default" | "danger"; }) { const pct = Math.min(100, Math.round((value / Math.max(max, 1)) * 100)); const barColor = variant === "danger" ? "bg-red-500" : "bg-[--color-ink]"; return (
{label &&
{label}
}
); } function EmptyState({ message, actionLabel, actionTo, }: { message: string; actionLabel: string; actionTo: string; }) { return (

{message}

{actionLabel}
); }