"use client"; import * as React from "react"; import { useState, useEffect } 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 { Badge } from "~/components/ui/badge"; import { Separator } from "~/components/ui/separator"; import { SearchableSelect } from "~/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "~/components/ui/select"; import { toast } from "sonner"; import { Calendar, FileText, User, Plus, Trash2, DollarSign, Clock, Edit3, Save, X, AlertCircle, Building, } from "lucide-react"; import { useRouter } from "next/navigation"; import { format } from "date-fns"; import { FormSkeleton } from "~/components/ui/skeleton"; import { EditableInvoiceItems } from "~/components/editable-invoice-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; } export function InvoiceForm({ invoiceId }: InvoiceFormProps) { const router = useRouter(); 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, items: [ { id: crypto.randomUUID(), date: new Date(), description: "", hours: 0, rate: 0, amount: 0, }, ], }); const [loading, setLoading] = useState(false); const [defaultRate, setDefaultRate] = useState(0); // 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, 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: 0, rate: 0, amount: 0, }, ], }); // Set default rate from first item if (existingInvoice.items?.[0]) { setDefaultRate(existingInvoice.items[0].rate); } } }, [existingInvoice, 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: 0, rate: defaultRate, amount: 0, }, ], })); }; // Remove item const removeItem = (idx: number) => { if (formData.items.length > 1) { setFormData((prev) => ({ ...prev, items: prev.items.filter((_, i) => i !== idx), })); } }; // Apply default rate to all items const applyDefaultRate = () => { setFormData((prev) => ({ ...prev, items: prev.items.map((item) => ({ ...item, rate: defaultRate, amount: item.hours * defaultRate, })), })); }; // 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/${invoiceId}`); }, onError: (error) => { toast.error(error.message || "Failed to update invoice"); }, }); // Handle form submit const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); // Validate form if (!formData.businessId) { toast.error("Please select a business"); return; } if (!formData.clientId) { toast.error("Please select a client"); return; } if (formData.items.some((item) => !item.description.trim())) { toast.error("Please fill in all item descriptions"); return; } if (formData.items.some((item) => item.hours <= 0)) { toast.error("Please enter valid hours for all items"); return; } if (formData.items.some((item) => item.rate <= 0)) { toast.error("Please enter valid rates for all items"); return; } setLoading(true); try { // In the handleSubmit, ensure items are sent in the current array order with no sorting const submitData = { ...formData, items: formData.items.map((item) => ({ date: new Date(item.date), description: item.description, hours: item.hours, rate: item.rate, amount: item.amount, // position will be set by backend based on array order })), }; if (invoiceId) { await updateInvoice.mutateAsync({ id: invoiceId, ...submitData, }); } else { await createInvoice.mutateAsync(submitData); } } finally { setLoading(false); } }; // Show loading state while fetching existing invoice data if (invoiceId && loadingInvoice) { return (