other modifications days ago
This commit is contained in:
@@ -13,7 +13,7 @@ import PageHeader from "@/components/common/PageHeader";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { createPageUrl } from "@/utils";
|
||||
import AutoInvoiceGenerator from "@/components/invoices/AutoInvoiceGenerator";
|
||||
import CreateInvoiceModal from "@/components/invoices/CreateInvoiceModal";
|
||||
|
||||
|
||||
const statusColors = {
|
||||
'Draft': 'bg-slate-100 text-slate-600 font-medium',
|
||||
@@ -34,7 +34,7 @@ export default function Invoices() {
|
||||
const navigate = useNavigate();
|
||||
const [activeTab, setActiveTab] = useState("all");
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
|
||||
|
||||
const { data: user } = useQuery({
|
||||
queryKey: ['current-user-invoices'],
|
||||
@@ -212,335 +212,327 @@ export default function Invoices() {
|
||||
<>
|
||||
<AutoInvoiceGenerator />
|
||||
|
||||
<div className="p-4 md:p-8 bg-slate-50 min-h-screen">
|
||||
<div className="max-w-[1600px] mx-auto">
|
||||
<PageHeader
|
||||
title="Invoices"
|
||||
subtitle={`${filteredInvoices.length} invoices • $${metrics.all.toLocaleString()} total`}
|
||||
actions={
|
||||
userRole === "vendor" && (
|
||||
<Button onClick={() => setShowCreateModal(true)} className="bg-[#0A39DF] hover:bg-[#0A39DF]/90">
|
||||
<Plus className="w-5 h-5 mr-2" />
|
||||
Create Invoice
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div className="p-4 md:p-6 bg-slate-100 min-h-screen">
|
||||
<div className="max-w-[1800px] mx-auto">
|
||||
<div className="flex gap-6">
|
||||
{/* Left Sidebar - Summary */}
|
||||
<div className="hidden lg:block w-72 flex-shrink-0 space-y-4">
|
||||
{/* Logo Card */}
|
||||
<Card className="border-0 bg-gradient-to-br from-[#0A39DF] to-[#1C323E] text-white overflow-hidden">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 bg-white/20 rounded-lg flex items-center justify-center">
|
||||
<FileText className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="font-bold">Invoices</h2>
|
||||
<p className="text-xs text-white/70">{visibleInvoices.length} total</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm text-white/70">Total Value</span>
|
||||
<span className="font-bold text-lg">${metrics.all.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="h-2 bg-white/20 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-emerald-400 rounded-full" style={{ width: `${(metrics.paid / metrics.all) * 100 || 0}%` }} />
|
||||
</div>
|
||||
<p className="text-xs text-white/60">{((metrics.paid / metrics.all) * 100 || 0).toFixed(0)}% collected</p>
|
||||
</div>
|
||||
{userRole === "vendor" && (
|
||||
<Button onClick={() => navigate(createPageUrl("InvoiceEditor"))} size="sm" className="w-full mt-4 bg-white text-[#0A39DF] hover:bg-white/90">
|
||||
<Plus className="w-4 h-4 mr-1" /> New Invoice
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Alert Banners */}
|
||||
{metrics.disputed > 0 && (
|
||||
<div className="mb-6 p-4 bg-red-50 border-l-4 border-red-500 rounded-lg flex items-center gap-3">
|
||||
<AlertTriangle className="w-5 h-5 text-red-600" />
|
||||
<div>
|
||||
<p className="font-semibold text-red-900">Disputed Invoices Require Attention</p>
|
||||
<p className="text-sm text-red-700">{getStatusCount("Disputed")} invoices are currently disputed</p>
|
||||
</div>
|
||||
{/* Status Breakdown */}
|
||||
<Card className="border-0 shadow-sm">
|
||||
<CardContent className="p-4">
|
||||
<h3 className="font-semibold text-slate-900 mb-3 text-sm">Status Breakdown</h3>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{ label: "Pending", status: "Pending Review", color: "bg-blue-500", value: getStatusCount("Pending Review"), amount: getTotalAmount("Pending Review") },
|
||||
{ label: "Approved", status: "Approved", color: "bg-emerald-500", value: getStatusCount("Approved"), amount: getTotalAmount("Approved") },
|
||||
{ label: "Disputed", status: "Disputed", color: "bg-red-500", value: getStatusCount("Disputed"), amount: getTotalAmount("Disputed") },
|
||||
{ label: "Overdue", status: "Overdue", color: "bg-amber-500", value: getStatusCount("Overdue"), amount: getTotalAmount("Overdue") },
|
||||
{ label: "Paid", status: "Paid", color: "bg-green-500", value: getStatusCount("Paid"), amount: getTotalAmount("Paid") },
|
||||
{ label: "Reconciled", status: "Reconciled", color: "bg-purple-500", value: getStatusCount("Reconciled"), amount: getTotalAmount("Reconciled") },
|
||||
].map(item => (
|
||||
<button
|
||||
key={item.status}
|
||||
onClick={() => setActiveTab(item.label.toLowerCase())}
|
||||
className={`w-full flex items-center gap-3 p-2 rounded-lg transition-all hover:bg-slate-50 ${activeTab === item.label.toLowerCase() ? 'bg-slate-100' : ''}`}
|
||||
>
|
||||
<div className={`w-2 h-2 rounded-full ${item.color}`} />
|
||||
<span className="text-sm text-slate-700 flex-1 text-left">{item.label}</span>
|
||||
<span className="text-xs text-slate-500 mr-1">${item.amount.toLocaleString()}</span>
|
||||
<Badge variant="outline" className="text-xs">{item.value}</Badge>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Sales & Revenue - Vendor Focused */}
|
||||
<Card className="border-0 shadow-sm bg-emerald-50">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<TrendingUp className="w-4 h-4 text-emerald-600" />
|
||||
<h3 className="font-semibold text-emerald-900 text-sm">Sales & Revenue</h3>
|
||||
</div>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-emerald-700">Total Sales</span>
|
||||
<span className="font-semibold text-emerald-900">${metrics.all.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-emerald-700">Collected</span>
|
||||
<span className="font-semibold text-emerald-900">${metrics.paid.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-emerald-700">Pending Revenue</span>
|
||||
<span className="font-semibold text-emerald-900">${metrics.outstanding.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-emerald-700">Collection Rate</span>
|
||||
<span className="font-semibold text-emerald-900">{((metrics.paid / metrics.all) * 100 || 0).toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-emerald-700">Avg. Invoice</span>
|
||||
<span className="font-semibold text-emerald-900">${visibleInvoices.length > 0 ? Math.round(metrics.all / visibleInvoices.length).toLocaleString() : 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Quick Insights */}
|
||||
<Card className="border-0 shadow-sm bg-amber-50">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Sparkles className="w-4 h-4 text-amber-600" />
|
||||
<h3 className="font-semibold text-amber-900 text-sm">Performance</h3>
|
||||
</div>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-amber-700">Avg. Payment</span>
|
||||
<span className="font-semibold text-amber-900">{insights.avgDays} days</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-amber-700">On-Time Rate</span>
|
||||
<span className="font-semibold text-amber-900">{insights.onTimeRate}%</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-amber-700">This Month</span>
|
||||
<span className="font-semibold text-amber-900">${insights.currentTotal.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-amber-700">Monthly Count</span>
|
||||
<span className="font-semibold text-amber-900">{insights.currentMonthCount} invoices</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-amber-700">MoM Change</span>
|
||||
<span className={`font-semibold ${insights.isGrowth ? 'text-emerald-700' : 'text-red-700'}`}>
|
||||
{insights.isGrowth ? '+' : ''}{insights.percentChange}%
|
||||
</span>
|
||||
</div>
|
||||
{insights.topClient && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-amber-700">Top Client</span>
|
||||
<span className="font-semibold text-amber-900 truncate max-w-[100px]" title={insights.topClient.name}>
|
||||
{insights.topClient.name}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Alerts */}
|
||||
{(metrics.disputed > 0 || metrics.overdue > 0) && (
|
||||
<Card className="border-0 shadow-sm border-l-4 border-l-red-500 bg-red-50">
|
||||
<CardContent className="p-4">
|
||||
<h3 className="font-semibold text-red-900 text-sm mb-2">Requires Attention</h3>
|
||||
{metrics.disputed > 0 && (
|
||||
<p className="text-xs text-red-700 mb-1">• {getStatusCount("Disputed")} disputed (${metrics.disputed.toLocaleString()})</p>
|
||||
)}
|
||||
{metrics.overdue > 0 && (
|
||||
<p className="text-xs text-red-700">• {getStatusCount("Overdue")} overdue (${metrics.overdue.toLocaleString()})</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{metrics.overdue > 0 && userRole === "client" && (
|
||||
<div className="mb-6 p-4 bg-amber-50 border-l-4 border-amber-500 rounded-lg flex items-center gap-3">
|
||||
<Clock className="w-5 h-5 text-amber-600" />
|
||||
<div>
|
||||
<p className="font-semibold text-amber-900">Overdue Payments</p>
|
||||
<p className="text-sm text-amber-700">${metrics.overdue.toLocaleString()} in overdue invoices</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status Tabs */}
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="mb-6">
|
||||
<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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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-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-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>
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* Top Bar */}
|
||||
<div className="bg-white rounded-xl shadow-sm mb-4 p-3 flex items-center gap-3 flex-wrap">
|
||||
<div className="relative flex-1 min-w-[200px] max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<Input
|
||||
placeholder="Search by invoice #, client, event..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-9 h-10 bg-slate-50 border-0"
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<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-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 className="lg:hidden">
|
||||
{userRole === "vendor" && (
|
||||
<Button onClick={() => navigate(createPageUrl("InvoiceEditor"))} size="sm" className="bg-[#0A39DF]">
|
||||
<Plus className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<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-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-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-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>
|
||||
{/* Stats Row - Mobile */}
|
||||
<div className="lg:hidden grid grid-cols-4 gap-2 mb-4">
|
||||
<Card className="border-0 shadow-sm">
|
||||
<CardContent className="p-3 text-center">
|
||||
<p className="text-lg font-bold text-slate-900">${(metrics.all / 1000).toFixed(0)}K</p>
|
||||
<p className="text-[10px] text-slate-500">Total</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-0 shadow-sm">
|
||||
<CardContent className="p-3 text-center">
|
||||
<p className="text-lg font-bold text-amber-600">${(metrics.outstanding / 1000).toFixed(0)}K</p>
|
||||
<p className="text-[10px] text-slate-500">Pending</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-0 shadow-sm">
|
||||
<CardContent className="p-3 text-center">
|
||||
<p className="text-lg font-bold text-red-600">{getStatusCount("Disputed")}</p>
|
||||
<p className="text-[10px] text-slate-500">Disputed</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="border-0 shadow-sm">
|
||||
<CardContent className="p-3 text-center">
|
||||
<p className="text-lg font-bold text-emerald-600">${(metrics.paid / 1000).toFixed(0)}K</p>
|
||||
<p className="text-[10px] text-slate-500">Paid</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</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">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-slate-400" />
|
||||
<Input
|
||||
placeholder="Search by invoice number, client, event..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Invoices Table */}
|
||||
<Card className="border-slate-200 shadow-lg">
|
||||
<CardContent className="p-0">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-slate-50 hover:bg-slate-50">
|
||||
<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={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) => {
|
||||
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 hover:bg-blue-50 hover:text-[#0A39DF]"
|
||||
>
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
View
|
||||
</Button>
|
||||
{/* Invoice Table */}
|
||||
<Card className="border-0 shadow-sm overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-slate-50 hover:bg-slate-50 border-b border-slate-200">
|
||||
<TableHead className="font-semibold text-slate-700 text-xs uppercase tracking-wide py-4">Invoice</TableHead>
|
||||
<TableHead className="font-semibold text-slate-700 text-xs uppercase tracking-wide">Client</TableHead>
|
||||
<TableHead className="font-semibold text-slate-700 text-xs uppercase tracking-wide">Event / Hub</TableHead>
|
||||
<TableHead className="font-semibold text-slate-700 text-xs uppercase tracking-wide">Date</TableHead>
|
||||
<TableHead className="font-semibold text-slate-700 text-xs uppercase tracking-wide text-right">Amount</TableHead>
|
||||
<TableHead className="font-semibold text-slate-700 text-xs uppercase tracking-wide text-center">Status</TableHead>
|
||||
<TableHead className="font-semibold text-slate-700 text-xs uppercase tracking-wide text-center">Due</TableHead>
|
||||
<TableHead className="font-semibold text-slate-700 text-xs uppercase tracking-wide text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredInvoices.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center py-16">
|
||||
<FileText className="w-12 h-12 mx-auto mb-3 text-slate-200" />
|
||||
<p className="font-medium text-slate-500">No invoices found</p>
|
||||
<p className="text-sm text-slate-400 mt-1">Try adjusting your search or filters</p>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
filteredInvoices.map((invoice) => {
|
||||
const invoiceDate = invoice.issue_date ? parseISO(invoice.issue_date) : new Date();
|
||||
const dateFormatted = format(invoiceDate, 'MMM d, yyyy');
|
||||
const dueDate = invoice.due_date ? format(parseISO(invoice.due_date), 'MMM d') : '—';
|
||||
const isOverdue = invoice.due_date && isPast(parseISO(invoice.due_date)) && invoice.status !== "Paid" && invoice.status !== "Reconciled";
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={invoice.id}
|
||||
className={`hover:bg-blue-50/50 cursor-pointer transition-all border-b border-slate-100 ${isOverdue ? 'bg-red-50/30' : ''}`}
|
||||
onClick={() => navigate(createPageUrl(`InvoiceDetail?id=${invoice.id}`))}
|
||||
>
|
||||
<TableCell className="py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-9 h-9 rounded-lg flex items-center justify-center flex-shrink-0 ${
|
||||
invoice.status === 'Paid' || invoice.status === 'Reconciled' ? 'bg-emerald-100' :
|
||||
invoice.status === 'Disputed' ? 'bg-red-100' :
|
||||
invoice.status === 'Overdue' ? 'bg-amber-100' : 'bg-blue-100'
|
||||
}`}>
|
||||
<FileText className={`w-4 h-4 ${
|
||||
invoice.status === 'Paid' || invoice.status === 'Reconciled' ? 'text-emerald-600' :
|
||||
invoice.status === 'Disputed' ? 'text-red-600' :
|
||||
invoice.status === 'Overdue' ? 'text-amber-600' : 'text-blue-600'
|
||||
}`} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-bold text-slate-900">{invoice.invoice_number}</p>
|
||||
<p className="text-xs text-slate-500">{invoice.manager_name || '—'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<p className="font-medium text-slate-900 truncate max-w-[150px]">{invoice.business_name || '—'}</p>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<p className="font-medium text-slate-900 truncate max-w-[200px]">{invoice.event_name || 'Untitled'}</p>
|
||||
<div className="flex items-center gap-1 mt-0.5">
|
||||
<MapPin className="w-3 h-3 text-slate-400" />
|
||||
<span className="text-xs text-slate-500">{invoice.hub || '—'}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-slate-700">{dateFormatted}</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<span className="font-bold text-slate-900 text-lg">${invoice.amount?.toLocaleString() || 0}</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge className={`${statusColors[invoice.status]} px-2.5 py-1`}>
|
||||
{invoice.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<span className={`text-sm font-medium ${isOverdue ? 'text-red-600' : 'text-slate-600'}`}>
|
||||
{dueDate}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button size="sm" variant="ghost" className="hover:bg-blue-100 hover:text-blue-700">
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" className="hover:bg-slate-100">
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* Table Footer */}
|
||||
{filteredInvoices.length > 0 && (
|
||||
<div className="p-4 border-t border-slate-100 bg-slate-50 flex items-center justify-between">
|
||||
<p className="text-sm text-slate-500">
|
||||
Showing <span className="font-medium text-slate-700">{filteredInvoices.length}</span> of <span className="font-medium text-slate-700">{visibleInvoices.length}</span> invoices
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-slate-500">Total: </span>
|
||||
<span className="font-bold text-slate-900">${filteredInvoices.reduce((sum, inv) => sum + (inv.amount || 0), 0).toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CreateInvoiceModal
|
||||
open={showCreateModal}
|
||||
onClose={() => setShowCreateModal(false)}
|
||||
/>
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user