"use client"; import { AlertTriangle, Bell, Building, Check, Copy, DollarSign, Edit, FileText, Link2, Link2Off, Loader2, Mail, MapPin, Phone, Plus, Trash2, User, } from "lucide-react"; import Link from "next/link"; import { notFound, useParams, useRouter, useSearchParams } from "next/navigation"; import { useState, useEffect } from "react"; import { toast } from "sonner"; import { StatusBadge } from "~/components/data/status-badge"; import { DashboardPage, dashboardGapClass, dashboardGridClass, } from "~/components/layout/dashboard-page"; import { DashboardPageHeader } from "~/components/layout/page-header"; import { cn } from "~/lib/utils"; import { Button } from "~/components/ui/button"; import { Badge } from "~/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "~/components/ui/dialog"; import { Popover, PopoverContent, PopoverTrigger, } from "~/components/ui/popover"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "~/components/ui/select"; import { Separator } from "~/components/ui/separator"; import { Textarea } from "~/components/ui/textarea"; import { Input } from "~/components/ui/input"; import { Label } from "~/components/ui/label"; import { DatePicker } from "~/components/ui/date-picker"; import { getEffectiveInvoiceStatus, isInvoiceOverdue, } from "~/lib/invoice-status"; import { api } from "~/trpc/react"; import type { StoredInvoiceStatus } from "~/types/invoice"; import { InvoiceDetailsSkeleton } from "./_components/invoice-details-skeleton"; import { PDFDownloadButton } from "./_components/pdf-download-button"; import { EnhancedSendInvoiceButton } from "~/components/forms/enhanced-send-invoice-button"; import { InvoiceTimerCard } from "./_components/invoice-timer-card"; const PAYMENT_METHODS = [ { value: "cash", label: "Cash" }, { value: "check", label: "Check" }, { value: "bank_transfer", label: "Bank Transfer" }, { value: "credit_card", label: "Credit Card" }, { value: "paypal", label: "PayPal" }, { value: "other", label: "Other" }, ] as const; function methodLabel(method: string) { return PAYMENT_METHODS.find((m) => m.value === method)?.label ?? method; } function daysSince(date: Date) { return Math.floor((Date.now() - new Date(date).getTime()) / 86_400_000); } function InvoiceViewContent({ invoiceId }: { invoiceId: string }) { const router = useRouter(); const searchParams = useSearchParams(); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [recordPaymentOpen, setRecordPaymentOpen] = useState(false); const [reminderOpen, setReminderOpen] = useState(false); const [shareOpen, setShareOpen] = useState(false); const [paymentAmount, setPaymentAmount] = useState(""); const [paymentMethod, setPaymentMethod] = useState("other"); const [paymentNotes, setPaymentNotes] = useState(""); const [reminderMessage, setReminderMessage] = useState(""); const [copied, setCopied] = useState(false); const { data: invoice, isLoading } = api.invoices.getById.useQuery({ id: invoiceId, }); const { data: payments, isLoading: paymentsLoading } = api.payments.getByInvoice.useQuery({ invoiceId }); const utils = api.useUtils(); useEffect(() => { if (searchParams.get("editBlocked") === "1") { toast.error("Only draft invoices can be edited"); router.replace(`/dashboard/invoices/${invoiceId}`); } }, [searchParams, invoiceId, router]); const invalidate = () => { void utils.invoices.getById.invalidate({ id: invoiceId }); void utils.payments.getByInvoice.invalidate({ invoiceId }); }; const deleteInvoice = api.invoices.delete.useMutation({ onSuccess: () => { toast.success("Invoice deleted"); router.push("/dashboard/invoices"); }, onError: (e) => toast.error(e.message ?? "Failed to delete invoice"), }); const updateStatus = api.invoices.updateStatus.useMutation({ onSuccess: (data) => { toast.success(data.message); invalidate(); }, onError: (e) => toast.error(e.message ?? "Failed to update status"), }); const createPayment = api.payments.create.useMutation({ onSuccess: () => { toast.success("Payment recorded"); setRecordPaymentOpen(false); setPaymentAmount(""); setPaymentMethod("other"); setPaymentNotes(""); invalidate(); }, onError: (e) => toast.error(e.message ?? "Failed to record payment"), }); const deletePayment = api.payments.delete.useMutation({ onSuccess: () => { toast.success("Payment removed"); invalidate(); }, onError: (e) => toast.error(e.message ?? "Failed to remove payment"), }); const generatePublicToken = api.invoices.generatePublicToken.useMutation({ onSuccess: () => { toast.success("Share link generated"); void utils.invoices.getById.invalidate({ id: invoiceId }); }, onError: (e) => toast.error(e.message ?? "Failed to generate link"), }); const revokePublicToken = api.invoices.revokePublicToken.useMutation({ onSuccess: () => { toast.success("Share link revoked"); void utils.invoices.getById.invalidate({ id: invoiceId }); }, onError: (e) => toast.error(e.message ?? "Failed to revoke link"), }); const sendReminder = api.invoices.sendReminder.useMutation({ onSuccess: () => { toast.success("Reminder sent"); setReminderOpen(false); setReminderMessage(""); void utils.invoices.getById.invalidate({ id: invoiceId }); }, onError: (e) => toast.error(e.message ?? "Failed to send reminder"), }); const updateInvoice = api.invoices.update.useMutation({ onSuccess: () => { toast.success("Reminder saved"); void utils.invoices.getById.invalidate({ id: invoiceId }); void utils.dashboard.getStats.invalidate(); }, onError: (e) => toast.error(e.message ?? "Failed to save reminder"), }); if (isLoading) return ; if (!invoice) notFound(); const formatDate = (date: Date) => new Intl.DateTimeFormat("en-US", { year: "numeric", month: "short", day: "numeric" }).format( new Date(date), ); const formatCurrency = (amount: number, currency = invoice.currency) => new Intl.NumberFormat("en-US", { style: "currency", currency }).format(amount); const subtotal = invoice.items.reduce((s, i) => s + i.amount, 0); const taxAmount = (subtotal * invoice.taxRate) / 100; const total = subtotal + taxAmount; const totalPaid = (payments ?? []).reduce((s, p) => s + p.amount, 0); const balanceDue = total - totalPaid; const storedStatus = invoice.status as StoredInvoiceStatus; const effectiveStatus = getEffectiveInvoiceStatus(storedStatus, invoice.dueDate); const isOverdue = isInvoiceOverdue(storedStatus, invoice.dueDate); const canSendReminder = effectiveStatus === "sent" || effectiveStatus === "overdue"; const publicUrl = invoice.publicToken ? `${window.location.origin}/i/${invoice.publicToken}` : null; const handleCopyLink = async () => { if (!publicUrl) return; await navigator.clipboard.writeText(publicUrl); setCopied(true); setTimeout(() => setCopied(false), 2000); }; const handleRecordPayment = () => { const amount = parseFloat(paymentAmount); if (isNaN(amount) || amount <= 0) { toast.error("Enter a valid payment amount"); return; } createPayment.mutate({ invoiceId, amount, date: new Date(), method: paymentMethod as Parameters[0]["method"], notes: paymentNotes || undefined, }); }; return ( {storedStatus === "draft" ? ( ) : null}
{/* Left Column */}
{/* Invoice Header */}

