Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-24 12:20:10 +03:00
co-authored by Copilot
parent 564e9e510f
commit 65964605e1
8 changed files with 193 additions and 60 deletions
@@ -3,9 +3,16 @@
import { createContext, useContext } from "react"
export type EstimateContextValue = {
id: string
label: string
data?: Record<string, any>
id?: number | string
title?: string | null
estimate_number?: string | null
discount?: string | null
sub_total?: number | string | null
total?: number | string | null
estimate_parts?: { quantity?: string | number; rate?: string | number }[]
estimate_services?: { quantity?: string | number; rate?: string | number }[]
estimate_expense_items?: { quantity?: string | number; rate?: string | number }[]
[key: string]: unknown
}
const EstimateContext = createContext<EstimateContextValue | null>(null)
@@ -84,6 +84,18 @@ function InfoItem({
export function EstimateGeneralInfo({ estimate }: EstimateGeneralInfoProps) {
return (
<div className="grid gap-6 md:grid-cols-2">
<div className="col-span-2">
<div className="flex flex-wrap gap-2">
{estimate?.labels?.map((label) => (
<Badge
key={label.id}
style={label.color_code ? { backgroundColor: label.color_code } : undefined}
>
{label.title}
</Badge>
))}
</div>
</div>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
@@ -136,7 +148,7 @@ export function EstimateGeneralInfo({ estimate }: EstimateGeneralInfoProps) {
label="Vehicle"
value={
estimate.vehicle
? `${estimate.vehicle.make ?? ""} ${estimate.vehicle.model ?? ""} (${estimate.vehicle.registration_number ?? ""})`.trim()
? `${estimate.vehicle.make ?? ""} ${estimate.vehicle.model ?? ""} ${(estimate as any).vehicle.year ?? ''}`.trim()
: estimate.vehicle_id ? `#${estimate.vehicle_id}` : null
}
/>
@@ -162,27 +174,7 @@ export function EstimateGeneralInfo({ estimate }: EstimateGeneralInfoProps) {
</CardContent>
</Card>
{estimate.labels && estimate.labels.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Tag className="size-4" /> Labels
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
{estimate.labels.map((label) => (
<Badge
key={label.id}
style={label.color_code ? { backgroundColor: label.color_code } : undefined}
>
{label.title}
</Badge>
))}
</div>
</CardContent>
</Card>
)}
{estimate.customer_remarks && estimate.customer_remarks.length > 0 && (
<Card>
@@ -0,0 +1,83 @@
"use client"
import { Card, CardContent } from "@/shared/components/ui/card"
import { Separator } from "@/shared/components/ui/separator"
import { formatCurrency, formatEnum } from "@/shared/utils/formatters"
import { useEstimate } from "./estimate-context"
export function EstimateTotalsSummary() {
const estimate = useEstimate()
if (!estimate) return null
const parts = estimate.estimate_parts ?? []
const services = estimate.estimate_services ?? []
const expenses = estimate.estimate_expense_items ?? []
const discount = estimate.discount
const displayTotal = parseFloat(String(estimate.total ?? 0)) || 0
const hasItems = parts.length > 0 || services.length > 0 || expenses.length > 0
if (!hasItems && displayTotal === 0) return null
const subTotal = parseFloat(String(estimate.sub_total ?? 0)) || 0
function lineTotal(items: { quantity?: string | number; rate?: string | number }[]) {
return items.reduce((sum, item) => {
const qty = parseFloat(String(item.quantity ?? 0))
const rate = parseFloat(String(item.rate ?? 0))
return sum + (isNaN(qty) || isNaN(rate) ? 0 : qty * rate)
}, 0)
}
return (
<Card>
<CardContent className="pt-6">
<div className="ml-auto max-w-sm space-y-2">
{hasItems && (
<>
{parts.length > 0 && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Parts ({parts.length})</span>
<span>{formatCurrency(lineTotal(parts))}</span>
</div>
)}
{services.length > 0 && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Services ({services.length})</span>
<span>{formatCurrency(lineTotal(services))}</span>
</div>
)}
{expenses.length > 0 && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Expenses ({expenses.length})</span>
<span>{formatCurrency(lineTotal(expenses))}</span>
</div>
)}
<Separator />
</>
)}
<div className="flex justify-between text-sm font-medium">
<span>Subtotal</span>
<span>{formatCurrency(subTotal)}</span>
</div>
{discount && discount !== "no" && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Discount ({formatEnum(discount)})</span>
<span className="text-muted-foreground">Applied</span>
</div>
)}
<Separator />
<div className="flex justify-between text-base font-semibold">
<span>Total</span>
<span>{formatCurrency(displayTotal)}</span>
</div>
</div>
</CardContent>
</Card>
)
}
@@ -72,6 +72,7 @@ function VehicleCombobox({
</FieldLabel>
<div ref={anchorRef}>
<Combobox
value={inputText || null}
onValueChange={(val) => {
const str = val !== null ? String(val) : ""
@@ -91,29 +92,20 @@ function VehicleCombobox({
showClear={!!inputText}
onBlur={onBlur}
aria-invalid={!!error || undefined}
/>
<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 &&
filtered.map((opt) => (
<ComboboxItem key={opt} value={opt}>
{opt}
</ComboboxItem>
))}
{!loading && filtered.length === 0 && (
<ComboboxEmpty>
{inputText
? `No suggestions for "${inputText}"`
: "Start typing to see suggestions"}
</ComboboxEmpty>
)}
</ComboboxList>
</ComboboxContent>
{(loading || filtered.length > 0) && (
<ComboboxContent anchor={anchorRef}>
<ComboboxList>
{!loading &&
filtered.map((opt) => (
<ComboboxItem key={opt} value={opt}>
{opt}
</ComboboxItem>
))}
</ComboboxList>
</ComboboxContent>
)}
</Combobox>
</div>
{error && <FieldError>{error}</FieldError>}
@@ -145,6 +137,8 @@ export function RhfVehicleIdentityField() {
const modelCtrl = useController({ name: "model", control })
const yearCtrl = useController({ name: "year", control })
const subModelCtrl = useController({ name: "sub_model", control })
const engineSizeCtrl = useController({ name: "engine_size", control })
const drivetrainCtrl = useController({ name: "drivetrain", control })
const makeValue = makeCtrl.field.value ?? ""
const modelValue = modelCtrl.field.value ?? ""
@@ -160,6 +154,8 @@ export function RhfVehicleIdentityField() {
model?: string
year?: string
sub_model?: string
engine_size?: string
drivetrain?: string
}
function extractRecords(response: unknown): MakeAndModelRecord[] {
@@ -205,10 +201,34 @@ export function RhfVehicleIdentityField() {
.map((r) => r.sub_model),
)
const engineSizes = unique(
allRecords
.filter(
(r) =>
(!makeValue || r.make === makeValue) &&
(!modelValue || r.model === modelValue) &&
(!yearValue || r.year === yearValue),
)
.map((r) => r.engine_size),
)
const drivetrains = unique(
allRecords
.filter(
(r) =>
(!makeValue || r.make === makeValue) &&
(!modelValue || r.model === modelValue) &&
(!yearValue || r.year === yearValue),
)
.map((r) => r.drivetrain),
)
const makesLoading = recordsLoading
const modelsLoading = false
const yearsLoading = false
const subModelsLoading = false
const engineSizesLoading = false
const drivetrainsLoading = false
// ── Cascading onChange handlers ──
@@ -218,17 +238,23 @@ export function RhfVehicleIdentityField() {
setValue("model", "")
setValue("year", "")
setValue("sub_model", "")
setValue("engine_size", "")
setValue("drivetrain", "")
}
function handleModelChange(val: string) {
modelCtrl.field.onChange(val)
setValue("year", "")
setValue("sub_model", "")
setValue("engine_size", "")
setValue("drivetrain", "")
}
function handleYearChange(val: string) {
yearCtrl.field.onChange(val)
setValue("sub_model", "")
setValue("engine_size", "")
setValue("drivetrain", "")
}
return (
@@ -269,6 +295,8 @@ export function RhfVehicleIdentityField() {
/>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<VehicleCombobox
label="Sub Model"
placeholder="e.g. LE"
@@ -279,6 +307,27 @@ export function RhfVehicleIdentityField() {
loading={subModelsLoading}
error={subModelCtrl.fieldState.error?.message}
/>
<VehicleCombobox
label="Engine Size"
placeholder="e.g. 2.5L"
value={engineSizeCtrl.field.value ?? ""}
onChange={(val) => engineSizeCtrl.field.onChange(val)}
onBlur={engineSizeCtrl.field.onBlur}
options={engineSizes}
loading={engineSizesLoading}
error={engineSizeCtrl.fieldState.error?.message}
/>
<VehicleCombobox
label="Drivetrain"
placeholder="e.g. FWD"
value={drivetrainCtrl.field.value ?? ""}
onChange={(val) => drivetrainCtrl.field.onChange(val)}
onBlur={drivetrainCtrl.field.onBlur}
options={drivetrains}
loading={drivetrainsLoading}
error={drivetrainCtrl.fieldState.error?.message}
/>
</div>
</div>
)
}
@@ -52,7 +52,6 @@ const DEFAULT_VALUES: VehicleFormValues = {
engine_size: "",
drivetrain: "",
mileage: "",
owners_number: "",
note: "",
image: null,
}
@@ -84,7 +83,6 @@ function mapToFormValues(data: unknown): VehicleFormValues {
engine_size: d.engine_size || "",
drivetrain: d.drivetrain || "",
mileage: d.mileage || "",
owners_number: d.owners_number || "",
note: d.note || "",
image: null,
}
@@ -106,7 +104,6 @@ function mapToPayload(values: VehicleFormValues) {
engine_size: values.engine_size || undefined,
drivetrain: values.drivetrain || undefined,
mileage: values.mileage || undefined,
owners_number: values.owners_number || undefined,
note: values.note || undefined,
image: values.image instanceof File ? values.image : undefined,
}
@@ -226,14 +223,7 @@ export function VehicleForm({ resourceId, initialData, onSuccess }: VehicleFormP
<RhfTextField name="vin_number" label="VIN Number" placeholder="e.g. 1HGBH41JXMN109186" />
</div>
{/* Technical specs */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfTextField name="engine_size" label="Engine Size" placeholder="e.g. 2.5L" />
<RhfTextField name="drivetrain" label="Drivetrain" placeholder="e.g. FWD" />
</div>
<RhfTextField name="owners_number" label="Number of Owners" placeholder="e.g. 1" />
{/* License & identifiers */}
<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="mileage" label="Mileage" placeholder="e.g. 10000" />