Files
CapaKraken/packages/db/prisma/schema.prisma
T
Hartmut 66878f18f4 feat: Activity History system — full audit coverage, UI, AI tools
Infrastructure (Phase 1):
- AuditLog schema: add source, entityName, summary fields + index
- createAuditEntry() helper: auto-diff, auto-summary, fire-and-forget
- auditLog query router: list, getByEntity, getTimeline, getActivitySummary

Audit Coverage (Phase 2 — 14 routers, 50+ mutations):
- vacation: create, approve, reject, cancel, batch ops (8 mutations)
- user: create, updateRole, setPermissions, resetPermissions (5 mutations)
- entitlement: set, bulkSet (3 mutations)
- client: create, update, delete, batchUpdateSortOrder
- org-unit: create, update, deactivate
- country: create, update, createCity, updateCity, deleteCity
- management-level: createGroup, updateGroup, createLevel, updateLevel, deleteLevel
- settings: updateSystemSettings (sensitive fields sanitized), testSmtp
- blueprint: create, update, updateRolePresets, delete, batchDelete, setGlobal
- rate-card: create, update, deactivate, addLine, updateLine, deleteLine, replaceLines
- calculation-rules: create, update, delete
- effort-rule: create, update, delete
- experience-multiplier: create, update, delete
- utilization-category: create, update

Admin UI (Phase 3):
- /admin/activity-log page with global searchable timeline
- Filters: entity type, action, user, date range, text search
- Expandable before/after diff view per entry
- Summary cards showing top entity types by change count
- EntityHistory reusable component for entity detail pages
- Sidebar nav link with clock icon

AI Assistant (Phase 4):
- query_change_history tool: "Who changed project X?"
- get_entity_timeline tool: "What happened to resource Y?"

Regression: 283 engine + 37 staffing tests pass. TypeScript clean.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-22 22:39:30 +01:00

1546 lines
52 KiB
Plaintext

// Planarchy — 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 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?
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[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("users")
}
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[]
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[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([countryId, name])
@@map("metro_cities")
}
// ─── 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)
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
@@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<string, unknown>
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)
isGlobal Boolean @default(false)
resources Resource[]
projects Project[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@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)
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<string, unknown>
dynamicFields Json @db.JsonB @default("{}")
blueprintId String?
blueprint Blueprint? @relation(fields: [blueprintId], references: [id])
isActive Boolean @default(true)
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])
@@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?
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<string, unknown>
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])
@@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("{}")
demandRequirement DemandRequirement? @relation(fields: [demandRequirementId], references: [id])
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])
@@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"
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])
@@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
updatedAt DateTime @updatedAt
@@map("system_settings")
}
// ─── 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])
@@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<string, unknown>, after: Record<string, unknown> }
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")
}