{invoice.invoiceNumber}

Issued {formatDate(invoice.issueDate)}
Due {formatDate(invoice.dueDate)}

Total Amount

{formatCurrency(total)}

{totalPaid > 0 && balanceDue > 0 && (

Balance due: {formatCurrency(balanceDue)}

)}
{/* Overdue Alert */} {isOverdue && (

Invoice Overdue

{Math.ceil( (new Date().getTime() - new Date(invoice.dueDate).getTime()) / (1000 * 60 * 60 * 24), )}{" "} days past due date

)} {/* Client & Business */}
Bill To

{invoice.client.name}

{invoice.client.email && (
{invoice.client.email}
)} {invoice.client.phone && (
{invoice.client.phone}
)} {(invoice.client.addressLine1 ?? invoice.client.city) && (
{invoice.client.addressLine1 &&
{invoice.client.addressLine1}
} {invoice.client.addressLine2 &&
{invoice.client.addressLine2}
} {(invoice.client.city ?? invoice.client.state ?? invoice.client.postalCode) && (
{[ invoice.client.city, invoice.client.state, invoice.client.postalCode, ] .filter(Boolean) .join(", ")}
)} {invoice.client.country &&
{invoice.client.country}
}
)}
{invoice.business && ( From

{invoice.business.name}

{invoice.business.email && (
{invoice.business.email}
)} {invoice.business.phone && (
{invoice.business.phone}
)}
)}
{/* Invoice Items */} Invoice Items {invoice.items.map((item) => (

{item.description}

{formatDate(item.date).replace(/ /g, " ")} {item.hours.toString()} hours @ ${item.rate}/hr

{formatCurrency(item.amount)}

))} {/* Totals */}
Subtotal: {formatCurrency(subtotal)}
{invoice.taxRate > 0 && (
Tax ({invoice.taxRate}%): {formatCurrency(taxAmount)}
)}
Total: {formatCurrency(total)}
{totalPaid > 0 && ( <>
Paid: − {formatCurrency(totalPaid)}
Balance Due: {formatCurrency(Math.max(0, balanceDue))}
)}
{/* Payments */} Payments {paymentsLoading ? (

Loading…

) : (payments ?? []).length === 0 ? (

No payments recorded yet.

) : (
{(payments ?? []).map((p) => (
{formatCurrency(p.amount)} {methodLabel(p.method)} {formatDate(p.date)} {p.notes && ( {p.notes} )}
))}
)}
{/* Notes */} {invoice.notes && ( Notes

{invoice.notes}

)}
{/* Right Column - Actions */}
{storedStatus === "draft" && ( )} Actions {storedStatus === "draft" ? ( ) : null} {invoice.items && invoice.client && ( )} {effectiveStatus === "draft" && ( )} {effectiveStatus === "draft" && ( updateInvoice.mutate({ id: invoiceId, sendReminderAt, }) } onClear={() => updateInvoice.mutate({ id: invoiceId, sendReminderAt: null }) } /> )} {(effectiveStatus === "sent" || effectiveStatus === "overdue") && ( )} {/* Send Reminder */} {canSendReminder && (
{invoice.lastReminderSentAt && (

Last sent {daysSince(invoice.lastReminderSentAt)} day {daysSince(invoice.lastReminderSentAt) === 1 ? "" : "s"} ago

)}
)} {/* Share Link */}

Client share link

{publicUrl ? ( <>

{publicUrl}

) : ( <>

Generate a shareable link your client can use to view this invoice without logging in.

)}
{/* Mark as Paid */} {(effectiveStatus === "sent" || effectiveStatus === "overdue") && ( )}
{/* Record Payment Dialog */} Record Payment Record a payment received for invoice {invoice.invoiceNumber}.
setPaymentAmount(e.target.value)} />
setPaymentNotes(e.target.value)} />
{/* Send Reminder Dialog */} Send Reminder Send a payment reminder to {invoice.client.name} for invoice{" "} {invoice.invoiceNumber}.