瀏覽代碼

sync feature

Bernt Christian Egeland 7 月之前
父節點
當前提交
5f92e89e89

+ 39 - 16
prisma/schema.prisma

@@ -323,6 +323,7 @@ model ServiceAttachment {
   description      String?
   includeInInvoice Boolean  @default(true)
   createdAt        DateTime @default(now())
+  updatedAt        DateTime @updatedAt
 
   serviceRecordId String
   serviceRecord   ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
@@ -332,12 +333,14 @@ model ServiceAttachment {
 }
 
 model ServicePart {
-  id         String  @id @default(cuid())
+  id         String   @id @default(cuid())
   partNumber String?
   name       String
-  quantity   Float   @default(1)
-  unitPrice  Float   @default(0)
-  total      Float   @default(0)
+  quantity   Float    @default(1)
+  unitPrice  Float    @default(0)
+  total      Float    @default(0)
+  createdAt  DateTime @default(now())
+  updatedAt  DateTime @updatedAt
 
   serviceRecordId String
   serviceRecord   ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
@@ -347,11 +350,13 @@ model ServicePart {
 }
 
 model ServiceLabor {
-  id          String @id @default(cuid())
+  id          String   @id @default(cuid())
   description String
-  hours       Float  @default(0)
-  rate        Float  @default(0)
-  total       Float  @default(0)
+  hours       Float    @default(0)
+  rate        Float    @default(0)
+  total       Float    @default(0)
+  createdAt   DateTime @default(now())
+  updatedAt   DateTime @updatedAt
 
   serviceRecordId String
   serviceRecord   ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)
@@ -447,6 +452,7 @@ model QuoteAttachment {
   description      String?
   includeInInvoice Boolean  @default(true)
   createdAt        DateTime @default(now())
+  updatedAt        DateTime @updatedAt
 
   quoteId String
   quote   Quote @relation(fields: [quoteId], references: [id], onDelete: Cascade)
@@ -456,12 +462,14 @@ model QuoteAttachment {
 }
 
 model QuotePart {
-  id         String  @id @default(cuid())
+  id         String   @id @default(cuid())
   partNumber String?
   name       String
-  quantity   Float   @default(1)
-  unitPrice  Float   @default(0)
-  total      Float   @default(0)
+  quantity   Float    @default(1)
+  unitPrice  Float    @default(0)
+  total      Float    @default(0)
+  createdAt  DateTime @default(now())
+  updatedAt  DateTime @updatedAt
 
   quoteId String
   quote   Quote  @relation(fields: [quoteId], references: [id], onDelete: Cascade)
@@ -471,11 +479,13 @@ model QuotePart {
 }
 
 model QuoteLabor {
-  id          String @id @default(cuid())
+  id          String   @id @default(cuid())
   description String
-  hours       Float  @default(0)
-  rate        Float  @default(0)
-  total       Float  @default(0)
+  hours       Float    @default(0)
+  rate        Float    @default(0)
+  total       Float    @default(0)
+  createdAt   DateTime @default(now())
+  updatedAt   DateTime @updatedAt
 
   quoteId String
   quote   Quote  @relation(fields: [quoteId], references: [id], onDelete: Cascade)
@@ -544,6 +554,7 @@ model Organization {
   inspectionTemplates    InspectionTemplate[]
   smsMessages            SmsMessage[]
   technicians            Technician[]
+  syncDeletions          SyncDeletion[]
 
   @@map("organizations")
 }
@@ -963,3 +974,15 @@ model ServiceRequest {
   @@index([customerId])
   @@map("service_requests")
 }
+
+model SyncDeletion {
+  id             String   @id @default(cuid())
+  entity         String
+  recordId       String
+  organizationId String
+  deletedAt      DateTime @default(now())
+  organization   Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
+
+  @@index([organizationId, deletedAt])
+  @@map("sync_deletions")
+}

+ 2 - 0
src/app/api/desktop/v1/customers/[id]/route.ts

@@ -3,6 +3,7 @@ import { db } from "@/lib/db";
 import { withDesktopAuth } from "@/lib/with-desktop-auth";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 import { updateCustomerSchema } from "@/features/customers/Schema/customerSchema";
+import { recordDeletion } from "@/lib/sync-deletion";
 
 export async function GET(
   request: Request,
@@ -91,6 +92,7 @@ export async function DELETE(
   return withDesktopAuth(
     request,
     async ({ organizationId }) => {
+      await recordDeletion("customer", id, organizationId);
       const result = await db.customer.deleteMany({
         where: { id, organizationId },
       });

+ 222 - 0
src/app/api/desktop/v1/sync/route.ts

@@ -0,0 +1,222 @@
+import { NextResponse } from "next/server";
+import { db } from "@/lib/db";
+import { withDesktopAuth } from "@/lib/with-desktop-auth";
+
+export async function GET(request: Request) {
+  return withDesktopAuth(request, async ({ organizationId }) => {
+    const { searchParams } = new URL(request.url);
+    const updatedSince = searchParams.get("updated_since");
+    const since = updatedSince ? new Date(updatedSince) : null;
+
+    const timestamp = new Date().toISOString();
+
+    // Build where clause for incremental vs full sync
+    const updatedFilter = since ? { updatedAt: { gt: since } } : {};
+    const createdFilter = since ? { createdAt: { gt: since } } : {};
+
+    // Query all entities in parallel
+    const [
+      customers,
+      vehicles,
+      serviceRecords,
+      serviceParts,
+      serviceLabor,
+      serviceAttachments,
+      payments,
+      notes,
+      reminders,
+      fuelLogs,
+      quotes,
+      quoteParts,
+      quoteLabor,
+      quoteAttachments,
+      inventoryParts,
+      recurringInvoices,
+      inspectionTemplates,
+      inspectionTemplateSections,
+      inspectionTemplateItems,
+      inspections,
+      inspectionItems,
+      technicians,
+      boardAssignments,
+      appSettings,
+      customFieldDefinitions,
+      customFieldValues,
+      smsMessages,
+      serviceRequests,
+      deletions,
+    ] = await Promise.all([
+      // customers
+      db.customer.findMany({
+        where: { organizationId, ...updatedFilter },
+      }),
+      // vehicles
+      db.vehicle.findMany({
+        where: { organizationId, ...updatedFilter },
+      }),
+      // serviceRecords (via vehicle)
+      db.serviceRecord.findMany({
+        where: { vehicle: { organizationId }, ...updatedFilter },
+      }),
+      // serviceParts (via service record → vehicle)
+      db.servicePart.findMany({
+        where: { serviceRecord: { vehicle: { organizationId } }, ...updatedFilter },
+      }),
+      // serviceLabor
+      db.serviceLabor.findMany({
+        where: { serviceRecord: { vehicle: { organizationId } }, ...updatedFilter },
+      }),
+      // serviceAttachments
+      db.serviceAttachment.findMany({
+        where: { serviceRecord: { vehicle: { organizationId } }, ...updatedFilter },
+      }),
+      // payments
+      db.payment.findMany({
+        where: { serviceRecord: { vehicle: { organizationId } }, ...updatedFilter },
+      }),
+      // notes
+      db.note.findMany({
+        where: { vehicle: { organizationId }, ...updatedFilter },
+      }),
+      // reminders
+      db.reminder.findMany({
+        where: { vehicle: { organizationId }, ...updatedFilter },
+      }),
+      // fuelLogs
+      db.fuelLog.findMany({
+        where: { vehicle: { organizationId }, ...updatedFilter },
+      }),
+      // quotes
+      db.quote.findMany({
+        where: { organizationId, ...updatedFilter },
+      }),
+      // quoteParts
+      db.quotePart.findMany({
+        where: { quote: { organizationId }, ...updatedFilter },
+      }),
+      // quoteLabor
+      db.quoteLabor.findMany({
+        where: { quote: { organizationId }, ...updatedFilter },
+      }),
+      // quoteAttachments
+      db.quoteAttachment.findMany({
+        where: { quote: { organizationId }, ...updatedFilter },
+      }),
+      // inventoryParts
+      db.inventoryPart.findMany({
+        where: { organizationId, ...updatedFilter },
+      }),
+      // recurringInvoices
+      db.recurringInvoice.findMany({
+        where: { vehicle: { organizationId }, ...updatedFilter },
+      }),
+      // inspectionTemplates
+      db.inspectionTemplate.findMany({
+        where: { organizationId, ...updatedFilter },
+      }),
+      // inspectionTemplateSections (no updatedAt, use createdAt filter or fetch all for template syncs)
+      since
+        ? db.inspectionTemplateSection.findMany({
+            where: { template: { organizationId, updatedAt: { gt: since } } },
+          })
+        : db.inspectionTemplateSection.findMany({
+            where: { template: { organizationId } },
+          }),
+      // inspectionTemplateItems
+      since
+        ? db.inspectionTemplateItem.findMany({
+            where: { section: { template: { organizationId, updatedAt: { gt: since } } } },
+          })
+        : db.inspectionTemplateItem.findMany({
+            where: { section: { template: { organizationId } } },
+          }),
+      // inspections
+      db.inspection.findMany({
+        where: { organizationId, ...updatedFilter },
+      }),
+      // inspectionItems (no updatedAt — fetch items for updated inspections)
+      since
+        ? db.inspectionItem.findMany({
+            where: { inspection: { organizationId, updatedAt: { gt: since } } },
+          })
+        : db.inspectionItem.findMany({
+            where: { inspection: { organizationId } },
+          }),
+      // technicians
+      db.technician.findMany({
+        where: { organizationId, ...updatedFilter },
+      }),
+      // boardAssignments
+      db.boardAssignment.findMany({
+        where: { organizationId, ...updatedFilter },
+      }),
+      // appSettings (no updatedAt)
+      db.appSetting.findMany({
+        where: { organizationId },
+      }),
+      // customFieldDefinitions
+      db.customFieldDefinition.findMany({
+        where: { organizationId, ...updatedFilter },
+      }),
+      // customFieldValues (no updatedAt — fetch all for updated definitions)
+      since
+        ? db.customFieldValue.findMany({
+            where: { field: { organizationId, updatedAt: { gt: since } } },
+          })
+        : db.customFieldValue.findMany({
+            where: { field: { organizationId } },
+          }),
+      // smsMessages
+      db.smsMessage.findMany({
+        where: { organizationId, ...updatedFilter },
+      }),
+      // serviceRequests
+      db.serviceRequest.findMany({
+        where: { organizationId, ...updatedFilter },
+      }),
+      // deletions
+      since
+        ? db.syncDeletion.findMany({
+            where: { organizationId, deletedAt: { gt: since } },
+          })
+        : [],
+    ]);
+
+    return NextResponse.json({
+      timestamp,
+      customers,
+      vehicles,
+      serviceRecords,
+      serviceParts,
+      serviceLabor,
+      serviceAttachments,
+      payments,
+      notes,
+      reminders,
+      fuelLogs,
+      quotes,
+      quoteParts,
+      quoteLabor,
+      quoteAttachments,
+      inventoryParts,
+      recurringInvoices,
+      inspectionTemplates,
+      inspectionTemplateSections,
+      inspectionTemplateItems,
+      inspections,
+      inspectionItems,
+      technicians,
+      boardAssignments,
+      appSettings,
+      customFieldDefinitions,
+      customFieldValues,
+      smsMessages,
+      serviceRequests,
+      deletions: deletions.map((d) => ({
+        entity: d.entity,
+        recordId: d.recordId,
+        deletedAt: d.deletedAt,
+      })),
+    });
+  });
+}

+ 2 - 0
src/app/api/desktop/v1/vehicles/[id]/notes/[noteId]/route.ts

@@ -3,6 +3,7 @@ import { db } from "@/lib/db";
 import { withDesktopAuth } from "@/lib/with-desktop-auth";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 import { updateNoteSchema } from "@/features/vehicles/Schema/noteSchema";
+import { recordDeletion } from "@/lib/sync-deletion";
 
 export async function PUT(
   request: Request,
@@ -54,6 +55,7 @@ export async function DELETE(
         return NextResponse.json({ error: "Note not found" }, { status: 404 });
       }
 
+      await recordDeletion("note", noteId, organizationId);
       await db.note.delete({ where: { id: noteId } });
       return new NextResponse(null, { status: 204 });
     },

+ 2 - 0
src/app/api/desktop/v1/vehicles/[id]/reminders/[reminderId]/route.ts

@@ -3,6 +3,7 @@ import { db } from "@/lib/db";
 import { withDesktopAuth } from "@/lib/with-desktop-auth";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 import { updateReminderSchema } from "@/features/vehicles/Schema/reminderSchema";
+import { recordDeletion } from "@/lib/sync-deletion";
 
 export async function PUT(
   request: Request,
@@ -59,6 +60,7 @@ export async function DELETE(
         return NextResponse.json({ error: "Reminder not found" }, { status: 404 });
       }
 
+      await recordDeletion("reminder", reminderId, organizationId);
       await db.reminder.delete({ where: { id: reminderId } });
       return new NextResponse(null, { status: 204 });
     },

+ 2 - 0
src/app/api/desktop/v1/vehicles/[id]/route.ts

@@ -3,6 +3,7 @@ import { db } from "@/lib/db";
 import { withDesktopAuth } from "@/lib/with-desktop-auth";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 import { updateVehicleSchema } from "@/features/vehicles/Schema/vehicleSchema";
+import { recordDeletion } from "@/lib/sync-deletion";
 
 export async function GET(
   request: Request,
@@ -155,6 +156,7 @@ export async function DELETE(
   return withDesktopAuth(
     request,
     async ({ organizationId }) => {
+      await recordDeletion("vehicle", id, organizationId);
       const result = await db.vehicle.deleteMany({
         where: { id, organizationId },
       });

+ 2 - 0
src/features/billing/Actions/recurringInvoiceActions.ts

@@ -11,6 +11,7 @@ import {
   type UpdateRecurringInvoiceInput,
 } from "../Schema/recurringInvoiceSchema";
 import { resolveInvoicePrefix } from "@/lib/invoice-utils";
+import { recordDeletion } from "@/lib/sync-deletion";
 
 export async function getRecurringInvoices() {
   return withAuth(async ({ organizationId }) => {
@@ -155,6 +156,7 @@ export async function deleteRecurringInvoice(id: string) {
     });
     if (!existing) throw new Error("Recurring invoice not found");
 
+    await recordDeletion("recurringInvoice", id, organizationId);
     await db.recurringInvoice.delete({ where: { id } });
 
     revalidatePath("/billing/recurring");

+ 9 - 0
src/features/custom-fields/Actions/customFieldActions.ts

@@ -6,6 +6,7 @@ import { createFieldDefinitionSchema, updateFieldDefinitionSchema } from "../Sch
 import { revalidatePath } from "next/cache";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 import { requireFeature } from "@/lib/features";
+import { recordDeletion } from "@/lib/sync-deletion";
 
 export async function getFieldDefinitions(entityType?: string) {
   return withAuth(async ({ organizationId }) => {
@@ -67,6 +68,14 @@ export async function deleteFieldDefinition(fieldId: string) {
     });
     if (!field) throw new Error("Field not found");
 
+    const values = await db.customFieldValue.findMany({
+      where: { fieldId },
+      select: { id: true },
+    });
+    const deletions: Promise<void>[] = [recordDeletion("customFieldDefinition", fieldId, organizationId)];
+    if (values.length) deletions.push(recordDeletion("customFieldValue", values.map(v => v.id), organizationId));
+    await Promise.all(deletions);
+
     await db.$transaction([
       db.customFieldValue.deleteMany({ where: { fieldId } }),
       db.customFieldDefinition.delete({ where: { id: fieldId } }),

+ 2 - 0
src/features/customers/Actions/customerActions.ts

@@ -7,6 +7,7 @@ import { revalidatePath } from "next/cache";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 import { getFeatures, FeatureGatedError } from "@/lib/features";
 import { createDraftServiceRecord } from "@/features/vehicles/Actions/createDraftServiceRecord";
+import { recordDeletion } from "@/lib/sync-deletion";
 
 export async function getCustomers() {
   return withAuth(async ({ userId, organizationId }) => {
@@ -90,6 +91,7 @@ export async function updateCustomer(input: unknown) {
 
 export async function deleteCustomer(customerId: string) {
   return withAuth(async ({ userId, organizationId }) => {
+    await recordDeletion("customer", customerId, organizationId);
     const result = await db.customer.deleteMany({ where: { id: customerId, organizationId } });
     if (result.count === 0) throw new Error("Customer not found");
     revalidatePath("/customers");

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

@@ -6,6 +6,7 @@ import { createInspectionSchema, updateInspectionItemSchema } from "../Schema/in
 import { revalidatePath } from "next/cache";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 import { notificationBus } from "@/lib/notification-bus";
+import { recordDeletion } from "@/lib/sync-deletion";
 
 export async function getInspectionsPaginated(params: {
   page?: number;
@@ -226,9 +227,18 @@ export async function deleteInspection(id: string) {
   return withAuth(async ({ organizationId }) => {
     const inspection = await db.inspection.findFirst({
       where: { id, organizationId },
+      include: {
+        items: { select: { id: true } },
+        boardAssignments: { select: { id: true } },
+      },
     });
     if (!inspection) throw new Error("Inspection not found");
 
+    const deletions: Promise<void>[] = [recordDeletion("inspection", id, organizationId)];
+    if (inspection.items.length) deletions.push(recordDeletion("inspectionItem", inspection.items.map(i => i.id), organizationId));
+    if (inspection.boardAssignments.length) deletions.push(recordDeletion("boardAssignment", inspection.boardAssignments.map(b => b.id), organizationId));
+    await Promise.all(deletions);
+
     await db.inspection.deleteMany({ where: { id, organizationId } });
     revalidatePath("/inspections");
     revalidatePath(`/vehicles/${inspection.vehicleId}`);

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

@@ -5,6 +5,7 @@ import { withAuth } from "@/lib/with-auth";
 import { createTemplateSchema, updateTemplateSchema } from "../Schema/templateSchema";
 import { revalidatePath } from "next/cache";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
+import { recordDeletion } from "@/lib/sync-deletion";
 
 export async function getTemplates() {
   return withAuth(async ({ organizationId }) => {
@@ -168,6 +169,18 @@ export async function deleteTemplate(id: string) {
       );
     }
 
+    const sections = await db.inspectionTemplateSection.findMany({
+      where: { templateId: id },
+      select: { id: true, items: { select: { id: true } } },
+    });
+    const deletions: Promise<void>[] = [recordDeletion("inspectionTemplate", id, organizationId)];
+    if (sections.length) {
+      deletions.push(recordDeletion("inspectionTemplateSection", sections.map(s => s.id), organizationId));
+      const itemIds = sections.flatMap(s => s.items.map(i => i.id));
+      if (itemIds.length) deletions.push(recordDeletion("inspectionTemplateItem", itemIds, organizationId));
+    }
+    await Promise.all(deletions);
+
     await db.inspectionTemplate.delete({ where: { id } });
 
     revalidatePath("/settings/templates");

+ 2 - 0
src/features/inventory/Actions/inventoryActions.ts

@@ -9,6 +9,7 @@ import { z } from "zod";
 import { unlink } from "fs/promises";
 import { resolveUploadPath } from "@/lib/resolve-upload-path";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
+import { recordDeletion } from "@/lib/sync-deletion";
 
 export async function getInventoryPartsPaginated(params: {
   page?: number;
@@ -141,6 +142,7 @@ export async function deleteInventoryPart(partId: string) {
       select: { imageUrl: true },
     });
 
+    await recordDeletion("inventoryPart", partId, organizationId);
     const result = await db.inventoryPart.deleteMany({
       where: { id: partId, organizationId },
     });

+ 2 - 0
src/features/payments/Actions/paymentActions.ts

@@ -5,6 +5,7 @@ import { withAuth } from "@/lib/with-auth";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 import { createPaymentSchema } from "../Schema/paymentSchema";
 import { revalidatePath } from "next/cache";
+import { recordDeletion } from "@/lib/sync-deletion";
 
 export async function createPayment(input: unknown) {
   return withAuth(async ({ organizationId }) => {
@@ -40,6 +41,7 @@ export async function deletePayment(paymentId: string) {
     });
     if (!payment) throw new Error("Payment not found");
 
+    await recordDeletion("payment", paymentId, organizationId);
     await db.payment.delete({ where: { id: paymentId } });
 
     const { vehicleId, id: serviceId } = payment.serviceRecord;

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

@@ -8,6 +8,7 @@ import { resolveInvoicePrefix } from "@/lib/invoice-utils";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 import { copyFile, mkdir } from "fs/promises";
 import path from "path";
+import { recordDeletion } from "@/lib/sync-deletion";
 
 export async function getQuotesPaginated(params: {
   page?: number;
@@ -221,9 +222,20 @@ export async function deleteQuote(quoteId: string) {
   return withAuth(async ({ userId, organizationId }) => {
     const quote = await db.quote.findFirst({
       where: { id: quoteId, organizationId },
+      include: {
+        partItems: { select: { id: true } },
+        laborItems: { select: { id: true } },
+        attachments: { select: { id: true } },
+      },
     });
     if (!quote) throw new Error("Quote not found");
 
+    const deletions: Promise<void>[] = [recordDeletion("quote", quoteId, organizationId)];
+    if (quote.partItems.length) deletions.push(recordDeletion("quotePart", quote.partItems.map(p => p.id), organizationId));
+    if (quote.laborItems.length) deletions.push(recordDeletion("quoteLabor", quote.laborItems.map(l => l.id), organizationId));
+    if (quote.attachments.length) deletions.push(recordDeletion("quoteAttachment", quote.attachments.map(a => a.id), organizationId));
+    await Promise.all(deletions);
+
     await db.quote.deleteMany({ where: { id: quoteId, organizationId } });
     revalidatePath("/quotes");
   }, { requiredPermissions: [{ action: PermissionAction.DELETE, subject: PermissionSubject.QUOTES }] });

+ 10 - 0
src/features/sms/Actions/smsActions.ts

@@ -7,6 +7,7 @@ import { requireFeature } from "@/lib/features";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
 import { SMS_TEMPLATE_DEFAULTS } from "@/lib/sms-templates";
+import { recordDeletion } from "@/lib/sync-deletion";
 
 export async function sendSmsToCustomer(input: {
   customerId: string;
@@ -129,6 +130,7 @@ export async function deleteSmsMessage(messageId: string) {
       });
       if (!message) throw new Error("Message not found");
 
+      await recordDeletion("smsMessage", messageId, organizationId);
       await db.smsMessage.delete({ where: { id: messageId } });
       return { deleted: true };
     },
@@ -149,6 +151,14 @@ export async function deleteConversation(customerId: string) {
       });
       if (!customer) throw new Error("Customer not found");
 
+      const messages = await db.smsMessage.findMany({
+        where: { organizationId, customerId },
+        select: { id: true },
+      });
+      if (messages.length) {
+        await recordDeletion("smsMessage", messages.map(m => m.id), organizationId);
+      }
+
       const { count } = await db.smsMessage.deleteMany({
         where: { organizationId, customerId },
       });

+ 2 - 0
src/features/vehicles/Actions/noteActions.ts

@@ -5,6 +5,7 @@ import { withAuth } from "@/lib/with-auth";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 import { createNoteSchema, updateNoteSchema } from "../Schema/noteSchema";
 import { revalidatePath } from "next/cache";
+import { recordDeletion } from "@/lib/sync-deletion";
 
 export async function getNotesPaginated(
   vehicleId: string,
@@ -88,6 +89,7 @@ export async function deleteNote(noteId: string) {
     });
     if (!note) throw new Error("Note not found");
 
+    await recordDeletion("note", noteId, organizationId);
     await db.note.delete({ where: { id: noteId } });
     revalidatePath(`/vehicles/${note.vehicleId}`);
   }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.VEHICLES }] });

+ 2 - 0
src/features/vehicles/Actions/reminderActions.ts

@@ -5,6 +5,7 @@ import { withAuth } from "@/lib/with-auth";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 import { createReminderSchema, updateReminderSchema } from "../Schema/reminderSchema";
 import { revalidatePath } from "next/cache";
+import { recordDeletion } from "@/lib/sync-deletion";
 
 export async function createReminder(input: unknown) {
   return withAuth(async ({ organizationId }) => {
@@ -72,6 +73,7 @@ export async function deleteReminder(reminderId: string) {
     });
     if (!reminder) throw new Error("Reminder not found");
 
+    await recordDeletion("reminder", reminderId, organizationId);
     await db.reminder.delete({ where: { id: reminderId } });
     revalidatePath(`/vehicles/${reminder.vehicleId}`);
     revalidatePath("/");

+ 18 - 1
src/features/vehicles/Actions/serviceActions.ts

@@ -10,6 +10,7 @@ import { resolveUploadPath } from "@/lib/resolve-upload-path";
 import { resolveInvoicePrefix } from "@/lib/invoice-utils";
 import { notificationBus } from "@/lib/notification-bus";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
+import { recordDeletion } from "@/lib/sync-deletion";
 
 export async function getServiceRecords(vehicleId: string) {
   return withAuth(async ({ organizationId }) => {
@@ -670,7 +671,13 @@ export async function deleteServiceRecord(recordId: string) {
   return withAuth(async ({ organizationId }) => {
     const record = await db.serviceRecord.findFirst({
       where: { id: recordId, vehicle: { organizationId } },
-      include: { attachments: true },
+      include: {
+        attachments: true,
+        partItems: { select: { id: true } },
+        laborItems: { select: { id: true } },
+        payments: { select: { id: true } },
+        boardAssignments: { select: { id: true } },
+      },
     });
     if (!record) throw new Error("Record not found");
 
@@ -684,6 +691,15 @@ export async function deleteServiceRecord(recordId: string) {
       }
     }
 
+    // Record cascade deletions
+    const deletions: Promise<void>[] = [recordDeletion("serviceRecord", recordId, organizationId)];
+    if (record.attachments.length) deletions.push(recordDeletion("serviceAttachment", record.attachments.map(a => a.id), organizationId));
+    if (record.partItems.length) deletions.push(recordDeletion("servicePart", record.partItems.map(p => p.id), organizationId));
+    if (record.laborItems.length) deletions.push(recordDeletion("serviceLabor", record.laborItems.map(l => l.id), organizationId));
+    if (record.payments.length) deletions.push(recordDeletion("payment", record.payments.map(p => p.id), organizationId));
+    if (record.boardAssignments.length) deletions.push(recordDeletion("boardAssignment", record.boardAssignments.map(b => b.id), organizationId));
+    await Promise.all(deletions);
+
     await db.serviceRecord.delete({ where: { id: recordId } });
     revalidatePath("/");
     revalidatePath(`/vehicles/${record.vehicleId}`);
@@ -707,6 +723,7 @@ export async function deleteServiceAttachment(attachmentId: string) {
       console.warn(`[deleteServiceAttachment] Failed to delete file "${filePath}":`, err);
     }
 
+    await recordDeletion("serviceAttachment", attachmentId, organizationId);
     await db.serviceAttachment.delete({ where: { id: attachmentId } });
 
     const { vehicleId, id: serviceId } = attachment.serviceRecord;

+ 58 - 1
src/features/vehicles/Actions/vehicleActions.ts

@@ -7,6 +7,7 @@ import { createVehicleSchema, updateVehicleSchema } from "../Schema/vehicleSchem
 import { revalidatePath } from "next/cache";
 import { unlink } from "fs/promises";
 import { resolveUploadPath } from "@/lib/resolve-upload-path";
+import { recordDeletion } from "@/lib/sync-deletion";
 
 export async function getVehicles() {
   return withAuth(async ({ organizationId }) => {
@@ -184,12 +185,68 @@ export async function deleteVehicle(vehicleId: string) {
       select: {
         imageUrl: true,
         serviceRecords: {
-          select: { attachments: { select: { fileUrl: true } } },
+          select: {
+            id: true,
+            attachments: { select: { id: true, fileUrl: true } },
+            partItems: { select: { id: true } },
+            laborItems: { select: { id: true } },
+            payments: { select: { id: true } },
+            boardAssignments: { select: { id: true } },
+          },
+        },
+        notes: { select: { id: true } },
+        fuelLogs: { select: { id: true } },
+        reminders: { select: { id: true } },
+        quotes: {
+          select: {
+            id: true,
+            partItems: { select: { id: true } },
+            laborItems: { select: { id: true } },
+            attachments: { select: { id: true } },
+          },
         },
+        recurringInvoices: { select: { id: true } },
+        inspections: {
+          select: {
+            id: true,
+            items: { select: { id: true } },
+            boardAssignments: { select: { id: true } },
+          },
+        },
+        serviceRequests: { select: { id: true } },
       },
     });
     if (!vehicle) throw new Error("Vehicle not found");
 
+    // Record all cascade deletions before deleting
+    const deletions: Promise<void>[] = [];
+    for (const sr of vehicle.serviceRecords) {
+      deletions.push(recordDeletion("serviceRecord", sr.id, organizationId));
+      if (sr.partItems.length) deletions.push(recordDeletion("servicePart", sr.partItems.map(p => p.id), organizationId));
+      if (sr.laborItems.length) deletions.push(recordDeletion("serviceLabor", sr.laborItems.map(l => l.id), organizationId));
+      if (sr.attachments.length) deletions.push(recordDeletion("serviceAttachment", sr.attachments.map(a => a.id), organizationId));
+      if (sr.payments.length) deletions.push(recordDeletion("payment", sr.payments.map(p => p.id), organizationId));
+      if (sr.boardAssignments.length) deletions.push(recordDeletion("boardAssignment", sr.boardAssignments.map(b => b.id), organizationId));
+    }
+    if (vehicle.notes.length) deletions.push(recordDeletion("note", vehicle.notes.map(n => n.id), organizationId));
+    if (vehicle.fuelLogs.length) deletions.push(recordDeletion("fuelLog", vehicle.fuelLogs.map(f => f.id), organizationId));
+    if (vehicle.reminders.length) deletions.push(recordDeletion("reminder", vehicle.reminders.map(r => r.id), organizationId));
+    for (const q of vehicle.quotes) {
+      deletions.push(recordDeletion("quote", q.id, organizationId));
+      if (q.partItems.length) deletions.push(recordDeletion("quotePart", q.partItems.map(p => p.id), organizationId));
+      if (q.laborItems.length) deletions.push(recordDeletion("quoteLabor", q.laborItems.map(l => l.id), organizationId));
+      if (q.attachments.length) deletions.push(recordDeletion("quoteAttachment", q.attachments.map(a => a.id), organizationId));
+    }
+    if (vehicle.recurringInvoices.length) deletions.push(recordDeletion("recurringInvoice", vehicle.recurringInvoices.map(r => r.id), organizationId));
+    for (const insp of vehicle.inspections) {
+      deletions.push(recordDeletion("inspection", insp.id, organizationId));
+      if (insp.items.length) deletions.push(recordDeletion("inspectionItem", insp.items.map(i => i.id), organizationId));
+      if (insp.boardAssignments.length) deletions.push(recordDeletion("boardAssignment", insp.boardAssignments.map(b => b.id), organizationId));
+    }
+    if (vehicle.serviceRequests.length) deletions.push(recordDeletion("serviceRequest", vehicle.serviceRequests.map(s => s.id), organizationId));
+    deletions.push(recordDeletion("vehicle", vehicleId, organizationId));
+    await Promise.all(deletions);
+
     await db.vehicle.deleteMany({ where: { id: vehicleId, organizationId } });
 
     // Clean up files from disk after DB deletion

+ 2 - 0
src/features/workboard/Actions/boardActions.ts

@@ -10,6 +10,7 @@ import {
   moveAssignmentSchema,
   removeAssignmentSchema,
 } from "../Schema/workboardSchema";
+import { recordDeletion } from "@/lib/sync-deletion";
 
 export type BoardAssignmentWithJob = {
   id: string;
@@ -396,6 +397,7 @@ export async function removeAssignment(input: unknown) {
         });
       }
 
+      await recordDeletion("boardAssignment", data.id, organizationId);
       await db.boardAssignment.delete({ where: { id: data.id } });
 
       notificationBus.emit("workboard", {

+ 2 - 0
src/features/workboard/Actions/technicianActions.ts

@@ -9,6 +9,7 @@ import {
   createTechnicianSchema,
   updateTechnicianSchema,
 } from "../Schema/workboardSchema";
+import { recordDeletion } from "@/lib/sync-deletion";
 
 export async function getTechnicians() {
   return withAuth(
@@ -95,6 +96,7 @@ export async function updateTechnician(input: unknown) {
 export async function deleteTechnician(id: string) {
   return withAuth(
     async ({ organizationId }) => {
+      await recordDeletion("technician", id, organizationId);
       await db.technician.deleteMany({
         where: { id, organizationId },
       });

+ 18 - 0
src/lib/sync-deletion.ts

@@ -0,0 +1,18 @@
+import { db } from "./db";
+
+export async function recordDeletion(
+  entity: string,
+  recordId: string | string[],
+  organizationId: string
+) {
+  const ids = Array.isArray(recordId) ? recordId : [recordId];
+  if (ids.length === 0) return;
+
+  await db.syncDeletion.createMany({
+    data: ids.map((id) => ({
+      entity,
+      recordId: id,
+      organizationId,
+    })),
+  });
+}