Răsfoiți Sursa

show default template

Bernt Christian Egeland 7 luni în urmă
părinte
comite
08c67436d3

+ 6 - 0
prisma/migrations/20260220164126_dvi_inspection/migration.sql → prisma/migrations/20260220172149_dvi_inspection/migration.sql

@@ -1,3 +1,6 @@
+-- AlterTable
+ALTER TABLE "quotes" ADD COLUMN     "inspectionId" TEXT;
+
 -- CreateTable
 CREATE TABLE "inspection_templates" (
     "id" TEXT NOT NULL,
@@ -103,6 +106,9 @@ CREATE INDEX "inspection_quote_requests_organizationId_idx" ON "inspection_quote
 -- CreateIndex
 CREATE INDEX "inspection_quote_requests_inspectionId_idx" ON "inspection_quote_requests"("inspectionId");
 
+-- AddForeignKey
+ALTER TABLE "quotes" ADD CONSTRAINT "quotes_inspectionId_fkey" FOREIGN KEY ("inspectionId") REFERENCES "inspections"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
 -- AddForeignKey
 ALTER TABLE "inspection_templates" ADD CONSTRAINT "inspection_templates_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE;
 

+ 4 - 0
prisma/schema.prisma

@@ -393,6 +393,9 @@ model Quote {
   vehicleId String?
   vehicle   Vehicle? @relation(fields: [vehicleId], references: [id], onDelete: SetNull)
 
+  inspectionId String?
+  inspection   Inspection? @relation(fields: [inspectionId], references: [id], onDelete: SetNull)
+
   partItems  QuotePart[]
   laborItems QuoteLabor[]
 
@@ -726,6 +729,7 @@ model Inspection {
 
   items         InspectionItem[]
   quoteRequests InspectionQuoteRequest[]
+  quotes        Quote[]
 
   @@index([vehicleId])
   @@index([organizationId])

+ 2 - 1
src/app/(authenticated)/quotes/new/page.tsx

@@ -29,7 +29,7 @@ export default async function NewQuotePage({
   const vehicles = vehiclesResult.success && vehiclesResult.data ? vehiclesResult.data : [];
 
   // Build prefill data from inspection if provided
-  let prefill: { title?: string; vehicleId?: string; customerId?: string; laborItems?: { description: string; hours: number; rate: number; total: number }[] } | undefined;
+  let prefill: { title?: string; vehicleId?: string; customerId?: string; inspectionId?: string; laborItems?: { description: string; hours: number; rate: number; total: number }[] } | undefined;
 
   if (params.fromInspection) {
     const inspResult = await getInspection(params.fromInspection);
@@ -42,6 +42,7 @@ export default async function NewQuotePage({
         title: `Inspection Repairs — ${insp.vehicle.year} ${insp.vehicle.make} ${insp.vehicle.model}`,
         vehicleId: insp.vehicle.id,
         customerId: insp.vehicle.customer?.id || undefined,
+        inspectionId: insp.id,
         laborItems: issueItems.map((item: { name: string; section: string; notes: string | null }) => ({
           description: `${item.section}: ${item.name}${item.notes ? ` — ${item.notes}` : ""}`,
           hours: 0,

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

@@ -83,6 +83,7 @@ export async function getInspection(id: string) {
         },
         template: { select: { id: true, name: true } },
         items: { orderBy: { sortOrder: "asc" } },
+        quotes: { select: { id: true, quoteNumber: true, status: true } },
       },
     });
     if (!inspection) throw new Error("Inspection not found");

+ 118 - 97
src/features/inspections/Actions/templateActions.ts

@@ -8,7 +8,7 @@ import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 
 export async function getTemplates() {
   return withAuth(async ({ organizationId }) => {
-    const templates = await db.inspectionTemplate.findMany({
+    let templates = await db.inspectionTemplate.findMany({
       where: { organizationId },
       include: {
         sections: {
@@ -18,6 +18,22 @@ export async function getTemplates() {
       },
       orderBy: { createdAt: "desc" },
     });
+
+    // Auto-seed default template for new organizations
+    if (templates.length === 0) {
+      await seedDefaultTemplateForOrg(organizationId);
+      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 }] });
 }
@@ -158,110 +174,115 @@ export async function deleteTemplate(id: string) {
   }, { 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;
+async function seedDefaultTemplateForOrg(organizationId: string) {
+  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 },
-                ],
-              },
+  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 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: "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: "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: "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 },
-                ],
-              },
+          },
+          {
+            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 } },
+          },
+        ],
       },
-    });
+    },
+    include: {
+      sections: { include: { items: true } },
+    },
+  });
 
