143 lines
6.0 KiB
TypeScript
143 lines
6.0 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useState } from "react"
|
|
import { useRouter } from "next/navigation"
|
|
import { FolderOpen, FileText, AlertCircle, DollarSign, TrendingUp, AlertTriangle, Calendar } from "lucide-react"
|
|
|
|
interface Deadline {
|
|
type: string
|
|
id: string
|
|
name: string
|
|
date: string
|
|
status_name: string
|
|
}
|
|
|
|
export default function ClientDashboard() {
|
|
const router = useRouter()
|
|
const [counts, setCounts] = useState({ projects: 0, invoices: 0, tickets: 0 })
|
|
const [stats, setStats] = useState({ totalBilled: 0, totalPaid: 0, totalOverdue: 0, upcomingDeadlines: [] as Deadline[] })
|
|
const [name, setName] = useState("")
|
|
|
|
const safeJson = (r: Response) => r.ok ? r.json() : Promise.reject()
|
|
|
|
useEffect(() => {
|
|
fetch("/client-portal/api/projects", { credentials: "include" })
|
|
.then(safeJson)
|
|
.then((d) => setCounts((prev) => ({ ...prev, projects: d.projects?.length || 0 })))
|
|
.catch(() => {})
|
|
|
|
fetch("/client-portal/api/invoices", { credentials: "include" })
|
|
.then(safeJson)
|
|
.then((d) => setCounts((prev) => ({ ...prev, invoices: d.invoices?.length || 0 })))
|
|
.catch(() => {})
|
|
|
|
fetch("/client-portal/api/support", { credentials: "include" })
|
|
.then(safeJson)
|
|
.then((d) => setCounts((prev) => ({ ...prev, tickets: d.tickets?.length || 0 })))
|
|
.catch(() => {})
|
|
|
|
fetch("/client-portal/api/dashboard/stats", { credentials: "include" })
|
|
.then(safeJson)
|
|
.then((d) => setStats(d))
|
|
.catch(() => {})
|
|
|
|
fetch("/api/auth/me", { credentials: "include" })
|
|
.then(safeJson)
|
|
.then((d) => setName(d.user?.firstName || d.user?.email || "Client"))
|
|
.catch(() => {})
|
|
}, [])
|
|
|
|
const cards = [
|
|
{ label: "Active Projects", value: counts.projects, icon: FolderOpen, color: "#1BB0CE", href: "/client-portal/projects" },
|
|
{ label: "Invoices", value: counts.invoices, icon: FileText, color: "#a855f7", href: "/client-portal/invoices" },
|
|
{ label: "Support Tickets", value: counts.tickets, icon: AlertCircle, color: "#f59e0b", href: "/client-portal/support" },
|
|
]
|
|
|
|
const formatCurrency = (amount: number) => {
|
|
const num = Number(amount) || 0
|
|
return `$${num.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<h1 className="text-xl font-bold text-foreground mb-1">Welcome, {name}</h1>
|
|
<p className="text-sm text-muted-foreground mb-6">Your project overview at a glance</p>
|
|
|
|
{/* Count cards */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
|
{cards.map((card) => {
|
|
const Icon = card.icon
|
|
return (
|
|
<button
|
|
key={card.label}
|
|
onClick={() => router.push(card.href)}
|
|
className="bg-card border border-border rounded-xl p-5 text-left hover:border-primary/30 transition-all group"
|
|
>
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<div className="w-10 h-10 rounded-lg flex items-center justify-center" style={{ backgroundColor: `${card.color}15` }}>
|
|
<Icon className="h-5 w-5" style={{ color: card.color }} />
|
|
</div>
|
|
<span className="text-2xl font-bold text-foreground">{card.value}</span>
|
|
</div>
|
|
<p className="text-sm text-muted-foreground">{card.label}</p>
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{/* Financial stats */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
|
|
<div className="bg-card border border-border rounded-xl p-5">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<DollarSign className="h-4 w-4 text-primary" />
|
|
<span className="text-xs text-muted-foreground uppercase tracking-wider">Total Billed</span>
|
|
</div>
|
|
<p className="text-xl font-bold text-foreground">{formatCurrency(stats.totalBilled)}</p>
|
|
</div>
|
|
<div className="bg-card border border-border rounded-xl p-5">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<TrendingUp className="h-4 w-4 text-emerald-400" />
|
|
<span className="text-xs text-muted-foreground uppercase tracking-wider">Total Paid</span>
|
|
</div>
|
|
<p className="text-xl font-bold text-emerald-400">{formatCurrency(stats.totalPaid)}</p>
|
|
</div>
|
|
<div className="bg-card border border-border rounded-xl p-5">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<AlertTriangle className="h-4 w-4 text-red-400" />
|
|
<span className="text-xs text-muted-foreground uppercase tracking-wider">Overdue</span>
|
|
</div>
|
|
<p className="text-xl font-bold text-red-400">{formatCurrency(stats.totalOverdue)}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Upcoming Deadlines */}
|
|
{stats.upcomingDeadlines.length > 0 && (
|
|
<div className="bg-card border border-border rounded-xl p-5">
|
|
<div className="flex items-center gap-2 mb-4">
|
|
<Calendar className="h-4 w-4 text-amber-400" />
|
|
<span className="text-sm font-semibold text-foreground">Upcoming Deadlines</span>
|
|
</div>
|
|
<div className="space-y-3">
|
|
{(stats.upcomingDeadlines || []).map((d, i) => (
|
|
<div key={`${d.type}-${d.id}-${i}`} className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className={`w-2 h-2 rounded-full ${
|
|
d.type === "invoice" ? "bg-purple-400" : "bg-primary"
|
|
}`} />
|
|
<div>
|
|
<p className="text-sm text-foreground">{d.name}</p>
|
|
<p className="text-[11px] text-muted-foreground capitalize">{d.type} · {d.status_name?.replace("_", " ")}</p>
|
|
</div>
|
|
</div>
|
|
<span className="text-xs text-muted-foreground">
|
|
{new Date(d.date).toLocaleDateString()}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|