From 6b356d2855c1de2b563b02e024f796d9a90b353b Mon Sep 17 00:00:00 2001 From: humam kerdiah Date: Wed, 13 May 2026 15:55:40 +0400 Subject: [PATCH] fix estimate table and expense items --- .claude/settings.local.json | 9 +++++ .env.prod | 2 + .../items/expense-item/page.tsx | 7 +++- .../(authenticated)/sales/estimates/page.tsx | 21 ++++++---- .../(authenticated)/sales/invoice/page.tsx | 19 +++++++-- .../modules/estimates/estimate-form.tsx | 5 ++- .../expense-items/expense-item-form.tsx | 40 ++++++++++++------- .../modules/invoices/invoice-form.tsx | 5 ++- .../modules/job-cards/job-card-form.tsx | 5 ++- .../resource-page/use-resource-page.ts | 6 ++- .../shared/hooks/use-resource-form.ts | 17 ++++++-- 11 files changed, 96 insertions(+), 40 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 .env.prod diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..9e421fb --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(pnpm --version)", + "Bash(grep -E \"\\\\.\\(tsx?|jsx?\\)$\")", + "Bash(grep -E \"\\\\.\\(tsx?\\)$\")" + ] + } +} diff --git a/.env.prod b/.env.prod new file mode 100644 index 0000000..4d35848 --- /dev/null +++ b/.env.prod @@ -0,0 +1,2 @@ +NIXPACKS_NODE_VERSION=22 +NEXT_PUBLIC_API_URL=http://reparee.test \ No newline at end of file diff --git a/apps/dashboard/app/(authenticated)/items/expense-item/page.tsx b/apps/dashboard/app/(authenticated)/items/expense-item/page.tsx index 9ee37bc..5b77926 100644 --- a/apps/dashboard/app/(authenticated)/items/expense-item/page.tsx +++ b/apps/dashboard/app/(authenticated)/items/expense-item/page.tsx @@ -16,11 +16,14 @@ export default function ExpenseItemPage() { headerProps={({ selectedItem, invalidateQuery }) => ({ actions: ( - {(resourceId) => ( + {(resourceId, { close }) => ( { + invalidateQuery() + close() + }} /> )} diff --git a/apps/dashboard/app/(authenticated)/sales/estimates/page.tsx b/apps/dashboard/app/(authenticated)/sales/estimates/page.tsx index ab2504f..a9136a9 100644 --- a/apps/dashboard/app/(authenticated)/sales/estimates/page.tsx +++ b/apps/dashboard/app/(authenticated)/sales/estimates/page.tsx @@ -1,5 +1,6 @@ "use client" +import { useRouter } from 'next/navigation' import { ResourcePage } from '@/shared/data-view/resource-page' import { ColumnHeader } from '@/shared/data-view/table-view' import FormDialog from '@/shared/components/form-dialog' @@ -14,11 +15,13 @@ import { getVehicleLabel } from '@/modules/vehicles/utils/getVehicleLabel' import { getFullName } from '@/shared/utils/getFullName' export default function EstimatesPage() { + const router = useRouter() return ( pageTitle="Estimates" routeKey={ESTIMATE_ROUTES.INDEX} getClient={(api) => api.estimates} + onRowClick={(row) => router.push(`/sales/estimates/${(row as any).id}`)} headerProps={({ selectedItem, invalidateQuery }) => ({ actions: ( @@ -39,7 +42,7 @@ export default function EstimatesPage() { const item = row.original return (
- + e.stopPropagation()}> {item.title} @@ -55,12 +58,14 @@ export default function EstimatesPage() { accessorKey: "customer_name", header: ({ column }) => , cell: ({ row }) => { - const item:any = row.original + const item:any = row.original + if (!item.customer?.id) return "—" return ( -
- - {getFullName(item.customer) || "—"} -
+ ) } }, @@ -68,8 +73,8 @@ export default function EstimatesPage() { accessorKey: "vehicle", header: ({ column }) => , cell: ({ row }) => { - const item :any= row.original - return + ) : ( +

{customerLabel}

+ )} +

