"use client"; import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, } from "recharts"; import { DollarSign, Users, FileText, Calendar, ArrowUpRight, ArrowDownRight, Bell, Search, MoreHorizontal, ChevronRight, } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { fetchDashboardData, fetchUserRole, type DashboardData } from "@/lib/supabase/dashboard-actions"; import { centsToDecimal, INVOICE_STATUS_LABELS } from "@/lib/supabase/invoicing-types"; // ── Skeleton ───────────────────────────────────────────────── function DashboardSkeleton() { return (
{[1, 2, 3, 4].map((i) => (
))}
); } // ── Components ─────────────────────────────────────────────── function MetricCard({ title, value, change, changeSuffix, trend, comparisonLabel, Icon, gradient, bgSoft, iconColor, }: { title: string; value: string; change: string | null; changeSuffix?: string; trend: "up" | "down" | "idle"; comparisonLabel?: string | null; Icon: typeof DollarSign; gradient: string; bgSoft: string; iconColor: string; }) { const TrendIcon = trend === "up" ? ArrowUpRight : trend === "down" ? ArrowDownRight : null; const trendColor = trend === "up" ? "text-emerald-600" : trend === "down" ? "text-red-500" : "text-gray-400"; const trendBg = trend === "up" ? "bg-emerald-50" : trend === "down" ? "bg-red-50" : "bg-gray-100"; return (
{trend !== "idle" && TrendIcon && change && (
{change === "new" ? "New" : `${change}${changeSuffix || "%"}`}
)}

{title}

{value}

