"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 */}
{/* Footer for floating bar trigger */}
{invoiceId ? "Edit Invoice" : "Create Invoice"}
{invoiceId ? "Update invoice details" : "Create a new invoice"}
{/* Floating Action Bar */}
{invoiceId ? "Edit Invoice" : "Create Invoice"}
{invoiceId ? "Update invoice details" : "Create a new invoice"}
}
>
>
);
}