"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, RhfAsyncSelectField, RhfSelectField, RhfTextField, RhfTextareaField, RhfAutoGenerateField, RhfDateField, } from "@/shared/components/form" import { useResourceForm } from "@/shared/hooks/use-resource-form" import { useFormMutation } from "@/shared/hooks/use-form-mutation" import { getTodayDate, toId, toRelation } from "@/shared/lib/utils" import { useAuthApi } from "@/shared/useApi" import { BillStatus, DiscountType, BILL_ROUTES, DEPARTMENT_ROUTES, JOB_CARD_ROUTES, PAYMENT_TERM_ROUTES, PURCHASE_ORDER_ROUTES, VENDOR_ROUTES, TAX_ROUTES, } from "@garage/api" import { toast } from "sonner" import { PartsSelectorField } from "@/modules/parts/parts-selector-field" import { ServicesSelectorField } from "@/modules/services/services-selector-field" import { ExpenseItemsSelectorField } from "@/modules/expense-items/expense-items-selector-field" import { useFormContext } from "react-hook-form" import { billFormSchema, type BillFormValues } from "./bill.schema" import { BillFormSummary } from "./bill-form-summary" export type BillFormProps = { resourceId?: string | null initialData?: unknown onSuccess?: () => void } const DEFAULT_VALUES: BillFormValues = { title: "", vendor: null, vendor_address: null, purchase_order: null, job_card: null, payment_term: null, department: null, tax: null, bill_number: "", bill_date: getTodayDate(), bill_due_date: "", status: "draft", discount: "no", discount_amount: undefined, notes: "", part_items: [], service_items: [], expense_items: [], } const STATUS_OPTIONS = BillStatus.map((value) => ({ value, label: value.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()), })) const DISCOUNT_OPTIONS = DiscountType.map((value) => ({ value, label: value.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()), })) const mapLookupOption = (item: any) => ({ value: String(item.id), label: item.name ?? item.title ?? `#${item.id}`, }) const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label } function mapToFormValues(data: unknown): BillFormValues { const d = (data as any)?.data ?? data ?? {} return { title: d.title || "", vendor: toRelation(d.vendor_id, d.vendor?.name ?? d.vendor_name), vendor_address: toRelation(d.vendor_address_id, d.vendor_address?.address), purchase_order: toRelation(d.purchase_order_id, d.purchase_order?.order_number ?? d.purchase_order_number), job_card: toRelation(d.job_card_id, d.job_card?.order_number ?? d.job_card_number), payment_term: toRelation(d.payment_terms_id, d.payment_terms?.name ?? d.payment_terms_name), department: toRelation(d.department_id, d.department?.name ?? d.department_name), tax: toRelation(d.tax_id, d.tax?.name ? `${d.tax.name} (${d.tax.rate}%)` : d.tax_title), bill_number: d.bill_number || "", bill_date: d.bill_date ? d.bill_date.split("T")[0] : "", bill_due_date: d.bill_due_date ? d.bill_due_date.split("T")[0] : "", status: d.status || "draft", discount: d.discount_type || "no", discount_amount: d.discount_amount != null ? Number(d.discount_amount) : undefined, notes: d.notes || "", part_items: (d.parts ?? []).map((p: any) => ({ part_id: p.part_id ?? p.id, title: p.part?.name ?? p.part_name ?? p.title ?? "", quantity: Number(p.quantity) || 1, rate: Number(p.rate) || 0, discount_amount: p.discount_amount != null ? Number(p.discount_amount) : undefined, description: p.description ?? "", })), service_items: (d.services ?? []).map((s: any) => ({ service_id: s.service_id ?? s.id, title: s.service?.name ?? s.service_name ?? s.title ?? "", quantity: Number(s.quantity) || 1, rate: Number(s.rate) || 0, discount_amount: s.discount_amount != null ? Number(s.discount_amount) : undefined, description: s.description ?? "", })), expense_items: (d.expenses ?? []).map((e: any) => ({ expense_id: e.expense_id ?? e.id, title: e.expense?.title ?? e.expense_title ?? e.title ?? "", quantity: Number(e.quantity) || 1, rate: Number(e.rate) || 0, discount_amount: e.discount_amount != null ? Number(e.discount_amount) : undefined, description: e.description ?? "", })), } } function mapFormToPayload(values: BillFormValues) { return { title: values.title, vendor_id: toId(values.vendor), vendor_address_id: toId(values.vendor_address), purchase_order_id: toId(values.purchase_order), job_card_id: toId(values.job_card), payment_terms_id: toId(values.payment_term), department_id: toId(values.department), tax_id: toId(values.tax) ? Number(toId(values.tax)) : undefined, bill_number: values.bill_number || undefined, bill_date: values.bill_date || undefined, bill_due_date: values.bill_due_date || undefined, status: values.status || undefined, discount_type: values.discount || undefined, discount_amount: values.discount === "transaction_level" ? (values.discount_amount ?? 0) : undefined, notes: values.notes || undefined, part_items: (values.part_items ?? []).map((item) => ({ part_id: item.part_id, quantity: item.quantity, rate: item.rate, discount_amount: values.discount === "line_item_level" ? (item.discount_amount ?? 0) : undefined, description: item.description || undefined, })), service_items: (values.service_items ?? []).map((item) => ({ service_id: item.service_id, quantity: item.quantity, rate: item.rate, discount_amount: values.discount === "line_item_level" ? (item.discount_amount ?? 0) : undefined, description: item.description || undefined, })), expense_items: (values.expense_items ?? []).map((item) => ({ expense_id: item.expense_id, quantity: item.quantity, rate: item.rate, discount_amount: values.discount === "line_item_level" ? (item.discount_amount ?? 0) : undefined, description: item.description || undefined, })), } } // ── Transaction-level discount field (conditional) ── function TransactionDiscountField() { const { watch } = useFormContext() const discount = watch("discount") if (discount !== "transaction_level") return null return ( ) } // ── Component ── export function BillForm({ resourceId, initialData, onSuccess }: BillFormProps) { const api = useAuthApi() const { form, isEditing } = useResourceForm({ schema: billFormSchema, defaultValues: DEFAULT_VALUES, resourceId, initialData, queryKey: [BILL_ROUTES.BY_ID, resourceId], mapToFormValues, }) const discount = form.watch("discount") const isLineItemDiscount = discount === "line_item_level" const { mutate, error, isPending } = useFormMutation(form, { mutationFn: (values: BillFormValues) => { const payload = mapFormToPayload(values) const promise = (isEditing && resourceId ? api.bills.update(resourceId, payload) : api.bills.create(payload)) as Promise toast.promise(promise, { loading: isEditing ? "Updating bill..." : "Creating bill...", success: isEditing ? "Bill updated successfully" : "Bill created successfully", error: isEditing ? "Failed to update bill" : "Failed to create bill", }) return promise }, onSuccess: () => { form.reset() onSuccess?.() }, }) return ( mutate(values)}> {error && ( {isEditing ? "Failed to update bill" : "Failed to create bill"} {error.message} )}
{/* ── Main column (9/12) ── */}
name="part_items" showDiscount={isLineItemDiscount} /> name="service_items" showDiscount={isLineItemDiscount} /> name="expense_items" showDiscount={isLineItemDiscount} />
{/* ── Sidebar column (3/12) ── */}
Details
api.vendors.list()} mapOption={mapLookupOption} {...STORE_OBJECT} /> api.taxes.list()} mapOption={(item: any) => ({ value: String(item.id), label: item.title ? `${item.title} (${item.rate}%)` : `#${item.id}`, })} {...STORE_OBJECT} /> api.departments.list()} mapOption={mapLookupOption} {...STORE_OBJECT} /> api.paymentTerms.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} /> api.purchaseOrders.list()} mapOption={(item: any) => ({ value: String(item.id), label: item.order_number || item.title || `#${item.id}`, })} {...STORE_OBJECT} />
) }