Compare commits
No commits in common. "c7eb23dd3f7e7c01b4dfa62628f837121e5c7de9" and "c5d406792fe97cb141204848a2a854a6d9b3bdcd" have entirely different histories.
c7eb23dd3f
...
c5d406792f
@ -1,11 +1,10 @@
|
|||||||
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", actions: <DashboardFiltersToolbar /> }} >
|
<DashboardPage headerProps={{title: "Dashboard"}} >
|
||||||
<DashboardContent />
|
<DashboardContent />
|
||||||
</DashboardPage>
|
</DashboardPage>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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 { Download, ExternalLink, Plus, Trash2 } from "lucide-react"
|
import { Plus, Trash2 } from "lucide-react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
import { useAuthApi } from "@/shared/useApi"
|
import { useAuthApi } from "@/shared/useApi"
|
||||||
@ -19,27 +19,10 @@ 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
|
||||||
document_number?: string
|
name: 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
|
||||||
}
|
}
|
||||||
@ -71,7 +54,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.document_number || `Document #${doc.id}`}"?`,
|
description: `Are you sure you want to delete "${doc.name}"?`,
|
||||||
confirmLabel: "Delete",
|
confirmLabel: "Delete",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
})
|
})
|
||||||
@ -82,74 +65,16 @@ export default function VehicleDocumentsPage() {
|
|||||||
|
|
||||||
const columns: ColumnDef<VehicleDocument>[] = [
|
const columns: ColumnDef<VehicleDocument>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "document_number",
|
accessorKey: "name",
|
||||||
header: ({ column }) => <ColumnHeader column={column} title="Document Number" />,
|
header: ({ column }) => <ColumnHeader column={column} title="Name" />,
|
||||||
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 }) => formatDateTime(getValue<string | undefined>()),
|
cell: ({ getValue }) => {
|
||||||
|
const val = getValue<string>()
|
||||||
|
return val ? new Date(val).toLocaleDateString() : "—"
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "actions",
|
||||||
@ -158,10 +83,7 @@ export default function VehicleDocumentsPage() {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
onClick={(e) => {
|
onClick={() => handleDelete(row.original)}
|
||||||
e.stopPropagation()
|
|
||||||
handleDelete(row.original)
|
|
||||||
}}
|
|
||||||
title="Delete document"
|
title="Delete document"
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4 text-destructive" />
|
<Trash2 className="size-4 text-destructive" />
|
||||||
@ -171,15 +93,6 @@ 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
|
||||||
|
|
||||||
@ -192,25 +105,21 @@ 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>
|
||||||
|
|||||||
@ -13,6 +13,7 @@ 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}
|
||||||
|
|||||||
@ -142,16 +142,21 @@ 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}
|
||||||
@ -161,6 +166,7 @@ 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">
|
||||||
|
|||||||
@ -129,28 +129,20 @@ 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}
|
||||||
@ -171,6 +163,7 @@ export default function VehicleOwnersPage() {
|
|||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</DashboardPage >
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,18 +3,15 @@
|
|||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { ResourcePage } from '@/shared/data-view/resource-page'
|
import { ResourcePage } from '@/shared/data-view/resource-page'
|
||||||
import { ColumnHeader } from '@/shared/data-view/table-view'
|
import { ColumnHeader } from '@/shared/data-view/table-view'
|
||||||
import type { ColumnDef } from '@tanstack/react-table'
|
|
||||||
import FormDialog from '@/shared/components/form-dialog'
|
import FormDialog from '@/shared/components/form-dialog'
|
||||||
import { ImportDataButton } from '@/shared/components/import-data-button'
|
import { ImportDataButton } from '@/shared/components/import-data-button'
|
||||||
import { ExportDataButton } from '@/shared/components/export-data-button'
|
import { ExportDataButton } from '@/shared/components/export-data-button'
|
||||||
import { DownloadSampleButton } from '@/shared/components/download-sample-button'
|
import { DownloadSampleButton } from '@/shared/components/download-sample-button'
|
||||||
import { useAuthApi } from '@/shared/useApi'
|
import { useAuthApi } from '@/shared/useApi'
|
||||||
import type { CrudResourceColumnHelpers, ResourceItem } from '@/shared/data-view/resource-page'
|
|
||||||
import { VehicleForm } from '@/modules/vehicles/vehicle-form'
|
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()
|
||||||
@ -40,7 +37,7 @@ export default function VehiclesPage() {
|
|||||||
onExport={(filters) => api.vehicles.exportData(filters)}
|
onExport={(filters) => api.vehicles.exportData(filters)}
|
||||||
fileName='vehicles'
|
fileName='vehicles'
|
||||||
/>
|
/>
|
||||||
<FormDialog title="Vehicle" classNames={{ dialogContent: 'lg:min-w-4xl' }}>
|
<FormDialog title="Vehicle">
|
||||||
{(resourceId) => (
|
{(resourceId) => (
|
||||||
<VehicleForm
|
<VehicleForm
|
||||||
resourceId={resourceId}
|
resourceId={resourceId}
|
||||||
@ -52,11 +49,9 @@ export default function VehiclesPage() {
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
})}
|
})}
|
||||||
|
|
||||||
columns={({ actionsColumn }) => [
|
columns={({ actionsColumn }) => [
|
||||||
{
|
{
|
||||||
id: "name",
|
accessorKey: "name",
|
||||||
|
|
||||||
header: ({ column }) => <ColumnHeader column={column} title="Vehicle" />,
|
header: ({ column }) => <ColumnHeader column={column} title="Vehicle" />,
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const r = row.original as any
|
const r = row.original as any
|
||||||
@ -76,14 +71,6 @@ 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" />,
|
||||||
@ -122,7 +109,14 @@ 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() : "—"
|
||||||
|
},
|
||||||
|
},
|
||||||
actionsColumn(),
|
actionsColumn(),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -134,7 +134,6 @@ export function CustomerForm({ resourceId, initialData, onSuccess }: CustomerFor
|
|||||||
mapToFormValues: mapCustomerToFormValues,
|
mapToFormValues: mapCustomerToFormValues,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { customer_type} = form.watch()
|
|
||||||
const { mutate, error, isPending } = useFormMutation(form, {
|
const { mutate, error, isPending } = useFormMutation(form, {
|
||||||
mutationFn: (values: CustomerFormValues) => {
|
mutationFn: (values: CustomerFormValues) => {
|
||||||
const payload = mapFormToPayload(values)
|
const payload = mapFormToPayload(values)
|
||||||
@ -193,11 +192,7 @@ export function CustomerForm({ resourceId, initialData, onSuccess }: CustomerFor
|
|||||||
<RhfTextField name="last_name" label="Last Name" placeholder="Doe" required />
|
<RhfTextField name="last_name" label="Last Name" placeholder="Doe" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{
|
|
||||||
customer_type?.label === "Company" &&
|
|
||||||
<RhfTextField name="company_name" label="Company Name" placeholder="Doe Holdings" />
|
<RhfTextField name="company_name" label="Company Name" placeholder="Doe Holdings" />
|
||||||
}
|
|
||||||
|
|
||||||
{/* Contact */}
|
{/* Contact */}
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
"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"
|
||||||
@ -14,38 +12,9 @@ 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 searchParams = useSearchParams()
|
const { data, isLoading, isError, error } = useDashboardData()
|
||||||
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 (
|
||||||
|
|||||||
@ -1,114 +0,0 @@
|
|||||||
"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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -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 { HomeDashboardQuery, HomeDashboardResponse } from "@garage/api"
|
import type { HomeDashboardResponse } from "@garage/api"
|
||||||
|
|
||||||
export type DashboardData = HomeDashboardResponse & Record<string, any>
|
export type DashboardData = HomeDashboardResponse & Record<string, any>
|
||||||
|
|
||||||
export function useDashboardData(filters?: HomeDashboardQuery) {
|
export function useDashboardData() {
|
||||||
const api = useAuthApi()
|
const api = useAuthApi()
|
||||||
|
|
||||||
return useQuery<DashboardData>({
|
return useQuery<DashboardData>({
|
||||||
queryKey: ["home", "dashboard", filters],
|
queryKey: ["home", "dashboard"],
|
||||||
queryFn: () => api.home.dashboard(filters),
|
queryFn: () => api.home.dashboard(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -252,7 +252,7 @@ export function JobCardForm({ resourceId, initialData, onSuccess }: JobCardFormP
|
|||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
<RhfCustomerSelectField name="customer" />
|
<RhfCustomerSelectField name="customer" />
|
||||||
<RhfVehicleSelectField name="vehicle" disabled={!customer?.value} customer_id={customer?.value} />
|
<RhfVehicleSelectField name="vehicle" customer_id={customer?.value} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RhfCheckboxField name="has_insurance" label="Has Insurance Work?" />
|
<RhfCheckboxField name="has_insurance" label="Has Insurance Work?" />
|
||||||
|
|||||||
@ -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({
|
||||||
name: z.string().min(1, "Name is required"),
|
title: z.string().min(1, "Title 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: { name: "" },
|
defaultValues: { title: "" },
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleSubmit = async (values: FormValues) => {
|
const handleSubmit = async (values: FormValues) => {
|
||||||
try {
|
try {
|
||||||
const result = await api.vehicleDocuments.createDocumentType({ name: values.name } as any)
|
const result = await api.vehicleDocuments.createDocumentType({ title: values.title } 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.name ?? String(item.id) })
|
onSuccess({ value: String(item.id), label: item.title ?? 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="name"
|
name="title"
|
||||||
label="Name"
|
label="Title"
|
||||||
placeholder="e.g. Registration Certificate"
|
placeholder="e.g. Registration Certificate"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -18,14 +18,8 @@ import type { VehicleFormValues } from "./vehicle.schema"
|
|||||||
|
|
||||||
// ── Helpers ──
|
// ── Helpers ──
|
||||||
|
|
||||||
function toOptionalString(value: unknown): string | undefined {
|
function unique(values: (string | undefined)[]): string[] {
|
||||||
if (typeof value === "string") return value || undefined
|
return [...new Set(values.filter((v): v is string => !!v))].sort()
|
||||||
if (typeof value === "number") return String(value)
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
function unique(values: unknown[]): string[] {
|
|
||||||
return [...new Set(values.map(toOptionalString).filter((v): v is string => !!v))].sort()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Internal combobox for a single free-text + suggestion field ──
|
// ── Internal combobox for a single free-text + suggestion field ──
|
||||||
@ -66,9 +60,8 @@ function VehicleCombobox({
|
|||||||
}, [value])
|
}, [value])
|
||||||
|
|
||||||
// Client-side filtering of suggestions based on what's been typed
|
// Client-side filtering of suggestions based on what's been typed
|
||||||
const normalizedInput = inputText.toLowerCase()
|
|
||||||
const filtered = options.filter((opt) =>
|
const filtered = options.filter((opt) =>
|
||||||
!normalizedInput || opt.toLowerCase().includes(normalizedInput),
|
!inputText || opt.toLowerCase().includes(inputText.toLowerCase()),
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -137,7 +130,7 @@ export function RhfVehicleIdentityField() {
|
|||||||
const { control, setValue } = useFormContext<VehicleFormValues>()
|
const { control, setValue } = useFormContext<VehicleFormValues>()
|
||||||
|
|
||||||
// Read shop_type to pass as shop_type_id filter to the API
|
// Read shop_type to pass as shop_type_id filter to the API
|
||||||
const shopType = useWatch({ control, name: "shop_type_id" })
|
const shopType = useWatch({ control, name: "shop_type" })
|
||||||
const shopTypeId = shopType?.value
|
const shopTypeId = shopType?.value
|
||||||
|
|
||||||
const makeCtrl = useController({ name: "make", control })
|
const makeCtrl = useController({ name: "make", control })
|
||||||
@ -165,26 +158,11 @@ export function RhfVehicleIdentityField() {
|
|||||||
drivetrain?: string
|
drivetrain?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeRecord(record: unknown): MakeAndModelRecord {
|
|
||||||
const value = record as Record<string, unknown>
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: typeof value.id === "number" ? value.id : undefined,
|
|
||||||
shop_type_id: typeof value.shop_type_id === "number" ? value.shop_type_id : undefined,
|
|
||||||
make: toOptionalString(value.make),
|
|
||||||
model: toOptionalString(value.model),
|
|
||||||
year: toOptionalString(value.year),
|
|
||||||
sub_model: toOptionalString(value.sub_model),
|
|
||||||
engine_size: toOptionalString(value.engine_size),
|
|
||||||
drivetrain: toOptionalString(value.drivetrain),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractRecords(response: unknown): MakeAndModelRecord[] {
|
function extractRecords(response: unknown): MakeAndModelRecord[] {
|
||||||
const obj = response as any
|
const obj = response as any
|
||||||
if (Array.isArray(obj?.data)) return obj.data.map(normalizeRecord)
|
if (Array.isArray(obj?.data)) return obj.data
|
||||||
if (Array.isArray(obj?.data?.data)) return obj.data.data.map(normalizeRecord)
|
if (Array.isArray(obj?.data?.data)) return obj.data.data
|
||||||
if (Array.isArray(obj)) return obj.map(normalizeRecord)
|
if (Array.isArray(obj)) return obj
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,16 +3,12 @@
|
|||||||
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 = {
|
||||||
@ -22,7 +18,10 @@ 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)
|
||||||
@ -37,7 +36,7 @@ export function VehicleActions({ vehicleId }: VehicleActionsProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem onSelect={() => editDialog.open(vehicleId)}>
|
<DropdownMenuItem onClick={handleEdit}>
|
||||||
<Pencil className="size-4" />
|
<Pencil className="size-4" />
|
||||||
Edit
|
Edit
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@ -46,23 +45,6 @@ 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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,8 +7,7 @@ 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,
|
||||||
RhfTextField,
|
RhfTextareaField,
|
||||||
RhfDateField,
|
|
||||||
RhfAsyncSelectField,
|
RhfAsyncSelectField,
|
||||||
RhfDocumentField,
|
RhfDocumentField,
|
||||||
} from "@/shared/components/form"
|
} from "@/shared/components/form"
|
||||||
@ -18,7 +17,6 @@ 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"
|
||||||
|
|
||||||
@ -35,10 +33,8 @@ 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 ──
|
||||||
@ -55,21 +51,17 @@ 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: vehicleId,
|
vehicle_id: Number(vehicleId),
|
||||||
document_type_id: toId(values.document_type),
|
document_type_id: toId(values.document_type),
|
||||||
customer_id: toId(values.customer),
|
file: values.file instanceof File ? values.file : undefined,
|
||||||
document_number: values.document_number || undefined,
|
note: values.note || undefined,
|
||||||
document_expire: values.document_expire || undefined,
|
|
||||||
document_file: values.file instanceof File ? values.file : undefined,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,36 +122,18 @@ 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"}
|
||||||
|
|||||||
@ -4,10 +4,8 @@ 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>
|
||||||
|
|||||||
@ -24,11 +24,9 @@ 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 { VEHICLE_ROUTES } from "@garage/api"
|
||||||
import { getFullName } from "@/shared/utils/getFullName"
|
|
||||||
|
|
||||||
// ── Props ──
|
// ── Props ──
|
||||||
|
|
||||||
@ -41,12 +39,11 @@ export type VehicleFormProps = {
|
|||||||
// ── Default values ──
|
// ── Default values ──
|
||||||
|
|
||||||
const DEFAULT_VALUES: VehicleFormValues = {
|
const DEFAULT_VALUES: VehicleFormValues = {
|
||||||
shop_type_id: null,
|
shop_type: null,
|
||||||
vehicle_body_type_id: null,
|
vehicle_body_type: null,
|
||||||
vehicle_fuel_type_id: null,
|
vehicle_fuel_type: null,
|
||||||
vehicle_transmission_id: null,
|
vehicle_transmission: null,
|
||||||
vehicle_color_id: null,
|
vehicle_color: null,
|
||||||
customer_id: null,
|
|
||||||
make: "",
|
make: "",
|
||||||
model: "",
|
model: "",
|
||||||
year: "",
|
year: "",
|
||||||
@ -73,12 +70,11 @@ function mapToFormValues(data: unknown): VehicleFormValues {
|
|||||||
const d = (data as any)?.data ?? data ?? {}
|
const d = (data as any)?.data ?? data ?? {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
shop_type_id: toRelation(d.shop_type_id, d.shop_type?.title),
|
shop_type: toRelation(d.shop_type_id, d.shop_type?.title),
|
||||||
vehicle_body_type_id: toRelation(d.vehicle_body_type_id, d.vehicle_body_type?.title),
|
vehicle_body_type: toRelation(d.vehicle_body_type_id, d.vehicle_body_type?.title),
|
||||||
vehicle_fuel_type_id: toRelation(d.vehicle_fuel_type_id, d.vehicle_fuel_type?.title),
|
vehicle_fuel_type: toRelation(d.vehicle_fuel_type_id, d.vehicle_fuel_type?.title),
|
||||||
vehicle_transmission_id: toRelation(d.vehicle_transmission_id, d.vehicle_transmission?.title),
|
vehicle_transmission: toRelation(d.vehicle_transmission_id, d.vehicle_transmission?.title),
|
||||||
vehicle_color_id: toRelation(d.vehicle_color_id, d.vehicle_color?.title),
|
vehicle_color: toRelation(d.vehicle_color_id, d.vehicle_color?.title),
|
||||||
customer_id: toRelation(d.customer_id, d.customer?.name),
|
|
||||||
make: d.make || "",
|
make: d.make || "",
|
||||||
model: d.model || "",
|
model: d.model || "",
|
||||||
year: d.year || "",
|
year: d.year || "",
|
||||||
@ -95,13 +91,11 @@ function mapToFormValues(data: unknown): VehicleFormValues {
|
|||||||
|
|
||||||
function mapToPayload(values: VehicleFormValues) {
|
function mapToPayload(values: VehicleFormValues) {
|
||||||
return {
|
return {
|
||||||
shop_type_id: toId(values.shop_type_id),
|
shop_type_id: toId(values.shop_type),
|
||||||
vehicle_body_type_id: toId(values.vehicle_body_type_id),
|
vehicle_body_type_id: toId(values.vehicle_body_type),
|
||||||
vehicle_fuel_type_id: toId(values.vehicle_fuel_type_id),
|
vehicle_fuel_type_id: toId(values.vehicle_fuel_type),
|
||||||
vehicle_transmission_id: toId(values.vehicle_transmission_id),
|
vehicle_transmission_id: toId(values.vehicle_transmission),
|
||||||
vehicle_color_id: toId(values.vehicle_color_id),
|
vehicle_color_id: toId(values.vehicle_color),
|
||||||
|
|
||||||
customer_id: toId(values.customer_id),
|
|
||||||
make: values.make,
|
make: values.make,
|
||||||
model: values.model,
|
model: values.model,
|
||||||
year: values.year,
|
year: values.year,
|
||||||
@ -126,8 +120,6 @@ 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,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -169,7 +161,7 @@ export function VehicleForm({ resourceId, initialData, onSuccess }: VehicleFormP
|
|||||||
{/* Associations */}
|
{/* Associations */}
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
<RhfAsyncSelectField
|
<RhfAsyncSelectField
|
||||||
name="shop_type_id"
|
name="shop_type"
|
||||||
label="Shop Type"
|
label="Shop Type"
|
||||||
placeholder="Select shop type"
|
placeholder="Select shop type"
|
||||||
queryKey={["shop-types"]}
|
queryKey={["shop-types"]}
|
||||||
@ -180,7 +172,7 @@ export function VehicleForm({ resourceId, initialData, onSuccess }: VehicleFormP
|
|||||||
{...STORE_OBJECT}
|
{...STORE_OBJECT}
|
||||||
/>
|
/>
|
||||||
<RhfAsyncSelectField
|
<RhfAsyncSelectField
|
||||||
name="vehicle_body_type_id"
|
name="vehicle_body_type"
|
||||||
label="Body Type"
|
label="Body Type"
|
||||||
placeholder="Select body type"
|
placeholder="Select body type"
|
||||||
queryKey={["vehicle-body-types"]}
|
queryKey={["vehicle-body-types"]}
|
||||||
@ -194,7 +186,7 @@ export function VehicleForm({ resourceId, initialData, onSuccess }: VehicleFormP
|
|||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
<RhfAsyncSelectField
|
<RhfAsyncSelectField
|
||||||
name="vehicle_fuel_type_id"
|
name="vehicle_fuel_type"
|
||||||
label="Fuel Type"
|
label="Fuel Type"
|
||||||
placeholder="Select fuel type"
|
placeholder="Select fuel type"
|
||||||
queryKey={["vehicle-fuel-types"]}
|
queryKey={["vehicle-fuel-types"]}
|
||||||
@ -205,7 +197,7 @@ export function VehicleForm({ resourceId, initialData, onSuccess }: VehicleFormP
|
|||||||
{...STORE_OBJECT}
|
{...STORE_OBJECT}
|
||||||
/>
|
/>
|
||||||
<RhfAsyncSelectField
|
<RhfAsyncSelectField
|
||||||
name="vehicle_transmission_id"
|
name="vehicle_transmission"
|
||||||
label="Transmission"
|
label="Transmission"
|
||||||
placeholder="Select transmission"
|
placeholder="Select transmission"
|
||||||
queryKey={["vehicle-transmissions"]}
|
queryKey={["vehicle-transmissions"]}
|
||||||
@ -219,7 +211,7 @@ export function VehicleForm({ resourceId, initialData, onSuccess }: VehicleFormP
|
|||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
<RhfAsyncSelectField
|
<RhfAsyncSelectField
|
||||||
name="vehicle_color_id"
|
name="vehicle_color"
|
||||||
label="Color"
|
label="Color"
|
||||||
placeholder="Select color"
|
placeholder="Select color"
|
||||||
queryKey={["vehicle-colors"]}
|
queryKey={["vehicle-colors"]}
|
||||||
@ -231,20 +223,6 @@ export function VehicleForm({ resourceId, initialData, onSuccess }: VehicleFormP
|
|||||||
/>
|
/>
|
||||||
<RhfTextField name="vin_number" label="VIN Number" placeholder="e.g. 1HGBH41JXMN109186" />
|
<RhfTextField name="vin_number" label="VIN Number" placeholder="e.g. 1HGBH41JXMN109186" />
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
||||||
<RhfAsyncSelectField
|
|
||||||
name="customer_id"
|
|
||||||
label="Customer"
|
|
||||||
placeholder="Select customer"
|
|
||||||
queryKey={["customers"]}
|
|
||||||
listFn={() => api.customers.list()}
|
|
||||||
mapOption={(op:any)=> ({ value: String(op.id), label: getFullName(op) })}
|
|
||||||
createForm={(props) => <CustomerForm {...props} />}
|
|
||||||
createLabel="Customer"
|
|
||||||
|
|
||||||
{...STORE_OBJECT}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* License & identifiers */}
|
{/* License & identifiers */}
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
|
|||||||
@ -6,12 +6,12 @@ export const relationFieldSchema = z
|
|||||||
|
|
||||||
export const vehicleFormSchema = z.object({
|
export const vehicleFormSchema = z.object({
|
||||||
// ── Relations ──
|
// ── Relations ──
|
||||||
shop_type_id: relationFieldSchema,
|
shop_type: relationFieldSchema,
|
||||||
vehicle_body_type_id: relationFieldSchema,
|
vehicle_body_type: relationFieldSchema,
|
||||||
vehicle_fuel_type_id: relationFieldSchema,
|
vehicle_fuel_type: relationFieldSchema,
|
||||||
vehicle_transmission_id: relationFieldSchema,
|
vehicle_transmission: relationFieldSchema,
|
||||||
vehicle_color_id: relationFieldSchema,
|
vehicle_color: relationFieldSchema,
|
||||||
customer_id: relationFieldSchema,
|
|
||||||
// ── Vehicle identity ──
|
// ── Vehicle identity ──
|
||||||
make: z.string().optional(),
|
make: z.string().optional(),
|
||||||
model: z.string().optional(),
|
model: z.string().optional(),
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"info": {
|
"info": {
|
||||||
"_postman_id": "097332d0-ce5c-4074-bad0-df5482013438",
|
"_postman_id": "559bc27f-b656-4554-a080-73c56d317ce5",
|
||||||
"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,120 +43463,13 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"url": {
|
"url": {
|
||||||
"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",
|
"raw": "{{base_url}}/api/home",
|
||||||
"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."
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -43606,120 +43499,13 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"url": {
|
"url": {
|
||||||
"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",
|
"raw": "{{base_url}}/api/home",
|
||||||
"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."
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -5,46 +5,6 @@ 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 {
|
||||||
@ -52,14 +12,7 @@ export class HomeClient extends ApiClient {
|
|||||||
super(baseUrl, defaultOptions)
|
super(baseUrl, defaultOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
async dashboard(query?: HomeDashboardQuery) {
|
async dashboard() {
|
||||||
return this.get(
|
return this.get(HOME_ROUTES.DASHBOARD)
|
||||||
HOME_ROUTES.DASHBOARD,
|
|
||||||
query
|
|
||||||
? ({
|
|
||||||
query,
|
|
||||||
} as never)
|
|
||||||
: undefined,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,14 +35,7 @@ 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 {
|
export { HomeClient, HOME_ROUTES, type HomeDashboardResponse } from "./home"
|
||||||
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"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user