@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user