{comparisonLabel &&

{comparisonLabel}

}
); } function StatusBadge({ status }: { status: string }) { const variants: Record = { paid: "bg-emerald-100 text-emerald-700 border-emerald-200", pending: "bg-amber-100 text-amber-700 border-amber-200", overdue: "bg-red-100 text-red-700 border-red-200", draft: "bg-gray-100 text-gray-600 border-gray-200", sent: "bg-blue-100 text-blue-700 border-blue-200", cancelled: "bg-gray-100 text-gray-500 border-gray-200", active: "bg-emerald-100 text-emerald-700 border-emerald-200", "on-leave": "bg-gray-100 text-gray-600 border-gray-200", }; const cls = variants[status] || "bg-gray-100 text-gray-600 border-gray-200"; const label = INVOICE_STATUS_LABELS[status as keyof typeof INVOICE_STATUS_LABELS] || status; return ( {label} ); } function Avatar({ initials, color }: { initials: string; color: string }) { const colors: Record = { teal: "bg-teal-500", blue: "bg-blue-500", purple: "bg-purple-500", orange: "bg-orange-500", pink: "bg-pink-500", }; return (
{initials}
); } const ICON_MAP: Record = { DollarSign, Users, Calendar, FileText, }; const avatarColors = ["teal", "blue", "purple", "orange", "pink"]; function formatDate(dateStr: string): string { const d = new Date(dateStr + "T00:00:00"); return d.toLocaleDateString("en-US", { month: "short", day: "numeric" }); } // ── Main Dashboard ─────────────────────────────────────────── export default function DashboardPage() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [userRole, setUserRole] = useState("employee"); const router = useRouter(); useEffect(() => { let cancelled = false; Promise.all([ fetchDashboardData(), fetchUserRole(), ]) .then(([d, role]) => { if (!cancelled) { setData(d); setUserRole(role); } }) .catch((err) => { if (!cancelled) { console.error("Dashboard error:", err); const errorMessage = err instanceof Error ? err.message : String(err); // If no organisation found, redirect to onboarding if (errorMessage.includes("Organisation error") || errorMessage.includes("No organisation found")) { router.push("/onboarding"); return; } setError(errorMessage); } }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [router]); const [greeting, setGreeting] = useState(""); useEffect(() => { const today = new Date(); setGreeting( today.toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric", }) ); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); if (loading) return ; if (error) { return (
{error}
); } if (!data) return null; const metrics: { title: string; value: string; change: string | null; trend: "up" | "down" | "idle"; comparisonLabel?: string | null; Icon: typeof DollarSign; gradient: string; bgSoft: string; iconColor: string; }[] = [ { title: "Cash on hand", value: `$${(data.metrics.cash_on_hand / 100).toLocaleString()}`, change: data.trends.cash?.value ?? null, trend: data.trends.cash ? (data.trends.cash.direction as "up" | "down") : "idle", comparisonLabel: data.metrics.cash_on_hand > 0 ? data.comparisonLabel : null, Icon: DollarSign, gradient: "from-teal-500 to-emerald-600", bgSoft: "bg-teal-50", iconColor: "text-teal-600", }, { title: "Outstanding invoices", value: `$${(data.metrics.outstanding_invoices / 100).toLocaleString()}`, change: data.trends.outstanding?.value ?? null, trend: data.trends.outstanding ? (data.trends.outstanding.direction as "up" | "down") : "idle", comparisonLabel: data.metrics.outstanding_invoices > 0 ? data.comparisonLabel : null, Icon: FileText, gradient: "from-blue-500 to-indigo-600", bgSoft: "bg-blue-50", iconColor: "text-blue-600", }, { title: "Payroll due", value: `$${(data.metrics.payroll_due / 100).toLocaleString()}`, change: null, trend: "idle", comparisonLabel: data.payrollDueDate ? `due ${data.payrollDueDate}` : null, Icon: Users, gradient: "from-purple-500 to-violet-600", bgSoft: "bg-purple-50", iconColor: "text-purple-600", }, { title: "Headcount", value: String(data.metrics.headcount), change: null, trend: "idle", comparisonLabel: null, Icon: Users, gradient: "from-orange-500 to-amber-600", bgSoft: "bg-orange-50", iconColor: "text-orange-600", }, ]; return (
{/* Header */}

Dashboard

{greeting}

{/* Metrics */}
{metrics.map((m, i) => )}
{/* Revenue Chart — visible to owner, admin, accountant */} {(userRole === "owner" || userRole === "admin" || userRole === "accountant") && (
Revenue Overview

Revenue vs expenses over the last 7 months

{data.revenueData.some((d) => d.revenue > 0) && ( +22.4% vs last period )}
{data.revenueData.every((d) => d.revenue === 0) ? (

No revenue data yet

Create and mark invoices as paid to see your revenue trend

) : ( v > 0 ? `$${v / 1000}k` : ""} /> [`$${Number(value).toLocaleString()}`, ""]} /> )}
)} {/* Two Columns */}
{/* Recent Invoices — visible to owner, admin, accountant */} {(userRole === "owner" || userRole === "admin" || userRole === "accountant") && (
Recent Invoices View all
{data.recentInvoices.map((invoice) => { const displayTotal = invoice.total_cents > 0 ? `$${centsToDecimal(invoice.total_cents)}` : invoice.amount != null ? `$${invoice.amount.toFixed(2)}` : "$0.00"; return ( ); })}
Company Due Amount Status
{invoice.client_name} {formatDate(invoice.due_date)} {displayTotal}
)} {/* Upcoming Events — visible to all roles */}
Upcoming
{data.upcomingEvents.map((event, index) => { const Icon = ICON_MAP[event.icon] || Calendar; return (

{event.title}

{event.date}

); })}
{/* Bottom Row — People Snapshot (owner/admin/hr) */} {(userRole === "owner" || userRole === "admin" || userRole === "hr_manager") && (
{/* People Snapshot */}
People Snapshot Manage
{data.peopleSnapshot.length === 0 ? (

No employees added yet.

) : (
{data.peopleSnapshot.map((person) => (

{person.name}

{person.department}{person.job_title ? ` · ${person.job_title}` : ""}

))}
)}
)} {/* Budget vs Actuals — visible to owner, admin, accountant */} {(userRole === "owner" || userRole === "admin" || userRole === "accountant") && data.budgetData.length > 0 && data.budgetData.some((b) => b.budget > 0) && ( Budget vs Actuals

Department-level monthly salary spend

{data.budgetData.map((item, index) => { const percentage = item.budget > 0 ? (item.actual / item.budget) * 100 : 0; const isOverBudget = percentage > 100; return (
{item.category}
${(item.actual / 1000).toFixed(1)}k / ${(item.budget / 1000).toFixed(1)}k {percentage.toFixed(0)}%
); })}
)}
); }