diff --git a/apps/dashboard/app/(authenticated)/page.tsx b/apps/dashboard/app/(authenticated)/page.tsx index d6b26d1..dddc8e5 100644 --- a/apps/dashboard/app/(authenticated)/page.tsx +++ b/apps/dashboard/app/(authenticated)/page.tsx @@ -1,10 +1,11 @@ import { DashboardHeader } from "@/base/components/layout/dashboard"; import DashboardPage from "@/base/components/layout/dashboard/dashboard-page"; import { DashboardContent } from "@/modules/home/dashboard-content"; +import { DashboardFiltersToolbar } from "@/modules/home/dashboard-filters-toolbar"; export default function page() { return ( - + }} > ) diff --git a/apps/dashboard/app/(authenticated)/sales/vehicles/[id]/documents/page.tsx b/apps/dashboard/app/(authenticated)/sales/vehicles/[id]/documents/page.tsx index 7313a03..c822ee8 100644 --- a/apps/dashboard/app/(authenticated)/sales/vehicles/[id]/documents/page.tsx +++ b/apps/dashboard/app/(authenticated)/sales/vehicles/[id]/documents/page.tsx @@ -4,7 +4,7 @@ import { useParams } from "next/navigation" import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query" import { type ColumnDef } from "@tanstack/react-table" import { useState } from "react" -import { Plus, Trash2 } from "lucide-react" +import { Download, ExternalLink, Plus, Trash2 } from "lucide-react" import { toast } from "sonner" import { useAuthApi } from "@/shared/useApi" @@ -19,10 +19,27 @@ import { DialogTitle, } from "@/shared/components/ui/dialog" import { VehicleDocumentForm } from "@/modules/vehicles/vehicle-document-form" +import { formatDate, formatDateTime } from "@/shared/utils/formatters" +import { getFullName } from "@/shared/utils/getFullName" type VehicleDocument = { 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 updated_at: string } @@ -54,7 +71,7 @@ export default function VehicleDocumentsPage() { const handleDelete = async (doc: VehicleDocument) => { const confirmed = await confirm({ 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", variant: "destructive", }) @@ -65,16 +82,74 @@ export default function VehicleDocumentsPage() { const columns: ColumnDef[] = [ { - accessorKey: "name", - header: ({ column }) => , + accessorKey: "document_number", + header: ({ column }) => , + cell: ({ getValue, row }) => { + const number = getValue() + const fallbackName = row.original.document_file?.split("/").pop() + return number || fallbackName || `Document #${row.original.id}` + }, + }, + { + accessorKey: "document_type_id", + header: ({ column }) => , + cell: ({ row, getValue }) => { + const typeName = row.original.document_type?.name + const value = getValue() + return typeName || (value ? `#${value}` : "—") + }, + }, + { + accessorKey: "customer_id", + header: ({ column }) => , + cell: ({ row, getValue }) => { + const name = getFullName(row.original.customer as any) || row.original.customer?.name + const value = getValue() + return name || (value ? `#${value}` : "—") + }, + }, + { + accessorKey: "document_expire", + header: ({ column }) => , + cell: ({ getValue }) => formatDate(getValue()), + }, + { + id: "file", + header: ({ column }) => , + cell: ({ row }) => { + const url = row.original.document_file_url + if (!url) return "No file" + + return ( +
e.stopPropagation()}> + + + Open + + + + Download + +
+ ) + }, + enableSorting: false, }, { accessorKey: "created_at", header: ({ column }) => , - cell: ({ getValue }) => { - const val = getValue() - return val ? new Date(val).toLocaleDateString() : "—" - }, + cell: ({ getValue }) => formatDateTime(getValue()), }, { id: "actions", @@ -83,7 +158,10 @@ export default function VehicleDocumentsPage() { - + + + + }} columns={columns} data={documents} pagination={pagination} sorting={[]} - onChange={() => {}} + onChange={() => { }} + onRowClick={handleRowClick} isLoading={isLoading} /> diff --git a/apps/dashboard/app/(authenticated)/sales/vehicles/[id]/layout.tsx b/apps/dashboard/app/(authenticated)/sales/vehicles/[id]/layout.tsx index fbcc34f..c214bfe 100644 --- a/apps/dashboard/app/(authenticated)/sales/vehicles/[id]/layout.tsx +++ b/apps/dashboard/app/(authenticated)/sales/vehicles/[id]/layout.tsx @@ -13,7 +13,6 @@ export default async function layout(props: { params: Promise<{ id: string }>, c return ( <> - - - - - Add Mileage - - ), - }} - > + + + + Add Mileage + + }} columns={columns} data={records} pagination={pagination} sorting={[]} - onChange={() => {}} + onChange={() => { }} isLoading={isLoading} /> - diff --git a/apps/dashboard/app/(authenticated)/sales/vehicles/[id]/owners/page.tsx b/apps/dashboard/app/(authenticated)/sales/vehicles/[id]/owners/page.tsx index ae28b28..c167ab5 100644 --- a/apps/dashboard/app/(authenticated)/sales/vehicles/[id]/owners/page.tsx +++ b/apps/dashboard/app/(authenticated)/sales/vehicles/[id]/owners/page.tsx @@ -129,41 +129,48 @@ export default function VehicleOwnersPage() { } return ( - setLinkDialogOpen(true)}> - - Add Owner - - ), - }}> - - - + - onChange={() => { }} - isLoading={isLoading} - /> - { - queryClient.invalidateQueries({ queryKey }) - setLinkDialogOpen(false) - }} - /> - - - + + + + , + + + } + + } + + columns={columns} + data={owners} + pagination={pagination} + sorting={[]} + + onChange={() => { }} + isLoading={isLoading} + /> + + { + queryClient.invalidateQueries({ queryKey }) + setLinkDialogOpen(false) + }} + /> + + ) } diff --git a/apps/dashboard/app/(authenticated)/sales/vehicles/page.tsx b/apps/dashboard/app/(authenticated)/sales/vehicles/page.tsx index d32cccb..08b4ebc 100644 --- a/apps/dashboard/app/(authenticated)/sales/vehicles/page.tsx +++ b/apps/dashboard/app/(authenticated)/sales/vehicles/page.tsx @@ -14,6 +14,7 @@ import { VehicleForm } from '@/modules/vehicles/vehicle-form' import { VEHICLE_ROUTES } from '@garage/api' import type { VehiclesClient } from '@garage/api' import { CarIcon } from 'lucide-react' +import { getFullName } from '@/shared/utils/getFullName' export default function VehiclesPage() { const router = useRouter() @@ -39,7 +40,7 @@ export default function VehiclesPage() { onExport={(filters) => api.vehicles.exportData(filters)} fileName='vehicles' /> - + {(resourceId) => ( ), })} - - columns={({ actionsColumn })=> [ + + columns={({ actionsColumn }) => [ { id: "name", - + header: ({ column }) => , cell: ({ row }) => { const r = row.original as any @@ -75,6 +76,14 @@ export default function VehiclesPage() { ) }, }, + { + accessorKey: "customers", + header: ({ column }: any) => , + cell: ({ row }: any) => { + const val = (row.original as any).customers?.[0] + return getFullName(val) + }, + } as any, { accessorKey: "year", header: ({ column }) => , @@ -113,22 +122,7 @@ export default function VehiclesPage() { return val != null ? `${Number(val).toLocaleString()} mi` : "—" }, }, - { - accessorKey: "created_at", - header: ({ column }) => , - cell: ({ row }) => { - const val = (row.original as any).created_at - return val ? new Date(val).toLocaleDateString() : "—" - }, - }, - { - accessorKey: "customer", - header: ({ column }) => , - cell: ({ row }) => { - const val = (row.original as any).customer - return val ? val.name : "—" - }, - }, + actionsColumn(), ]} /> diff --git a/apps/dashboard/modules/home/dashboard-content.tsx b/apps/dashboard/modules/home/dashboard-content.tsx index fb5d889..bd3ebd6 100644 --- a/apps/dashboard/modules/home/dashboard-content.tsx +++ b/apps/dashboard/modules/home/dashboard-content.tsx @@ -1,5 +1,7 @@ "use client" +import { useMemo } from "react" +import { useSearchParams } from "next/navigation" import { Loader2 } from "lucide-react" import { useDashboardData } from "./use-dashboard-data" import { FinancialTotalsCards } from "./financial-totals-cards" @@ -12,9 +14,38 @@ import { ItemsTotalsCard } from "./items-totals-card" import { CustomersTotalsCard } from "./customers-totals-card" import { SalesPurchaseCards } from "./sales-purchase-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() { - const { data, isLoading, isError, error } = useDashboardData() + const searchParams = useSearchParams() + const filters = useMemo(() => { + 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) { return ( diff --git a/apps/dashboard/modules/home/dashboard-filters-toolbar.tsx b/apps/dashboard/modules/home/dashboard-filters-toolbar.tsx new file mode 100644 index 0000000..4bf828f --- /dev/null +++ b/apps/dashboard/modules/home/dashboard-filters-toolbar.tsx @@ -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 = { + 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(() => { + 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) => { + 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 ( +
+
+ +
+ + {period === "custom" ? ( + <> +
+ { + updateParams({ start_date: event.target.value || null }) + }} + /> +
+
+ { + updateParams({ end_date: event.target.value || null }) + }} + /> +
+ + ) : null} +
+ ) +} diff --git a/apps/dashboard/modules/home/use-dashboard-data.ts b/apps/dashboard/modules/home/use-dashboard-data.ts index e6555b8..01d798a 100644 --- a/apps/dashboard/modules/home/use-dashboard-data.ts +++ b/apps/dashboard/modules/home/use-dashboard-data.ts @@ -2,15 +2,15 @@ import { useQuery } from "@tanstack/react-query" import { useAuthApi } from "@/shared/useApi" -import type { HomeDashboardResponse } from "@garage/api" +import type { HomeDashboardQuery, HomeDashboardResponse } from "@garage/api" export type DashboardData = HomeDashboardResponse & Record -export function useDashboardData() { +export function useDashboardData(filters?: HomeDashboardQuery) { const api = useAuthApi() return useQuery({ - queryKey: ["home", "dashboard"], - queryFn: () => api.home.dashboard(), + queryKey: ["home", "dashboard", filters], + queryFn: () => api.home.dashboard(filters), }) } diff --git a/apps/dashboard/modules/vehicles/inline-forms/document-type-inline-form.tsx b/apps/dashboard/modules/vehicles/inline-forms/document-type-inline-form.tsx index 1f22404..cecfb4b 100644 --- a/apps/dashboard/modules/vehicles/inline-forms/document-type-inline-form.tsx +++ b/apps/dashboard/modules/vehicles/inline-forms/document-type-inline-form.tsx @@ -11,7 +11,7 @@ import { toast } from "sonner" import { useAuthApi } from "@/shared/useApi" const schema = z.object({ - title: z.string().min(1, "Title is required"), + name: z.string().min(1, "Name is required"), }) type FormValues = z.infer @@ -21,16 +21,16 @@ export function DocumentTypeInlineForm({ onSuccess }: InlineCreateFormProps) { const form = useForm({ resolver: zodResolver(schema), - defaultValues: { title: "" }, + defaultValues: { name: "" }, }) const handleSubmit = async (values: FormValues) => { 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") form.reset() 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 { toast.error("Failed to create document type") } @@ -40,8 +40,8 @@ export function DocumentTypeInlineForm({ onSuccess }: InlineCreateFormProps) { diff --git a/apps/dashboard/modules/vehicles/vehicle-actions.tsx b/apps/dashboard/modules/vehicles/vehicle-actions.tsx index b086be5..ceecd24 100644 --- a/apps/dashboard/modules/vehicles/vehicle-actions.tsx +++ b/apps/dashboard/modules/vehicles/vehicle-actions.tsx @@ -3,12 +3,16 @@ import { useAuthApi } from "@/shared/useApi" import { useRouter } from "next/navigation" 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 { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } 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" type VehicleActionsProps = { @@ -18,10 +22,7 @@ type VehicleActionsProps = { export function VehicleActions({ vehicleId }: VehicleActionsProps) { const api = useAuthApi() const router = useRouter() - - const handleEdit = () => { - router.push(`/sales/vehicles/${vehicleId}/edit`) - } + const editDialog = useFormDialog("vehicle-details-edit") const handleDelete = async () => { await api.vehicles.destroy(vehicleId) @@ -36,7 +37,7 @@ export function VehicleActions({ vehicleId }: VehicleActionsProps) { - + editDialog.open(vehicleId)}> Edit @@ -45,6 +46,23 @@ export function VehicleActions({ vehicleId }: VehicleActionsProps) { Delete + { if (!isOpen) editDialog.close() }}> + + + Vehicle + + + { + editDialog.close() + router.refresh() + }} + /> + + + ) } diff --git a/apps/dashboard/modules/vehicles/vehicle-document-form.tsx b/apps/dashboard/modules/vehicles/vehicle-document-form.tsx index 0b06279..0a65184 100644 --- a/apps/dashboard/modules/vehicles/vehicle-document-form.tsx +++ b/apps/dashboard/modules/vehicles/vehicle-document-form.tsx @@ -7,7 +7,8 @@ import { Alert, AlertTitle } from "@/shared/components/ui/alert" import { FieldGroup } from "@/shared/components/ui/field" import { Rhform, - RhfTextareaField, + RhfTextField, + RhfDateField, RhfAsyncSelectField, RhfDocumentField, } from "@/shared/components/form" @@ -17,6 +18,7 @@ import { useAuthApi } from "@/shared/useApi" import { useResourceForm } from "@/shared/hooks/use-resource-form" import { useFormMutation } from "@/shared/hooks/use-form-mutation" import { toRelation, toId } from "@/shared/lib/utils" +import { getFullName } from "@/shared/utils/getFullName" import { vehicleDocumentFormSchema, type VehicleDocumentFormValues } from "./vehicle-document.schema" @@ -33,8 +35,10 @@ export type VehicleDocumentFormProps = { const DEFAULT_VALUES: VehicleDocumentFormValues = { document_type: null, + customer: null, + document_number: "", + document_expire: "", file: null, - note: "", } // ── Mapping helpers ── @@ -51,17 +55,21 @@ function mapToFormValues(data: unknown): VehicleDocumentFormValues { return { 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, - note: d.note || "", } } function mapToPayload(values: VehicleDocumentFormValues, vehicleId: string) { return { - vehicle_id: Number(vehicleId), + vehicle_id: vehicleId, document_type_id: toId(values.document_type), - file: values.file instanceof File ? values.file : undefined, - note: values.note || undefined, + customer_id: toId(values.customer), + 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} /> + {/* api.customers.list()} + mapOption={(item: any) => ({ + value: String(item.id), + label: getFullName(item) || item.name || `#${item.id}`, + })} + {...STORE_OBJECT} + /> */} + + + + + - -