From 11d920ba6a732f2879dda79d07460fffeed93316 Mon Sep 17 00:00:00 2001 From: "Najjar\\NajjarV02" Date: Fri, 5 Jun 2026 14:58:18 +0400 Subject: [PATCH] fix(dashboard): resolve Asma tester-reported UI issues - 3.1 currency: replace hardcoded $ with AED in item & job-card price cells - 5.1 calendar: /calendars now renders the month calendar (was redirect to list) - 7.2 nav: remove dead Time Clocks / Time Sheets links (no routes, 404) - 5.4 check-in: enforce required department at schema level - 5.6 payments received: add invoice picker so the invoice is marked Paid - 5.2 share link: forward ws workspace param from the public inspection page Co-Authored-By: Claude Opus 4.8 (1M context) --- .../app/(authenticated)/calendars/page.tsx | 11 +++++-- .../items/expense-item/page.tsx | 3 +- .../items/service-group/page.tsx | 3 +- .../(authenticated)/items/services/page.tsx | 3 +- .../job-cards/[id]/expense-items/page.tsx | 5 +-- .../sales/job-cards/[id]/parts/page.tsx | 5 +-- .../sales/job-cards/[id]/services/page.tsx | 7 ++-- .../dashboard/app/inspection/[token]/page.tsx | 33 +++++++++++++++---- apps/dashboard/config/navGroups.tsx | 6 ++-- .../expense-items/expense-items-columns.tsx | 3 +- .../job-cards/job-card-check-in-dialog.tsx | 10 ++++++ .../dashboard/modules/parts/parts-columns.tsx | 5 +-- .../payment-received-form.tsx | 32 ++++++++++++++++-- .../payment-received.schema.ts | 1 + .../modules/services/services-columns.tsx | 3 +- 15 files changed, 104 insertions(+), 26 deletions(-) diff --git a/apps/dashboard/app/(authenticated)/calendars/page.tsx b/apps/dashboard/app/(authenticated)/calendars/page.tsx index 327344e..bfea14f 100644 --- a/apps/dashboard/app/(authenticated)/calendars/page.tsx +++ b/apps/dashboard/app/(authenticated)/calendars/page.tsx @@ -1,5 +1,12 @@ -import { redirect } from "next/navigation" +"use client" + +import { AppointmentsCalendarView } from "@/modules/appointments/appointments-calendar-view" export default function CalendarsPage() { - return redirect("/calendar/appointment/list") + return ( +
+

Calendar

+ +
+ ) } diff --git a/apps/dashboard/app/(authenticated)/items/expense-item/page.tsx b/apps/dashboard/app/(authenticated)/items/expense-item/page.tsx index cd14df0..0a4f9fd 100644 --- a/apps/dashboard/app/(authenticated)/items/expense-item/page.tsx +++ b/apps/dashboard/app/(authenticated)/items/expense-item/page.tsx @@ -4,6 +4,7 @@ import { ResourcePage } from "@/shared/data-view/resource-page" import { ColumnHeader } from "@/shared/data-view/table-view" import FormDialog from "@/shared/components/form-dialog" import { ExpenseItemForm } from "@/modules/expense-items/expense-item-form" +import { Money } from "@/shared/components/money" import { EXPENSE_ITEM_ROUTES } from "@garage/api" import type { ExpenseItemsClient } from "@garage/api" @@ -41,7 +42,7 @@ export default function ExpenseItemPage() { header: ({ column }) => , cell: ({ row }) => { const val = (row.original as any).purchase_price - return val != null ? `$${Number(val).toFixed(2)}` : "—" + return val != null ? : "—" }, }, { diff --git a/apps/dashboard/app/(authenticated)/items/service-group/page.tsx b/apps/dashboard/app/(authenticated)/items/service-group/page.tsx index 9439682..113d1c0 100644 --- a/apps/dashboard/app/(authenticated)/items/service-group/page.tsx +++ b/apps/dashboard/app/(authenticated)/items/service-group/page.tsx @@ -4,6 +4,7 @@ import { ResourcePage } from "@/shared/data-view/resource-page" import { ColumnHeader } from "@/shared/data-view/table-view" import FormDialog from "@/shared/components/form-dialog" import { ServiceGroupForm } from "@/modules/service-groups/service-group-form" +import { Money } from "@/shared/components/money" import { Badge } from "@/shared/components/ui/badge" import { SERVICE_GROUP_ROUTES } from "@garage/api" import type { ServiceGroupsClient } from "@garage/api" @@ -50,7 +51,7 @@ export default function ServiceGroupPage() { header: ({ column }) => , cell: ({ row }) => { const val = (row.original as any).selling_price - return val != null ? `$${Number(val).toFixed(2)}` : "—" + return val != null ? : "—" }, }, { diff --git a/apps/dashboard/app/(authenticated)/items/services/page.tsx b/apps/dashboard/app/(authenticated)/items/services/page.tsx index 3bf5686..95cd9ce 100644 --- a/apps/dashboard/app/(authenticated)/items/services/page.tsx +++ b/apps/dashboard/app/(authenticated)/items/services/page.tsx @@ -5,6 +5,7 @@ import { ColumnHeader } from "@/shared/data-view/table-view" import FormDialog from "@/shared/components/form-dialog" import { ImportDataButton } from "@/shared/components/import-data-button" import { ExportDataButton } from "@/shared/components/export-data-button" +import { Money } from "@/shared/components/money" import { useAuthApi } from "@/shared/useApi" import { ServiceForm } from "@/modules/services/service-form" import { SERVICE_ROUTES } from "@garage/api" @@ -75,7 +76,7 @@ export default function ServicesPage() { header: ({ column }) => , cell: ({ row }) => { const val = (row.original as any).selling_price - return val != null ? `$${Number(val).toFixed(2)}` : "—" + return val != null ? : "—" }, }, { diff --git a/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/expense-items/page.tsx b/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/expense-items/page.tsx index 7df0650..21a2944 100644 --- a/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/expense-items/page.tsx +++ b/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/expense-items/page.tsx @@ -24,6 +24,7 @@ import { toast } from "sonner" import { Ellipsis, Plus } from "lucide-react" import type { ColumnDef } from "@tanstack/react-table" import { JobCardExpenseItemForm } from "@/modules/job-cards/job-card-expense-item-form" +import { Money } from "@/shared/components/money" import { formatDate } from "@/shared/utils/formatters" // TODO: expense items invalidation is not working properly when create new expense item line. Need to investigate why and fix it. @@ -93,7 +94,7 @@ export default function JobCardExpenseItemsPage({ header: ({ column }) => , cell: ({ row }) => { const val = row.original.rate - return val != null ? `$${Number(val).toFixed(2)}` : "—" + return val != null ? : "—" }, }, { @@ -101,7 +102,7 @@ export default function JobCardExpenseItemsPage({ header: ({ column }) => , cell: ({ row }) => { const val = row.original.discount_amount - return val != null && val > 0 ? `$${Number(val).toFixed(2)}` : "—" + return val != null && val > 0 ? : "—" }, }, { diff --git a/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/parts/page.tsx b/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/parts/page.tsx index 6a0ef4a..bb48564 100644 --- a/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/parts/page.tsx +++ b/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/parts/page.tsx @@ -24,6 +24,7 @@ import { toast } from "sonner" import { Ellipsis, Plus } from "lucide-react" import type { ColumnDef } from "@tanstack/react-table" import { JobCardPartForm } from "@/modules/job-cards/job-card-part-form" +import { Money } from "@/shared/components/money" import { formatDate } from "@/shared/utils/formatters" import { JOB_CARD_ROUTES } from "@garage/api" @@ -93,7 +94,7 @@ export default function JobCardPartsPage({ header: ({ column }) => , cell: ({ row }) => { const val = row.original.rate - return val != null ? `$${Number(val).toFixed(2)}` : "—" + return val != null ? : "—" }, }, { @@ -101,7 +102,7 @@ export default function JobCardPartsPage({ header: ({ column }) => , cell: ({ row }) => { const val = row.original.discount_amount - return val != null && val > 0 ? `$${Number(val).toFixed(2)}` : "—" + return val != null && val > 0 ? : "—" }, }, { diff --git a/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/services/page.tsx b/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/services/page.tsx index 59bf6ed..5cb9847 100644 --- a/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/services/page.tsx +++ b/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/services/page.tsx @@ -24,6 +24,7 @@ import { toast } from "sonner" import { Ellipsis, Plus } from "lucide-react" import type { ColumnDef } from "@tanstack/react-table" import { JobCardServiceForm } from "@/modules/job-cards/job-card-service-form" +import { Money } from "@/shared/components/money" import { formatDate } from "@/shared/utils/formatters" import { Badge } from "@/shared/components/ui/badge" @@ -108,7 +109,7 @@ export default function JobCardServicesPage({ header: ({ column }) => , cell: ({ row }) => { const val = row.original.rate - return val != null ? `$${Number(val).toFixed(2)}` : "—" + return val != null ? : "—" }, }, { @@ -116,7 +117,7 @@ export default function JobCardServicesPage({ header: ({ column }) => , cell: ({ row }) => { const lr = row.original.labor_rate - return lr ? `${lr.title} ($${Number(lr.rate).toFixed(2)})` : "—" + return lr ? {lr.title} () : "—" }, }, { @@ -134,7 +135,7 @@ export default function JobCardServicesPage({ header: ({ column }) => , cell: ({ row }) => { const val = row.original.discount_amount - return val != null && val > 0 ? `$${Number(val).toFixed(2)}` : "—" + return val != null && val > 0 ? : "—" }, }, { diff --git a/apps/dashboard/app/inspection/[token]/page.tsx b/apps/dashboard/app/inspection/[token]/page.tsx index 0b08129..8c0d0c2 100644 --- a/apps/dashboard/app/inspection/[token]/page.tsx +++ b/apps/dashboard/app/inspection/[token]/page.tsx @@ -1,7 +1,7 @@ "use client" -import { useEffect, useState } from "react" -import { useParams } from "next/navigation" +import { Suspense, useEffect, useState } from "react" +import { useParams, useSearchParams } from "next/navigation" import { AlertCircle, Calendar, @@ -125,9 +125,13 @@ function ScoreRing({ score }: { score: number | null }) { ) } -export default function PublicInspectionPage() { +function PublicInspectionContent() { const params = useParams<{ token: string }>() const token = params.token + // Workspace UUID baked into the share URL — the public API endpoint needs it + // to resolve which tenant DB holds this inspection. + const searchParams = useSearchParams() + const ws = searchParams.get("ws") ?? "" const [data, setData] = useState(null) const [error, setError] = useState(null) const [loading, setLoading] = useState(true) @@ -135,7 +139,8 @@ export default function PublicInspectionPage() { useEffect(() => { const base = process.env.NEXT_PUBLIC_API_URL ?? "" - fetch(`${base.replace(/\/$/, "")}/api/public/inspections/${token}`, { + const wsQuery = ws ? `?ws=${encodeURIComponent(ws)}` : "" + fetch(`${base.replace(/\/$/, "")}/api/public/inspections/${token}${wsQuery}`, { headers: { Accept: "application/json", "X-Lang": "en" }, }) .then(async (res) => { @@ -148,7 +153,7 @@ export default function PublicInspectionPage() { .then((json: { data: PublicInspection }) => setData(json.data)) .catch((e: Error) => setError(e.message)) .finally(() => setLoading(false)) - }, [token]) + }, [token, ws]) if (loading) { return ( @@ -200,7 +205,7 @@ export default function PublicInspectionPage() { Inspection report +
+ Loading inspection… +
+ + } + > + + + ) +} + function InfoCell({ icon: Icon, label, diff --git a/apps/dashboard/config/navGroups.tsx b/apps/dashboard/config/navGroups.tsx index 90478f3..ce14a6a 100644 --- a/apps/dashboard/config/navGroups.tsx +++ b/apps/dashboard/config/navGroups.tsx @@ -142,8 +142,10 @@ export const navGroups: NavGroup[] = [ icon: , items: [ { title: "Employees", href: "/productivity/employees", icon: }, - { title: "Time Clocks", href: "/productivity/time-clocks", icon: }, - { title: "Time Sheets", href: "/productivity/timesheet", icon: }, + // Time Clocks / Time Sheets have no routes yet — links 404. Re-enable once + // /productivity/time-clocks and /productivity/timesheet pages exist. + // { title: "Time Clocks", href: "/productivity/time-clocks", icon: }, + // { title: "Time Sheets", href: "/productivity/timesheet", icon: }, // { title: "Payroll", href: "/productivity/payroll", icon: }, // { title: "Payments Made", href: "/productivity/employee-payments-made", icon: }, { title: "Shop Calendars", href: "/productivity/shop-calendars", icon: }, diff --git a/apps/dashboard/modules/expense-items/expense-items-columns.tsx b/apps/dashboard/modules/expense-items/expense-items-columns.tsx index 86daeed..2f25cd8 100644 --- a/apps/dashboard/modules/expense-items/expense-items-columns.tsx +++ b/apps/dashboard/modules/expense-items/expense-items-columns.tsx @@ -1,4 +1,5 @@ import { ColumnHeader } from "@/shared/data-view/table-view" +import { Money } from "@/shared/components/money" import type { ColumnDef } from "@tanstack/react-table" /** Core expense-item columns shared between the expense items page and selector dialogs. */ @@ -16,7 +17,7 @@ export const expenseItemColumns = { header: ({ column }) => , cell: ({ row }) => { const val = (row.original as any).purchase_price - return val != null ? `$${Number(val).toFixed(2)}` : "—" + return val != null ? : "—" }, }, chartOfAccount: { diff --git a/apps/dashboard/modules/job-cards/job-card-check-in-dialog.tsx b/apps/dashboard/modules/job-cards/job-card-check-in-dialog.tsx index d951b22..73601e2 100644 --- a/apps/dashboard/modules/job-cards/job-card-check-in-dialog.tsx +++ b/apps/dashboard/modules/job-cards/job-card-check-in-dialog.tsx @@ -38,6 +38,16 @@ const checkInSchema = z.object({ department: z.object({ value: z.any(), label: z.string() }).nullable().optional(), delivery_date: z.string().optional(), delivery_time: z.string().optional(), +}).superRefine((values, ctx) => { + // Department is marked required in the UI (asterisk); enforce it at the + // schema level so the form cannot be submitted without one. + if (!values.department) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ["department"], + message: "Department is required", + }) + } }) type CheckInFormValues = z.infer diff --git a/apps/dashboard/modules/parts/parts-columns.tsx b/apps/dashboard/modules/parts/parts-columns.tsx index e736927..9c076a6 100644 --- a/apps/dashboard/modules/parts/parts-columns.tsx +++ b/apps/dashboard/modules/parts/parts-columns.tsx @@ -1,4 +1,5 @@ import { ColumnHeader } from "@/shared/data-view/table-view" +import { Money } from "@/shared/components/money" import { Badge } from "@/shared/components/ui/badge" import type { ColumnDef } from "@tanstack/react-table" @@ -32,7 +33,7 @@ export const partColumns = { header: ({ column }) => , cell: ({ row }) => { const val = (row.original as any).selling_price - return val != null ? `$${Number(val).toFixed(2)}` : "—" + return val != null ? : "—" }, }, purchasePrice: { @@ -40,7 +41,7 @@ export const partColumns = { header: ({ column }) => , cell: ({ row }) => { const val = (row.original as any).purchase_price - return val != null ? `$${Number(val).toFixed(2)}` : "—" + return val != null ? : "—" }, }, stock: { diff --git a/apps/dashboard/modules/payment-received/payment-received-form.tsx b/apps/dashboard/modules/payment-received/payment-received-form.tsx index 8d1be35..361cb0b 100644 --- a/apps/dashboard/modules/payment-received/payment-received-form.tsx +++ b/apps/dashboard/modules/payment-received/payment-received-form.tsx @@ -25,7 +25,7 @@ import { paymentReceivedFormSchema, type PaymentReceivedFormValues, } from "./payment-received.schema" -import { PAYMENT_MODE_ROUTES, CUSTOMER_ROUTES, JOB_CARD_ROUTES, PAYMENT_RECEIVED_ROUTES } from "@garage/api" +import { PAYMENT_MODE_ROUTES, CUSTOMER_ROUTES, JOB_CARD_ROUTES, INVOICE_ROUTES, PAYMENT_RECEIVED_ROUTES } from "@garage/api" // ── Props ── @@ -45,6 +45,7 @@ export type PaymentReceivedFormProps = { const DEFAULT_VALUES: PaymentReceivedFormValues = { job_card: null, + invoice: null, payment_mode: null, customer: null, amount_received: 0, @@ -61,6 +62,9 @@ function mapToFormValues(data: unknown): PaymentReceivedFormValues { const jobCardId = d.job_card_id ?? d.job_card?.id const jobCardLabel = d.job_card?.title ?? d.job_card_name + const invoiceId = d.invoice_id ?? d.invoice?.id + const invoiceLabel = d.invoice?.invoice_number ?? d.invoice?.title ?? d.invoice_number + const paymentModeId = d.payment_mode_id ?? d.payment_mode?.id const paymentModeLabel = d.payment_mode?.title ?? d.payment_mode?.name ?? d.payment_mode_name @@ -76,6 +80,7 @@ function mapToFormValues(data: unknown): PaymentReceivedFormValues { return { job_card: toRelation(jobCardId, jobCardLabel), + invoice: toRelation(invoiceId, invoiceLabel), payment_mode: toRelation(paymentModeId, paymentModeLabel), customer: toRelation(customerId, customerLabel), amount_received: d.amount_received != null ? Number(d.amount_received) : 0, @@ -86,6 +91,9 @@ function mapToFormValues(data: unknown): PaymentReceivedFormValues { } function mapFormToPayload(values: PaymentReceivedFormValues, invoiceId?: string | null) { + // Prefer the invoice the form was opened from; otherwise the one picked in + // the standalone form. Linking invoice_id is what flips the invoice to Paid. + const resolvedInvoiceId = invoiceId ?? toId(values.invoice) return { job_card_id: toId(values.job_card), payment_mode_id: toId(values.payment_mode), @@ -94,7 +102,7 @@ function mapFormToPayload(values: PaymentReceivedFormValues, invoiceId?: string payment_number: values.payment_number || undefined, payment_date: values.payment_date, note: values.note || undefined, - ...(invoiceId ? { invoice_id: Number(invoiceId) } : {}), + ...(resolvedInvoiceId ? { invoice_id: Number(resolvedInvoiceId) } : {}), } } @@ -114,6 +122,10 @@ export function PaymentReceivedForm({ resourceId, initialData, onSuccess, defaul const isJobCardLocked = !resourceId && (lockJobCard ?? !!defaultJobCard?.id) const isCustomerLocked = !resourceId && (lockCustomer ?? !!invoiceCustomer?.id) + // When opened from a specific invoice the link is implicit (invoiceId prop). + // Standalone (Payments Received → Add) needs a picker so the payment can be + // tied to an invoice — otherwise the invoice never shows as Paid. + const showInvoiceField = !invoiceId const customerLabel = invoiceCustomer?.first_name ? `${invoiceCustomer.first_name} ${invoiceCustomer.last_name || ""}`.trim() @@ -225,6 +237,22 @@ export function PaymentReceivedForm({ resourceId, initialData, onSuccess, defaul )} + {showInvoiceField && ( + api.invoices.list()} + mapOption={(item: any) => ({ + value: String(item.id), + label: item.invoice_number ?? item.title ?? `#${item.id}`, + })} + {...STORE_OBJECT} + /> + )} +
, cell: ({ row }) => { const val = (row.original as any).selling_price - return val != null ? `$${Number(val).toFixed(2)}` : "—" + return val != null ? : "—" }, }, } satisfies Record>