update forms

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-05-07 21:02:15 +03:00
co-authored by Copilot
parent dd32658500
commit 38565298fc
32 changed files with 797 additions and 236 deletions
@@ -8,6 +8,7 @@ import { FieldGroup } from "@/shared/components/ui/field"
import {
Rhform,
RhfTextField,
RhfSelectField,
RhfTextareaField,
RhfAsyncSelectField,
RhfDateField,
@@ -25,8 +26,9 @@ import {
purchaseOrderFormSchema,
type PurchaseOrderFormValues,
} from "./purchase-order.schema"
import { VENDOR_ROUTES, JOB_CARD_ROUTES, DEPARTMENT_ROUTES } from "@garage/api"
import { VENDOR_ROUTES, JOB_CARD_ROUTES, DEPARTMENT_ROUTES, InvoiceDiscount } from "@garage/api"
import { getFullName } from "@/shared/utils/getFullName"
import { useFormContext } from "react-hook-form"
// ── Props ──
@@ -47,6 +49,10 @@ const DEFAULT_VALUES: PurchaseOrderFormValues = {
order_date: new Date().toISOString().split("T")[0],
delivery_date: "",
notes: "",
terms_and_conditions: "",
discount_type: "no",
discount_amount: undefined,
label_ids: [],
items: [],
}
@@ -64,7 +70,11 @@ function mapToFormValues(data: unknown): PurchaseOrderFormValues {
delivery_date: d.delivery_date || "",
order_number: d.order_number || "" as any,
notes: d.notes || "",
items: (d.parts ?? []).map((p: any) => ({
terms_and_conditions: d.terms_and_conditions || "",
discount_type: d.discount_type || "no",
discount_amount: d.discount_amount != null ? Number(d.discount_amount) : undefined,
label_ids: Array.isArray(d.label_ids) ? d.label_ids.map((id: any) => Number(id)).filter((id: number) => Number.isFinite(id)) : [],
items: (d.parts ?? d.items ?? []).map((p: any) => ({
part_id: p.part_id ?? p.id,
title: p.part?.title ?? p.title ?? "",
quantity: p.quantity ?? 1,
@@ -84,6 +94,10 @@ function mapFormToPayload(values: PurchaseOrderFormValues) {
order_number: values.order_number,
delivery_date: values.delivery_date || undefined,
notes: values.notes || undefined,
terms_and_conditions: values.terms_and_conditions || undefined,
discount_type: values.discount_type || undefined,
discount_amount: values.discount_type === "transaction_level" ? (values.discount_amount ?? 0) : undefined,
label_ids: values.label_ids?.length ? values.label_ids : undefined,
items: (values.items ?? []).map((item) => ({
part_id: item.part_id,
quantity: item.quantity,
@@ -93,6 +107,27 @@ function mapFormToPayload(values: PurchaseOrderFormValues) {
}
}
const DISCOUNT_OPTIONS = InvoiceDiscount.map((v) => ({
value: v,
label: v.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()),
}))
function TransactionDiscountField() {
const { watch } = useFormContext<PurchaseOrderFormValues>()
const discount = watch("discount_type")
if (discount !== "transaction_level") return null
return (
<RhfTextField
name="discount_amount"
label="Discount Amount"
type="number"
placeholder="0.00"
/>
)
}
// ── Shared mapOption for async selects ──
const mapLookupOption = (item: any) => ({
@@ -156,17 +191,27 @@ export function PurchaseOrderForm({ resourceId, initialData, onSuccess }: Purcha
<FieldGroup>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfTextField name="title" label="Title" placeholder="Enter purchase order title" required />
<RhfTextField name="title" label="Title" placeholder="Enter purchase order title" />
<RhfAutoGenerateField autoFetch table="purchase-order" name="order_number" label="Order Number" placeholder="Enter purchase order number" required />
<RhfDateField name="order_date" label="Order Date" />
<RhfDateField name="delivery_date" label="Delivery Date" />
</div>
<RhfSelectField
name="discount_type"
label="Discount Type"
placeholder="Select discount type"
options={DISCOUNT_OPTIONS}
/>
<TransactionDiscountField />
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfAsyncSelectField
name="vendor"
label="Vendor"
placeholder="Select vendor"
required
queryKey={[VENDOR_ROUTES.INDEX]}
listFn={() => api.vendors.list()}
mapOption={mapVendorOption}
@@ -185,6 +230,7 @@ export function PurchaseOrderForm({ resourceId, initialData, onSuccess }: Purcha
name="department"
label="Department"
placeholder="Select department"
required
queryKey={[DEPARTMENT_ROUTES.INDEX]}
listFn={() => api.departments.list()}
mapOption={mapLookupOption}
@@ -196,6 +242,7 @@ export function PurchaseOrderForm({ resourceId, initialData, onSuccess }: Purcha
name="job_card"
label="Job Card"
placeholder="Select job card"
required
queryKey={[JOB_CARD_ROUTES.INDEX]}
listFn={() => api.jobCards.list()}
mapOption={(item: any) => ({
@@ -206,6 +253,7 @@ export function PurchaseOrderForm({ resourceId, initialData, onSuccess }: Purcha
/>
<RhfTextareaField name="notes" label="Notes" rows={3} />
<RhfTextareaField name="terms_and_conditions" label="Terms & Conditions" rows={3} />
<PartsSelectorField<PurchaseOrderFormValues, "items"> name="items" />
@@ -1,9 +1,34 @@
import { z } from "zod"
import { InvoiceDiscount } from "@garage/api"
const relationFieldSchema = z
.object({ value: z.string(), label: z.string() })
.nullable()
const requiredRelationFieldSchema = relationFieldSchema.superRefine((value, ctx) => {
if (!value?.value) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "This field is required",
})
}
})
const optionalDateSchema = z.preprocess(
(value) => (value === "" || value == null ? undefined : value),
z.string().date().optional(),
)
const optionalStringMax255Schema = z.preprocess(
(value) => (value === "" || value == null ? undefined : value),
z.string().max(255, "Must be at most 255 characters").optional(),
)
const optionalNumberSchema = z.preprocess(
(value) => (value === "" || value == null ? undefined : value),
z.coerce.number().min(0).optional(),
)
const purchaseOrderItemSchema = z.object({
part_id: z.number(),
title: z.string(),
@@ -14,16 +39,20 @@ const purchaseOrderItemSchema = z.object({
const purchaseOrderFormSchema = z.object({
// ── Relations ──
vendor: relationFieldSchema,
job_card: relationFieldSchema,
department: relationFieldSchema,
vendor: requiredRelationFieldSchema,
job_card: requiredRelationFieldSchema,
department: requiredRelationFieldSchema,
// ── Basic info ──
title: z.string().min(1, "Title is required"),
order_date: z.string().optional(),
delivery_date: z.string().optional(),
title: optionalStringMax255Schema,
order_date: optionalDateSchema,
delivery_date: optionalDateSchema,
notes: z.string().optional(),
order_number: z.string().min(1, "Order number is required"),
terms_and_conditions: z.string().optional(),
discount_type: z.enum(InvoiceDiscount).optional(),
discount_amount: optionalNumberSchema,
label_ids: z.array(z.number()).optional(),
order_number: z.string().trim().min(1, "Order number is required").max(255, "Order number must be at most 255 characters"),
// ── Items (parts) ──
items: z.array(purchaseOrderItemSchema).optional(),