瀏覽代碼

new dvi inspection to vehicle with templates

Bernt Christian Egeland 7 月之前
父節點
當前提交
ea0a590ec2
共有 25 個文件被更改,包括 2947 次插入 和 51 次删除
  1. 103 0
      prisma/migrations/20260220155420_dvi_inspection/migration.sql
  2. 104 15
      prisma/schema.prisma
  3. 35 0
      src/app/(authenticated)/inspections/[id]/page.tsx
  4. 250 0
      src/app/(authenticated)/inspections/inspections-client.tsx
  5. 55 0
      src/app/(authenticated)/inspections/page.tsx
  6. 1 1
      src/app/(authenticated)/settings/settings-nav.tsx
  7. 18 7
      src/app/(authenticated)/settings/templates/page.tsx
  8. 62 26
      src/app/(authenticated)/settings/templates/template-settings.tsx
  9. 7 1
      src/app/(authenticated)/vehicles/[id]/page.tsx
  10. 99 1
      src/app/(authenticated)/vehicles/[id]/vehicle-detail-client.tsx
  11. 252 0
      src/app/(public)/share/inspection/[orgId]/[token]/inspection-view.tsx
  12. 100 0
      src/app/(public)/share/inspection/[orgId]/[token]/page.tsx
  13. 8 0
      src/app/api/files/public/[token]/[...path]/route.ts
  14. 2 0
      src/components/app-sidebar.tsx
  15. 216 0
      src/features/inspections/Actions/inspectionActions.ts
  16. 42 0
      src/features/inspections/Actions/inspectionShareActions.ts
  17. 265 0
      src/features/inspections/Actions/templateActions.ts
  18. 529 0
      src/features/inspections/Components/InspectionPageClient.tsx
  19. 124 0
      src/features/inspections/Components/InspectionShareDialog.tsx
  20. 171 0
      src/features/inspections/Components/NewInspectionDialog.tsx
  21. 265 0
      src/features/inspections/Components/TemplateForm.tsx
  22. 184 0
      src/features/inspections/Components/TemplateListClient.tsx
  23. 16 0
      src/features/inspections/Schema/inspectionSchema.ts
  24. 28 0
      src/features/inspections/Schema/templateSchema.ts
  25. 11 0
      src/lib/permissions.ts

+ 103 - 0
prisma/migrations/20260220155420_dvi_inspection/migration.sql

@@ -0,0 +1,103 @@
+-- CreateTable
+CREATE TABLE "inspection_templates" (
+    "id" TEXT NOT NULL,
+    "name" TEXT NOT NULL,
+    "description" TEXT,
+    "isDefault" BOOLEAN NOT NULL DEFAULT false,
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+    "organizationId" TEXT NOT NULL,
+
+    CONSTRAINT "inspection_templates_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "inspection_template_sections" (
+    "id" TEXT NOT NULL,
+    "name" TEXT NOT NULL,
+    "sortOrder" INTEGER NOT NULL DEFAULT 0,
+    "templateId" TEXT NOT NULL,
+
+    CONSTRAINT "inspection_template_sections_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "inspection_template_items" (
+    "id" TEXT NOT NULL,
+    "name" TEXT NOT NULL,
+    "sortOrder" INTEGER NOT NULL DEFAULT 0,
+    "sectionId" TEXT NOT NULL,
+
+    CONSTRAINT "inspection_template_items_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "inspections" (
+    "id" TEXT NOT NULL,
+    "status" TEXT NOT NULL DEFAULT 'in_progress',
+    "mileage" INTEGER,
+    "notes" TEXT,
+    "publicToken" TEXT,
+    "completedAt" TIMESTAMP(3),
+    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    "updatedAt" TIMESTAMP(3) NOT NULL,
+    "vehicleId" TEXT NOT NULL,
+    "templateId" TEXT NOT NULL,
+    "technicianId" TEXT NOT NULL,
+    "organizationId" TEXT NOT NULL,
+
+    CONSTRAINT "inspections_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "inspection_items" (
+    "id" TEXT NOT NULL,
+    "name" TEXT NOT NULL,
+    "section" TEXT NOT NULL,
+    "sortOrder" INTEGER NOT NULL DEFAULT 0,
+    "condition" TEXT NOT NULL DEFAULT 'not_inspected',
+    "notes" TEXT,
+    "imageUrl" TEXT,
+    "inspectionId" TEXT NOT NULL,
+
+    CONSTRAINT "inspection_items_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE INDEX "inspection_templates_organizationId_idx" ON "inspection_templates"("organizationId");
+
+-- CreateIndex
+CREATE INDEX "inspection_template_sections_templateId_idx" ON "inspection_template_sections"("templateId");
+
+-- CreateIndex
+CREATE INDEX "inspection_template_items_sectionId_idx" ON "inspection_template_items"("sectionId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "inspections_publicToken_key" ON "inspections"("publicToken");
+
+-- CreateIndex
+CREATE INDEX "inspections_vehicleId_idx" ON "inspections"("vehicleId");
+
+-- CreateIndex
+CREATE INDEX "inspections_organizationId_idx" ON "inspections"("organizationId");
+
+-- CreateIndex
+CREATE INDEX "inspection_items_inspectionId_idx" ON "inspection_items"("inspectionId");
+
+-- AddForeignKey
+ALTER TABLE "inspection_templates" ADD CONSTRAINT "inspection_templates_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "inspection_template_sections" ADD CONSTRAINT "inspection_template_sections_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "inspection_templates"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "inspection_template_items" ADD CONSTRAINT "inspection_template_items_sectionId_fkey" FOREIGN KEY ("sectionId") REFERENCES "inspection_template_sections"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "inspections" ADD CONSTRAINT "inspections_vehicleId_fkey" FOREIGN KEY ("vehicleId") REFERENCES "vehicles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "inspections" ADD CONSTRAINT "inspections_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "inspection_templates"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "inspection_items" ADD CONSTRAINT "inspection_items_inspectionId_fkey" FOREIGN KEY ("inspectionId") REFERENCES "inspections"("id") ON DELETE CASCADE ON UPDATE CASCADE;

+ 104 - 15
prisma/schema.prisma

@@ -108,26 +108,26 @@ model TwoFactor {
 }
 }
 
 
 model Vehicle {
 model Vehicle {
-  id            String    @id @default(cuid())
-  make          String
-  model         String
-  year          Int
-  vin           String?
-  licensePlate  String?
-  color         String?
-  mileage       Int       @default(0)
-  fuelType      String?   @default("gasoline")
-  transmission  String?   @default("automatic")
-  engineSize    String?
-  purchaseDate  DateTime?
-  purchasePrice Float?
-  imageUrl      String?
+  id                     String    @id @default(cuid())
+  make                   String
+  model                  String
+  year                   Int
+  vin                    String?
+  licensePlate           String?
+  color                  String?
+  mileage                Int       @default(0)
+  fuelType               String?   @default("gasoline")
+  transmission           String?   @default("automatic")
+  engineSize             String?
+  purchaseDate           DateTime?
+  purchasePrice          Float?
+  imageUrl               String?
   isArchived             Boolean   @default(false)
   isArchived             Boolean   @default(false)
   archiveReason          String?
   archiveReason          String?
   maintenanceDismissed   Boolean   @default(false)
   maintenanceDismissed   Boolean   @default(false)
   maintenanceDismissedAt DateTime?
   maintenanceDismissedAt DateTime?
   createdAt              DateTime  @default(now())
   createdAt              DateTime  @default(now())
-  updatedAt     DateTime  @updatedAt
+  updatedAt              DateTime  @updatedAt
 
 
   userId String
   userId String
   user   User   @relation(fields: [userId], references: [id], onDelete: Cascade)
   user   User   @relation(fields: [userId], references: [id], onDelete: Cascade)
@@ -144,6 +144,7 @@ model Vehicle {
   reminders         Reminder[]
   reminders         Reminder[]
   quotes            Quote[]
   quotes            Quote[]
   recurringInvoices RecurringInvoice[]
   recurringInvoices RecurringInvoice[]
+  inspections       Inspection[]
 
 
   @@index([organizationId])
   @@index([organizationId])
   @@map("vehicles")
   @@map("vehicles")
@@ -484,6 +485,7 @@ model Organization {
   settings               AppSetting[]
   settings               AppSetting[]
   subscription           Subscription?
   subscription           Subscription?
   invitations            TeamInvitation[]
   invitations            TeamInvitation[]
+  inspectionTemplates    InspectionTemplate[]
 
 
   @@map("organizations")
   @@map("organizations")
 }
 }
@@ -657,3 +659,90 @@ model RecurringLabor {
 
 
   @@map("recurring_labor")
   @@map("recurring_labor")
 }
 }
+
+model InspectionTemplate {
+  id          String   @id @default(cuid())
+  name        String
+  description String?
+  isDefault   Boolean  @default(false)
+  createdAt   DateTime @default(now())
+  updatedAt   DateTime @updatedAt
+
+  organizationId String
+  organization   Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
+
+  sections    InspectionTemplateSection[]
+  inspections Inspection[]
+
+  @@index([organizationId])
+  @@map("inspection_templates")
+}
+
+model InspectionTemplateSection {
+  id        String @id @default(cuid())
+  name      String
+  sortOrder Int    @default(0)
+
+  templateId String
+  template   InspectionTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade)
+
+  items InspectionTemplateItem[]
+
+  @@index([templateId])
+  @@map("inspection_template_sections")
+}
+
+model InspectionTemplateItem {
+  id        String @id @default(cuid())
+  name      String
+  sortOrder Int    @default(0)
+
+  sectionId String
+  section   InspectionTemplateSection @relation(fields: [sectionId], references: [id], onDelete: Cascade)
+
+  @@index([sectionId])
+  @@map("inspection_template_items")
+}
+
+model Inspection {
+  id          String    @id @default(cuid())
+  status      String    @default("in_progress")
+  mileage     Int?
+  notes       String?
+  publicToken String?   @unique
+  completedAt DateTime?
+  createdAt   DateTime  @default(now())
+  updatedAt   DateTime  @updatedAt
+
+  vehicleId String
+  vehicle   Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
+
+  templateId String
+  template   InspectionTemplate @relation(fields: [templateId], references: [id])
+
+  technicianId String
+
+  organizationId String
+
+  items InspectionItem[]
+
+  @@index([vehicleId])
+  @@index([organizationId])
+  @@map("inspections")
+}
+
+model InspectionItem {
+  id        String  @id @default(cuid())
+  name      String
+  section   String
+  sortOrder Int     @default(0)
+  condition String  @default("not_inspected")
+  notes     String?
+  imageUrl  String?
+
+  inspectionId String
+  inspection   Inspection @relation(fields: [inspectionId], references: [id], onDelete: Cascade)
+
+  @@index([inspectionId])
+  @@map("inspection_items")
+}

+ 35 - 0
src/app/(authenticated)/inspections/[id]/page.tsx

@@ -0,0 +1,35 @@
+import { getInspection } from "@/features/inspections/Actions/inspectionActions";
+import { InspectionPageClient } from "@/features/inspections/Components/InspectionPageClient";
+import { PageHeader } from "@/components/page-header";
+
+export default async function InspectionDetailPage({
+  params,
+}: {
+  params: Promise<{ id: string }>;
+}) {
+  const { id } = await params;
+
+  const result = await getInspection(id);
+
+  if (!result.success || !result.data) {
+    return (
+      <>
+        <PageHeader />
+        <div className="flex h-[50vh] items-center justify-center">
+          <p className="text-muted-foreground">
+            {result.error || "Inspection not found"}
+          </p>
+        </div>
+      </>
+    );
+  }
+
+  return (
+    <>
+      <PageHeader />
+      <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
+        <InspectionPageClient inspection={result.data} />
+      </div>
+    </>
+  );
+}

+ 250 - 0
src/app/(authenticated)/inspections/inspections-client.tsx

@@ -0,0 +1,250 @@
+"use client";
+
+import { useState, useCallback, useTransition } from "react";
+import { useRouter, usePathname, useSearchParams } from "next/navigation";
+import { useFormatDate } from "@/lib/use-format-date";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import {
+  Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
+} from "@/components/ui/table";
+import { DataTablePagination } from "@/components/data-table-pagination";
+import { Loader2, Plus, Search } from "lucide-react";
+import { NewInspectionDialog } from "@/features/inspections/Components/NewInspectionDialog";
+
+interface InspectionRecord {
+  id: string;
+  status: string;
+  mileage: number | null;
+  createdAt: Date;
+  completedAt: Date | null;
+  vehicle: { id: string; make: string; model: string; year: number; licensePlate: string | null };
+  template: { id: string; name: string };
+  items: { id: string; condition: string }[];
+}
+
+interface PaginatedData {
+  records: InspectionRecord[];
+  total: number;
+  page: number;
+  pageSize: number;
+  totalPages: number;
+  statusCounts: Record<string, number>;
+}
+
+interface TemplateOption {
+  id: string;
+  name: string;
+  isDefault: boolean;
+}
+
+const statusTabs = [
+  { key: "all", label: "All" },
+  { key: "in_progress", label: "In Progress" },
+  { key: "completed", label: "Completed" },
+];
+
+const statusColors: Record<string, string> = {
+  in_progress: "bg-blue-500/10 text-blue-500 border-blue-500/20",
+  completed: "bg-emerald-500/10 text-emerald-600 border-emerald-500/20",
+};
+
+const conditionColors: Record<string, string> = {
+  pass: "bg-emerald-500",
+  fail: "bg-red-500",
+  attention: "bg-amber-500",
+  not_inspected: "bg-gray-300 dark:bg-gray-600",
+};
+
+function InspectionProgress({ items }: { items: { condition: string }[] }) {
+  if (items.length === 0) return null;
+  const counts = { pass: 0, fail: 0, attention: 0, not_inspected: 0 };
+  for (const item of items) {
+    const c = item.condition as keyof typeof counts;
+    if (c in counts) counts[c]++;
+  }
+  return (
+    <div className="flex items-center gap-1.5">
+      <div className="flex h-2 w-20 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
+        {(["pass", "fail", "attention", "not_inspected"] as const).map((c) => {
+          const pct = (counts[c] / items.length) * 100;
+          if (pct === 0) return null;
+          return (
+            <div
+              key={c}
+              className={conditionColors[c]}
+              style={{ width: `${pct}%` }}
+            />
+          );
+        })}
+      </div>
+      <span className="text-xs text-muted-foreground">
+        {items.length - counts.not_inspected}/{items.length}
+      </span>
+    </div>
+  );
+}
+
+export function InspectionsClient({
+  data,
+  templates,
+  search,
+  statusFilter,
+}: {
+  data: PaginatedData;
+  templates: TemplateOption[];
+  search: string;
+  statusFilter: string;
+}) {
+  const router = useRouter();
+  const { formatDate } = useFormatDate();
+  const pathname = usePathname();
+  const searchParams = useSearchParams();
+  const [isPending, startTransition] = useTransition();
+  const [searchInput, setSearchInput] = useState(search);
+  const [showNewDialog, setShowNewDialog] = useState(false);
+
+  const navigate = useCallback(
+    (params: Record<string, string | number | undefined>) => {
+      const newParams = new URLSearchParams(searchParams.toString());
+      for (const [key, value] of Object.entries(params)) {
+        if (value === undefined || value === "") {
+          newParams.delete(key);
+        } else {
+          newParams.set(key, String(value));
+        }
+      }
+      if (!("page" in params)) newParams.delete("page");
+      startTransition(() => {
+        router.push(`${pathname}?${newParams.toString()}`);
+      });
+    },
+    [router, pathname, searchParams]
+  );
+
+  const handleSearch = useCallback(
+    (e: React.FormEvent) => {
+      e.preventDefault();
+      navigate({ search: searchInput || undefined });
+    },
+    [navigate, searchInput]
+  );
+
+  return (
+    <div className="space-y-4">
+      <div className="flex flex-wrap gap-2">
+        {statusTabs.map((tab) => {
+          const isActive = statusFilter === tab.key;
+          const count = tab.key === "all" ? undefined : data.statusCounts[tab.key] || 0;
+          return (
+            <Button
+              key={tab.key}
+              variant={isActive ? "default" : "outline"}
+              size="sm"
+              onClick={() => navigate({ status: tab.key === "all" ? undefined : tab.key })}
+            >
+              {tab.label}
+              {count !== undefined && (
+                <Badge variant="secondary" className="ml-1.5 h-5 min-w-5 px-1 text-xs">
+                  {count}
+                </Badge>
+              )}
+            </Button>
+          );
+        })}
+      </div>
+
+      <div className="flex items-center justify-between gap-2">
+        <div className="flex flex-1 items-center gap-2">
+          <form onSubmit={handleSearch} className="relative flex-1 sm:max-w-sm">
+            <Search className="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
+            <Input
+              placeholder="Search inspections..."
+              value={searchInput}
+              onChange={(e) => setSearchInput(e.target.value)}
+              className="pl-9"
+            />
+          </form>
+          {isPending && <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />}
+        </div>
+        <Button size="sm" onClick={() => setShowNewDialog(true)}>
+          <Plus className="mr-1 h-3.5 w-3.5" />
+          New Inspection
+        </Button>
+      </div>
+
+      <div className="rounded-lg border">
+        <Table>
+          <TableHeader>
+            <TableRow>
+              <TableHead>Vehicle</TableHead>
+              <TableHead className="hidden md:table-cell">Template</TableHead>
+              <TableHead className="w-32">Progress</TableHead>
+              <TableHead className="w-28">Status</TableHead>
+              <TableHead className="w-24">Date</TableHead>
+            </TableRow>
+          </TableHeader>
+          <TableBody>
+            {data.records.length === 0 ? (
+              <TableRow>
+                <TableCell colSpan={5} className="h-32 text-center text-muted-foreground">
+                  No inspections found.
+                </TableCell>
+              </TableRow>
+            ) : (
+              data.records.map((insp) => (
+                <TableRow
+                  key={insp.id}
+                  className="cursor-pointer"
+                  onClick={() => router.push(`/inspections/${insp.id}`)}
+                >
+                  <TableCell>
+                    <div>
+                      <p className="font-medium">
+                        {insp.vehicle.year} {insp.vehicle.make} {insp.vehicle.model}
+                      </p>
+                      {insp.vehicle.licensePlate && (
+                        <p className="text-xs text-muted-foreground font-mono">
+                          {insp.vehicle.licensePlate}
+                        </p>
+                      )}
+                    </div>
+                  </TableCell>
+                  <TableCell className="hidden md:table-cell text-muted-foreground">
+                    {insp.template.name}
+                  </TableCell>
+                  <TableCell>
+                    <InspectionProgress items={insp.items} />
+                  </TableCell>
+                  <TableCell>
+                    <Badge variant="outline" className={`text-xs ${statusColors[insp.status] || ""}`}>
+                      {insp.status === "in_progress" ? "In Progress" : "Completed"}
+                    </Badge>
+                  </TableCell>
+                  <TableCell className="font-mono text-xs">
+                    {formatDate(new Date(insp.createdAt))}
+                  </TableCell>
+                </TableRow>
+              ))
+            )}
+          </TableBody>
+        </Table>
+      </div>
+
+      <DataTablePagination
+        total={data.total}
+        page={data.page}
+        pageSize={data.pageSize}
+        totalPages={data.totalPages}
+        onNavigate={navigate}
+      />
+
+      <NewInspectionDialog
+        open={showNewDialog}
+        onOpenChange={setShowNewDialog}
+        templates={templates}
+      />
+    </div>
+  );
+}

+ 55 - 0
src/app/(authenticated)/inspections/page.tsx

@@ -0,0 +1,55 @@
+import { getInspectionsPaginated } from "@/features/inspections/Actions/inspectionActions";
+import { getTemplates } from "@/features/inspections/Actions/templateActions";
+import { InspectionsClient } from "./inspections-client";
+import { PageHeader } from "@/components/page-header";
+
+export default async function InspectionsPage({
+  searchParams,
+}: {
+  searchParams: Promise<{
+    page?: string;
+    pageSize?: string;
+    search?: string;
+    status?: string;
+  }>;
+}) {
+  const params = await searchParams;
+  const [result, templatesResult] = await Promise.all([
+    getInspectionsPaginated({
+      page: params.page ? parseInt(params.page) : 1,
+      pageSize: params.pageSize ? parseInt(params.pageSize) : 20,
+      search: params.search,
+      status: params.status || "all",
+    }),
+    getTemplates(),
+  ]);
+
+  if (!result.success || !result.data) {
+    return (
+      <>
+        <PageHeader />
+        <div className="flex h-[50vh] items-center justify-center">
+          <p className="text-muted-foreground">
+            {result.error || "Failed to load inspections"}
+          </p>
+        </div>
+      </>
+    );
+  }
+
+  const templates = templatesResult.success && templatesResult.data ? templatesResult.data : [];
+
+  return (
+    <>
+      <PageHeader />
+      <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
+        <InspectionsClient
+          data={result.data}
+          templates={templates}
+          search={params.search || ""}
+          statusFilter={params.status || "all"}
+        />
+      </div>
+    </>
+  );
+}

+ 1 - 1
src/app/(authenticated)/settings/settings-nav.tsx

@@ -69,7 +69,7 @@ const settingsNav: SettingsNavItem[] = [
     title: "Templates",
     title: "Templates",
     href: "/settings/templates",
     href: "/settings/templates",
     icon: Layout,
     icon: Layout,
-    description: "Invoice & quote styling",
+    description: "Invoice, quote & inspection templates",
     gate: "customTemplates",
     gate: "customTemplates",
   },
   },
   {
   {

+ 18 - 7
src/app/(authenticated)/settings/templates/page.tsx

@@ -5,6 +5,7 @@ import { getLayoutData } from "@/lib/get-layout-data";
 import { getFeatures, isCloudMode } from "@/lib/features";
 import { getFeatures, isCloudMode } from "@/lib/features";
 import { FeatureLockedMessage } from "../feature-locked-message";
 import { FeatureLockedMessage } from "../feature-locked-message";
 import { redirect } from "next/navigation";
 import { redirect } from "next/navigation";
+import { getTemplates, seedDefaultTemplate } from "@/features/inspections/Actions/templateActions";
 
 
 export default async function TemplatePage() {
 export default async function TemplatePage() {
   const data = await getLayoutData();
   const data = await getLayoutData();
@@ -24,16 +25,25 @@ export default async function TemplatePage() {
     );
     );
   }
   }
 
 
-  const result = await getSettings([
-    SETTING_KEYS.INVOICE_PRIMARY_COLOR,
-    SETTING_KEYS.INVOICE_FONT_FAMILY,
-    SETTING_KEYS.INVOICE_HEADER_STYLE,
-    SETTING_KEYS.QUOTE_PRIMARY_COLOR,
-    SETTING_KEYS.QUOTE_FONT_FAMILY,
-    SETTING_KEYS.QUOTE_HEADER_STYLE,
+  // Auto-seed default inspection template if none exist
+  await seedDefaultTemplate();
+
+  const [result, inspectionTemplatesResult] = await Promise.all([
+    getSettings([
+      SETTING_KEYS.INVOICE_PRIMARY_COLOR,
+      SETTING_KEYS.INVOICE_FONT_FAMILY,
+      SETTING_KEYS.INVOICE_HEADER_STYLE,
+      SETTING_KEYS.QUOTE_PRIMARY_COLOR,
+      SETTING_KEYS.QUOTE_FONT_FAMILY,
+      SETTING_KEYS.QUOTE_HEADER_STYLE,
+    ]),
+    getTemplates(),
   ]);
   ]);
 
 
   const settings = result.success && result.data ? result.data : {};
   const settings = result.success && result.data ? result.data : {};
+  const inspectionTemplates = inspectionTemplatesResult.success && inspectionTemplatesResult.data
+    ? inspectionTemplatesResult.data
+    : [];
 
 
   return (
   return (
     <TemplateSettings
     <TemplateSettings
@@ -47,6 +57,7 @@ export default async function TemplatePage() {
         fontFamily: settings[SETTING_KEYS.QUOTE_FONT_FAMILY] || "Helvetica",
         fontFamily: settings[SETTING_KEYS.QUOTE_FONT_FAMILY] || "Helvetica",
         headerStyle: settings[SETTING_KEYS.QUOTE_HEADER_STYLE] || "standard",
         headerStyle: settings[SETTING_KEYS.QUOTE_HEADER_STYLE] || "standard",
       }}
       }}
