From cad0662478e64a5014226a474ede9b37d9aacbab Mon Sep 17 00:00:00 2001 From: dhinesh-m24 Date: Mon, 9 Feb 2026 16:44:00 +0530 Subject: [PATCH] feat: Implement detail view for specific invoice --- .../finance/invoices/InvoiceDetail.tsx | 281 ++++++++++++++++++ apps/web/src/routes.tsx | 4 +- 2 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/features/finance/invoices/InvoiceDetail.tsx diff --git a/apps/web/src/features/finance/invoices/InvoiceDetail.tsx b/apps/web/src/features/finance/invoices/InvoiceDetail.tsx new file mode 100644 index 00000000..6c8c5313 --- /dev/null +++ b/apps/web/src/features/finance/invoices/InvoiceDetail.tsx @@ -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 = { + 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 ( + +
+
+
+
+ ); + } + + if (!invoice) { + return ( + +
+

Invoice not found

+ +
+
+ ); + } + + 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 ( + navigate("/invoices")} leadingIcon={}> + Back + + } + actions={ +
+ + {invoice.status === "DRAFT" && ( + + )} + {invoice.status !== "PAID" && ( + + )} +
+ } + > +
+
+ {/* Header & Client Info */} + + +
+
+
+

Client Information

+
+
+ +
+
+

{invoice.business?.businessName}

+

{invoice.business?.email}

+

{invoice.business?.phone}

+
+
+
+
+ + {invoice.hub || (invoice.order as any)?.teamHub?.hubName} +
+
+ +
+
+ Status + {status.label} +
+
+ Issued + {issueDate} +
+
+ Due Date + {dueDate} +
+
+
+
+
+ + {/* Line Items Table */} + +
+

Line Items (Shifts & Staff)

+
+ + + + Staff / Role + Hours + Rate + Total + + + + {lineItems.length === 0 ? ( + + + No line items recorded for this invoice. + + + ) : ( + lineItems.map((item: any, idx: number) => ( + + +

{item.staffName || "Staff Member"}

+

{item.roleName || "Support"}

+
+ {item.hours || 0}h + ${(item.rate || 0).toFixed(2)} + ${(item.total || 0).toFixed(2)} +
+ )) + )} +
+
+
+ + {/* Payment History */} + +
+

Payment History

+
+
+ {payments.length === 0 ? ( +
+ No payment records found. +
+ ) : ( + + + {payments.map((payment) => ( + + +
+
+ +
+
+

Payment Received

+

+ {payment.createdAt ? format(parseISO(payment.createdAt as string), "MMM d, yyyy") : "—"} +

+
+
+
+ + + {payment.status} + + +
+ ))} +
+
+ )} +
+
+
+ + {/* Financial Summary Sidebar */} +
+ + +

Financial Summary

+
+
+ Subtotal + ${(invoice.subtotal || 0).toLocaleString(undefined, { minimumFractionDigits: 2 })} +
+
+ Fees + ${(invoice.otherCharges || 0).toLocaleString(undefined, { minimumFractionDigits: 2 })} +
+
+
+

Grand Total

+

+ ${invoice.amount.toLocaleString(undefined, { minimumFractionDigits: 2 })} +

+
+
+
+
+
+ + + +

Internal Notes

+

+ {invoice.notes || "No internal notes provided for this invoice."} +

+
+
+ +
+ +

+ For any questions regarding this invoice, please contact support@krowworkforce.com +

+
+
+
+
+ ); +} diff --git a/apps/web/src/routes.tsx b/apps/web/src/routes.tsx index b073604c..614dd96f 100644 --- a/apps/web/src/routes.tsx +++ b/apps/web/src/routes.tsx @@ -25,6 +25,7 @@ import Schedule from './features/operations/schedule/Schedule'; import StaffAvailability from './features/operations/availability/StaffAvailability'; import TaskBoard from './features/operations/tasks/TaskBoard'; import InvoiceList from './features/finance/invoices/InvoiceList'; +import InvoiceDetail from './features/finance/invoices/InvoiceDetail'; /** @@ -116,7 +117,8 @@ const AppRoutes: React.FC = () => { {/* Finance Routes */} } /> - + } /> + } />