filters , order inspections , inventory items , credit notes

This commit is contained in:
2026-04-08 14:38:16 +03:00
parent 90c84a0bda
commit bb02b77be2
33 changed files with 2552 additions and 145 deletions
@@ -0,0 +1,282 @@
"use client"
import { z } from "zod"
import { parseAsInteger, parseAsString, parseAsBoolean } from "nuqs"
import { toRelation, toId, type RelationFieldValue } from "@/shared/lib/utils"
import {
RhfAsyncSelectField,
RhfDateField,
RhfCheckboxField,
RhfSelectField,
} from "@/shared/components/form"
import { useAuthApi } from "@/shared/useApi"
import {
CUSTOMER_ROUTES,
EMPLOYEE_ROUTES,
DEPARTMENT_ROUTES,
VEHICLE_ROUTES,
LABEL_ROUTES,
INSURANCE_TYPE_ROUTES,
JobCardStatus,
} from "@garage/api"
import { formatEnum } from "@/shared/utils/formatters"
import { Separator } from "@/shared/components/ui/separator"
import type { UseFilterParamsOptions } from "@/shared/hooks/use-filter-params"
// ── Schema ──
const relationField = z.object({ value: z.string(), label: z.string() }).nullable().optional()
const jobCardFilterSchema = z.object({
customer_id: relationField,
insurer_id: relationField,
vehicle_id: relationField,
department_id: relationField,
sales_person_id: relationField,
primary_technician_id: relationField,
service_writer_id: relationField,
label_ids: z.string().optional(),
check_in_date: z.string().optional(),
check_out_date: z.string().optional(),
delivery_date: z.string().optional(),
start_date: z.string().optional(),
end_date: z.string().optional(),
convert_invoice: z.boolean().optional(),
has_start_or_delivery_date: z.boolean().optional(),
has_start_date: z.boolean().optional(),
has_delivery_date: z.boolean().optional(),
has_both_dates: z.boolean().optional(),
})
type JobCardFilterValues = z.infer<typeof jobCardFilterSchema>
// ── Defaults ──
const defaultValues: JobCardFilterValues = {
customer_id: null,
insurer_id: null,
vehicle_id: null,
department_id: null,
sales_person_id: null,
primary_technician_id: null,
service_writer_id: null,
label_ids: "",
check_in_date: "",
check_out_date: "",
delivery_date: "",
start_date: "",
end_date: "",
convert_invoice: false,
has_start_or_delivery_date: false,
has_start_date: false,
has_delivery_date: false,
has_both_dates: false,
}
// ── nuqs Parsers ──
const paramsParsers = {
customer_id: parseAsInteger,
insurer_id: parseAsInteger,
vehicle_id: parseAsInteger,
department_id: parseAsInteger,
sales_person_id: parseAsInteger,
primary_technician_id: parseAsInteger,
service_writer_id: parseAsInteger,
label_ids: parseAsString,
check_in_date: parseAsString,
check_out_date: parseAsString,
delivery_date: parseAsString,
start_date: parseAsString,
end_date: parseAsString,
convert_invoice: parseAsBoolean,
has_start_or_delivery_date: parseAsBoolean,
has_start_date: parseAsBoolean,
has_delivery_date: parseAsBoolean,
has_both_dates: parseAsBoolean,
}
// ── Mappers ──
function mapParamsToFormValues(params: Record<string, any>): Partial<JobCardFilterValues> {
return {
customer_id: params.customer_id ? toRelation(params.customer_id) : null,
insurer_id: params.insurer_id ? toRelation(params.insurer_id) : null,
vehicle_id: params.vehicle_id ? toRelation(params.vehicle_id) : null,
department_id: params.department_id ? toRelation(params.department_id) : null,
sales_person_id: params.sales_person_id ? toRelation(params.sales_person_id) : null,
primary_technician_id: params.primary_technician_id ? toRelation(params.primary_technician_id) : null,
service_writer_id: params.service_writer_id ? toRelation(params.service_writer_id) : null,
label_ids: params.label_ids ?? "",
check_in_date: params.check_in_date ?? "",
check_out_date: params.check_out_date ?? "",
delivery_date: params.delivery_date ?? "",
start_date: params.start_date ?? "",
end_date: params.end_date ?? "",
convert_invoice: params.convert_invoice ?? false,
has_start_or_delivery_date: params.has_start_or_delivery_date ?? false,
has_start_date: params.has_start_date ?? false,
has_delivery_date: params.has_delivery_date ?? false,
has_both_dates: params.has_both_dates ?? false,
}
}
function mapFormValuesToParams(values: JobCardFilterValues): Record<string, any> {
return {
customer_id: toId(values.customer_id as RelationFieldValue) ?? null,
insurer_id: toId(values.insurer_id as RelationFieldValue) ?? null,
vehicle_id: toId(values.vehicle_id as RelationFieldValue) ?? null,
department_id: toId(values.department_id as RelationFieldValue) ?? null,
sales_person_id: toId(values.sales_person_id as RelationFieldValue) ?? null,
primary_technician_id: toId(values.primary_technician_id as RelationFieldValue) ?? null,
service_writer_id: toId(values.service_writer_id as RelationFieldValue) ?? null,
label_ids: values.label_ids || null,
check_in_date: values.check_in_date || null,
check_out_date: values.check_out_date || null,
delivery_date: values.delivery_date || null,
start_date: values.start_date || null,
end_date: values.end_date || null,
convert_invoice: values.convert_invoice || null,
has_start_or_delivery_date: values.has_start_or_delivery_date || null,
has_start_date: values.has_start_date || null,
has_delivery_date: values.has_delivery_date || null,
has_both_dates: values.has_both_dates || null,
}
}
// ── Filter Config (for useFilterParams) ──
export const jobCardFilterConfig: UseFilterParamsOptions<JobCardFilterValues> = {
schema: jobCardFilterSchema,
defaultValues,
paramsParsers,
mapParamsToFormValues,
mapFormValuesToParams,
}
// ── Filter Fields Component ──
function SectionLabel({ children }: { children: React.ReactNode }) {
return <p className="text-sm font-medium text-muted-foreground pt-2">{children}</p>
}
export function JobCardFilterFields() {
const api = useAuthApi()
const employeeMapOption = (item: any) => ({
value: String(item.id),
label: [item.first_name, item.last_name].filter(Boolean).join(" ") || `Employee #${item.id}`,
})
const customerMapOption = (item: any) => {
const name = [item.first_name, item.last_name].filter(Boolean).join(" ")
return {
value: String(item.id),
label: name || item.company_name || `Customer #${item.id}`,
}
}
const vehicleMapOption = (item: any) => ({
value: String(item.id),
label: [item.year, item.make, item.model].filter(Boolean).join(" ") || `Vehicle #${item.id}`,
})
return (
<>
{/* ── Relations ── */}
<SectionLabel>Relations</SectionLabel>
<RhfAsyncSelectField
name="customer_id"
label="Customer"
queryKey={[CUSTOMER_ROUTES.INDEX]}
listFn={() => api.customers.list({ per_page: 100 })}
mapOption={customerMapOption}
placeholder="All customers"
/>
<RhfAsyncSelectField
name="vehicle_id"
label="Vehicle"
queryKey={[VEHICLE_ROUTES.INDEX]}
listFn={() => api.vehicles.list({ per_page: 100 })}
mapOption={vehicleMapOption}
placeholder="All vehicles"
/>
<RhfAsyncSelectField
name="insurer_id"
label="Insurance Type"
queryKey={[INSURANCE_TYPE_ROUTES.INDEX]}
listFn={() => api.insuranceTypes.list({ per_page: 100 })}
mapOption={(item: any) => ({ value: String(item.id), label: item.title ?? item.name ?? `#${item.id}` })}
placeholder="All insurers"
/>
<RhfAsyncSelectField
name="department_id"
label="Department"
queryKey={[DEPARTMENT_ROUTES.INDEX]}
listFn={() => api.departments.list({ per_page: 100 })}
mapOption={(item: any) => ({ value: String(item.id), label: item.title ?? item.name ?? `#${item.id}` })}
placeholder="All departments"
/>
<Separator />
<SectionLabel>Assigned Staff</SectionLabel>
<RhfAsyncSelectField
name="sales_person_id"
label="Sales Person"
queryKey={[EMPLOYEE_ROUTES.INDEX, "sales_person"]}
listFn={() => api.employees.list({ per_page: 100 })}
mapOption={employeeMapOption}
placeholder="All sales persons"
/>
<RhfAsyncSelectField
name="primary_technician_id"
label="Primary Technician"
queryKey={[EMPLOYEE_ROUTES.INDEX, "technician"]}
listFn={() => api.employees.list({ per_page: 100 })}
mapOption={employeeMapOption}
placeholder="All technicians"
/>
<RhfAsyncSelectField
name="service_writer_id"
label="Service Writer"
queryKey={[EMPLOYEE_ROUTES.INDEX, "service_writer"]}
listFn={() => api.employees.list({ per_page: 100 })}
mapOption={employeeMapOption}
placeholder="All service writers"
/>
{/* ── Dates ── */}
<Separator />
<SectionLabel>Dates</SectionLabel>
<div className="grid grid-cols-2 gap-3">
<RhfDateField name="check_in_date" label="Check-in Date" />
<RhfDateField name="check_out_date" label="Check-out Date" />
</div>
<RhfDateField name="delivery_date" label="Delivery Date" />
<div className="grid grid-cols-2 gap-3">
<RhfDateField name="start_date" label="Start Date" />
<RhfDateField name="end_date" label="End Date" />
</div>
{/* ── Flags ── */}
<Separator />
<SectionLabel>Flags</SectionLabel>
<RhfCheckboxField name="convert_invoice" label="Convert to Invoice" />
<RhfCheckboxField name="has_start_or_delivery_date" label="Has Start or Delivery Date" />
<RhfCheckboxField name="has_start_date" label="Has Start Date" />
<RhfCheckboxField name="has_delivery_date" label="Has Delivery Date" />
<RhfCheckboxField name="has_both_dates" label="Has Both Dates" />
</>
)
}