export with one error
This commit is contained in:
@@ -7,7 +7,7 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { FileText, Plus, Search, Eye, AlertTriangle, CheckCircle, Clock, DollarSign, Edit } from "lucide-react";
|
||||
import { FileText, Plus, Search, Eye, AlertTriangle, CheckCircle, Clock, DollarSign, Edit, TrendingUp, TrendingDown, Calendar, ArrowUpRight, Sparkles, BarChart3, PieChart, MapPin, User } from "lucide-react";
|
||||
import { format, parseISO, isPast } from "date-fns";
|
||||
import PageHeader from "@/components/common/PageHeader";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
@@ -16,16 +16,18 @@ import AutoInvoiceGenerator from "@/components/invoices/AutoInvoiceGenerator";
|
||||
import CreateInvoiceModal from "@/components/invoices/CreateInvoiceModal";
|
||||
|
||||
const statusColors = {
|
||||
'Draft': 'bg-slate-500 text-white',
|
||||
'Pending Review': 'bg-amber-500 text-white',
|
||||
'Approved': 'bg-green-500 text-white',
|
||||
'Disputed': 'bg-red-500 text-white',
|
||||
'Under Review': 'bg-orange-500 text-white',
|
||||
'Resolved': 'bg-blue-500 text-white',
|
||||
'Overdue': 'bg-red-600 text-white',
|
||||
'Paid': 'bg-emerald-500 text-white',
|
||||
'Reconciled': 'bg-purple-500 text-white',
|
||||
'Cancelled': 'bg-slate-400 text-white',
|
||||
'Draft': 'bg-slate-100 text-slate-600 font-medium',
|
||||
'Open': 'bg-blue-100 text-blue-700 font-medium',
|
||||
'Pending Review': 'bg-blue-100 text-blue-700 font-medium',
|
||||
'Confirmed': 'bg-amber-100 text-amber-700 font-medium',
|
||||
'Approved': 'bg-emerald-100 text-emerald-700 font-medium',
|
||||
'Disputed': 'bg-red-100 text-red-700 font-medium',
|
||||
'Under Review': 'bg-orange-100 text-orange-700 font-medium',
|
||||
'Resolved': 'bg-cyan-100 text-cyan-700 font-medium',
|
||||
'Overdue': 'bg-red-100 text-red-700 font-medium',
|
||||
'Paid': 'bg-emerald-100 text-emerald-700 font-medium',
|
||||
'Reconciled': 'bg-purple-100 text-purple-700 font-medium',
|
||||
'Cancelled': 'bg-slate-100 text-slate-600 font-medium',
|
||||
};
|
||||
|
||||
export default function Invoices() {
|
||||
@@ -126,8 +128,86 @@ export default function Invoices() {
|
||||
disputed: getTotalAmount("Disputed"),
|
||||
overdue: getTotalAmount("Overdue"),
|
||||
paid: getTotalAmount("Paid"),
|
||||
outstanding: getTotalAmount("Pending Review") + getTotalAmount("Approved") + getTotalAmount("Overdue"),
|
||||
};
|
||||
|
||||
// Smart Insights
|
||||
const insights = React.useMemo(() => {
|
||||
const currentMonth = visibleInvoices.filter(inv => {
|
||||
const issueDate = parseISO(inv.issue_date);
|
||||
const now = new Date();
|
||||
return issueDate.getMonth() === now.getMonth() && issueDate.getFullYear() === now.getFullYear();
|
||||
});
|
||||
|
||||
const lastMonth = visibleInvoices.filter(inv => {
|
||||
const issueDate = parseISO(inv.issue_date);
|
||||
const now = new Date();
|
||||
const lastMonthDate = new Date(now.getFullYear(), now.getMonth() - 1);
|
||||
return issueDate.getMonth() === lastMonthDate.getMonth() && issueDate.getFullYear() === lastMonthDate.getFullYear();
|
||||
});
|
||||
|
||||
const currentTotal = currentMonth.reduce((sum, inv) => sum + (inv.amount || 0), 0);
|
||||
const lastTotal = lastMonth.reduce((sum, inv) => sum + (inv.amount || 0), 0);
|
||||
const percentChange = lastTotal > 0 ? ((currentTotal - lastTotal) / lastTotal * 100).toFixed(1) : 0;
|
||||
|
||||
const avgPaymentTime = visibleInvoices
|
||||
.filter(inv => inv.status === "Paid" && inv.paid_date && inv.issue_date)
|
||||
.map(inv => {
|
||||
const days = Math.floor((parseISO(inv.paid_date) - parseISO(inv.issue_date)) / (1000 * 60 * 60 * 24));
|
||||
return days;
|
||||
});
|
||||
const avgDays = avgPaymentTime.length > 0 ? Math.round(avgPaymentTime.reduce((a, b) => a + b, 0) / avgPaymentTime.length) : 0;
|
||||
|
||||
const onTimePayments = visibleInvoices.filter(inv =>
|
||||
inv.status === "Paid" && inv.paid_date && inv.due_date && parseISO(inv.paid_date) <= parseISO(inv.due_date)
|
||||
).length;
|
||||
const totalPaid = visibleInvoices.filter(inv => inv.status === "Paid").length;
|
||||
const onTimeRate = totalPaid > 0 ? ((onTimePayments / totalPaid) * 100).toFixed(0) : 0;
|
||||
|
||||
const topClient = Object.entries(
|
||||
visibleInvoices.reduce((acc, inv) => {
|
||||
const client = inv.business_name || "Unknown";
|
||||
acc[client] = (acc[client] || 0) + (inv.amount || 0);
|
||||
return acc;
|
||||
}, {})
|
||||
).sort((a, b) => b[1] - a[1])[0];
|
||||
|
||||
// For clients: calculate best hub by reconciliation rate
|
||||
const bestHub = userRole === "client" ? (() => {
|
||||
const hubStats = visibleInvoices.reduce((acc, inv) => {
|
||||
const hub = inv.hub || "Unknown";
|
||||
if (!acc[hub]) {
|
||||
acc[hub] = { total: 0, reconciled: 0, paid: 0 };
|
||||
}
|
||||
acc[hub].total++;
|
||||
if (inv.status === "Reconciled") acc[hub].reconciled++;
|
||||
if (inv.status === "Paid" || inv.status === "Reconciled") acc[hub].paid++;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const sortedHubs = Object.entries(hubStats)
|
||||
.map(([hub, stats]) => ({
|
||||
hub,
|
||||
rate: stats.total > 0 ? ((stats.paid / stats.total) * 100).toFixed(0) : 0,
|
||||
total: stats.total
|
||||
}))
|
||||
.sort((a, b) => b.rate - a.rate);
|
||||
|
||||
return sortedHubs[0] || null;
|
||||
})() : null;
|
||||
|
||||
return {
|
||||
percentChange,
|
||||
isGrowth: percentChange > 0,
|
||||
avgDays,
|
||||
onTimeRate,
|
||||
topClient: topClient ? { name: topClient[0], amount: topClient[1] } : null,
|
||||
bestHub,
|
||||
currentMonthCount: currentMonth.length,
|
||||
currentTotal,
|
||||
};
|
||||
}, [visibleInvoices, userRole]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AutoInvoiceGenerator />
|
||||
@@ -170,90 +250,190 @@ export default function Invoices() {
|
||||
|
||||
{/* Status Tabs */}
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="mb-6">
|
||||
<TabsList className="bg-white border border-slate-200 h-auto p-1 flex-wrap">
|
||||
<TabsTrigger value="all">
|
||||
All <Badge variant="secondary" className="ml-2">{getStatusCount("all")}</Badge>
|
||||
<TabsList className="bg-slate-100 border border-slate-200 h-auto p-1.5 flex-wrap gap-1">
|
||||
<TabsTrigger
|
||||
value="all"
|
||||
className="data-[state=active]:bg-blue-600 data-[state=active]:text-white bg-white text-slate-700 hover:bg-slate-50 transition-all rounded-md px-3 py-2"
|
||||
>
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
All
|
||||
<Badge className="ml-2 bg-yellow-400 text-yellow-900 hover:bg-yellow-400 border-0 font-bold">{getStatusCount("all")}</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="pending">
|
||||
Pending Review <Badge variant="secondary" className="ml-2">{getStatusCount("Pending Review")}</Badge>
|
||||
<TabsTrigger
|
||||
value="pending"
|
||||
className="data-[state=active]:bg-blue-600 data-[state=active]:text-white bg-white text-slate-700 hover:bg-slate-50 transition-all rounded-md px-3 py-2"
|
||||
>
|
||||
<Clock className="w-4 h-4 mr-2" />
|
||||
Pending
|
||||
<Badge className="ml-2 bg-yellow-400 text-yellow-900 hover:bg-yellow-400 border-0 font-bold">{getStatusCount("Pending Review")}</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="approved">
|
||||
Approved <Badge variant="secondary" className="ml-2">{getStatusCount("Approved")}</Badge>
|
||||
<TabsTrigger
|
||||
value="approved"
|
||||
className="data-[state=active]:bg-blue-600 data-[state=active]:text-white bg-white text-slate-700 hover:bg-slate-50 transition-all rounded-md px-3 py-2"
|
||||
>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
Approved
|
||||
<Badge className="ml-2 bg-yellow-400 text-yellow-900 hover:bg-yellow-400 border-0 font-bold">{getStatusCount("Approved")}</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="disputed">
|
||||
Disputed <Badge variant="secondary" className="ml-2">{getStatusCount("Disputed")}</Badge>
|
||||
<TabsTrigger
|
||||
value="disputed"
|
||||
className="data-[state=active]:bg-blue-600 data-[state=active]:text-white bg-white text-slate-700 hover:bg-slate-50 transition-all rounded-md px-3 py-2"
|
||||
>
|
||||
<AlertTriangle className="w-4 h-4 mr-2" />
|
||||
Disputed
|
||||
<Badge className="ml-2 bg-yellow-400 text-yellow-900 hover:bg-yellow-400 border-0 font-bold">{getStatusCount("Disputed")}</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="overdue">
|
||||
Overdue <Badge variant="secondary" className="ml-2">{getStatusCount("Overdue")}</Badge>
|
||||
<TabsTrigger
|
||||
value="overdue"
|
||||
className="data-[state=active]:bg-blue-600 data-[state=active]:text-white bg-white text-slate-700 hover:bg-slate-50 transition-all rounded-md px-3 py-2"
|
||||
>
|
||||
<AlertTriangle className="w-4 h-4 mr-2" />
|
||||
Overdue
|
||||
<Badge className="ml-2 bg-yellow-400 text-yellow-900 hover:bg-yellow-400 border-0 font-bold">{getStatusCount("Overdue")}</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="paid">
|
||||
Paid <Badge variant="secondary" className="ml-2">{getStatusCount("Paid")}</Badge>
|
||||
<TabsTrigger
|
||||
value="paid"
|
||||
className="data-[state=active]:bg-blue-600 data-[state=active]:text-white bg-white text-slate-700 hover:bg-slate-50 transition-all rounded-md px-3 py-2"
|
||||
>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
Paid
|
||||
<Badge className="ml-2 bg-yellow-400 text-yellow-900 hover:bg-yellow-400 border-0 font-bold">{getStatusCount("Paid")}</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="reconciled">
|
||||
Reconciled <Badge variant="secondary" className="ml-2">{getStatusCount("Reconciled")}</Badge>
|
||||
<TabsTrigger
|
||||
value="reconciled"
|
||||
className="data-[state=active]:bg-blue-600 data-[state=active]:text-white bg-white text-slate-700 hover:bg-slate-50 transition-all rounded-md px-3 py-2"
|
||||
>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
Reconciled
|
||||
<Badge className="ml-2 bg-yellow-400 text-yellow-900 hover:bg-yellow-400 border-0 font-bold">{getStatusCount("Reconciled")}</Badge>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
{/* Metric Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<Card className="border-slate-200">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center">
|
||||
<FileText className="w-6 h-6 text-blue-600" />
|
||||
<Card className="border-0 bg-blue-50 shadow-sm hover:shadow-md transition-all">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||
<FileText className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-slate-500">Total</p>
|
||||
<p className="text-2xl font-bold text-slate-900">${metrics.all.toLocaleString()}</p>
|
||||
<p className="text-xs text-blue-600 uppercase tracking-wider font-semibold mb-0.5">Total Value</p>
|
||||
<p className="text-2xl font-bold text-blue-700">${metrics.all.toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-slate-200">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 bg-amber-100 rounded-lg flex items-center justify-center">
|
||||
<Clock className="w-6 h-6 text-amber-600" />
|
||||
<Card className="border-0 bg-amber-50 shadow-sm hover:shadow-md transition-all">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-amber-500 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||
<DollarSign className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-slate-500">Pending</p>
|
||||
<p className="text-2xl font-bold text-amber-600">${metrics.pending.toLocaleString()}</p>
|
||||
<p className="text-xs text-amber-600 uppercase tracking-wider font-semibold mb-0.5">Outstanding</p>
|
||||
<p className="text-2xl font-bold text-amber-700">${metrics.outstanding.toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-slate-200">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 bg-red-100 rounded-lg flex items-center justify-center">
|
||||
<AlertTriangle className="w-6 h-6 text-red-600" />
|
||||
<Card className="border-0 bg-red-50 shadow-sm hover:shadow-md transition-all">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-red-500 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||
<AlertTriangle className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-slate-500">Overdue</p>
|
||||
<p className="text-2xl font-bold text-red-600">${metrics.overdue.toLocaleString()}</p>
|
||||
<p className="text-xs text-red-600 uppercase tracking-wider font-semibold mb-0.5">Disputed</p>
|
||||
<p className="text-2xl font-bold text-red-700">${metrics.disputed.toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-slate-200">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center">
|
||||
<CheckCircle className="w-6 h-6 text-green-600" />
|
||||
<Card className="border-0 bg-emerald-50 shadow-sm hover:shadow-md transition-all">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-emerald-500 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||
<CheckCircle className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-slate-500">Paid</p>
|
||||
<p className="text-2xl font-bold text-green-600">${metrics.paid.toLocaleString()}</p>
|
||||
<p className="text-xs text-emerald-600 uppercase tracking-wider font-semibold mb-0.5">Paid</p>
|
||||
<p className="text-2xl font-bold text-emerald-700">${metrics.paid.toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Smart Insights Banner */}
|
||||
<div className="mb-6 bg-slate-100 rounded-2xl p-6 shadow-sm border border-slate-200">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 bg-amber-500 rounded-xl flex items-center justify-center">
|
||||
<Sparkles className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-slate-900">Smart Insights</h3>
|
||||
<p className="text-sm text-slate-500">AI-powered analysis of your invoice performance</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="bg-white rounded-xl p-4 border border-slate-200">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-slate-500">This Month</span>
|
||||
<div className={`flex items-center gap-1 ${insights.isGrowth ? 'text-emerald-600' : 'text-red-600'}`}>
|
||||
{insights.isGrowth ? <TrendingUp className="w-4 h-4" /> : <TrendingDown className="w-4 h-4" />}
|
||||
<span className="text-xs font-bold">{insights.percentChange}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-slate-900">${insights.currentTotal.toLocaleString()}</p>
|
||||
<p className="text-xs text-slate-400 mt-1">{insights.currentMonthCount} invoices</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl p-4 border border-slate-200">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-slate-500">Avg. Payment Time</span>
|
||||
<Calendar className="w-4 h-4 text-slate-400" />
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-slate-900">{insights.avgDays} days</p>
|
||||
<p className="text-xs text-slate-400 mt-1">From issue to payment</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl p-4 border border-slate-200">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-slate-500">On-Time Rate</span>
|
||||
<CheckCircle className="w-4 h-4 text-slate-400" />
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-slate-900">{insights.onTimeRate}%</p>
|
||||
<p className="text-xs text-slate-400 mt-1">Paid before due date</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl p-4 border border-slate-200">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-slate-500">
|
||||
{userRole === "client" ? "Best Hub" : "Top Client"}
|
||||
</span>
|
||||
<ArrowUpRight className="w-4 h-4 text-slate-400" />
|
||||
</div>
|
||||
{userRole === "client" ? (
|
||||
<>
|
||||
<p className="text-lg font-bold text-slate-900 truncate">{insights.bestHub?.hub || "—"}</p>
|
||||
<p className="text-xs text-slate-400 mt-1">{insights.bestHub?.rate || 0}% on-time</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-lg font-bold text-slate-900 truncate">{insights.topClient?.name || "—"}</p>
|
||||
<p className="text-xs text-slate-400 mt-1">${insights.topClient?.amount.toLocaleString() || 0}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="bg-white rounded-lg p-4 mb-6 border border-slate-200">
|
||||
<div className="relative">
|
||||
@@ -268,72 +448,87 @@ export default function Invoices() {
|
||||
</div>
|
||||
|
||||
{/* Invoices Table */}
|
||||
<Card className="border-slate-200">
|
||||
<Card className="border-slate-200 shadow-lg">
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-slate-50 hover:bg-slate-50">
|
||||
<TableHead>Invoice #</TableHead>
|
||||
<TableHead>Client</TableHead>
|
||||
<TableHead>Event</TableHead>
|
||||
<TableHead>Vendor</TableHead>
|
||||
<TableHead>Issue Date</TableHead>
|
||||
<TableHead>Due Date</TableHead>
|
||||
<TableHead className="text-right">Amount</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
<TableHead className="text-slate-600 font-semibold uppercase text-xs">Invoice #</TableHead>
|
||||
<TableHead className="text-slate-600 font-semibold uppercase text-xs">Hub</TableHead>
|
||||
<TableHead className="text-slate-600 font-semibold uppercase text-xs">Event</TableHead>
|
||||
<TableHead className="text-slate-600 font-semibold uppercase text-xs">Manager</TableHead>
|
||||
<TableHead className="text-slate-600 font-semibold uppercase text-xs">Date & Time</TableHead>
|
||||
<TableHead className="text-slate-600 font-semibold uppercase text-xs">Amount</TableHead>
|
||||
<TableHead className="text-slate-600 font-semibold uppercase text-xs">Status</TableHead>
|
||||
<TableHead className="text-slate-600 font-semibold uppercase text-xs">Action</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredInvoices.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} className="text-center py-12 text-slate-500">
|
||||
<TableCell colSpan={8} className="text-center py-12 text-slate-500">
|
||||
<FileText className="w-12 h-12 mx-auto mb-3 text-slate-300" />
|
||||
<p className="font-medium">No invoices found</p>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredInvoices.map((invoice) => (
|
||||
<TableRow key={invoice.id} className="hover:bg-slate-50">
|
||||
<TableCell className="font-semibold">{invoice.invoice_number}</TableCell>
|
||||
<TableCell>{invoice.business_name}</TableCell>
|
||||
<TableCell>{invoice.event_name}</TableCell>
|
||||
<TableCell>{invoice.vendor_name || "—"}</TableCell>
|
||||
<TableCell>{format(parseISO(invoice.issue_date), 'MMM dd, yyyy')}</TableCell>
|
||||
<TableCell className={isPast(parseISO(invoice.due_date)) && invoice.status !== "Paid" ? "text-red-600 font-semibold" : ""}>
|
||||
{format(parseISO(invoice.due_date), 'MMM dd, yyyy')}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-bold">${invoice.amount?.toLocaleString()}</TableCell>
|
||||
<TableCell>
|
||||
<Badge className={statusColors[invoice.status]}>
|
||||
{invoice.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
filteredInvoices.map((invoice) => {
|
||||
const invoiceDate = parseISO(invoice.issue_date);
|
||||
const dayOfWeek = format(invoiceDate, 'EEEE');
|
||||
const dateFormatted = format(invoiceDate, 'MM.dd.yy');
|
||||
|
||||
return (
|
||||
<TableRow key={invoice.id} className="hover:bg-slate-50 transition-all border-b border-slate-100">
|
||||
<TableCell className="font-bold text-slate-900">{invoice.invoice_number}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="w-4 h-4 text-purple-600" />
|
||||
<span className="text-slate-900 font-medium">{invoice.hub || "—"}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-slate-900 font-medium">{invoice.event_name}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="w-4 h-4 text-slate-400" />
|
||||
<span className="text-slate-700">{invoice.manager_name || invoice.created_by || "—"}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="space-y-0.5">
|
||||
<div className="text-slate-900 font-medium">{dateFormatted}</div>
|
||||
<div className="flex items-center gap-1.5 text-xs text-slate-500">
|
||||
<Clock className="w-3 h-3" />
|
||||
<span>{dayOfWeek}</span>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-5 h-5 bg-green-500 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<DollarSign className="w-3 h-3 text-white" />
|
||||
</div>
|
||||
<span className="font-bold text-slate-900">${invoice.amount?.toLocaleString()}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge className={`${statusColors[invoice.status]} px-3 py-1 rounded-md text-xs`}>
|
||||
{invoice.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => navigate(createPageUrl(`InvoiceDetail?id=${invoice.id}`))}
|
||||
className="font-semibold"
|
||||
className="font-semibold hover:bg-blue-50 hover:text-[#0A39DF]"
|
||||
>
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
View
|
||||
</Button>
|
||||
{userRole === "vendor" && invoice.status === "Draft" && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => navigate(createPageUrl(`InvoiceEditor?id=${invoice.id}`))}
|
||||
className="font-semibold text-blue-600"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
Reference in New Issue
Block a user