"use client" import { AlertTriangle, Plus, Save, Trash2 } from "lucide-react" import { useFieldArray } from "react-hook-form" 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, RhfCheckboxField, } from "@/shared/components/form" import { ShopTypeInlineForm } from "@/modules/vehicles/inline-forms/shop-type-inline-form" import { InventoryCategoryInlineForm } from "@/modules/services/inline-forms/inventory-category-inline-form" import { UnitTypeInlineForm } from "@/modules/services/inline-forms/unit-type-inline-form" import { DepartmentInlineForm } from "@/modules/services/inline-forms/department-inline-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 { toId } from "@/shared/lib/utils" import { serviceGroupFormSchema, type ServiceGroupFormValues } from "./service-group.schema" import { SERVICE_GROUP_ROUTES } from "@garage/api" // ── Props ── export type ServiceGroupFormProps = { resourceId?: string | null initialData?: unknown onSuccess?: () => void } // ── Default values ── const DEFAULT_VALUES: ServiceGroupFormValues = { shop_type: null, inventory_category: null, unit_type: null, department: null, service_name: "", code: "", service_description: "", selling_price: undefined, selling_chart_of_account: "", show_as_lump_sum: false, mark_as_recommended: false, set_packaged_pricing: false, is_active: true, parts: [], services: [], } // ── Mapping helpers ── const mapLookupOption = (item: any) => ({ value: String(item.id), label: item.name ?? item.title ?? String(item.id), }) const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label } const partLineToForm = (r: any) => ({ id: r.id, part: r.part ? { value: String(r.part.id), label: r.part.title ?? r.part.name ?? `#${r.part.id}` } : null, quantity: r.quantity != null ? Number(r.quantity) : 1, rate: r.rate != null ? Number(r.rate) : 0, }) const serviceLineToForm = (r: any) => ({ id: r.id, service: r.service ? { value: String(r.service.id), label: r.service.title ?? r.service.name ?? `#${r.service.id}` } : null, rate: r.rate != null ? Number(r.rate) : 0, hours: r.hours != null ? Number(r.hours) : 0, }) function mapToFormValues(data: unknown): ServiceGroupFormValues { const d = (data as any)?.data ?? data ?? {} return { shop_type: null, inventory_category: null, unit_type: null, department: null, service_name: d.service_name ?? d.name ?? "", code: d.code ?? "", service_description: d.service_description ?? "", selling_price: d.selling_price ?? undefined, selling_chart_of_account: d.selling_chart_of_account ?? "", show_as_lump_sum: d.show_as_lump_sum ?? false, mark_as_recommended: d.mark_as_recommended ?? false, set_packaged_pricing: d.set_packaged_pricing ?? false, is_active: d.is_active ?? true, parts: Array.isArray(d._parts) ? d._parts.map(partLineToForm) : [], services: Array.isArray(d._services) ? d._services.map(serviceLineToForm) : [], } } function mapCreatePayload(values: ServiceGroupFormValues) { return { service_name: values.service_name, shop_type_id: toId(values.shop_type), code: values.code || undefined, inventory_category_id: toId(values.inventory_category), unit_type_id: toId(values.unit_type), department_id: toId(values.department), service_description: values.service_description || undefined, show_as_lump_sum: values.show_as_lump_sum, mark_as_recommended: values.mark_as_recommended, set_packaged_pricing: values.set_packaged_pricing, selling_price: values.selling_price, selling_chart_of_account: values.selling_chart_of_account || undefined, is_active: values.is_active, } } function mapUpdatePayload(values: ServiceGroupFormValues) { return { service_name: values.service_name, selling_price: values.selling_price, is_active: values.is_active, } } // ── Component ── export function ServiceGroupForm({ resourceId, initialData, onSuccess }: ServiceGroupFormProps) { const api = useAuthApi() const { form, isEditing } = useResourceForm({ schema: serviceGroupFormSchema, defaultValues: DEFAULT_VALUES, resourceId, initialData, queryKey: [SERVICE_GROUP_ROUTES.BY_ID, resourceId], // Fetch the group together with its existing parts/services so the // pickers can be pre-populated when editing. initialize: async (id) => { const [group, parts, services] = await Promise.all([ api.serviceGroups.show(id), api.serviceGroupParts.list({ service_group_id: id } as any), api.serviceGroupServices.list({ service_group_id: id } as any), ]) const g = (group as any)?.data ?? group ?? {} return { data: { ...g, _parts: (parts as any)?.data ?? [], _services: (services as any)?.data ?? [], }, } }, mapToFormValues, }) const partsArray = useFieldArray({ control: form.control, name: "parts" }) const servicesArray = useFieldArray({ control: form.control, name: "services" }) // Replace the package's parts/services on the server with the current form // rows: delete the existing rows, then recreate from the form. Mirrors the // "replace details" approach used elsewhere (e.g. payments). const syncIncludes = async (groupId: string | number, values: ServiceGroupFormValues) => { const gid = Number(groupId) const existingParts = await api.serviceGroupParts.list({ service_group_id: gid } as any) const existingPartIds = (((existingParts as any)?.data) ?? []).map((r: any) => r.id) await Promise.all(existingPartIds.map((id: any) => api.serviceGroupParts.destroy(String(id)))) await Promise.all( (values.parts ?? []) .filter((p) => p.part?.value) .map((p) => api.serviceGroupParts.create({ service_group_id: gid, part_id: Number(p.part!.value), quantity: Number(p.quantity ?? 1), rate: Number(p.rate ?? 0), } as any), ), ) const existingServices = await api.serviceGroupServices.list({ service_group_id: gid } as any) const existingServiceIds = (((existingServices as any)?.data) ?? []).map((r: any) => r.id) await Promise.all(existingServiceIds.map((id: any) => api.serviceGroupServices.destroy(String(id)))) await Promise.all( (values.services ?? []) .filter((s) => s.service?.value) .map((s) => api.serviceGroupServices.create({ service_group_id: gid, service_id: Number(s.service!.value), rate: Number(s.rate ?? 0), hours: Number(s.hours ?? 0), } as any), ), ) } const { mutate, error, isPending } = useFormMutation(form, { mutationFn: (values: ServiceGroupFormValues) => { const run = async () => { const groupRes: any = isEditing && resourceId ? await api.serviceGroups.update(resourceId, mapUpdatePayload(values)) : await api.serviceGroups.create(mapCreatePayload(values)) const groupId = resourceId ?? groupRes?.data?.id ?? groupRes?.id if (groupId) { await syncIncludes(groupId, values) } return groupRes } const promise = run() toast.promise(promise, { loading: isEditing ? "Updating package..." : "Creating package...", success: isEditing ? "Package updated" : "Package created", error: isEditing ? "Failed to update package" : "Failed to create package", }) return promise }, onSuccess: () => { form.reset() onSuccess?.() }, }) return ( mutate(values)}> {error && ( {isEditing ? "Failed to update service group" : "Failed to create service group"} {error.message} )}
{!isEditing && ( <>
api.shopTypes.list()} mapOption={mapLookupOption} createForm={(props) => } createLabel="Shop Type" {...STORE_OBJECT} /> api.inventory.listCategories()} mapOption={mapLookupOption} createForm={(props) => } createLabel="Category" {...STORE_OBJECT} />
api.inventory.listUnitTypes()} mapOption={mapLookupOption} createForm={(props) => } createLabel="Unit Type" {...STORE_OBJECT} /> api.departments.list()} mapOption={(item: any) => ({ value: String(item.id), label: item.name ?? String(item.id) })} createForm={(props) => } createLabel="Department" {...STORE_OBJECT} />
)}
{/* ── Parts in this package ── */}

Parts in this package

{partsArray.fields.length === 0 && (

No parts added yet.

)}
{partsArray.fields.map((field, index) => (
api.parts.list()} mapOption={mapLookupOption} {...STORE_OBJECT} />
))}
{/* ── Services in this package ── */}

Services in this package

{servicesArray.fields.length === 0 && (

No services added yet.

)}
{servicesArray.fields.map((field, index) => (
api.services.list()} mapOption={mapLookupOption} {...STORE_OBJECT} />
))}
) }