+      inspectionTemplates={inspectionTemplates}
     />
     />
   );
   );
 }
 }

+ 62 - 26
src/app/(authenticated)/settings/templates/template-settings.tsx

@@ -19,6 +19,7 @@ import { templatePresets } from "@/features/settings/Schema/templatePresets";
 import { Check, Loader2, Palette } from "lucide-react";
 import { Check, Loader2, Palette } from "lucide-react";
 import { ReadOnlyBanner, SaveButton, ReadOnlyWrapper } from "../read-only-guard";
 import { ReadOnlyBanner, SaveButton, ReadOnlyWrapper } from "../read-only-guard";
 import { cn } from "@/lib/utils";
 import { cn } from "@/lib/utils";
+import { TemplateListClient } from "@/features/inspections/Components/TemplateListClient";
 
 
 interface TemplateValues {
 interface TemplateValues {
   primaryColor: string;
   primaryColor: string;
@@ -26,7 +27,7 @@ interface TemplateValues {
   headerStyle: string;
   headerStyle: string;
 }
 }
 
 
-type TabType = "invoice" | "quotation";
+type TabType = "invoice" | "quotation" | "inspections";
 
 
 const fontMap: Record<string, string> = {
 const fontMap: Record<string, string> = {
   Helvetica: "Helvetica, Arial, sans-serif",
   Helvetica: "Helvetica, Arial, sans-serif",
@@ -450,12 +451,29 @@ function TemplateTab({
   );
   );
 }
 }
 
 
+interface InspectionTemplateSection {
+  id: string;
+  name: string;
+  sortOrder: number;
+  items: { id: string; name: string; sortOrder: number }[];
+}
+
+interface InspectionTemplate {
+  id: string;
+  name: string;
+  description: string | null;
+  isDefault: boolean;
+  sections: InspectionTemplateSection[];
+}
+
 export function TemplateSettings({
 export function TemplateSettings({
   initialInvoiceValues,
   initialInvoiceValues,
   initialQuoteValues,
   initialQuoteValues,
+  inspectionTemplates = [],
 }: {
 }: {
   initialInvoiceValues: TemplateValues;
   initialInvoiceValues: TemplateValues;
   initialQuoteValues: TemplateValues;
   initialQuoteValues: TemplateValues;
+  inspectionTemplates?: InspectionTemplate[];
 }) {
 }) {
   const [tab, setTab] = useState<TabType>("invoice");
   const [tab, setTab] = useState<TabType>("invoice");
   const [saving, setSaving] = useState(false);
   const [saving, setSaving] = useState(false);
@@ -521,34 +539,52 @@ export function TemplateSettings({
         >
         >
           Quotation
           Quotation
         </button>
         </button>
+        <button
+          type="button"
+          onClick={() => setTab("inspections")}
+          className={cn(
+            "flex-1 rounded-md px-4 py-2 text-sm font-medium transition-colors",
+            tab === "inspections"
+              ? "bg-background text-foreground shadow-sm"
+              : "text-muted-foreground hover:text-foreground"
+          )}
+        >
+          Inspections
+        </button>
       </div>
       </div>
 
 
-      <ReadOnlyWrapper>
-        {tab === "invoice" ? (
-          <TemplateTab
-            values={invoiceValues}
-            setValues={setInvoiceValues}
-            documentLabel="Invoice"
-            billToLabel="Bill To"
-          />
-        ) : (
-          <TemplateTab
-            values={quoteValues}
-            setValues={setQuoteValues}
-            documentLabel="Quotation"
-            billToLabel="Prepared For"
-          />
-        )}
-      </ReadOnlyWrapper>
+      {tab === "inspections" ? (
+        <TemplateListClient templates={inspectionTemplates} />
+      ) : (
+        <>
+          <ReadOnlyWrapper>
+            {tab === "invoice" ? (
+              <TemplateTab
+                values={invoiceValues}
+                setValues={setInvoiceValues}
+                documentLabel="Invoice"
+                billToLabel="Bill To"
+              />
+            ) : (
+              <TemplateTab
+                values={quoteValues}
+                setValues={setQuoteValues}
+                documentLabel="Quotation"
+                billToLabel="Prepared For"
+              />
+            )}
+          </ReadOnlyWrapper>
 
 
-      <SaveButton>
-        <div className="flex justify-end">
-          <Button onClick={handleSave} disabled={saving}>
-            {saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
-            Save {tab === "invoice" ? "Invoice" : "Quotation"} Template
-          </Button>
-        </div>
-      </SaveButton>
+          <SaveButton>
+            <div className="flex justify-end">
+              <Button onClick={handleSave} disabled={saving}>
+                {saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+                Save {tab === "invoice" ? "Invoice" : "Quotation"} Template
+              </Button>
+            </div>
+          </SaveButton>
+        </>
+      )}
     </div>
     </div>
   );
   );
 }
 }

+ 7 - 1
src/app/(authenticated)/vehicles/[id]/page.tsx

@@ -4,6 +4,8 @@ import { getCustomersList } from "@/features/customers/Actions/customerActions";
 import { getSettings } from "@/features/settings/Actions/settingsActions";
 import { getSettings } from "@/features/settings/Actions/settingsActions";
 import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
 import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
 import { getVehiclePredictedMileage } from "@/features/vehicles/Actions/predictedMaintenanceActions";
 import { getVehiclePredictedMileage } from "@/features/vehicles/Actions/predictedMaintenanceActions";
+import { getVehicleInspections } from "@/features/inspections/Actions/inspectionActions";
+import { getTemplates } from "@/features/inspections/Actions/templateActions";
 import { VehicleDetailClient } from "./vehicle-detail-client";
 import { VehicleDetailClient } from "./vehicle-detail-client";
 import { PageHeader } from "@/components/page-header";
 import { PageHeader } from "@/components/page-header";
 
 
@@ -22,7 +24,7 @@ export default async function VehicleDetailPage({
   const search = typeof sp.search === "string" ? sp.search : "";
   const search = typeof sp.search === "string" ? sp.search : "";
   const type = typeof sp.type === "string" ? sp.type : "all";
   const type = typeof sp.type === "string" ? sp.type : "all";
 
 
-  const [result, customersResult, serviceResult, settingsResult, maintenanceSettingsResult] = await Promise.all([
+  const [result, customersResult, serviceResult, settingsResult, maintenanceSettingsResult, inspectionsResult, templatesResult] = await Promise.all([
     getVehicle(id),
     getVehicle(id),
     getCustomersList(),
     getCustomersList(),
     getServiceRecordsPaginated(id, { page, pageSize, search, type }),
     getServiceRecordsPaginated(id, { page, pageSize, search, type }),
@@ -32,6 +34,8 @@ export default async function VehicleDetailPage({
       SETTING_KEYS.MAINTENANCE_SERVICE_INTERVAL,
       SETTING_KEYS.MAINTENANCE_SERVICE_INTERVAL,
       SETTING_KEYS.MAINTENANCE_APPROACHING_THRESHOLD,
       SETTING_KEYS.MAINTENANCE_APPROACHING_THRESHOLD,
     ]),
     ]),
+    getVehicleInspections(id),
+    getTemplates(),
   ]);
   ]);
 
 
   if (!result.success || !result.data) {
   if (!result.success || !result.data) {
@@ -113,6 +117,8 @@ export default async function VehicleDetailPage({
           currencyCode={currencyCode}
           currencyCode={currencyCode}
           unitSystem={unitSystem}
           unitSystem={unitSystem}
           predictionData={predictionData}
           predictionData={predictionData}
+          inspections={inspectionsResult.success && inspectionsResult.data ? inspectionsResult.data : []}
+          inspectionTemplates={templatesResult.success && templatesResult.data ? templatesResult.data : []}
         />
         />
       </div>
       </div>
     </>
     </>

+ 99 - 1
src/app/(authenticated)/vehicles/[id]/vehicle-detail-client.tsx

@@ -37,6 +37,7 @@ import {
   Bell,
   Bell,
   Car,
   Car,
   CheckCircle2,
   CheckCircle2,
+  ClipboardCheck,
   Clock,
   Clock,
   DollarSign,
   DollarSign,
   EyeOff,
   EyeOff,
@@ -54,6 +55,7 @@ import {
   Wrench,
   Wrench,
   X,
   X,
 } from 'lucide-react'
 } from 'lucide-react'
+import { NewInspectionDialog } from '@/features/inspections/Components/NewInspectionDialog'
 import {
 import {
   DropdownMenu,
   DropdownMenu,
   DropdownMenuContent,
   DropdownMenuContent,
@@ -162,6 +164,8 @@ export function VehicleDetailClient({
   currencyCode = 'USD',
   currencyCode = 'USD',
   unitSystem = 'imperial',
   unitSystem = 'imperial',
   predictionData,
   predictionData,
+  inspections,
+  inspectionTemplates,
 }: {
 }: {
   vehicle: VehicleDetail
   vehicle: VehicleDetail
   customers: CustomerOption[]
   customers: CustomerOption[]
@@ -180,6 +184,15 @@ export function VehicleDetailClient({
     maintenanceDismissed: boolean
     maintenanceDismissed: boolean
     confidencePercent: number
     confidencePercent: number
   } | null
   } | null
+  inspections?: {
+    id: string
+    status: string
+    createdAt: Date
+    completedAt: Date | null
+    template: { id: string; name: string }
+    items: { id: string; condition: string }[]
+  }[]
+  inspectionTemplates?: { id: string; name: string; isDefault: boolean }[]
 }) {
 }) {
   const distUnit = unitSystem === 'metric' ? 'km' : 'mi'
   const distUnit = unitSystem === 'metric' ? 'km' : 'mi'
   const router = useRouter()
   const router = useRouter()
@@ -195,6 +208,7 @@ export function VehicleDetailClient({
   const [showImage, setShowImage] = useState(false)
   const [showImage, setShowImage] = useState(false)
   const [showArchiveDialog, setShowArchiveDialog] = useState(false)
   const [showArchiveDialog, setShowArchiveDialog] = useState(false)
   const [showDeleteDialog, setShowDeleteDialog] = useState(false)
   const [showDeleteDialog, setShowDeleteDialog] = useState(false)
+  const [showNewInspection, setShowNewInspection] = useState(false)
   const [isDismissPending, startDismissTransition] = useTransition()
   const [isDismissPending, startDismissTransition] = useTransition()
 
 
   const handleDismissMaintenance = () => {
   const handleDismissMaintenance = () => {
@@ -540,11 +554,15 @@ export function VehicleDetailClient({
 
 
       {/* Tabs */}
       {/* Tabs */}
       <Tabs defaultValue="services" className="space-y-4">
       <Tabs defaultValue="services" className="space-y-4">
-        <TabsList className="grid w-full grid-cols-3">
+        <TabsList className="grid w-full grid-cols-4">
           <TabsTrigger value="services" className="gap-1.5">
           <TabsTrigger value="services" className="gap-1.5">
             <Wrench className="h-3.5 w-3.5" />
             <Wrench className="h-3.5 w-3.5" />
             Services
             Services
           </TabsTrigger>
           </TabsTrigger>
+          <TabsTrigger value="inspections" className="gap-1.5">
+            <ClipboardCheck className="h-3.5 w-3.5" />
+            Inspections
+          </TabsTrigger>
           <TabsTrigger value="notes" className="gap-1.5">
           <TabsTrigger value="notes" className="gap-1.5">
             <StickyNote className="h-3.5 w-3.5" />
             <StickyNote className="h-3.5 w-3.5" />
             Notes
             Notes
@@ -575,6 +593,80 @@ export function VehicleDetailClient({
           />
           />
         </TabsContent>
         </TabsContent>
 
 
+        {/* Inspections Tab */}
+        <TabsContent value="inspections" className="space-y-4">
+          <div className="flex justify-end">
+            <Button size="sm" onClick={() => setShowNewInspection(true)}>
+              <Plus className="mr-1 h-3.5 w-3.5" />
+              New Inspection
+            </Button>
+          </div>
+
+          {(!inspections || inspections.length === 0) ? (
+            <Card className="border-dashed">
+              <CardContent className="flex flex-col items-center py-12">
+                <ClipboardCheck className="mb-3 h-10 w-10 text-muted-foreground/40" />
+                <p className="text-sm text-muted-foreground">No inspections yet</p>
+              </CardContent>
+            </Card>
+          ) : (
+            <div className="rounded-lg border">
+              <Table>
+                <TableHeader>
+                  <TableRow>
+                    <TableHead>Template</TableHead>
+                    <TableHead className="w-32">Progress</TableHead>
+                    <TableHead className="w-28">Status</TableHead>
+                    <TableHead className="w-24">Date</TableHead>
+                  </TableRow>
+                </TableHeader>
+                <TableBody>
+                  {inspections.map((insp) => {
+                    const total = insp.items.length
+                    const inspected = insp.items.filter((i) => i.condition !== 'not_inspected').length
+                    const passCount = insp.items.filter((i) => i.condition === 'pass').length
+                    const failCount = insp.items.filter((i) => i.condition === 'fail').length
+                    return (
+                      <TableRow
+                        key={insp.id}
+                        className="cursor-pointer"
+                        onClick={() => router.push(`/inspections/${insp.id}`)}
+                      >
+                        <TableCell className="font-medium">{insp.template.name}</TableCell>
+                        <TableCell>
+                          <div className="flex items-center gap-1.5">
+                            <div className="flex h-2 w-16 overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
+                              {total > 0 && (
+                                <>
+                                  <div className="bg-emerald-500" style={{ width: `${(passCount / total) * 100}%` }} />
+                                  <div className="bg-red-500" style={{ width: `${(failCount / total) * 100}%` }} />
+                                  <div className="bg-amber-500" style={{ width: `${((inspected - passCount - failCount) / total) * 100}%` }} />
+                                </>
+                              )}
+                            </div>
+                            <span className="text-xs text-muted-foreground">{inspected}/{total}</span>
+                          </div>
+                        </TableCell>
+                        <TableCell>
+                          <Badge
+                            variant="outline"
+                            className={`text-xs ${insp.status === 'completed' ? 'bg-emerald-500/10 text-emerald-600 border-emerald-500/20' : 'bg-blue-500/10 text-blue-500 border-blue-500/20'}`}
+                          >
+                            {insp.status === 'completed' ? 'Completed' : 'In Progress'}
+                          </Badge>
+                        </TableCell>
+                        <TableCell className="font-mono text-xs text-muted-foreground">
+                          {formatDate(new Date(insp.createdAt))}
+                        </TableCell>
+                      </TableRow>
+                    )
+                  })}
+                </TableBody>
+              </Table>
+            </div>
+          )}
+        </TabsContent>
+
         {/* Notes Tab */}
         {/* Notes Tab */}
         <TabsContent value="notes" className="space-y-4">
         <TabsContent value="notes" className="space-y-4">
           <div className="flex justify-end">
           <div className="flex justify-end">
@@ -796,6 +888,12 @@ export function VehicleDetailClient({
         onOpenChange={setShowReminderForm}
         onOpenChange={setShowReminderForm}
         reminder={editingReminder}
         reminder={editingReminder}
       />
       />
+      <NewInspectionDialog
+        open={showNewInspection}
+        onOpenChange={setShowNewInspection}
+        templates={inspectionTemplates || []}
+        preselectedVehicleId={vehicle.id}
+      />
       <ArchiveVehicleDialog
       <ArchiveVehicleDialog
         open={showArchiveDialog}
         open={showArchiveDialog}
         onOpenChange={setShowArchiveDialog}
         onOpenChange={setShowArchiveDialog}

+ 252 - 0
src/app/(public)/share/inspection/[orgId]/[token]/inspection-view.tsx

@@ -0,0 +1,252 @@
+"use client";
+
+import { formatDate as fmtDate, DEFAULT_DATE_FORMAT } from "@/lib/format";
+import { Check, X, AlertTriangle, ClipboardCheck } from "lucide-react";
+
+interface InspectionItem {
+  id: string;
+  name: string;
+  section: string;
+  sortOrder: number;
+  condition: string;
+  notes: string | null;
+  imageUrl: string | null;
+}
+
+interface InspectionRecord {
+  id: string;
+  status: string;
+  mileage: number | null;
+  notes: string | null;
+  completedAt: Date | null;
+  createdAt: Date;
+  vehicle: {
+    make: string;
+    model: string;
+    year: number;
+    vin: string | null;
+    licensePlate: string | null;
+    mileage: number;
+    customer: { name: string; email: string | null; phone: string | null } | null;
+  };
+  template: { name: string };
+  items: InspectionItem[];
+}
+
+const conditionConfig: Record<string, { label: string; color: string; bgColor: string; icon: React.ReactNode }> = {
+  pass: {
+    label: "Pass",
+    color: "text-emerald-700",
+    bgColor: "bg-emerald-100 border-emerald-300",
+    icon: <Check className="h-4 w-4 text-emerald-600" />,
+  },
+  fail: {
+    label: "Fail",
+    color: "text-red-700",
+    bgColor: "bg-red-100 border-red-300",
+    icon: <X className="h-4 w-4 text-red-600" />,
+  },
+  attention: {
+    label: "Attention",
+    color: "text-amber-700",
+    bgColor: "bg-amber-100 border-amber-300",
+    icon: <AlertTriangle className="h-4 w-4 text-amber-600" />,
+  },
+  not_inspected: {
+    label: "Not Inspected",
+    color: "text-gray-500",
+    bgColor: "bg-gray-100 border-gray-300",
+    icon: null,
+  },
+};
+
+export function InspectionView({
+  inspection,
+  workshop,
+  logoUrl,
+  primaryColor,
+  showTorqvoiceBranding,
+  dateFormat,
+  timezone,
+}: {
+  inspection: InspectionRecord;
+  workshop: { name: string; address: string; phone: string; email: string };
+  logoUrl: string;
+  primaryColor: string;
+  showTorqvoiceBranding: boolean;
+  dateFormat?: string;
+  timezone?: string;
+}) {
+  const fmt = dateFormat || DEFAULT_DATE_FORMAT;
+  const tz = timezone || "America/New_York";
+  const formatDate = (d: Date) => fmtDate(new Date(d), fmt, tz);
+
+  // Group items by section
+  const sections: Record<string, InspectionItem[]> = {};
+  for (const item of inspection.items) {
+    if (!sections[item.section]) sections[item.section] = [];
+    sections[item.section].push(item);
+  }
+
+  const totalItems = inspection.items.length;
+  const passCount = inspection.items.filter((i) => i.condition === "pass").length;
+  const failCount = inspection.items.filter((i) => i.condition === "fail").length;
+  const attentionCount = inspection.items.filter((i) => i.condition === "attention").length;
+
+  return (
+    <div className="mx-auto max-w-3xl p-4 sm:p-8">
+      {/* Header with shop branding */}
+      <div className="mb-8 rounded-xl border p-6" style={{ borderTopColor: primaryColor, borderTopWidth: "4px" }}>
+        <div className="flex items-start justify-between gap-4">
+          <div>
+            {logoUrl && (
+              <img src={logoUrl} alt={workshop.name} className="mb-3 h-12 object-contain" />
+            )}
+            <h2 className="text-xl font-bold">{workshop.name}</h2>
+            {workshop.address && (
+              <p className="text-sm text-gray-500">{workshop.address}</p>
+            )}
+            {workshop.phone && (
+              <p className="text-sm text-gray-500">{workshop.phone}</p>
+            )}
+            {workshop.email && (
+              <p className="text-sm text-gray-500">{workshop.email}</p>
+            )}
+          </div>
+          <div className="flex items-center gap-2 text-right">
+            <ClipboardCheck className="h-6 w-6" style={{ color: primaryColor }} />
+            <div>
+              <p className="text-lg font-bold">Vehicle Inspection</p>
+              <p className="text-sm text-gray-500">{formatDate(inspection.createdAt)}</p>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      {/* Vehicle and customer info */}
+      <div className="mb-6 grid grid-cols-2 gap-6 rounded-lg border p-4">
+        <div>
+          <h3 className="mb-2 text-xs font-semibold uppercase text-gray-400">Vehicle</h3>
+          <p className="font-semibold">
+            {inspection.vehicle.year} {inspection.vehicle.make} {inspection.vehicle.model}
+          </p>
+          {inspection.vehicle.vin && (
+            <p className="text-sm text-gray-500">VIN: {inspection.vehicle.vin}</p>
+          )}
+          {inspection.vehicle.licensePlate && (
+            <p className="text-sm text-gray-500">Plate: {inspection.vehicle.licensePlate}</p>
+          )}
+          {inspection.mileage && (
+            <p className="text-sm text-gray-500">Mileage: {inspection.mileage.toLocaleString()}</p>
+          )}
+        </div>
+        {inspection.vehicle.customer && (
+          <div>
+            <h3 className="mb-2 text-xs font-semibold uppercase text-gray-400">Customer</h3>
+            <p className="font-semibold">{inspection.vehicle.customer.name}</p>
+            {inspection.vehicle.customer.email && (
+              <p className="text-sm text-gray-500">{inspection.vehicle.customer.email}</p>
+            )}
+            {inspection.vehicle.customer.phone && (
+              <p className="text-sm text-gray-500">{inspection.vehicle.customer.phone}</p>
+            )}
+          </div>
+        )}
+      </div>
+
+      {/* Summary bar */}
+      <div className="mb-6 flex items-center gap-4 rounded-lg border p-4">
+        <div className="text-sm text-gray-500">
+          <span className="font-semibold text-gray-900">{totalItems}</span> items inspected
+        </div>
+        <div className="flex items-center gap-1.5">
+          <div className="h-3 w-3 rounded-full bg-emerald-500" />
+          <span className="text-sm font-medium">{passCount} Pass</span>
+        </div>
+        <div className="flex items-center gap-1.5">
+          <div className="h-3 w-3 rounded-full bg-red-500" />
+          <span className="text-sm font-medium">{failCount} Fail</span>
+        </div>
+        <div className="flex items-center gap-1.5">
+          <div className="h-3 w-3 rounded-full bg-amber-500" />
+          <span className="text-sm font-medium">{attentionCount} Attention</span>
+        </div>
+        {/* Progress bar */}
+        <div className="ml-auto flex h-3 w-32 overflow-hidden rounded-full bg-gray-200">
+          {totalItems > 0 && (
+            <>
+              <div className="bg-emerald-500" style={{ width: `${(passCount / totalItems) * 100}%` }} />
+              <div className="bg-red-500" style={{ width: `${(failCount / totalItems) * 100}%` }} />
+              <div className="bg-amber-500" style={{ width: `${(attentionCount / totalItems) * 100}%` }} />
+            </>
+          )}
+        </div>
+      </div>
+
+      {/* Inspection sections */}
+      <div className="space-y-6">
+        {Object.entries(sections).map(([sectionName, items]) => (
+          <div key={sectionName} className="rounded-lg border overflow-hidden">
+            <div className="border-b px-4 py-3" style={{ backgroundColor: `${primaryColor}10` }}>
+              <h3 className="font-semibold">{sectionName}</h3>
+            </div>
+            <div className="divide-y">
+              {items.map((item) => {
+                const config = conditionConfig[item.condition] || conditionConfig.not_inspected;
+                return (
+                  <div key={item.id} className="px-4 py-3">
+                    <div className="flex items-center justify-between gap-2">
+                      <span className="text-sm font-medium">{item.name}</span>
+                      <span
+                        className={`inline-flex items-center gap-1 rounded-full border px-2.5 py-0.5 text-xs font-medium ${config.bgColor} ${config.color}`}
+                      >
+                        {config.icon}
+                        {config.label}
+                      </span>
+                    </div>
+                    {item.notes && (
+                      <p className="mt-1 text-sm text-gray-500">{item.notes}</p>
+                    )}
+                    {item.imageUrl && (
+                      <div className="mt-2">
+                        {/\.(mp4|webm|mov)$/i.test(item.imageUrl) ? (
+                          <video
+                            src={item.imageUrl}
+                            controls
+                            className="h-48 max-w-sm rounded-lg border"
+                          />
+                        ) : (
+                          <img
+                            src={item.imageUrl}
+                            alt={item.name}
+                            className="h-32 rounded-lg object-cover border"
+                          />
+                        )}
+                      </div>
+                    )}
+                  </div>
+                );
+              })}
+            </div>
+          </div>
+        ))}
+      </div>
+
+      {/* Footer */}
+      {inspection.notes && (
+        <div className="mt-6 rounded-lg border p-4">
+          <h3 className="mb-2 text-xs font-semibold uppercase text-gray-400">Notes</h3>
+          <p className="text-sm text-gray-600 whitespace-pre-wrap">{inspection.notes}</p>
+        </div>
+      )}
+
+      {showTorqvoiceBranding && (
+        <div className="mt-8 text-center">
+          <p className="text-xs text-gray-400">
+            Powered by TorqVoice
+          </p>
+        </div>
+      )}
+    </div>
+  );
+}

+ 100 - 0
src/app/(public)/share/inspection/[orgId]/[token]/page.tsx

@@ -0,0 +1,100 @@
+import { db } from "@/lib/db";
+import { notFound } from "next/navigation";
+import { InspectionView } from "./inspection-view";
+import { getFeatures } from "@/lib/features";
+import type { Metadata } from "next";
+
+export const revalidate = 60;
+
+export const metadata: Metadata = {
+  robots: { index: false, follow: false },
+};
+
+export default async function PublicInspectionPage({
+  params,
+}: {
+  params: Promise<{ orgId: string; token: string }>;
+}) {
+  const { orgId, token } = await params;
+
+  const inspection = await db.inspection.findFirst({
+    where: { publicToken: token, organizationId: orgId },
+    include: {
+      vehicle: {
+        select: {
+          make: true,
+          model: true,
+          year: true,
+          vin: true,
+          licensePlate: true,
+          mileage: true,
+          customer: { select: { name: true, email: true, phone: true } },
+        },
+      },
+      template: { select: { name: true } },
+      items: { orderBy: { sortOrder: "asc" } },
+    },
+  });
+
+  if (!inspection) {
+    notFound();
+  }
+
+  const [settings, org, features] = await Promise.all([
+    db.appSetting.findMany({
+      where: {
+        organizationId: orgId,
+        key: {
+          in: [
+            "workshop.address",
+            "workshop.phone",
+            "workshop.email",
+            "workshop.logo",
+            "workshop.currencyCode",
+            "workshop.dateFormat",
+            "workshop.timezone",
+            "invoice.primaryColor",
+          ],
+        },
+      },
+    }),
+    db.organization.findUnique({
+      where: { id: orgId },
+      select: { name: true },
+    }),
+    getFeatures(orgId),
+  ]);
+
+  const settingsMap: Record<string, string> = {};
+  for (const s of settings) settingsMap[s.key] = s.value;
+
+  const workshop = {
+    name: org?.name || "",
+    address: settingsMap["workshop.address"] || "",
+    phone: settingsMap["workshop.phone"] || "",
+    email: settingsMap["workshop.email"] || "",
+  };
+
+  // Rewrite logo URL for public access
+  const rawLogoUrl = settingsMap["workshop.logo"] || "";
+  let logoUrl = "";
+  if (rawLogoUrl) {
+    const match = rawLogoUrl.match(/^\/api\/files\/[^/]+\/(.+)$/);
+    if (match) logoUrl = `/api/files/public/${token}/${match[1]}`;
+    else logoUrl = rawLogoUrl;
+  }
+
+  const primaryColor = settingsMap["invoice.primaryColor"] || "#d97706";
+
+  return (
+    <InspectionView
+      inspection={inspection}
+      workshop={workshop}
+      logoUrl={logoUrl}
+      primaryColor={primaryColor}
+      showTorqvoiceBranding={!features.brandingRemoved}
+      dateFormat={settingsMap["workshop.dateFormat"] || undefined}
+      timezone={settingsMap["workshop.timezone"] || undefined}
+    />
+  );
+}

+ 8 - 0
src/app/api/files/public/[token]/[...path]/route.ts

@@ -58,6 +58,14 @@ export async function GET(
     });
     });
     if (quote?.organizationId) {
     if (quote?.organizationId) {
       orgId = quote.organizationId;
       orgId = quote.organizationId;
+    } else {
+      const inspection = await db.inspection.findFirst({
+        where: { publicToken: token },
+        select: { organizationId: true },
+      });
+      if (inspection?.organizationId) {
+        orgId = inspection.organizationId;
+      }
     }
     }
   }
   }
 
 

+ 2 - 0
src/components/app-sidebar.tsx

@@ -36,6 +36,7 @@ import {
   Car,
   Car,
   Check,
   Check,
   ChevronsUpDown,
   ChevronsUpDown,
+  ClipboardCheck,
   ClipboardList,
   ClipboardList,
   FileText,
   FileText,
   LayoutDashboard,
   LayoutDashboard,
@@ -60,6 +61,7 @@ const baseNavItems = [
   { title: 'Vehicles', url: '/vehicles', icon: Car },
   { title: 'Vehicles', url: '/vehicles', icon: Car },
   { title: 'Customers', url: '/customers', icon: Users },
   { title: 'Customers', url: '/customers', icon: Users },
   { title: 'Work Orders', url: '/work-orders', icon: ClipboardList },
   { title: 'Work Orders', url: '/work-orders', icon: ClipboardList },
+  { title: 'Inspections', url: '/inspections', icon: ClipboardCheck },
   { title: 'Calendar', url: '/calendar', icon: CalendarDays },
   { title: 'Calendar', url: '/calendar', icon: CalendarDays },
   { title: 'Quotes', url: '/quotes', icon: FileText },
   { title: 'Quotes', url: '/quotes', icon: FileText },
   { title: 'Billing', url: '/billing', icon: Receipt },
   { title: 'Billing', url: '/billing', icon: Receipt },

+ 216 - 0
src/features/inspections/Actions/inspectionActions.ts

@@ -0,0 +1,216 @@
+"use server";
+
+import { db } from "@/lib/db";
+import { withAuth } from "@/lib/with-auth";
+import { createInspectionSchema, updateInspectionItemSchema } from "../Schema/inspectionSchema";
+import { revalidatePath } from "next/cache";
+import { PermissionAction, PermissionSubject } from "@/lib/permissions";
+
+export async function getInspectionsPaginated(params: {
+  page?: number;
+  pageSize?: number;
+  search?: string;
+  status?: string;
+}) {
+  return withAuth(async ({ organizationId }) => {
+    const page = params.page || 1;
+    const pageSize = params.pageSize || 20;
+    const skip = (page - 1) * pageSize;
+
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    const where: any = { organizationId };
+
+    if (params.status && params.status !== "all") {
+      where.status = params.status;
+    }
+
+    if (params.search) {
+      where.OR = [
+        { vehicle: { make: { contains: params.search, mode: "insensitive" } } },
+        { vehicle: { model: { contains: params.search, mode: "insensitive" } } },
+        { vehicle: { licensePlate: { contains: params.search, mode: "insensitive" } } },
+        { template: { name: { contains: params.search, mode: "insensitive" } } },
+      ];
+    }
+
+    const [records, total, statusCounts] = await Promise.all([
+      db.inspection.findMany({
+        where,
+        include: {
+          vehicle: { select: { id: true, make: true, model: true, year: true, licensePlate: true } },
+          template: { select: { id: true, name: true } },
+          items: { select: { id: true, condition: true } },
+        },
+        orderBy: { createdAt: "desc" },
+        skip,
+        take: pageSize,
+      }),
+      db.inspection.count({ where }),
+      db.inspection.groupBy({
+        by: ["status"],
+        where: { organizationId },
+        _count: true,
+      }),
+    ]);
+
+    const counts: Record<string, number> = {};
+    for (const g of statusCounts) {
+      counts[g.status] = g._count;
+    }
+
+    return {
+      records,
+      total,
+      page,
+      pageSize,
+      totalPages: Math.ceil(total / pageSize),
+      statusCounts: counts,
+    };
+  }, { requiredPermissions: [{ action: PermissionAction.READ, subject: PermissionSubject.INSPECTIONS }] });
+}
+
+export async function getInspection(id: string) {
+  return withAuth(async ({ organizationId }) => {
+    const inspection = await db.inspection.findFirst({
+      where: { id, organizationId },
+      include: {
+        vehicle: {
+          select: {
+            id: true, make: true, model: true, year: true, vin: true,
+            licensePlate: true, mileage: true,
+            customer: { select: { id: true, name: true, email: true, phone: true } },
+          },
+        },
+        template: { select: { id: true, name: true } },
+        items: { orderBy: { sortOrder: "asc" } },
+      },
+    });
+    if (!inspection) throw new Error("Inspection not found");
+    return inspection;
+  }, { requiredPermissions: [{ action: PermissionAction.READ, subject: PermissionSubject.INSPECTIONS }] });
+}
+
+export async function getVehicleInspections(vehicleId: string) {
+  return withAuth(async ({ organizationId }) => {
+    const inspections = await db.inspection.findMany({
+      where: { vehicleId, organizationId },
+      include: {
+        template: { select: { id: true, name: true } },
+        items: { select: { id: true, condition: true } },
+      },
+      orderBy: { createdAt: "desc" },
+    });
+    return inspections;
+  }, { requiredPermissions: [{ action: PermissionAction.READ, subject: PermissionSubject.INSPECTIONS }] });
+}
+
+export async function createInspection(input: unknown) {
+  return withAuth(async ({ userId, organizationId }) => {
+    const data = createInspectionSchema.parse(input);
+
+    // Verify vehicle belongs to org
+    const vehicle = await db.vehicle.findFirst({
+      where: { id: data.vehicleId, organizationId },
+    });
+    if (!vehicle) throw new Error("Vehicle not found");
+
+    // Verify template belongs to org
+    const template = await db.inspectionTemplate.findFirst({
+      where: { id: data.templateId, organizationId },
+      include: {
+        sections: {
+          include: { items: { orderBy: { sortOrder: "asc" } } },
+          orderBy: { sortOrder: "asc" },
+        },
+      },
+    });
+    if (!template) throw new Error("Template not found");
+
+    const inspection = await db.$transaction(async (tx) => {
+      const created = await tx.inspection.create({
+        data: {
+          vehicleId: data.vehicleId,
+          templateId: data.templateId,
+          mileage: data.mileage,
+          technicianId: userId,
+          organizationId,
+        },
+      });
+
+      // Copy template items into inspection items
+      const items = template.sections.flatMap((section) =>
+        section.items.map((item) => ({
+          name: item.name,
+          section: section.name,
+          sortOrder: item.sortOrder,
+          inspectionId: created.id,
+        }))
+      );
+
+      if (items.length > 0) {
+        await tx.inspectionItem.createMany({ data: items });
+      }
+
+      return created;
+    });
+
+    revalidatePath("/inspections");
+    revalidatePath(`/vehicles/${data.vehicleId}`);
+    return inspection;
+  }, { requiredPermissions: [{ action: PermissionAction.CREATE, subject: PermissionSubject.INSPECTIONS }] });
+}
+
+export async function updateInspectionItem(itemId: string, input: unknown) {
+  return withAuth(async ({ organizationId }) => {
+    const data = updateInspectionItemSchema.parse(input);
+
+    const item = await db.inspectionItem.findFirst({
+      where: { id: itemId, inspection: { organizationId } },
+    });
+    if (!item) throw new Error("Inspection item not found");
+
+    const updated = await db.inspectionItem.update({
+      where: { id: itemId },
+      data: {
+        condition: data.condition,
+        notes: data.notes,
+        imageUrl: data.imageUrl,
+      },
+    });
+
+    revalidatePath(`/inspections/${item.inspectionId}`);
+    return updated;
+  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.INSPECTIONS }] });
+}
+
+export async function completeInspection(id: string) {
+  return withAuth(async ({ organizationId }) => {
+    const inspection = await db.inspection.findFirst({
+      where: { id, organizationId },
+    });
+    if (!inspection) throw new Error("Inspection not found");
+
+    await db.inspection.update({
+      where: { id },
+      data: { status: "completed", completedAt: new Date() },
+    });
+
+    revalidatePath("/inspections");
+    revalidatePath(`/inspections/${id}`);
+    revalidatePath(`/vehicles/${inspection.vehicleId}`);
+    return { success: true };
+  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.INSPECTIONS }] });
+}
+
+export async function deleteInspection(id: string) {
+  return withAuth(async ({ organizationId }) => {
+    const inspection = await db.inspection.findFirst({
+      where: { id, organizationId },
+    });
+    if (!inspection) throw new Error("Inspection not found");
+
+    await db.inspection.delete({ where: { id } });
+    revalidatePath("/inspections");
+    revalidatePath(`/vehicles/${inspection.vehicleId}`);
+  }, { requiredPermissions: [{ action: PermissionAction.DELETE, subject: PermissionSubject.INSPECTIONS }] });
+}

+ 42 - 0
src/features/inspections/Actions/inspectionShareActions.ts

@@ -0,0 +1,42 @@
+"use server";
+
+import { randomUUID } from "crypto";
+import { db } from "@/lib/db";
+import { withAuth } from "@/lib/with-auth";
+import { revalidatePath } from "next/cache";
+import { PermissionAction, PermissionSubject } from "@/lib/permissions";
+
+export async function generateInspectionPublicLink(inspectionId: string) {
+  return withAuth(async ({ organizationId }) => {
+    const inspection = await db.inspection.findFirst({
+      where: { id: inspectionId, organizationId },
+    });
+    if (!inspection) throw new Error("Inspection not found");
+
+    const token = randomUUID();
+    await db.inspection.update({
+      where: { id: inspectionId },
+      data: { publicToken: token },
+    });
+
+    revalidatePath(`/inspections/${inspectionId}`);
+    return { token, organizationId };
+  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.INSPECTIONS }] });
+}
+
+export async function revokeInspectionPublicLink(inspectionId: string) {
+  return withAuth(async ({ organizationId }) => {
+    const inspection = await db.inspection.findFirst({
+      where: { id: inspectionId, organizationId },
+    });
+    if (!inspection) throw new Error("Inspection not found");
+
+    await db.inspection.update({
+      where: { id: inspectionId },
+      data: { publicToken: null },
+    });
+
+    revalidatePath(`/inspections/${inspectionId}`);
+    return { revoked: true };
+  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.INSPECTIONS }] });
+}

+ 265 - 0
src/features/inspections/Actions/templateActions.ts

@@ -0,0 +1,265 @@
+"use server";
+
+import { db } from "@/lib/db";
+import { withAuth } from "@/lib/with-auth";
+import { createTemplateSchema, updateTemplateSchema } from "../Schema/templateSchema";
+import { revalidatePath } from "next/cache";
+import { PermissionAction, PermissionSubject } from "@/lib/permissions";
+
+export async function getTemplates() {
+  return withAuth(async ({ organizationId }) => {
+    const templates = await db.inspectionTemplate.findMany({
+      where: { organizationId },
+      include: {
+        sections: {
+          include: { items: true },
+          orderBy: { sortOrder: "asc" },
+        },
+      },
+      orderBy: { createdAt: "desc" },
+    });
+    return templates;
+  }, { requiredPermissions: [{ action: PermissionAction.READ, subject: PermissionSubject.INSPECTIONS }] });
+}
+
+export async function getTemplate(id: string) {
+  return withAuth(async ({ organizationId }) => {
+    const template = await db.inspectionTemplate.findFirst({
+      where: { id, organizationId },
+      include: {
+        sections: {
+          include: { items: { orderBy: { sortOrder: "asc" } } },
+          orderBy: { sortOrder: "asc" },
+        },
+      },
+    });
+    if (!template) throw new Error("Template not found");
+    return template;
+  }, { requiredPermissions: [{ action: PermissionAction.READ, subject: PermissionSubject.INSPECTIONS }] });
+}
+
+export async function createTemplate(input: unknown) {
+  return withAuth(async ({ organizationId }) => {
+    const data = createTemplateSchema.parse(input);
+
+    const template = await db.$transaction(async (tx) => {
+      // If setting as default, unset other defaults
+      if (data.isDefault) {
+        await tx.inspectionTemplate.updateMany({
+          where: { organizationId, isDefault: true },
+          data: { isDefault: false },
+        });
+      }
+
+      const created = await tx.inspectionTemplate.create({
+        data: {
+          name: data.name,
+          description: data.description,
+          isDefault: data.isDefault,
+          organizationId,
+          sections: {
+            create: data.sections.map((section, sIdx) => ({
+              name: section.name,
+              sortOrder: section.sortOrder ?? sIdx,
+              items: {
+                create: section.items.map((item, iIdx) => ({
+                  name: item.name,
+                  sortOrder: item.sortOrder ?? iIdx,
+                })),
+              },
+            })),
+          },
+        },
+        include: {
+          sections: { include: { items: true } },
+        },
+      });
+
+      return created;
+    });
+
+    revalidatePath("/settings/inspections");
+    return template;
+  }, { requiredPermissions: [{ action: PermissionAction.CREATE, subject: PermissionSubject.INSPECTIONS }] });
+}
+
+export async function updateTemplate(input: unknown) {
+  return withAuth(async ({ organizationId }) => {
+    const data = updateTemplateSchema.parse(input);
+
+    const existing = await db.inspectionTemplate.findFirst({
+      where: { id: data.id, organizationId },
+    });
+    if (!existing) throw new Error("Template not found");
+
+    const template = await db.$transaction(async (tx) => {
+      // If setting as default, unset other defaults
+      if (data.isDefault) {
+        await tx.inspectionTemplate.updateMany({
+          where: { organizationId, isDefault: true, id: { not: data.id } },
+          data: { isDefault: false },
+        });
+      }
+
+      // Delete existing sections (cascades to items)
+      await tx.inspectionTemplateSection.deleteMany({
+        where: { templateId: data.id },
+      });
+
+      const updated = await tx.inspectionTemplate.update({
+        where: { id: data.id },
+        data: {
+          name: data.name,
+          description: data.description,
+          isDefault: data.isDefault,
+          sections: {
+            create: data.sections.map((section, sIdx) => ({
+              name: section.name,
+              sortOrder: section.sortOrder ?? sIdx,
+              items: {
+                create: section.items.map((item, iIdx) => ({
+                  name: item.name,
+                  sortOrder: item.sortOrder ?? iIdx,
+                })),
+              },
+            })),
+          },
+        },
+        include: {
+          sections: { include: { items: true } },
+        },
+      });
+
+      return updated;
+    });
+
+    revalidatePath("/settings/inspections");
+    return template;
+  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.INSPECTIONS }] });
+}
+
+export async function deleteTemplate(id: string) {
+  return withAuth(async ({ organizationId }) => {
+    const template = await db.inspectionTemplate.findFirst({
+      where: { id, organizationId },
+    });
+    if (!template) throw new Error("Template not found");
+
+    await db.$transaction(async (tx) => {
+      // Delete inspections that reference this template (and their items via cascade)
+      await tx.inspection.deleteMany({ where: { templateId: id } });
+      await tx.inspectionTemplate.delete({ where: { id } });
+    });
+
+    revalidatePath("/settings/templates");
+    revalidatePath("/inspections");
+  }, { requiredPermissions: [{ action: PermissionAction.DELETE, subject: PermissionSubject.INSPECTIONS }] });
+}
+
+export async function seedDefaultTemplate() {
+  return withAuth(async ({ organizationId }) => {
+    const count = await db.inspectionTemplate.count({ where: { organizationId } });
+    if (count > 0) return null;
+
+    const template = await db.inspectionTemplate.create({
+      data: {
+        name: "Standard Multi-Point Inspection",
+        description: "Comprehensive vehicle inspection covering all major systems",
+        isDefault: true,
+        organizationId,
+        sections: {
+          create: [
+            {
+              name: "Exterior",
+              sortOrder: 0,
+              items: {
+                create: [
+                  { name: "Body condition", sortOrder: 0 },
+                  { name: "Paint", sortOrder: 1 },
+                  { name: "Lights", sortOrder: 2 },
+                  { name: "Windshield", sortOrder: 3 },
+                  { name: "Wipers", sortOrder: 4 },
+                  { name: "Tires", sortOrder: 5 },
+                  { name: "Wheels", sortOrder: 6 },
+                ],
+              },
+            },
+            {
+              name: "Under Hood",
+              sortOrder: 1,
+              items: {
+                create: [
+                  { name: "Engine oil", sortOrder: 0 },
+                  { name: "Coolant", sortOrder: 1 },
+                  { name: "Brake fluid", sortOrder: 2 },
+                  { name: "Power steering", sortOrder: 3 },
+                  { name: "Battery", sortOrder: 4 },
+                  { name: "Belts", sortOrder: 5 },
+                  { name: "Hoses", sortOrder: 6 },
+                  { name: "Air filter", sortOrder: 7 },
+                ],
+              },
+            },
+            {
+              name: "Under Vehicle",
+              sortOrder: 2,
+              items: {
+                create: [
+                  { name: "Exhaust", sortOrder: 0 },
+                  { name: "Suspension", sortOrder: 1 },
+                  { name: "CV joints", sortOrder: 2 },
+                  { name: "Brake lines", sortOrder: 3 },
+                ],
+              },
+            },
+            {
+              name: "Interior",
+              sortOrder: 3,
+              items: {
+                create: [
+                  { name: "Dash lights", sortOrder: 0 },
+                  { name: "Horn", sortOrder: 1 },
+                  { name: "A/C", sortOrder: 2 },
+                  { name: "Heater", sortOrder: 3 },
+                  { name: "Seat belts", sortOrder: 4 },
+                ],
+              },
+            },
+            {
+              name: "Brakes",
+              sortOrder: 4,
+              items: {
+                create: [
+                  { name: "Front pads", sortOrder: 0 },
+                  { name: "Rear pads", sortOrder: 1 },
+                  { name: "Rotors", sortOrder: 2 },
+                  { name: "Brake lines", sortOrder: 3 },
+                ],
+              },
+            },
+            {
+              name: "Tires",
+              sortOrder: 5,
+              items: {
+                create: [
+                  { name: "Tread depth (LF)", sortOrder: 0 },
+                  { name: "Tread depth (RF)", sortOrder: 1 },
+                  { name: "Tread depth (LR)", sortOrder: 2 },
+                  { name: "Tread depth (RR)", sortOrder: 3 },
+                  { name: "Tire pressure", sortOrder: 4 },
+                  { name: "Spare tire", sortOrder: 5 },
+                ],
+              },
+            },
+          ],
+        },
+      },
+      include: {
+        sections: { include: { items: true } },
+      },
+    });
+
+    revalidatePath("/settings/inspections");
+    return template;
+  }, { requiredPermissions: [{ action: PermissionAction.CREATE, subject: PermissionSubject.INSPECTIONS }] });
+}

+ 529 - 0
src/features/inspections/Components/InspectionPageClient.tsx

@@ -0,0 +1,529 @@
+"use client";
+
+import { useState, useTransition, useRef } from "react";
+import { useRouter } from "next/navigation";
+import Link from "next/link";
+import { useFormatDate } from "@/lib/use-format-date";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { Textarea } from "@/components/ui/textarea";
+import {
+  AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
+  AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle,
+} from "@/components/ui/alert-dialog";
+import {
+  DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import {
+  ArrowLeft, Camera, Check, CheckCircle2, ClipboardCheck, Loader2,
+  MoreVertical, Share2, Trash2, X,
+} from "lucide-react";
+import { toast } from "sonner";
+import { updateInspectionItem, completeInspection, deleteInspection } from "../Actions/inspectionActions";
+import { InspectionShareDialog } from "./InspectionShareDialog";
+
+type Condition = "pass" | "fail" | "attention" | "not_inspected";
+
+interface InspectionItem {
+  id: string;
+  name: string;
+  section: string;
+  sortOrder: number;
+  condition: string;
+  notes: string | null;
+  imageUrl: string | null;
+}
+
+interface InspectionData {
+  id: string;
+  status: string;
+  mileage: number | null;
+  notes: string | null;
+  publicToken: string | null;
+  completedAt: Date | null;
+  createdAt: Date;
+  organizationId: string;
+  vehicle: {
+    id: string;
+    make: string;
+    model: string;
+    year: number;
+    vin: string | null;
+    licensePlate: string | null;
+    mileage: number;
+    customer: { id: string; name: string; email: string | null; phone: string | null } | null;
+  };
+  template: { id: string; name: string };
+  items: InspectionItem[];
+}
+
+const conditionConfig: Record<Condition, { label: string; color: string; bgColor: string; icon: React.ReactNode }> = {
+  pass: {
+    label: "Pass",
+    color: "text-emerald-600",
+    bgColor: "bg-emerald-500 hover:bg-emerald-600",
+    icon: <Check className="h-3.5 w-3.5" />,
+  },
+  fail: {
+    label: "Fail",
+    color: "text-red-600",
+    bgColor: "bg-red-500 hover:bg-red-600",
+    icon: <X className="h-3.5 w-3.5" />,
+  },
+  attention: {
+    label: "Attention",
+    color: "text-amber-600",
+    bgColor: "bg-amber-500 hover:bg-amber-600",
+    icon: <span className="text-xs font-bold">!</span>,
+  },
+  not_inspected: {
+    label: "N/A",
+    color: "text-gray-400",
+    bgColor: "bg-gray-300 hover:bg-gray-400 dark:bg-gray-600 dark:hover:bg-gray-500",
+    icon: null,
+  },
+};
+
+function ConditionButton({
+  condition,
+  active,
+  onClick,
+  disabled,
+}: {
+  condition: Condition;
+  active: boolean;
+  onClick: () => void;
+  disabled?: boolean;
+}) {
+  const config = conditionConfig[condition];
+  return (
+    <button
+      type="button"
+      onClick={onClick}
+      disabled={disabled}
+      className={`flex h-8 w-8 items-center justify-center rounded-full text-white transition-all ${
+        active
+          ? `${config.bgColor} ring-2 ring-offset-2 ring-offset-background`
+          : "bg-gray-200 dark:bg-gray-700 text-gray-500 hover:opacity-80"
+      } ${active ? `ring-${condition === "pass" ? "emerald" : condition === "fail" ? "red" : condition === "attention" ? "amber" : "gray"}-500/50` : ""} disabled:opacity-50`}
+      title={config.label}
+    >
+      {config.icon}
+    </button>
+  );
+}
+
+function InspectionItemRow({
+  item,
+  isCompleted,
+}: {
+  item: InspectionItem;
+  isCompleted: boolean;
+}) {
+  const [condition, setCondition] = useState(item.condition as Condition);
+  const [notes, setNotes] = useState(item.notes || "");
+  const [imageUrl, setImageUrl] = useState(item.imageUrl || "");
+  const [showNotes, setShowNotes] = useState(!!item.notes);
+  const [isSaving, startSaving] = useTransition();
+  const [isUploading, setIsUploading] = useState(false);
+  const fileInputRef = useRef<HTMLInputElement>(null);
+
+  const handleConditionChange = (newCondition: Condition) => {
+    if (isCompleted) return;
+    setCondition(newCondition);
+    startSaving(async () => {
+      const result = await updateInspectionItem(item.id, {
+        condition: newCondition,
+        notes: notes || undefined,
+        imageUrl: imageUrl || undefined,
+      });
+      if (!result.success) {
+        setCondition(item.condition as Condition);
+        toast.error("Failed to update item");
+      } else {
+        toast.success("Saved");
+      }
+    });
+  };
+
+  const handleNotesBlur = () => {
+    if (notes === (item.notes || "") || isCompleted) return;
+    startSaving(async () => {
+      const result = await updateInspectionItem(item.id, {
+        condition,
+        notes: notes || undefined,
+        imageUrl: imageUrl || undefined,
+      });
+      if (result.success) {
+        toast.success("Notes saved");
+      } else {
+        toast.error("Failed to save notes");
+      }
+    });
+  };
+
+  const handleMediaUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
+    const file = e.target.files?.[0];
+    if (!file || isCompleted) return;
+
+    setIsUploading(true);
+    const formData = new FormData();
+    formData.append("file", file);
+
+    try {
+      const res = await fetch("/api/upload/service-files", {
+        method: "POST",
+        body: formData,
+      });
+      const data = await res.json();
+      if (data.url) {
+        setImageUrl(data.url);
+        await updateInspectionItem(item.id, {
+          condition,
+          notes: notes || undefined,
+          imageUrl: data.url,
+        });
+        toast.success("File uploaded");
+      } else {
+        toast.error(data.error || "Upload failed");
+      }
+    } catch {
+      toast.error("Upload failed");
+    } finally {
+      setIsUploading(false);
+    }
+  };
+
+  const isVideo = (url: string) => /\.(mp4|webm|mov)$/i.test(url);
+
+  const conditionBadgeColor = conditionConfig[condition]?.color || "text-gray-400";
+
+  return (
+    <div className="space-y-2 rounded-lg border p-3">
+      <div className="flex items-center justify-between gap-2">
+        <div className="flex items-center gap-2 min-w-0">
+          <span className={`text-sm font-medium ${conditionBadgeColor}`}>
+            {item.name}
+          </span>
+          {isSaving && <Loader2 className="h-3 w-3 animate-spin text-muted-foreground" />}
+        </div>
+        <div className="flex items-center gap-1.5 shrink-0">
+          {(["pass", "attention", "fail"] as Condition[]).map((c) => (
+            <ConditionButton
+              key={c}
+              condition={c}
+              active={condition === c}
+              onClick={() => handleConditionChange(c)}
+              disabled={isCompleted}
+            />
+          ))}
+          <button
+            type="button"
+            onClick={() => setShowNotes(!showNotes)}
+            className="ml-1 text-xs text-muted-foreground hover:text-foreground"
+          >
+            {showNotes ? "Hide" : "Notes"}
+          </button>
+          <button
+            type="button"
+            onClick={() => fileInputRef.current?.click()}
+            disabled={isCompleted || isUploading}
+            className="ml-1 text-muted-foreground hover:text-foreground disabled:opacity-50"
+          >
+            {isUploading ? (
+              <Loader2 className="h-4 w-4 animate-spin" />
+            ) : (
+              <Camera className="h-4 w-4" />
+            )}
+          </button>
+          <input
+            ref={fileInputRef}
+            type="file"
+            accept="image/*,video/mp4,video/webm,video/quicktime"
+            className="hidden"
+            onChange={handleMediaUpload}
+          />
+        </div>
+      </div>
+
+      {showNotes && (
+        <Textarea
+          value={notes}
+          onChange={(e) => setNotes(e.target.value)}
+          onBlur={handleNotesBlur}
+          placeholder="Add notes..."
+          className="text-sm min-h-[60px]"
+          disabled={isCompleted}
+        />
+      )}
+
+      {imageUrl && (
+        <div className="mt-2">
+          {isVideo(imageUrl) ? (
+            <video
+              src={imageUrl}
+              controls
+              className="h-32 max-w-xs rounded-lg border"
+            />
+          ) : (
+            <img
+              src={imageUrl}
+              alt={item.name}
+              className="h-20 w-20 rounded-lg object-cover border"
+            />
+          )}
+        </div>
+      )}
+    </div>
+  );
+}
+
+export function InspectionPageClient({
+  inspection,
+}: {
+  inspection: InspectionData;
+}) {
+  const router = useRouter();
+  const { formatDate } = useFormatDate();
+  const [isPending, startTransition] = useTransition();
+  const [showCompleteDialog, setShowCompleteDialog] = useState(false);
+  const [showDeleteDialog, setShowDeleteDialog] = useState(false);
+  const [showShareDialog, setShowShareDialog] = useState(false);
+
+  const isCompleted = inspection.status === "completed";
+
+  // Group items by section
+  const sections: Record<string, InspectionItem[]> = {};
+  for (const item of inspection.items) {
+    if (!sections[item.section]) sections[item.section] = [];
+    sections[item.section].push(item);
+  }
+
+  const totalItems = inspection.items.length;
+  const inspectedItems = inspection.items.filter((i) => i.condition !== "not_inspected").length;
+  const passCount = inspection.items.filter((i) => i.condition === "pass").length;
+  const failCount = inspection.items.filter((i) => i.condition === "fail").length;
+  const attentionCount = inspection.items.filter((i) => i.condition === "attention").length;
+
+  const handleComplete = () => {
+    startTransition(async () => {
+      const result = await completeInspection(inspection.id);
+      if (result.success) {
+        toast.success("Inspection completed");
+        setShowCompleteDialog(false);
+        router.refresh();
+      } else {
+        toast.error(result.error || "Failed to complete inspection");
+      }
+    });
+  };
+
+  const handleDelete = () => {
+    startTransition(async () => {
+      const result = await deleteInspection(inspection.id);
+      if (result.success) {
+        toast.success("Inspection deleted");
+        router.push("/inspections");
+      } else {
+        toast.error(result.error || "Failed to delete inspection");
+      }
+    });
+  };
+
+  return (
+    <div className="space-y-4">
+      {/* Header */}
+      <div className="space-y-3">
+        <div className="flex items-center justify-between">
+          <Link
+            href="/inspections"
+            className="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors"
+          >
+            <ArrowLeft className="h-4 w-4" />
+            Back to inspections
+          </Link>
+          <div className="flex items-center gap-2">
+            {!isCompleted && (
+              <Button size="sm" onClick={() => setShowCompleteDialog(true)}>
+                <CheckCircle2 className="mr-1 h-3.5 w-3.5" />
+                Complete Inspection
+              </Button>
+            )}
+            <Button variant="outline" size="sm" onClick={() => setShowShareDialog(true)}>
+              <Share2 className="mr-1 h-3.5 w-3.5" />
+              Share
+            </Button>
+            <DropdownMenu>
+              <DropdownMenuTrigger asChild>
+                <Button variant="ghost" size="icon" className="h-8 w-8">
+                  <MoreVertical className="h-4 w-4" />
+                </Button>
+              </DropdownMenuTrigger>
+              <DropdownMenuContent align="end">
+                <DropdownMenuSeparator />
+                <DropdownMenuItem
+                  className="text-destructive"
+                  onClick={() => setShowDeleteDialog(true)}
+                >
+                  <Trash2 className="mr-2 h-4 w-4" />
+                  Delete
+                </DropdownMenuItem>
+              </DropdownMenuContent>
+            </DropdownMenu>
+          </div>
+        </div>
+
+        <div className="flex items-center gap-3">
+          <div className="flex h-11 w-11 items-center justify-center rounded-xl bg-primary/10">
+            <ClipboardCheck className="h-5 w-5 text-primary" />
+          </div>
+          <div className="min-w-0 flex-1">
+            <div className="flex items-baseline gap-2">
+              <h1 className="text-lg font-bold leading-tight">
+                {inspection.vehicle.year} {inspection.vehicle.make} {inspection.vehicle.model}
+              </h1>
+              <Badge
+                variant="outline"
+                className={`text-xs ${isCompleted ? "bg-emerald-500/10 text-emerald-600 border-emerald-500/20" : "bg-blue-500/10 text-blue-500 border-blue-500/20"}`}
+              >
+                {isCompleted ? "Completed" : "In Progress"}
+              </Badge>
+            </div>
+            <div className="mt-0.5 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
+              <span>{inspection.template.name}</span>
+              <span>&middot;</span>
+              <span>{formatDate(new Date(inspection.createdAt))}</span>
+              {inspection.mileage && (
+                <>
+                  <span>&middot;</span>
+                  <span>{inspection.mileage.toLocaleString()} mi</span>
+                </>
+              )}
+              {inspection.vehicle.licensePlate && (
+                <>
+                  <span>&middot;</span>
+                  <span className="font-mono">{inspection.vehicle.licensePlate}</span>
+                </>
+              )}
+              {inspection.vehicle.customer && (
+                <>
+                  <span>&middot;</span>
+                  <Link href={`/customers/${inspection.vehicle.customer.id}`} className="hover:underline">
+                    {inspection.vehicle.customer.name}
+                  </Link>
+                </>
+              )}
+            </div>
+          </div>
+        </div>
+
+        {/* Summary stats */}
+        <div className="flex items-center gap-4 text-sm">
+          <div className="flex items-center gap-1.5 text-muted-foreground">
+            <span className="font-semibold text-foreground">{inspectedItems}/{totalItems}</span>
+            <span className="text-xs">inspected</span>
+          </div>
+          <div className="flex items-center gap-1.5">
+            <div className="h-3 w-3 rounded-full bg-emerald-500" />
+            <span className="text-xs font-medium">{passCount} pass</span>
+          </div>
+          <div className="flex items-center gap-1.5">
+            <div className="h-3 w-3 rounded-full bg-red-500" />
+            <span className="text-xs font-medium">{failCount} fail</span>
+          </div>
+          <div className="flex items-center gap-1.5">
+            <div className="h-3 w-3 rounded-full bg-amber-500" />
+            <span className="text-xs font-medium">{attentionCount} attention</span>
+          </div>
+        </div>
+      </div>
+
+      {/* Inspection sections */}
+      <div className="space-y-4">
+        {Object.entries(sections).map(([sectionName, items]) => {
+          const sectionPass = items.filter((i) => i.condition === "pass").length;
+          const sectionTotal = items.length;
+          return (
+            <Card key={sectionName}>
+              <CardHeader className="pb-3">
+                <div className="flex items-center justify-between">
+                  <CardTitle className="text-base">{sectionName}</CardTitle>
+                  <span className="text-xs text-muted-foreground">
+                    {sectionPass}/{sectionTotal} pass
+                  </span>
+                </div>
+              </CardHeader>
+              <CardContent className="space-y-2">
+                {items.map((item) => (
+                  <InspectionItemRow
+                    key={item.id}
+                    item={item}
+                    isCompleted={isCompleted}
+                  />
+                ))}
+              </CardContent>
+            </Card>
+          );
+        })}
+      </div>
+
+      {/* Complete confirmation dialog */}
+      <AlertDialog open={showCompleteDialog} onOpenChange={setShowCompleteDialog}>
+        <AlertDialogContent>
+          <AlertDialogHeader>
+            <AlertDialogTitle>Complete inspection?</AlertDialogTitle>
+            <AlertDialogDescription>
+              This will mark the inspection as completed. You won&apos;t be able to modify
+              items after completing.
+              {totalItems - inspectedItems > 0 && (
+                <span className="block mt-2 font-medium text-amber-600">
+                  {totalItems - inspectedItems} item(s) have not been inspected yet.
+                </span>
+              )}
+            </AlertDialogDescription>
+          </AlertDialogHeader>
+          <AlertDialogFooter>
+            <AlertDialogCancel>Cancel</AlertDialogCancel>
+            <AlertDialogAction onClick={handleComplete} disabled={isPending}>
+              {isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+              Complete
+            </AlertDialogAction>
+          </AlertDialogFooter>
+        </AlertDialogContent>
+      </AlertDialog>
+
+      {/* Delete confirmation dialog */}
+      <AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
+        <AlertDialogContent>
+          <AlertDialogHeader>
+            <AlertDialogTitle>Delete inspection?</AlertDialogTitle>
+            <AlertDialogDescription>
+              This will permanently delete this inspection and all its data. This action cannot be undone.
+            </AlertDialogDescription>
+          </AlertDialogHeader>
+          <AlertDialogFooter>
+            <AlertDialogCancel>Cancel</AlertDialogCancel>
+            <AlertDialogAction
+              onClick={handleDelete}
+              disabled={isPending}
+              className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
+            >
+              {isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+              Delete
+            </AlertDialogAction>
+          </AlertDialogFooter>
+        </AlertDialogContent>
+      </AlertDialog>
+
+      {/* Share dialog */}
+      <InspectionShareDialog
+        open={showShareDialog}
+        onOpenChange={setShowShareDialog}
+        inspectionId={inspection.id}
+        organizationId={inspection.organizationId}
+        publicToken={inspection.publicToken}
+      />
+    </div>
+  );
+}

+ 124 - 0
src/features/inspections/Components/InspectionShareDialog.tsx

@@ -0,0 +1,124 @@
+"use client";
+
+import { useState, useTransition } from "react";
+import { useRouter } from "next/navigation";
+import {
+  Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription,
+} from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Check, Copy, Link2, Loader2, Unlink } from "lucide-react";
+import { toast } from "sonner";
+import {
+  generateInspectionPublicLink,
+  revokeInspectionPublicLink,
+} from "../Actions/inspectionShareActions";
+
+export function InspectionShareDialog({
+  open,
+  onOpenChange,
+  inspectionId,
+  organizationId,
+  publicToken,
+}: {
+  open: boolean;
+  onOpenChange: (open: boolean) => void;
+  inspectionId: string;
+  organizationId: string;
+  publicToken: string | null;
+}) {
+  const router = useRouter();
+  const [isPending, startTransition] = useTransition();
+  const [copied, setCopied] = useState(false);
+  const [token, setToken] = useState(publicToken);
+
+  const shareUrl = token
+    ? `${typeof window !== "undefined" ? window.location.origin : ""}/share/inspection/${organizationId}/${token}`
+    : null;
+
+  const handleGenerate = () => {
+    startTransition(async () => {
+      const result = await generateInspectionPublicLink(inspectionId);
+      if (result.success && result.data) {
+        setToken(result.data.token);
+        toast.success("Share link generated");
+        router.refresh();
+      } else {
+        toast.error(result.error || "Failed to generate link");
+      }
+    });
+  };
+
+  const handleRevoke = () => {
+    startTransition(async () => {
+      const result = await revokeInspectionPublicLink(inspectionId);
+      if (result.success) {
+        setToken(null);
+        toast.success("Share link revoked");
+        router.refresh();
+      } else {
+        toast.error(result.error || "Failed to revoke link");
+      }
+    });
+  };
+
+  const handleCopy = async () => {
+    if (!shareUrl) return;
+    await navigator.clipboard.writeText(shareUrl);
+    setCopied(true);
+    toast.success("Link copied to clipboard");
+    setTimeout(() => setCopied(false), 2000);
+  };
+
+  return (
+    <Dialog open={open} onOpenChange={onOpenChange}>
+      <DialogContent className="sm:max-w-md">
+        <DialogHeader>
+          <DialogTitle>Share Inspection</DialogTitle>
+          <DialogDescription>
+            Share a read-only view of this inspection with your customer.
+          </DialogDescription>
+        </DialogHeader>
+
+        <div className="space-y-4">
+          {shareUrl ? (
+            <>
+              <div className="flex items-center gap-2">
+                <Input value={shareUrl} readOnly className="font-mono text-xs" />
+                <Button size="icon" variant="outline" onClick={handleCopy}>
+                  {copied ? (
+                    <Check className="h-4 w-4 text-emerald-500" />
+                  ) : (
+                    <Copy className="h-4 w-4" />
+                  )}
+                </Button>
+              </div>
+              <Button
+                variant="outline"
+                className="w-full text-destructive"
+                onClick={handleRevoke}
+                disabled={isPending}
+              >
+                {isPending ? (
+                  <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+                ) : (
+                  <Unlink className="mr-2 h-4 w-4" />
+                )}
+                Revoke Link
+              </Button>
+            </>
+          ) : (
+            <Button className="w-full" onClick={handleGenerate} disabled={isPending}>
+              {isPending ? (
+                <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+              ) : (
+                <Link2 className="mr-2 h-4 w-4" />
+              )}
+              Generate Share Link
+            </Button>
+          )}
+        </div>
+      </DialogContent>
+    </Dialog>
+  );
+}

+ 171 - 0
src/features/inspections/Components/NewInspectionDialog.tsx

@@ -0,0 +1,171 @@
+"use client";
+
+import { useState, useEffect, useTransition } from "react";
+import { useRouter } from "next/navigation";
+import {
+  Dialog, DialogContent, DialogHeader, DialogTitle,
+} from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+  Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
+} from "@/components/ui/select";
+import { Loader2 } from "lucide-react";
+import { toast } from "sonner";
+import { createInspection } from "../Actions/inspectionActions";
+import { getVehicles } from "@/features/vehicles/Actions/vehicleActions";
+
+interface TemplateOption {
+  id: string;
+  name: string;
+  isDefault: boolean;
+}
+
+interface VehicleOption {
+  id: string;
+  make: string;
+  model: string;
+  year: number;
+  licensePlate: string | null;
+}
+
+export function NewInspectionDialog({
+  open,
+  onOpenChange,
+  templates,
+  preselectedVehicleId,
+}: {
+  open: boolean;
+  onOpenChange: (open: boolean) => void;
+  templates: TemplateOption[];
+  preselectedVehicleId?: string;
+}) {
+  const router = useRouter();
+  const [isPending, startTransition] = useTransition();
+  const [vehicles, setVehicles] = useState<VehicleOption[]>([]);
+  const [loadingVehicles, setLoadingVehicles] = useState(false);
+
+  const defaultTemplate = templates.find((t) => t.isDefault);
+  const [vehicleId, setVehicleId] = useState(preselectedVehicleId || "");
+  const [templateId, setTemplateId] = useState(defaultTemplate?.id || "");
+  const [mileage, setMileage] = useState("");
+
+  useEffect(() => {
+    if (open && vehicles.length === 0) {
+      setLoadingVehicles(true);
+      getVehicles().then((result) => {
+        if (result.success && result.data) {
+          setVehicles(result.data.map((v) => ({
+            id: v.id,
+            make: v.make,
+            model: v.model,
+            year: v.year,
+            licensePlate: v.licensePlate,
+          })));
+        }
+        setLoadingVehicles(false);
+      });
+    }
+  }, [open, vehicles.length]);
+
+  useEffect(() => {
+    if (open) {
+      setVehicleId(preselectedVehicleId || "");
+      setTemplateId(defaultTemplate?.id || "");
+      setMileage("");
+    }
+  }, [open, preselectedVehicleId, defaultTemplate?.id]);
+
+  const handleSubmit = (e: React.FormEvent) => {
+    e.preventDefault();
+    startTransition(async () => {
+      const result = await createInspection({
+        vehicleId,
+        templateId,
+        mileage: mileage ? parseInt(mileage, 10) : undefined,
+      });
+
+      if (result.success && result.data) {
+        toast.success("Inspection created");
+        onOpenChange(false);
+        router.push(`/inspections/${result.data.id}`);
+      } else {
+        toast.error(result.error || "Failed to create inspection");
+      }
+    });
+  };
+
+  return (
+    <Dialog open={open} onOpenChange={onOpenChange}>
+      <DialogContent className="sm:max-w-md">
+        <DialogHeader>
+          <DialogTitle>New Inspection</DialogTitle>
+        </DialogHeader>
+        <form onSubmit={handleSubmit} className="space-y-4">
+          <div className="space-y-2">
+            <Label>Vehicle</Label>
+            {loadingVehicles ? (
+              <div className="flex items-center gap-2 text-sm text-muted-foreground">
+                <Loader2 className="h-4 w-4 animate-spin" />
+                Loading vehicles...
+              </div>
+            ) : (
+              <Select value={vehicleId} onValueChange={setVehicleId} required>
+                <SelectTrigger>
+                  <SelectValue placeholder="Select vehicle" />
+                </SelectTrigger>
+                <SelectContent>
+                  {vehicles.map((v) => (
+                    <SelectItem key={v.id} value={v.id}>
+                      {v.year} {v.make} {v.model}
+                      {v.licensePlate ? ` (${v.licensePlate})` : ""}
+                    </SelectItem>
+                  ))}
+                </SelectContent>
+              </Select>
+            )}
+          </div>
+
+          <div className="space-y-2">
+            <Label>Template</Label>
+            <Select value={templateId} onValueChange={setTemplateId} required>
+              <SelectTrigger>
+                <SelectValue placeholder="Select template" />
+              </SelectTrigger>
+              <SelectContent>
+                {templates.map((t) => (
+                  <SelectItem key={t.id} value={t.id}>
+                    {t.name}
+                    {t.isDefault ? " (Default)" : ""}
+                  </SelectItem>
+                ))}
+              </SelectContent>
+            </Select>
+          </div>
+
+          <div className="space-y-2">
+            <Label>Mileage (optional)</Label>
+            <Input
+              type="number"
+              value={mileage}
+              onChange={(e) => setMileage(e.target.value)}
+              placeholder="Current mileage"
+              min={0}
+            />
+          </div>
+
+          <div className="flex justify-end gap-2">
+            <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
+              Cancel
+            </Button>
+            <Button type="submit" disabled={isPending || !vehicleId || !templateId}>
+              {isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+              Start Inspection
+            </Button>
+          </div>
+        </form>
+      </DialogContent>
+    </Dialog>
+  );
+}

+ 265 - 0
src/features/inspections/Components/TemplateForm.tsx

@@ -0,0 +1,265 @@
+"use client";
+
+import { useState, useTransition } from "react";
+import { useRouter } from "next/navigation";
+import {
+  Sheet, SheetContent, SheetHeader, SheetTitle,
+} from "@/components/ui/sheet";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Switch } from "@/components/ui/switch";
+import { Loader2, Plus, Trash2, GripVertical } from "lucide-react";
+import { toast } from "sonner";
+import { createTemplate, updateTemplate } from "../Actions/templateActions";
+
+interface TemplateSection {
+  id?: string;
+  name: string;
+  sortOrder: number;
+  items: { id?: string; name: string; sortOrder: number }[];
+}
+
+interface TemplateData {
+  id: string;
+  name: string;
+  description: string | null;
+  isDefault: boolean;
+  sections: TemplateSection[];
+}
+
+export function TemplateForm({
+  open,
+  onOpenChange,
+  template,
+}: {
+  open: boolean;
+  onOpenChange: (open: boolean) => void;
+  template?: TemplateData;
+}) {
+  const router = useRouter();
+  const [isPending, startTransition] = useTransition();
+  const isEdit = !!template;
+
+  const [name, setName] = useState(template?.name || "");
+  const [description, setDescription] = useState(template?.description || "");
+  const [isDefault, setIsDefault] = useState(template?.isDefault || false);
+  const [sections, setSections] = useState<TemplateSection[]>(
+    template?.sections.map((s, sIdx) => ({
+      name: s.name,
+      sortOrder: s.sortOrder ?? sIdx,
+      items: s.items.map((i, iIdx) => ({ name: i.name, sortOrder: i.sortOrder ?? iIdx })),
+    })) || [{ name: "", sortOrder: 0, items: [{ name: "", sortOrder: 0 }] }]
+  );
+
+  // Reset form when template changes
+  const resetForm = () => {
+    setName(template?.name || "");
+    setDescription(template?.description || "");
+    setIsDefault(template?.isDefault || false);
+    setSections(
+      template?.sections.map((s, sIdx) => ({
+        name: s.name,
+        sortOrder: s.sortOrder ?? sIdx,
+        items: s.items.map((i, iIdx) => ({ name: i.name, sortOrder: i.sortOrder ?? iIdx })),
+      })) || [{ name: "", sortOrder: 0, items: [{ name: "", sortOrder: 0 }] }]
+    );
+  };
+
+  const handleOpenChange = (open: boolean) => {
+    if (open) resetForm();
+    onOpenChange(open);
+  };
+
+  const addSection = () => {
+    setSections([...sections, { name: "", sortOrder: sections.length, items: [{ name: "", sortOrder: 0 }] }]);
+  };
+
+  const removeSection = (idx: number) => {
+    setSections(sections.filter((_, i) => i !== idx));
+  };
+
+  const updateSectionName = (idx: number, val: string) => {
+    const updated = [...sections];
+    updated[idx] = { ...updated[idx], name: val };
+    setSections(updated);
+  };
+
+  const addItem = (sectionIdx: number) => {
+    const updated = [...sections];
+    updated[sectionIdx] = {
+      ...updated[sectionIdx],
+      items: [...updated[sectionIdx].items, { name: "", sortOrder: updated[sectionIdx].items.length }],
+    };
+    setSections(updated);
+  };
+
+  const removeItem = (sectionIdx: number, itemIdx: number) => {
+    const updated = [...sections];
+    updated[sectionIdx] = {
+      ...updated[sectionIdx],
+      items: updated[sectionIdx].items.filter((_, i) => i !== itemIdx),
+    };
+    setSections(updated);
+  };
+
+  const updateItemName = (sectionIdx: number, itemIdx: number, val: string) => {
+    const updated = [...sections];
+    const items = [...updated[sectionIdx].items];
+    items[itemIdx] = { ...items[itemIdx], name: val };
+    updated[sectionIdx] = { ...updated[sectionIdx], items };
+    setSections(updated);
+  };
+
+  const handleSubmit = (e: React.FormEvent) => {
+    e.preventDefault();
+
+    const payload = {
+      ...(isEdit ? { id: template!.id } : {}),
+      name,
+      description: description || undefined,
+      isDefault,
+      sections: sections.map((s, sIdx) => ({
+        name: s.name,
+        sortOrder: sIdx,
+        items: s.items.map((i, iIdx) => ({
+          name: i.name,
+          sortOrder: iIdx,
+        })),
+      })),
+    };
+
+    startTransition(async () => {
+      const result = isEdit
+        ? await updateTemplate(payload)
+        : await createTemplate(payload);
+
+      if (result.success) {
+        toast.success(isEdit ? "Template updated" : "Template created");
+        onOpenChange(false);
+        router.refresh();
+      } else {
+        toast.error(result.error || "Failed to save template");
+      }
+    });
+  };
+
+  return (
+    <Sheet open={open} onOpenChange={handleOpenChange}>
+      <SheetContent className="w-full sm:max-w-lg overflow-y-auto p-6">
+        <SheetHeader className="mb-6">
+          <SheetTitle>{isEdit ? "Edit Template" : "New Template"}</SheetTitle>
+        </SheetHeader>
+        <form onSubmit={handleSubmit} className="space-y-6">
+          <div className="space-y-2">
+            <Label htmlFor="template-name">Name</Label>
+            <Input
+              id="template-name"
+              value={name}
+              onChange={(e) => setName(e.target.value)}
+              placeholder="e.g. Standard Multi-Point Inspection"
+              required
+            />
+          </div>
+
+          <div className="space-y-2">
+            <Label htmlFor="template-desc">Description</Label>
+            <Input
+              id="template-desc"
+              value={description}
+              onChange={(e) => setDescription(e.target.value)}
+              placeholder="Optional description"
+            />
+          </div>
+
+          <div className="flex items-center gap-2">
+            <Switch id="template-default" checked={isDefault} onCheckedChange={setIsDefault} />
+            <Label htmlFor="template-default">Set as default template</Label>
+          </div>
+
+          <div className="space-y-4">
+            <div className="flex items-center justify-between">
+              <Label>Sections</Label>
+              <Button type="button" variant="outline" size="sm" onClick={addSection}>
+                <Plus className="mr-1 h-3.5 w-3.5" />
+                Add Section
+              </Button>
+            </div>
+
+            {sections.map((section, sIdx) => (
+              <div key={sIdx} className="rounded-lg border p-4 space-y-3">
+                <div className="flex items-center gap-2">
+                  <GripVertical className="h-4 w-4 text-muted-foreground shrink-0" />
+                  <Input
+                    value={section.name}
+                    onChange={(e) => updateSectionName(sIdx, e.target.value)}
+                    placeholder="Section name (e.g. Exterior)"
+                    className="font-medium"
+                    required
+                  />
+                  {sections.length > 1 && (
+                    <Button
+                      type="button"
+                      variant="ghost"
+                      size="icon"
+                      className="h-8 w-8 shrink-0 text-destructive"
+                      onClick={() => removeSection(sIdx)}
+                    >
+                      <Trash2 className="h-3.5 w-3.5" />
+                    </Button>
+                  )}
+                </div>
+
+                <div className="space-y-2 ml-6">
+                  {section.items.map((item, iIdx) => (
+                    <div key={iIdx} className="flex items-center gap-2">
+                      <GripVertical className="h-3.5 w-3.5 text-muted-foreground/50 shrink-0" />
+                      <Input
+                        value={item.name}
+                        onChange={(e) => updateItemName(sIdx, iIdx, e.target.value)}
+                        placeholder="Item name (e.g. Body condition)"
+                        className="text-sm"
+                        required
+                      />
+                      {section.items.length > 1 && (
+                        <Button
+                          type="button"
+                          variant="ghost"
+                          size="icon"
+                          className="h-7 w-7 shrink-0 text-muted-foreground hover:text-destructive"
+                          onClick={() => removeItem(sIdx, iIdx)}
+                        >
+                          <Trash2 className="h-3 w-3" />
+                        </Button>
+                      )}
+                    </div>
+                  ))}
+                  <Button
+                    type="button"
+                    variant="ghost"
+                    size="sm"
+                    className="text-xs"
+                    onClick={() => addItem(sIdx)}
+                  >
+                    <Plus className="mr-1 h-3 w-3" />
+                    Add Item
+                  </Button>
+                </div>
+              </div>
+            ))}
+          </div>
+
+          <div className="flex justify-end gap-2 pt-6">
+            <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
+              Cancel
+            </Button>
+            <Button type="submit" disabled={isPending}>
+              {isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+              {isEdit ? "Update" : "Create"} Template
+            </Button>
+          </div>
+        </form>
+      </SheetContent>
+    </Sheet>
+  );
+}

+ 184 - 0
src/features/inspections/Components/TemplateListClient.tsx

@@ -0,0 +1,184 @@
+"use client";
+
+import { useState, useTransition } from "react";
+import { useRouter } from "next/navigation";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import {
+  Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
+} from "@/components/ui/table";
+import {
+  DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import {
+  AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent,
+  AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle,
+} from "@/components/ui/alert-dialog";
+import { Loader2, MoreVertical, Pencil, Plus, Trash2 } from "lucide-react";
+import { toast } from "sonner";
+import { deleteTemplate } from "../Actions/templateActions";
+import { TemplateForm } from "./TemplateForm";
+
+interface TemplateSection {
+  id: string;
+  name: string;
+  sortOrder: number;
+  items: { id: string; name: string; sortOrder: number }[];
+}
+
+interface Template {
+  id: string;
+  name: string;
+  description: string | null;
+  isDefault: boolean;
+  sections: TemplateSection[];
+}
+
+export function TemplateListClient({ templates }: { templates: Template[] }) {
+  const router = useRouter();
+  const [showForm, setShowForm] = useState(false);
+  const [editingTemplate, setEditingTemplate] = useState<Template | undefined>();
+  const [deleteId, setDeleteId] = useState<string | null>(null);
+  const [isDeleting, startDeleteTransition] = useTransition();
+
+  const handleDelete = (id: string) => {
+    startDeleteTransition(async () => {
+      const result = await deleteTemplate(id);
+      if (result.success) {
+        toast.success("Template deleted");
+        setDeleteId(null);
+        router.refresh();
+      } else {
+        toast.error(result.error || "Failed to delete template");
+      }
+    });
+  };
+
+  const handleEdit = (template: Template) => {
+    setEditingTemplate(template);
+    setShowForm(true);
+  };
+
+  const handleCreate = () => {
+    setEditingTemplate(undefined);
+    setShowForm(true);
+  };
+
+  const totalItems = (t: Template) => t.sections.reduce((sum, s) => sum + s.items.length, 0);
+
+  return (
+    <div className="space-y-4">
+      <div className="flex items-center justify-between">
+        <div>
+          <h2 className="text-lg font-semibold">Inspection Templates</h2>
+          <p className="text-sm text-muted-foreground">
+            Define multi-point inspection checklists for technicians
+          </p>
+        </div>
+        <Button size="sm" onClick={handleCreate}>
+          <Plus className="mr-1 h-3.5 w-3.5" />
+          New Template
+        </Button>
+      </div>
+
+      <div className="rounded-lg border">
+        <Table>
+          <TableHeader>
+            <TableRow>
+              <TableHead>Name</TableHead>
+              <TableHead className="w-28">Sections</TableHead>
+              <TableHead className="w-28">Items</TableHead>
+              <TableHead className="w-28">Default</TableHead>
+              <TableHead className="w-12"></TableHead>
+            </TableRow>
+          </TableHeader>
+          <TableBody>
+            {templates.length === 0 ? (
+              <TableRow>
+                <TableCell colSpan={5} className="h-32 text-center text-muted-foreground">
+                  No templates yet. Create your first inspection template.
+                </TableCell>
+              </TableRow>
+            ) : (
+              templates.map((t) => (
+                <TableRow key={t.id}>
+                  <TableCell>
+                    <div>
+                      <p className="font-medium">{t.name}</p>
+                      {t.description && (
+                        <p className="text-xs text-muted-foreground truncate max-w-xs">
+                          {t.description}
+                        </p>
+                      )}
+                    </div>
+                  </TableCell>
+                  <TableCell>{t.sections.length}</TableCell>
+                  <TableCell>{totalItems(t)}</TableCell>
+                  <TableCell>
+                    {t.isDefault && (
+                      <Badge variant="secondary" className="text-xs">Default</Badge>
+                    )}
+                  </TableCell>
+                  <TableCell className="px-2">
+                    <DropdownMenu>
+                      <DropdownMenuTrigger asChild>
+                        <Button variant="ghost" size="icon" className="h-7 w-7">
+                          <MoreVertical className="h-3.5 w-3.5" />
+                        </Button>
+                      </DropdownMenuTrigger>
+                      <DropdownMenuContent align="end">
+                        <DropdownMenuItem onClick={() => handleEdit(t)}>
+                          <Pencil className="mr-2 h-4 w-4" />
+                          Edit
+                        </DropdownMenuItem>
+                        <DropdownMenuItem
+                          className="text-destructive"
+                          onClick={() => setDeleteId(t.id)}
+                        >
+                          <Trash2 className="mr-2 h-4 w-4" />
+                          Delete
+                        </DropdownMenuItem>
+                      </DropdownMenuContent>
+                    </DropdownMenu>
+                  </TableCell>
+                </TableRow>
+              ))
+            )}
+          </TableBody>
+        </Table>
+      </div>
+
+      <TemplateForm
+        open={showForm}
+        onOpenChange={(open) => {
+          setShowForm(open);
+          if (!open) setEditingTemplate(undefined);
+        }}
+        template={editingTemplate}
+      />
+
+      <AlertDialog open={!!deleteId} onOpenChange={(open) => !open && setDeleteId(null)}>
+        <AlertDialogContent>
+          <AlertDialogHeader>
+            <AlertDialogTitle>Delete template?</AlertDialogTitle>
+            <AlertDialogDescription>
+              This will permanently delete this inspection template. Existing inspections
+              created from this template will not be affected.
+            </AlertDialogDescription>
+          </AlertDialogHeader>
+          <AlertDialogFooter>
+            <AlertDialogCancel>Cancel</AlertDialogCancel>
+            <AlertDialogAction
+              onClick={() => deleteId && handleDelete(deleteId)}
+              disabled={isDeleting}
+              className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
+            >
+              {isDeleting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+              Delete
+            </AlertDialogAction>
+          </AlertDialogFooter>
+        </AlertDialogContent>
+      </AlertDialog>
+    </div>
+  );
+}

+ 16 - 0
src/features/inspections/Schema/inspectionSchema.ts

@@ -0,0 +1,16 @@
+import { z } from "zod";
+
+export const createInspectionSchema = z.object({
+  vehicleId: z.string().min(1, "Vehicle is required"),
+  templateId: z.string().min(1, "Template is required"),
+  mileage: z.coerce.number().int().min(0).optional(),
+});
+
+export const updateInspectionItemSchema = z.object({
+  condition: z.enum(["pass", "fail", "attention", "not_inspected"]).default("not_inspected"),
+  notes: z.string().optional(),
+  imageUrl: z.string().optional(),
+});
+
+export type CreateInspectionInput = z.infer<typeof createInspectionSchema>;
+export type UpdateInspectionItemInput = z.infer<typeof updateInspectionItemSchema>;

+ 28 - 0
src/features/inspections/Schema/templateSchema.ts

@@ -0,0 +1,28 @@
+import { z } from "zod";
+
+export const templateItemSchema = z.object({
+  name: z.string().min(1, "Item name is required"),
+  sortOrder: z.number().int().min(0).default(0),
+});
+
+export const templateSectionSchema = z.object({
+  name: z.string().min(1, "Section name is required"),
+  sortOrder: z.number().int().min(0).default(0),
+  items: z.array(templateItemSchema).min(1, "Section must have at least one item"),
+});
+
+export const createTemplateSchema = z.object({
+  name: z.string().min(1, "Template name is required"),
+  description: z.string().optional(),
+  isDefault: z.boolean().default(false),
+  sections: z.array(templateSectionSchema).min(1, "Template must have at least one section"),
+});
+
+export const updateTemplateSchema = createTemplateSchema.extend({
+  id: z.string(),
+});
+
+export type TemplateItemInput = z.infer<typeof templateItemSchema>;
+export type TemplateSectionInput = z.infer<typeof templateSectionSchema>;
+export type CreateTemplateInput = z.infer<typeof createTemplateSchema>;
+export type UpdateTemplateInput = z.infer<typeof updateTemplateSchema>;

+ 11 - 0
src/lib/permissions.ts

@@ -15,6 +15,7 @@ export enum PermissionSubject {
   SERVICES = "services",
   SERVICES = "services",
   BILLING = "billing",
   BILLING = "billing",
   INVENTORY = "inventory",
   INVENTORY = "inventory",
+  INSPECTIONS = "inspections",
   REPORTS = "reports",
   REPORTS = "reports",
   SETTINGS = "settings",
   SETTINGS = "settings",
 }
 }
@@ -111,6 +112,16 @@ export const permissionGroups: PermissionGroup[] = [
       { action: PermissionAction.DELETE, label: "Delete" },
       { action: PermissionAction.DELETE, label: "Delete" },
     ],
     ],
   },
   },
+  {
+    name: "Inspections",
+    subject: PermissionSubject.INSPECTIONS,
+    permissions: [
+      { action: PermissionAction.CREATE, label: "Create" },
+      { action: PermissionAction.READ, label: "View" },
+      { action: PermissionAction.UPDATE, label: "Edit" },
+      { action: PermissionAction.DELETE, label: "Delete" },
+    ],
+  },
   {
   {
     name: "Reports",
     name: "Reports",
     subject: PermissionSubject.REPORTS,
     subject: PermissionSubject.REPORTS,