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:
2026-09-14 11:49:21 +02:00
co-authored by Claude Opus 5
parent 1701db0a62
commit bf4456718e
21 changed files with 2670 additions and 12 deletions
@@ -0,0 +1,926 @@
-- Craftvia domain model (MVP). Generated via prisma migrate diff + RLS block.
-- CreateEnum
CREATE TYPE "CustomerStatus" AS ENUM ('active', 'inactive', 'provisional', 'merged');
-- CreateEnum
CREATE TYPE "SiteStatus" AS ENUM ('active', 'inactive', 'provisional');
-- CreateEnum
CREATE TYPE "TeamStatus" AS ENUM ('active', 'inactive');
-- CreateEnum
CREATE TYPE "WorkOrderStatus" AS ENUM ('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');
-- CreateEnum
CREATE TYPE "WorkOrderPriority" AS ENUM ('low', 'normal', 'high', 'urgent');
-- CreateEnum
CREATE TYPE "MaterialUsageStatus" AS ENUM ('fully_used', 'partially_used', 'not_used', 'additional');
-- CreateEnum
CREATE TYPE "WorkSessionStatus" AS ENUM ('en_route', 'running', 'paused', 'ended');
-- CreateEnum
CREATE TYPE "TimeEntryType" AS ENUM ('travel', 'work', 'break', 'material_procurement', 'return_travel', 'interruption');
-- CreateEnum
CREATE TYPE "ActivityNoteKind" AS ENUM ('work_done', 'deviation', 'problem', 'additional_work', 'not_executable', 'follow_up', 'recommendation', 'customer_note', 'general');
-- CreateEnum
CREATE TYPE "DocumentCategory" AS ENUM ('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');
-- CreateEnum
CREATE TYPE "DocumentVisibility" AS ENUM ('backoffice_only', 'team_lead', 'team', 'customer_report');
-- CreateEnum
CREATE TYPE "UploadStatus" AS ENUM ('pending', 'uploaded', 'failed', 'quarantined');
-- CreateEnum
CREATE TYPE "PhotoPhase" AS ENUM ('before', 'during', 'after');
-- CreateEnum
CREATE TYPE "TranscriptionStatus" AS ENUM ('pending', 'done', 'failed', 'disabled');
-- CreateEnum
CREATE TYPE "ReportType" AS ENUM ('daily', 'completion');
-- CreateEnum
CREATE TYPE "ReportStatus" AS ENUM ('draft', 'submitted', 'team_approved', 'approved', 'rejected', 'superseded');
-- CreateEnum
CREATE TYPE "SignatureOutcome" AS ENUM ('signed', 'customer_absent', 'refused', 'later', 'not_required');
-- CreateEnum
CREATE TYPE "ImportStatus" AS ENUM ('uploaded', 'processing', 'review_required', 'confirmed', 'failed', 'discarded');
-- CreateEnum
CREATE TYPE "SyncOpStatus" AS ENUM ('applied', 'conflict', 'rejected');
-- CreateTable
CREATE TABLE "number_sequences" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"key" TEXT NOT NULL,
"prefix" TEXT NOT NULL DEFAULT '',
"next_value" INTEGER NOT NULL DEFAULT 1,
"padding" INTEGER NOT NULL DEFAULT 5,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "number_sequences_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "order_types" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"key" TEXT NOT NULL,
"name" TEXT NOT NULL,
"active" BOOLEAN NOT NULL DEFAULT true,
"signature_required" BOOLEAN NOT NULL DEFAULT true,
"sort_order" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "order_types_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "checklist_templates" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"order_type_id" TEXT,
"name" TEXT NOT NULL,
"active" BOOLEAN NOT NULL DEFAULT true,
"items" JSONB NOT NULL DEFAULT '[]',
"required_photos" JSONB NOT NULL DEFAULT '[]',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "checklist_templates_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "customers" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"customer_number" TEXT,
"company_name" TEXT,
"salutation" TEXT,
"first_name" TEXT,
"last_name" TEXT,
"street" TEXT,
"house_number" TEXT,
"postal_code" TEXT,
"city" TEXT,
"country" TEXT NOT NULL DEFAULT 'DE',
"phone" TEXT,
"mobile" TEXT,
"email" TEXT,
"notes" TEXT,
"billing_notes" TEXT,
"status" "CustomerStatus" NOT NULL DEFAULT 'active',
"is_provisional" BOOLEAN NOT NULL DEFAULT false,
"merged_into_id" TEXT,
"created_by_id" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"deleted_at" TIMESTAMP(3),
CONSTRAINT "customers_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "contacts" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"customer_id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"role" TEXT,
"phone" TEXT,
"mobile" TEXT,
"email" TEXT,
"preferred_channel" TEXT,
"notes" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"deleted_at" TIMESTAMP(3),
CONSTRAINT "contacts_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "sites" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"customer_id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"street" TEXT,
"house_number" TEXT,
"postal_code" TEXT,
"city" TEXT,
"country" TEXT NOT NULL DEFAULT 'DE',
"contact_id" TEXT,
"on_site_contact" TEXT,
"phone" TEXT,
"access_notes" TEXT,
"parking_notes" TEXT,
"safety_notes" TEXT,
"technical_notes" TEXT,
"status" "SiteStatus" NOT NULL DEFAULT 'active',
"latitude" DOUBLE PRECISION,
"longitude" DOUBLE PRECISION,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"deleted_at" TIMESTAMP(3),
CONSTRAINT "sites_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "teams" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"leader_user_id" TEXT,
"status" "TeamStatus" NOT NULL DEFAULT 'active',
"phone" TEXT,
"vehicle" TEXT,
"area" TEXT,
"notes" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"deleted_at" TIMESTAMP(3),
CONSTRAINT "teams_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "team_members" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"team_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"valid_from" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"valid_to" TIMESTAMP(3),
CONSTRAINT "team_members_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "work_orders" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"number" TEXT NOT NULL,
"external_order_number" TEXT,
"offer_number" TEXT,
"customer_id" TEXT NOT NULL,
"site_id" TEXT,
"contact_id" TEXT,
"order_type_id" TEXT,
"priority" "WorkOrderPriority" NOT NULL DEFAULT 'normal',
"status" "WorkOrderStatus" NOT NULL DEFAULT 'draft',
"title" TEXT NOT NULL,
"description" TEXT,
"scope" TEXT,
"planned_start" TIMESTAMP(3),
"planned_end" TIMESTAMP(3),
"assigned_team_id" TEXT,
"team_lead_user_id" TEXT,
"signature_required" BOOLEAN NOT NULL DEFAULT true,
"billing_type" TEXT,
"internal_notes" TEXT,
"technician_notes" TEXT,
"is_emergency" BOOLEAN NOT NULL DEFAULT false,
"emergency_reason" TEXT,
"source_import_id" TEXT,
"follow_up_work" TEXT,
"version" INTEGER NOT NULL DEFAULT 1,
"created_by_id" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"deleted_at" TIMESTAMP(3),
CONSTRAINT "work_orders_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "work_order_assignees" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"work_order_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
CONSTRAINT "work_order_assignees_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "work_order_status_changes" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"work_order_id" TEXT NOT NULL,
"from_status" "WorkOrderStatus",
"to_status" "WorkOrderStatus" NOT NULL,
"actor_id" TEXT,
"reason" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "work_order_status_changes_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "checklist_items" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"work_order_id" TEXT NOT NULL,
"key" TEXT NOT NULL,
"label" TEXT NOT NULL,
"required" BOOLEAN NOT NULL DEFAULT false,
"requires_photo" BOOLEAN NOT NULL DEFAULT false,
"sort_order" INTEGER NOT NULL DEFAULT 0,
"checked" BOOLEAN NOT NULL DEFAULT false,
"checked_by_id" TEXT,
"checked_at" TIMESTAMP(3),
"comment" TEXT,
CONSTRAINT "checklist_items_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "photo_requirements" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"work_order_id" TEXT NOT NULL,
"key" TEXT NOT NULL,
"label" TEXT NOT NULL,
"sort_order" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "photo_requirements_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "material_plans" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"work_order_id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"article_number" TEXT,
"planned_quantity" DECIMAL(12,3) NOT NULL,
"unit" TEXT NOT NULL,
"notes" TEXT,
"sort_order" INTEGER NOT NULL DEFAULT 0,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "material_plans_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "material_usages" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"work_order_id" TEXT NOT NULL,
"material_plan_id" TEXT,
"work_session_id" TEXT,
"name" TEXT NOT NULL,
"article_number" TEXT,
"actual_quantity" DECIMAL(12,3) NOT NULL,
"unit" TEXT NOT NULL,
"usage_status" "MaterialUsageStatus" NOT NULL,
"deviation_reason" TEXT,
"notes" TEXT,
"photo_id" TEXT,
"recorded_by_id" TEXT,
"client_id" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "material_usages_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "work_sessions" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"work_order_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"team_id" TEXT,
"status" "WorkSessionStatus" NOT NULL DEFAULT 'running',
"started_at" TIMESTAMP(3) NOT NULL,
"ended_at" TIMESTAMP(3),
"start_lat" DOUBLE PRECISION,
"start_lng" DOUBLE PRECISION,
"started_offline" BOOLEAN NOT NULL DEFAULT false,
"device_info" TEXT,
"client_id" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "work_sessions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "time_entries" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"work_session_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"type" "TimeEntryType" NOT NULL DEFAULT 'work',
"started_at" TIMESTAMP(3) NOT NULL,
"ended_at" TIMESTAMP(3),
"corrected" BOOLEAN NOT NULL DEFAULT false,
"correction_reason" TEXT,
"corrected_by_id" TEXT,
"client_id" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "time_entries_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "activity_notes" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"work_order_id" TEXT NOT NULL,
"author_id" TEXT,
"kind" "ActivityNoteKind" NOT NULL DEFAULT 'general',
"text" TEXT NOT NULL,
"voice_note_id" TEXT,
"client_id" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"deleted_at" TIMESTAMP(3),
CONSTRAINT "activity_notes_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "documents" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"customer_id" TEXT,
"site_id" TEXT,
"work_order_id" TEXT,
"category" "DocumentCategory" NOT NULL,
"title" TEXT,
"file_name" TEXT NOT NULL,
"storage_key" TEXT NOT NULL,
"preview_key" TEXT,
"mime_type" TEXT NOT NULL,
"file_size" INTEGER NOT NULL,
"checksum" TEXT NOT NULL,
"version" INTEGER NOT NULL DEFAULT 1,
"lineage_id" TEXT NOT NULL,
"visibility" "DocumentVisibility" NOT NULL DEFAULT 'team',
"approval_status" TEXT,
"upload_status" "UploadStatus" NOT NULL DEFAULT 'uploaded',
"uploaded_by_id" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"deleted_at" TIMESTAMP(3),
CONSTRAINT "documents_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "photos" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"work_order_id" TEXT NOT NULL,
"work_session_id" TEXT,
"document_id" TEXT NOT NULL,
"checklist_item_id" TEXT,
"photo_requirement_id" TEXT,
"phase" "PhotoPhase",
"comment" TEXT,
"taken_at" TIMESTAMP(3) NOT NULL,
"latitude" DOUBLE PRECISION,
"longitude" DOUBLE PRECISION,
"taken_by_id" TEXT,
"include_in_report" BOOLEAN NOT NULL DEFAULT true,
"client_id" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "photos_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "voice_notes" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"work_order_id" TEXT NOT NULL,
"work_session_id" TEXT,
"document_id" TEXT NOT NULL,
"duration_seconds" INTEGER,
"transcript" TEXT,
"transcription_status" "TranscriptionStatus" NOT NULL DEFAULT 'pending',
"transcription_model" TEXT,
"recorded_by_id" TEXT,
"recorded_at" TIMESTAMP(3) NOT NULL,
"client_id" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "voice_notes_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "reports" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"work_order_id" TEXT NOT NULL,
"type" "ReportType" NOT NULL,
"report_date" DATE NOT NULL,
"version" INTEGER NOT NULL DEFAULT 1,
"lineage_id" TEXT NOT NULL,
"status" "ReportStatus" NOT NULL DEFAULT 'draft',
"content" JSONB NOT NULL,
"ai_drafted" BOOLEAN NOT NULL DEFAULT false,
"ai_generation_id" TEXT,
"pdf_document_id" TEXT,
"pdf_checksum" TEXT,
"created_by_id" TEXT,
"submitted_at" TIMESTAMP(3),
"team_approved_by_id" TEXT,
"team_approved_at" TIMESTAMP(3),
"approved_by_id" TEXT,
"approved_at" TIMESTAMP(3),
"rejection_reason" TEXT,
"client_id" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "reports_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "signatures" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"report_id" TEXT NOT NULL,
"outcome" "SignatureOutcome" NOT NULL,
"signer_name" TEXT,
"signer_role" TEXT,
"image_document_id" TEXT,
"confirmation_text" TEXT,
"reason" TEXT,
"signed_at" TIMESTAMP(3) NOT NULL,
"captured_by_id" TEXT,
"client_id" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "signatures_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "import_jobs" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"document_id" TEXT NOT NULL,
"status" "ImportStatus" NOT NULL DEFAULT 'uploaded',
"error_message" TEXT,
"extracted_text" TEXT,
"extraction" JSONB,
"extraction_version" TEXT,
"extraction_model" TEXT,
"provider" TEXT,
"corrections" JSONB,
"duplicate_candidates" JSONB,
"imported_by_id" TEXT,
"confirmed_by_id" TEXT,
"confirmed_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "import_jobs_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "notifications" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"type" TEXT NOT NULL,
"title" TEXT NOT NULL,
"message" TEXT NOT NULL,
"entity_type" TEXT,
"entity_id" TEXT,
"link" TEXT,
"read_at" TIMESTAMP(3),
"emailed_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "notifications_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "sync_operations" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"client_op_id" TEXT NOT NULL,
"op_type" TEXT NOT NULL,
"entity_type" TEXT,
"entity_id" TEXT,
"base_version" INTEGER,
"payload" JSONB NOT NULL,
"status" "SyncOpStatus" NOT NULL,
"result" JSONB,
"error_code" TEXT,
"client_created_at" TIMESTAMP(3) NOT NULL,
"received_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"resolved_by_id" TEXT,
"resolved_at" TIMESTAMP(3),
CONSTRAINT "sync_operations_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ai_generations" (
"id" TEXT NOT NULL,
"tenant_id" TEXT NOT NULL,
"kind" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"model" TEXT NOT NULL,
"entity_type" TEXT,
"entity_id" TEXT,
"input" JSONB,
"output" JSONB,
"input_tokens" INTEGER,
"output_tokens" INTEGER,
"created_by_id" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ai_generations_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "number_sequences_tenant_id_key_key" ON "number_sequences"("tenant_id", "key");
-- CreateIndex
CREATE UNIQUE INDEX "order_types_tenant_id_key_key" ON "order_types"("tenant_id", "key");
-- CreateIndex
CREATE INDEX "checklist_templates_tenant_id_idx" ON "checklist_templates"("tenant_id");
-- CreateIndex
CREATE INDEX "customers_tenant_id_status_idx" ON "customers"("tenant_id", "status");
-- CreateIndex
CREATE INDEX "customers_tenant_id_company_name_idx" ON "customers"("tenant_id", "company_name");
-- CreateIndex
CREATE INDEX "customers_tenant_id_last_name_idx" ON "customers"("tenant_id", "last_name");
-- CreateIndex
CREATE UNIQUE INDEX "customers_tenant_id_customer_number_key" ON "customers"("tenant_id", "customer_number");
-- CreateIndex
CREATE INDEX "contacts_tenant_id_customer_id_idx" ON "contacts"("tenant_id", "customer_id");
-- CreateIndex
CREATE INDEX "sites_tenant_id_customer_id_idx" ON "sites"("tenant_id", "customer_id");
-- CreateIndex
CREATE UNIQUE INDEX "teams_tenant_id_name_key" ON "teams"("tenant_id", "name");
-- CreateIndex
CREATE INDEX "team_members_tenant_id_user_id_idx" ON "team_members"("tenant_id", "user_id");
-- CreateIndex
CREATE INDEX "team_members_team_id_idx" ON "team_members"("team_id");
-- CreateIndex
CREATE UNIQUE INDEX "work_orders_source_import_id_key" ON "work_orders"("source_import_id");
-- CreateIndex
CREATE INDEX "work_orders_tenant_id_status_idx" ON "work_orders"("tenant_id", "status");
-- CreateIndex
CREATE INDEX "work_orders_tenant_id_assigned_team_id_status_idx" ON "work_orders"("tenant_id", "assigned_team_id", "status");
-- CreateIndex
CREATE INDEX "work_orders_tenant_id_planned_start_idx" ON "work_orders"("tenant_id", "planned_start");
-- CreateIndex
CREATE INDEX "work_orders_tenant_id_customer_id_idx" ON "work_orders"("tenant_id", "customer_id");
-- CreateIndex
CREATE INDEX "work_orders_tenant_id_site_id_idx" ON "work_orders"("tenant_id", "site_id");
-- CreateIndex
CREATE UNIQUE INDEX "work_orders_tenant_id_number_key" ON "work_orders"("tenant_id", "number");
-- CreateIndex
CREATE INDEX "work_order_assignees_tenant_id_user_id_idx" ON "work_order_assignees"("tenant_id", "user_id");
-- CreateIndex
CREATE UNIQUE INDEX "work_order_assignees_work_order_id_user_id_key" ON "work_order_assignees"("work_order_id", "user_id");
-- CreateIndex
CREATE INDEX "work_order_status_changes_tenant_id_work_order_id_idx" ON "work_order_status_changes"("tenant_id", "work_order_id");
-- CreateIndex
CREATE INDEX "checklist_items_tenant_id_work_order_id_idx" ON "checklist_items"("tenant_id", "work_order_id");
-- CreateIndex
CREATE INDEX "photo_requirements_tenant_id_idx" ON "photo_requirements"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "photo_requirements_work_order_id_key_key" ON "photo_requirements"("work_order_id", "key");
-- CreateIndex
CREATE INDEX "material_plans_tenant_id_work_order_id_idx" ON "material_plans"("tenant_id", "work_order_id");
-- CreateIndex
CREATE UNIQUE INDEX "material_usages_client_id_key" ON "material_usages"("client_id");
-- CreateIndex
CREATE INDEX "material_usages_tenant_id_work_order_id_idx" ON "material_usages"("tenant_id", "work_order_id");
-- CreateIndex
CREATE UNIQUE INDEX "work_sessions_client_id_key" ON "work_sessions"("client_id");
-- CreateIndex
CREATE INDEX "work_sessions_tenant_id_work_order_id_idx" ON "work_sessions"("tenant_id", "work_order_id");
-- CreateIndex
CREATE INDEX "work_sessions_tenant_id_user_id_status_idx" ON "work_sessions"("tenant_id", "user_id", "status");
-- CreateIndex
CREATE UNIQUE INDEX "time_entries_client_id_key" ON "time_entries"("client_id");
-- CreateIndex
CREATE INDEX "time_entries_tenant_id_work_session_id_idx" ON "time_entries"("tenant_id", "work_session_id");
-- CreateIndex
CREATE INDEX "time_entries_tenant_id_user_id_started_at_idx" ON "time_entries"("tenant_id", "user_id", "started_at");
-- CreateIndex
CREATE UNIQUE INDEX "activity_notes_voice_note_id_key" ON "activity_notes"("voice_note_id");
-- CreateIndex
CREATE UNIQUE INDEX "activity_notes_client_id_key" ON "activity_notes"("client_id");
-- CreateIndex
CREATE INDEX "activity_notes_tenant_id_work_order_id_idx" ON "activity_notes"("tenant_id", "work_order_id");
-- CreateIndex
CREATE INDEX "documents_tenant_id_work_order_id_idx" ON "documents"("tenant_id", "work_order_id");
-- CreateIndex
CREATE INDEX "documents_tenant_id_site_id_idx" ON "documents"("tenant_id", "site_id");
-- CreateIndex
CREATE INDEX "documents_tenant_id_customer_id_idx" ON "documents"("tenant_id", "customer_id");
-- CreateIndex
CREATE UNIQUE INDEX "documents_lineage_id_version_key" ON "documents"("lineage_id", "version");
-- CreateIndex
CREATE UNIQUE INDEX "photos_document_id_key" ON "photos"("document_id");
-- CreateIndex
CREATE UNIQUE INDEX "photos_client_id_key" ON "photos"("client_id");
-- CreateIndex
CREATE INDEX "photos_tenant_id_work_order_id_idx" ON "photos"("tenant_id", "work_order_id");
-- CreateIndex
CREATE UNIQUE INDEX "voice_notes_document_id_key" ON "voice_notes"("document_id");
-- CreateIndex
CREATE UNIQUE INDEX "voice_notes_client_id_key" ON "voice_notes"("client_id");
-- CreateIndex
CREATE INDEX "voice_notes_tenant_id_work_order_id_idx" ON "voice_notes"("tenant_id", "work_order_id");
-- CreateIndex
CREATE UNIQUE INDEX "reports_pdf_document_id_key" ON "reports"("pdf_document_id");
-- CreateIndex
CREATE UNIQUE INDEX "reports_client_id_key" ON "reports"("client_id");
-- CreateIndex
CREATE INDEX "reports_tenant_id_work_order_id_idx" ON "reports"("tenant_id", "work_order_id");
-- CreateIndex
CREATE INDEX "reports_tenant_id_status_idx" ON "reports"("tenant_id", "status");
-- CreateIndex
CREATE UNIQUE INDEX "reports_lineage_id_version_key" ON "reports"("lineage_id", "version");
-- CreateIndex
CREATE UNIQUE INDEX "signatures_report_id_key" ON "signatures"("report_id");
-- CreateIndex
CREATE UNIQUE INDEX "signatures_image_document_id_key" ON "signatures"("image_document_id");
-- CreateIndex
CREATE UNIQUE INDEX "signatures_client_id_key" ON "signatures"("client_id");
-- CreateIndex
CREATE INDEX "signatures_tenant_id_idx" ON "signatures"("tenant_id");
-- CreateIndex
CREATE UNIQUE INDEX "import_jobs_document_id_key" ON "import_jobs"("document_id");
-- CreateIndex
CREATE INDEX "import_jobs_tenant_id_status_idx" ON "import_jobs"("tenant_id", "status");
-- CreateIndex
CREATE INDEX "notifications_tenant_id_user_id_read_at_idx" ON "notifications"("tenant_id", "user_id", "read_at");
-- CreateIndex
CREATE INDEX "sync_operations_tenant_id_status_idx" ON "sync_operations"("tenant_id", "status");
-- CreateIndex
CREATE UNIQUE INDEX "sync_operations_tenant_id_client_op_id_key" ON "sync_operations"("tenant_id", "client_op_id");
-- CreateIndex
CREATE INDEX "ai_generations_tenant_id_entity_type_entity_id_idx" ON "ai_generations"("tenant_id", "entity_type", "entity_id");
-- AddForeignKey
ALTER TABLE "checklist_templates" ADD CONSTRAINT "checklist_templates_order_type_id_fkey" FOREIGN KEY ("order_type_id") REFERENCES "order_types"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "contacts" ADD CONSTRAINT "contacts_customer_id_fkey" FOREIGN KEY ("customer_id") REFERENCES "customers"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "sites" ADD CONSTRAINT "sites_customer_id_fkey" FOREIGN KEY ("customer_id") REFERENCES "customers"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "sites" ADD CONSTRAINT "sites_contact_id_fkey" FOREIGN KEY ("contact_id") REFERENCES "contacts"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "teams" ADD CONSTRAINT "teams_leader_user_id_fkey" FOREIGN KEY ("leader_user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "team_members" ADD CONSTRAINT "team_members_team_id_fkey" FOREIGN KEY ("team_id") REFERENCES "teams"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "team_members" ADD CONSTRAINT "team_members_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "work_orders" ADD CONSTRAINT "work_orders_customer_id_fkey" FOREIGN KEY ("customer_id") REFERENCES "customers"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "work_orders" ADD CONSTRAINT "work_orders_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "sites"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "work_orders" ADD CONSTRAINT "work_orders_contact_id_fkey" FOREIGN KEY ("contact_id") REFERENCES "contacts"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "work_orders" ADD CONSTRAINT "work_orders_order_type_id_fkey" FOREIGN KEY ("order_type_id") REFERENCES "order_types"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "work_orders" ADD CONSTRAINT "work_orders_assigned_team_id_fkey" FOREIGN KEY ("assigned_team_id") REFERENCES "teams"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "work_orders" ADD CONSTRAINT "work_orders_team_lead_user_id_fkey" FOREIGN KEY ("team_lead_user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "work_orders" ADD CONSTRAINT "work_orders_source_import_id_fkey" FOREIGN KEY ("source_import_id") REFERENCES "import_jobs"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "work_order_assignees" ADD CONSTRAINT "work_order_assignees_work_order_id_fkey" FOREIGN KEY ("work_order_id") REFERENCES "work_orders"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "work_order_assignees" ADD CONSTRAINT "work_order_assignees_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "work_order_status_changes" ADD CONSTRAINT "work_order_status_changes_work_order_id_fkey" FOREIGN KEY ("work_order_id") REFERENCES "work_orders"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "checklist_items" ADD CONSTRAINT "checklist_items_work_order_id_fkey" FOREIGN KEY ("work_order_id") REFERENCES "work_orders"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "photo_requirements" ADD CONSTRAINT "photo_requirements_work_order_id_fkey" FOREIGN KEY ("work_order_id") REFERENCES "work_orders"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "material_plans" ADD CONSTRAINT "material_plans_work_order_id_fkey" FOREIGN KEY ("work_order_id") REFERENCES "work_orders"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "material_usages" ADD CONSTRAINT "material_usages_work_order_id_fkey" FOREIGN KEY ("work_order_id") REFERENCES "work_orders"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "material_usages" ADD CONSTRAINT "material_usages_material_plan_id_fkey" FOREIGN KEY ("material_plan_id") REFERENCES "material_plans"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "material_usages" ADD CONSTRAINT "material_usages_work_session_id_fkey" FOREIGN KEY ("work_session_id") REFERENCES "work_sessions"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "work_sessions" ADD CONSTRAINT "work_sessions_work_order_id_fkey" FOREIGN KEY ("work_order_id") REFERENCES "work_orders"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "work_sessions" ADD CONSTRAINT "work_sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "time_entries" ADD CONSTRAINT "time_entries_work_session_id_fkey" FOREIGN KEY ("work_session_id") REFERENCES "work_sessions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "activity_notes" ADD CONSTRAINT "activity_notes_work_order_id_fkey" FOREIGN KEY ("work_order_id") REFERENCES "work_orders"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "activity_notes" ADD CONSTRAINT "activity_notes_voice_note_id_fkey" FOREIGN KEY ("voice_note_id") REFERENCES "voice_notes"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "documents" ADD CONSTRAINT "documents_customer_id_fkey" FOREIGN KEY ("customer_id") REFERENCES "customers"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "documents" ADD CONSTRAINT "documents_site_id_fkey" FOREIGN KEY ("site_id") REFERENCES "sites"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "documents" ADD CONSTRAINT "documents_work_order_id_fkey" FOREIGN KEY ("work_order_id") REFERENCES "work_orders"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "photos" ADD CONSTRAINT "photos_work_order_id_fkey" FOREIGN KEY ("work_order_id") REFERENCES "work_orders"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "photos" ADD CONSTRAINT "photos_checklist_item_id_fkey" FOREIGN KEY ("checklist_item_id") REFERENCES "checklist_items"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "photos" ADD CONSTRAINT "photos_photo_requirement_id_fkey" FOREIGN KEY ("photo_requirement_id") REFERENCES "photo_requirements"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "voice_notes" ADD CONSTRAINT "voice_notes_work_order_id_fkey" FOREIGN KEY ("work_order_id") REFERENCES "work_orders"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "reports" ADD CONSTRAINT "reports_work_order_id_fkey" FOREIGN KEY ("work_order_id") REFERENCES "work_orders"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "signatures" ADD CONSTRAINT "signatures_report_id_fkey" FOREIGN KEY ("report_id") REFERENCES "reports"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "notifications" ADD CONSTRAINT "notifications_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- Row Level Security for all Craftvia tenant tables (mirror of TENANT_MODELS)
SELECT enable_tenant_rls('number_sequences');
SELECT enable_tenant_rls('order_types');
SELECT enable_tenant_rls('checklist_templates');
SELECT enable_tenant_rls('customers');
SELECT enable_tenant_rls('contacts');
SELECT enable_tenant_rls('sites');
SELECT enable_tenant_rls('teams');
SELECT enable_tenant_rls('team_members');
SELECT enable_tenant_rls('work_orders');
SELECT enable_tenant_rls('work_order_assignees');
SELECT enable_tenant_rls('work_order_status_changes');
SELECT enable_tenant_rls('checklist_items');
SELECT enable_tenant_rls('photo_requirements');
SELECT enable_tenant_rls('material_plans');
SELECT enable_tenant_rls('material_usages');
SELECT enable_tenant_rls('work_sessions');
SELECT enable_tenant_rls('time_entries');
SELECT enable_tenant_rls('activity_notes');
SELECT enable_tenant_rls('documents');
SELECT enable_tenant_rls('photos');
SELECT enable_tenant_rls('voice_notes');
SELECT enable_tenant_rls('reports');
SELECT enable_tenant_rls('signatures');
SELECT enable_tenant_rls('import_jobs');
SELECT enable_tenant_rls('notifications');
SELECT enable_tenant_rls('sync_operations');
SELECT enable_tenant_rls('ai_generations');
+863
View File
@@ -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")
}