feat: Implement detail view for specific invoice

This commit is contained in:
dhinesh-m24
2026-02-09 16:44:00 +05:30
parent e25b37b39b
commit cad0662478
2 changed files with 284 additions and 1 deletions

View File

@@ -0,0 +1,281 @@
import { Badge } from "@/common/components/ui/badge";
import { Button } from "@/common/components/ui/button";
import { Card, CardContent } from "@/common/components/ui/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/common/components/ui/table";
import { InvoiceStatus } from "@/dataconnect-generated";
import { useGetInvoiceById, useUpdateInvoice, useListRecentPaymentsByInvoiceId } from "@/dataconnect-generated/react";
import { dataConnect } from "@/features/auth/firebase";
import DashboardLayout from "@/features/layouts/DashboardLayout";
import type { RootState } from "@/store/store";
import { format, parseISO } from "date-fns";
import { ArrowLeft, Download, Mail, CheckCircle, FileText, User, Calendar, MapPin, DollarSign } from "lucide-react";
import { useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
const statusConfig: Record<string, { label: string; className: string }> = {
DRAFT: { label: "Draft", className: "bg-slate-100 text-slate-600 border-transparent" },
PENDING: { label: "Sent", className: "bg-blue-50 text-blue-700 border-blue-200" },
PENDING_REVIEW: { label: "Pending Review", className: "bg-amber-50 text-amber-700 border-amber-200" },
APPROVED: { label: "Approved", className: "bg-emerald-50 text-emerald-700 border-emerald-200" },
DISPUTED: { label: "Disputed", className: "bg-red-50 text-red-700 border-red-200" },
OVERDUE: { label: "Overdue", className: "bg-rose-50 text-rose-700 border-rose-200" },
PAID: { label: "Paid", className: "bg-emerald-50 text-emerald-700 border-emerald-200" },
};
export default function InvoiceDetail() {
const navigate = useNavigate();
const { id: invoiceId } = useParams<{ id: string }>();
const { user } = useSelector((state: RootState) => state.auth);
// Fetch Invoice Data
const { data: invoiceData, isLoading: loadingInvoice } = useGetInvoiceById(dataConnect, { id: invoiceId! });
const invoice = invoiceData?.invoice;
// Fetch Payment History
const { data: paymentsData, isLoading: loadingPayments } = useListRecentPaymentsByInvoiceId(dataConnect, { invoiceId: invoiceId! });
const payments = paymentsData?.recentPayments || [];
// Mutations
const { mutate: updateInvoice } = useUpdateInvoice(dataConnect);
const handleMarkAsPaid = () => {
if (!invoiceId) return;
updateInvoice({ id: invoiceId, status: InvoiceStatus.PAID });
};
const handleSendInvoice = () => {
// Logic for sending invoice (e.g., email service)
console.log("Sending invoice...");
updateInvoice({ id: invoiceId!, status: InvoiceStatus.PENDING });
};
const handleDownloadPDF = () => {
window.print();
};
if (loadingInvoice) {
return (
<DashboardLayout title="Invoice Detail">
<div className="flex items-center justify-center min-h-[50vh]">
<div className="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin"></div>
</div>
</DashboardLayout>
);
}
if (!invoice) {
return (
<DashboardLayout title="Invoice Not Found">
<div className="flex flex-col items-center justify-center min-h-[50vh] gap-4">
<p className="text-xl font-semibold">Invoice not found</p>
<Button onClick={() => navigate("/invoices")} leadingIcon={<ArrowLeft />}>
Back to Invoices
</Button>
</div>
</DashboardLayout>
);
}
const status = statusConfig[invoice.status] || { label: invoice.status, className: "" };
const issueDate = invoice.issueDate ? format(parseISO(invoice.issueDate as string), "MMM d, yyyy") : "—";
const dueDate = invoice.dueDate ? format(parseISO(invoice.dueDate as string), "MMM d, yyyy") : "—";
// Parse JSON data for roles/charges (assuming they are JSON from the schema)
const lineItems = (invoice.roles as any[]) || [];
return (
<DashboardLayout
title={`Invoice ${invoice.invoiceNumber}`}
backAction={
<Button variant="ghost" onClick={() => navigate("/invoices")} leadingIcon={<ArrowLeft />}>
Back
</Button>
}
actions={
<div className="flex gap-2 print:hidden">
<Button variant="outline" leadingIcon={<Download />} onClick={handleDownloadPDF}>
Download PDF
</Button>
{invoice.status === "DRAFT" && (
<Button leadingIcon={<Mail />} onClick={handleSendInvoice}>
Send Invoice
</Button>
)}
{invoice.status !== "PAID" && (
<Button variant="default" leadingIcon={<CheckCircle />} onClick={handleMarkAsPaid} className="bg-emerald-600 hover:bg-emerald-700">
Mark as Paid
</Button>
)}
</div>
}
>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
{/* Header & Client Info */}
<Card className="border-border/50 shadow-sm">
<CardContent className="p-6">
<div className="flex flex-col md:flex-row justify-between gap-6">
<div className="space-y-4">
<div>
<h3 className="text-xs font-bold text-muted-foreground uppercase tracking-wider mb-2">Client Information</h3>
<div className="flex items-start gap-3">
<div className="w-10 h-10 bg-primary/10 rounded-lg flex items-center justify-center text-primary">
<User className="w-5 h-5" />
</div>
<div>
<p className="font-bold text-lg">{invoice.business?.businessName}</p>
<p className="text-sm text-secondary-text">{invoice.business?.email}</p>
<p className="text-sm text-secondary-text">{invoice.business?.phone}</p>
</div>
</div>
</div>
<div className="flex items-center gap-2 text-sm text-secondary-text">
<MapPin className="w-4 h-4" />
<span>{invoice.hub || (invoice.order as any)?.teamHub?.hubName}</span>
</div>
</div>
<div className="bg-muted/30 p-4 rounded-xl space-y-3 min-w-[240px]">
<div className="flex justify-between items-center border-b border-border/50 pb-2">
<span className="text-xs font-medium text-secondary-text">Status</span>
<Badge className={`${status.className} font-bold`}>{status.label}</Badge>
</div>
<div className="flex justify-between items-center text-sm">
<span className="text-secondary-text flex items-center gap-1.5"><Calendar className="w-3.5 h-3.5" /> Issued</span>
<span className="font-medium">{issueDate}</span>
</div>
<div className="flex justify-between items-center text-sm">
<span className="text-secondary-text flex items-center gap-1.5"><Calendar className="w-3.5 h-3.5" /> Due Date</span>
<span className="font-medium">{dueDate}</span>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Line Items Table */}
<Card className="border-border/50 shadow-sm overflow-hidden">
<div className="p-4 border-b border-border/50 bg-muted/20">
<h3 className="font-bold text-sm">Line Items (Shifts & Staff)</h3>
</div>
<Table>
<TableHeader>
<TableRow className="bg-muted/10">
<TableHead className="text-[10px] font-bold uppercase py-3">Staff / Role</TableHead>
<TableHead className="text-[10px] font-bold uppercase text-center">Hours</TableHead>
<TableHead className="text-[10px] font-bold uppercase text-right">Rate</TableHead>
<TableHead className="text-[10px] font-bold uppercase text-right">Total</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{lineItems.length === 0 ? (
<TableRow>
<TableCell colSpan={4} className="text-center py-8 text-muted-foreground text-sm">
No line items recorded for this invoice.
</TableCell>
</TableRow>
) : (
lineItems.map((item: any, idx: number) => (
<TableRow key={idx} className="border-b border-border/40">
<TableCell className="py-4">
<p className="font-bold text-xs">{item.staffName || "Staff Member"}</p>
<p className="text-[10px] text-muted-foreground">{item.roleName || "Support"}</p>
</TableCell>
<TableCell className="text-center text-xs">{item.hours || 0}h</TableCell>
<TableCell className="text-right text-xs">${(item.rate || 0).toFixed(2)}</TableCell>
<TableCell className="text-right font-bold text-xs">${(item.total || 0).toFixed(2)}</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</Card>
{/* Payment History */}
<Card className="border-border/50 shadow-sm overflow-hidden">
<div className="p-4 border-b border-border/50 bg-muted/20">
<h3 className="font-bold text-sm">Payment History</h3>
</div>
<div className="p-0">
{payments.length === 0 ? (
<div className="p-8 text-center text-muted-foreground text-sm">
No payment records found.
</div>
) : (
<Table>
<TableBody>
{payments.map((payment) => (
<TableRow key={payment.id}>
<TableCell className="py-4">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-emerald-50 text-emerald-600 flex items-center justify-center">
<DollarSign className="w-4 h-4" />
</div>
<div>
<p className="font-bold text-xs">Payment Received</p>
<p className="text-[10px] text-muted-foreground">
{payment.createdAt ? format(parseISO(payment.createdAt as string), "MMM d, yyyy") : "—"}
</p>
</div>
</div>
</TableCell>
<TableCell className="text-right">
<Badge variant="outline" className="text-emerald-700 bg-emerald-50 border-emerald-100 text-[10px]">
{payment.status}
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
</Card>
</div>
{/* Financial Summary Sidebar */}
<div className="space-y-6">
<Card className="border-border/50 shadow-sm bg-primary/[0.02]">
<CardContent className="p-6">
<h3 className="font-bold text-sm mb-4">Financial Summary</h3>
<div className="space-y-3">
<div className="flex justify-between text-sm text-secondary-text">
<span>Subtotal</span>
<span className="font-medium">${(invoice.subtotal || 0).toLocaleString(undefined, { minimumFractionDigits: 2 })}</span>
</div>
<div className="flex justify-between text-sm text-secondary-text">
<span>Fees</span>
<span className="font-medium">${(invoice.otherCharges || 0).toLocaleString(undefined, { minimumFractionDigits: 2 })}</span>
</div>
<div className="pt-3 border-t border-border/50 flex justify-between items-end">
<div>
<p className="text-xs font-bold text-muted-foreground uppercase tracking-widest">Grand Total</p>
<p className="text-2xl font-black text-primary-text mt-1">
${invoice.amount.toLocaleString(undefined, { minimumFractionDigits: 2 })}
</p>
</div>
</div>
</div>
</CardContent>
</Card>
<Card className="border-border/50 shadow-sm">
<CardContent className="p-6">
<h3 className="font-bold text-sm mb-3">Internal Notes</h3>
<p className="text-sm text-secondary-text italic">
{invoice.notes || "No internal notes provided for this invoice."}
</p>
</CardContent>
</Card>
<div className="p-4 bg-muted/20 rounded-xl border border-border/50 text-center">
<FileText className="w-8 h-8 mx-auto mb-2 text-muted-foreground/30" />
<p className="text-xs text-secondary-text leading-relaxed">
For any questions regarding this invoice, please contact support@krowworkforce.com
</p>
</div>
</div>
</div>
</DashboardLayout>
);
}

View File

@@ -25,6 +25,7 @@ import Schedule from './features/operations/schedule/Schedule';
import StaffAvailability from './features/operations/availability/StaffAvailability'; import StaffAvailability from './features/operations/availability/StaffAvailability';
import TaskBoard from './features/operations/tasks/TaskBoard'; import TaskBoard from './features/operations/tasks/TaskBoard';
import InvoiceList from './features/finance/invoices/InvoiceList'; import InvoiceList from './features/finance/invoices/InvoiceList';
import InvoiceDetail from './features/finance/invoices/InvoiceDetail';
/** /**
@@ -116,6 +117,7 @@ const AppRoutes: React.FC = () => {
{/* Finance Routes */} {/* Finance Routes */}
<Route path="/invoices" element={<InvoiceList />} /> <Route path="/invoices" element={<InvoiceList />} />
<Route path="/invoices/:id" element={<InvoiceDetail />} />
</Route> </Route>
<Route path="*" element={<Navigate to="/login" replace />} /> <Route path="*" element={<Navigate to="/login" replace />} />