"use client"; import * as React from "react"; import { useState, useRef } from "react"; import { api } from "~/trpc/react"; import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; import { Input } from "~/components/ui/input"; import { Button } from "~/components/ui/button"; import { Label } from "~/components/ui/label"; import { DatePicker } from "~/components/ui/date-picker"; import { Separator } from "~/components/ui/separator"; import { Textarea } from "~/components/ui/textarea"; import { NumberInput } from "~/components/ui/number-input"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "~/components/ui/select"; import { toast } from "sonner"; import { FileText, DollarSign, Clock, Save, Check } from "lucide-react"; import { useRouter } from "next/navigation"; import { FloatingActionBar } from "~/components/layout/floating-action-bar"; import { InvoiceLineItems } from "~/components/forms/invoice-line-items"; const STATUS_OPTIONS = [ { value: "draft", label: "Draft" }, { value: "sent", label: "Sent" }, { value: "paid", label: "Paid" }, { value: "overdue", label: "Overdue" }, ] as const; interface InvoiceFormProps { invoiceId?: string; } // Custom skeleton for invoice form function InvoiceFormSkeleton() { return (
{/* Header */}
{/* Form Content */}
{/* Left Column - Content with Tabs */}
{/* Tabs */}
{/* Invoice Details Card */}
{/* Invoice Items Card */}
{/* Right Column - Summary */}
); } export function InvoiceForm({ invoiceId }: InvoiceFormProps) { const router = useRouter(); const headerRef = useRef(null); const footerRef = useRef(null); const [formData, setFormData] = useState({ invoiceNumber: `INV-${new Date().toISOString().slice(0, 10).replace(/-/g, "")}-${Date.now().toString().slice(-6)}`, businessId: "", clientId: "", issueDate: new Date(), dueDate: new Date(), status: "draft" as "draft" | "sent" | "paid" | "overdue", notes: "", taxRate: 0, defaultHourlyRate: 100, items: [ { id: crypto.randomUUID(), date: new Date(), description: "", hours: 1, rate: 100, amount: 100, }, ], }); const [loading, setLoading] = useState(false); // Fetch clients and businesses for dropdowns const { data: clients, isLoading: loadingClients } = api.clients.getAll.useQuery(); const { data: businesses, isLoading: loadingBusinesses } = api.businesses.getAll.useQuery(); // Fetch existing invoice data if editing const { data: existingInvoice, isLoading: loadingInvoice } = api.invoices.getById.useQuery({ id: invoiceId! }, { enabled: !!invoiceId }); // Populate form with existing data when editing React.useEffect(() => { if (existingInvoice && invoiceId) { setFormData({ invoiceNumber: existingInvoice.invoiceNumber, businessId: existingInvoice.businessId ?? "", clientId: existingInvoice.clientId, issueDate: new Date(existingInvoice.issueDate), dueDate: new Date(existingInvoice.dueDate), status: existingInvoice.status as "draft" | "sent" | "paid" | "overdue", notes: existingInvoice.notes ?? "", taxRate: existingInvoice.taxRate, defaultHourlyRate: 100, items: existingInvoice.items?.map((item) => ({ id: crypto.randomUUID(), date: new Date(item.date), description: item.description, hours: item.hours, rate: item.rate, amount: item.amount, })) || [ { id: crypto.randomUUID(), date: new Date(), description: "", hours: 1, rate: formData.defaultHourlyRate, amount: formData.defaultHourlyRate, }, ], }); } }, [existingInvoice, invoiceId]); // Auto-fill default business for new invoices React.useEffect(() => { if (!invoiceId && businesses && !formData.businessId) { const defaultBusiness = businesses.find((b) => b.isDefault); if (defaultBusiness) { setFormData((prev) => ({ ...prev, businessId: defaultBusiness.id })); } } }, [businesses, formData.businessId, invoiceId]); // Calculate totals const totals = React.useMemo(() => { const subtotal = formData.items.reduce( (sum, item) => sum + item.hours * item.rate, 0, ); const taxAmount = (subtotal * formData.taxRate) / 100; const total = subtotal + taxAmount; return { subtotal, taxAmount, total }; }, [formData.items, formData.taxRate]); // Add new item const addItem = () => { setFormData((prev) => ({ ...prev, items: [ ...prev.items, { id: crypto.randomUUID(), date: new Date(), description: "", hours: 1, rate: formData.defaultHourlyRate, amount: formData.defaultHourlyRate, }, ], })); }; // Remove item const removeItem = (idx: number) => { if (formData.items.length > 1) { setFormData((prev) => ({ ...prev, items: prev.items.filter((_, i) => i !== idx), })); } }; // Update item const updateItem = ( idx: number, field: string, value: string | number | Date, ) => { setFormData((prev) => ({ ...prev, items: prev.items.map((item, i) => i === idx ? { ...item, [field]: value } : item, ), })); }; // Move item up const moveItemUp = (idx: number) => { if (idx === 0) return; // Already at top setFormData((prev) => { const newItems = [...prev.items]; [newItems[idx - 1], newItems[idx]] = [newItems[idx], newItems[idx - 1]]; return { ...prev, items: newItems }; }); }; // Move item down const moveItemDown = (idx: number) => { if (idx === formData.items.length - 1) return; // Already at bottom setFormData((prev) => { const newItems = [...prev.items]; [newItems[idx], newItems[idx + 1]] = [newItems[idx + 1], newItems[idx]]; return { ...prev, items: newItems }; }); }; // tRPC mutations const createInvoice = api.invoices.create.useMutation({ onSuccess: () => { toast.success("Invoice created successfully"); router.push("/dashboard/invoices"); }, onError: (error) => { toast.error(error.message || "Failed to create invoice"); }, }); const updateInvoice = api.invoices.update.useMutation({ onSuccess: () => { toast.success("Invoice updated successfully"); router.push("/dashboard/invoices"); }, onError: (error) => { toast.error(error.message || "Failed to update invoice"); }, }); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); try { const invoiceData = { invoiceNumber: formData.invoiceNumber, businessId: formData.businessId || undefined, clientId: formData.clientId, issueDate: formData.issueDate, dueDate: formData.dueDate, status: formData.status, notes: formData.notes, taxRate: formData.taxRate, items: formData.items.map((item) => ({ date: item.date, description: item.description, hours: item.hours, rate: item.rate, amount: item.hours * item.rate, })), }; if (invoiceId) { await updateInvoice.mutateAsync({ id: invoiceId, ...invoiceData }); } else { await createInvoice.mutateAsync(invoiceData); } } catch (error) { console.error("Error saving invoice:", error); } finally { setLoading(false); } }; // Show loading state if (loadingClients || loadingBusinesses || (invoiceId && loadingInvoice)) { return ; } return ( <>
{/* Header */}

{invoiceId ? "Edit Invoice" : "Create Invoice"}

{invoiceId ? "Update invoice details" : "Create a new invoice"}

{/* Form Content */}
{/* Left Column - Content with Tabs */}
Invoice Details Invoice Items {/* Invoice Details */} Invoice Details
setFormData((prev) => ({ ...prev, issueDate: date ?? new Date(), })) } />
setFormData((prev) => ({ ...prev, dueDate: date ?? new Date(), })) } />
setFormData((prev) => ({ ...prev, taxRate: value, })) } min={0} max={100} step={1} suffix="%" width="full" />
setFormData((prev) => ({ ...prev, defaultHourlyRate: value, })) } min={0} step={1} prefix="$" width="full" />