garage-erp/apps/dashboard/modules/estimates/estimate-part-config-form.tsx
2026-04-15 04:59:05 +03:00

156 lines
5.0 KiB
TypeScript

"use client"
import React from "react"
import { z } from "zod"
import { AlertTriangle } from "lucide-react"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
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,
RhfTextareaField,
RhfAsyncSelectField,
} from "@/shared/components/form"
import { useAuthApi } from "@/shared/useApi"
import { toast } from "sonner"
import { DEPARTMENT_ROUTES } from "@garage/api"
// ── Schema ──
const schema = z.object({
quantity: z.coerce.number().min(1, "Quantity is required"),
rate: z.string().min(1, "Rate is required"),
department: z.object({ value: z.string(), label: z.string() }).nullable().optional(),
description: z.string().optional(),
})
const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label }
type FormValues = z.infer<typeof schema>
export type EstimatePartConfigFormProps = {
part: { id: number; title?: string; purchase_price?: string | number }
estimateId: string
onSuccess?: () => void
onCancel?: () => void
}
export function EstimatePartConfigForm({
part,
estimateId,
onSuccess,
onCancel,
}: EstimatePartConfigFormProps) {
const api = useAuthApi()
const form = useForm<FormValues>({
resolver: zodResolver(schema) as any,
defaultValues: {
quantity: 1,
rate: part.purchase_price != null ? String(part.purchase_price) : "",
department: null,
description: "",
},
})
const [error, setError] = React.useState<string | null>(null)
const [isPending, setIsPending] = React.useState(false)
async function handleSubmit(values: FormValues) {
setError(null)
setIsPending(true)
try {
const promise = api.estimates.addPart(estimateId, {
part_id: part.id,
quantity: values.quantity,
rate: values.rate,
department_id: values.department ? Number(values.department.value) : undefined,
description: values.description || undefined,
} as any)
toast.promise(promise, {
loading: "Adding part...",
success: "Part added successfully",
error: "Failed to add part",
})
await promise
form.reset()
onSuccess?.()
} catch (err: any) {
setError(err?.message ?? "An unexpected error occurred")
} finally {
setIsPending(false)
}
}
return (
<Rhform form={form} onSubmit={handleSubmit}>
{error && (
<Alert variant="destructive" className="mb-4">
<AlertTriangle className="me-2 h-4 w-4" />
<AlertTitle>Failed to add part</AlertTitle>
{error}
</Alert>
)}
<FieldGroup>
<div className="rounded-md bg-muted px-3 py-2 text-sm font-medium">
{part.title ?? `Part #${part.id}`}
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfTextField
name="quantity"
label="Quantity"
type="number"
placeholder="1"
required
/>
<RhfTextField
name="rate"
label="Rate"
type="number"
placeholder="0.00"
required
/>
</div>
<RhfTextareaField
name="description"
label="Description"
placeholder="Optional description"
rows={2}
/>
<RhfAsyncSelectField
name="department"
label="Department"
placeholder="Select department"
queryKey={[DEPARTMENT_ROUTES.INDEX]}
listFn={() => api.departments.list()}
mapOption={(item: any) => ({
value: String(item.id),
label: item.name ?? item.title ?? String(item.id),
})}
{...STORE_OBJECT}
/>
<div className="flex justify-end gap-2">
{onCancel && (
<Button type="button" variant="outline" onClick={onCancel} disabled={isPending}>
Cancel
</Button>
)}
<Button type="submit" disabled={isPending}>
{isPending ? "Adding..." : "Add Part"}
</Button>
</div>
</FieldGroup>
</Rhform>
)
}