fix: add staleTime to job card expense, parts, and services queries; update job card general info and payments received components

This commit is contained in:
2026-05-13 17:20:59 +04:00
parent 6b356d2855
commit 05b55b5721
12 changed files with 174 additions and 91 deletions
@@ -63,24 +63,48 @@ const DEFAULT_VALUES: EstimateFormValues = {
// ── Mapping helpers ──
function customerLabel(c: any): string | undefined {
if (!c) return undefined
const name = [c.first_name, c.last_name].filter(Boolean).join(" ").trim()
return c.company_name || name || undefined
}
function vehicleLabel(v: any): string | undefined {
if (!v) return undefined
const core = [v.make, v.model, v.sub_model].filter(Boolean).join(" ")
const suffix = [v.year, v.license_plate].filter(Boolean).join(" - ")
if (core && suffix) return `${core} (${suffix})`
return core || suffix || undefined
}
function mapToFormValues(data: unknown): EstimateFormValues {
const d = (data as any)?.data ?? data ?? {}
return {
title: d.title || "",
customer: toRelation(d.customer_id, d.customer_name),
vehicle: toRelation(d.vehicle_id, d.vehicle_name),
department: toRelation(d.department_id, d.department_name),
insurance_type: toRelation(d.insurance_type_id, d.insurance_type_title ?? d.insurance_type_name ?? d.insurance_type?.title),
customer: toRelation(
d.customer_id ?? d.customer?.id,
d.customer_name ?? customerLabel(d.customer),
),
vehicle: toRelation(
d.vehicle_id ?? d.vehicle?.id,
d.vehicle_name ?? vehicleLabel(d.vehicle),
),
department: toRelation(
d.department_id ?? d.department?.id,
d.department_name ?? d.department?.name ?? d.department?.title,
),
insurance_type: toRelation(
d.insurance_type_id ?? d.insurance_type?.id,
d.insurance_type_title ?? d.insurance_type_name ?? d.insurance_type?.title ?? d.insurance_type?.name,
),
insurer: toRelation(
d.insurer_id,
d.insurer_name
?? [d.insurer?.first_name, d.insurer?.last_name].filter(Boolean).join(" ")
?? d.insurer?.company_name,
d.insurer_id ?? d.insurer?.id,
d.insurer_name ?? customerLabel(d.insurer),
),
service_writer: toRelation(
d.service_writer_id,
d.service_writer_name ?? [d.service_writer?.first_name, d.service_writer?.last_name].filter(Boolean).join(" "),
d.service_writer_id ?? d.service_writer?.id,
d.service_writer_name ?? customerLabel(d.service_writer),
),
estimate_number: d.estimate_number || "",
date: d.date ? d.date.split("T")[0] : "",
@@ -1,3 +1,5 @@
"use client"
import {
ClipboardList,
Calendar,
@@ -30,8 +32,9 @@ import PaymentReceivedPage from "@/app/(authenticated)/sales/payment-received/pa
import JobCardPaymentsReceived from "./job-card-payments-received"
import { formatDate } from "@/shared/utils/formatters"
import type { JobCardShowData } from "@garage/api"
import { useJobCard } from "./job-card-context"
type JobCard = JobCardShowData
type JobCard = JobCardShowData
function InfoItem({
@@ -67,7 +70,14 @@ const statusColorMap: Record<string, string> = {
cancelled: "destructive",
}
export function JobCardGeneralInfo({ jobCard }: { jobCard: JobCard }) {
export function JobCardGeneralInfo({ jobCard: jobCardProp }: { jobCard?: JobCard } = {}) {
const jobCardFromContext = useJobCard()
const jobCard = (jobCardProp ?? jobCardFromContext) as JobCard
if (!jobCard) {
return <div className="text-muted-foreground">Job card not found.</div>
}
const formatStatus = (status?: string) => {
if (!status) return null
return status
@@ -17,12 +17,20 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/shared/co
import { Button } from "@/shared/components/ui/button"
import { useJobCard } from "./job-card-context"
import { formatDate, formatCurrency } from "@/shared/utils/formatters"
import { useState } from "react"
export default function JobCardPaymentsReceived() {
const jobCard = useJobCard()
const [hasOpened, setHasOpened] = useState(false)
return (
<Collapsible defaultOpen={false} className="group/collapsible">
<Collapsible
defaultOpen={false}
className="group/collapsible"
onOpenChange={(open) => {
if (open) setHasOpened(true)
}}
>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
@@ -36,6 +44,7 @@ export default function JobCardPaymentsReceived() {
</CardHeader>
<CollapsibleContent>
<CardContent className="pt-0">
{hasOpened && (
<CrudResource<PaymentReceivedClient>
extraParams={{ job_card_id: jobCard?.id }}
routeKey={PAYMENT_RECEIVED_ROUTES.INDEX}
@@ -48,6 +57,8 @@ export default function JobCardPaymentsReceived() {
resourceId={resourceId}
defaultJobCard={{ id: jobCard?.id, title: jobCard?.title }}
invoiceCustomer={jobCard?.customer as any}
lockJobCard
lockCustomer
onSuccess={invalidateQuery}
/>
)}
@@ -127,6 +138,7 @@ export default function JobCardPaymentsReceived() {
actionsColumn(),
]}
/>
)}
</CardContent>
</CollapsibleContent>
</Card>
@@ -5,7 +5,8 @@ import { AlertTriangle, Plus, Save } from "lucide-react"
import { Button } from "@/shared/components/ui/button"
import { Alert, AlertTitle } from "@/shared/components/ui/alert"
import { FieldGroup } from "@/shared/components/ui/field"
import { Field, FieldGroup, FieldLabel } from "@/shared/components/ui/field"
import { Input } from "@/shared/components/ui/input"
import {
Rhform,
RhfTextField,
@@ -36,6 +37,8 @@ export type PaymentReceivedFormProps = {
invoiceId?: string | null
invoiceCustomer?: { id?: number | null; first_name?: string | null; last_name?: string | null } | null
invoiceAmount?: number | string | null
lockJobCard?: boolean
lockCustomer?: boolean
}
// ── Default values ──
@@ -90,27 +93,33 @@ const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) =
// ── Component ──
export function PaymentReceivedForm({ resourceId, initialData, onSuccess, defaultJobCard, invoiceId, invoiceCustomer, invoiceAmount }: PaymentReceivedFormProps) {
export function PaymentReceivedForm({ resourceId, initialData, onSuccess, defaultJobCard, invoiceId, invoiceCustomer, invoiceAmount, lockJobCard, lockCustomer }: PaymentReceivedFormProps) {
const api = useAuthApi()
const isJobCardLocked = !resourceId && (lockJobCard ?? !!defaultJobCard?.id)
const isCustomerLocked = !resourceId && (lockCustomer ?? !!invoiceCustomer?.id)
const customerLabel = invoiceCustomer?.first_name
? `${invoiceCustomer.first_name} ${invoiceCustomer.last_name || ""}`.trim()
: (invoiceCustomer as any)?.company_name || (invoiceCustomer as any)?.name || ""
const resolvedInitialData = useMemo(() => {
const base: any = { ...(initialData as any) }
if (!resourceId) {
if (defaultJobCard?.id != null) {
base.job_card = toRelation(defaultJobCard.id, defaultJobCard.title ?? undefined)
base.job_card_id = defaultJobCard.id
base.job_card_name = defaultJobCard.title ?? undefined
}
if (invoiceCustomer?.id != null) {
const customerLabel = invoiceCustomer.first_name
? `${invoiceCustomer.first_name} ${invoiceCustomer.last_name || ""}`.trim()
: (invoiceCustomer as any).company_name || (invoiceCustomer as any).name || undefined
base.customer = toRelation(invoiceCustomer.id, customerLabel)
base.customer_id = invoiceCustomer.id
base.customer_name = customerLabel
}
if (invoiceAmount != null && invoiceAmount !== "") {
base.amount_received = Number(invoiceAmount)
}
}
return Object.keys(base).length ? base : initialData
}, [resourceId, defaultJobCard, initialData, invoiceCustomer, invoiceAmount])
}, [resourceId, defaultJobCard, initialData, invoiceCustomer, invoiceAmount, customerLabel])
const { form, isEditing } = useResourceForm<PaymentReceivedFormValues, any>({
schema: paymentReceivedFormSchema,
@@ -153,33 +162,49 @@ export function PaymentReceivedForm({ resourceId, initialData, onSuccess, defaul
<FieldGroup>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfAsyncSelectField
name="customer"
label="Customer"
placeholder="Select customer"
required
queryKey={[CUSTOMER_ROUTES.INDEX]}
listFn={() => api.customers.list()}
mapOption={(item: any) => ({
value: String(item.id),
label: item.first_name
? `${item.first_name} ${item.last_name || ""}`.trim()
: item.name || `#${item.id}`,
})}
{...STORE_OBJECT}
/>
<RhfAsyncSelectField
name="job_card"
label="Job Card"
placeholder="Select job card"
queryKey={[JOB_CARD_ROUTES.INDEX]}
listFn={() => api.jobCards.list()}
mapOption={(item: any) => ({
value: String(item.id),
label: item.title,
})}
{...STORE_OBJECT}
/>
{isCustomerLocked ? (
<Field>
<FieldLabel>
Customer<span className="text-destructive ms-0.5">*</span>
</FieldLabel>
<Input value={customerLabel} readOnly disabled />
</Field>
) : (
<RhfAsyncSelectField
name="customer"
label="Customer"
placeholder="Select customer"
required
queryKey={[CUSTOMER_ROUTES.INDEX]}
listFn={() => api.customers.list()}
mapOption={(item: any) => ({
value: String(item.id),
label: item.first_name
? `${item.first_name} ${item.last_name || ""}`.trim()
: item.name || `#${item.id}`,
})}
{...STORE_OBJECT}
/>
)}
{isJobCardLocked ? (
<Field>
<FieldLabel>Job Card</FieldLabel>
<Input value={defaultJobCard?.title ?? ""} readOnly disabled />
</Field>
) : (
<RhfAsyncSelectField
name="job_card"
label="Job Card"
placeholder="Select job card"
queryKey={[JOB_CARD_ROUTES.INDEX]}
listFn={() => api.jobCards.list()}
mapOption={(item: any) => ({
value: String(item.id),
label: item.title,
})}
{...STORE_OBJECT}
/>
)}
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">