Architektur: Craftvia-Domänenmodell, Verträge und Team-Schnitte
- Migration 0002_craftvia_domain: 27 Fachtabellen inkl. RLS (enable_tenant_rls) - TENANT_MODELS (db.ts, backup/topology.ts) um alle Fachmodelle ergänzt - moduleGuard liefert DB-autoritative Rechte; ServiceCtx für Domänen-Services - Verträge: Statusmaschine, Events, Nummernkreise, Sichtbarkeits-Scopes, Job-Queues + Worker, KI-Provider-Interfaces, Sync-Envelope - docs/craftvia/ARCHITEKTUR.md mit Lanes, Ownership und DoD Gate: tsc, lint, build, 22/22 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -144,6 +144,13 @@ model User {
|
||||
identity Identity @relation(fields: [identityId], references: [id])
|
||||
userRoles UserRole[]
|
||||
|
||||
teamsLed Team[] @relation("TeamLeader")
|
||||
teamMemberships TeamMember[] @relation("TeamMemberships")
|
||||
workOrdersLed WorkOrder[] @relation("WorkOrderTeamLead")
|
||||
workOrderAssignments WorkOrderAssignee[] @relation("WorkOrderAssignments")
|
||||
workSessions WorkSession[] @relation("WorkSessionUser")
|
||||
notifications Notification[] @relation("UserNotifications")
|
||||
|
||||
@@unique([tenantId, email])
|
||||
// Eine Person (Identity) hat je Mandant höchstens EINE Mitgliedschaft.
|
||||
@@unique([tenantId, identityId])
|
||||
@@ -422,3 +429,859 @@ model BackupJob {
|
||||
@@index([status])
|
||||
@@map("backup_jobs")
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Craftvia domain model (MVP) — appended to prisma/schema.prisma
|
||||
// Conventions: camelCase fields @map snake_case, tables @@map plural snake_case.
|
||||
// Every tenant table carries tenantId (plain column, guarded by dbForTenant + RLS)
|
||||
// and is listed in BOTH TENANT_MODELS lists (src/server/db.ts, backup/topology.ts).
|
||||
// Soft delete: deletedAt on business records (spec §27.5).
|
||||
// ============================================================================
|
||||
|
||||
// ---------- Numbering / configuration ----------
|
||||
|
||||
model NumberSequence {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
key String // "customer" | "work_order" | "report" | "emergency"
|
||||
prefix String @default("")
|
||||
nextValue Int @default(1) @map("next_value")
|
||||
padding Int @default(5)
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@unique([tenantId, key])
|
||||
@@map("number_sequences")
|
||||
}
|
||||
|
||||
model OrderType {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
key String // montage | reparatur | wartung | stoerung | notdienst | besichtigung | abnahme | nacharbeit | custom
|
||||
name String
|
||||
active Boolean @default(true)
|
||||
signatureRequired Boolean @default(true) @map("signature_required")
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
checklistTemplates ChecklistTemplate[]
|
||||
workOrders WorkOrder[]
|
||||
|
||||
@@unique([tenantId, key])
|
||||
@@map("order_types")
|
||||
}
|
||||
|
||||
model ChecklistTemplate {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
orderTypeId String? @map("order_type_id")
|
||||
name String
|
||||
active Boolean @default(true)
|
||||
// items: [{ key, label, required, requiresPhoto }]
|
||||
items Json @default("[]")
|
||||
// requiredPhotos: [{ key, label }]
|
||||
requiredPhotos Json @default("[]") @map("required_photos")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
orderType OrderType? @relation(fields: [orderTypeId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([tenantId])
|
||||
@@map("checklist_templates")
|
||||
}
|
||||
|
||||
// ---------- Customers / contacts / sites ----------
|
||||
|
||||
enum CustomerStatus {
|
||||
active
|
||||
inactive
|
||||
provisional // "Vorläufig – Prüfung durch Backoffice erforderlich"
|
||||
merged
|
||||
}
|
||||
|
||||
model Customer {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
customerNumber String? @map("customer_number")
|
||||
companyName String? @map("company_name")
|
||||
salutation String?
|
||||
firstName String? @map("first_name")
|
||||
lastName String? @map("last_name")
|
||||
street String?
|
||||
houseNumber String? @map("house_number")
|
||||
postalCode String? @map("postal_code")
|
||||
city String?
|
||||
country String @default("DE")
|
||||
phone String?
|
||||
mobile String?
|
||||
email String?
|
||||
notes String?
|
||||
billingNotes String? @map("billing_notes")
|
||||
status CustomerStatus @default(active)
|
||||
isProvisional Boolean @default(false) @map("is_provisional")
|
||||
mergedIntoId String? @map("merged_into_id")
|
||||
createdById String? @map("created_by_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
|
||||
contacts Contact[]
|
||||
sites Site[]
|
||||
workOrders WorkOrder[]
|
||||
documents Document[]
|
||||
|
||||
@@unique([tenantId, customerNumber])
|
||||
@@index([tenantId, status])
|
||||
@@index([tenantId, companyName])
|
||||
@@index([tenantId, lastName])
|
||||
@@map("customers")
|
||||
}
|
||||
|
||||
model Contact {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
customerId String @map("customer_id")
|
||||
name String
|
||||
role String? // Funktion
|
||||
phone String?
|
||||
mobile String?
|
||||
email String?
|
||||
preferredChannel String? @map("preferred_channel") // phone | mobile | email
|
||||
notes String?
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
|
||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||
sites Site[]
|
||||
workOrders WorkOrder[]
|
||||
|
||||
@@index([tenantId, customerId])
|
||||
@@map("contacts")
|
||||
}
|
||||
|
||||
enum SiteStatus {
|
||||
active
|
||||
inactive
|
||||
provisional
|
||||
}
|
||||
|
||||
model Site {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
customerId String @map("customer_id")
|
||||
name String
|
||||
street String?
|
||||
houseNumber String? @map("house_number")
|
||||
postalCode String? @map("postal_code")
|
||||
city String?
|
||||
country String @default("DE")
|
||||
contactId String? @map("contact_id")
|
||||
onSiteContact String? @map("on_site_contact") // free text if no Contact
|
||||
phone String?
|
||||
accessNotes String? @map("access_notes")
|
||||
parkingNotes String? @map("parking_notes")
|
||||
safetyNotes String? @map("safety_notes")
|
||||
technicalNotes String? @map("technical_notes")
|
||||
status SiteStatus @default(active)
|
||||
latitude Float?
|
||||
longitude Float?
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
|
||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Restrict)
|
||||
contact Contact? @relation(fields: [contactId], references: [id], onDelete: SetNull)
|
||||
workOrders WorkOrder[]
|
||||
documents Document[]
|
||||
|
||||
@@index([tenantId, customerId])
|
||||
@@map("sites")
|
||||
}
|
||||
|
||||
// ---------- Teams ----------
|
||||
|
||||
enum TeamStatus {
|
||||
active
|
||||
inactive
|
||||
}
|
||||
|
||||
model Team {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
name String
|
||||
leaderUserId String? @map("leader_user_id")
|
||||
status TeamStatus @default(active)
|
||||
phone String?
|
||||
vehicle String?
|
||||
area String?
|
||||
notes String?
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
|
||||
leader User? @relation("TeamLeader", fields: [leaderUserId], references: [id], onDelete: SetNull)
|
||||
members TeamMember[]
|
||||
workOrders WorkOrder[]
|
||||
|
||||
@@unique([tenantId, name])
|
||||
@@map("teams")
|
||||
}
|
||||
|
||||
model TeamMember {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
teamId String @map("team_id")
|
||||
userId String @map("user_id")
|
||||
validFrom DateTime @default(now()) @map("valid_from")
|
||||
validTo DateTime? @map("valid_to")
|
||||
|
||||
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
user User @relation("TeamMemberships", fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([tenantId, userId])
|
||||
@@index([teamId])
|
||||
@@map("team_members")
|
||||
}
|
||||
|
||||
// ---------- Work orders ----------
|
||||
|
||||
enum WorkOrderStatus {
|
||||
draft
|
||||
review_required
|
||||
planned
|
||||
assigned
|
||||
accepted
|
||||
en_route
|
||||
in_progress
|
||||
paused
|
||||
waiting_material
|
||||
daily_report_created
|
||||
technically_completed
|
||||
signature_pending
|
||||
in_review
|
||||
released_for_billing
|
||||
billed
|
||||
cancelled
|
||||
}
|
||||
|
||||
enum WorkOrderPriority {
|
||||
low
|
||||
normal
|
||||
high
|
||||
urgent
|
||||
}
|
||||
|
||||
model WorkOrder {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
number String // internal, from NumberSequence "work_order"
|
||||
externalOrderNumber String? @map("external_order_number")
|
||||
offerNumber String? @map("offer_number")
|
||||
customerId String @map("customer_id")
|
||||
siteId String? @map("site_id")
|
||||
contactId String? @map("contact_id")
|
||||
orderTypeId String? @map("order_type_id")
|
||||
priority WorkOrderPriority @default(normal)
|
||||
status WorkOrderStatus @default(draft)
|
||||
title String
|
||||
description String?
|
||||
scope String? // Leistungsumfang
|
||||
plannedStart DateTime? @map("planned_start")
|
||||
plannedEnd DateTime? @map("planned_end")
|
||||
assignedTeamId String? @map("assigned_team_id")
|
||||
teamLeadUserId String? @map("team_lead_user_id")
|
||||
signatureRequired Boolean @default(true) @map("signature_required")
|
||||
billingType String? @map("billing_type") // fixed | time_material | maintenance_contract | warranty
|
||||
internalNotes String? @map("internal_notes")
|
||||
technicianNotes String? @map("technician_notes")
|
||||
isEmergency Boolean @default(false) @map("is_emergency")
|
||||
emergencyReason String? @map("emergency_reason")
|
||||
sourceImportId String? @unique @map("source_import_id")
|
||||
followUpWork String? @map("follow_up_work") // offene Folgearbeiten (aggregiert)
|
||||
// incremented on every mutation — used by offline sync conflict detection
|
||||
version Int @default(1)
|
||||
createdById String? @map("created_by_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
|
||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Restrict)
|
||||
site Site? @relation(fields: [siteId], references: [id], onDelete: SetNull)
|
||||
contact Contact? @relation(fields: [contactId], references: [id], onDelete: SetNull)
|
||||
orderType OrderType? @relation(fields: [orderTypeId], references: [id], onDelete: SetNull)
|
||||
team Team? @relation(fields: [assignedTeamId], references: [id], onDelete: SetNull)
|
||||
teamLead User? @relation("WorkOrderTeamLead", fields: [teamLeadUserId], references: [id], onDelete: SetNull)
|
||||
sourceImport ImportJob? @relation("ImportCreatedOrder", fields: [sourceImportId], references: [id], onDelete: SetNull)
|
||||
|
||||
assignees WorkOrderAssignee[]
|
||||
statusHistory WorkOrderStatusChange[]
|
||||
checklistItems ChecklistItem[]
|
||||
photoRequirements PhotoRequirement[]
|
||||
materialPlans MaterialPlan[]
|
||||
materialUsages MaterialUsage[]
|
||||
workSessions WorkSession[]
|
||||
notes ActivityNote[]
|
||||
photos Photo[]
|
||||
voiceNotes VoiceNote[]
|
||||
reports Report[]
|
||||
documents Document[]
|
||||
|
||||
@@unique([tenantId, number])
|
||||
@@index([tenantId, status])
|
||||
@@index([tenantId, assignedTeamId, status])
|
||||
@@index([tenantId, plannedStart])
|
||||
@@index([tenantId, customerId])
|
||||
@@index([tenantId, siteId])
|
||||
@@map("work_orders")
|
||||
}
|
||||
|
||||
model WorkOrderAssignee {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
workOrderId String @map("work_order_id")
|
||||
userId String @map("user_id")
|
||||
|
||||
workOrder WorkOrder @relation(fields: [workOrderId], references: [id], onDelete: Cascade)
|
||||
user User @relation("WorkOrderAssignments", fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([workOrderId, userId])
|
||||
@@index([tenantId, userId])
|
||||
@@map("work_order_assignees")
|
||||
}
|
||||
|
||||
model WorkOrderStatusChange {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
workOrderId String @map("work_order_id")
|
||||
fromStatus WorkOrderStatus? @map("from_status")
|
||||
toStatus WorkOrderStatus @map("to_status")
|
||||
actorId String? @map("actor_id")
|
||||
reason String?
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
workOrder WorkOrder @relation(fields: [workOrderId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([tenantId, workOrderId])
|
||||
@@map("work_order_status_changes")
|
||||
}
|
||||
|
||||
model ChecklistItem {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
workOrderId String @map("work_order_id")
|
||||
key String
|
||||
label String
|
||||
required Boolean @default(false)
|
||||
requiresPhoto Boolean @default(false) @map("requires_photo")
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
checked Boolean @default(false)
|
||||
checkedById String? @map("checked_by_id")
|
||||
checkedAt DateTime? @map("checked_at")
|
||||
comment String?
|
||||
|
||||
workOrder WorkOrder @relation(fields: [workOrderId], references: [id], onDelete: Cascade)
|
||||
photos Photo[]
|
||||
|
||||
@@index([tenantId, workOrderId])
|
||||
@@map("checklist_items")
|
||||
}
|
||||
|
||||
model PhotoRequirement {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
workOrderId String @map("work_order_id")
|
||||
key String // ausgangszustand | typenschild | leitungsverlauf | zwischenschritt | fertige_montage | funktionspruefung | arbeitsbereich_abschluss | custom
|
||||
label String
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
|
||||
workOrder WorkOrder @relation(fields: [workOrderId], references: [id], onDelete: Cascade)
|
||||
photos Photo[]
|
||||
|
||||
@@unique([workOrderId, key])
|
||||
@@index([tenantId])
|
||||
@@map("photo_requirements")
|
||||
}
|
||||
|
||||
// ---------- Materials ----------
|
||||
|
||||
model MaterialPlan {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
workOrderId String @map("work_order_id")
|
||||
name String
|
||||
articleNumber String? @map("article_number")
|
||||
plannedQuantity Decimal @map("planned_quantity") @db.Decimal(12, 3)
|
||||
unit String
|
||||
notes String?
|
||||
sortOrder Int @default(0) @map("sort_order")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
workOrder WorkOrder @relation(fields: [workOrderId], references: [id], onDelete: Cascade)
|
||||
usages MaterialUsage[]
|
||||
|
||||
@@index([tenantId, workOrderId])
|
||||
@@map("material_plans")
|
||||
}
|
||||
|
||||
enum MaterialUsageStatus {
|
||||
fully_used
|
||||
partially_used
|
||||
not_used
|
||||
additional
|
||||
}
|
||||
|
||||
model MaterialUsage {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
workOrderId String @map("work_order_id")
|
||||
materialPlanId String? @map("material_plan_id")
|
||||
workSessionId String? @map("work_session_id")
|
||||
name String
|
||||
articleNumber String? @map("article_number")
|
||||
actualQuantity Decimal @map("actual_quantity") @db.Decimal(12, 3)
|
||||
unit String
|
||||
usageStatus MaterialUsageStatus @map("usage_status")
|
||||
deviationReason String? @map("deviation_reason")
|
||||
notes String?
|
||||
photoId String? @map("photo_id")
|
||||
recordedById String? @map("recorded_by_id")
|
||||
clientId String? @unique @map("client_id") // offline local id
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
workOrder WorkOrder @relation(fields: [workOrderId], references: [id], onDelete: Cascade)
|
||||
materialPlan MaterialPlan? @relation(fields: [materialPlanId], references: [id], onDelete: SetNull)
|
||||
workSession WorkSession? @relation(fields: [workSessionId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([tenantId, workOrderId])
|
||||
@@map("material_usages")
|
||||
}
|
||||
|
||||
// ---------- Field execution ----------
|
||||
|
||||
enum WorkSessionStatus {
|
||||
en_route
|
||||
running
|
||||
paused
|
||||
ended
|
||||
}
|
||||
|
||||
model WorkSession {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
workOrderId String @map("work_order_id")
|
||||
userId String @map("user_id")
|
||||
teamId String? @map("team_id")
|
||||
status WorkSessionStatus @default(running)
|
||||
startedAt DateTime @map("started_at")
|
||||
endedAt DateTime? @map("ended_at")
|
||||
startLat Float? @map("start_lat")
|
||||
startLng Float? @map("start_lng")
|
||||
startedOffline Boolean @default(false) @map("started_offline")
|
||||
deviceInfo String? @map("device_info")
|
||||
clientId String? @unique @map("client_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
workOrder WorkOrder @relation(fields: [workOrderId], references: [id], onDelete: Cascade)
|
||||
user User @relation("WorkSessionUser", fields: [userId], references: [id], onDelete: Restrict)
|
||||
entries TimeEntry[]
|
||||
materials MaterialUsage[]
|
||||
|
||||
@@index([tenantId, workOrderId])
|
||||
@@index([tenantId, userId, status])
|
||||
@@map("work_sessions")
|
||||
}
|
||||
|
||||
enum TimeEntryType {
|
||||
travel
|
||||
work
|
||||
break
|
||||
material_procurement
|
||||
return_travel
|
||||
interruption
|
||||
}
|
||||
|
||||
model TimeEntry {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
workSessionId String @map("work_session_id")
|
||||
userId String @map("user_id")
|
||||
type TimeEntryType @default(work)
|
||||
startedAt DateTime @map("started_at")
|
||||
endedAt DateTime? @map("ended_at")
|
||||
// manual corrections must be audited (spec §12.2)
|
||||
corrected Boolean @default(false)
|
||||
correctionReason String? @map("correction_reason")
|
||||
correctedById String? @map("corrected_by_id")
|
||||
clientId String? @unique @map("client_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
workSession WorkSession @relation(fields: [workSessionId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([tenantId, workSessionId])
|
||||
@@index([tenantId, userId, startedAt])
|
||||
@@map("time_entries")
|
||||
}
|
||||
|
||||
enum ActivityNoteKind {
|
||||
work_done
|
||||
deviation
|
||||
problem
|
||||
additional_work
|
||||
not_executable
|
||||
follow_up
|
||||
recommendation
|
||||
customer_note
|
||||
general
|
||||
}
|
||||
|
||||
model ActivityNote {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
workOrderId String @map("work_order_id")
|
||||
authorId String? @map("author_id")
|
||||
kind ActivityNoteKind @default(general)
|
||||
text String
|
||||
voiceNoteId String? @unique @map("voice_note_id")
|
||||
clientId String? @unique @map("client_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
|
||||
workOrder WorkOrder @relation(fields: [workOrderId], references: [id], onDelete: Cascade)
|
||||
voiceNote VoiceNote? @relation(fields: [voiceNoteId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([tenantId, workOrderId])
|
||||
@@map("activity_notes")
|
||||
}
|
||||
|
||||
// ---------- Files ----------
|
||||
|
||||
enum DocumentCategory {
|
||||
order_confirmation
|
||||
technical_drawing
|
||||
floor_plan
|
||||
wiring_diagram
|
||||
assembly_instructions
|
||||
safety_document
|
||||
product_document
|
||||
customer_note
|
||||
work_record
|
||||
daily_report
|
||||
completion_report
|
||||
customer_approval
|
||||
photo
|
||||
voice_note
|
||||
signature
|
||||
other
|
||||
}
|
||||
|
||||
enum DocumentVisibility {
|
||||
backoffice_only
|
||||
team_lead
|
||||
team
|
||||
customer_report
|
||||
}
|
||||
|
||||
enum UploadStatus {
|
||||
pending
|
||||
uploaded
|
||||
failed
|
||||
quarantined
|
||||
}
|
||||
|
||||
model Document {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
customerId String? @map("customer_id")
|
||||
siteId String? @map("site_id")
|
||||
workOrderId String? @map("work_order_id")
|
||||
category DocumentCategory
|
||||
title String?
|
||||
fileName String @map("file_name")
|
||||
storageKey String @map("storage_key") // <tenantId>/...
|
||||
previewKey String? @map("preview_key") // thumbnail
|
||||
mimeType String @map("mime_type")
|
||||
fileSize Int @map("file_size")
|
||||
checksum String // sha256 hex
|
||||
version Int @default(1)
|
||||
// groups versions of the same logical document
|
||||
lineageId String @map("lineage_id")
|
||||
visibility DocumentVisibility @default(team)
|
||||
approvalStatus String? @map("approval_status") // draft | approved
|
||||
uploadStatus UploadStatus @default(uploaded) @map("upload_status")
|
||||
uploadedById String? @map("uploaded_by_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
deletedAt DateTime? @map("deleted_at")
|
||||
|
||||
customer Customer? @relation(fields: [customerId], references: [id], onDelete: SetNull)
|
||||
site Site? @relation(fields: [siteId], references: [id], onDelete: SetNull)
|
||||
workOrder WorkOrder? @relation(fields: [workOrderId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@unique([lineageId, version])
|
||||
@@index([tenantId, workOrderId])
|
||||
@@index([tenantId, siteId])
|
||||
@@index([tenantId, customerId])
|
||||
@@map("documents")
|
||||
}
|
||||
|
||||
enum PhotoPhase {
|
||||
before
|
||||
during
|
||||
after
|
||||
}
|
||||
|
||||
model Photo {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
workOrderId String @map("work_order_id")
|
||||
workSessionId String? @map("work_session_id")
|
||||
documentId String @unique @map("document_id")
|
||||
checklistItemId String? @map("checklist_item_id")
|
||||
photoRequirementId String? @map("photo_requirement_id")
|
||||
phase PhotoPhase?
|
||||
comment String?
|
||||
takenAt DateTime @map("taken_at")
|
||||
latitude Float?
|
||||
longitude Float?
|
||||
takenById String? @map("taken_by_id")
|
||||
includeInReport Boolean @default(true) @map("include_in_report")
|
||||
clientId String? @unique @map("client_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
workOrder WorkOrder @relation(fields: [workOrderId], references: [id], onDelete: Cascade)
|
||||
checklistItem ChecklistItem? @relation(fields: [checklistItemId], references: [id], onDelete: SetNull)
|
||||
photoRequirement PhotoRequirement? @relation(fields: [photoRequirementId], references: [id], onDelete: SetNull)
|
||||
|
||||
@@index([tenantId, workOrderId])
|
||||
@@map("photos")
|
||||
}
|
||||
|
||||
enum TranscriptionStatus {
|
||||
pending
|
||||
done
|
||||
failed
|
||||
disabled
|
||||
}
|
||||
|
||||
model VoiceNote {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
workOrderId String @map("work_order_id")
|
||||
workSessionId String? @map("work_session_id")
|
||||
documentId String @unique @map("document_id")
|
||||
durationSeconds Int? @map("duration_seconds")
|
||||
transcript String?
|
||||
transcriptionStatus TranscriptionStatus @default(pending) @map("transcription_status")
|
||||
transcriptionModel String? @map("transcription_model")
|
||||
recordedById String? @map("recorded_by_id")
|
||||
recordedAt DateTime @map("recorded_at")
|
||||
clientId String? @unique @map("client_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
workOrder WorkOrder @relation(fields: [workOrderId], references: [id], onDelete: Cascade)
|
||||
activityNote ActivityNote?
|
||||
|
||||
@@index([tenantId, workOrderId])
|
||||
@@map("voice_notes")
|
||||
}
|
||||
|
||||
// ---------- Reports & signatures ----------
|
||||
|
||||
enum ReportType {
|
||||
daily
|
||||
completion
|
||||
}
|
||||
|
||||
enum ReportStatus {
|
||||
draft // incl. Lotse-Entwurf
|
||||
submitted // by technician, "Zur Prüfung"
|
||||
team_approved
|
||||
approved // final → immutable PDF
|
||||
rejected // correction requested
|
||||
superseded
|
||||
}
|
||||
|
||||
model Report {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
workOrderId String @map("work_order_id")
|
||||
type ReportType
|
||||
reportDate DateTime @map("report_date") @db.Date
|
||||
version Int @default(1)
|
||||
// groups versions of the same report
|
||||
lineageId String @map("lineage_id")
|
||||
status ReportStatus @default(draft)
|
||||
// structured snapshot (see src/lib/reports/content.ts ReportContent)
|
||||
content Json
|
||||
aiDrafted Boolean @default(false) @map("ai_drafted")
|
||||
aiGenerationId String? @map("ai_generation_id")
|
||||
pdfDocumentId String? @unique @map("pdf_document_id")
|
||||
pdfChecksum String? @map("pdf_checksum")
|
||||
createdById String? @map("created_by_id")
|
||||
submittedAt DateTime? @map("submitted_at")
|
||||
teamApprovedById String? @map("team_approved_by_id")
|
||||
teamApprovedAt DateTime? @map("team_approved_at")
|
||||
approvedById String? @map("approved_by_id")
|
||||
approvedAt DateTime? @map("approved_at")
|
||||
rejectionReason String? @map("rejection_reason")
|
||||
clientId String? @unique @map("client_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
workOrder WorkOrder @relation(fields: [workOrderId], references: [id], onDelete: Restrict)
|
||||
signature Signature?
|
||||
|
||||
@@unique([lineageId, version])
|
||||
@@index([tenantId, workOrderId])
|
||||
@@index([tenantId, status])
|
||||
@@map("reports")
|
||||
}
|
||||
|
||||
enum SignatureOutcome {
|
||||
signed
|
||||
customer_absent
|
||||
refused
|
||||
later
|
||||
not_required
|
||||
}
|
||||
|
||||
model Signature {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
reportId String @unique @map("report_id")
|
||||
outcome SignatureOutcome
|
||||
signerName String? @map("signer_name")
|
||||
signerRole String? @map("signer_role")
|
||||
imageDocumentId String? @unique @map("image_document_id") // PNG of the drawn signature
|
||||
confirmationText String? @map("confirmation_text")
|
||||
reason String? // required for absent/refused/later
|
||||
signedAt DateTime @map("signed_at")
|
||||
capturedById String? @map("captured_by_id")
|
||||
clientId String? @unique @map("client_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
report Report @relation(fields: [reportId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([tenantId])
|
||||
@@map("signatures")
|
||||
}
|
||||
|
||||
// ---------- PDF import ----------
|
||||
|
||||
enum ImportStatus {
|
||||
uploaded
|
||||
processing
|
||||
review_required // "Entwurf – Prüfung erforderlich"
|
||||
confirmed
|
||||
failed
|
||||
discarded
|
||||
}
|
||||
|
||||
model ImportJob {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
documentId String @unique @map("document_id") // original file (kept forever)
|
||||
status ImportStatus @default(uploaded)
|
||||
errorMessage String? @map("error_message")
|
||||
extractedText String? @map("extracted_text")
|
||||
// [{ field, value, confidence }] + positions — see src/lib/imports/extraction.ts
|
||||
extraction Json?
|
||||
extractionVersion String? @map("extraction_version")
|
||||
extractionModel String? @map("extraction_model")
|
||||
provider String?
|
||||
// user corrections: { field: { from, to } }
|
||||
corrections Json?
|
||||
// duplicate candidates: [{ customerId, score, reasons[] }]
|
||||
duplicateCandidates Json? @map("duplicate_candidates")
|
||||
importedById String? @map("imported_by_id")
|
||||
confirmedById String? @map("confirmed_by_id")
|
||||
confirmedAt DateTime? @map("confirmed_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
createdWorkOrder WorkOrder? @relation("ImportCreatedOrder")
|
||||
|
||||
@@index([tenantId, status])
|
||||
@@map("import_jobs")
|
||||
}
|
||||
|
||||
// ---------- Notifications ----------
|
||||
|
||||
model Notification {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
userId String @map("user_id")
|
||||
type String // see src/lib/events.ts EVENT_TYPES
|
||||
title String
|
||||
message String
|
||||
entityType String? @map("entity_type")
|
||||
entityId String? @map("entity_id")
|
||||
link String?
|
||||
readAt DateTime? @map("read_at")
|
||||
emailedAt DateTime? @map("emailed_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
user User @relation("UserNotifications", fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([tenantId, userId, readAt])
|
||||
@@map("notifications")
|
||||
}
|
||||
|
||||
// ---------- Offline sync ----------
|
||||
|
||||
enum SyncOpStatus {
|
||||
applied
|
||||
conflict
|
||||
rejected
|
||||
}
|
||||
|
||||
model SyncOperation {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
userId String @map("user_id")
|
||||
clientOpId String @map("client_op_id") // idempotency key from device
|
||||
opType String @map("op_type")
|
||||
entityType String? @map("entity_type")
|
||||
entityId String? @map("entity_id")
|
||||
baseVersion Int? @map("base_version")
|
||||
payload Json
|
||||
status SyncOpStatus
|
||||
result Json?
|
||||
errorCode String? @map("error_code")
|
||||
clientCreatedAt DateTime @map("client_created_at")
|
||||
receivedAt DateTime @default(now()) @map("received_at")
|
||||
resolvedById String? @map("resolved_by_id")
|
||||
resolvedAt DateTime? @map("resolved_at")
|
||||
|
||||
@@unique([tenantId, clientOpId])
|
||||
@@index([tenantId, status])
|
||||
@@map("sync_operations")
|
||||
}
|
||||
|
||||
// ---------- Lotse (AI) ----------
|
||||
|
||||
model AiGeneration {
|
||||
id String @id @default(cuid())
|
||||
tenantId String @map("tenant_id")
|
||||
kind String // report_draft | voice_summary | completeness_check | import_extraction
|
||||
provider String
|
||||
model String
|
||||
entityType String? @map("entity_type")
|
||||
entityId String? @map("entity_id")
|
||||
input Json?
|
||||
output Json?
|
||||
inputTokens Int? @map("input_tokens")
|
||||
outputTokens Int? @map("output_tokens")
|
||||
createdById String? @map("created_by_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@index([tenantId, entityType, entityId])
|
||||
@@map("ai_generations")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user