feat: add template checkpoint edit dialog and vendor management components
- Implemented TemplateCheckpointEditDialog for creating and editing inspection checkpoints. - Added VendorActions component for managing vendor actions including edit, activate/deactivate, and delete. - Created VendorContext for managing vendor state across components. - Developed VendorGeneralInfo component to display detailed vendor information. - Introduced AedSymbol and Money components for consistent currency representation. - Added PromptDialog for user input prompts throughout the application. - Implemented RelationLink component for unified related-data display in CRUD tables. - Created InspectionTemplatesClient for API interactions related to inspection templates.
This commit is contained in:
@@ -7,7 +7,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 { 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, ClockIcon } from "lucide-react"
|
||||
import { Badge } from "@/shared/components/ui/badge"
|
||||
@@ -69,6 +69,9 @@ export default function JobCardAppointmentsPage({
|
||||
return (
|
||||
<ResourcePage<AppointmentsClient>
|
||||
routeKey={APPOINTMENT_ROUTES.INDEX}
|
||||
searchable
|
||||
searchPlaceholder="Search appointments..."
|
||||
statusFilter={{ statuses: AppointmentStatus }}
|
||||
getClient={(api) => api.appointments}
|
||||
extraParams={{ job_card_id: jobCardId }}
|
||||
header={null}
|
||||
|
||||
@@ -6,9 +6,12 @@ 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 { useJobCard } from "@/modules/job-cards/job-card-context"
|
||||
import { RelationLink } from "@/shared/components/relation-link"
|
||||
import { Building2, FileTextIcon } from "lucide-react"
|
||||
import { getFullName } from "@/shared/utils/getFullName"
|
||||
|
||||
export default function JobCardBillsPage({
|
||||
params,
|
||||
@@ -25,6 +28,9 @@ export default function JobCardBillsPage({
|
||||
return (
|
||||
<ResourcePage<BillsClient>
|
||||
routeKey={BILL_ROUTES.INDEX}
|
||||
searchable
|
||||
searchPlaceholder="Search bills..."
|
||||
statusFilter={{ statuses: BillStatus }}
|
||||
getClient={(api) => api.bills}
|
||||
extraParams={{ job_card_id: jobCardId }}
|
||||
header={null}
|
||||
@@ -46,7 +52,16 @@ export default function JobCardBillsPage({
|
||||
{
|
||||
accessorKey: "bill_number",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Bill #" />,
|
||||
cell: ({ row }) => (row.original as any).bill_number || "—",
|
||||
cell: ({ row }) => {
|
||||
const item = row.original as any
|
||||
return (
|
||||
<RelationLink
|
||||
href={item.id ? `/purchase/bill/${item.id}` : null}
|
||||
icon={FileTextIcon}
|
||||
label={item.bill_number}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "title",
|
||||
@@ -55,7 +70,16 @@ export default function JobCardBillsPage({
|
||||
{
|
||||
accessorKey: "vendor_name",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Vendor" />,
|
||||
cell: ({ row }) => (row.original as any).vendor_name || "—",
|
||||
cell: ({ row }) => {
|
||||
const item = row.original as any
|
||||
return (
|
||||
<RelationLink
|
||||
href={item.vendor?.id ? `/purchase/vendor/${item.vendor.id}` : null}
|
||||
icon={Building2}
|
||||
label={item.vendor?.company_name || getFullName(item.vendor) || item.vendor_name}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "bill_date",
|
||||
|
||||
@@ -4,10 +4,14 @@ import { ResourcePage } from "@/shared/data-view/resource-page"
|
||||
import { ColumnHeader } from "@/shared/data-view/table-view"
|
||||
import FormDialog from "@/shared/components/form-dialog"
|
||||
import { InspectionForm } from "@/modules/inspections/inspection-form"
|
||||
import { INSPECTION_ROUTES } from "@garage/api"
|
||||
import { INSPECTION_ROUTES, InspectionStatus } from "@garage/api"
|
||||
import type { InspectionsClient } from "@garage/api"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useJobCard } from "@/modules/job-cards/job-card-context"
|
||||
import { RelationLink } from "@/shared/components/relation-link"
|
||||
import { Car, UserIcon } from "lucide-react"
|
||||
import { getFullName } from "@/shared/utils/getFullName"
|
||||
import { getVehicleLabel } from "@/modules/vehicles/utils/getVehicleLabel"
|
||||
|
||||
export default function InspectionsPage() {
|
||||
const router = useRouter()
|
||||
@@ -17,6 +21,9 @@ export default function InspectionsPage() {
|
||||
pageTitle="Inspections"
|
||||
extraParams={{job_card_id: jobCard?.id}}
|
||||
routeKey={INSPECTION_ROUTES.INDEX}
|
||||
searchable
|
||||
searchPlaceholder="Search inspections..."
|
||||
statusFilter={{ statuses: InspectionStatus }}
|
||||
getClient={(api) => api.inspections}
|
||||
onRowClick={(row) => router.push(`/sales/inspections/${(row as any).id}`)}
|
||||
headerProps={({ selectedItem, invalidateQuery }) => ({
|
||||
@@ -42,7 +49,13 @@ export default function InspectionsPage() {
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Customer" />,
|
||||
cell: ({ row }) => {
|
||||
const c = (row.original as any).customer
|
||||
return c ? `${c.first_name ?? ""} ${c.last_name ?? ""}`.trim() : "—"
|
||||
return (
|
||||
<RelationLink
|
||||
href={c?.id ? `/sales/customers/${c.id}` : null}
|
||||
icon={UserIcon}
|
||||
label={getFullName(c)}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -50,7 +63,14 @@ export default function InspectionsPage() {
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Vehicle" />,
|
||||
cell: ({ row }) => {
|
||||
const v = (row.original as any).vehicle
|
||||
return v ? `${v.make ?? ""} ${v.model ?? ""}`.trim() : "—"
|
||||
return (
|
||||
<RelationLink
|
||||
href={v?.id ? `/sales/vehicles/${v.id}` : null}
|
||||
icon={Car}
|
||||
label={getVehicleLabel(v as any)}
|
||||
meta={v?.license_plate}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -8,6 +8,9 @@ import { PurchaseOrderForm } from "@/modules/purchase-orders/purchase-order-form
|
||||
import { PURCHASE_ORDER_ROUTES } from "@garage/api"
|
||||
import type { PurchaseOrdersClient } from "@garage/api"
|
||||
import { useJobCard } from "@/modules/job-cards/job-card-context"
|
||||
import { RelationLink } from "@/shared/components/relation-link"
|
||||
import { Building2, FileTextIcon } from "lucide-react"
|
||||
import { getFullName } from "@/shared/utils/getFullName"
|
||||
|
||||
export default function JobCardPurchaseOrdersPage({
|
||||
params,
|
||||
@@ -24,6 +27,8 @@ export default function JobCardPurchaseOrdersPage({
|
||||
return (
|
||||
<ResourcePage<PurchaseOrdersClient>
|
||||
routeKey={PURCHASE_ORDER_ROUTES.INDEX}
|
||||
searchable
|
||||
searchPlaceholder="Search purchase orders..."
|
||||
getClient={(api) => api.purchaseOrders}
|
||||
extraParams={{ job_card_id: jobCardId }}
|
||||
header={null}
|
||||
@@ -46,7 +51,16 @@ export default function JobCardPurchaseOrdersPage({
|
||||
{
|
||||
accessorKey: "order_number",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Order #" />,
|
||||
cell: ({ row }) => (row.original as any).order_number || "—",
|
||||
cell: ({ row }) => {
|
||||
const item = row.original as any
|
||||
return (
|
||||
<RelationLink
|
||||
href={item.id ? `/purchase/purchase-order/${item.id}` : null}
|
||||
icon={FileTextIcon}
|
||||
label={item.order_number}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "title",
|
||||
@@ -55,7 +69,16 @@ export default function JobCardPurchaseOrdersPage({
|
||||
{
|
||||
accessorKey: "vendor_name",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Vendor" />,
|
||||
cell: ({ row }) => (row.original as any).vendor_name || "—",
|
||||
cell: ({ row }) => {
|
||||
const item = row.original as any
|
||||
return (
|
||||
<RelationLink
|
||||
href={item.vendor?.id ? `/purchase/vendor/${item.vendor.id}` : null}
|
||||
icon={Building2}
|
||||
label={item.vendor?.company_name || getFullName(item.vendor) || item.vendor_name}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "order_date",
|
||||
|
||||
@@ -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 { 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"
|
||||
import { Badge } from "@/shared/components/ui/badge"
|
||||
import { useJobCard } from "@/modules/job-cards/job-card-context"
|
||||
@@ -26,6 +26,9 @@ export default function JobCardTasksPage({
|
||||
return (
|
||||
<ResourcePage<TasksClient>
|
||||
routeKey={TASK_ROUTES.INDEX}
|
||||
searchable
|
||||
searchPlaceholder="Search tasks..."
|
||||
statusFilter={{ statuses: TaskStatus }}
|
||||
getClient={(api) => api.tasks}
|
||||
extraParams={{ job_card_id: jobCardId }}
|
||||
header={null}
|
||||
|
||||
@@ -6,12 +6,9 @@ import FormDialog from '@/shared/components/form-dialog'
|
||||
import { JobCardForm } from '@/modules/job-cards/job-card-form'
|
||||
import { JOB_CARD_ROUTES, JobCardStatus } from '@garage/api'
|
||||
import type { JobCardsClient } from '@garage/api'
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/shared/components/ui/tabs'
|
||||
import { ClipboardListIcon, SearchIcon } from 'lucide-react'
|
||||
import { ClipboardListIcon } from 'lucide-react'
|
||||
import { Badge } from '@/shared/components/ui/badge'
|
||||
import { Input } from '@/shared/components/ui/input'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useState, useEffect, useMemo } from 'react'
|
||||
import { formatDate, formatEnum, formatNumber } from '@/shared/utils/formatters'
|
||||
import { useFilterParams } from '@/shared/hooks/use-filter-params'
|
||||
import { FilterDrawer, FilterTrigger } from '@/shared/components/filter-drawer'
|
||||
@@ -37,30 +34,17 @@ const statusColorMap: Record<string, string> = {
|
||||
|
||||
export default function JobCardsPage() {
|
||||
const router = useRouter()
|
||||
const [searchInput, setSearchInput] = useState("")
|
||||
const [search, setSearch] = useState("")
|
||||
const [statusFilter, setStatusFilter] = useState<string>("all")
|
||||
|
||||
const filter = useFilterParams(jobCardFilterConfig)
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setSearch(searchInput), 400)
|
||||
return () => clearTimeout(timer)
|
||||
}, [searchInput])
|
||||
|
||||
const extraParams = useMemo(() => {
|
||||
const params: Record<string, unknown> = { ...filter.appliedParams }
|
||||
if (search) params.search = search
|
||||
if (statusFilter !== "all") params.status = statusFilter
|
||||
return params
|
||||
}, [filter.appliedParams, search, statusFilter])
|
||||
|
||||
return (
|
||||
<>
|
||||
<ResourcePage<JobCardsClient>
|
||||
routeKey={JOB_CARD_ROUTES.INDEX}
|
||||
getClient={(api) => api.jobCards}
|
||||
extraParams={extraParams}
|
||||
searchable
|
||||
searchPlaceholder="Search job cards..."
|
||||
statusFilter={{ statuses: JobCardStatus }}
|
||||
extraParams={filter.appliedParams}
|
||||
onRowClick={(row) => router.push(`/sales/job-cards/${row.id}`)}
|
||||
headerProps={({ selectedItem, invalidateQuery }) => ({
|
||||
title: "Job Cards",
|
||||
@@ -135,30 +119,6 @@ export default function JobCardsPage() {
|
||||
},
|
||||
actionsColumn(),
|
||||
]}
|
||||
|
||||
tableHeader={
|
||||
<div className="flex justify-between">
|
||||
<Tabs value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<TabsList variant="line">
|
||||
<TabsTrigger value="all">All</TabsTrigger>
|
||||
{JobCardStatus.map((status) => (
|
||||
<TabsTrigger key={status} value={status}>
|
||||
{formatEnum(status)}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<div className="relative w-64">
|
||||
<SearchIcon className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search job cards..."
|
||||
value={searchInput}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
className="pl-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<FilterDrawer
|
||||
|
||||
Reference in New Issue
Block a user