{subline}

) }, diff --git a/apps/dashboard/modules/estimates/estimate-form.tsx b/apps/dashboard/modules/estimates/estimate-form.tsx index ea4dffd..086ca88 100644 --- a/apps/dashboard/modules/estimates/estimate-form.tsx +++ b/apps/dashboard/modules/estimates/estimate-form.tsx @@ -140,12 +140,13 @@ const DISCOUNT_OPTIONS = DiscountType.map((value) => ({ export function EstimateForm({ resourceId, initialData, onSuccess }: EstimateFormProps) { const api = useAuthApi() - const { form, isEditing } = useResourceForm({ + const { form, isEditing, invalidate } = useResourceForm({ schema: estimateFormSchema, defaultValues: DEFAULT_VALUES, resourceId, initialData, queryKey: [ESTIMATE_ROUTES.BY_ID, resourceId], + initialize: (id) => api.estimates.show(id), mapToFormValues, }) @@ -163,7 +164,7 @@ export function EstimateForm({ resourceId, initialData, onSuccess }: EstimateFor return promise }, onSuccess: () => { - form.reset() + if (!isEditing) form.reset() onSuccess?.() }, }) diff --git a/apps/dashboard/modules/expense-items/expense-item-form.tsx b/apps/dashboard/modules/expense-items/expense-item-form.tsx index 814e1f4..823e783 100644 --- a/apps/dashboard/modules/expense-items/expense-item-form.tsx +++ b/apps/dashboard/modules/expense-items/expense-item-form.tsx @@ -27,9 +27,9 @@ import { INVENTORY_CATEGORY_ROUTES, INVENTORY_ROUTES, DEPARTMENT_ROUTES, - VENDOR_ROUTES, } from "@garage/api" import { InventoryCategoryCrudDialog } from "./inventory-category-crud-dialog" +import { RhfVendorSelectField } from "@/modules/vendors/rhf-vendor-select-field" // ── Constants ── @@ -84,15 +84,26 @@ function mapToFormValues(data: unknown): ExpenseItemFormValues { sku: d.sku || "", item_code: d.item_code || "", description: d.description || "", - category: toRelation(d.category_id, d.category_title ?? d.category_name), - unit_type: toRelation(d.unit_type_id, d.unit_type_title ?? d.unit_type_name), - department: toRelation(d.department_id, d.department_name ?? d.department_title), + category: toRelation( + d.category_id ?? d.category?.id, + d.category_title ?? d.category_name ?? d.category?.title ?? d.category?.name, + ), + unit_type: toRelation( + d.unit_type_id ?? d.unit_type?.id, + d.unit_type_title ?? d.unit_type_name ?? d.unit_type?.title ?? d.unit_type?.name, + ), + department: toRelation( + d.department_id ?? d.department?.id, + d.department_name ?? d.department_title ?? d.department?.name ?? d.department?.title, + ), purchase_information: d.purchase_information ?? true, purchase_price: d.purchase_price ?? undefined, purchase_chart_of_account: d.purchase_chart_of_account || "", purchase_preferred_vendor: toRelation( - d.purchase_preferred_vendor_id, - d.purchase_preferred_vendor_name, + d.purchase_preferred_vendor_id ?? d.purchase_preferred_vendor?.id, + d.purchase_preferred_vendor_name + ?? d.purchase_preferred_vendor?.company_name + ?? [d.purchase_preferred_vendor?.first_name, d.purchase_preferred_vendor?.last_name].filter(Boolean).join(" "), ), sales_information: d.sales_information ?? false, selling_price: d.selling_price ?? undefined, @@ -127,7 +138,7 @@ function mapFormToPayload(values: ExpenseItemFormValues) { export function ExpenseItemForm({ resourceId, initialData, onSuccess }: ExpenseItemFormProps) { const api = useAuthApi() - const { form, isEditing } = useResourceForm({ + const { form, isEditing, invalidate } = useResourceForm({ schema: expenseItemFormSchema, defaultValues: DEFAULT_VALUES, resourceId, @@ -150,7 +161,7 @@ export function ExpenseItemForm({ resourceId, initialData, onSuccess }: ExpenseI return promise }, onSuccess: () => { - form.reset() + if (!isEditing) form.reset() onSuccess?.() }, }) @@ -247,7 +258,7 @@ export function ExpenseItemForm({ resourceId, initialData, onSuccess }: ExpenseI {/* Purchase Information */} - {/* @@ -259,22 +270,21 @@ export function ExpenseItemForm({ resourceId, initialData, onSuccess }: ExpenseI placeholder="0.00" type="number" /> + {/* TODO(phase-2): wire Purchase Chart of Account to the chart-of-accounts module (currently disabled, marked "Coming soon"). */} - api.vendors.list()} - mapOption={(item: any) => ({ value: String(item.id), label: item.name ?? String(item.id) })} - {...STORE_OBJECT} - /> */} + /> {/* Sales Information */} {/* ({ + const { form, isEditing, invalidate } = useResourceForm({ schema: invoiceFormSchema, defaultValues: DEFAULT_VALUES, resourceId, initialData, queryKey: [INVOICE_ROUTES.BY_ID, resourceId], + initialize: (id) => api.invoices.show(id), mapToFormValues, }) @@ -277,7 +278,7 @@ export function InvoiceForm({ resourceId, initialData, onSuccess }: InvoiceFormP return promise }, onSuccess: () => { - form.reset() + if (!isEditing) form.reset() onSuccess?.() }, }) diff --git a/apps/dashboard/modules/job-cards/job-card-form.tsx b/apps/dashboard/modules/job-cards/job-card-form.tsx index 23ebf09..77ca6c2 100644 --- a/apps/dashboard/modules/job-cards/job-card-form.tsx +++ b/apps/dashboard/modules/job-cards/job-card-form.tsx @@ -231,12 +231,13 @@ export function JobCardForm({ resourceId, initialData, onSuccess }: JobCardFormP const api = useAuthApi() const [isCheckInDialogOpen, setIsCheckInDialogOpen] = useState(false) - const { form, isEditing } = useResourceForm({ + const { form, isEditing, invalidate } = useResourceForm({ schema: jobCardFormSchema, defaultValues: DEFAULT_VALUES, resourceId, initialData, queryKey: [JOB_CARD_ROUTES.BY_ID, resourceId], + initialize: (id) => api.jobCards.show(id), mapToFormValues, }) @@ -260,7 +261,7 @@ export function JobCardForm({ resourceId, initialData, onSuccess }: JobCardFormP }, onSuccess: () => { setIsCheckInDialogOpen(false) - form.reset() + if (!isEditing) form.reset() onSuccess?.() }, }) diff --git a/apps/dashboard/shared/data-view/resource-page/use-resource-page.ts b/apps/dashboard/shared/data-view/resource-page/use-resource-page.ts index 34e4a2e..5779bfa 100644 --- a/apps/dashboard/shared/data-view/resource-page/use-resource-page.ts +++ b/apps/dashboard/shared/data-view/resource-page/use-resource-page.ts @@ -1,6 +1,6 @@ "use client" -import { useState } from "react" +import { useEffect, useState } from "react" import { useMutation } from "@tanstack/react-query" import { toast } from "sonner" import { confirm } from "@/shared/components/confirm-dialog" @@ -42,6 +42,10 @@ export function useResourcePage({ const { open: openDialog, close: closeDialog, isOpen, resourceId } = useFormDialog(paramKey) const [selectedItem, setSelectedItem] = useState(null) + useEffect(() => { + if (!resourceId) setSelectedItem(null) + }, [resourceId]) + const tableQuery = useDataTableQuery({ queryKey: [routeKey], client, diff --git a/apps/dashboard/shared/hooks/use-resource-form.ts b/apps/dashboard/shared/hooks/use-resource-form.ts index 2b3a35b..c278d64 100644 --- a/apps/dashboard/shared/hooks/use-resource-form.ts +++ b/apps/dashboard/shared/hooks/use-resource-form.ts @@ -3,7 +3,7 @@ import { useEffect } from "react" import { useForm, type DefaultValues, type FieldValues, type UseFormReturn } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" -import { useQuery, type QueryKey } from "@tanstack/react-query" +import { useQuery, useQueryClient, type QueryKey } from "@tanstack/react-query" import type { ZodType } from "zod" type UseResourceFormOptions = { @@ -20,6 +20,7 @@ type UseResourceFormReturn = { form: UseFormReturn isEditing: boolean isInitializing: boolean + invalidate: () => void } export function useResourceForm({ @@ -32,11 +33,15 @@ export function useResourceForm): UseResourceFormReturn { const isEditing = !!resourceId + const queryClient = useQueryClient() + const resolvedQueryKey = queryKey ?? ["resource", resourceId] const { data: queriedData, isLoading: isQueryLoading } = useQuery({ - queryKey: queryKey ?? ["resource", resourceId], + queryKey: resolvedQueryKey, queryFn: () => initialize!(resourceId!), enabled: isEditing && !!initialize, + staleTime: 0, + refetchOnMount: "always", }) const resolvedData = queriedData ?? (isEditing ? initialData : undefined) @@ -49,7 +54,7 @@ export function useResourceForm { if (!isEditing) { if (initialData) { - form.reset({ ...defaultValues, ...initialData } as any) + form.reset({ ...defaultValues, ...mapToFormValues(initialData) } as any) } else { form.reset(defaultValues) } @@ -61,5 +66,9 @@ export function useResourceForm { + queryClient.invalidateQueries({ queryKey: resolvedQueryKey }) + } + + return { form, isEditing, isInitializing: isEditing && !!initialize && isQueryLoading, invalidate } }