From 65964605e1a6b7eed09d98bb1623f75cea78a690 Mon Sep 17 00:00:00 2001 From: Mohammad Khyata Date: Fri, 24 Apr 2026 12:20:10 +0300 Subject: [PATCH] updates Co-authored-by: Copilot --- .../sales/estimates/[id]/layout.tsx | 9 +- .../sales/estimates/[id]/page.tsx | 2 + .../(authenticated)/sales/estimates/page.tsx | 5 + .../modules/estimates/estimate-context.tsx | 13 ++- .../estimates/estimate-general-info.tsx | 36 +++---- .../estimates/estimate-totals-summary.tsx | 83 +++++++++++++++++ .../vehicles/rhf-vehicle-identity-field.tsx | 93 ++++++++++++++----- .../modules/vehicles/vehicle-form.tsx | 12 +-- 8 files changed, 193 insertions(+), 60 deletions(-) create mode 100644 apps/dashboard/modules/estimates/estimate-totals-summary.tsx diff --git a/apps/dashboard/app/(authenticated)/sales/estimates/[id]/layout.tsx b/apps/dashboard/app/(authenticated)/sales/estimates/[id]/layout.tsx index 5bf62dd..757c18e 100644 --- a/apps/dashboard/app/(authenticated)/sales/estimates/[id]/layout.tsx +++ b/apps/dashboard/app/(authenticated)/sales/estimates/[id]/layout.tsx @@ -6,6 +6,7 @@ import { CreateInvoiceFromEstimateButton } from '@/modules/estimates/create-invo import { CreateJobCardFromEstimateButton } from '@/modules/estimates/create-job-card-from-estimate-button' import { FileTextIcon } from 'lucide-react' import React from 'react' +import { formatDate } from '@/shared/utils/formatters' export default async function layout(props: { params: Promise<{ id: string }> @@ -21,14 +22,18 @@ export default async function layout(props: { ? `${estimateData.estimate_number}${estimateData.title ? ` — ${estimateData.title}` : ''}` : title + if (!estimateData) { + return
Estimate not found.
+ } + return ( - + } title={title} description={ - estimateData?.date ? `Date: ${estimateData.date}` : undefined + estimateData?.date ? `Date: ${formatDate(estimateData.date)}` : undefined } backHref="/sales/estimates" actions={ diff --git a/apps/dashboard/app/(authenticated)/sales/estimates/[id]/page.tsx b/apps/dashboard/app/(authenticated)/sales/estimates/[id]/page.tsx index 24b9499..d141830 100644 --- a/apps/dashboard/app/(authenticated)/sales/estimates/[id]/page.tsx +++ b/apps/dashboard/app/(authenticated)/sales/estimates/[id]/page.tsx @@ -3,6 +3,7 @@ import { EstimateGeneralInfo } from '@/modules/estimates/estimate-general-info' import { EstimateServicesSection } from '@/modules/estimates/estimate-services-section' import { EstimatePartsSection } from '@/modules/estimates/estimate-parts-section' import { EstimateExpenseItemsSection } from '@/modules/estimates/estimate-expense-items-section' +import { EstimateTotalsSummary } from '@/modules/estimates/estimate-totals-summary' import DashboardPage from '@/base/components/layout/dashboard/dashboard-page' export default async function page(props: { params: Promise<{ id: string }> }) { @@ -23,6 +24,7 @@ export default async function page(props: { params: Promise<{ id: string }> }) { + ) diff --git a/apps/dashboard/app/(authenticated)/sales/estimates/page.tsx b/apps/dashboard/app/(authenticated)/sales/estimates/page.tsx index 668de4d..2fa2273 100644 --- a/apps/dashboard/app/(authenticated)/sales/estimates/page.tsx +++ b/apps/dashboard/app/(authenticated)/sales/estimates/page.tsx @@ -9,6 +9,7 @@ import type { EstimatesClient } from '@garage/api' import { Car, FileTextIcon } from 'lucide-react' import { Button } from '@/shared/components/ui/button' import Link from 'next/link' +import { formatDate } from '@/shared/utils/formatters' export default function EstimatesPage() { return ( @@ -67,6 +68,10 @@ export default function EstimatesPage() { { accessorKey: "date", header: ({ column }) => , + cell: ({ row }) => { + const item = row.original + return formatDate(item.date) + } }, { accessorKey: "has_insurance", diff --git a/apps/dashboard/modules/estimates/estimate-context.tsx b/apps/dashboard/modules/estimates/estimate-context.tsx index 0851ce5..c5ba45f 100644 --- a/apps/dashboard/modules/estimates/estimate-context.tsx +++ b/apps/dashboard/modules/estimates/estimate-context.tsx @@ -3,9 +3,16 @@ import { createContext, useContext } from "react" export type EstimateContextValue = { - id: string - label: string - data?: Record + 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(null) diff --git a/apps/dashboard/modules/estimates/estimate-general-info.tsx b/apps/dashboard/modules/estimates/estimate-general-info.tsx index f3e2fe2..733aa37 100644 --- a/apps/dashboard/modules/estimates/estimate-general-info.tsx +++ b/apps/dashboard/modules/estimates/estimate-general-info.tsx @@ -84,6 +84,18 @@ function InfoItem({ export function EstimateGeneralInfo({ estimate }: EstimateGeneralInfoProps) { return (
+
+
+ {estimate?.labels?.map((label) => ( + + {label.title} + + ))} +
+
@@ -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) { - {estimate.labels && estimate.labels.length > 0 && ( - - - - Labels - - - -
- {estimate.labels.map((label) => ( - - {label.title} - - ))} -
-
-
- )} + {estimate.customer_remarks && estimate.customer_remarks.length > 0 && ( diff --git a/apps/dashboard/modules/estimates/estimate-totals-summary.tsx b/apps/dashboard/modules/estimates/estimate-totals-summary.tsx new file mode 100644 index 0000000..515624f --- /dev/null +++ b/apps/dashboard/modules/estimates/estimate-totals-summary.tsx @@ -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 ( + + +
+ {hasItems && ( + <> + {parts.length > 0 && ( +
+ Parts ({parts.length}) + {formatCurrency(lineTotal(parts))} +
+ )} + {services.length > 0 && ( +
+ Services ({services.length}) + {formatCurrency(lineTotal(services))} +
+ )} + {expenses.length > 0 && ( +
+ Expenses ({expenses.length}) + {formatCurrency(lineTotal(expenses))} +
+ )} + + + )} + +
+ Subtotal + {formatCurrency(subTotal)} +
+ + {discount && discount !== "no" && ( +
+ Discount ({formatEnum(discount)}) + Applied +
+ )} + + + +
+ Total + {formatCurrency(displayTotal)} +
+
+
+
+ ) +} diff --git a/apps/dashboard/modules/vehicles/rhf-vehicle-identity-field.tsx b/apps/dashboard/modules/vehicles/rhf-vehicle-identity-field.tsx index 59eb862..8c3bf7d 100644 --- a/apps/dashboard/modules/vehicles/rhf-vehicle-identity-field.tsx +++ b/apps/dashboard/modules/vehicles/rhf-vehicle-identity-field.tsx @@ -72,6 +72,7 @@ function VehicleCombobox({
{ const str = val !== null ? String(val) : "" @@ -91,29 +92,20 @@ function VehicleCombobox({ showClear={!!inputText} onBlur={onBlur} aria-invalid={!!error || undefined} + /> - - - {loading && ( -
- -
- )} - {!loading && - filtered.map((opt) => ( - - {opt} - - ))} - {!loading && filtered.length === 0 && ( - - {inputText - ? `No suggestions for "${inputText}"` - : "Start typing to see suggestions"} - - )} -
-
+ {(loading || filtered.length > 0) && ( + + + {!loading && + filtered.map((opt) => ( + + {opt} + + ))} + + + )}
{error && {error}} @@ -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() { />
+ +
+ engineSizeCtrl.field.onChange(val)} + onBlur={engineSizeCtrl.field.onBlur} + options={engineSizes} + loading={engineSizesLoading} + error={engineSizeCtrl.fieldState.error?.message} + /> + drivetrainCtrl.field.onChange(val)} + onBlur={drivetrainCtrl.field.onBlur} + options={drivetrains} + loading={drivetrainsLoading} + error={drivetrainCtrl.fieldState.error?.message} + /> +
) } diff --git a/apps/dashboard/modules/vehicles/vehicle-form.tsx b/apps/dashboard/modules/vehicles/vehicle-form.tsx index aa7f149..59e90dc 100644 --- a/apps/dashboard/modules/vehicles/vehicle-form.tsx +++ b/apps/dashboard/modules/vehicles/vehicle-form.tsx @@ -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 - {/* Technical specs */} -
- - -
- - - + {/* License & identifiers */}