fix many bugs
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
parent
0c43c8edd2
commit
e1ef6fa2ea
@ -6,10 +6,12 @@ import FormDialog from '@/shared/components/form-dialog'
|
|||||||
import { EstimateForm } from '@/modules/estimates/estimate-form'
|
import { EstimateForm } from '@/modules/estimates/estimate-form'
|
||||||
import { ESTIMATE_ROUTES } from '@garage/api'
|
import { ESTIMATE_ROUTES } from '@garage/api'
|
||||||
import type { EstimatesClient } from '@garage/api'
|
import type { EstimatesClient } from '@garage/api'
|
||||||
import { Car, FileTextIcon } from 'lucide-react'
|
import { Car, FileTextIcon, UserIcon } from 'lucide-react'
|
||||||
import { Button } from '@/shared/components/ui/button'
|
import { Button } from '@/shared/components/ui/button'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { formatDate } from '@/shared/utils/formatters'
|
import { formatDate } from '@/shared/utils/formatters'
|
||||||
|
import { getVehicleLabel } from '@/modules/vehicles/utils/getVehicleLabel'
|
||||||
|
import { getFullName } from '@/shared/utils/getFullName'
|
||||||
|
|
||||||
export default function EstimatesPage() {
|
export default function EstimatesPage() {
|
||||||
return (
|
return (
|
||||||
@ -52,15 +54,24 @@ export default function EstimatesPage() {
|
|||||||
{
|
{
|
||||||
accessorKey: "customer_name",
|
accessorKey: "customer_name",
|
||||||
header: ({ column }) => <ColumnHeader column={column} title="Customer" />,
|
header: ({ column }) => <ColumnHeader column={column} title="Customer" />,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const item:any = row.original
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<UserIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
|
<span>{getFullName(item.customer) || "—"}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "vehicle_id",
|
accessorKey: "vehicle",
|
||||||
header: ({ column }) => <ColumnHeader column={column} title="Vehicle" />,
|
header: ({ column }) => <ColumnHeader column={column} title="Vehicle" />,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const item = row.original
|
const item :any= row.original
|
||||||
return <Button variant="outline" asChild size="sm">
|
return <Button variant="outline" asChild size="sm">
|
||||||
<Link href={`/sales/vehicles/${item.vehicle_id}`}>
|
<Link href={`/sales/vehicles/${item.vehicle?.id}`}>
|
||||||
<Car/> Go to vehicle
|
<Car/> {getVehicleLabel(item.vehicle as any) || "—"}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import {
|
|||||||
UserIcon,
|
UserIcon,
|
||||||
ClipboardListIcon,
|
ClipboardListIcon,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
|
import { getFullName } from "@/shared/utils/getFullName"
|
||||||
|
|
||||||
type PaymentReceivedItem = {
|
type PaymentReceivedItem = {
|
||||||
id: number
|
id: number
|
||||||
@ -63,24 +64,24 @@ export default function PaymentReceivedPage() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "customer_name",
|
accessorKey: "customer",
|
||||||
header: ({ column }) => <ColumnHeader column={column} title="Customer" />,
|
header: ({ column }) => <ColumnHeader column={column} title="Customer" />,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const item = row.original as unknown as PaymentReceivedItem
|
const item:any = row.original as unknown as PaymentReceivedItem
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<UserIcon className="h-4 w-4 text-muted-foreground" />
|
<UserIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
<span>{item.customer_name || "—"}</span>
|
<span>{getFullName(item.customer) || "—"}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "job_card_name",
|
accessorKey: "job_card",
|
||||||
header: ({ column }) => <ColumnHeader column={column} title="Job Card" />,
|
header: ({ column }) => <ColumnHeader column={column} title="Job Card" />,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const item = row.original as unknown as PaymentReceivedItem
|
const item:any = row.original as unknown as PaymentReceivedItem
|
||||||
const label = item.job_card_number || item.job_card_name
|
const label = item.job_card?.title
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ClipboardListIcon className="h-4 w-4 text-muted-foreground" />
|
<ClipboardListIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
@ -111,14 +112,14 @@ export default function PaymentReceivedPage() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "payment_mode_name",
|
accessorKey: "payment_mode",
|
||||||
header: ({ column }) => <ColumnHeader column={column} title="Payment Mode" />,
|
header: ({ column }) => <ColumnHeader column={column} title="Payment Mode" />,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const item = row.original as unknown as PaymentReceivedItem
|
const item:any = row.original as unknown as PaymentReceivedItem
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<CreditCardIcon className="h-4 w-4 text-muted-foreground" />
|
<CreditCardIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
<span className="capitalize">{item.payment_mode_name || "—"}</span>
|
<span className="capitalize">{item.payment_mode?.title || "—"}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import {
|
|||||||
RhfTextareaField,
|
RhfTextareaField,
|
||||||
RhfSelectField,
|
RhfSelectField,
|
||||||
RhfAsyncSelectField,
|
RhfAsyncSelectField,
|
||||||
|
RhfTimeField,
|
||||||
} from "@/shared/components/form"
|
} from "@/shared/components/form"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { useAuthApi } from "@/shared/useApi"
|
import { useAuthApi } from "@/shared/useApi"
|
||||||
@ -40,8 +41,8 @@ export type AppointmentFormProps = {
|
|||||||
const DEFAULT_VALUES: AppointmentFormValues = {
|
const DEFAULT_VALUES: AppointmentFormValues = {
|
||||||
title: "",
|
title: "",
|
||||||
date: "",
|
date: "",
|
||||||
from_time: "",
|
from_time: "00:00:00",
|
||||||
to_time: "",
|
to_time: "00:00:00",
|
||||||
customer: null,
|
customer: null,
|
||||||
vehicle: null,
|
vehicle: null,
|
||||||
service_writer: null,
|
service_writer: null,
|
||||||
@ -157,8 +158,8 @@ export function AppointmentForm({ resourceId, initialData, onSuccess }: Appointm
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<RhfTextField name="from_time" label="From Time" placeholder="HH:MM" type="time" />
|
<RhfTimeField name="from_time" label="From Time" placeholder="HH:MM" />
|
||||||
<RhfTextField name="to_time" label="To Time" placeholder="HH:MM" type="time" />
|
<RhfTimeField name="to_time" label="To Time" placeholder="HH:MM" />
|
||||||
</div>
|
</div>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,8 @@ import {
|
|||||||
customerFormSchema,
|
customerFormSchema,
|
||||||
type CustomerFormValues,
|
type CustomerFormValues,
|
||||||
} from "./customer.schema"
|
} from "./customer.schema"
|
||||||
import { CUSTOMER_ROUTES } from "@garage/api"
|
import { CUSTOMER_ROUTES, PAYMENT_TERM_ROUTES } from "@garage/api"
|
||||||
|
import { PaymentTermCrudDialog } from "./payment-term-crud-dialog"
|
||||||
|
|
||||||
// ── Constants ──
|
// ── Constants ──
|
||||||
|
|
||||||
@ -69,7 +70,7 @@ function mapCustomerToFormValues(data: unknown): CustomerFormValues {
|
|||||||
const c = (data as any)?.data ?? data ?? {}
|
const c = (data as any)?.data ?? data ?? {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
customer_type: toRelation(c.customer_type_id, c.customer_type_name),
|
customer_type: toRelation(c.customer_type_id, c.customer_type?.name ?? c.customer_type_name),
|
||||||
referral_source: toRelation(c.referral_source_id, c.referral_source_name),
|
referral_source: toRelation(c.referral_source_id, c.referral_source_name),
|
||||||
payment_terms: toRelation(c.payment_terms_id, c.payment_terms_name),
|
payment_terms: toRelation(c.payment_terms_id, c.payment_terms_name),
|
||||||
country: toRelation(c.country_id, c.country_name),
|
country: toRelation(c.country_id, c.country_name),
|
||||||
@ -113,10 +114,10 @@ function mapFormToPayload(values: CustomerFormValues) {
|
|||||||
|
|
||||||
const mapLookupOption = (item: any) => ({
|
const mapLookupOption = (item: any) => ({
|
||||||
value: String(item.id),
|
value: String(item.id),
|
||||||
label: item.name,
|
label: item.title||item.name || `#${item.id}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label }
|
const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label || o.name || `#${o.id}` }
|
||||||
|
|
||||||
// ── Component ──
|
// ── Component ──
|
||||||
|
|
||||||
@ -173,6 +174,7 @@ export function CustomerForm({ resourceId, initialData, onSuccess }: CustomerFor
|
|||||||
options={SALUTATION_OPTIONS}
|
options={SALUTATION_OPTIONS}
|
||||||
/>
|
/>
|
||||||
<RhfAsyncSelectField
|
<RhfAsyncSelectField
|
||||||
|
required
|
||||||
name="customer_type"
|
name="customer_type"
|
||||||
label="Customer Type"
|
label="Customer Type"
|
||||||
placeholder="Select customer type"
|
placeholder="Select customer type"
|
||||||
@ -211,16 +213,22 @@ export function CustomerForm({ resourceId, initialData, onSuccess }: CustomerFor
|
|||||||
mapOption={mapLookupOption}
|
mapOption={mapLookupOption}
|
||||||
{...STORE_OBJECT}
|
{...STORE_OBJECT}
|
||||||
/>
|
/>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<span className="text-sm font-medium">Payment Terms</span>
|
||||||
|
<PaymentTermCrudDialog />
|
||||||
|
</div>
|
||||||
<RhfAsyncSelectField
|
<RhfAsyncSelectField
|
||||||
name="payment_terms"
|
name="payment_terms"
|
||||||
label="Payment Terms"
|
label=""
|
||||||
placeholder="Select payment terms"
|
placeholder="Select payment terms"
|
||||||
queryKey={["payment-terms"]}
|
queryKey={[PAYMENT_TERM_ROUTES.INDEX]}
|
||||||
listFn={() => api.paymentTerms.list()}
|
listFn={() => api.paymentTerms.list()}
|
||||||
mapOption={mapLookupOption}
|
mapOption={mapLookupOption}
|
||||||
{...STORE_OBJECT}
|
{...STORE_OBJECT}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Address */}
|
{/* Address */}
|
||||||
<RhfTextField name="address_line_1" label="Address Line 1" placeholder="Street 10" />
|
<RhfTextField name="address_line_1" label="Address Line 1" placeholder="Street 10" />
|
||||||
|
|||||||
@ -12,7 +12,7 @@ type RelationField = z.infer<typeof relationFieldSchema>
|
|||||||
|
|
||||||
const customerFormSchema = z.object({
|
const customerFormSchema = z.object({
|
||||||
// ── Relations (stored as objects, mapped to IDs on submit) ──
|
// ── Relations (stored as objects, mapped to IDs on submit) ──
|
||||||
customer_type: relationFieldSchema,
|
customer_type: relationFieldSchema.refine((val) => !!val?.value, "Customer type is required"),
|
||||||
referral_source: relationFieldSchema,
|
referral_source: relationFieldSchema,
|
||||||
payment_terms: relationFieldSchema,
|
payment_terms: relationFieldSchema,
|
||||||
country: relationFieldSchema,
|
country: relationFieldSchema,
|
||||||
|
|||||||
@ -0,0 +1,33 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { CrudDialog } from "@/shared/components/crud-dialog"
|
||||||
|
import { ColumnHeader } from "@/shared/data-view/table-view"
|
||||||
|
import { useAuthApi } from "@/shared/useApi"
|
||||||
|
import { PAYMENT_TERM_ROUTES } from "@garage/api"
|
||||||
|
import { PaymentTermForm } from "./payment-term-form"
|
||||||
|
|
||||||
|
export function PaymentTermCrudDialog() {
|
||||||
|
const api = useAuthApi()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CrudDialog
|
||||||
|
title="Payment Terms"
|
||||||
|
queryKey={[PAYMENT_TERM_ROUTES.INDEX]}
|
||||||
|
getClient={() => api.paymentTerms}
|
||||||
|
resourceLabel="payment term"
|
||||||
|
columns={() => [
|
||||||
|
{
|
||||||
|
accessorKey: "title",
|
||||||
|
header: ({ column }) => <ColumnHeader column={column} title="Name" />,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
renderForm={({ resourceId, initialData, onSuccess }) => (
|
||||||
|
<PaymentTermForm
|
||||||
|
resourceId={resourceId}
|
||||||
|
initialData={initialData}
|
||||||
|
onSuccess={onSuccess}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
86
apps/dashboard/modules/customers/payment-term-form.tsx
Normal file
86
apps/dashboard/modules/customers/payment-term-form.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { z } from "zod"
|
||||||
|
import { useForm } from "react-hook-form"
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { Plus, Save } from "lucide-react"
|
||||||
|
import { Button } from "@/shared/components/ui/button"
|
||||||
|
import { FieldGroup } from "@/shared/components/ui/field"
|
||||||
|
import { Rhform, RhfTextField } from "@/shared/components/form"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
import { useAuthApi } from "@/shared/useApi"
|
||||||
|
import { useEffect } from "react"
|
||||||
|
|
||||||
|
// ── Schema ──
|
||||||
|
|
||||||
|
const paymentTermSchema = z.object({
|
||||||
|
name: z.string().min(1, "Name is required"),
|
||||||
|
})
|
||||||
|
|
||||||
|
type PaymentTermFormValues = z.infer<typeof paymentTermSchema>
|
||||||
|
|
||||||
|
// ── Props ──
|
||||||
|
|
||||||
|
type PaymentTermFormProps = {
|
||||||
|
resourceId?: string | null
|
||||||
|
initialData?: any
|
||||||
|
onSuccess?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Component ──
|
||||||
|
|
||||||
|
export function PaymentTermForm({ resourceId, initialData, onSuccess }: PaymentTermFormProps) {
|
||||||
|
const api = useAuthApi()
|
||||||
|
const isEditing = !!resourceId
|
||||||
|
|
||||||
|
const form = useForm<PaymentTermFormValues>({
|
||||||
|
resolver: zodResolver(paymentTermSchema),
|
||||||
|
defaultValues: { name: "" },
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialData) {
|
||||||
|
const d = initialData?.data ?? initialData
|
||||||
|
form.reset({ name: d.title ?? "" })
|
||||||
|
}
|
||||||
|
}, [initialData, form])
|
||||||
|
|
||||||
|
const handleSubmit = async (values: PaymentTermFormValues) => {
|
||||||
|
try {
|
||||||
|
const promise = isEditing
|
||||||
|
? api.paymentTerms.update(resourceId!, { title: values.name } as any)
|
||||||
|
: api.paymentTerms.create({ title: values.name } as any)
|
||||||
|
|
||||||
|
toast.promise(promise, {
|
||||||
|
loading: isEditing ? "Updating..." : "Creating...",
|
||||||
|
success: isEditing ? "Updated successfully" : "Created successfully",
|
||||||
|
error: isEditing ? "Failed to update" : "Failed to create",
|
||||||
|
})
|
||||||
|
|
||||||
|
await promise
|
||||||
|
form.reset()
|
||||||
|
onSuccess?.()
|
||||||
|
} catch {
|
||||||
|
// toast already shown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Rhform form={form} onSubmit={handleSubmit}>
|
||||||
|
<FieldGroup>
|
||||||
|
<RhfTextField
|
||||||
|
name="name"
|
||||||
|
label="Name"
|
||||||
|
placeholder="e.g. Net 30"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Button type="submit" disabled={form.formState.isSubmitting}>
|
||||||
|
{isEditing ? <Save className="h-4 w-4" /> : <Plus className="h-4 w-4" />}
|
||||||
|
{form.formState.isSubmitting
|
||||||
|
? (isEditing ? "Updating..." : "Creating...")
|
||||||
|
: (isEditing ? "Update" : "Create")}
|
||||||
|
</Button>
|
||||||
|
</FieldGroup>
|
||||||
|
</Rhform>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -31,7 +31,15 @@ export default function InvoicePaymentsSection() {
|
|||||||
|
|
||||||
<PaymentReceivedForm
|
<PaymentReceivedForm
|
||||||
invoiceId={invoice?.id as string}
|
invoiceId={invoice?.id as string}
|
||||||
invoiceCustomer={invoice?.customer as any}
|
invoiceCustomer={
|
||||||
|
(invoice?.customer as any) ??
|
||||||
|
((invoice as any)?.customer_id
|
||||||
|
? {
|
||||||
|
id: (invoice as any).customer_id,
|
||||||
|
first_name: (invoice as any).customer_name,
|
||||||
|
}
|
||||||
|
: null)
|
||||||
|
}
|
||||||
invoiceAmount={invoice?.balance_due as any}
|
invoiceAmount={invoice?.balance_due as any}
|
||||||
resourceId={resourceId}
|
resourceId={resourceId}
|
||||||
onSuccess={()=>{router.refresh(); invalidateQuery()}}
|
onSuccess={()=>{router.refresh(); invalidateQuery()}}
|
||||||
|
|||||||
@ -17,9 +17,13 @@ export function InsuranceTypeCrudDialog() {
|
|||||||
resourceLabel="insurance type"
|
resourceLabel="insurance type"
|
||||||
columns={() => [
|
columns={() => [
|
||||||
{
|
{
|
||||||
accessorKey: "name",
|
accessorKey: "title",
|
||||||
header: ({ column }) => <ColumnHeader column={column} title="Name" />,
|
header: ({ column }) => <ColumnHeader column={column} title="Name" />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "description",
|
||||||
|
header: ({ column }) => <ColumnHeader column={column} title="Description" />,
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
renderForm={({ resourceId, initialData, onSuccess }) => (
|
renderForm={({ resourceId, initialData, onSuccess }) => (
|
||||||
<InsuranceTypeForm
|
<InsuranceTypeForm
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { zodResolver } from "@hookform/resolvers/zod"
|
|||||||
import { Plus, Save } from "lucide-react"
|
import { Plus, Save } from "lucide-react"
|
||||||
import { Button } from "@/shared/components/ui/button"
|
import { Button } from "@/shared/components/ui/button"
|
||||||
import { FieldGroup } from "@/shared/components/ui/field"
|
import { FieldGroup } from "@/shared/components/ui/field"
|
||||||
import { Rhform, RhfTextField } from "@/shared/components/form"
|
import { Rhform, RhfTextField, RhfTextareaField } from "@/shared/components/form"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { useAuthApi } from "@/shared/useApi"
|
import { useAuthApi } from "@/shared/useApi"
|
||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
@ -15,6 +15,7 @@ import { useEffect } from "react"
|
|||||||
|
|
||||||
const insuranceTypeSchema = z.object({
|
const insuranceTypeSchema = z.object({
|
||||||
name: z.string().min(1, "Name is required"),
|
name: z.string().min(1, "Name is required"),
|
||||||
|
description: z.string().min(1, "Description is required"),
|
||||||
})
|
})
|
||||||
|
|
||||||
type InsuranceTypeFormValues = z.infer<typeof insuranceTypeSchema>
|
type InsuranceTypeFormValues = z.infer<typeof insuranceTypeSchema>
|
||||||
@ -35,22 +36,22 @@ export function InsuranceTypeForm({ resourceId, initialData, onSuccess }: Insura
|
|||||||
|
|
||||||
const form = useForm<InsuranceTypeFormValues>({
|
const form = useForm<InsuranceTypeFormValues>({
|
||||||
resolver: zodResolver(insuranceTypeSchema),
|
resolver: zodResolver(insuranceTypeSchema),
|
||||||
defaultValues: { name: "" },
|
defaultValues: { name: "", description: "" },
|
||||||
})
|
})
|
||||||
|
|
||||||
// Pre-fill when editing
|
// Pre-fill when editing
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialData) {
|
if (initialData) {
|
||||||
const d = initialData?.data ?? initialData
|
const d = initialData?.data ?? initialData
|
||||||
form.reset({ name: d.name ?? "" })
|
form.reset({ name: d.title ?? "", description: d.description ?? "" })
|
||||||
}
|
}
|
||||||
}, [initialData, form])
|
}, [initialData, form])
|
||||||
|
|
||||||
const handleSubmit = async (values: InsuranceTypeFormValues) => {
|
const handleSubmit = async (values: InsuranceTypeFormValues) => {
|
||||||
try {
|
try {
|
||||||
const promise = isEditing
|
const promise = isEditing
|
||||||
? api.insuranceTypes.update(resourceId!, { title: values.name } as any)
|
? api.insuranceTypes.update(resourceId!, { title: values.name, description: values.description } as any)
|
||||||
: api.insuranceTypes.create({ title: values.name } as any)
|
: api.insuranceTypes.create({ title: values.name, description: values.description } as any)
|
||||||
|
|
||||||
toast.promise(promise, {
|
toast.promise(promise, {
|
||||||
loading: isEditing ? "Updating..." : "Creating...",
|
loading: isEditing ? "Updating..." : "Creating...",
|
||||||
@ -75,6 +76,13 @@ export function InsuranceTypeForm({ resourceId, initialData, onSuccess }: Insura
|
|||||||
placeholder="e.g. Comprehensive"
|
placeholder="e.g. Comprehensive"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
<RhfTextareaField
|
||||||
|
name="description"
|
||||||
|
label="Description"
|
||||||
|
placeholder="e.g. Comprehensive insurance coverage for vehicles"
|
||||||
|
rows={3}
|
||||||
|
required
|
||||||
|
/>
|
||||||
<Button type="submit" disabled={form.formState.isSubmitting}>
|
<Button type="submit" disabled={form.formState.isSubmitting}>
|
||||||
{isEditing ? <Save className="h-4 w-4" /> : <Plus className="h-4 w-4" />}
|
{isEditing ? <Save className="h-4 w-4" /> : <Plus className="h-4 w-4" />}
|
||||||
{form.formState.isSubmitting
|
{form.formState.isSubmitting
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import { Calendar } from "@/shared/components/ui/calendar"
|
|||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { Pencil, CalendarIcon, UserCog, UserCheck, Loader2 } from "lucide-react"
|
import { Pencil, CalendarIcon, UserCog, UserCheck, Loader2 } from "lucide-react"
|
||||||
import { format } from "date-fns"
|
import { format } from "date-fns"
|
||||||
import { EmployeeCombobox } from "@/modules/employees/employee-combobox"
|
import { EmployeeCombobox, type EmployeeOption } from "@/modules/employees/employee-combobox"
|
||||||
|
|
||||||
type JobCardActionsProps = {
|
type JobCardActionsProps = {
|
||||||
jobCardId: string
|
jobCardId: string
|
||||||
@ -82,6 +82,16 @@ function EmployeePickerDialog({
|
|||||||
isPending,
|
isPending,
|
||||||
onSelect,
|
onSelect,
|
||||||
}: EmployeePickerDialogProps) {
|
}: EmployeePickerDialogProps) {
|
||||||
|
const handleSelect = (emp: EmployeeOption | null) => {
|
||||||
|
console.log('Selected employee object:', emp, 'Value:', emp?.value, 'Type of value:', typeof emp?.value)
|
||||||
|
if (emp && emp.value) {
|
||||||
|
const employeeId = Number(emp.value)
|
||||||
|
console.log('Parsed employee ID:', employeeId, 'Type:', typeof employeeId)
|
||||||
|
onSelect(employeeId)
|
||||||
|
onOpenChange(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="sm:max-w-md">
|
||||||
@ -91,9 +101,7 @@ function EmployeePickerDialog({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<EmployeeCombobox
|
<EmployeeCombobox
|
||||||
value={null}
|
value={null}
|
||||||
onValueChange={(emp) => {
|
onValueChange={handleSelect}
|
||||||
if (emp) onSelect(Number(emp.value))
|
|
||||||
}}
|
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
placeholder="Search employees..."
|
placeholder="Search employees..."
|
||||||
showClear={false}
|
showClear={false}
|
||||||
@ -131,7 +139,8 @@ export function JobCardActions({ jobCardId, orderDate, serviceWriterName, salesP
|
|||||||
})
|
})
|
||||||
|
|
||||||
const changeServiceWriterMutation = useMutation({
|
const changeServiceWriterMutation = useMutation({
|
||||||
mutationFn: (employeeId: number) => {
|
mutationFn: async (employeeId: number) => {
|
||||||
|
console.log('Sending service writer ID:', employeeId, 'Type:', typeof employeeId)
|
||||||
const promise = api.jobCards.changeServiceWriter(jobCardId, { service_writer_id: employeeId })
|
const promise = api.jobCards.changeServiceWriter(jobCardId, { service_writer_id: employeeId })
|
||||||
toast.promise(promise, {
|
toast.promise(promise, {
|
||||||
loading: "Updating service writer...",
|
loading: "Updating service writer...",
|
||||||
@ -147,7 +156,8 @@ export function JobCardActions({ jobCardId, orderDate, serviceWriterName, salesP
|
|||||||
})
|
})
|
||||||
|
|
||||||
const changeSalesPersonMutation = useMutation({
|
const changeSalesPersonMutation = useMutation({
|
||||||
mutationFn: (employeeId: number) => {
|
mutationFn: async (employeeId: number) => {
|
||||||
|
console.log('Sending sales person ID:', employeeId, 'Type:', typeof employeeId)
|
||||||
const promise = api.jobCards.changeSalesPerson(jobCardId, { sales_person_id: employeeId })
|
const promise = api.jobCards.changeSalesPerson(jobCardId, { sales_person_id: employeeId })
|
||||||
toast.promise(promise, {
|
toast.promise(promise, {
|
||||||
loading: "Updating sales person...",
|
loading: "Updating sales person...",
|
||||||
@ -162,6 +172,23 @@ export function JobCardActions({ jobCardId, orderDate, serviceWriterName, salesP
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const changePrimaryTechnicianMutation = useMutation({
|
||||||
|
mutationFn: async (employeeId: number) => {
|
||||||
|
console.log('Sending primary technician ID:', employeeId, 'Type:', typeof employeeId)
|
||||||
|
const promise = api.jobCards.changeTechnician(jobCardId, { technician_id: employeeId })
|
||||||
|
toast.promise(promise, {
|
||||||
|
loading: "Updating primary technician...",
|
||||||
|
success: "Primary technician updated successfully",
|
||||||
|
error: "Failed to update primary technician",
|
||||||
|
})
|
||||||
|
return promise
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
setPrimaryTechnicianDialogOpen(false)
|
||||||
|
router.refresh()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -226,7 +253,7 @@ export function JobCardActions({ jobCardId, orderDate, serviceWriterName, salesP
|
|||||||
icon={UserCheck}
|
icon={UserCheck}
|
||||||
label="Primary Technician"
|
label="Primary Technician"
|
||||||
value={primaryTechnicianName ?? null}
|
value={primaryTechnicianName ?? null}
|
||||||
isPending={false}
|
isPending={changePrimaryTechnicianMutation.isPending}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@ -239,7 +266,10 @@ export function JobCardActions({ jobCardId, orderDate, serviceWriterName, salesP
|
|||||||
title="Change Service Writer"
|
title="Change Service Writer"
|
||||||
description="Search and select an employee to assign as service writer."
|
description="Search and select an employee to assign as service writer."
|
||||||
isPending={changeServiceWriterMutation.isPending}
|
isPending={changeServiceWriterMutation.isPending}
|
||||||
onSelect={(id) => changeServiceWriterMutation.mutate(id)}
|
onSelect={(id) => {
|
||||||
|
console.log('Service Writer dialog onSelect called with:', id, 'Type:', typeof id)
|
||||||
|
changeServiceWriterMutation.mutate(id)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<EmployeePickerDialog
|
<EmployeePickerDialog
|
||||||
@ -248,24 +278,22 @@ export function JobCardActions({ jobCardId, orderDate, serviceWriterName, salesP
|
|||||||
title="Change Sales Person"
|
title="Change Sales Person"
|
||||||
description="Search and select an employee to assign as sales person."
|
description="Search and select an employee to assign as sales person."
|
||||||
isPending={changeSalesPersonMutation.isPending}
|
isPending={changeSalesPersonMutation.isPending}
|
||||||
onSelect={(id) => changeSalesPersonMutation.mutate(id)}
|
onSelect={(id) => {
|
||||||
/>
|
console.log('Dialog onSelect called with:', id, 'Type:', typeof id)
|
||||||
<EmployeePickerDialog
|
changeSalesPersonMutation.mutate(id)
|
||||||
open={salesPersonDialogOpen}
|
}}
|
||||||
onOpenChange={setSalesPersonDialogOpen}
|
|
||||||
title="Change Sales Person"
|
|
||||||
description="Search and select an employee to assign as sales person."
|
|
||||||
isPending={changeSalesPersonMutation.isPending}
|
|
||||||
onSelect={(id) => changeSalesPersonMutation.mutate(id)}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<EmployeePickerDialog
|
<EmployeePickerDialog
|
||||||
open={primaryTechnicianDialogOpen}
|
open={primaryTechnicianDialogOpen}
|
||||||
onOpenChange={setPrimaryTechnicianDialogOpen}
|
onOpenChange={setPrimaryTechnicianDialogOpen}
|
||||||
title="Change Primary Technician"
|
title="Change Primary Technician"
|
||||||
description="Search and select an employee to assign as primary technician."
|
description="Search and select an employee to assign as primary technician."
|
||||||
isPending={false}
|
isPending={changePrimaryTechnicianMutation.isPending}
|
||||||
onSelect={()=>{}}
|
onSelect={(id) => {
|
||||||
// onSelect={(id) => changePrimaryTechnicianMutation.mutate(id)}
|
console.log('Primary Technician dialog onSelect called with:', id, 'Type:', typeof id)
|
||||||
|
changePrimaryTechnicianMutation.mutate(id)
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -69,9 +69,10 @@ const DEFAULT_VALUES: JobCardFormValues = {
|
|||||||
check_in_date: "",
|
check_in_date: "",
|
||||||
check_in_time: "",
|
check_in_time: "",
|
||||||
start_date: "",
|
start_date: "",
|
||||||
start_time: "",
|
// Must be initialized with 00:00:00
|
||||||
|
start_time: "00:00:00",
|
||||||
delivery_date: "",
|
delivery_date: "",
|
||||||
delivery_time: "",
|
delivery_time: "00:00:00",
|
||||||
km_in: "",
|
km_in: "",
|
||||||
fuel_level: "",
|
fuel_level: "",
|
||||||
has_insurance: false,
|
has_insurance: false,
|
||||||
|
|||||||
@ -79,7 +79,7 @@ export function JobCardGeneralInfo({ jobCard }: { jobCard: JobCard }) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<JobCardActions
|
<JobCardActions
|
||||||
jobCardId={String(jobCard)}
|
jobCardId={String(jobCard.id )}
|
||||||
orderDate={jobCard.order_date ?? null}
|
orderDate={jobCard.order_date ?? null}
|
||||||
serviceWriterName={jobCard.service_writer?.first_name}
|
serviceWriterName={jobCard.service_writer?.first_name}
|
||||||
salesPersonName={jobCard.sales_person?.first_name}
|
salesPersonName={jobCard.sales_person?.first_name}
|
||||||
|
|||||||
@ -47,6 +47,7 @@ export default function JobCardPaymentsReceived() {
|
|||||||
<PaymentReceivedForm
|
<PaymentReceivedForm
|
||||||
resourceId={resourceId}
|
resourceId={resourceId}
|
||||||
defaultJobCard={{ id: jobCard?.id, title: jobCard?.title }}
|
defaultJobCard={{ id: jobCard?.id, title: jobCard?.title }}
|
||||||
|
invoiceCustomer={jobCard?.customer as any}
|
||||||
onSuccess={invalidateQuery}
|
onSuccess={invalidateQuery}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -83,14 +84,14 @@ export default function JobCardPaymentsReceived() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "payment_mode_name",
|
accessorKey: "payment_mode",
|
||||||
header: ({ column }) => <ColumnHeader column={column} title="Payment Mode" />,
|
header: ({ column }) => <ColumnHeader column={column} title="Payment Mode" />,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const item = row.original as any
|
const item = row.original as any
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<CreditCardIcon className="h-4 w-4 text-muted-foreground" />
|
<CreditCardIcon className="h-4 w-4 text-muted-foreground" />
|
||||||
<span className="capitalize">{item.payment_mode_name || "—"}</span>
|
<span className="capitalize">{(item.payment_mode?.title) || "—"}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@ -100,12 +100,10 @@ export function PaymentReceivedForm({ resourceId, initialData, onSuccess, defaul
|
|||||||
base.job_card = toRelation(defaultJobCard.id, defaultJobCard.title ?? undefined)
|
base.job_card = toRelation(defaultJobCard.id, defaultJobCard.title ?? undefined)
|
||||||
}
|
}
|
||||||
if (invoiceCustomer?.id != null) {
|
if (invoiceCustomer?.id != null) {
|
||||||
base.customer = toRelation(
|
const customerLabel = invoiceCustomer.first_name
|
||||||
invoiceCustomer.id,
|
|
||||||
invoiceCustomer.first_name
|
|
||||||
? `${invoiceCustomer.first_name} ${invoiceCustomer.last_name || ""}`.trim()
|
? `${invoiceCustomer.first_name} ${invoiceCustomer.last_name || ""}`.trim()
|
||||||
: undefined
|
: (invoiceCustomer as any).company_name || (invoiceCustomer as any).name || undefined
|
||||||
)
|
base.customer = toRelation(invoiceCustomer.id, customerLabel)
|
||||||
}
|
}
|
||||||
if (invoiceAmount != null && invoiceAmount !== "") {
|
if (invoiceAmount != null && invoiceAmount !== "") {
|
||||||
base.amount_received = String(invoiceAmount)
|
base.amount_received = String(invoiceAmount)
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import { useAuthApi } from "@/shared/useApi"
|
|||||||
import { useResourceForm } from "@/shared/hooks/use-resource-form"
|
import { useResourceForm } from "@/shared/hooks/use-resource-form"
|
||||||
import { useFormMutation } from "@/shared/hooks/use-form-mutation"
|
import { useFormMutation } from "@/shared/hooks/use-form-mutation"
|
||||||
import { toRelation, toId } from "@/shared/lib/utils"
|
import { toRelation, toId } from "@/shared/lib/utils"
|
||||||
|
import { formatUppercase } from "@/shared/utils/formatters"
|
||||||
|
|
||||||
import { vehicleFormSchema, type VehicleFormValues } from "./vehicle.schema"
|
import { vehicleFormSchema, type VehicleFormValues } from "./vehicle.schema"
|
||||||
import { VEHICLE_ROUTES } from "@garage/api"
|
import { VEHICLE_ROUTES } from "@garage/api"
|
||||||
@ -225,7 +226,12 @@ export function VehicleForm({ resourceId, initialData, onSuccess }: VehicleFormP
|
|||||||
|
|
||||||
{/* License & identifiers */}
|
{/* License & identifiers */}
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
<RhfTextField name="license_plate" label="License Plate" placeholder="e.g. ABC-123" />
|
<RhfTextField
|
||||||
|
name="license_plate"
|
||||||
|
label="License Plate"
|
||||||
|
placeholder="e.g. ABC-123"
|
||||||
|
formatter={formatUppercase}
|
||||||
|
/>
|
||||||
<RhfTextField name="mileage" label="Mileage" placeholder="e.g. 10000" />
|
<RhfTextField name="mileage" label="Mileage" placeholder="e.g. 10000" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -21,6 +21,42 @@ export type UseCrudDialogOptions<TClient extends CrudDialogClient> = {
|
|||||||
resourceLabel?: string
|
resourceLabel?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CrudListShape = {
|
||||||
|
data?: unknown
|
||||||
|
meta?: {
|
||||||
|
last_page?: number
|
||||||
|
total?: number
|
||||||
|
}
|
||||||
|
pagination?: {
|
||||||
|
last_page?: number
|
||||||
|
total?: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCrudListResponse(response: unknown): {
|
||||||
|
items: any[]
|
||||||
|
meta?: { last_page?: number; total?: number }
|
||||||
|
} {
|
||||||
|
const root = (response ?? {}) as CrudListShape
|
||||||
|
const directData = root.data
|
||||||
|
|
||||||
|
if (Array.isArray(directData)) {
|
||||||
|
return { items: directData, meta: root.meta ?? root.pagination }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directData && typeof directData === "object") {
|
||||||
|
const nested = directData as CrudListShape
|
||||||
|
if (Array.isArray(nested.data)) {
|
||||||
|
return {
|
||||||
|
items: nested.data,
|
||||||
|
meta: nested.meta ?? nested.pagination ?? root.meta ?? root.pagination,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { items: [], meta: root.meta ?? root.pagination }
|
||||||
|
}
|
||||||
|
|
||||||
// ── Hook ──
|
// ── Hook ──
|
||||||
|
|
||||||
export function useCrudDialog<TClient extends CrudDialogClient>({
|
export function useCrudDialog<TClient extends CrudDialogClient>({
|
||||||
@ -46,17 +82,23 @@ export function useCrudDialog<TClient extends CrudDialogClient>({
|
|||||||
|
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
queryKey: fullQueryKey,
|
queryKey: fullQueryKey,
|
||||||
queryFn: () => {
|
queryFn: async () => {
|
||||||
const params: Record<string, unknown> = { page, per_page: pageSize }
|
const params: Record<string, unknown> = { page, per_page: pageSize }
|
||||||
if (sortBy) params.sort_by = sortBy
|
if (sortBy) params.sort_by = sortBy
|
||||||
if (sortOrder) params.sort_order = sortOrder
|
if (sortOrder) params.sort_order = sortOrder
|
||||||
return client.list(params)
|
|
||||||
|
try {
|
||||||
|
return await client.list(params)
|
||||||
|
} catch {
|
||||||
|
// Some endpoints ignore/reject pagination params; retry without params.
|
||||||
|
return client.list()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const responseData = (data as any)?.data ?? []
|
const normalized = normalizeCrudListResponse(data)
|
||||||
const items = Array.isArray(responseData) ? responseData : []
|
const items = normalized.items
|
||||||
const meta = (data as any)?.meta
|
const meta = normalized.meta
|
||||||
|
|
||||||
const pagination: DataViewPaginationState = {
|
const pagination: DataViewPaginationState = {
|
||||||
page,
|
page,
|
||||||
|
|||||||
@ -5,6 +5,7 @@ export type TextInputFieldProps = BaseFieldControlProps<string> & {
|
|||||||
placeholder?: string
|
placeholder?: string
|
||||||
type?: React.HTMLInputTypeAttribute
|
type?: React.HTMLInputTypeAttribute
|
||||||
step?: React.InputHTMLAttributes<HTMLInputElement>["step"]
|
step?: React.InputHTMLAttributes<HTMLInputElement>["step"]
|
||||||
|
formatter?: (value: string) => string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TextInputField({
|
export function TextInputField({
|
||||||
@ -17,11 +18,12 @@ export function TextInputField({
|
|||||||
placeholder,
|
placeholder,
|
||||||
type = "text",
|
type = "text",
|
||||||
step,
|
step,
|
||||||
|
formatter,
|
||||||
}: TextInputFieldProps) {
|
}: TextInputFieldProps) {
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(formatter ? formatter(e.target.value) : e.target.value)}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
name={name}
|
name={name}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export function TimePickerField({
|
|||||||
disabled,
|
disabled,
|
||||||
invalid,
|
invalid,
|
||||||
placeholder = "Pick a time",
|
placeholder = "Pick a time",
|
||||||
withSeconds = true,
|
withSeconds = false,
|
||||||
}: TimePickerFieldProps) {
|
}: TimePickerFieldProps) {
|
||||||
const { hours, minutes, seconds } = parseTime(value ?? "")
|
const { hours, minutes, seconds } = parseTime(value ?? "")
|
||||||
const hasValue = !!value
|
const hasValue = !!value
|
||||||
|
|||||||
@ -99,3 +99,11 @@ export function formatCurrency(
|
|||||||
if (isNaN(num)) return "—"
|
if (isNaN(num)) return "—"
|
||||||
return new Intl.NumberFormat(locale, { style: "currency", currency }).format(num)
|
return new Intl.NumberFormat(locale, { style: "currency", currency }).format(num)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format text to uppercase.
|
||||||
|
*/
|
||||||
|
export function formatUppercase(value?: string | null): string {
|
||||||
|
if (value == null) return ""
|
||||||
|
return value.toUpperCase()
|
||||||
|
}
|
||||||
|
|||||||
@ -20,6 +20,7 @@ export const JOB_CARD_ROUTES = {
|
|||||||
DELETE_ATTACHMENT: "/api/job-cards/{id}/delete-attachment",
|
DELETE_ATTACHMENT: "/api/job-cards/{id}/delete-attachment",
|
||||||
CHANGE_SERVICE_WRITER: "/api/job-cards/{id}/change-service-writer-id",
|
CHANGE_SERVICE_WRITER: "/api/job-cards/{id}/change-service-writer-id",
|
||||||
CHANGE_SALES_PERSON: "/api/job-cards/{id}/change-sales-person-id",
|
CHANGE_SALES_PERSON: "/api/job-cards/{id}/change-sales-person-id",
|
||||||
|
CHANGE_TECHNICIAN: "/api/job-cards/{id}/change-technician-id",
|
||||||
GET_PARTS: "/api/job-cards/{id}/get-parts",
|
GET_PARTS: "/api/job-cards/{id}/get-parts",
|
||||||
ADD_PART: "/api/job-cards/{id}/add-part",
|
ADD_PART: "/api/job-cards/{id}/add-part",
|
||||||
UPDATE_PART: "/api/job-cards/{id}/update-part",
|
UPDATE_PART: "/api/job-cards/{id}/update-part",
|
||||||
@ -118,6 +119,10 @@ export class JobCardsClient extends CrudClient<
|
|||||||
return this.post(JOB_CARD_ROUTES.CHANGE_SALES_PERSON, payload, { params: { id } })
|
return this.post(JOB_CARD_ROUTES.CHANGE_SALES_PERSON, payload, { params: { id } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async changeTechnician(id: string, payload: ApiRequestBody<typeof JOB_CARD_ROUTES.CHANGE_TECHNICIAN, "post">) {
|
||||||
|
return this.post(JOB_CARD_ROUTES.CHANGE_TECHNICIAN, payload, { params: { id } })
|
||||||
|
}
|
||||||
|
|
||||||
async getParts(id: string, params?: Record<string, unknown>) {
|
async getParts(id: string, params?: Record<string, unknown>) {
|
||||||
return this.get(JOB_CARD_ROUTES.GET_PARTS, { params: { id }, query: params as any })
|
return this.get(JOB_CARD_ROUTES.GET_PARTS, { params: { id }, query: params as any })
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user