diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..0b58641 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,13 @@ +{ + "permissions": { + "allow": [ + "Bash(pnpm --version)", + "Bash(grep -E \"\\\\.\\(tsx?|jsx?\\)$\")", + "Bash(grep -E \"\\\\.\\(tsx?\\)$\")" + ] + }, + "enabledMcpjsonServers": [ + "code-review-graph" + ], + "enableAllProjectMcpServers": true +} 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)/calendar/appointment/list/page.tsx b/apps/dashboard/app/(authenticated)/calendar/appointment/list/page.tsx index b8cae45..9600a69 100644 --- a/apps/dashboard/app/(authenticated)/calendar/appointment/list/page.tsx +++ b/apps/dashboard/app/(authenticated)/calendar/appointment/list/page.tsx @@ -5,11 +5,11 @@ import { ResourcePage } from "@/shared/data-view/resource-page" import { ColumnHeader } from "@/shared/data-view/table-view" import FormDialog from "@/shared/components/form-dialog" import { AppointmentForm } from "@/modules/appointments/appointment-form" -import { APPOINTMENT_ROUTES } from "@garage/api" +import { APPOINTMENT_ROUTES, AppointmentStatus } from "@garage/api" import type { AppointmentsClient } from "@garage/api" -import { CalendarCheck2Icon, ClipboardListIcon, ClockIcon, ExternalLinkIcon } from "lucide-react" +import { CalendarCheck2Icon, ClipboardListIcon, ClockIcon } from "lucide-react" import { Badge } from "@/shared/components/ui/badge" -import { Button } from "@/shared/components/ui/button" +import { RelationLink } from "@/shared/components/relation-link" const STATUS_COLORS: Record = { requested: "bg-yellow-100 text-yellow-800", @@ -26,6 +26,9 @@ export default function AppointmentsPage() { pageTitle="Appointments" routeKey={APPOINTMENT_ROUTES.INDEX} + searchable + searchPlaceholder="Search appointments..." + statusFilter={{ statuses: AppointmentStatus }} getClient={(api) => api.appointments} onRowClick={(row) => router.push(`/calendar/appointment/${(row as any).id}`)} headerProps={({ selectedItem, invalidateQuery }) => ({ @@ -86,22 +89,14 @@ export default function AppointmentsPage() { id: "job_card", header: ({ column }) => , cell: ({ row }) => { - const jobCardId = (row.original as any).job_card_id - if (!jobCardId) return + const item = row.original as any + const jobCardId = item.job_card_id ?? item.job_card?.id return ( - + ) }, }, diff --git a/apps/dashboard/app/(authenticated)/items/adjustment/page.tsx b/apps/dashboard/app/(authenticated)/items/adjustment/page.tsx index 977677d..2370d8a 100644 --- a/apps/dashboard/app/(authenticated)/items/adjustment/page.tsx +++ b/apps/dashboard/app/(authenticated)/items/adjustment/page.tsx @@ -201,15 +201,20 @@ export default function InventoryAdjustmentsPage() { pageTitle="Inventory Adjustments" routeKey={INVENTORY_ADJUSTMENT_ROUTES.INDEX} + searchable + searchPlaceholder="Search adjustments..." getClient={(api) => api.inventoryAdjustments} headerProps={({ selectedItem, invalidateQuery }) => ({ actions: ( - - {(resourceId) => ( + + {(resourceId, { close }) => ( { invalidateQuery(); close() }} /> )} diff --git a/apps/dashboard/app/(authenticated)/items/expense-item/page.tsx b/apps/dashboard/app/(authenticated)/items/expense-item/page.tsx index 9ee37bc..cd14df0 100644 --- a/apps/dashboard/app/(authenticated)/items/expense-item/page.tsx +++ b/apps/dashboard/app/(authenticated)/items/expense-item/page.tsx @@ -12,15 +12,20 @@ export default function ExpenseItemPage() { pageTitle="Expense Items" routeKey={EXPENSE_ITEM_ROUTES.INDEX} + searchable + searchPlaceholder="Search expense items..." getClient={(api) => api.expenseItems} headerProps={({ selectedItem, invalidateQuery }) => ({ actions: ( - {(resourceId) => ( + {(resourceId, { close }) => ( { + invalidateQuery() + close() + }} /> )} diff --git a/apps/dashboard/app/(authenticated)/items/parts/page.tsx b/apps/dashboard/app/(authenticated)/items/parts/page.tsx index d3806d0..656ebcf 100644 --- a/apps/dashboard/app/(authenticated)/items/parts/page.tsx +++ b/apps/dashboard/app/(authenticated)/items/parts/page.tsx @@ -17,6 +17,8 @@ export default function PartsPage() { pageTitle="Parts" routeKey={PARTS_ROUTES.INDEX} + searchable + searchPlaceholder="Search parts..." getClient={(api) => api.parts} headerProps={({ selectedItem, invalidateQuery }) => ({ actions: ( @@ -24,6 +26,7 @@ export default function PartsPage() { api.parts.importData(file)} onSuccess={invalidateQuery} + entityLabel="Parts" /> api.parts.exportData(filters)} diff --git a/apps/dashboard/app/(authenticated)/items/service-group/page.tsx b/apps/dashboard/app/(authenticated)/items/service-group/page.tsx index 0a8d5db..9439682 100644 --- a/apps/dashboard/app/(authenticated)/items/service-group/page.tsx +++ b/apps/dashboard/app/(authenticated)/items/service-group/page.tsx @@ -13,6 +13,8 @@ export default function ServiceGroupPage() { pageTitle="Service Groups" routeKey={SERVICE_GROUP_ROUTES.INDEX} + searchable + searchPlaceholder="Search service groups..." getClient={(api) => api.serviceGroups} headerProps={({ selectedItem, invalidateQuery }) => ({ actions: ( diff --git a/apps/dashboard/app/(authenticated)/items/services/page.tsx b/apps/dashboard/app/(authenticated)/items/services/page.tsx index ecf4ebc..3bf5686 100644 --- a/apps/dashboard/app/(authenticated)/items/services/page.tsx +++ b/apps/dashboard/app/(authenticated)/items/services/page.tsx @@ -17,6 +17,8 @@ export default function ServicesPage() { pageTitle="Services" routeKey={SERVICE_ROUTES.INDEX} + searchable + searchPlaceholder="Search services..." getClient={(api) => api.services} headerProps={({ selectedItem, invalidateQuery }) => ({ actions: ( @@ -24,6 +26,7 @@ export default function ServicesPage() { api.services.importData(file)} onSuccess={invalidateQuery} + entityLabel="Services" /> api.services.exportData(filters)} diff --git a/apps/dashboard/app/(authenticated)/productivity/employees/page.tsx b/apps/dashboard/app/(authenticated)/productivity/employees/page.tsx index 22ad472..0e27117 100644 --- a/apps/dashboard/app/(authenticated)/productivity/employees/page.tsx +++ b/apps/dashboard/app/(authenticated)/productivity/employees/page.tsx @@ -14,6 +14,9 @@ export default function EmployeesPage() { pageTitle="Employees" routeKey={EMPLOYEE_ROUTES.INDEX} + searchable + searchPlaceholder="Search employees..." + statusFilter={{ statuses: ["active", "inactive"] }} getClient={(api) => api.employees} onRowClick={(row) => router.push(`/productivity/employees/${(row as any).id}`)} headerProps={({ selectedItem, invalidateQuery }) => ({ diff --git a/apps/dashboard/app/(authenticated)/productivity/holidays/page.tsx b/apps/dashboard/app/(authenticated)/productivity/holidays/page.tsx index 398a769..6594c85 100644 --- a/apps/dashboard/app/(authenticated)/productivity/holidays/page.tsx +++ b/apps/dashboard/app/(authenticated)/productivity/holidays/page.tsx @@ -12,6 +12,8 @@ export default function HolidayYearsPage() { pageTitle="Holiday Years" routeKey={HOLIDAY_YEAR_ROUTES.INDEX} + searchable + searchPlaceholder="Search holidays..." getClient={(api) => api.holidayYears} headerProps={({ selectedItem, invalidateQuery }) => ({ actions: ( diff --git a/apps/dashboard/app/(authenticated)/productivity/shop-calendars/page.tsx b/apps/dashboard/app/(authenticated)/productivity/shop-calendars/page.tsx index 8742647..22dafea 100644 --- a/apps/dashboard/app/(authenticated)/productivity/shop-calendars/page.tsx +++ b/apps/dashboard/app/(authenticated)/productivity/shop-calendars/page.tsx @@ -13,6 +13,8 @@ export default function ShopCalendarsPage() { pageTitle="Shop Calendars" routeKey={SHOP_CALENDAR_ROUTES.INDEX} + searchable + searchPlaceholder="Search shop calendars..." getClient={(api) => api.shopCalendars} headerProps={({ selectedItem, invalidateQuery }) => ({ actions: ( diff --git a/apps/dashboard/app/(authenticated)/productivity/shop-timings/page.tsx b/apps/dashboard/app/(authenticated)/productivity/shop-timings/page.tsx index ca97435..0962d6e 100644 --- a/apps/dashboard/app/(authenticated)/productivity/shop-timings/page.tsx +++ b/apps/dashboard/app/(authenticated)/productivity/shop-timings/page.tsx @@ -13,6 +13,8 @@ export default function ShopTimingsPage() { pageTitle="Shop Timings" routeKey={SHOP_TIMING_ROUTES.INDEX} + searchable + searchPlaceholder="Search shop timings..." getClient={(api) => api.shopTimings} headerProps={({ selectedItem, invalidateQuery }) => ({ actions: ( diff --git a/apps/dashboard/app/(authenticated)/productivity/tasks/page.tsx b/apps/dashboard/app/(authenticated)/productivity/tasks/page.tsx index 829d4a1..05a694f 100644 --- a/apps/dashboard/app/(authenticated)/productivity/tasks/page.tsx +++ b/apps/dashboard/app/(authenticated)/productivity/tasks/page.tsx @@ -4,13 +4,16 @@ import { ResourcePage } from "@/shared/data-view/resource-page" import { ColumnHeader } from "@/shared/data-view/table-view" import FormDialog from "@/shared/components/form-dialog" import { TaskForm } from "@/modules/tasks/task-form" -import { TASK_ROUTES } from "@garage/api" +import { TASK_ROUTES, TaskStatus } from "@garage/api" import type { TasksClient } from "@garage/api" export default function TasksPage() { return ( routeKey={TASK_ROUTES.INDEX} + searchable + searchPlaceholder="Search tasks..." + statusFilter={{ statuses: TaskStatus }} getClient={(api) => api.tasks} headerProps={({ selectedItem, invalidateQuery }) => ({ title: "Tasks", diff --git a/apps/dashboard/app/(authenticated)/purchase/bill/[id]/layout.tsx b/apps/dashboard/app/(authenticated)/purchase/bill/[id]/layout.tsx index 035551c..4455b05 100644 --- a/apps/dashboard/app/(authenticated)/purchase/bill/[id]/layout.tsx +++ b/apps/dashboard/app/(authenticated)/purchase/bill/[id]/layout.tsx @@ -3,6 +3,7 @@ import { getServerApi } from '@garage/api/server' import { BillActions } from '@/modules/bills/bill-actions' import { BillProvider, type BillResponse } from '@/modules/bills/bill-context' import BillStatusBadge from '@/modules/bills/bill-status-badge' +import { ShareDocumentButton } from '@/shared/components/share-document-button' import { ReceiptIcon } from 'lucide-react' import React from 'react' @@ -26,6 +27,7 @@ export default async function BillDetailLayout(props: { actions={
+
} diff --git a/apps/dashboard/app/(authenticated)/purchase/bill/page.tsx b/apps/dashboard/app/(authenticated)/purchase/bill/page.tsx index 38ce5c3..cec70c0 100644 --- a/apps/dashboard/app/(authenticated)/purchase/bill/page.tsx +++ b/apps/dashboard/app/(authenticated)/purchase/bill/page.tsx @@ -6,9 +6,13 @@ import { Badge } from "@/shared/components/ui/badge" import { ResourcePage } from "@/shared/data-view/resource-page" import { ColumnHeader } from "@/shared/data-view/table-view" import { BillForm } from "@/modules/bills/bill-form" -import { BILL_ROUTES } from "@garage/api" +import { BILL_ROUTES, BillStatus } from "@garage/api" import type { BillsClient } from "@garage/api" import { formatDate } from "@/shared/utils/formatters" +import { getFullName } from "@/shared/utils/getFullName" +import { Money } from "@/shared/components/money" +import { RelationLink } from "@/shared/components/relation-link" +import { Building2 } from "lucide-react" export default function BillsPage() { const router = useRouter() @@ -17,6 +21,9 @@ export default function BillsPage() { pageTitle="Bills" routeKey={BILL_ROUTES.INDEX} + searchable + searchPlaceholder="Search bills..." + statusFilter={{ statuses: BillStatus }} getClient={(api) => api.bills} onRowClick={(row) => router.push(`/purchase/bill/${(row as any).id}`)} headerProps={({ selectedItem, invalidateQuery }) => ({ @@ -41,12 +48,18 @@ export default function BillsPage() { { accessorKey: "total", header: ({ column }) => , - cell: ({ row }) => (row.original as any).total || "—", + cell: ({ row }) => { + const v = (row.original as any).total + return v != null && v !== "" ? : "—" + }, }, { accessorKey: "balance_due", header: ({ column }) => , - cell: ({ row }) => (row.original as any).balance_due || "—", + cell: ({ row }) => { + const v = (row.original as any).balance_due + return v != null && v !== "" ? : "—" + }, }, { accessorKey: "title", @@ -55,7 +68,16 @@ export default function BillsPage() { { accessorKey: "vendor", header: ({ column }) => , - cell: ({ row }) => (row.original as any).vendor?.name || "—", + cell: ({ row }) => { + const vendor = (row.original as any).vendor + return ( + + ) + }, }, { accessorKey: "bill_date", diff --git a/apps/dashboard/app/(authenticated)/purchase/expense/page.tsx b/apps/dashboard/app/(authenticated)/purchase/expense/page.tsx index 97b1783..e9d518a 100644 --- a/apps/dashboard/app/(authenticated)/purchase/expense/page.tsx +++ b/apps/dashboard/app/(authenticated)/purchase/expense/page.tsx @@ -5,10 +5,14 @@ import { ColumnHeader } from "@/shared/data-view/table-view" import FormDialog from "@/shared/components/form-dialog" import { ExpenseForm } from "@/modules/expenses/expense-form" import { Badge } from "@/shared/components/ui/badge" -import { EXPENSE_ROUTES } from "@garage/api" +import { EXPENSE_ROUTES, ExpenseStatus } from "@garage/api" import type { ExpensesClient } from "@garage/api" import { useRouter } from "next/navigation" -import { formatCurrency, formatDate, formatEnum } from "@/shared/utils/formatters" +import { formatDate, formatEnum } from "@/shared/utils/formatters" +import { Money } from "@/shared/components/money" +import { RelationLink } from "@/shared/components/relation-link" +import { Building2 } from "lucide-react" +import { getFullName } from "@/shared/utils/getFullName" export default function ExpensesPage() { const router = useRouter() @@ -16,6 +20,9 @@ export default function ExpensesPage() { pageTitle="Expenses" routeKey={EXPENSE_ROUTES.INDEX} + searchable + searchPlaceholder="Search expenses..." + statusFilter={{ statuses: ExpenseStatus }} getClient={(api) => api.expenses} onRowClick={(row)=>router.push(`/purchase/expense/${row.id}`)} headerProps={({ selectedItem, invalidateQuery }) => ({ @@ -49,7 +56,13 @@ export default function ExpensesPage() { header: () => "Vendor", cell: ({ row }) => { const vendor = (row.original as any).vendor - return vendor?.company_name || vendor?.name || "—" + return ( + + ) }, }, { @@ -60,12 +73,12 @@ export default function ExpensesPage() { { accessorKey: "total", header: () => "Total", - cell: ({ row }) => formatCurrency((row.original as any).total ?? 0), + cell: ({ row }) => , }, { accessorKey: "balance_due", header: () => "Balance Due", - cell: ({ row }) => formatCurrency((row.original as any).balance_due ?? 0), + cell: ({ row }) => , }, { accessorKey: "status", diff --git a/apps/dashboard/app/(authenticated)/purchase/payments-made/page.tsx b/apps/dashboard/app/(authenticated)/purchase/payments-made/page.tsx index 0adf019..34b203e 100644 --- a/apps/dashboard/app/(authenticated)/purchase/payments-made/page.tsx +++ b/apps/dashboard/app/(authenticated)/purchase/payments-made/page.tsx @@ -32,8 +32,11 @@ import { } from "@/shared/components/ui/dialog" import { confirm } from "@/shared/components/confirm-dialog" import { useAuthApi } from "@/shared/useApi" +import { getFullName } from "@/shared/utils/getFullName" import { PAYMENT_MADE_ROUTES } from "@garage/api" import type { PaymentMadesClient } from "@garage/api" +import { RelationLink } from "@/shared/components/relation-link" +import { Building2 } from "lucide-react" // ── Attachment helpers ── @@ -208,10 +211,13 @@ function AttachmentsDialog({ type PaymentMadeItem = { id: number payment_number?: string + vendor?: { id?: number | string; company_name?: string | null; first_name?: string | null; last_name?: string | null; name?: string | null } | null vendor_name?: string + employee?: { id?: number | string; first_name?: string | null; last_name?: string | null; name?: string | null } | null employee_name?: string payment_for?: string payment_made?: string | number + payment_mode?: { name?: string | null; title?: string | null } | null payment_mode_name?: string payment_date?: string paid_through?: string @@ -230,14 +236,20 @@ export default function PaymentsMadePage() { pageTitle="Payments Made" routeKey={PAYMENT_MADE_ROUTES.INDEX} + searchable + searchPlaceholder="Search payments..." getClient={(api) => api.paymentMades} - headerProps={({ invalidateQuery }) => ({ + headerProps={({ selectedItem, invalidateQuery }) => ({ actions: ( - {(resourceId) => ( + {(resourceId, { close }) => ( { + invalidateQuery() + close() + }} /> )} @@ -262,11 +274,27 @@ export default function PaymentsMadePage() { header: ({ column }) => , cell: ({ row }) => { const item = row.original as unknown as PaymentMadeItem + const isVendor = + item.vendor?.company_name || + getFullName(item.vendor) || + item.vendor?.name || + item.vendor_name + const label = + isVendor || + getFullName(item.employee) || + item.employee?.name || + item.employee_name + const href = isVendor && item.vendor?.id + ? `/purchase/vendor/${item.vendor.id}` + : item.employee?.id + ? `/productivity/employees/${item.employee.id}` + : null return ( -
- - {item.vendor_name || "—"} -
+ ) }, }, @@ -309,10 +337,15 @@ export default function PaymentsMadePage() { header: ({ column }) => , cell: ({ row }) => { const item = row.original as unknown as PaymentMadeItem + const label = + item.payment_mode?.name || + item.payment_mode?.title || + item.payment_mode_name || + "—" return (
- {item.payment_mode_name || "—"} + {label}
) }, diff --git a/apps/dashboard/app/(authenticated)/purchase/purchase-order/[id]/layout.tsx b/apps/dashboard/app/(authenticated)/purchase/purchase-order/[id]/layout.tsx index 8ce7e11..1137693 100644 --- a/apps/dashboard/app/(authenticated)/purchase/purchase-order/[id]/layout.tsx +++ b/apps/dashboard/app/(authenticated)/purchase/purchase-order/[id]/layout.tsx @@ -3,6 +3,7 @@ import { getServerApi } from '@garage/api/server' import { PurchaseOrderActions } from '@/modules/purchase-orders/purchase-order-actions' import { PurchaseOrderProvider } from '@/modules/purchase-orders/purchase-order-context' import { CreateBillFromPOButton } from '@/modules/purchase-orders/create-bill-from-po-button' +import { ShareDocumentButton } from '@/shared/components/share-document-button' import { ClipboardList } from 'lucide-react' import React from 'react' @@ -30,6 +31,7 @@ export default async function layout(props: { actions={
+
} diff --git a/apps/dashboard/app/(authenticated)/purchase/purchase-order/page.tsx b/apps/dashboard/app/(authenticated)/purchase/purchase-order/page.tsx index 9f11930..ba89299 100644 --- a/apps/dashboard/app/(authenticated)/purchase/purchase-order/page.tsx +++ b/apps/dashboard/app/(authenticated)/purchase/purchase-order/page.tsx @@ -7,6 +7,9 @@ import FormDialog from "@/shared/components/form-dialog" import { PurchaseOrderForm } from "@/modules/purchase-orders/purchase-order-form" import { PURCHASE_ORDER_ROUTES } from "@garage/api" import type { PurchaseOrdersClient } from "@garage/api" +import { RelationLink } from "@/shared/components/relation-link" +import { Building2 } from "lucide-react" +import { getFullName } from "@/shared/utils/getFullName" export default function PurchaseOrdersPage() { const router = useRouter() @@ -15,6 +18,8 @@ export default function PurchaseOrdersPage() { pageTitle="Purchase Orders" routeKey={PURCHASE_ORDER_ROUTES.INDEX} + searchable + searchPlaceholder="Search purchase orders..." getClient={(api) => api.purchaseOrders} onRowClick={(row) => router.push(`/purchase/purchase-order/${(row as any).id}`)} headerProps={({ selectedItem, invalidateQuery }) => ({ @@ -43,7 +48,16 @@ export default function PurchaseOrdersPage() { { accessorKey: "vendor_name", header: ({ column }) => , - cell: ({ row }) => (row.original as any).vendor_name || "—", + cell: ({ row }) => { + const item = row.original as any + return ( + + ) + }, }, { accessorKey: "order_date", diff --git a/apps/dashboard/app/(authenticated)/purchase/vendor-credit/page.tsx b/apps/dashboard/app/(authenticated)/purchase/vendor-credit/page.tsx index 748594d..8165b1f 100644 --- a/apps/dashboard/app/(authenticated)/purchase/vendor-credit/page.tsx +++ b/apps/dashboard/app/(authenticated)/purchase/vendor-credit/page.tsx @@ -5,14 +5,20 @@ import { Badge } from "@/shared/components/ui/badge" import { ResourcePage } from "@/shared/data-view/resource-page" import { ColumnHeader } from "@/shared/data-view/table-view" import { VendorCreditForm } from "@/modules/vendor-credits/vendor-credit-form" -import { VENDOR_CREDIT_ROUTES } from "@garage/api" +import { VENDOR_CREDIT_ROUTES, VendorCreditStatus } from "@garage/api" import type { VendorCreditsClient } from "@garage/api" +import { RelationLink } from "@/shared/components/relation-link" +import { Building2, FileTextIcon } from "lucide-react" +import { getFullName } from "@/shared/utils/getFullName" export default function VendorCreditsPage() { return ( pageTitle="Vendor Credits" routeKey={VENDOR_CREDIT_ROUTES.INDEX} + searchable + searchPlaceholder="Search vendor credits..." + statusFilter={{ statuses: VendorCreditStatus }} getClient={(api) => api.vendorCredits} headerProps={({ selectedItem, invalidateQuery }) => ({ actions: ( @@ -35,12 +41,30 @@ export default function VendorCreditsPage() { { accessorKey: "vendor_name", header: ({ column }) => , - cell: ({ row }) => (row.original as any).vendor_name || "—", + cell: ({ row }) => { + const item = row.original as any + return ( + + ) + }, }, { accessorKey: "bill_number", header: ({ column }) => , - cell: ({ row }) => (row.original as any).bill_number || "—", + cell: ({ row }) => { + const item = row.original as any + return ( + + ) + }, }, { accessorKey: "vendor_credit_date", diff --git a/apps/dashboard/app/(authenticated)/purchase/vendor/[id]/layout.tsx b/apps/dashboard/app/(authenticated)/purchase/vendor/[id]/layout.tsx new file mode 100644 index 0000000..29bac15 --- /dev/null +++ b/apps/dashboard/app/(authenticated)/purchase/vendor/[id]/layout.tsx @@ -0,0 +1,37 @@ +import { DashboardDetailsPage } from "@/base/components/layout/dashboard" +import { getServerApi } from "@garage/api/server" +import { VendorActions } from "@/modules/vendors/vendor-actions" +import { VendorProvider } from "@/modules/vendors/vendor-context" +import React from "react" + +export default async function layout(props: { + params: Promise<{ id: string }> + children: React.ReactNode +}) { + const { id } = await props.params + const api = await getServerApi() + const response = (await api.vendors.getById(id)) as { data?: Record } + const vendor = response?.data + + const company = vendor?.company_name as string | undefined + const fullName = [vendor?.first_name, vendor?.last_name].filter(Boolean).join(" ").trim() + const title = company || fullName || "Vendor Details" + const isActive = vendor?.is_active ?? vendor?.status === "active" + + return ( + + } + tabs={[ + { href: `/purchase/vendor/${id}`, label: "Details" }, + ]} + > + {props.children} + + + ) +} diff --git a/apps/dashboard/app/(authenticated)/purchase/vendor/[id]/page.tsx b/apps/dashboard/app/(authenticated)/purchase/vendor/[id]/page.tsx new file mode 100644 index 0000000..f8739ee --- /dev/null +++ b/apps/dashboard/app/(authenticated)/purchase/vendor/[id]/page.tsx @@ -0,0 +1,17 @@ +import { getServerApi } from "@garage/api/server" +import { VendorGeneralInfo } from "@/modules/vendors/vendor-general-info" + +export default async function VendorDetailPage(props: { params: Promise<{ id: string }> }) { + const { id } = await props.params + const api = await getServerApi() + // The OpenAPI schema doesn't yet describe GET /api/vendors/{id} (only PUT/DELETE), + // so cast through `any` here until the schema is regenerated. + const response = (await api.vendors.getById(id)) as { data?: Record } + const vendor = response?.data + + if (!vendor) { + return
Vendor not found.
+ } + + return +} diff --git a/apps/dashboard/app/(authenticated)/purchase/vendor/page.tsx b/apps/dashboard/app/(authenticated)/purchase/vendor/page.tsx index 5d9bda3..33f031a 100644 --- a/apps/dashboard/app/(authenticated)/purchase/vendor/page.tsx +++ b/apps/dashboard/app/(authenticated)/purchase/vendor/page.tsx @@ -1,18 +1,28 @@ "use client" +import { useRouter } from "next/navigation" +import { toast } from "sonner" +import { Power } from "lucide-react" import { ResourcePage } from "@/shared/data-view/resource-page" import { ColumnHeader } from "@/shared/data-view/table-view" import FormDialog from "@/shared/components/form-dialog" +import { Badge } from "@/shared/components/ui/badge" import { VendorForm } from "@/modules/vendors/vendor-form" +import { useAuthApi } from "@/shared/useApi" import { VENDOR_ROUTES } from "@garage/api" import type { VendorsClient } from "@garage/api" export default function VendorsPage() { + const router = useRouter() + const api = useAuthApi() return ( pageTitle="Vendors" routeKey={VENDOR_ROUTES.INDEX} + searchable + searchPlaceholder="Search vendors..." getClient={(api) => api.vendors} + onRowClick={(row) => router.push(`/purchase/vendor/${(row as any).id}`)} headerProps={({ selectedItem, invalidateQuery }) => ({ actions: ( @@ -46,6 +56,18 @@ export default function VendorsPage() { header: ({ column }) => , cell: ({ row }) => (row.original as any).email || "—", }, + { + accessorKey: "is_active", + header: ({ column }) => , + cell: ({ row }) => { + const isActive = Boolean((row.original as any).is_active) + return ( + + {isActive ? "Active" : "Inactive"} + + ) + }, + }, { accessorKey: "created_at", header: ({ column }) => , @@ -54,7 +76,26 @@ export default function VendorsPage() { return val ? new Date(val).toLocaleDateString() : "—" }, }, - actionsColumn(), + actionsColumn({ + extraItems: (row) => { + const isActive = Boolean((row as any).is_active) + return [ + { + label: isActive ? "Deactivate" : "Activate", + icon: Power, + onClick: async () => { + try { + await api.vendors.toggleStatus({ id: Number((row as any).id) } as any) + toast.success(isActive ? "Vendor deactivated." : "Vendor activated.") + router.refresh() + } catch { + toast.error("Failed to update vendor status.") + } + }, + }, + ] + }, + }), ]} /> ) diff --git a/apps/dashboard/app/(authenticated)/sales/credit-notes/page.tsx b/apps/dashboard/app/(authenticated)/sales/credit-notes/page.tsx index e874c36..22a8611 100644 --- a/apps/dashboard/app/(authenticated)/sales/credit-notes/page.tsx +++ b/apps/dashboard/app/(authenticated)/sales/credit-notes/page.tsx @@ -5,7 +5,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 { CreditNoteForm } from "@/modules/credit-notes/credit-note-form" -import { CREDIT_NOTE_ROUTES } from "@garage/api" +import { CREDIT_NOTE_ROUTES, CreditNoteStatus } from "@garage/api" import type { CreditNotesClient } from "@garage/api" type CreditNoteItem = { @@ -25,6 +25,9 @@ export default function CreditNotesPage() { pageTitle="Credit Notes" routeKey={CREDIT_NOTE_ROUTES.INDEX} + searchable + searchPlaceholder="Search credit notes..." + statusFilter={{ statuses: CreditNoteStatus }} getClient={(api) => api.creditNotes} onRowClick={(row) => router.push(`/sales/credit-notes/${(row as any).id}`)} headerProps={({ selectedItem, invalidateQuery }) => ({ diff --git a/apps/dashboard/app/(authenticated)/sales/customers/[id]/vehicles/page.tsx b/apps/dashboard/app/(authenticated)/sales/customers/[id]/vehicles/page.tsx index f062b5a..c352a05 100644 --- a/apps/dashboard/app/(authenticated)/sales/customers/[id]/vehicles/page.tsx +++ b/apps/dashboard/app/(authenticated)/sales/customers/[id]/vehicles/page.tsx @@ -32,6 +32,8 @@ export default function CustomerVehiclesPage({ params }: { params: Promise<{ id: )} pageTitle="Customer Vehicles" routeKey={VEHICLE_ROUTES.INDEX} + searchable + searchPlaceholder="Search vehicles..." getClient={(api) => api.vehicles} extraParams={{ customer_id: customerId }} header={null} diff --git a/apps/dashboard/app/(authenticated)/sales/customers/page.tsx b/apps/dashboard/app/(authenticated)/sales/customers/page.tsx index 4c5dc21..0a8d8b2 100644 --- a/apps/dashboard/app/(authenticated)/sales/customers/page.tsx +++ b/apps/dashboard/app/(authenticated)/sales/customers/page.tsx @@ -6,7 +6,6 @@ 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 { DownloadSampleButton } from '@/shared/components/download-sample-button' import { useAuthApi } from '@/shared/useApi' import { CustomerForm } from '@/modules/customers/customer-form' import { CUSTOMER_ROUTES } from '@garage/api' @@ -20,18 +19,19 @@ export default function CustomersPage() { pageTitle='Customers' routeKey={CUSTOMER_ROUTES.INDEX} + searchable + searchPlaceholder="Search customers..." getClient={(api) => api.customers} onRowClick={(row) => router.push(`/sales/customers/${(row as any).id}`)} headerProps={({ selectedItem, invalidateQuery }) => ({ actions: (
- api.customers.downloadImportSample()} - fileName='customers-import-sample' - /> api.customers.importData(file)} onSuccess={invalidateQuery} + entityLabel="Customers" + onDownloadSample={() => api.customers.downloadImportSample()} + sampleFileName='customers-import-sample' /> api.customers.exportData(filters)} diff --git a/apps/dashboard/app/(authenticated)/sales/estimates/[id]/layout.tsx b/apps/dashboard/app/(authenticated)/sales/estimates/[id]/layout.tsx index 757c18e..470b81c 100644 --- a/apps/dashboard/app/(authenticated)/sales/estimates/[id]/layout.tsx +++ b/apps/dashboard/app/(authenticated)/sales/estimates/[id]/layout.tsx @@ -4,6 +4,7 @@ import { EstimateActions } from '@/modules/estimates/estimate-actions' import { EstimateProvider } from '@/modules/estimates/estimate-context' import { CreateInvoiceFromEstimateButton } from '@/modules/estimates/create-invoice-from-estimate-button' import { CreateJobCardFromEstimateButton } from '@/modules/estimates/create-job-card-from-estimate-button' +import { ShareDocumentButton } from '@/shared/components/share-document-button' import { FileTextIcon } from 'lucide-react' import React from 'react' import { formatDate } from '@/shared/utils/formatters' @@ -40,6 +41,7 @@ export default async function layout(props: {
+
} diff --git a/apps/dashboard/app/(authenticated)/sales/estimates/page.tsx b/apps/dashboard/app/(authenticated)/sales/estimates/page.tsx index ab2504f..d1d04b8 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' @@ -7,18 +8,22 @@ import { EstimateForm } from '@/modules/estimates/estimate-form' import { ESTIMATE_ROUTES } from '@garage/api' import type { EstimatesClient } from '@garage/api' import { Car, FileTextIcon, UserIcon } from 'lucide-react' -import { Button } from '@/shared/components/ui/button' import Link from 'next/link' import { formatDate } from '@/shared/utils/formatters' import { getVehicleLabel } from '@/modules/vehicles/utils/getVehicleLabel' import { getFullName } from '@/shared/utils/getFullName' +import { RelationLink } from '@/shared/components/relation-link' export default function EstimatesPage() { + const router = useRouter() return ( pageTitle="Estimates" routeKey={ESTIMATE_ROUTES.INDEX} + searchable + searchPlaceholder="Search estimates..." getClient={(api) => api.estimates} + onRowClick={(row) => router.push(`/sales/estimates/${(row as any).id}`)} headerProps={({ selectedItem, invalidateQuery }) => ({ actions: ( @@ -39,7 +44,7 @@ export default function EstimatesPage() { const item = row.original return (
- + e.stopPropagation()}> {item.title} @@ -55,26 +60,30 @@ export default function EstimatesPage() { accessorKey: "customer_name", header: ({ column }) => , cell: ({ row }) => { - const item:any = row.original + const item: any = row.original return ( -
- - {getFullName(item.customer) || "—"} -
+ ) - } + }, }, { accessorKey: "vehicle", header: ({ column }) => , cell: ({ row }) => { - const item :any= row.original - return - } + const item: any = row.original + return ( + + ) + }, }, { accessorKey: "date", diff --git a/apps/dashboard/app/(authenticated)/sales/inspections/[id]/checkpoints/page.tsx b/apps/dashboard/app/(authenticated)/sales/inspections/[id]/checkpoints/page.tsx index 211739a..ddac344 100644 --- a/apps/dashboard/app/(authenticated)/sales/inspections/[id]/checkpoints/page.tsx +++ b/apps/dashboard/app/(authenticated)/sales/inspections/[id]/checkpoints/page.tsx @@ -1,589 +1,458 @@ "use client" -import { use, useState, useRef, useCallback } from "react" -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" -import { useAuthApi } from "@/shared/useApi" -import { DataTable, ColumnHeader } from "@/shared/data-view/table-view" -import DashboardPage from "@/base/components/layout/dashboard/dashboard-page" -import { Badge } from "@/shared/components/ui/badge" -import { Button } from "@/shared/components/ui/button" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/shared/components/ui/dropdown-menu" -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from "@/shared/components/ui/dialog" -import { Input } from "@/shared/components/ui/input" -import { Label } from "@/shared/components/ui/label" -import { Textarea } from "@/shared/components/ui/textarea" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/shared/components/ui/select" +import { useCallback, useEffect, useMemo, useState } from "react" +import Link from "next/link" +import { useParams } from "next/navigation" import { toast } from "sonner" import { - Plus, - Ellipsis, - Pencil, - Trash2, - CheckCircle2, AlertTriangle, - XCircle, - MinusCircle, - CircleDot, - Paperclip, - FileUp, - FileText, - FileImage, - File, - X, + Car, + ChevronLeft, + ChevronRight, + CircleCheck, + FileSignature, + Gauge, + Image as ImageIcon, + Loader2, + Share2, + StickyNote, + User, } from "lucide-react" -import { INSPECTION_ROUTES } from "@garage/api" -// ── Types ── +import { Button } from "@/shared/components/ui/button" +import { Badge } from "@/shared/components/ui/badge" +import { + Card, + CardAction, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/shared/components/ui/card" +import { Progress } from "@/shared/components/ui/progress" +import { Separator } from "@/shared/components/ui/separator" +import { Tabs, TabsList, TabsTrigger } from "@/shared/components/ui/tabs" +import { useAuthApi } from "@/shared/useApi" +import { + CheckpointFillDialog, + isCheckpointComplete, + type Checkpoint, + type Severity, +} from "@/modules/inspections/checkpoint-fill-dialog" +import { SignaturePad } from "@/modules/inspections/signature-pad" +import { InspectionShareDialog } from "@/modules/inspections/inspection-share-dialog" -type CheckpointItem = { +type Inspection = { id: number - inspection_id?: number - name?: string - description?: string - record_type?: string - condition_rate?: number - file?: string - status?: string - created_at?: string - updated_at?: string + title: string + order_number?: string | null + status?: string | null + odometer?: number | null + template?: { id: number; name: string } | null + customer?: { first_name?: string; last_name?: string } | null + vehicle?: { make?: string; model?: string; year?: string | number; license_plate?: string } | null + check_points: Checkpoint[] + technician_signature_url?: string | null + customer_signature_url?: string | null } -// ── Constants ── - -const CHECKPOINT_STATUSES = [ - { value: "passed", label: "Passed", icon: CheckCircle2, color: "bg-green-100 text-green-800" }, - { value: "need_attention", label: "Need Attention", icon: AlertTriangle, color: "bg-yellow-100 text-yellow-800" }, - { value: "failed", label: "Failed", icon: XCircle, color: "bg-red-100 text-red-800" }, - { value: "not_applicable", label: "Not Applicable", icon: MinusCircle, color: "bg-gray-100 text-gray-800" }, - { value: "not_inspected", label: "Not Inspected", icon: CircleDot, color: "bg-blue-100 text-blue-800" }, -] as const - -const RECORD_TYPES = [ - { value: "record_conditions", label: "Record Conditions" }, - { value: "record_audio", label: "Record Audio" }, - { value: "record_video", label: "Record Video" }, - { value: "capture_photo", label: "Capture Photo" }, -] as const - -function formatStatus(status?: string) { - if (!status) return "Not Inspected" - return status.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()) +const SEVERITY_DOT: Record = { + good: "bg-emerald-500 shadow-emerald-500/30", + attention: "bg-amber-500 shadow-amber-500/30", + critical: "bg-rose-500 shadow-rose-500/30", + na: "bg-slate-400 shadow-slate-400/30", + not_inspected: "bg-muted shadow-none", +} +const SEVERITY_LABEL: Record = { + good: "Good", + attention: "Attention", + critical: "Critical", + na: "N/A", + not_inspected: "Not inspected", +} +const SEVERITY_BADGE: Record = { + good: "default", + attention: "secondary", + critical: "destructive", + na: "outline", + not_inspected: "outline", } -function getStatusConfig(status?: string) { - return CHECKPOINT_STATUSES.find((s) => s.value === status) || CHECKPOINT_STATUSES[4] -} - -// ── Checkpoint Form Dialog ── - -function CheckpointFormDialog({ - open, - onOpenChange, - inspectionId, - checkpoint, - onSuccess, -}: { - open: boolean - onOpenChange: (open: boolean) => void - inspectionId: string - checkpoint?: CheckpointItem | null - onSuccess: () => void -}) { - const api = useAuthApi() - const [name, setName] = useState(checkpoint?.name ?? "") - const [description, setDescription] = useState(checkpoint?.description ?? "") - const [recordType, setRecordType] = useState(checkpoint?.record_type ?? "record_conditions") - const [isPending, setIsPending] = useState(false) - - const isEditing = !!checkpoint - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - if (!name.trim()) { - toast.error("Name is required") - return - } - setIsPending(true) - try { - const payload = { - inspection_id: Number(inspectionId), - name: name.trim(), - description: description.trim() || undefined, - record_type: recordType, - } - - if (isEditing) { - const promise = api.inspections.updateCheckpoint(String(checkpoint.id), payload) - toast.promise(promise, { - loading: "Updating checkpoint...", - success: "Checkpoint updated", - error: "Failed to update checkpoint", - }) - await promise - } else { - const promise = api.inspections.createCheckpoint(payload) - toast.promise(promise, { - loading: "Creating checkpoint...", - success: "Checkpoint created", - error: "Failed to create checkpoint", - }) - await promise - } - onSuccess() - onOpenChange(false) - } finally { - setIsPending(false) - } +function groupBySection(cps: Checkpoint[]) { + const map = new Map() + for (const cp of cps) { + const key = cp.section_name || "Other" + if (!map.has(key)) map.set(key, []) + map.get(key)!.push(cp) } - - return ( - - - - {isEditing ? "Edit Checkpoint" : "Add Checkpoint"} - -
-
- - setName(e.target.value)} - required - /> -
-
- -