diff --git a/apps/dashboard/app/(authenticated)/sales/customers/page.tsx b/apps/dashboard/app/(authenticated)/sales/customers/page.tsx index 8c853e9..4c5dc21 100644 --- a/apps/dashboard/app/(authenticated)/sales/customers/page.tsx +++ b/apps/dashboard/app/(authenticated)/sales/customers/page.tsx @@ -6,6 +6,7 @@ import { ColumnHeader } from '@/shared/data-view/table-view' import FormDialog from '@/shared/components/form-dialog' import { ImportDataButton } from '@/shared/components/import-data-button' import { ExportDataButton } from '@/shared/components/export-data-button' +import { DownloadSampleButton } from '@/shared/components/download-sample-button' import { useAuthApi } from '@/shared/useApi' import { CustomerForm } from '@/modules/customers/customer-form' import { CUSTOMER_ROUTES } from '@garage/api' @@ -24,6 +25,10 @@ export default function CustomersPage() { headerProps={({ selectedItem, invalidateQuery }) => ({ actions: (
+ api.customers.downloadImportSample()} + fileName='customers-import-sample' + /> api.customers.importData(file)} onSuccess={invalidateQuery} diff --git a/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/appointments/page.tsx b/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/appointments/page.tsx index b08eb1d..4e577d8 100644 --- a/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/appointments/page.tsx +++ b/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/appointments/page.tsx @@ -1,7 +1,8 @@ "use client" +import { useEffect } from "react" import { use } from "react" -import { useRouter } from "next/navigation" +import { usePathname, useRouter, useSearchParams } from "next/navigation" import { ResourcePage } from "@/shared/data-view/resource-page" import { ColumnHeader } from "@/shared/data-view/table-view" import FormDialog from "@/shared/components/form-dialog" @@ -27,8 +28,20 @@ export default function JobCardAppointmentsPage({ }) { const { id: jobCardId } = use(params) const router = useRouter() + const pathname = usePathname() + const searchParams = useSearchParams() const jobCard = useJobCard() + useEffect(() => { + if (searchParams.get("create") !== "1") return + + const params = new URLSearchParams(searchParams.toString()) + params.delete("create") + params.set("dialog", "true") + + router.replace(`${pathname}?${params.toString()}`) + }, [pathname, router, searchParams]) + const defaultJobCard = jobCard ? { value: String((jobCard as any).id), label: (jobCard as any).label || (jobCard as any).title || `Job Card` } : null diff --git a/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/expense-items/page.tsx b/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/expense-items/page.tsx index 481cdd8..e9bc333 100644 --- a/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/expense-items/page.tsx +++ b/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/expense-items/page.tsx @@ -48,7 +48,7 @@ export default function JobCardExpenseItemsPage({ const rows = (data as any)?.data ?? [] - const invalidate = () => queryClient.invalidateQueries({ queryKey }).then(() => router.refresh()) + const invalidate = () => queryClient.invalidateQueries({ queryKey , refetchType:'all'}).then(() => router.refresh()) async function handleDelete(row: any) { const confirmed = await confirm({ diff --git a/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/layout.tsx b/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/layout.tsx index 07da768..9b7d9d4 100644 --- a/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/layout.tsx +++ b/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/layout.tsx @@ -58,15 +58,15 @@ export default async function JobCardDetailLayout(props: { params: Promise<{ id: label: `Attachments (${docs?.length || 0})` }, + { + href: `/sales/job-cards/${id}/appointments`, + label: `Appointments (${jobCard?.appointments_count || 0})` + }, // { // href: `/sales/job-cards/${id}/inspections`, // label: `Inspections (${(jobCard as any)?.inspections_count || 0})` // }, - // { - // href: `/sales/job-cards/${id}/appointments`, - // label: `Appointments (${jobCard?.appointments_count || 0})` - // }, // { // href: `/sales/job-cards/${id}/tasks`, diff --git a/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/parts/page.tsx b/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/parts/page.tsx index 5a30e0c..bf52d14 100644 --- a/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/parts/page.tsx +++ b/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/parts/page.tsx @@ -25,6 +25,7 @@ import { Ellipsis, Plus } from "lucide-react" import type { ColumnDef } from "@tanstack/react-table" import { JobCardPartForm } from "@/modules/job-cards/job-card-part-form" import { formatDate } from "@/shared/utils/formatters" +import { JOB_CARD_ROUTES } from "@garage/api" export default function JobCardPartsPage({ params, @@ -35,7 +36,7 @@ export default function JobCardPartsPage({ const api = useAuthApi() const queryClient = useQueryClient() const router = useRouter() - const queryKey = ["job-card-parts", jobCardId] + const queryKey = [JOB_CARD_ROUTES.GET_PARTS, jobCardId] const [dialogOpen, setDialogOpen] = useState(false) const [editItem, setEditItem] = useState(null) @@ -47,7 +48,7 @@ export default function JobCardPartsPage({ const rows = (data as any)?.data ?? [] - const invalidate = () => queryClient.invalidateQueries({ queryKey }).then(() => router.refresh()) + const invalidate = () => queryClient.invalidateQueries({ queryKey, type: 'all', refetchType: 'all' },).then(() => router.refresh()) async function handleDelete(row: any) { const confirmed = await confirm({ diff --git a/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/services/page.tsx b/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/services/page.tsx index b744122..1f8accb 100644 --- a/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/services/page.tsx +++ b/apps/dashboard/app/(authenticated)/sales/job-cards/[id]/services/page.tsx @@ -50,7 +50,7 @@ export default function JobCardServicesPage({ const rows = (data as any)?.data ?? [] - const invalidate = () => queryClient.invalidateQueries({ queryKey }).then(() => router.refresh()) + const invalidate = () => queryClient.invalidateQueries({ queryKey , refetchType:'all'}).then(() => router.refresh()) async function handleDelete(row: any) { const confirmed = await confirm({ diff --git a/apps/dashboard/app/(authenticated)/sales/vehicles/page.tsx b/apps/dashboard/app/(authenticated)/sales/vehicles/page.tsx index 0182a0f..9903a8d 100644 --- a/apps/dashboard/app/(authenticated)/sales/vehicles/page.tsx +++ b/apps/dashboard/app/(authenticated)/sales/vehicles/page.tsx @@ -6,6 +6,7 @@ import { ColumnHeader } from '@/shared/data-view/table-view' import FormDialog from '@/shared/components/form-dialog' import { ImportDataButton } from '@/shared/components/import-data-button' import { ExportDataButton } from '@/shared/components/export-data-button' +import { DownloadSampleButton } from '@/shared/components/download-sample-button' import { useAuthApi } from '@/shared/useApi' import { VehicleForm } from '@/modules/vehicles/vehicle-form' import { VEHICLE_ROUTES } from '@garage/api' @@ -24,6 +25,10 @@ export default function VehiclesPage() { headerProps={({ selectedItem, invalidateQuery }) => ({ actions: (
+ api.vehicles.downloadImportSample()} + fileName='vehicles-import-sample' + /> api.vehicles.importData(file)} onSuccess={invalidateQuery} diff --git a/apps/dashboard/app/favicon.ico b/apps/dashboard/app/favicon.ico index 718d6fe..1476729 100644 Binary files a/apps/dashboard/app/favicon.ico and b/apps/dashboard/app/favicon.ico differ diff --git a/apps/dashboard/modules/home/nav-groups-links.ts b/apps/dashboard/modules/home/nav-groups-links.ts new file mode 100644 index 0000000..21ee7fa --- /dev/null +++ b/apps/dashboard/modules/home/nav-groups-links.ts @@ -0,0 +1,21 @@ +import { navGroups } from "@/config/navGroups" + +function normalizeTitle(title: string) { + return title.trim().toLowerCase() +} + +const titleToHref = new Map() + +for (const group of navGroups) { + for (const item of group.items) { + titleToHref.set(normalizeTitle(item.title), item.href) + + for (const subItem of item.items ?? []) { + titleToHref.set(normalizeTitle(subItem.title), subItem.href) + } + } +} + +export function getNavHrefByTitle(title: string) { + return titleToHref.get(normalizeTitle(title)) +} diff --git a/apps/dashboard/modules/home/sales-purchase-cards.tsx b/apps/dashboard/modules/home/sales-purchase-cards.tsx index 1700476..a150cbc 100644 --- a/apps/dashboard/modules/home/sales-purchase-cards.tsx +++ b/apps/dashboard/modules/home/sales-purchase-cards.tsx @@ -1,8 +1,10 @@ "use client" +import Link from "next/link" import { FileText, FileSearch, Receipt, ShoppingCart } from "lucide-react" import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card" import type { DashboardData } from "./use-dashboard-data" +import { getNavHrefByTitle } from "./nav-groups-links" type Props = { data: DashboardData } @@ -11,15 +13,15 @@ export function SalesPurchaseCards({ data }: Props) { const purchase = data.purchase_totals const salesStats = [ - { label: "Inspections", value: sales?.inspections ?? 0, icon: FileSearch }, - { label: "Estimates", value: sales?.estimates ?? 0, icon: FileText }, - { label: "Invoices", value: sales?.invoices ?? 0, icon: Receipt }, + { label: "Inspections", value: sales?.inspections ?? 0, icon: FileSearch, href: getNavHrefByTitle("Inspections") }, + { label: "Estimates", value: sales?.estimates ?? 0, icon: FileText, href: getNavHrefByTitle("Estimates") }, + { label: "Invoices", value: sales?.invoices ?? 0, icon: Receipt, href: getNavHrefByTitle("Invoices") }, ] const purchaseStats = [ - { label: "Purchase Orders", value: purchase?.purchase_orders ?? 0, icon: ShoppingCart }, - { label: "Bills", value: purchase?.bills ?? 0, icon: Receipt }, - { label: "Expenses", value: purchase?.expenses ?? 0, icon: FileText }, + { label: "Purchase Orders", value: purchase?.purchase_orders ?? 0, icon: ShoppingCart, href: getNavHrefByTitle("Purchase Orders") }, + { label: "Bills", value: purchase?.bills ?? 0, icon: Receipt, href: getNavHrefByTitle("Bills") }, + { label: "Expenses", value: purchase?.expenses ?? 0, icon: FileText, href: getNavHrefByTitle("Expenses") }, ] return ( @@ -34,16 +36,30 @@ export function SalesPurchaseCards({ data }: Props) {
{salesStats.map((stat) => ( -
- - {stat.value} - - {stat.label} - -
+ stat.href ? ( + + + {stat.value} + + {stat.label} + + + ) : ( +
+ + {stat.value} + + {stat.label} + +
+ ) ))}
@@ -59,16 +75,30 @@ export function SalesPurchaseCards({ data }: Props) {
{purchaseStats.map((stat) => ( -
- - {stat.value} - - {stat.label} - -
+ stat.href ? ( + + + {stat.value} + + {stat.label} + + + ) : ( +
+ + {stat.value} + + {stat.label} + +
+ ) ))}
diff --git a/apps/dashboard/modules/job-cards/job-card-dropdown.tsx b/apps/dashboard/modules/job-cards/job-card-dropdown.tsx index 207063e..188fbb2 100644 --- a/apps/dashboard/modules/job-cards/job-card-dropdown.tsx +++ b/apps/dashboard/modules/job-cards/job-card-dropdown.tsx @@ -5,7 +5,7 @@ import { confirm } from '@/shared/components/confirm-dialog'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/shared/components/ui/dropdown-menu' import { Button } from '@/shared/components/ui/button' import { toast } from 'sonner' -import { Ellipsis, FileText, Pencil, Printer, Trash2 } from 'lucide-react'; +import { CalendarPlus, Ellipsis, FileText, Pencil, Printer, Trash2 } from 'lucide-react'; import { useDocumentPrint } from '@/shared/hooks/use-document-print'; import { useJobCard } from './job-card-context'; import { useState } from 'react'; @@ -30,6 +30,10 @@ export default function JobCardDropdown({ id }: { id: string }) { print("job_card", id, "print") } + const handleCreateAppointment = () => { + router.push(`/sales/job-cards/${id}/appointments?create=1`) + } + const handleConvertToInvoice = async () => { const confirmed = await confirm({ title: "Convert to Invoice", @@ -52,7 +56,7 @@ export default function JobCardDropdown({ id }: { id: string }) { toast.info("An invoice already exists for this job card.") router.push(`/sales/invoice/${conflictId}`) } else { - toast.error("Failed to convert job card to invoice") + toast.error(err?.message || "Failed to convert job card to invoice") } } finally { setIsConverting(false) @@ -78,38 +82,52 @@ export default function JobCardDropdown({ id }: { id: string }) { } } return ( - - - - - - - - - - Edit - - - - {isPrinting ? "Printing..." : "Print"} - - {jobCard?.status !== "draft" && ( - <> - + )} + + + + + + + + + + + Edit + + + + {isPrinting ? "Printing..." : "Print"} + + + + + Create Appointment + + {jobCard?.status !== "draft" && ( {isConverting ? "Converting..." : "Convert to Invoice"} - - )} - - - - Delete - - - + )} + + + + Delete + + + +
) } \ No newline at end of file diff --git a/apps/dashboard/public/assets/android-chrome-192x192.png b/apps/dashboard/public/assets/android-chrome-192x192.png new file mode 100644 index 0000000..993f485 Binary files /dev/null and b/apps/dashboard/public/assets/android-chrome-192x192.png differ diff --git a/apps/dashboard/public/assets/android-chrome-512x512.png b/apps/dashboard/public/assets/android-chrome-512x512.png new file mode 100644 index 0000000..093bc20 Binary files /dev/null and b/apps/dashboard/public/assets/android-chrome-512x512.png differ diff --git a/apps/dashboard/public/assets/apple-touch-icon.png b/apps/dashboard/public/assets/apple-touch-icon.png new file mode 100644 index 0000000..1b62026 Binary files /dev/null and b/apps/dashboard/public/assets/apple-touch-icon.png differ diff --git a/apps/dashboard/public/assets/favicon-16x16.png b/apps/dashboard/public/assets/favicon-16x16.png new file mode 100644 index 0000000..8bee2d9 Binary files /dev/null and b/apps/dashboard/public/assets/favicon-16x16.png differ diff --git a/apps/dashboard/public/assets/favicon-32x32.png b/apps/dashboard/public/assets/favicon-32x32.png new file mode 100644 index 0000000..d42235f Binary files /dev/null and b/apps/dashboard/public/assets/favicon-32x32.png differ diff --git a/apps/dashboard/public/assets/site.webmanifest b/apps/dashboard/public/assets/site.webmanifest new file mode 100644 index 0000000..45dc8a2 --- /dev/null +++ b/apps/dashboard/public/assets/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file diff --git a/apps/dashboard/shared/components/download-sample-button.tsx b/apps/dashboard/shared/components/download-sample-button.tsx new file mode 100644 index 0000000..03733a2 --- /dev/null +++ b/apps/dashboard/shared/components/download-sample-button.tsx @@ -0,0 +1,47 @@ +"use client" + +import { useState } from "react" +import { Button } from "@/shared/components/ui/button" +import { Download, Loader2 } from "lucide-react" +import { toast } from "sonner" + +type DownloadSampleButtonProps = { + onDownload: () => Promise + fileName?: string + label?: string +} + +export function DownloadSampleButton({ + onDownload, + fileName = "import-sample", + label = "Sample", +}: DownloadSampleButtonProps) { + const [isPending, setIsPending] = useState(false) + + const handleDownload = async () => { + setIsPending(true) + try { + const blob = await onDownload() + const url = URL.createObjectURL(blob) + const anchor = document.createElement("a") + anchor.href = url + anchor.download = `${fileName}.xlsx` + document.body.appendChild(anchor) + anchor.click() + document.body.removeChild(anchor) + URL.revokeObjectURL(url) + toast.success("Sample downloaded successfully") + } catch (err: any) { + toast.error(err?.message ?? "Failed to download sample") + } finally { + setIsPending(false) + } + } + + return ( + + ) +} \ No newline at end of file diff --git a/packages/api/open-api/schema.json b/packages/api/open-api/schema.json index ff41c7c..d056066 100644 --- a/packages/api/open-api/schema.json +++ b/packages/api/open-api/schema.json @@ -1700,6 +1700,19 @@ } } }, + "/api/customers/import/sample": { + "get": { + "tags": [ + "Customers" + ], + "summary": "Download customers import sample (xlsx/csv)", + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/api/customers/import": { "post": { "tags": [ @@ -5122,6 +5135,19 @@ } } }, + "/api/vehicles/import/sample": { + "get": { + "tags": [ + "Vehicles" + ], + "summary": "Download vehicles import sample (xlsx/csv)", + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/api/vehicles/import": { "post": { "tags": [ @@ -32921,6 +32947,19 @@ } } }, + "/api/parts/import/sample": { + "get": { + "tags": [ + "Import Parts" + ], + "summary": "Download parts import sample (xlsx/csv)", + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/api/parts/import": { "post": { "tags": [ @@ -35132,6 +35171,19 @@ } } }, + "/api/services/import/sample": { + "get": { + "tags": [ + "Import Services" + ], + "summary": "Download services import sample (xlsx/csv)", + "responses": { + "200": { + "description": "OK" + } + } + } + }, "/api/import-services": { "post": { "tags": [ diff --git a/packages/api/postman/collection.json b/packages/api/postman/collection.json index 418d58b..0d3e516 100644 --- a/packages/api/postman/collection.json +++ b/packages/api/postman/collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "6ef9e473-ce87-4b4b-9e48-89d153d50fca", + "_postman_id": "559bc27f-b656-4554-a080-73c56d317ce5", "name": "Reparee Collection", "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", @@ -1343,6 +1343,48 @@ } ] }, + { + "name": "Download customers import sample (xlsx/csv)", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{auth_token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "*/*" + } + ], + "url": { + "raw": "{{base_url}}/api/customers/import/sample?format=xlsx", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "customers", + "import", + "sample" + ], + "query": [ + { + "key": "format", + "value": "xlsx", + "description": "xlsx | csv" + } + ] + } + }, + "response": [] + }, { "name": "Import customers from Excel file.", "request": { @@ -4917,6 +4959,48 @@ } ] }, + { + "name": "Download vehicles import sample (xlsx/csv)", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{auth_token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "*/*" + } + ], + "url": { + "raw": "{{base_url}}/api/vehicles/import/sample?format=xlsx", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "vehicles", + "import", + "sample" + ], + "query": [ + { + "key": "format", + "value": "xlsx", + "description": "xlsx | csv" + } + ] + } + }, + "response": [] + }, { "name": "Import vehicles from Excel file.", "request": { @@ -24371,6 +24455,48 @@ { "name": "Import Parts", "item": [ + { + "name": "Download parts import sample (xlsx/csv)", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{auth_token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "*/*" + } + ], + "url": { + "raw": "{{base_url}}/api/parts/import/sample?format=xlsx", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "parts", + "import", + "sample" + ], + "query": [ + { + "key": "format", + "value": "xlsx", + "description": "xlsx | csv" + } + ] + } + }, + "response": [] + }, { "name": "Import parts from Excel file.", "request": { @@ -26187,6 +26313,48 @@ { "name": "Import Services", "item": [ + { + "name": "Download services import sample (xlsx/csv)", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{auth_token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "*/*" + } + ], + "url": { + "raw": "{{base_url}}/api/services/import/sample?format=xlsx", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "services", + "import", + "sample" + ], + "query": [ + { + "key": "format", + "value": "xlsx", + "description": "xlsx | csv" + } + ] + } + }, + "response": [] + }, { "name": "Import services from Excel file.", "request": { @@ -43393,7 +43561,7 @@ { "key": "type", "value": "invoice", - "description": "inspection | estimate | job_card | invoice | payment_received | expense | purchase_order | bill | payment_made" + "description": "Allowed values: inspection | estimate | job_card | invoice | payment_received | expense | purchase_order | bill | payment_made" }, { "key": "id", @@ -43435,7 +43603,12 @@ ], "body": { "mode": "raw", - "raw": "{\n \"type\": \"job_card\",\n \"id\": 1,\n \"mode\": \"print\"\n}" + "raw": "{\n \"type\": \"job_card\",\n \"id\": 1,\n \"mode\": \"print\"\n}", + "options": { + "raw": { + "language": "json" + } + } }, "url": { "raw": "{{base_url}}/api/document-print", diff --git a/packages/api/src/clients/customers.ts b/packages/api/src/clients/customers.ts index 1cd6ae1..89f1632 100644 --- a/packages/api/src/clients/customers.ts +++ b/packages/api/src/clients/customers.ts @@ -1,6 +1,6 @@ import { CrudClient } from "../infra/crud-client" -import { ApiClient, type ApiClientOptions } from "../infra/client" -import type { ApiPath, ApiRequestBody } from "../infra/types" +import type { ApiClientOptions } from "../infra/client" +import type { ApiPath } from "../infra/types" import type { ApiListQueryParams } from "../contracts/types" export const CUSTOMER_ROUTES = { @@ -8,6 +8,7 @@ export const CUSTOMER_ROUTES = { BY_ID: "/api/customers/{id}", EXPORT: "/api/customers/export", IMPORT: "/api/customers/import", + IMPORT_SAMPLE: "/api/customers/import/sample", CUSTOMER_TYPES: "/api/customer-types", NOTES: "/api/customers/{id}/notes/{note_id}", } as const satisfies Record @@ -15,7 +16,15 @@ export const CUSTOMER_ROUTES = { export class CustomersClient extends CrudClient { constructor(baseUrl?: string, defaultOptions?: ApiClientOptions) { - super(baseUrl, defaultOptions, CUSTOMER_ROUTES.INDEX, CUSTOMER_ROUTES.BY_ID) + super( + baseUrl, + defaultOptions, + CUSTOMER_ROUTES.INDEX, + CUSTOMER_ROUTES.BY_ID, + CUSTOMER_ROUTES.IMPORT, + CUSTOMER_ROUTES.EXPORT, + "GET", + ) } async getById(id: string) { @@ -26,12 +35,8 @@ export class CustomersClient extends CrudClient) { - return this.post(CUSTOMER_ROUTES.IMPORT, payload) + async downloadImportSample() { + return this.fetchBlob(CUSTOMER_ROUTES.IMPORT_SAMPLE, { method: "GET" }) } async addNote(id: string, payload: { note: string }) { diff --git a/packages/api/src/clients/vehicles.ts b/packages/api/src/clients/vehicles.ts index cb06e88..9a18810 100644 --- a/packages/api/src/clients/vehicles.ts +++ b/packages/api/src/clients/vehicles.ts @@ -8,6 +8,7 @@ export const VEHICLE_ROUTES = { BY_ID: "/api/vehicles/{id}", EXPORT: "/api/vehicles/export", IMPORT: "/api/vehicles/import", + IMPORT_SAMPLE: "/api/vehicles/import/sample", GET_OWNERS: "/api/get-vehicle-owners", LINK_CUSTOMER: "/api/link-customer-to-vehicle", UNLINK_CUSTOMER: "/api/unlink-customer-from-vehicle", @@ -31,7 +32,15 @@ export class VehiclesClient extends CrudClient< typeof VEHICLE_ROUTES.BY_ID > { constructor(baseUrl?: string, defaultOptions?: ApiClientOptions) { - super(baseUrl, defaultOptions, VEHICLE_ROUTES.INDEX, VEHICLE_ROUTES.BY_ID) + super( + baseUrl, + defaultOptions, + VEHICLE_ROUTES.INDEX, + VEHICLE_ROUTES.BY_ID, + VEHICLE_ROUTES.IMPORT, + VEHICLE_ROUTES.EXPORT, + "GET", + ) } @@ -53,12 +62,8 @@ export class VehiclesClient extends CrudClient< return this.postFormData(url, fd) } - async export() { - return this.get(VEHICLE_ROUTES.EXPORT) - } - - async import(payload: ApiRequestBody) { - return this.post(VEHICLE_ROUTES.IMPORT, payload) + async downloadImportSample() { + return this.fetchBlob(VEHICLE_ROUTES.IMPORT_SAMPLE, { method: "GET" }) } async getOwners(vehicleId: string | number, query?: ApiListQueryParams) { diff --git a/packages/api/src/infra/crud-client.ts b/packages/api/src/infra/crud-client.ts index 991007d..bea65e7 100644 --- a/packages/api/src/infra/crud-client.ts +++ b/packages/api/src/infra/crud-client.ts @@ -72,7 +72,7 @@ export abstract class CrudClient< if (this.exportMethod === "POST") { return this.fetchBlob(route, { method: "POST", body: filters ?? {} }) } - return this.fetchBlob(route) + return this.fetchBlob(route, { method: "GET" }) } } diff --git a/packages/api/types/index.ts b/packages/api/types/index.ts index ad6fe1a..717e03e 100644 --- a/packages/api/types/index.ts +++ b/packages/api/types/index.ts @@ -1005,6 +1005,40 @@ export interface paths { patch?: never; trace?: never; }; + "/api/customers/import/sample": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Download customers import sample (xlsx/csv) */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/customers/import": { parameters: { query?: never; @@ -3568,6 +3602,40 @@ export interface paths { patch?: never; trace?: never; }; + "/api/vehicles/import/sample": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Download vehicles import sample (xlsx/csv) */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/vehicles/import": { parameters: { query?: never; @@ -22368,6 +22436,40 @@ export interface paths { patch?: never; trace?: never; }; + "/api/parts/import/sample": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Download parts import sample (xlsx/csv) */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/parts/import": { parameters: { query?: never; @@ -23938,6 +24040,40 @@ export interface paths { patch?: never; trace?: never; }; + "/api/services/import/sample": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Download services import sample (xlsx/csv) */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/import-services": { parameters: { query?: never;