Yaltopia-Ticket-Admin/src/pages/admin/dashboard/index.tsx
“kirukib” 4795822065 Add dashboard quick access cards for commerce routes
Link cards for invoices, proforma, proforma requests, payments, and payment requests above metrics; show loading state below quick access.

Made-with: Cursor
2026-04-15 10:24:56 +03:00

379 lines
14 KiB
TypeScript

import { useQuery } from "@tanstack/react-query";
import { Link } from "react-router-dom";
import {
Receipt,
FileSearch,
ClipboardList,
CreditCard,
FileClock,
ChevronRight,
} from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { dashboardService, invoiceService } from "@/services";
import { Badge } from "@/components/ui/badge";
import { format } from "date-fns";
import { cn } from "@/lib/utils";
const COLORS = ["#10b981", "#f59e0b", "#ef4444", "#3b82f6", "#8b5cf6"];
const COMMERCE_QUICK_LINKS = [
{
label: "Invoices",
description: "Browse, search, and manage issued invoices.",
path: "/admin/invoices",
icon: Receipt,
color: "text-slate-700",
},
{
label: "Proforma",
description: "View and manage proforma invoices and drafts.",
path: "/admin/proforma",
icon: FileSearch,
color: "text-blue-600",
},
{
label: "Proforma requests",
description: "Review and process incoming proforma requests.",
path: "/admin/proforma-requests",
icon: ClipboardList,
color: "text-violet-600",
},
{
label: "Payments",
description: "Recorded payments and transaction history.",
path: "/admin/payments",
icon: CreditCard,
color: "text-emerald-600",
},
{
label: "Payment requests",
description: "Pending and processed payment requests.",
path: "/admin/payment-requests",
icon: FileClock,
color: "text-amber-600",
},
] as const;
export default function DashboardPage() {
const { data: metrics, isLoading: metricsLoading } = useQuery({
queryKey: ["admin", "dashboard", "metrics"],
queryFn: () => dashboardService.getMetrics(),
});
const { data: scannedInvoices, isLoading: scannedLoading } = useQuery({
queryKey: ["admin", "dashboard", "scanned"],
queryFn: () => dashboardService.getScannedInvoices(),
});
const { data: statusBreakdown, isLoading: statusLoading } = useQuery({
queryKey: ["admin", "dashboard", "status-breakdown"],
queryFn: () => dashboardService.getInvoiceStatusBreakdown(),
});
const { data: proformaRequests, isLoading: requestsLoading } = useQuery({
queryKey: ["admin", "dashboard", "proforma-requests"],
queryFn: () => invoiceService.getProformaRequests({ limit: 5 }),
});
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
}).format(amount);
};
const dataLoading =
metricsLoading || scannedLoading || statusLoading || requestsLoading;
return (
<div className="p-8 space-y-12 max-w-7xl mx-auto bg-white min-h-screen">
<header>
<h1 className="text-3xl font-bold text-gray-900 tracking-tight">
Dashboard Overview
</h1>
<p className="text-gray-500 mt-1">
Operational status and pending verification items.
</p>
</header>
{/* Quick access — invoices, proforma, payments */}
<section className="space-y-4">
<h2 className="text-sm font-bold text-gray-400 uppercase tracking-widest">
Quick access
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-4">
{COMMERCE_QUICK_LINKS.map((item) => (
<Link
key={item.path}
to={item.path}
className="group block rounded-none outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
<Card className="h-full cursor-pointer border-slate-200/60 shadow-sm hover:shadow-md transition-all rounded-none bg-white overflow-hidden">
<CardHeader className="pb-2 space-y-0 border-b border-slate-50 bg-slate-50/30">
<div className="flex items-center justify-between">
<span className="text-[10px] font-bold uppercase tracking-widest text-slate-400">
{item.label}
</span>
<item.icon
className={cn(
"w-4 h-4 shrink-0 transition-colors",
item.color,
)}
/>
</div>
</CardHeader>
<CardContent className="pt-3 pb-4 flex items-end justify-between gap-2">
<p className="text-xs text-muted-foreground leading-relaxed line-clamp-3">
{item.description}
</p>
<ChevronRight className="w-4 h-4 shrink-0 text-slate-300 group-hover:translate-x-0.5 transition-transform" />
</CardContent>
</Card>
</Link>
))}
</div>
</section>
{dataLoading ? (
<div className="py-16 text-center text-gray-500 font-medium border border-dashed border-gray-200 rounded-none">
Loading dashboard data
</div>
) : (
<>
{/* Top Metrics Cards */}
<section className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card className="border-none bg-gray-50 shadow-none rounded-none">
<CardHeader className="pb-1">
<CardTitle className="text-[10px] font-bold text-gray-400 uppercase tracking-widest">
Gross Revenue
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-black text-gray-900">
{formatCurrency(metrics?.totalRevenue || 0)}
</div>
</CardContent>
</Card>
<Card className="border-none bg-gray-50 shadow-none rounded-none">
<CardHeader className="pb-1">
<CardTitle className="text-[10px] font-bold text-gray-400 uppercase tracking-widest">
Total Payments
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-black text-gray-900">
{(metrics?.totalPayments || 0).toLocaleString()}
</div>
</CardContent>
</Card>
<Card className="border-none bg-gray-50 shadow-none rounded-none">
<CardHeader className="pb-1">
<CardTitle className="text-[10px] font-bold text-gray-400 uppercase tracking-widest">
Total Invoices
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-black text-gray-900">
{(metrics?.totalInvoices || 0).toLocaleString()}
</div>
</CardContent>
</Card>
</section>
{/* Invoice Status Breakdown (Full Width) */}
<section className="space-y-4">
<h2 className="text-xl font-bold text-gray-900">
Invoice Status Breakdown
</h2>
<div className="border bg-white divide-y overflow-hidden rounded-none">
{statusBreakdown && statusBreakdown.length > 0 ? (
statusBreakdown.map((item, idx) => (
<div
key={idx}
className="p-4 flex items-center justify-between hover:bg-gray-50"
>
<div className="flex items-center gap-3">
<div
className="w-2.5 h-2.5"
style={{ backgroundColor: COLORS[idx % COLORS.length] }}
/>
<span className="text-sm font-bold uppercase tracking-wide text-gray-700">
{item.status}
</span>
</div>
<span className="text-lg font-black text-gray-900">
{item.count.toLocaleString()}
</span>
</div>
))
) : (
<div className="p-12 text-center text-gray-400 italic">
No status data available.
</div>
)}
{/* Detailed Stats Row */}
<div className="p-6 grid grid-cols-3 gap-8 bg-gray-50/30">
<div className="text-left">
<p className="text-[10px] font-bold text-gray-400 uppercase tracking-widest">
Paid Invoices
</p>
<p className="text-2xl font-black text-green-600">
{metrics?.paidInvoices || 0}
</p>
</div>
<div className="text-left border-l pl-8">
<p className="text-[10px] font-bold text-gray-400 uppercase tracking-widest">
Pending Invoices
</p>
<p className="text-2xl font-black text-orange-500">
{metrics?.pendingInvoices || 0}
</p>
</div>
<div className="text-left border-l pl-8">
<p className="text-[10px] font-bold text-gray-400 uppercase tracking-widest">
Overdue Invoices
</p>
<p className="text-2xl font-black text-red-500">
{metrics?.overdueInvoices || 0}
</p>
</div>
</div>
</div>
</section>
{/* Recent Proforma Requests (Full Width) */}
<section className="space-y-4">
<h2 className="text-xl font-bold text-gray-900">
Recent Proforma Requests
</h2>
<div className="border overflow-hidden rounded-none">
<table className="w-full text-left">
<thead className="bg-gray-50 border-b">
<tr>
<th className="px-6 py-4 text-[10px] font-bold text-gray-500 uppercase tracking-widest">
Title
</th>
<th className="px-6 py-4 text-[10px] font-bold text-gray-500 uppercase tracking-widest">
Category
</th>
<th className="px-6 py-4 text-[10px] font-bold text-gray-500 uppercase tracking-widest">
Status
</th>
<th className="px-6 py-4 text-[10px] font-bold text-gray-500 uppercase tracking-widest">
Deadline
</th>
</tr>
</thead>
<tbody className="bg-white divide-y">
{proformaRequests?.data && proformaRequests.data.length > 0 ? (
proformaRequests.data.map((request) => (
<tr
key={request.id}
className="hover:bg-gray-50 transition-colors"
>
<td className="px-6 py-5">
<div className="text-sm font-bold text-gray-900 uppercase tracking-tight">
{request.title}
</div>
</td>
<td className="px-6 py-5 text-sm font-medium text-gray-600">
{request.category}
</td>
<td className="px-6 py-5">
<Badge
variant="outline"
className="rounded-none border-gray-200 font-bold text-[10px] uppercase tracking-widest"
>
{request.status}
</Badge>
</td>
<td className="px-6 py-5 text-sm text-gray-500 font-medium">
{format(
new Date(request.submissionDeadline),
"MMM dd, yyyy",
)}
</td>
</tr>
))
) : (
<tr>
<td
colSpan={4}
className="px-6 py-20 text-center text-gray-400 italic font-medium uppercase tracking-widest text-[10px]"
>
No proforma requests recorded.
</td>
</tr>
)}
</tbody>
</table>
</div>
</section>
{/* Pending Verification (Full Width) */}
<section className="space-y-4">
<h2 className="text-xl font-bold text-gray-900">
Pending Verification
</h2>
<div className="border overflow-hidden rounded-none">
<table className="w-full text-left">
<thead className="bg-gray-50 border-b">
<tr>
<th className="px-6 py-4 text-[10px] font-bold text-gray-500 uppercase tracking-widest">
Invoice Number
</th>
<th className="px-6 py-4 text-[10px] font-bold text-gray-500 uppercase tracking-widest">
Customer
</th>
<th className="px-6 py-4 text-[10px] font-bold text-gray-500 uppercase tracking-widest">
Amount
</th>
<th className="px-6 py-4 text-[10px] font-bold text-gray-500 uppercase tracking-widest">
Issue Date
</th>
</tr>
</thead>
<tbody className="bg-white divide-y">
{scannedInvoices && scannedInvoices.length > 0 ? (
scannedInvoices.map((invoice) => (
<tr
key={invoice.id}
className="hover:bg-gray-50 transition-colors"
>
<td className="px-6 py-5 text-sm font-bold text-gray-900">
{invoice.invoiceNumber}
</td>
<td className="px-6 py-5 text-sm text-gray-600">
{invoice.customerName}
</td>
<td className="px-6 py-5 text-sm font-bold text-gray-900">
{formatCurrency(invoice.amount)}
</td>
<td className="px-6 py-5 text-sm text-gray-500">
{new Date(invoice.issueDate).toLocaleDateString()}
</td>
</tr>
))
) : (
<tr>
<td
colSpan={4}
className="px-6 py-20 text-center text-gray-400 italic"
>
No invoices are currently pending verification.
</td>
</tr>
)}
</tbody>
</table>
</div>
</section>
</>
)}
</div>
);
}