92a982b151
Engine (packages/engine): - calculateShoringRatio() pure function: onshore/offshore hours, country breakdown, threshold check, weighted by hours not headcount - 12 unit tests: empty, 100% onshore/offshore, mixed ratios, custom threshold, case-insensitive, unknown country, FTE weighting Schema: - Project.shoringThreshold (default 55%) — per-project configurable - Project.onshoreCountryCode (default "DE") — configurable onshore country API (project router): - getShoringRatio query: loads assignments with resource.country, computes ratio, returns full breakdown - update mutation: accepts shoringThreshold + onshoreCountryCode UI: - ShoringIndicator: stacked horizontal bar with country segments, severity badge (green/yellow/red), hover tooltip, dark theme - ShoringBadge: mini colored dot + % for project list column - ProjectModal: "Max Offshore %" number input - Project detail: indicator after budget status card - Project list: "Shoring" column (default hidden, toggleable) AI Assistant: - get_shoring_ratio tool: human-readable breakdown with threshold alert Colors: green (<threshold-10), yellow (threshold-10 to threshold), red (>=threshold) Default: 55% offshore threshold, "DE" as onshore country Co-Authored-By: claude-flow <ruv@ruv.net>
1552 lines
52 KiB
Plaintext
1552 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?
|
|
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<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
|
|
// Gemini image generation
|
|
geminiApiKey String?
|
|
geminiModel String? @default("gemini-2.5-flash-image")
|
|
imageProvider String? @default("dalle") // "dalle" | "gemini"
|
|
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")
|
|
}
|