// CapaKraken — Prisma Schema // All monetary values stored as integer cents to avoid float precision issues. generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // ─── Enums ─────────────────────────────────────────────────────────────────── enum SystemRole { ADMIN MANAGER CONTROLLER USER VIEWER } enum OrderType { BD CHARGEABLE INTERNAL OVERHEAD } enum AllocationType { INT EXT } enum AllocationStatus { PROPOSED CONFIRMED ACTIVE COMPLETED CANCELLED } enum BlueprintTarget { RESOURCE PROJECT } enum ProjectStatus { DRAFT ACTIVE ON_HOLD COMPLETED CANCELLED } enum EstimateStatus { DRAFT IN_REVIEW APPROVED ARCHIVED } enum EstimateVersionStatus { WORKING BASELINE SUBMITTED APPROVED SUPERSEDED } enum EstimateExportFormat { XLSX CSV JSON SAP MMP } enum ResourceType { EMPLOYEE FREELANCER APPRENTICE INTERN STUDENT } enum AuditAction { CREATE UPDATE DELETE SHIFT IMPORT } enum VacationType { ANNUAL SICK PUBLIC_HOLIDAY OTHER } enum VacationStatus { PENDING APPROVED REJECTED CANCELLED } enum HolidayCalendarScope { COUNTRY STATE CITY } enum ImportBatchStatus { DRAFT STAGING STAGED REVIEW_READY APPROVED COMMITTING COMMITTED FAILED CANCELLED } enum StagedRecordStatus { PARSED NORMALIZED UNRESOLVED APPROVED REJECTED COMMITTED FAILED } enum DispoImportSourceKind { REFERENCE PLANNING CHARGEABILITY ROSTER } enum AbsenceTrigger { SICK VACATION PUBLIC_HOLIDAY CUSTOM } enum CostEffect { CHARGE ZERO REDUCE } enum ChargeabilityEffect { COUNT SKIP } enum DispoStagedRecordType { RESOURCE CLIENT PROJECT ASSIGNMENT VACATION AVAILABILITY_RULE UNRESOLVED } // ─── Auth.js Compatible Models ─────────────────────────────────────────────── model User { id String @id @default(cuid()) name String? email String @unique emailVerified DateTime? image String? passwordHash String? systemRole SystemRole @default(USER) permissionOverrides Json? @db.JsonB dashboardLayout Json? @db.JsonB columnPreferences Json? @db.JsonB favoriteProjectIds Json? @db.JsonB // string[] of project IDs lastLoginAt DateTime? lastActiveAt DateTime? totpSecret String? // Base32 TOTP secret totpEnabled Boolean @default(false) lastTotpAt DateTime? // tracks last successful TOTP use to prevent replay isActive Boolean @default(true) deletedAt DateTime? accounts Account[] sessions Session[] auditLogs AuditLog[] vacationsRequested Vacation[] @relation("vacation_requested") vacationsApproved Vacation[] @relation("vacation_approved") resource Resource? notifications Notification[] tasksAssigned Notification[] @relation("taskAssignee") notificationsSent Notification[] @relation("notificationSender") broadcasts NotificationBroadcast[] @relation("broadcastSender") comments Comment[] activeSessions ActiveSession[] reportTemplates ReportTemplate[] assistantApprovals AssistantApproval[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("users") } enum AssistantApprovalStatus { PENDING APPROVED CANCELLED EXPIRED } enum ReportTemplateEntity { RESOURCE PROJECT ASSIGNMENT RESOURCE_MONTH } model ReportTemplate { id String @id @default(cuid()) ownerId String name String description String? entity ReportTemplateEntity config Json @db.JsonB isShared Boolean @default(false) owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([ownerId, updatedAt]) @@unique([ownerId, name]) @@map("report_templates") } model AssistantApproval { id String @id @default(cuid()) userId String conversationId String toolName String toolArguments String @db.Text summary String status AssistantApprovalStatus @default(PENDING) approvedAt DateTime? cancelledAt DateTime? expiresAt DateTime createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId, conversationId, status, expiresAt]) @@index([status, expiresAt]) @@map("assistant_approvals") } model Account { id String @id @default(cuid()) userId String type String provider String providerAccountId String refresh_token String? @db.Text access_token String? @db.Text expires_at Int? token_type String? scope String? id_token String? @db.Text session_state String? user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([provider, providerAccountId]) @@map("accounts") } model Session { id String @id @default(cuid()) sessionToken String @unique userId String expires DateTime user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("sessions") } model VerificationToken { identifier String token String @unique expires DateTime @@unique([identifier, token]) @@map("verification_tokens") } // ─── Import Staging ─────────────────────────────────────────────────────────── model ImportBatch { id String @id @default(cuid()) sourceSystem String @default("DISPO_V2") status ImportBatchStatus @default(DRAFT) referenceSourceFile String? planningSourceFile String? chargeabilitySourceFile String? notes String? summary Json @db.JsonB @default("{}") startedAt DateTime? stagedAt DateTime? approvedAt DateTime? committedAt DateTime? failedAt DateTime? stagedResources StagedResource[] stagedClients StagedClient[] stagedProjects StagedProject[] stagedAssignments StagedAssignment[] stagedVacations StagedVacation[] stagedAvailabilityRules StagedAvailabilityRule[] stagedUnresolvedRecords StagedUnresolvedRecord[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([status]) @@map("import_batches") } model StagedResource { id String @id @default(cuid()) importBatchId String status StagedRecordStatus @default(PARSED) sourceKind DispoImportSourceKind sourceWorkbook String sourceSheet String sourceRow Int sourceColumn String? canonicalExternalId String enterpriseId String? eid String? displayName String? email String? chapter String? chapterCode String? managementLevelGroupName String? managementLevelName String? countryCode String? metroCityName String? clientUnitName String? resourceType ResourceType? chargeabilityTarget Float? fte Float? lcrCents Int? ucrCents Int? availability Json? @db.JsonB roleTokens String[] @default([]) warnings String[] @default([]) errorMessage String? rawPayload Json @db.JsonB normalizedData Json @db.JsonB @default("{}") importBatch ImportBatch @relation(fields: [importBatchId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([importBatchId, status]) @@index([canonicalExternalId]) @@map("staged_resources") } model StagedClient { id String @id @default(cuid()) importBatchId String status StagedRecordStatus @default(PARSED) sourceKind DispoImportSourceKind sourceWorkbook String sourceSheet String sourceRow Int sourceColumn String? clientCode String? parentClientCode String? name String sortOrder Int? isActive Boolean @default(true) warnings String[] @default([]) errorMessage String? rawPayload Json @db.JsonB normalizedData Json @db.JsonB @default("{}") importBatch ImportBatch @relation(fields: [importBatchId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([importBatchId, status]) @@index([clientCode]) @@map("staged_clients") } model StagedProject { id String @id @default(cuid()) importBatchId String status StagedRecordStatus @default(PARSED) sourceKind DispoImportSourceKind sourceWorkbook String sourceSheet String sourceRow Int sourceColumn String? projectKey String shortCode String? name String? clientCode String? utilizationCategoryCode String? orderType OrderType? allocationType AllocationType? winProbability Int? isInternal Boolean @default(false) isTbd Boolean @default(false) startDate DateTime? @db.Date endDate DateTime? @db.Date warnings String[] @default([]) errorMessage String? rawPayload Json @db.JsonB normalizedData Json @db.JsonB @default("{}") importBatch ImportBatch @relation(fields: [importBatchId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([importBatchId, status]) @@index([projectKey]) @@map("staged_projects") } model StagedAssignment { id String @id @default(cuid()) importBatchId String status StagedRecordStatus @default(PARSED) sourceKind DispoImportSourceKind sourceWorkbook String sourceSheet String sourceRow Int sourceColumn String? resourceExternalId String projectKey String? assignmentDate DateTime? @db.Date startDate DateTime? @db.Date endDate DateTime? @db.Date hoursPerDay Float? percentage Float? slotFraction Float? roleToken String? roleName String? chapterToken String? utilizationCategoryCode String? winProbability Int? isInternal Boolean @default(false) isUnassigned Boolean @default(false) isTbd Boolean @default(false) warnings String[] @default([]) errorMessage String? rawPayload Json @db.JsonB normalizedData Json @db.JsonB @default("{}") importBatch ImportBatch @relation(fields: [importBatchId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([importBatchId, status]) @@index([resourceExternalId]) @@index([projectKey]) @@index([assignmentDate]) @@map("staged_assignments") } model StagedVacation { id String @id @default(cuid()) importBatchId String status StagedRecordStatus @default(PARSED) sourceKind DispoImportSourceKind sourceWorkbook String sourceSheet String sourceRow Int sourceColumn String? resourceExternalId String vacationType VacationType startDate DateTime @db.Date endDate DateTime @db.Date note String? holidayName String? isHalfDay Boolean @default(false) halfDayPart String? isPublicHoliday Boolean @default(false) warnings String[] @default([]) errorMessage String? rawPayload Json @db.JsonB normalizedData Json @db.JsonB @default("{}") importBatch ImportBatch @relation(fields: [importBatchId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([importBatchId, status]) @@index([resourceExternalId]) @@index([startDate, endDate]) @@map("staged_vacations") } model StagedAvailabilityRule { id String @id @default(cuid()) importBatchId String status StagedRecordStatus @default(PARSED) sourceKind DispoImportSourceKind sourceWorkbook String sourceSheet String sourceRow Int sourceColumn String? resourceExternalId String ruleType String weekday Int? effectiveStartDate DateTime? @db.Date effectiveEndDate DateTime? @db.Date availableHours Float? percentage Float? isResolved Boolean @default(false) warnings String[] @default([]) errorMessage String? rawPayload Json @db.JsonB normalizedData Json @db.JsonB @default("{}") importBatch ImportBatch @relation(fields: [importBatchId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([importBatchId, status]) @@index([resourceExternalId]) @@map("staged_availability_rules") } model StagedUnresolvedRecord { id String @id @default(cuid()) importBatchId String status StagedRecordStatus @default(UNRESOLVED) sourceKind DispoImportSourceKind sourceWorkbook String sourceSheet String sourceRow Int sourceColumn String? recordType DispoStagedRecordType resourceExternalId String? projectKey String? message String resolutionHint String? warnings String[] @default([]) rawPayload Json @db.JsonB normalizedData Json @db.JsonB @default("{}") importBatch ImportBatch @relation(fields: [importBatchId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([importBatchId, status]) @@index([recordType]) @@index([resourceExternalId]) @@index([projectKey]) @@map("staged_unresolved_records") } // ─── Country / Metro City ──────────────────────────────────────────────────── model Country { id String @id @default(cuid()) code String @unique // ISO 3166-1 alpha-2 (DE, IN, ES, ...) name String dailyWorkingHours Float @default(8.0) scheduleRules Json? @db.JsonB // e.g. Spain variable schedule isActive Boolean @default(true) metroCities MetroCity[] holidayCalendars HolidayCalendar[] resources Resource[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("countries") } model MetroCity { id String @id @default(cuid()) name String countryId String country Country @relation(fields: [countryId], references: [id]) resources Resource[] holidayCalendars HolidayCalendar[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([countryId, name]) @@map("metro_cities") } model HolidayCalendar { id String @id @default(cuid()) name String scopeType HolidayCalendarScope countryId String stateCode String? metroCityId String? isActive Boolean @default(true) priority Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt country Country @relation(fields: [countryId], references: [id], onDelete: Cascade) metroCity MetroCity? @relation(fields: [metroCityId], references: [id], onDelete: Cascade) entries HolidayCalendarEntry[] @@index([countryId, scopeType]) @@index([countryId, stateCode]) @@index([metroCityId]) // Scope uniqueness is enforced via partial unique indexes in SQL migrations. @@map("holiday_calendars") } model HolidayCalendarEntry { id String @id @default(cuid()) holidayCalendarId String date DateTime @db.Date name String isRecurringAnnual Boolean @default(false) source String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt holidayCalendar HolidayCalendar @relation(fields: [holidayCalendarId], references: [id], onDelete: Cascade) @@unique([holidayCalendarId, date]) @@index([date]) @@map("holiday_calendar_entries") } // ─── Org Unit Hierarchy ───────────────────────────────────────────────────── model OrgUnit { id String @id @default(cuid()) name String shortName String? level Int // 5, 6, or 7 parentId String? parent OrgUnit? @relation("OrgUnitTree", fields: [parentId], references: [id]) children OrgUnit[] @relation("OrgUnitTree") sortOrder Int @default(0) isActive Boolean @default(true) resources Resource[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([parentId, name]) @@index([level]) @@map("org_units") } // ─── Utilization Category ─────────────────────────────────────────────────── model UtilizationCategory { id String @id @default(cuid()) code String @unique // "Chg", "BD", "MD&I", etc. name String description String? sortOrder Int @default(0) isActive Boolean @default(true) isDefault Boolean @default(false) projects Project[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("utilization_categories") } // ─── Client / WBS ─────────────────────────────────────────────────────────── model Client { id String @id @default(cuid()) name String code String? @unique // optional short code e.g. "BMW" parentId String? parent Client? @relation("ClientTree", fields: [parentId], references: [id]) children Client[] @relation("ClientTree") isActive Boolean @default(true) deletedAt DateTime? sortOrder Int @default(0) tags String[] @default([]) projects Project[] resourceClientUnits Resource[] @relation("resource_client_unit") rateCards RateCard[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([parentId, name]) @@map("clients") } // ─── Management Level ─────────────────────────────────────────────────────── model ManagementLevelGroup { id String @id @default(cuid()) name String @unique targetPercentage Float // e.g. 0.805 for 80.5% sortOrder Int @default(0) levels ManagementLevel[] resources Resource[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("management_level_groups") } model ManagementLevel { id String @id @default(cuid()) name String @unique // e.g. "10-Senior Analyst" groupId String group ManagementLevelGroup @relation(fields: [groupId], references: [id]) resources Resource[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([groupId]) @@map("management_levels") } // ─── Blueprint System ───────────────────────────────────────────────────────── model Blueprint { id String @id @default(cuid()) name String target BlueprintTarget description String? // fieldDefs: BlueprintFieldDefinition[] (see shared/types) fieldDefs Json @db.JsonB // defaults: Record defaults Json @db.JsonB @default("{}") // validationRules: BlueprintValidationRule[] validationRules Json @db.JsonB @default("[]") // rolePresets: StaffingRequirement[] — default roles for project creation wizard rolePresets Json @db.JsonB @default("[]") isActive Boolean @default(true) deletedAt DateTime? isGlobal Boolean @default(false) resources Resource[] projects Project[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([isActive]) @@index([target]) @@map("blueprints") } // ─── Role ───────────────────────────────────────────────────────────────────── model Role { id String @id @default(cuid()) name String @unique description String? color String? // hex color e.g. "#6366f1" isActive Boolean @default(true) deletedAt DateTime? resourceRoles ResourceRole[] demandRequirements DemandRequirement[] assignments Assignment[] areaResources Resource[] @relation("resource_area_role") estimateDemandLines EstimateDemandLine[] @relation("estimate_demand_line_role") rateCardLines RateCardLine[] @relation("rate_card_line_role") resourceCostSnapshots ResourceCostSnapshot[] @relation("resource_cost_snapshot_role") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("roles") } // ─── ResourceRole ───────────────────────────────────────────────────────────── model ResourceRole { id String @id @default(cuid()) resourceId String roleId String isPrimary Boolean @default(false) resource Resource @relation(fields: [resourceId], references: [id], onDelete: Cascade) role Role @relation(fields: [roleId], references: [id], onDelete: Restrict) @@unique([resourceId, roleId]) @@map("resource_roles") } // ─── Resource ───────────────────────────────────────────────────────────────── model Resource { id String @id @default(cuid()) eid String @unique // Employee ID, e.g. "EMP-001" displayName String email String @unique chapter String? // Monetary values in integer cents (e.g. 8500 = 85.00 EUR/h) lcrCents Int // Labor Cost Rate (internal) ucrCents Int // Utilization Cost Rate (charge-out) currency String @default("EUR") chargeabilityTarget Float @default(80) // Percent 0-100 // availability: WeekdayAvailability availability Json @db.JsonB // skills: SkillEntry[] skills Json @db.JsonB @default("[]") // dynamicFields: Record dynamicFields Json @db.JsonB @default("{}") blueprintId String? blueprint Blueprint? @relation(fields: [blueprintId], references: [id]) isActive Boolean @default(true) deletedAt DateTime? userId String? @unique user User? @relation(fields: [userId], references: [id]) // Skill Matrix fields portfolioUrl String? roleId String? aiSummary String? aiSummaryUpdatedAt DateTime? skillMatrixUpdatedAt DateTime? areaRole Role? @relation("resource_area_role", fields: [roleId], references: [id], onDelete: SetNull) // Value Score fields valueScore Int? valueScoreBreakdown Json? @db.JsonB valueScoreUpdatedAt DateTime? // Vacation-related postalCode String? federalState String? // e.g. "BY" (Bavaria), derived from postalCode or set manually // Dispo v2: Country / Metro City / Org Unit countryId String? country Country? @relation(fields: [countryId], references: [id]) metroCityId String? metroCity MetroCity? @relation(fields: [metroCityId], references: [id]) orgUnitId String? orgUnit OrgUnit? @relation(fields: [orgUnitId], references: [id]) // Dispo v2: Management Level managementLevelGroupId String? managementLevelGroup ManagementLevelGroup? @relation(fields: [managementLevelGroupId], references: [id]) managementLevelId String? managementLevel ManagementLevel? @relation(fields: [managementLevelId], references: [id]) // Dispo v2: Resource classification resourceType ResourceType @default(EMPLOYEE) chgResponsibility Boolean @default(true) // "Chg Responsibility" status rolledOff Boolean @default(false) departed Boolean @default(false) enterpriseId String? @unique // Enterprise ID e.g. "a.kasperovich" (username, not email) clientUnitId String? clientUnit Client? @relation("resource_client_unit", fields: [clientUnitId], references: [id]) fte Float @default(1.0) // at least 2 decimal places assignments Assignment[] vacations Vacation[] resourceRoles ResourceRole[] vacationEntitlements VacationEntitlement[] estimateDemandLines EstimateDemandLine[] @relation("estimate_demand_line_resource") resourceCostSnapshots ResourceCostSnapshot[] @relation("resource_cost_snapshot_resource") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([chapter]) @@index([isActive]) @@index([countryId]) @@index([orgUnitId]) @@index([resourceType]) @@index([managementLevelGroupId]) @@index([blueprintId]) @@index([roleId]) @@map("resources") } // ─── Project ────────────────────────────────────────────────────────────────── model Project { id String @id @default(cuid()) shortCode String @unique // e.g. "PROJ-001" name String orderType OrderType allocationType AllocationType winProbability Int @default(100) // 0-100 budgetCents Int // Total budget in cents startDate DateTime @db.Date endDate DateTime @db.Date status ProjectStatus @default(DRAFT) responsiblePerson String? shoringThreshold Int? @default(55) // Max offshore % before alert (0-100) onshoreCountryCode String? @default("DE") // Country code considered "onshore" color String? // Hex color for timeline display, e.g. "#3b82f6" coverImageUrl String? @db.Text // Base64 data-URL for project cover art coverFocusY Int @default(50) // Vertical focus point 0-100 (% from top) // staffingReqs: StaffingRequirement[] staffingReqs Json @db.JsonB @default("[]") // dynamicFields: Record dynamicFields Json @db.JsonB @default("{}") blueprintId String? blueprint Blueprint? @relation(fields: [blueprintId], references: [id]) // Dispo v2: Utilization Category + Client utilizationCategoryId String? utilizationCategory UtilizationCategory? @relation(fields: [utilizationCategoryId], references: [id]) clientId String? client Client? @relation(fields: [clientId], references: [id]) demandRequirements DemandRequirement[] assignments Assignment[] estimates Estimate[] calculationRules CalculationRule[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([status]) @@index([startDate, endDate]) @@index([orderType]) @@index([clientId]) @@map("projects") } // ─── Estimating ────────────────────────────────────────────────────────────── model Estimate { id String @id @default(cuid()) projectId String? name String opportunityId String? baseCurrency String @default("EUR") status EstimateStatus @default(DRAFT) latestVersionNumber Int @default(1) project Project? @relation(fields: [projectId], references: [id], onDelete: SetNull) versions EstimateVersion[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([projectId]) @@index([status]) @@index([opportunityId]) @@map("estimates") } model EstimateVersion { id String @id @default(cuid()) estimateId String versionNumber Int label String? status EstimateVersionStatus @default(WORKING) notes String? lockedAt DateTime? projectSnapshot Json @db.JsonB @default("{}") commercialTerms Json? @db.JsonB estimate Estimate @relation(fields: [estimateId], references: [id], onDelete: Cascade) assumptions EstimateAssumption[] scopeItems ScopeItem[] demandLines EstimateDemandLine[] resourceSnapshots ResourceCostSnapshot[] metrics EstimateMetric[] exports EstimateExport[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([estimateId, versionNumber]) @@index([status]) @@map("estimate_versions") } model EstimateAssumption { id String @id @default(cuid()) estimateVersionId String category String key String label String valueType String @default("json") value Json @db.JsonB sortOrder Int @default(0) notes String? estimateVersion EstimateVersion @relation(fields: [estimateVersionId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([estimateVersionId, category, key]) @@index([estimateVersionId, sortOrder]) @@map("estimate_assumptions") } model ScopeItem { id String @id @default(cuid()) estimateVersionId String sequenceNo Int scopeType String packageCode String? name String description String? scene String? page String? location String? assumptionCategory String? technicalSpec Json @db.JsonB @default("{}") frameCount Int? itemCount Float? unitMode String? internalComments String? externalComments String? sortOrder Int @default(0) metadata Json @db.JsonB @default("{}") estimateVersion EstimateVersion @relation(fields: [estimateVersionId], references: [id], onDelete: Cascade) demandLines EstimateDemandLine[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([estimateVersionId, sortOrder]) @@index([estimateVersionId, sequenceNo]) @@map("scope_items") } model EstimateDemandLine { id String @id @default(cuid()) estimateVersionId String scopeItemId String? roleId String? resourceId String? lineType String @default("LABOR") name String chapter String? hours Float days Float? fte Float? rateSource String? costRateCents Int @default(0) billRateCents Int @default(0) currency String @default("EUR") costTotalCents Int @default(0) priceTotalCents Int @default(0) monthlySpread Json @db.JsonB @default("{}") staffingAttributes Json @db.JsonB @default("{}") metadata Json @db.JsonB @default("{}") estimateVersion EstimateVersion @relation(fields: [estimateVersionId], references: [id], onDelete: Cascade) scopeItem ScopeItem? @relation(fields: [scopeItemId], references: [id], onDelete: SetNull) role Role? @relation("estimate_demand_line_role", fields: [roleId], references: [id], onDelete: SetNull) resource Resource? @relation("estimate_demand_line_resource", fields: [resourceId], references: [id], onDelete: SetNull) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([estimateVersionId]) @@index([scopeItemId]) @@index([roleId]) @@index([resourceId]) @@map("estimate_demand_lines") } model RateCard { id String @id @default(cuid()) name String currency String @default("EUR") effectiveFrom DateTime? @db.Date effectiveTo DateTime? @db.Date source String? isActive Boolean @default(true) clientId String? client Client? @relation(fields: [clientId], references: [id], onDelete: SetNull) lines RateCardLine[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([isActive]) @@index([clientId]) @@index([effectiveFrom, effectiveTo]) @@map("rate_cards") } model RateCardLine { id String @id @default(cuid()) rateCardId String roleId String? chapter String? location String? seniority String? workType String? serviceGroup String? costRateCents Int billRateCents Int? machineRateCents Int? attributes Json @db.JsonB @default("{}") rateCard RateCard @relation(fields: [rateCardId], references: [id], onDelete: Cascade) role Role? @relation("rate_card_line_role", fields: [roleId], references: [id], onDelete: SetNull) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([rateCardId]) @@index([roleId]) @@map("rate_card_lines") } model ResourceCostSnapshot { id String @id @default(cuid()) estimateVersionId String resourceId String? sourceEid String? displayName String chapter String? roleId String? currency String @default("EUR") lcrCents Int ucrCents Int fte Float? location String? country String? level String? workType String? attributes Json @db.JsonB @default("{}") estimateVersion EstimateVersion @relation(fields: [estimateVersionId], references: [id], onDelete: Cascade) resource Resource? @relation("resource_cost_snapshot_resource", fields: [resourceId], references: [id], onDelete: SetNull) role Role? @relation("resource_cost_snapshot_role", fields: [roleId], references: [id], onDelete: SetNull) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([estimateVersionId]) @@index([resourceId]) @@map("resource_cost_snapshots") } model EstimateMetric { id String @id @default(cuid()) estimateVersionId String key String label String metricGroup String? valueDecimal Float valueCents Int? currency String? metadata Json @db.JsonB @default("{}") estimateVersion EstimateVersion @relation(fields: [estimateVersionId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([estimateVersionId, key]) @@index([estimateVersionId, metricGroup]) @@map("estimate_metrics") } model EstimateExport { id String @id @default(cuid()) estimateVersionId String format EstimateExportFormat fileName String storageKey String? payload Json? @db.JsonB estimateVersion EstimateVersion @relation(fields: [estimateVersionId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([estimateVersionId, format]) @@map("estimate_exports") } model EffortRuleSet { id String @id @default(cuid()) name String description String? isDefault Boolean @default(false) rules EffortRule[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([isDefault]) @@map("effort_rule_sets") } model EffortRule { id String @id @default(cuid()) ruleSetId String scopeType String discipline String chapter String? unitMode String @default("flat") hoursPerUnit Float description String? sortOrder Int @default(0) ruleSet EffortRuleSet @relation(fields: [ruleSetId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([ruleSetId]) @@index([scopeType]) @@map("effort_rules") } // ─── Experience Multipliers ────────────────────────────────────────────────── model ExperienceMultiplierSet { id String @id @default(cuid()) name String description String? isDefault Boolean @default(false) rules ExperienceMultiplierRule[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([isDefault]) @@map("experience_multiplier_sets") } model ExperienceMultiplierRule { id String @id @default(cuid()) multiplierSetId String chapter String? location String? level String? costMultiplier Float @default(1.0) billMultiplier Float @default(1.0) shoringRatio Float? additionalEffortRatio Float? description String? sortOrder Int @default(0) multiplierSet ExperienceMultiplierSet @relation(fields: [multiplierSetId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([multiplierSetId]) @@map("experience_multiplier_rules") } // ─── DemandRequirement / Assignment (additive migration target) ───────────── model DemandRequirement { id String @id @default(cuid()) projectId String startDate DateTime @db.Date endDate DateTime @db.Date hoursPerDay Float percentage Float role String? roleId String? headcount Int @default(1) budgetCents Int @default(0) status AllocationStatus @default(PROPOSED) metadata Json @db.JsonB @default("{}") project Project @relation(fields: [projectId], references: [id]) roleEntity Role? @relation(fields: [roleId], references: [id]) assignments Assignment[] createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([projectId]) @@index([startDate, endDate]) @@index([status]) @@index([projectId, status, startDate]) @@index([roleId]) @@map("demand_requirements") } model Assignment { id String @id @default(cuid()) demandRequirementId String? resourceId String projectId String startDate DateTime @db.Date endDate DateTime @db.Date hoursPerDay Float percentage Float role String? roleId String? dailyCostCents Int status AllocationStatus @default(PROPOSED) metadata Json @db.JsonB @default("{}") overbookingAcknowledged Boolean @default(false) demandRequirement DemandRequirement? @relation(fields: [demandRequirementId], references: [id], onDelete: SetNull) resource Resource @relation(fields: [resourceId], references: [id]) project Project @relation(fields: [projectId], references: [id]) roleEntity Role? @relation(fields: [roleId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([resourceId, projectId, startDate, endDate], name: "unique_assignment") @@index([demandRequirementId]) @@index([resourceId]) @@index([projectId]) @@index([startDate, endDate]) @@index([status]) @@index([resourceId, status, startDate]) @@index([projectId, startDate, endDate]) @@index([status, startDate, endDate]) @@index([projectId, status, startDate, endDate]) @@index([resourceId, status, endDate]) @@index([projectId, status, endDate]) @@index([roleId]) @@map("assignments") } // ─── Vacation ───────────────────────────────────────────────────────────────── model Vacation { id String @id @default(cuid()) resourceId String type VacationType status VacationStatus @default(PENDING) startDate DateTime @db.Date endDate DateTime @db.Date note String? rejectionReason String? isHalfDay Boolean @default(false) halfDayPart String? // "MORNING" | "AFTERNOON" deductedDays Float? holidayCountryCode String? holidayCountryName String? holidayFederalState String? holidayMetroCityName String? holidayCalendarDates Json? @db.JsonB holidayLegacyPublicHolidayDates Json? @db.JsonB requestedById String approvedById String? approvedAt DateTime? resource Resource @relation(fields: [resourceId], references: [id]) requestedBy User @relation("vacation_requested", fields: [requestedById], references: [id]) approvedBy User? @relation("vacation_approved", fields: [approvedById], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([resourceId]) @@index([startDate, endDate]) @@index([status]) @@index([resourceId, status, startDate, endDate]) @@index([requestedById]) @@map("vacations") } // ─── VacationEntitlement ────────────────────────────────────────────────────── model VacationEntitlement { id String @id @default(cuid()) resourceId String year Int entitledDays Float @default(28) // annual quota (including carryover) carryoverDays Float @default(0) // days carried over from previous year usedDays Float @default(0) // APPROVED ANNUAL+OTHER days consumed (cached) pendingDays Float @default(0) // PENDING ANNUAL+OTHER days (cached) resource Resource @relation(fields: [resourceId], references: [id], onDelete: Cascade) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@unique([resourceId, year]) @@index([year]) @@map("vacation_entitlements") } // ─── Notification Enums ────────────────────────────────────────────────────── enum NotificationCategory { NOTIFICATION REMINDER TASK APPROVAL } enum NotificationPriority { LOW NORMAL HIGH URGENT } enum TaskStatus { OPEN IN_PROGRESS DONE DISMISSED } // ─── Notification ───────────────────────────────────────────────────────────── model Notification { id String @id @default(cuid()) userId String type String // e.g. "VACATION_REQUESTED", "OVERALLOCATION", "PROJECT_DEADLINE" title String body String? entityId String? // related entity (vacation ID, allocation ID, project ID) entityType String? // "vacation", "allocation", "project" readAt DateTime? createdAt DateTime @default(now()) // Extended fields category NotificationCategory @default(NOTIFICATION) priority NotificationPriority @default(NORMAL) link String? // Task fields taskStatus TaskStatus? taskAction String? assigneeId String? dueDate DateTime? completedAt DateTime? completedBy String? // Reminder fields remindAt DateTime? recurrence String? // "daily" | "weekly" | "monthly" | null nextRemindAt DateTime? // Targeting sourceId String? senderId String? channel String @default("in_app") updatedAt DateTime @updatedAt // Relations user User @relation(fields: [userId], references: [id], onDelete: Cascade) assignee User? @relation("taskAssignee", fields: [assigneeId], references: [id]) sender User? @relation("notificationSender", fields: [senderId], references: [id]) @@index([userId, readAt]) @@index([userId, category, taskStatus]) @@index([nextRemindAt]) @@index([assigneeId, taskStatus]) @@index([category, nextRemindAt]) @@index([userId, dueDate]) @@map("notifications") } // ─── Notification Broadcast ───────────────────────────────────────────────── model NotificationBroadcast { id String @id @default(cuid()) senderId String title String body String? link String? category NotificationCategory @default(NOTIFICATION) priority NotificationPriority @default(NORMAL) channel String @default("in_app") targetType String // "user" | "role" | "project" | "orgUnit" | "all" targetValue String? scheduledAt DateTime? sentAt DateTime? recipientCount Int @default(0) createdAt DateTime @default(now()) sender User @relation("broadcastSender", fields: [senderId], references: [id]) @@index([senderId]) @@index([scheduledAt, sentAt]) @@map("notification_broadcasts") } // ─── System Role Configuration ──────────────────────────────────────────────── model SystemRoleConfig { role SystemRole @id label String // Display label, e.g. "Manager" description String? // Optional description of the role defaultPermissions Json @db.JsonB // PermissionKey[] — default permissions for this role color String? // Badge color, e.g. "purple", "blue" sortOrder Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("system_role_configs") } // ─── System Settings ────────────────────────────────────────────────────────── model SystemSettings { id String @id @default("singleton") aiProvider String? @default("openai") // "openai" | "azure" azureOpenAiEndpoint String? // Azure only: base URL e.g. "https://myinstance.cognitiveservices.azure.com" azureOpenAiDeployment String? // deployment/model name e.g. "gpt-5" azureOpenAiApiKey String? // API key (plaintext, internal only) azureApiVersion String? @default("2025-01-01-preview") // Azure API version aiMaxCompletionTokens Int? @default(2000) aiTemperature Float? @default(1) aiSummaryPrompt String? // prompt template; use {role} {chapter} {mainSkills} {topSkills} scoreWeights Json? @db.JsonB // ValueScoreWeights (5 floats summing to 1.0) scoreVisibleRoles Json? @db.JsonB // SystemRole[] e.g. ["ADMIN","MANAGER"] // SMTP / email settings smtpHost String? smtpPort Int? @default(587) smtpUser String? smtpPassword String? smtpFrom String? smtpTls Boolean? @default(true) // Global viewer-side anonymization anonymizationEnabled Boolean? @default(false) anonymizationDomain String? @default("superhartmut.de") anonymizationSeed String? anonymizationMode String? @default("global") anonymizationAliases Json? @db.JsonB // Vacation defaults vacationDefaultDays Int? @default(28) // default annual entitlement // Timeline undo timelineUndoMaxSteps Int? @default(50) // max undo history depth // DALL-E image generation (Azure requires separate deployment) azureDalleDeployment String? // e.g. "dall-e-3" — Azure DALL-E deployment name azureDalleEndpoint String? // Optional: separate endpoint for DALL-E (if different from chat) azureDalleApiKey String? // Optional: separate API key for DALL-E // Gemini image generation geminiApiKey String? geminiModel String? @default("gemini-2.5-flash-image") imageProvider String? @default("dalle") // "dalle" | "gemini" // Session timeout settings sessionMaxAge Int? @default(28800) // Absolute timeout in seconds (8h) sessionIdleTimeout Int? @default(1800) // Idle timeout in seconds (30min) // Concurrent session limit (kick-oldest strategy) maxConcurrentSessions Int? @default(3) // MFA enforcement: roles that are required to have TOTP enabled (JSON array of SystemRole strings) // e.g. ["ADMIN"] or ["ADMIN","MANAGER"]. Null = no enforcement. requireMfaForRoles Json? @db.JsonB updatedAt DateTime @updatedAt @@map("system_settings") } // ─── Active Session Registry (JWT session tracking) ────────────────────────── model ActiveSession { id String @id @default(cuid()) userId String jti String @unique // JWT ID — unique per token createdAt DateTime @default(now()) lastSeenAt DateTime @default(now()) userAgent String? ipAddress String? user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@index([userId, createdAt]) @@map("active_sessions") } // ─── Calculation Rules ──────────────────────────────────────────────────────── model CalculationRule { id String @id @default(cuid()) name String description String? // ── Matching ── triggerType AbsenceTrigger projectId String? orderType OrderType? // ── Effects ── costEffect CostEffect costReductionPercent Int? // only for REDUCE (0-100) chargeabilityEffect ChargeabilityEffect // ── Ordering ── priority Int @default(0) isActive Boolean @default(true) project Project? @relation(fields: [projectId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([triggerType, isActive]) @@index([projectId]) @@map("calculation_rules") } // ─── Comment ───────────────────────────────────────────────────────────────── model Comment { id String @id @default(cuid()) entityType String // "estimate", "estimate_version", "scope_item", "demand_line" entityId String parentId String? // for replies authorId String body String @db.Text mentions String[] // user IDs mentioned resolved Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt author User @relation(fields: [authorId], references: [id]) parent Comment? @relation("CommentReplies", fields: [parentId], references: [id]) replies Comment[] @relation("CommentReplies") @@index([entityType, entityId]) @@index([authorId]) @@index([parentId]) @@map("comments") } // ─── Audit Log ──────────────────────────────────────────────────────────────── model AuditLog { id String @id @default(cuid()) entityType String // "Resource" | "Project" | "Allocation" | "Blueprint" entityId String action AuditAction userId String? user User? @relation(fields: [userId], references: [id]) // changes: { before: Record, after: Record } changes Json @db.JsonB createdAt DateTime @default(now()) source String? // "ui" | "api" | "ai" | "import" | "cron" entityName String? // Human-readable name e.g. "Porsche Taycan Project" summary String? // One-liner: "Changed status from DRAFT to ACTIVE" @@index([entityType, entityId]) @@index([userId]) @@index([createdAt]) @@index([entityType, createdAt]) @@map("audit_logs") } // ─── Webhook ────────────────────────────────────────────────────────────────── model Webhook { id String @id @default(cuid()) name String url String secret String? // HMAC signing secret events String[] // ["allocation.created", "project.status_changed", etc.] isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@map("webhooks") } // ─── Invite Token ───────────────────────────────────────────────────────────── model InviteToken { id String @id @default(cuid()) email String role SystemRole @default(USER) token String @unique expiresAt DateTime usedAt DateTime? createdById String // userId of the inviting admin createdAt DateTime @default(now()) @@index([token]) @@index([email]) @@map("invite_tokens") } model PasswordResetToken { id String @id @default(cuid()) email String token String @unique expiresAt DateTime usedAt DateTime? createdAt DateTime @default(now()) @@index([token]) @@index([email]) @@map("password_reset_tokens") }