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 (
-
-
-
-
-