Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Mohammad Khyata 2026-05-07 13:32:35 +03:00
parent 4faafe6667
commit c7eb23dd3f
17 changed files with 668 additions and 120 deletions

View File

@ -1,10 +1,11 @@
import { DashboardHeader } from "@/base/components/layout/dashboard"; import { DashboardHeader } from "@/base/components/layout/dashboard";
import DashboardPage from "@/base/components/layout/dashboard/dashboard-page"; import DashboardPage from "@/base/components/layout/dashboard/dashboard-page";
import { DashboardContent } from "@/modules/home/dashboard-content"; import { DashboardContent } from "@/modules/home/dashboard-content";
import { DashboardFiltersToolbar } from "@/modules/home/dashboard-filters-toolbar";
export default function page() { export default function page() {
return ( return (
<DashboardPage headerProps={{title: "Dashboard"}} > <DashboardPage headerProps={{ title: "Dashboard", actions: <DashboardFiltersToolbar /> }} >
<DashboardContent /> <DashboardContent />
</DashboardPage> </DashboardPage>
) )

View File

@ -4,7 +4,7 @@ import { useParams } from "next/navigation"
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { type ColumnDef } from "@tanstack/react-table" import { type ColumnDef } from "@tanstack/react-table"
import { useState } from "react" import { useState } from "react"
import { Plus, Trash2 } from "lucide-react" import { Download, ExternalLink, Plus, Trash2 } from "lucide-react"
import { toast } from "sonner" import { toast } from "sonner"
import { useAuthApi } from "@/shared/useApi" import { useAuthApi } from "@/shared/useApi"
@ -19,10 +19,27 @@ import {
DialogTitle, DialogTitle,
} from "@/shared/components/ui/dialog" } from "@/shared/components/ui/dialog"
import { VehicleDocumentForm } from "@/modules/vehicles/vehicle-document-form" import { VehicleDocumentForm } from "@/modules/vehicles/vehicle-document-form"
import { formatDate, formatDateTime } from "@/shared/utils/formatters"
import { getFullName } from "@/shared/utils/getFullName"
type VehicleDocument = { type VehicleDocument = {
id: number id: number
name: string document_number?: string
document_type_id?: number
document_type?: {
id?: number
name?: string
} | null
customer_id?: number
customer?: {
id?: number
first_name?: string
last_name?: string
name?: string
} | null
document_expire?: string
document_file?: string
document_file_url?: string | null
created_at: string created_at: string
updated_at: string updated_at: string
} }
@ -54,7 +71,7 @@ export default function VehicleDocumentsPage() {
const handleDelete = async (doc: VehicleDocument) => { const handleDelete = async (doc: VehicleDocument) => {
const confirmed = await confirm({ const confirmed = await confirm({
title: "Delete Document", title: "Delete Document",
description: `Are you sure you want to delete "${doc.name}"?`, description: `Are you sure you want to delete "${doc.document_number || `Document #${doc.id}`}"?`,
confirmLabel: "Delete", confirmLabel: "Delete",
variant: "destructive", variant: "destructive",
}) })
@ -65,16 +82,74 @@ export default function VehicleDocumentsPage() {
const columns: ColumnDef<VehicleDocument>[] = [ const columns: ColumnDef<VehicleDocument>[] = [
{ {
accessorKey: "name", accessorKey: "document_number",
header: ({ column }) => <ColumnHeader column={column} title="Name" />, header: ({ column }) => <ColumnHeader column={column} title="Document Number" />,
cell: ({ getValue, row }) => {
const number = getValue<string>()
const fallbackName = row.original.document_file?.split("/").pop()
return number || fallbackName || `Document #${row.original.id}`
},
},
{
accessorKey: "document_type_id",
header: ({ column }) => <ColumnHeader column={column} title="Document Type" />,
cell: ({ row, getValue }) => {
const typeName = row.original.document_type?.name
const value = getValue<number | undefined>()
return typeName || (value ? `#${value}` : "—")
},
},
{
accessorKey: "customer_id",
header: ({ column }) => <ColumnHeader column={column} title="Customer" />,
cell: ({ row, getValue }) => {
const name = getFullName(row.original.customer as any) || row.original.customer?.name
const value = getValue<number | undefined>()
return name || (value ? `#${value}` : "—")
},
},
{
accessorKey: "document_expire",
header: ({ column }) => <ColumnHeader column={column} title="Expiry Date" />,
cell: ({ getValue }) => formatDate(getValue<string | undefined>()),
},
{
id: "file",
header: ({ column }) => <ColumnHeader column={column} title="File" />,
cell: ({ row }) => {
const url = row.original.document_file_url
if (!url) return "No file"
return (
<div className="flex items-center gap-2" onClick={(e) => e.stopPropagation()}>
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 text-primary underline"
title="Open document"
>
<ExternalLink className="size-3.5" />
Open
</a>
<a
href={url}
download
className="inline-flex items-center gap-1 text-primary underline"
title="Download document"
>
<Download className="size-3.5" />
Download
</a>
</div>
)
},
enableSorting: false,
}, },
{ {
accessorKey: "created_at", accessorKey: "created_at",
header: ({ column }) => <ColumnHeader column={column} title="Uploaded At" />, header: ({ column }) => <ColumnHeader column={column} title="Uploaded At" />,
cell: ({ getValue }) => { cell: ({ getValue }) => formatDateTime(getValue<string | undefined>()),
const val = getValue<string>()
return val ? new Date(val).toLocaleDateString() : "—"
},
}, },
{ {
id: "actions", id: "actions",
@ -83,7 +158,10 @@ export default function VehicleDocumentsPage() {
<Button <Button
variant="ghost" variant="ghost"
size="icon-sm" size="icon-sm"
onClick={() => handleDelete(row.original)} onClick={(e) => {
e.stopPropagation()
handleDelete(row.original)
}}
title="Delete document" title="Delete document"
> >
<Trash2 className="size-4 text-destructive" /> <Trash2 className="size-4 text-destructive" />
@ -93,6 +171,15 @@ export default function VehicleDocumentsPage() {
}, },
] ]
const handleRowClick = (doc: VehicleDocument) => {
if (doc.document_file_url) {
window.open(doc.document_file_url, "_blank", "noopener,noreferrer")
return
}
toast.info("This document has no file to open.")
}
const documents = (data as any)?.data ?? [] const documents = (data as any)?.data ?? []
const meta = (data as any)?.meta const meta = (data as any)?.meta
@ -105,21 +192,25 @@ export default function VehicleDocumentsPage() {
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex items-center justify-end">
<Card>
<CardContent>
<DataTable
slots={{
actions: <div className="flex items-center justify-end">
<Button onClick={() => setDialogOpen(true)}> <Button onClick={() => setDialogOpen(true)}>
<Plus className="size-4" /> <Plus className="size-4" />
Upload Document Upload Document
</Button> </Button>
</div> </div>
}}
<Card>
<CardContent>
<DataTable
columns={columns} columns={columns}
data={documents} data={documents}
pagination={pagination} pagination={pagination}
sorting={[]} sorting={[]}
onChange={() => { }} onChange={() => { }}
onRowClick={handleRowClick}
isLoading={isLoading} isLoading={isLoading}
/> />
</CardContent> </CardContent>

View File

@ -13,7 +13,6 @@ export default async function layout(props: { params: Promise<{ id: string }>, c
return ( return (
<> <>
<DashboardDetailsPage <DashboardDetailsPage
className='p-0 lg:p-0'
avatarSrc={vehicle.data?.image_url || ""} avatarSrc={vehicle.data?.image_url || ""}
// avatarSrc={vehicle.data?.image_url || ""} // avatarSrc={vehicle.data?.image_url || ""}
title={title} title={title}

View File

@ -142,21 +142,16 @@ export default function VehicleMileagePage() {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<DashboardPage
headerProps={{
title: 'Mileage',
actions: (
<Button onClick={handleCreate}>
<Plus className="size-4" />
Add Mileage
</Button>
),
}}
>
<Card> <Card>
<CardContent> <CardContent>
<DataTable <DataTable
slots={{
actions: <Button onClick={handleCreate}>
<Plus className="size-4" />
Add Mileage
</Button>
}}
columns={columns} columns={columns}
data={records} data={records}
pagination={pagination} pagination={pagination}
@ -166,7 +161,6 @@ export default function VehicleMileagePage() {
/> />
</CardContent> </CardContent>
</Card> </Card>
</DashboardPage>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}> <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="min-w-md"> <DialogContent className="min-w-md">

View File

@ -129,20 +129,28 @@ export default function VehicleOwnersPage() {
} }
return ( return (
<DashboardPage headerProps={{
title: "Owners",
actions: (
<Button className="w-full" size={'lg'} onClick={() => setLinkDialogOpen(true)}>
<Plus />
Add Owner
</Button>
),
}}>
<Card> <Card>
<CardContent> <CardContent>
<DataTable <DataTable
slots={{
actions:
<div className="flex justify-end">
<Button size={'lg'} onClick={() => setLinkDialogOpen(true)}>
<Plus />
Add Owner
</Button>
</div>,
}
}
columns={columns} columns={columns}
data={owners} data={owners}
pagination={pagination} pagination={pagination}
@ -163,7 +171,6 @@ export default function VehicleOwnersPage() {
/> />
</CardContent> </CardContent>
</Card> </Card>
</DashboardPage >
) )
} }

View File

@ -14,6 +14,7 @@ import { VehicleForm } from '@/modules/vehicles/vehicle-form'
import { VEHICLE_ROUTES } from '@garage/api' import { VEHICLE_ROUTES } from '@garage/api'
import type { VehiclesClient } from '@garage/api' import type { VehiclesClient } from '@garage/api'
import { CarIcon } from 'lucide-react' import { CarIcon } from 'lucide-react'
import { getFullName } from '@/shared/utils/getFullName'
export default function VehiclesPage() { export default function VehiclesPage() {
const router = useRouter() const router = useRouter()
@ -75,6 +76,14 @@ export default function VehiclesPage() {
) )
}, },
}, },
{
accessorKey: "customers",
header: ({ column }: any) => <ColumnHeader column={column} title="Customer" />,
cell: ({ row }: any) => {
const val = (row.original as any).customers?.[0]
return getFullName(val)
},
} as any,
{ {
accessorKey: "year", accessorKey: "year",
header: ({ column }) => <ColumnHeader column={column} title="Year" />, header: ({ column }) => <ColumnHeader column={column} title="Year" />,
@ -113,22 +122,7 @@ export default function VehiclesPage() {
return val != null ? `${Number(val).toLocaleString()} mi` : "—" return val != null ? `${Number(val).toLocaleString()} mi` : "—"
}, },
}, },
{
accessorKey: "created_at",
header: ({ column }) => <ColumnHeader column={column} title="Created" />,
cell: ({ row }) => {
const val = (row.original as any).created_at
return val ? new Date(val).toLocaleDateString() : "—"
},
},
{
accessorKey: "customer",
header: ({ column }) => <ColumnHeader column={column} title="Customer" />,
cell: ({ row }) => {
const val = (row.original as any).customer
return val ? val.name : "—"
},
},
actionsColumn(), actionsColumn(),
]} ]}
/> />

View File

@ -1,5 +1,7 @@
"use client" "use client"
import { useMemo } from "react"
import { useSearchParams } from "next/navigation"
import { Loader2 } from "lucide-react" import { Loader2 } from "lucide-react"
import { useDashboardData } from "./use-dashboard-data" import { useDashboardData } from "./use-dashboard-data"
import { FinancialTotalsCards } from "./financial-totals-cards" import { FinancialTotalsCards } from "./financial-totals-cards"
@ -12,9 +14,38 @@ import { ItemsTotalsCard } from "./items-totals-card"
import { CustomersTotalsCard } from "./customers-totals-card" import { CustomersTotalsCard } from "./customers-totals-card"
import { SalesPurchaseCards } from "./sales-purchase-cards" import { SalesPurchaseCards } from "./sales-purchase-cards"
import { VehicleStatsCards } from "./vehicle-stats-cards" import { VehicleStatsCards } from "./vehicle-stats-cards"
import { DashboardPeriods, type DashboardPeriod, type HomeDashboardQuery } from "@garage/api"
const DEFAULT_PERIOD: DashboardPeriod = "this_month"
function isDashboardPeriod(value: string | null): value is DashboardPeriod {
return Boolean(value && (DashboardPeriods as readonly string[]).includes(value))
}
export function DashboardContent() { export function DashboardContent() {
const { data, isLoading, isError, error } = useDashboardData() const searchParams = useSearchParams()
const filters = useMemo<HomeDashboardQuery>(() => {
const periodValue = searchParams.get("period")
const period = isDashboardPeriod(periodValue) ? periodValue : DEFAULT_PERIOD
const nextFilters: HomeDashboardQuery = { period }
if (period === "custom") {
const startDate = searchParams.get("start_date")
const endDate = searchParams.get("end_date")
if (startDate) {
nextFilters.start_date = startDate
}
if (endDate) {
nextFilters.end_date = endDate
}
}
return nextFilters
}, [searchParams])
const { data, isLoading, isError, error } = useDashboardData(filters)
if (isLoading) { if (isLoading) {
return ( return (

View File

@ -0,0 +1,114 @@
"use client"
import { useMemo } from "react"
import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { DashboardPeriods, type DashboardPeriod } from "@garage/api"
import { Input } from "@/shared/components/ui/input"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/shared/components/ui/select"
const DEFAULT_PERIOD: DashboardPeriod = "this_month"
const PERIOD_LABELS: Record<DashboardPeriod, string> = {
today: "Today",
yesterday: "Yesterday",
this_week: "This Week",
this_month: "This Month",
last_month: "Last Month",
last_3_months: "Last 3 Months",
last_6_months: "Last 6 Months",
last_year: "Last Year",
year_to_date: "Year to Date",
all_time: "All Time",
custom: "Custom",
}
function isDashboardPeriod(value: string | null): value is DashboardPeriod {
return Boolean(value && (DashboardPeriods as readonly string[]).includes(value))
}
export function DashboardFiltersToolbar() {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const period = useMemo<DashboardPeriod>(() => {
const value = searchParams.get("period")
return isDashboardPeriod(value) ? value : DEFAULT_PERIOD
}, [searchParams])
const startDate = searchParams.get("start_date") ?? ""
const endDate = searchParams.get("end_date") ?? ""
const updateParams = (updates: Record<string, string | null>) => {
const params = new URLSearchParams(searchParams.toString())
Object.entries(updates).forEach(([key, value]) => {
if (!value) {
params.delete(key)
return
}
params.set(key, value)
})
const query = params.toString()
router.replace(query ? `${pathname}?${query}` : pathname, { scroll: false })
}
return (
<div className="flex flex-wrap items-end gap-2">
<div className="w-44">
<Select
value={period}
onValueChange={(value) => {
const nextPeriod = value as DashboardPeriod
updateParams({
period: nextPeriod,
start_date: nextPeriod === "custom" ? startDate || null : null,
end_date: nextPeriod === "custom" ? endDate || null : null,
})
}}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Period" />
</SelectTrigger>
<SelectContent>
{DashboardPeriods.map((value) => (
<SelectItem key={value} value={value}>
{PERIOD_LABELS[value]}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{period === "custom" ? (
<>
<div className="w-36">
<Input
type="date"
value={startDate}
onChange={(event) => {
updateParams({ start_date: event.target.value || null })
}}
/>
</div>
<div className="w-36">
<Input
type="date"
value={endDate}
onChange={(event) => {
updateParams({ end_date: event.target.value || null })
}}
/>
</div>
</>
) : null}
</div>
)
}

View File

@ -2,15 +2,15 @@
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import { useAuthApi } from "@/shared/useApi" import { useAuthApi } from "@/shared/useApi"
import type { HomeDashboardResponse } from "@garage/api" import type { HomeDashboardQuery, HomeDashboardResponse } from "@garage/api"
export type DashboardData = HomeDashboardResponse & Record<string, any> export type DashboardData = HomeDashboardResponse & Record<string, any>
export function useDashboardData() { export function useDashboardData(filters?: HomeDashboardQuery) {
const api = useAuthApi() const api = useAuthApi()
return useQuery<DashboardData>({ return useQuery<DashboardData>({
queryKey: ["home", "dashboard"], queryKey: ["home", "dashboard", filters],
queryFn: () => api.home.dashboard(), queryFn: () => api.home.dashboard(filters),
}) })
} }

View File

@ -11,7 +11,7 @@ import { toast } from "sonner"
import { useAuthApi } from "@/shared/useApi" import { useAuthApi } from "@/shared/useApi"
const schema = z.object({ const schema = z.object({
title: z.string().min(1, "Title is required"), name: z.string().min(1, "Name is required"),
}) })
type FormValues = z.infer<typeof schema> type FormValues = z.infer<typeof schema>
@ -21,16 +21,16 @@ export function DocumentTypeInlineForm({ onSuccess }: InlineCreateFormProps) {
const form = useForm<FormValues>({ const form = useForm<FormValues>({
resolver: zodResolver(schema), resolver: zodResolver(schema),
defaultValues: { title: "" }, defaultValues: { name: "" },
}) })
const handleSubmit = async (values: FormValues) => { const handleSubmit = async (values: FormValues) => {
try { try {
const result = await api.vehicleDocuments.createDocumentType({ title: values.title } as any) const result = await api.vehicleDocuments.createDocumentType({ name: values.name } as any)
toast.success("Document type created") toast.success("Document type created")
form.reset() form.reset()
const item = (result as any)?.data ?? result const item = (result as any)?.data ?? result
onSuccess({ value: String(item.id), label: item.title ?? item.name ?? String(item.id) }) onSuccess({ value: String(item.id), label: item.name ?? String(item.id) })
} catch { } catch {
toast.error("Failed to create document type") toast.error("Failed to create document type")
} }
@ -40,8 +40,8 @@ export function DocumentTypeInlineForm({ onSuccess }: InlineCreateFormProps) {
<Rhform form={form} onSubmit={handleSubmit}> <Rhform form={form} onSubmit={handleSubmit}>
<FieldGroup> <FieldGroup>
<RhfTextField <RhfTextField
name="title" name="name"
label="Title" label="Name"
placeholder="e.g. Registration Certificate" placeholder="e.g. Registration Certificate"
required required
/> />

View File

@ -3,12 +3,16 @@
import { useAuthApi } from "@/shared/useApi" import { useAuthApi } from "@/shared/useApi"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { Button } from "@/shared/components/ui/button" import { Button } from "@/shared/components/ui/button"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/shared/components/ui/dialog"
import { ScrollArea } from "@/shared/components/ui/scroll-area"
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/shared/components/ui/dropdown-menu" } from "@/shared/components/ui/dropdown-menu"
import { useFormDialog } from "@/shared/components/form-dialog"
import { VehicleForm } from "./vehicle-form"
import { Ellipsis, Pencil, Trash2 } from "lucide-react" import { Ellipsis, Pencil, Trash2 } from "lucide-react"
type VehicleActionsProps = { type VehicleActionsProps = {
@ -18,10 +22,7 @@ type VehicleActionsProps = {
export function VehicleActions({ vehicleId }: VehicleActionsProps) { export function VehicleActions({ vehicleId }: VehicleActionsProps) {
const api = useAuthApi() const api = useAuthApi()
const router = useRouter() const router = useRouter()
const editDialog = useFormDialog("vehicle-details-edit")
const handleEdit = () => {
router.push(`/sales/vehicles/${vehicleId}/edit`)
}
const handleDelete = async () => { const handleDelete = async () => {
await api.vehicles.destroy(vehicleId) await api.vehicles.destroy(vehicleId)
@ -36,7 +37,7 @@ export function VehicleActions({ vehicleId }: VehicleActionsProps) {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem onClick={handleEdit}> <DropdownMenuItem onSelect={() => editDialog.open(vehicleId)}>
<Pencil className="size-4" /> <Pencil className="size-4" />
Edit Edit
</DropdownMenuItem> </DropdownMenuItem>
@ -45,6 +46,23 @@ export function VehicleActions({ vehicleId }: VehicleActionsProps) {
Delete Delete
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
<Dialog open={editDialog.isOpen} onOpenChange={(isOpen) => { if (!isOpen) editDialog.close() }}>
<DialogContent className="min-w-xl lg:min-w-4xl">
<DialogHeader>
<DialogTitle className="text-2xl font-bold">Vehicle</DialogTitle>
</DialogHeader>
<ScrollArea className="max-h-[80vh] px-4">
<VehicleForm
resourceId={editDialog.resourceId}
onSuccess={() => {
editDialog.close()
router.refresh()
}}
/>
</ScrollArea>
</DialogContent>
</Dialog>
</DropdownMenu> </DropdownMenu>
) )
} }

View File

@ -7,7 +7,8 @@ import { Alert, AlertTitle } from "@/shared/components/ui/alert"
import { FieldGroup } from "@/shared/components/ui/field" import { FieldGroup } from "@/shared/components/ui/field"
import { import {
Rhform, Rhform,
RhfTextareaField, RhfTextField,
RhfDateField,
RhfAsyncSelectField, RhfAsyncSelectField,
RhfDocumentField, RhfDocumentField,
} from "@/shared/components/form" } from "@/shared/components/form"
@ -17,6 +18,7 @@ import { useAuthApi } from "@/shared/useApi"
import { useResourceForm } from "@/shared/hooks/use-resource-form" import { useResourceForm } from "@/shared/hooks/use-resource-form"
import { useFormMutation } from "@/shared/hooks/use-form-mutation" import { useFormMutation } from "@/shared/hooks/use-form-mutation"
import { toRelation, toId } from "@/shared/lib/utils" import { toRelation, toId } from "@/shared/lib/utils"
import { getFullName } from "@/shared/utils/getFullName"
import { vehicleDocumentFormSchema, type VehicleDocumentFormValues } from "./vehicle-document.schema" import { vehicleDocumentFormSchema, type VehicleDocumentFormValues } from "./vehicle-document.schema"
@ -33,8 +35,10 @@ export type VehicleDocumentFormProps = {
const DEFAULT_VALUES: VehicleDocumentFormValues = { const DEFAULT_VALUES: VehicleDocumentFormValues = {
document_type: null, document_type: null,
customer: null,
document_number: "",
document_expire: "",
file: null, file: null,
note: "",
} }
// ── Mapping helpers ── // ── Mapping helpers ──
@ -51,17 +55,21 @@ function mapToFormValues(data: unknown): VehicleDocumentFormValues {
return { return {
document_type: toRelation(d.document_type_id, d.document_type?.name ?? d.document_type?.title), document_type: toRelation(d.document_type_id, d.document_type?.name ?? d.document_type?.title),
customer: toRelation(d.customer_id, getFullName(d.customer) || d.customer?.name),
document_number: d.document_number || "",
document_expire: d.document_expire || "",
file: null, file: null,
note: d.note || "",
} }
} }
function mapToPayload(values: VehicleDocumentFormValues, vehicleId: string) { function mapToPayload(values: VehicleDocumentFormValues, vehicleId: string) {
return { return {
vehicle_id: Number(vehicleId), vehicle_id: vehicleId,
document_type_id: toId(values.document_type), document_type_id: toId(values.document_type),
file: values.file instanceof File ? values.file : undefined, customer_id: toId(values.customer),
note: values.note || undefined, document_number: values.document_number || undefined,
document_expire: values.document_expire || undefined,
document_file: values.file instanceof File ? values.file : undefined,
} }
} }
@ -122,18 +130,36 @@ export function VehicleDocumentForm({ vehicleId, resourceId, initialData, onSucc
{...STORE_OBJECT} {...STORE_OBJECT}
/> />
{/* <RhfAsyncSelectField
name="customer"
label="Customer"
placeholder="Select customer"
queryKey={["customers"]}
listFn={() => api.customers.list()}
mapOption={(item: any) => ({
value: String(item.id),
label: getFullName(item) || item.name || `#${item.id}`,
})}
{...STORE_OBJECT}
/> */}
<RhfTextField
name="document_number"
label="Document Number"
placeholder="e.g. DOC-001"
/>
<RhfDateField
name="document_expire"
label="Expiry Date"
/>
<RhfDocumentField <RhfDocumentField
name="file" name="file"
label="Document File" label="Document File"
required required
/> />
<RhfTextareaField
name="note"
label="Note"
placeholder="Optional notes about this document"
/>
<Button type="submit" disabled={isPending} className="w-full"> <Button type="submit" disabled={isPending} className="w-full">
{isPending ? null : isEditing ? <Save /> : <Plus />} {isPending ? null : isEditing ? <Save /> : <Plus />}
{isPending ? "Saving..." : isEditing ? "Update Document" : "Upload Document"} {isPending ? "Saving..." : isEditing ? "Update Document" : "Upload Document"}

View File

@ -4,8 +4,10 @@ const relationFieldSchema = z.object({ value: z.string(), label: z.string() }).n
export const vehicleDocumentFormSchema = z.object({ export const vehicleDocumentFormSchema = z.object({
document_type: relationFieldSchema, document_type: relationFieldSchema,
customer: relationFieldSchema,
document_number: z.string().optional(),
document_expire: z.string().optional(),
file: z.instanceof(File, { message: "File is required" }).nullable(), file: z.instanceof(File, { message: "File is required" }).nullable(),
note: z.string().optional(),
}) })
export type VehicleDocumentFormValues = z.infer<typeof vehicleDocumentFormSchema> export type VehicleDocumentFormValues = z.infer<typeof vehicleDocumentFormSchema>

View File

@ -24,6 +24,7 @@ import { useResourceForm } from "@/shared/hooks/use-resource-form"
import { useFormMutation } from "@/shared/hooks/use-form-mutation" import { useFormMutation } from "@/shared/hooks/use-form-mutation"
import { toRelation, toId } from "@/shared/lib/utils" import { toRelation, toId } from "@/shared/lib/utils"
import { formatUppercase } from "@/shared/utils/formatters" import { formatUppercase } from "@/shared/utils/formatters"
import { VEHICLE_ROUTES } from "@garage/api"
import { vehicleFormSchema, type VehicleFormValues } from "./vehicle.schema" import { vehicleFormSchema, type VehicleFormValues } from "./vehicle.schema"
import { CustomerForm } from "../customers/customer-form" import { CustomerForm } from "../customers/customer-form"
@ -125,6 +126,8 @@ export function VehicleForm({ resourceId, initialData, onSuccess }: VehicleFormP
defaultValues: DEFAULT_VALUES, defaultValues: DEFAULT_VALUES,
resourceId, resourceId,
initialData, initialData,
initialize: (id) => api.vehicles.getById(id),
queryKey: [VEHICLE_ROUTES.BY_ID, resourceId],
mapToFormValues, mapToFormValues,
}) })

View File

@ -1,6 +1,6 @@
{ {
"info": { "info": {
"_postman_id": "559bc27f-b656-4554-a080-73c56d317ce5", "_postman_id": "097332d0-ce5c-4074-bad0-df5482013438",
"name": "Reparee Collection", "name": "Reparee Collection",
"description": "Auto-generated from OpenAPI spec. Import storage/app/openapi-default.json for the full schema.", "description": "Auto-generated from OpenAPI spec. Import storage/app/openapi-default.json for the full schema.",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
@ -43463,13 +43463,120 @@
} }
], ],
"url": { "url": {
"raw": "{{base_url}}/api/home", "raw": "{{base_url}}/api/home?period=this_month&start_date=&end_date=&financial_period=this_month&financial_start_date=&financial_end_date=&work_order_period=this_month&work_order_start_date=&work_order_end_date=&appointment_period=this_month&appointment_start_date=&appointment_end_date=&sales_period=this_month&sales_start_date=&sales_end_date=&purchase_period=this_month&purchase_start_date=&purchase_end_date=&upcoming_filter=today&upcoming_per_page=10&upcoming_page=1",
"host": [ "host": [
"{{base_url}}" "{{base_url}}"
], ],
"path": [ "path": [
"api", "api",
"home" "home"
],
"query": [
{
"key": "period",
"value": "this_month",
"description": "Optional. Main dashboard period. Allowed: today, yesterday, this_week, this_month, last_month, last_3_months, last_6_months, last_year, year_to_date, all_time, custom."
},
{
"key": "start_date",
"value": "",
"description": "Optional. Main dashboard custom start date (YYYY-MM-DD). Use when period=custom."
},
{
"key": "end_date",
"value": "",
"description": "Optional. Main dashboard custom end date (YYYY-MM-DD). Use when period=custom."
},
{
"key": "financial_period",
"value": "this_month",
"description": "Optional. Financial section period. Allowed: today, yesterday, this_week, this_month, last_month, last_3_months, last_6_months, last_year, year_to_date, all_time, custom."
},
{
"key": "financial_start_date",
"value": "",
"description": "Optional. Financial custom start date (YYYY-MM-DD). Use when financial_period=custom."
},
{
"key": "financial_end_date",
"value": "",
"description": "Optional. Financial custom end date (YYYY-MM-DD). Use when financial_period=custom."
},
{
"key": "work_order_period",
"value": "this_month",
"description": "Optional. Work orders section period. Allowed: today, yesterday, this_week, this_month, last_month, last_3_months, last_6_months, last_year, year_to_date, all_time, custom."
},
{
"key": "work_order_start_date",
"value": "",
"description": "Optional. Work orders custom start date (YYYY-MM-DD). Use when work_order_period=custom."
},
{
"key": "work_order_end_date",
"value": "",
"description": "Optional. Work orders custom end date (YYYY-MM-DD). Use when work_order_period=custom."
},
{
"key": "appointment_period",
"value": "this_month",
"description": "Optional. Appointments section period. Allowed: today, yesterday, this_week, this_month, last_month, last_3_months, last_6_months, last_year, year_to_date, all_time, custom."
},
{
"key": "appointment_start_date",
"value": "",
"description": "Optional. Appointments custom start date (YYYY-MM-DD). Use when appointment_period=custom."
},
{
"key": "appointment_end_date",
"value": "",
"description": "Optional. Appointments custom end date (YYYY-MM-DD). Use when appointment_period=custom."
},
{
"key": "sales_period",
"value": "this_month",
"description": "Optional. Sales section period. Allowed: today, yesterday, this_week, this_month, last_month, last_3_months, last_6_months, last_year, year_to_date, all_time, custom."
},
{
"key": "sales_start_date",
"value": "",
"description": "Optional. Sales custom start date (YYYY-MM-DD). Use when sales_period=custom."
},
{
"key": "sales_end_date",
"value": "",
"description": "Optional. Sales custom end date (YYYY-MM-DD). Use when sales_period=custom."
},
{
"key": "purchase_period",
"value": "this_month",
"description": "Optional. Purchases section period. Allowed: today, yesterday, this_week, this_month, last_month, last_3_months, last_6_months, last_year, year_to_date, all_time, custom."
},
{
"key": "purchase_start_date",
"value": "",
"description": "Optional. Purchases custom start date (YYYY-MM-DD). Use when purchase_period=custom."
},
{
"key": "purchase_end_date",
"value": "",
"description": "Optional. Purchases custom end date (YYYY-MM-DD). Use when purchase_period=custom."
},
{
"key": "upcoming_filter",
"value": "today",
"description": "Optional. Upcoming appointments filter. Allowed: today, tomorrow, this_week, next_week. Default: today."
},
{
"key": "upcoming_per_page",
"value": "10",
"description": "Optional. Upcoming appointments page size. Integer 1-100. Default: 10."
},
{
"key": "upcoming_page",
"value": "1",
"description": "Optional. Upcoming appointments page number. Default: 1."
}
] ]
} }
}, },
@ -43499,13 +43606,120 @@
} }
], ],
"url": { "url": {
"raw": "{{base_url}}/api/home", "raw": "{{base_url}}/api/home?period=this_month&start_date=&end_date=&financial_period=this_month&financial_start_date=&financial_end_date=&work_order_period=this_month&work_order_start_date=&work_order_end_date=&appointment_period=this_month&appointment_start_date=&appointment_end_date=&sales_period=this_month&sales_start_date=&sales_end_date=&purchase_period=this_month&purchase_start_date=&purchase_end_date=&upcoming_filter=today&upcoming_per_page=10&upcoming_page=1",
"host": [ "host": [
"{{base_url}}" "{{base_url}}"
], ],
"path": [ "path": [
"api", "api",
"home" "home"
],
"query": [
{
"key": "period",
"value": "this_month",
"description": "Optional. Main dashboard period. Allowed: today, yesterday, this_week, this_month, last_month, last_3_months, last_6_months, last_year, year_to_date, all_time, custom."
},
{
"key": "start_date",
"value": "",
"description": "Optional. Main dashboard custom start date (YYYY-MM-DD). Use when period=custom."
},
{
"key": "end_date",
"value": "",
"description": "Optional. Main dashboard custom end date (YYYY-MM-DD). Use when period=custom."
},
{
"key": "financial_period",
"value": "this_month",
"description": "Optional. Financial section period. Allowed: today, yesterday, this_week, this_month, last_month, last_3_months, last_6_months, last_year, year_to_date, all_time, custom."
},
{
"key": "financial_start_date",
"value": "",
"description": "Optional. Financial custom start date (YYYY-MM-DD). Use when financial_period=custom."
},
{
"key": "financial_end_date",
"value": "",
"description": "Optional. Financial custom end date (YYYY-MM-DD). Use when financial_period=custom."
},
{
"key": "work_order_period",
"value": "this_month",
"description": "Optional. Work orders section period. Allowed: today, yesterday, this_week, this_month, last_month, last_3_months, last_6_months, last_year, year_to_date, all_time, custom."
},
{
"key": "work_order_start_date",
"value": "",
"description": "Optional. Work orders custom start date (YYYY-MM-DD). Use when work_order_period=custom."
},
{
"key": "work_order_end_date",
"value": "",
"description": "Optional. Work orders custom end date (YYYY-MM-DD). Use when work_order_period=custom."
},
{
"key": "appointment_period",
"value": "this_month",
"description": "Optional. Appointments section period. Allowed: today, yesterday, this_week, this_month, last_month, last_3_months, last_6_months, last_year, year_to_date, all_time, custom."
},
{
"key": "appointment_start_date",
"value": "",
"description": "Optional. Appointments custom start date (YYYY-MM-DD). Use when appointment_period=custom."
},
{
"key": "appointment_end_date",
"value": "",
"description": "Optional. Appointments custom end date (YYYY-MM-DD). Use when appointment_period=custom."
},
{
"key": "sales_period",
"value": "this_month",
"description": "Optional. Sales section period. Allowed: today, yesterday, this_week, this_month, last_month, last_3_months, last_6_months, last_year, year_to_date, all_time, custom."
},
{
"key": "sales_start_date",
"value": "",
"description": "Optional. Sales custom start date (YYYY-MM-DD). Use when sales_period=custom."
},
{
"key": "sales_end_date",
"value": "",
"description": "Optional. Sales custom end date (YYYY-MM-DD). Use when sales_period=custom."
},
{
"key": "purchase_period",
"value": "this_month",
"description": "Optional. Purchases section period. Allowed: today, yesterday, this_week, this_month, last_month, last_3_months, last_6_months, last_year, year_to_date, all_time, custom."
},
{
"key": "purchase_start_date",
"value": "",
"description": "Optional. Purchases custom start date (YYYY-MM-DD). Use when purchase_period=custom."
},
{
"key": "purchase_end_date",
"value": "",
"description": "Optional. Purchases custom end date (YYYY-MM-DD). Use when purchase_period=custom."
},
{
"key": "upcoming_filter",
"value": "today",
"description": "Optional. Upcoming appointments filter. Allowed: today, tomorrow, this_week, next_week. Default: today."
},
{
"key": "upcoming_per_page",
"value": "10",
"description": "Optional. Upcoming appointments page size. Integer 1-100. Default: 10."
},
{
"key": "upcoming_page",
"value": "1",
"description": "Optional. Upcoming appointments page number. Default: 1."
}
] ]
} }
}, },

View File

@ -5,6 +5,46 @@ export const HOME_ROUTES = {
DASHBOARD: "/api/home", DASHBOARD: "/api/home",
} as const satisfies Record<string, ApiPath> } as const satisfies Record<string, ApiPath>
export const DashboardPeriods = [
"today",
"yesterday",
"this_week",
"this_month",
"last_month",
"last_3_months",
"last_6_months",
"last_year",
"year_to_date",
"all_time",
"custom",
] as const
export type DashboardPeriod = (typeof DashboardPeriods)[number]
export type HomeDashboardQuery = {
period?: DashboardPeriod
start_date?: string
end_date?: string
financial_period?: DashboardPeriod
financial_start_date?: string
financial_end_date?: string
work_order_period?: DashboardPeriod
work_order_start_date?: string
work_order_end_date?: string
appointment_period?: DashboardPeriod
appointment_start_date?: string
appointment_end_date?: string
sales_period?: DashboardPeriod
sales_start_date?: string
sales_end_date?: string
purchase_period?: DashboardPeriod
purchase_start_date?: string
purchase_end_date?: string
upcoming_filter?: "today" | "tomorrow" | "this_week" | "next_week"
upcoming_per_page?: number
upcoming_page?: number
}
export type HomeDashboardResponse = ApiResponse<typeof HOME_ROUTES.DASHBOARD, "get"> export type HomeDashboardResponse = ApiResponse<typeof HOME_ROUTES.DASHBOARD, "get">
export class HomeClient extends ApiClient { export class HomeClient extends ApiClient {
@ -12,7 +52,14 @@ export class HomeClient extends ApiClient {
super(baseUrl, defaultOptions) super(baseUrl, defaultOptions)
} }
async dashboard() { async dashboard(query?: HomeDashboardQuery) {
return this.get(HOME_ROUTES.DASHBOARD) return this.get(
HOME_ROUTES.DASHBOARD,
query
? ({
query,
} as never)
: undefined,
)
} }
} }

View File

@ -35,7 +35,14 @@ export { ShopCalendarsClient, SHOP_CALENDAR_ROUTES } from "./shop-calendars"
export { HolidayYearsClient, HOLIDAY_YEAR_ROUTES } from "./holiday-years" export { HolidayYearsClient, HOLIDAY_YEAR_ROUTES } from "./holiday-years"
export { TaxesClient, TAX_ROUTES } from "./taxes" export { TaxesClient, TAX_ROUTES } from "./taxes"
export { InvoicesClient, INVOICE_ROUTES, type InvoiceShowData } from "./invoices" export { InvoicesClient, INVOICE_ROUTES, type InvoiceShowData } from "./invoices"
export { HomeClient, HOME_ROUTES, type HomeDashboardResponse } from "./home" export {
HomeClient,
HOME_ROUTES,
DashboardPeriods,
type DashboardPeriod,
type HomeDashboardQuery,
type HomeDashboardResponse,
} from "./home"
export { BillsClient, BILL_ROUTES } from "./bills" export { BillsClient, BILL_ROUTES } from "./bills"
export { ReasonsClient, REASON_ROUTES } from "./reasons" export { ReasonsClient, REASON_ROUTES } from "./reasons"
export { DocumentPrintClient, DOCUMENT_PRINT_ROUTES, type DocumentPrintType, type DocumentPrintMode } from "./document-print" export { DocumentPrintClient, DOCUMENT_PRINT_ROUTES, type DocumentPrintType, type DocumentPrintMode } from "./document-print"