-    revalidatePath("/settings/inspections");
-    return template;
+  revalidatePath("/settings/inspections");
+  return template;
+}
+
+export async function seedDefaultTemplate() {
+  return withAuth(async ({ organizationId }) => {
+    return seedDefaultTemplateForOrg(organizationId);
   }, { requiredPermissions: [{ action: PermissionAction.CREATE, subject: PermissionSubject.INSPECTIONS }] });
 }
+

+ 12 - 2
src/features/inspections/Components/InspectionPageClient.tsx

@@ -56,6 +56,7 @@ interface InspectionData {
   };
   template: { id: string; name: string };
   items: InspectionItem[];
+  quotes: { id: string; quoteNumber: string | null; status: string }[];
 }
 
 const conditionConfig: Record<Condition, { label: string; color: string; bgColor: string; icon: React.ReactNode }> = {
@@ -484,7 +485,16 @@ export function InspectionPageClient({
                 Complete Inspection
               </Button>
             )}
-            {hasIssueItems && (
+            {inspection.quotes.length > 0 ? (
+              <Button
+                variant="outline"
+                size="sm"
+                onClick={() => router.push(`/quotes/${inspection.quotes[0].id}`)}
+              >
+                <FileText className="mr-1 h-3.5 w-3.5" />
+                View Quote
+              </Button>
+            ) : hasIssueItems ? (
               <Button
                 variant="outline"
                 size="sm"
@@ -493,7 +503,7 @@ export function InspectionPageClient({
                 <FileText className="mr-1 h-3.5 w-3.5" />
                 Create Quote
               </Button>
-            )}
+            ) : null}
             <Button variant="outline" size="sm" onClick={() => setShowShareDialog(true)}>
               <Share2 className="mr-1 h-3.5 w-3.5" />
               Share

+ 3 - 0
src/features/quotes/Actions/quoteActions.ts

@@ -81,6 +81,9 @@ export async function getQuote(quoteId: string) {
         vehicle: {
           select: { id: true, make: true, model: true, year: true, vin: true, licensePlate: true, mileage: true },
         },
+        inspection: {
+          select: { id: true },
+        },
       },
     });
     if (!quote) throw new Error("Quote not found");

+ 2 - 0
src/features/quotes/Components/QuoteForm.tsx

@@ -96,6 +96,7 @@ interface PrefillData {
   title?: string;
   vehicleId?: string;
   customerId?: string;
+  inspectionId?: string;
   laborItems?: QuoteLaborInput[];
 }
 
@@ -206,6 +207,7 @@ export function QuoteForm({
       customerId: customerId || undefined,
       vehicleId: vehicleId || undefined,
       notes: notes || undefined,
+      inspectionId: prefill?.inspectionId || undefined,
       partItems: partItems.filter((p) => p.name),
       laborItems: laborItems.filter((l) => l.description),
       subtotal,

+ 11 - 0
src/features/quotes/Components/QuotePageClient.tsx

@@ -39,6 +39,7 @@ import {
   ArrowLeft,
   ArrowRight,
   Car,
+  ClipboardCheck,
   Download,
   FileText,
   Globe,
@@ -95,6 +96,7 @@ interface QuoteRecord {
   notes: string | null;
   publicToken: string | null;
   convertedToId: string | null;
+  inspectionId: string | null;
   createdAt: Date;
   partItems: { id: string; partNumber: string | null; name: string; quantity: number; unitPrice: number; total: number }[];
   laborItems: { id: string; description: string; hours: number; rate: number; total: number }[];
@@ -480,6 +482,15 @@ export function QuotePageClient({
             <Input id="validUntil" name="validUntil" type="date" defaultValue={defaultValidDate} />
           </div>
         </div>
+        {quote.inspectionId && (
+          <Link
+            href={`/inspections/${quote.inspectionId}`}
+            className="flex items-center gap-2 rounded-md bg-muted/50 px-3 py-2 text-sm text-muted-foreground transition-colors hover:text-foreground"
+          >
+            <ClipboardCheck className="h-3.5 w-3.5 shrink-0" />
+            <span>View linked inspection</span>
+          </Link>
+        )}
       </div>
 
       {/* Totals */}

+ 1 - 0
src/features/quotes/Schema/quoteSchema.ts

@@ -32,6 +32,7 @@ export const createQuoteSchema = z.object({
   discountAmount: z.coerce.number().min(0).default(0),
   totalAmount: z.coerce.number().min(0).default(0),
   notes: z.string().optional(),
+  inspectionId: z.string().optional(),
 });
 
 export const updateQuoteSchema = createQuoteSchema.partial().extend({