"use client" import { AlertTriangle, Plus, Save } from "lucide-react" import { Button } from "@/shared/components/ui/button" import { Alert, AlertTitle } from "@/shared/components/ui/alert" import { FieldGroup } from "@/shared/components/ui/field" import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" import { Rhform, RhfTextField, RhfSelectField, RhfAsyncSelectField, RhfTextareaField, RhfDateField, } from "@/shared/components/form" import { toast } from "sonner" import { useAuthApi } from "@/shared/useApi" import { useResourceForm } from "@/shared/hooks/use-resource-form" import { useFormMutation } from "@/shared/hooks/use-form-mutation" import { getTodayDate, toRelation, toId } from "@/shared/lib/utils" import { formatTaxLabel } from "@/shared/utils/formatters" import { expenseFormSchema, type ExpenseFormValues, } from "./expense.schema" import { EXPENSE_ROUTES, JOB_CARD_ROUTES, VENDOR_ROUTES, DEPARTMENT_ROUTES, INVENTORY_CATEGORY_ROUTES, TAX_ROUTES, ExpenseStatus, InvoiceDiscount, } from "@garage/api" import { useFormContext } from "react-hook-form" import { RhfLabelPickerField, type LabelItem } from "@/modules/labels/rhf-label-picker-field" import { ExpenseItemsSelectorField } from "@/modules/expense-items/expense-items-selector-field" import { InventoryCategoryCrudDialog } from "@/modules/expense-items/inventory-category-crud-dialog" import { ExpenseFormSummary } from "./expense-form-summary" // ── Constants ── const STATUS_OPTIONS = ExpenseStatus.map((value) => ({ value, label: value.replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase()), })) const DISCOUNT_OPTIONS = InvoiceDiscount.map((v) => ({ value: v, label: v.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()), })) // ── Props ── export type ExpenseFormProps = { resourceId?: string | null initialData?: unknown onSuccess?: () => void } // ── Default values ── const DEFAULT_VALUES: ExpenseFormValues = { job_card: null, category: null, vendor: null, department: null, tax: null, title: "", invoice_number: "", expense_date: getTodayDate(), notes: "", status: "open", discount: "no", discount_amount: undefined, labels: [], items: [], } // ── Mapping helpers ── function mapToFormValues(data: unknown): ExpenseFormValues { const d = (data as any)?.data ?? data ?? {} return { job_card: toRelation(d.job_card_id, d.job_card?.order_number ?? d.job_card_name), category: toRelation(d.category_id, d.category?.name ?? d.category?.title ?? d.category_name), vendor: toRelation(d.vendor_id, d.vendor?.company_name ?? d.vendor?.name ?? d.vendor_name), department: toRelation(d.department_id, d.department?.name ?? d.department_name), tax: toRelation(d.tax_id, formatTaxLabel(d.tax, d.tax_title ?? "") || undefined), title: d.title || "", invoice_number: d.invoice_number || "", expense_date: d.expense_date ? d.expense_date.split("T")[0] : "", notes: d.notes || "", status: d.status || "open", discount: d.discount || "no", discount_amount: d.discount_amount != null ? Number(d.discount_amount) : undefined, labels: (d.labels ?? []).map((label: any): LabelItem => ({ id: Number(label.id), title: label.title ?? label.name ?? "", color_code: label.color_code ?? "", })), items: (d.expense_items ?? d.items ?? []).map((item: any) => ({ expense_id: item.expense_item_id ?? item.expense_item?.id ?? item.id, title: item.expense_item?.item_name ?? item.expense_item?.name ?? item.item_name ?? item.title ?? "", quantity: Number(item.quantity) || 1, rate: Number(item.rate) || 0, discount_amount: item.discount_amount != null ? Number(item.discount_amount) : undefined, chart_of_account: item.chart_of_account != null ? String(item.chart_of_account) : "", description: item.description ?? "", })), } } function mapFormToPayload(values: ExpenseFormValues) { return { job_card_id: toId(values.job_card), category_id: toId(values.category), vendor_id: toId(values.vendor), department_id: toId(values.department), tax_id: toId(values.tax) ? Number(toId(values.tax)) : undefined, title: values.title, invoice_number: values.invoice_number || undefined, expense_date: values.expense_date || undefined, notes: values.notes || undefined, status: values.status || undefined, discount: values.discount || undefined, discount_amount: values.discount === "transaction_level" ? (values.discount_amount ?? 0) : undefined, label_ids: values.labels?.map((label) => label.id) ?? [], items: (values.items ?? []).map((item) => ({ expense_item_id: item.expense_id, quantity: item.quantity, rate: item.rate, discount_amount: values.discount === "line_item_level" ? (item.discount_amount ?? 0) : undefined, chart_of_account: item.chart_of_account || undefined, description: item.description || undefined, })), } } // ── Shared mapOption for async selects ── const mapLookupOption = (item: any) => ({ value: String(item.id), label: item.name ?? item.title ?? `#${item.id}`, }) const mapVendorOption = (item: any) => ({ value: String(item.id), label: item.company_name ?? item.name ?? `#${item.id}`, }) const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label } // ── Transaction-level discount amount field ── function TransactionDiscountField() { const { watch } = useFormContext() const discount = watch("discount") if (discount !== "transaction_level") return null return ( ) } // ── Component ── export function ExpenseForm({ resourceId, initialData, onSuccess }: ExpenseFormProps) { const api = useAuthApi() const { form, isEditing } = useResourceForm({ schema: expenseFormSchema, defaultValues: DEFAULT_VALUES, resourceId, initialData, queryKey: [EXPENSE_ROUTES.BY_ID, resourceId], mapToFormValues, }) const discount = form.watch("discount") const isLineItemDiscount = discount === "line_item_level" const { mutate, error, isPending } = useFormMutation(form, { mutationFn: (values: ExpenseFormValues) => { const payload = mapFormToPayload(values) const promise = (isEditing && resourceId ? api.expenses.update(resourceId, payload as any) : api.expenses.create(payload as any)) as Promise toast.promise(promise, { loading: isEditing ? "Updating expense..." : "Creating expense...", success: isEditing ? "Expense updated successfully" : "Expense created successfully", error: isEditing ? "Failed to update expense" : "Failed to create expense", }) return promise }, onSuccess: () => { form.reset() onSuccess?.() }, }) return ( mutate(values)}> {error && ( {isEditing ? "Failed to update expense" : "Failed to create expense"} {error.message} )}
name="items" label="Expense Items" triggerLabel="Add Expense Items" showChartOfAccount showDiscount={isLineItemDiscount} />
Details api.taxes.list()} mapOption={(item: any) => ({ value: String(item.id), label: item.title ? `${item.title} (${item.rate}%)` : `#${item.id}`, })} {...STORE_OBJECT} /> api.vendors.list()} mapOption={mapVendorOption} {...STORE_OBJECT} /> api.departments.list()} mapOption={mapLookupOption} {...STORE_OBJECT} /> api.jobCards.list()} mapOption={(item: any) => ({ value: String(item.id), label: item.order_number ?? item.estimate_number ?? `#${item.id}`, })} {...STORE_OBJECT} />
Category
api.inventoryCategories.list()} mapOption={mapLookupOption} {...STORE_OBJECT} />
) }