This commit is contained in:
2026-04-06 02:32:47 +03:00
parent 5f3d208158
commit 24a44481a0
152 changed files with 85913 additions and 44338 deletions
@@ -1,17 +1,9 @@
"use client"
import { useState, useRef } from "react"
import { useMutation, useQuery } from "@tanstack/react-query"
import { useState } from "react"
import { useMutation } from "@tanstack/react-query"
import { useAuthApi } from "@/shared/useApi"
import { useRouter } from "next/navigation"
import { Button } from "@/shared/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/shared/components/ui/dropdown-menu"
import {
Dialog,
DialogContent,
@@ -19,39 +11,55 @@ import {
DialogTitle,
DialogDescription,
} from "@/shared/components/ui/dialog"
import {
Combobox,
ComboboxInput,
ComboboxContent,
ComboboxList,
ComboboxItem,
ComboboxEmpty,
} from "@/shared/components/ui/combobox"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/shared/components/ui/popover"
import { Calendar } from "@/shared/components/ui/calendar"
import { confirm } from "@/shared/components/confirm-dialog"
import { toast } from "sonner"
import { Ellipsis, Pencil, Trash2, CalendarIcon, UserCog, UserCheck, Loader2 } from "lucide-react"
import { Pencil, CalendarIcon, UserCog, UserCheck, Loader2 } from "lucide-react"
import { format } from "date-fns"
import { EMPLOYEE_ROUTES } from "@garage/api"
import { EmployeeCombobox } from "@/modules/employees/employee-combobox"
type JobCardActionsProps = {
jobCardId: string
orderDate?: string | null
serviceWriterName?: string | null
salesPersonName?: string | null
}
type Employee = {
id: number
first_name?: string
last_name?: string
name?: string
}
// ── Informative Action Card ──
function getEmployeeName(emp: Employee) {
return emp.name || [emp.first_name, emp.last_name].filter(Boolean).join(" ") || `Employee #${emp.id}`
function ActionCard({
icon: Icon,
label,
value,
isPending,
}: {
icon: React.ComponentType<{ className?: string }>
label: string
value?: string | null
isPending?: boolean
}) {
return (
<div className="group relative flex items-center gap-3 rounded-lg border bg-card px-4 py-3 text-left transition-colors hover:bg-accent hover:text-accent-foreground cursor-pointer select-none">
<div className="flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground transition-colors group-hover:bg-background">
<Icon className="size-4" />
</div>
<div className="flex flex-col gap-0.5 overflow-hidden">
<span className="text-[11px] font-medium text-muted-foreground leading-none">{label}</span>
<span className="text-sm font-semibold leading-tight truncate">
{value ?? <span className="font-normal text-muted-foreground italic">Not set</span>}
</span>
</div>
{isPending ? (
<Loader2 className="ml-2 size-3.5 shrink-0 text-muted-foreground animate-spin" />
) : (
<Pencil className="ml-2 size-3 shrink-0 text-muted-foreground opacity-0 group-hover:opacity-60 transition-opacity" />
)}
</div>
)
}
// ── Employee Picker Dialog ──
@@ -61,8 +69,6 @@ type EmployeePickerDialogProps = {
onOpenChange: (open: boolean) => void
title: string
description: string
employees: Employee[]
loading: boolean
isPending: boolean
onSelect: (employeeId: number) => void
}
@@ -72,18 +78,9 @@ function EmployeePickerDialog({
onOpenChange,
title,
description,
employees,
loading,
isPending,
onSelect,
}: EmployeePickerDialogProps) {
const anchorRef = useRef<HTMLDivElement>(null)
const handleSelect = (emp: Employee | null) => {
if (!emp) return
onSelect(emp.id)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md">
@@ -91,36 +88,15 @@ function EmployeePickerDialog({
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<div ref={anchorRef}>
<Combobox
value={null}
onValueChange={handleSelect}
disabled={isPending}
>
<ComboboxInput
placeholder="Search employees..."
showClear={false}
/>
<ComboboxContent anchor={anchorRef}>
<ComboboxList>
{loading && (
<div className="flex items-center justify-center py-4">
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
</div>
)}
{!loading &&
employees.map((emp) => (
<ComboboxItem key={emp.id} value={emp}>
{getEmployeeName(emp)}
</ComboboxItem>
))}
{!loading && employees.length === 0 && (
<ComboboxEmpty>No employees found</ComboboxEmpty>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
<EmployeeCombobox
value={null}
onValueChange={(emp) => {
if (emp) onSelect(Number(emp.value))
}}
disabled={isPending}
placeholder="Search employees..."
showClear={false}
/>
</DialogContent>
</Dialog>
)
@@ -128,42 +104,13 @@ function EmployeePickerDialog({
// ── Main Component ──
export function JobCardActions({ jobCardId }: JobCardActionsProps) {
export function JobCardActions({ jobCardId, orderDate, serviceWriterName, salesPersonName }: JobCardActionsProps) {
const api = useAuthApi()
const router = useRouter()
const [datePickerOpen, setDatePickerOpen] = useState(false)
const [serviceWriterDialogOpen, setServiceWriterDialogOpen] = useState(false)
const [salesPersonDialogOpen, setSalesPersonDialogOpen] = useState(false)
const { data: employeesData, isLoading: employeesLoading } = useQuery({
queryKey: [EMPLOYEE_ROUTES.INDEX],
queryFn: () => api.employees.list(),
})
const employees: Employee[] = (employeesData as any)?.data ?? []
const handleEdit = () => {
router.push(`/sales/job-cards/${jobCardId}/edit`)
}
const handleDelete = async () => {
const confirmed = await confirm({
title: "Delete Job Card",
description: "Are you sure you want to delete this job card? This action cannot be undone.",
confirmLabel: "Delete",
variant: "destructive",
})
if (confirmed) {
const promise = api.jobCards.destroy(jobCardId)
toast.promise(promise, {
loading: "Deleting job card...",
success: "Job card deleted successfully",
error: "Failed to delete job card",
})
await promise
router.push("/sales/job-cards")
}
}
const changeDateMutation = useMutation({
mutationFn: (date: Date) => {
@@ -215,15 +162,20 @@ export function JobCardActions({ jobCardId }: JobCardActionsProps) {
})
return (
<div className="flex items-center gap-1">
<div className="flex flex-wrap items-stretch gap-2">
{/* Check-in Date Action Card */}
<Popover open={datePickerOpen} onOpenChange={setDatePickerOpen}>
<PopoverTrigger asChild>
<Button variant="outline" size="sm">
<CalendarIcon className="size-4" />
<span className="hidden sm:inline">Change Date</span>
</Button>
<button type="button" className="focus:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-lg">
<ActionCard
icon={CalendarIcon}
label="Order Date"
value={orderDate ? new Date(orderDate).toLocaleDateString() : null}
isPending={changeDateMutation.isPending}
/>
</button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="end">
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
onSelect={(date) => {
@@ -234,42 +186,42 @@ export function JobCardActions({ jobCardId }: JobCardActionsProps) {
</PopoverContent>
</Popover>
<Button variant="outline" size="sm" onClick={() => setServiceWriterDialogOpen(true)}>
<UserCog className="size-4" />
<span className="hidden sm:inline">Service Writer</span>
</Button>
{/* Service Writer Action Card */}
<button
type="button"
className="focus:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-lg"
onClick={() => setServiceWriterDialogOpen(true)}
>
<ActionCard
icon={UserCog}
label="Service Writer"
value={serviceWriterName ?? null}
isPending={changeServiceWriterMutation.isPending}
/>
</button>
<Button variant="outline" size="sm" onClick={() => setSalesPersonDialogOpen(true)}>
<UserCheck className="size-4" />
<span className="hidden sm:inline">Sales Person</span>
</Button>
{/* Sales Person Action Card */}
<button
type="button"
className="focus:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-lg"
onClick={() => setSalesPersonDialogOpen(true)}
>
<ActionCard
icon={UserCheck}
label="Sales Person"
value={salesPersonName ?? null}
isPending={changeSalesPersonMutation.isPending}
/>
</button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<Ellipsis className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={handleEdit}>
<Pencil className="size-4" />
Edit
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive" onClick={handleDelete}>
<Trash2 className="size-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{/* Edit / Delete Dropdown */}
<EmployeePickerDialog
open={serviceWriterDialogOpen}
onOpenChange={setServiceWriterDialogOpen}
title="Change Service Writer"
description="Search and select an employee to assign as service writer."
employees={employees}
loading={employeesLoading}
isPending={changeServiceWriterMutation.isPending}
onSelect={(id) => changeServiceWriterMutation.mutate(id)}
/>
@@ -279,8 +231,6 @@ export function JobCardActions({ jobCardId }: JobCardActionsProps) {
onOpenChange={setSalesPersonDialogOpen}
title="Change Sales Person"
description="Search and select an employee to assign as sales person."
employees={employees}
loading={employeesLoading}
isPending={changeSalesPersonMutation.isPending}
onSelect={(id) => changeSalesPersonMutation.mutate(id)}
/>
@@ -2,31 +2,26 @@
import { createContext, useContext, useState, useCallback } from "react"
import type { JobCardStatus } from "./job-card.schema"
type JobCardContextValue = {
id: string
label: string
status: JobCardStatus
setStatus: (status: JobCardStatus) => void
}
const JobCardContext = createContext<JobCardContextValue | null>(null)
import { JobCardResponseData } from "@garage/api"
const JobCardContext = createContext<JobCardResponseData | null>(null)
export function JobCardProvider({
jobCard,
children,
}: {
jobCard: { id: string; label: string; status: JobCardStatus }
jobCard: JobCardResponseData
children: React.ReactNode
}) {
const [status, setStatusState] = useState<JobCardStatus>(jobCard.status)
const [status, setStatusState] = useState<JobCardStatus>(jobCard.status as JobCardStatus)
const setStatus = useCallback((newStatus: JobCardStatus) => {
setStatusState(newStatus)
}, [])
return (
<JobCardContext.Provider value={{ id: jobCard.id, label: jobCard.label, status, setStatus }}>
<JobCardContext.Provider value={{ ...jobCard, status, setStatus } as JobCardResponseData}>
{children}
</JobCardContext.Provider>
)
@@ -0,0 +1,56 @@
"use client"
import { confirm } from '@/shared/components/confirm-dialog';
import { api } from '@garage/api';
import { useRouter } from 'next/dist/client/components/navigation';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/shared/components/ui/dropdown-menu'
import { Button } from '@/shared/components/ui/button'
import { toast } from 'sonner'
import { Ellipsis, Pencil, Trash2 } from 'lucide-react';
export default function JobCardDropdown({ id }: { id: string }) {
const router = useRouter();
const handleEdit = () => {
router.push(`/sales/job-cards/${id}/edit`)
}
const handleDelete = async () => {
const confirmed = await confirm({
title: "Delete Job Card",
description: "Are you sure you want to delete this job card? This action cannot be undone.",
confirmLabel: "Delete",
variant: "destructive",
})
if (confirmed) {
const promise = api.jobCards.destroy(id)
toast.promise(promise, {
loading: "Deleting job card...",
success: "Job card deleted successfully",
error: "Failed to delete job card",
})
await promise
router.push("/sales/job-cards")
}
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className="self-stretch h-auto aspect-square">
<Ellipsis className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={handleEdit}>
<Pencil className="size-4" />
Edit
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem variant="destructive" onClick={handleDelete}>
<Trash2 className="size-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
@@ -10,6 +10,8 @@ import {
RhfTextField,
RhfSelectField,
RhfAsyncSelectField,
RhfDateField,
RhfTimeField,
} from "@/shared/components/form"
import { toast } from "sonner"
import { useAuthApi } from "@/shared/useApi"
@@ -23,8 +25,13 @@ import {
TAX_INCLUSIVE_OPTIONS,
DISCOUNT_TYPE_OPTIONS,
DISCOUNT_AT_OPTIONS,
ESTIMATE_TO_OPTIONS,
FUEL_LEVEL_OPTIONS,
JOB_CARD_STATUS_OPTIONS,
} from "./job-card.schema"
import { JOB_CARD_ROUTES, CUSTOMER_ROUTES, VEHICLE_ROUTES } from "@garage/api"
import { JOB_CARD_ROUTES, EMPLOYEE_ROUTES, DEPARTMENT_ROUTES } from "@garage/api"
import { RhfCustomerSelectField } from "@/modules/customers/rhf-customer-select-field"
import { RhfVehicleSelectField } from "@/modules/vehicles/rhf-vehicle-select-field"
// ── Props ──
@@ -40,10 +47,17 @@ const DEFAULT_VALUES: JobCardFormValues = {
title: "",
customer: null,
vehicle: null,
status: "draft",
department: null,
service_writer: null,
status: "check_in",
estimate_to: "Customer",
tax_inclusive: "Tax Inclusive",
discount_type: "no",
discount_at: "inclusive_of_tax",
// check_in_date: "",
// check_in_time: (() => { const n = new Date(); return `${String(n.getHours()).padStart(2,"0")}:${String(n.getMinutes()).padStart(2,"0")}:${String(n.getSeconds()).padStart(2,"0")}` })(),
km_in: "",
fuel_level: "",
}
// ── Mapping helpers ──
@@ -53,12 +67,19 @@ function mapToFormValues(data: unknown): JobCardFormValues {
return {
title: d.title || "",
customer: toRelation(d.customer_id, d.customer_name),
vehicle: toRelation(d.vehicle_id, d.vehicle_name),
customer: toRelation(d.customer_id, d.customer ? `${d.customer.first_name} ${d.customer.last_name}` : undefined),
vehicle: toRelation(d.vehicle_id, d.vehicle ? `${d.vehicle.make} ${d.vehicle.model}` : undefined),
department: toRelation(d.department_id, d.department?.name),
service_writer: toRelation(d.service_writer_id, d.service_writer ? `${d.service_writer.first_name} ${d.service_writer.last_name}` : undefined),
status: d.status || "draft",
estimate_to: d.estimate_to || "Customer",
tax_inclusive: d.tax_inclusive || "Tax Inclusive",
discount_type: d.discount_type || "no",
discount_at: d.discount_at || "inclusive_of_tax",
check_in_date: d.check_in_date ? d.check_in_date.split("T")[0] : "",
check_in_time: d.check_in_time ? d.check_in_time.split("T")[0] : "",
km_in: d.km_in != null ? String(d.km_in) : "",
fuel_level: d.fuel_level || "",
}
}
@@ -67,10 +88,17 @@ function mapFormToPayload(values: JobCardFormValues) {
title: values.title,
customer_id: toId(values.customer),
vehicle_id: toId(values.vehicle),
department_id: toId(values.department),
service_writer_id: toId(values.service_writer),
status: values.status || undefined,
estimate_to: values.estimate_to || undefined,
tax_inclusive: values.tax_inclusive || undefined,
discount_type: values.discount_type || undefined,
discount_at: values.discount_at || undefined,
check_in_date: values.check_in_date || undefined,
check_in_time: values.check_in_time || undefined,
km_in: values.km_in ? Number(values.km_in) : undefined,
fuel_level: values.fuel_level || undefined,
}
}
@@ -81,14 +109,9 @@ const mapLookupOption = (item: any) => ({
label: item.name,
})
const mapCustomerOption = (item: any) => ({
const mapEmployeeOption = (item: any) => ({
value: String(item.id),
label: [item.first_name, item.last_name].filter(Boolean).join(" "),
})
const mapVehicleOption = (item: any) => ({
value: String(item.id),
label: [item.year, item.make_name, item.model_name].filter(Boolean).join(" "),
label: `${item.first_name} ${item.last_name}`,
})
const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label }
@@ -141,27 +164,62 @@ export function JobCardForm({ resourceId, initialData, onSuccess }: JobCardFormP
<FieldGroup>
<RhfTextField name="title" label="Title" placeholder="Job Card 001" required />
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfCustomerSelectField name="customer" />
<RhfVehicleSelectField name="vehicle" />
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
{/* <RhfSelectField
name="status"
label="Status"
placeholder="Select status"
options={JOB_CARD_STATUS_OPTIONS}
/> */}
<RhfSelectField
name="estimate_to"
label="Estimate To"
placeholder="Select estimate to"
options={ESTIMATE_TO_OPTIONS}
/>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfAsyncSelectField
name="customer"
label="Customer"
placeholder="Select customer"
queryKey={[CUSTOMER_ROUTES.INDEX]}
listFn={() => api.customers.list()}
mapOption={mapCustomerOption}
name="department"
label="Department"
placeholder="Select department"
queryKey={[DEPARTMENT_ROUTES.INDEX]}
listFn={() => api.departments.list()}
mapOption={mapLookupOption}
{...STORE_OBJECT}
/>
<RhfAsyncSelectField
name="vehicle"
label="Vehicle"
placeholder="Select vehicle"
queryKey={[VEHICLE_ROUTES.INDEX]}
listFn={() => api.vehicles.list()}
mapOption={mapVehicleOption}
name="service_writer"
label="Service Writer"
placeholder="Select service writer"
queryKey={[EMPLOYEE_ROUTES.INDEX]}
listFn={() => api.employees.list()}
mapOption={mapEmployeeOption}
{...STORE_OBJECT}
/>
</div>
{/* <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfDateField name="check_in_date" label="Check-in Date" />
<RhfTimeField name="check_in_time" label="Check-in Time" withSeconds />
</div> */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfTextField name="km_in" label="KM In" placeholder="50000" type="number" />
<RhfSelectField
name="fuel_level"
label="Fuel Level"
placeholder="Select fuel level"
options={FUEL_LEVEL_OPTIONS}
/>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<RhfSelectField
name="tax_inclusive"
@@ -20,37 +20,15 @@ import {
} from "@/shared/components/ui/card"
import { Badge } from "@/shared/components/ui/badge"
import { Separator } from "@/shared/components/ui/separator"
import { JobCardActions } from "./job-card-actions"
import { getVehicleLabel } from "../vehicles/utils/getVehicleLabel"
import { JobCardResponseData } from "@garage/api"
import { getFullName } from "@/shared/utils/getFullName"
type JobCardData = {
id?: number
title?: string
status?: string
check_in_date?: string
km_in?: number
tax_inclusive?: string
discount_type?: string
discount_at?: string
customer_id?: number
customer_name?: string
vehicle_id?: number
vehicle_name?: string
department_id?: number
department_name?: string
sales_person_id?: number
sales_person_name?: string
service_writer_id?: number
service_writer_name?: string
purchase_orders_count?: number
bills_count?: number
expenses_count?: number
tasks_count?: number
appointments_count?: number
created_at?: string
updated_at?: string
}
type JobCardGeneralInfoProps = {
jobCard: JobCardData
jobCard: JobCardResponseData
}
function InfoItem({
@@ -86,7 +64,7 @@ const statusColorMap: Record<string, string> = {
cancelled: "destructive",
}
export function JobCardGeneralInfo({ jobCard }: JobCardGeneralInfoProps) {
export function JobCardGeneralInfo({ jobCard }: { jobCard: JobCardResponseData }) {
const formatStatus = (status?: string) => {
if (!status) return null
return status
@@ -95,9 +73,17 @@ export function JobCardGeneralInfo({ jobCard }: JobCardGeneralInfoProps) {
.join(" ")
}
console.log(jobCard)
return (
<div className="grid gap-6 md:grid-cols-2">
{/* Job Card Details */}
<div className="flex flex-col gap-6">
{jobCard.service_writer?.first_name}
<JobCardActions
jobCardId={String(jobCard.id)}
orderDate={jobCard.order_date ?? null}
serviceWriterName={jobCard.service_writer?.first_name}
salesPersonName={jobCard.sales_person?.first_name}
/>
<div className="grid gap-6 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
@@ -155,27 +141,27 @@ export function JobCardGeneralInfo({ jobCard }: JobCardGeneralInfoProps) {
<InfoItem
icon={Users}
label="Customer"
value={jobCard.customer_name}
value={getFullName(jobCard.customer)}
/>
<InfoItem
<InfoItem
icon={Car}
label="Vehicle"
value={jobCard.vehicle_name}
value={getVehicleLabel(jobCard.vehicle as any)}
/>
<InfoItem
icon={Building2}
label="Department"
value={jobCard.department_name}
value={jobCard.department as any}
/>
<InfoItem
icon={Briefcase}
label="Sales Person"
value={jobCard.sales_person_name}
value={getFullName(jobCard.sales_person as any)}
/>
<InfoItem
icon={UserCheck}
label="Service Writer"
value={jobCard.service_writer_name}
value={getFullName(jobCard.service_writer as any)}
/>
</div>
</CardContent>
@@ -244,6 +230,7 @@ export function JobCardGeneralInfo({ jobCard }: JobCardGeneralInfoProps) {
/>
</CardContent>
</Card>
</div>
</div>
)
}
@@ -70,7 +70,7 @@ export function JobCardStatusStepper({ jobCardId }: JobCardStatusStepperProps) {
<TooltipProvider>
<div className="flex items-center gap-0 overflow-x-auto">
{JOB_CARD_STATUSES.map((step, index) => {
const Icon = STATUS_ICONS[step.value]
const Icon = STATUS_ICONS[step.value] ?? CircleDot
const isActive = step.value === currentStatus
const isCompleted = index < currentIndex
const isTransitioning = isPending && variables === step.value
@@ -1,4 +1,12 @@
import { z } from "zod"
import {
JobCardStatus,
EstimateTo,
TaxInclusive,
DiscountType,
DiscountAt,
FuelLevel,
} from "@garage/api"
const relationFieldSchema = z
.object({ value: z.string(), label: z.string() })
@@ -6,31 +14,37 @@ const relationFieldSchema = z
// ── Job Card Statuses ──
export const JOB_CARD_STATUSES = [
{ value: "draft", label: "Draft" },
{ value: "check_in", label: "Check In" },
{ value: "in_progress", label: "In Progress" },
{ value: "on_hold", label: "On Hold" },
{ value: "ready_to_deliver", label: "Ready to Deliver" },
{ value: "delivered", label: "Delivered" },
] as const
export const JOB_CARD_STATUS_OPTIONS = JobCardStatus.map((v) => ({
value: v,
label: v
.replace(/_/g, " ")
.replace(/\b\w/g, (c) => c.toUpperCase()),
}))
export type JobCardStatus = (typeof JOB_CARD_STATUSES)[number]["value"]
const ESTIMATE_TO_OPTIONS = EstimateTo.map((v) => ({ value: v, label: v }))
const TAX_INCLUSIVE_OPTIONS = [
{ value: "Tax Inclusive", label: "Tax Inclusive" },
{ value: "Tax Exclusive", label: "Tax Exclusive" },
]
const TAX_INCLUSIVE_OPTIONS = TaxInclusive.map((v) => ({ value: v, label: v }))
const DISCOUNT_TYPE_OPTIONS = [
{ value: "no", label: "No Discount" },
{ value: "transaction_level", label: "Transaction Level" },
]
const DISCOUNT_TYPE_OPTIONS = DiscountType.map((v) => ({
value: v,
label: v
.replace(/_/g, " ")
.replace(/\b\w/g, (c) => c.toUpperCase()),
}))
const DISCOUNT_AT_OPTIONS = [
{ value: "inclusive_of_tax", label: "Inclusive of Tax" },
{ value: "exclusive_of_tax", label: "Exclusive of Tax" },
]
const DISCOUNT_AT_OPTIONS = DiscountAt.map((v) => ({
value: v,
label: v
.replace(/_/g, " ")
.replace(/\b\w/g, (c) => c.toUpperCase()),
}))
const FUEL_LEVEL_OPTIONS = FuelLevel.map((v) => ({
value: v,
label: v
.replace(/_/g, " ")
.replace(/\b\w/g, (c) => c.toUpperCase()),
}))
const jobCardFormSchema = z.object({
// ── Required fields ──
@@ -39,15 +53,36 @@ const jobCardFormSchema = z.object({
// ── Relations ──
customer: relationFieldSchema,
vehicle: relationFieldSchema,
department: relationFieldSchema,
service_writer: relationFieldSchema,
// ── Settings ──
status: z.string().optional(),
estimate_to: z.string().optional(),
tax_inclusive: z.string().optional(),
discount_type: z.string().optional(),
discount_at: z.string().optional(),
// ── Check-in details ──
check_in_date: z.string().optional(),
check_in_time: z.string().optional(),
km_in: z.string().optional(),
fuel_level: z.string().optional(),
})
type JobCardFormValues = z.infer<typeof jobCardFormSchema>
export { jobCardFormSchema, relationFieldSchema, TAX_INCLUSIVE_OPTIONS, DISCOUNT_TYPE_OPTIONS, DISCOUNT_AT_OPTIONS }
export {
jobCardFormSchema,
relationFieldSchema,
TAX_INCLUSIVE_OPTIONS,
DISCOUNT_TYPE_OPTIONS,
DISCOUNT_AT_OPTIONS,
ESTIMATE_TO_OPTIONS,
FUEL_LEVEL_OPTIONS,
JOB_CARD_STATUS_OPTIONS,
}
// Backward-compat alias used by job-card-status-stepper
export const JOB_CARD_STATUSES = JOB_CARD_STATUS_OPTIONS
export type { JobCardFormValues }
export type { JobCardStatus } from "@garage/api"