"use client" import { useMemo } from "react" 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 { Rhform, RhfTextField, RhfSelectField, RhfTextareaField, RhfAsyncSelectField, RhfAutoGenerateField, 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 { toRelation, toId, getTodayDate } from "@/shared/lib/utils" import { paymentMadeFormSchema, type PaymentMadeFormValues, } from "./payment-made.schema" import { ApiError, BILL_ROUTES, PAYMENT_MODE_ROUTES, EMPLOYEE_ROUTES, PaymentFor, } from "@garage/api" import { RhfVendorSelectField } from "@/modules/vendors/rhf-vendor-select-field" // ── Constants ── const PAYMENT_FOR_OPTIONS = PaymentFor.map((value) => ({ value, label: value.charAt(0).toUpperCase() + value.slice(1), })) // ── Props ── export type PaymentMadeFormProps = { resourceId?: string | null initialData?: unknown onSuccess?: () => void billId?: string | number | null expenseId?: string | number | null } // ── Default values ── const DEFAULT_VALUES: PaymentMadeFormValues & { details: Array<{ bill_id?: string | number | null; amount_paid?: number, expense_id?: string | number | null }> } = { bill: null, vendor: null, employee: null, payment_mode: null, payment_for: "bill", amount: 0, payment_number: "", payment_reference: "", payment_date: getTodayDate(), paid_through: "", notes: "", details: [{ amount_paid: 0 }] } // ── Mapping helpers ── function mapToFormValues(data: unknown): typeof DEFAULT_VALUES { const d = (data as any)?.data ?? data ?? {} // PaymentMade lists carry the linked bill/expense inside details[]; top-level // bill_id/expense_id only exist on inputs sent from a bill/expense subpage. const firstDetail = Array.isArray(d.details) ? d.details[0] ?? {} : {} const billId = d.bill_id ?? firstDetail.bill_id ?? firstDetail.bill?.id const billNumber = d.bill?.bill_number ?? d.bill_number ?? firstDetail.bill?.bill_number const expenseId = d.expense_id ?? firstDetail.expense_id ?? firstDetail.expense?.id // Resolve payment_mode label from nested object (title) or fallback to id const paymentModeId = d.payment_mode_id ?? d.payment_mode?.id const paymentModeLabel = d.payment_mode?.title ?? d.payment_mode?.name ?? d.payment_mode_name return { bill: toRelation(billId, billNumber), vendor: toRelation(d.vendor_id ?? d.vendor?.id, d.vendor?.company_name ?? d.vendor?.name ?? d.vendor_name), employee: toRelation(d.employee_id ?? d.employee?.id, d.employee?.first_name ? `${d.employee.first_name} ${d.employee.last_name ?? ""}`.trim() : d.employee_name), payment_mode: toRelation(paymentModeId, paymentModeLabel), payment_for: d.payment_for || (expenseId ? "expense" : "bill"), amount: d.payment_made != null ? Number(d.payment_made) : firstDetail.amount_paid != null ? Number(firstDetail.amount_paid) : 0, payment_number: d.payment_number || "", payment_reference: d.payment_reference || "", payment_date: d.payment_date || "", paid_through: d.paid_through || "", notes: d.notes || "", details: [{ bill_id: billId, expense_id: expenseId, amount_paid: firstDetail.amount_paid ?? 0 }], } } function mapFormToPayload(values: PaymentMadeFormValues, billId?: string | number | null, expenseId?: string | number | null) { const resolvedBillId = billId ?? toId(values.bill) const paymentDetails = values.payment_for === "expense" ? [{ expense_id: expenseId, amount_paid: values.amount || 0 }] : [{ bill_id: resolvedBillId, amount_paid: values.amount || 0 }] return { bill_id: values.payment_for === "bill" ? Number(resolvedBillId) : undefined, vendor_id: toId(values.vendor), employee_id: toId(values.employee) || undefined, payment_mode_id: toId(values.payment_mode), payment_for: values.payment_for, amount: values.amount || 0, payment_number: values.payment_number || undefined, payment_reference: values.payment_reference || undefined, payment_date: values.payment_date, paid_through: values.paid_through || undefined, notes: values.notes || undefined, payment_made: values.amount || 0, details: paymentDetails, ...(expenseId ? { expense_id: Number(expenseId) } : {}), } } // ── Shared mapOption for async selects ── const mapLookupOption = (item: any) => ({ value: String(item.id), label: item.name ?? item.title ?? `#${item.id}`, }) const mapBillOption = (item: any) => ({ value: String(item.id), label: item.bill_number ?? item.title ?? `#${item.id}`, }) const mapVendorOption = (item: any) => ({ value: String(item.id), label: item.name ?? item.company_name ?? `#${item.id}`, }) const mapEmployeeOption = (item: any) => ({ value: String(item.id), label: item.first_name ? `${item.first_name} ${item.last_name || ""}`.trim() : item.name ?? `#${item.id}`, }) const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label } // ── Component ── export function PaymentMadeForm({ resourceId, initialData, onSuccess, billId, expenseId }: PaymentMadeFormProps) { const api = useAuthApi() const paymentForOptions = useMemo(() => { if (expenseId) { return PAYMENT_FOR_OPTIONS.filter((option) => option.value === "expense") } return PAYMENT_FOR_OPTIONS.filter((option) => option.value === "bill") }, [expenseId]) const resolvedInitialData = useMemo(() => { const base: any = { ...(initialData as any) } if (!resourceId) { if (billId) { base.payment_for = "bill" } else if (expenseId) { base.payment_for = "expense" } else { base.payment_for = "bill" } } return Object.keys(base).length ? base : initialData }, [resourceId, billId, expenseId, initialData]) const { form, isEditing } = useResourceForm({ schema: paymentMadeFormSchema, defaultValues: DEFAULT_VALUES, resourceId, initialData: resolvedInitialData, mapToFormValues, }) const vendor = form.watch("vendor") const employee = form.watch("employee") const paymentFor = form.watch("payment_for") const vendorRequired = !employee?.value const employeeRequired = !vendor?.value const showBillField = !billId && !expenseId const { mutate, error, isPending } = useFormMutation(form, { mutationFn: (values: PaymentMadeFormValues) => { const payload = mapFormToPayload(values, billId, expenseId) const promise = (isEditing && resourceId ? api.paymentMades.update(resourceId, payload as any) : api.paymentMades.create(payload as any)) as Promise toast.promise(promise, { loading: isEditing ? "Updating payment..." : "Recording payment...", success: isEditing ? "Payment updated successfully" : "Payment recorded successfully", error: isEditing ? "Failed to update payment" : "Failed to record payment", }) return promise }, onError: (error) => { if (!(error instanceof ApiError) || !error.validationErrors) { return } const vendorError = error.validationErrors.vendor_id?.[0] const employeeError = error.validationErrors.employee_id?.[0] const billError = error.validationErrors.bill_id?.[0] if (billError) { form.setError("bill", { message: billError }) } if (!vendorError && !employeeError) { return } const message = "Select either a vendor or an employee, not both" form.setError("vendor", { message }) form.setError("employee", { message }) }, onSuccess: () => { form.reset() onSuccess?.() }, }) return ( mutate(values)}> {error && ( {isEditing ? "Failed to update payment" : "Failed to record payment"} {error.message} )}
{showBillField && ( api.bills.list()} mapOption={mapBillOption} {...STORE_OBJECT} /> )} api.employees.list()} mapOption={mapEmployeeOption} {...STORE_OBJECT} />
api.paymentModes.list()} mapOption={mapLookupOption} {...STORE_OBJECT} />
) }