refactor: enhance employee and service forms with new fields and validation

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-05-08 15:55:33 +03:00
co-authored by Copilot
parent b0361fcbfb
commit 4b51ffe457
7 changed files with 448 additions and 143 deletions
@@ -26,7 +26,8 @@ import {
type PaymentMadeFormValues,
} from "./payment-made.schema"
import {
PAYMENT_MADE_ROUTES,
ApiError,
BILL_ROUTES,
PAYMENT_MODE_ROUTES,
EMPLOYEE_ROUTES,
PaymentFor,
@@ -53,6 +54,7 @@ export type PaymentMadeFormProps = {
// ── 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,
@@ -76,6 +78,7 @@ function mapToFormValues(data: unknown): typeof DEFAULT_VALUES {
const paymentModeLabel = d.payment_mode?.title ?? d.payment_mode?.name ?? d.payment_mode_name
return {
bill: toRelation(d.bill_id, d.bill?.bill_number ?? d.bill_number),
vendor: toRelation(d.vendor_id, d.vendor?.company_name ?? d.vendor?.name ?? d.vendor_name),
employee: toRelation(d.employee_id, d.employee?.first_name ? `${d.employee.first_name} ${d.employee.last_name ?? ""}`.trim() : d.employee_name),
payment_mode: toRelation(paymentModeId, paymentModeLabel),
@@ -92,10 +95,13 @@ function mapToFormValues(data: unknown): typeof DEFAULT_VALUES {
function mapFormToPayload(values: PaymentMadeFormValues, billId?: string | number | null, expenseId?: string | number | null) {
const resolvedBillId = billId ?? toId(values.bill)
const paymentDetails =
expenseId ? [{ expense_id: expenseId, amount_paid: values.amount || 0 }]
: [{ bill_id: billId, amount_paid: values.amount || 0 }]
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),
@@ -108,7 +114,6 @@ function mapFormToPayload(values: PaymentMadeFormValues, billId?: string | numbe
notes: values.notes || undefined,
payment_made: values.amount || 0,
details: paymentDetails,
...(billId ? { bill_id: Number(billId) } : {}),
...(expenseId ? { expense_id: Number(expenseId) } : {}),
}
}
@@ -120,6 +125,11 @@ const mapLookupOption = (item: any) => ({
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}`,
@@ -139,6 +149,13 @@ const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) =
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) {
@@ -146,6 +163,8 @@ export function PaymentMadeForm({ resourceId, initialData, onSuccess, billId, ex
base.payment_for = "bill"
} else if (expenseId) {
base.payment_for = "expense"
} else {
base.payment_for = "bill"
}
}
return Object.keys(base).length ? base : initialData
@@ -159,6 +178,13 @@ export function PaymentMadeForm({ resourceId, initialData, onSuccess, billId, ex
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)
@@ -172,6 +198,27 @@ export function PaymentMadeForm({ resourceId, initialData, onSuccess, billId, ex
})
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?.()
@@ -192,14 +239,30 @@ export function PaymentMadeForm({ resourceId, initialData, onSuccess, billId, ex
<FieldGroup>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{showBillField && (
<RhfAsyncSelectField
name="bill"
label="Bill"
placeholder="Select bill"
required={paymentFor === "bill"}
queryKey={[BILL_ROUTES.INDEX]}
listFn={() => api.bills.list()}
mapOption={mapBillOption}
{...STORE_OBJECT}
/>
)}
<RhfVendorSelectField
name="vendor"
label="Vendor"
required={vendorRequired}
description="Select either a vendor or an employee."
/>
<RhfAsyncSelectField
name="employee"
label="Employee"
placeholder="Select employee"
required={employeeRequired}
description="Select either a vendor or an employee."
queryKey={[EMPLOYEE_ROUTES.INDEX]}
listFn={() => api.employees.list()}
mapOption={mapEmployeeOption}
@@ -212,7 +275,7 @@ export function PaymentMadeForm({ resourceId, initialData, onSuccess, billId, ex
name="payment_for"
label="Payment For"
placeholder="Select type"
options={PAYMENT_FOR_OPTIONS}
options={paymentForOptions}
required
/>
<RhfTextField
@@ -27,6 +27,7 @@ const detailsItemSchema = z.object({
const paymentMadeFormSchema = z.object({
// ── Relations ──
bill: relationFieldSchema,
vendor: relationFieldSchema,
employee: relationFieldSchema,
payment_mode: requiredRelationFieldSchema,
@@ -41,9 +42,18 @@ const paymentMadeFormSchema = z.object({
notes: z.string().optional(),
details: z.array(detailsItemSchema).min(1, "At least one payment detail is required"),
}).superRefine((values, ctx) => {
const hasBill = Boolean(values.bill?.value)
const hasVendor = Boolean(values.vendor?.value)
const hasEmployee = Boolean(values.employee?.value)
if (values.payment_for === "bill" && !hasBill) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["bill"],
message: "Bill is required",
})
}
if (!hasVendor && !hasEmployee) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
@@ -56,6 +66,19 @@ const paymentMadeFormSchema = z.object({
message: "Vendor or employee is required",
})
}
if (hasVendor && hasEmployee) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["vendor"],
message: "Select either a vendor or an employee, not both",
})
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["employee"],
message: "Select either a vendor or an employee, not both",
})
}
})
type PaymentMadeFormValues = z.infer<typeof paymentMadeFormSchema>