Files
teOS/apps/api/prisma/schema.prisma
Flexomatic81 6a8265d3dc feat: move configuration from .env to DB with Admin UI management
Replace hardcoded .env configuration with database-backed settings
manageable through the Admin web interface. This reduces .env to
bootstrap-only variables (DB, Keycloak, encryption keys).

Backend:
- Add SystemSetting Prisma model with category, valueType, isSecret
- Add system-settings NestJS module (CRUD, 60s cache, encryption)
- Refactor all 7 connectors to lazy-load credentials from DB via
  CredentialsService.findActiveByType() instead of ConfigService
- Add event-driven credential reload (@nestjs/event-emitter)
- Dynamic CORS origins and conditional Swagger from DB settings
- Fix JWT strategy: use Keycloak JWKS (RS256) instead of symmetric secret
- Add SYSTEM_SETTINGS_VIEW/MANAGE permissions
- Seed 13 default settings (sync intervals, features, branding, CORS)
- Add env-to-db migration script (prisma/migrate-env-to-db.ts)

Frontend:
- Add use-credentials hook (full CRUD for integration credentials)
- Add use-system-settings hook (read/update system settings)
- Wire admin-integrations page to real API (create/update/test/toggle)
- Add admin system-settings page with 4 tabs (Branding, CORS, Sync, Features)
- Fix sidebar double-highlighting with exactMatch flag
- Fix integration detail fallback when API unavailable
- Fix API client to unwrap backend's {success, data} envelope
- Update NEXT_PUBLIC_API_URL to include /v1 version prefix
- Fix activity-widget hydration error
- Add i18n keys for systemSettings (de + en)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 20:07:39 +01:00

675 lines
18 KiB
Plaintext

// Prisma Schema for tOS - Team Operating System
// Database: PostgreSQL
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// =============================================================================
// ENUMS
// =============================================================================
enum ContractType {
FULL_TIME
PART_TIME
MINI_JOB
INTERN
WORKING_STUDENT
FREELANCE
TEMPORARY
}
enum AbsenceType {
VACATION
SICK
SICK_CHILD
SPECIAL_LEAVE
UNPAID_LEAVE
PARENTAL_LEAVE
HOME_OFFICE
BUSINESS_TRIP
TRAINING
COMPENSATION
}
enum ApprovalStatus {
PENDING
APPROVED
REJECTED
CANCELLED
}
enum TimeEntryType {
REGULAR
OVERTIME
HOLIDAY
SICK
VACATION
CORRECTION
}
enum ReviewType {
PROBATION
ANNUAL
PROMOTION
PROJECT
FEEDBACK
EXIT
}
enum S3StatusType {
GREEN
YELLOW
RED
NOT_APPLICABLE
}
enum S3Type {
SEIRI // Sort - Aussortieren
SEITON // Set in Order - Aufräumen
SEISO // Shine - Sauberkeit
}
enum SkillLevel {
NONE
BEGINNER
INTERMEDIATE
ADVANCED
EXPERT
TRAINER
}
enum IntegrationType {
DATEV
PERSONIO
SAP
CALENDAR
EMAIL
CUSTOM
PLENTYONE
ZULIP
TODOIST
FREESCOUT
NEXTCLOUD
ECODMS
GEMBADOCS
}
enum SyncStatus {
PENDING
RUNNING
SUCCESS
ERROR
}
// SQCDM Categories for Morning Meeting
enum SQCDMCategory {
SAFETY // S - Sicherheit
QUALITY // Q - Qualitaet
COST // C - Kosten
DELIVERY // D - Lieferung
MORALE // M - Moral/Mitarbeiter
}
// Meeting Status
enum MeetingStatus {
SCHEDULED
IN_PROGRESS
COMPLETED
CANCELLED
}
// KPI Status (Traffic Light)
enum KPIStatus {
GREEN
YELLOW
RED
NEUTRAL
}
// Trend Direction
enum Trend {
UP
DOWN
STABLE
}
// Action Status
enum ActionStatus {
OPEN
IN_PROGRESS
COMPLETED
CANCELLED
}
// Priority Level
enum Priority {
LOW
MEDIUM
HIGH
CRITICAL
}
// =============================================================================
// CORE MODELS
// =============================================================================
model User {
id String @id @default(cuid())
email String @unique
firstName String
lastName String
keycloakId String? @unique
avatar String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
departmentId String?
department Department? @relation("UserDepartment", fields: [departmentId], references: [id])
roles UserRole[]
employee Employee?
preferences UserPreference?
auditLogs AuditLog[]
// Manager relations
managedDepartments Department[] @relation("DepartmentManager")
correctedTimeEntries TimeEntry[] @relation("TimeEntryCorrectedBy")
approvedAbsences Absence[] @relation("AbsenceApprovedBy")
conductedReviews EmployeeReview[] @relation("ReviewConductedBy")
skillAssessments SkillMatrixEntry[] @relation("SkillAssessedBy")
integrationCredentials IntegrationCredential[]
// LEAN 3S relations
s3PlansCreated S3Plan[] @relation("S3PlanCreatedBy")
s3StatusCompleted S3Status[] @relation("S3StatusCompletedBy")
// Morning Meeting relations
conductedMeetings MorningMeeting[] @relation("MeetingConductor")
assignedMeetingActions MorningMeetingAction[] @relation("ActionAssignee")
@@index([email])
@@index([keycloakId])
@@index([departmentId])
}
model Department {
id String @id @default(cuid())
name String
code String? @unique
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Self-relation for hierarchy
parentId String?
parent Department? @relation("DepartmentHierarchy", fields: [parentId], references: [id])
children Department[] @relation("DepartmentHierarchy")
// Manager relation
managerId String?
manager User? @relation("DepartmentManager", fields: [managerId], references: [id])
// Relations
users User[] @relation("UserDepartment")
onboardingTasks OnboardingTask[]
s3Plans S3Plan[]
skills Skill[]
morningMeetings MorningMeeting[]
@@index([parentId])
@@index([managerId])
}
model Role {
id String @id @default(cuid())
name String @unique
description String?
permissions Json @default("[]")
isSystem Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
users UserRole[]
@@index([name])
}
model UserRole {
id String @id @default(cuid())
userId String
roleId String
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
@@unique([userId, roleId])
@@index([userId])
@@index([roleId])
}
// =============================================================================
// HR MODELS
// =============================================================================
model Employee {
id String @id @default(cuid())
employeeNumber String @unique
position String
entryDate DateTime
exitDate DateTime?
contractType ContractType
workingHours Float @default(40)
salary String? // Encrypted salary (AES-256-GCM)
taxClass Int?
bankAccount Json? // Encrypted bank account (AES-256-GCM)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
timeEntries TimeEntry[]
absences Absence[]
reviews EmployeeReview[]
skillMatrixEntries SkillMatrixEntry[]
@@index([employeeNumber])
@@index([userId])
@@index([entryDate])
}
model TimeEntry {
id String @id @default(cuid())
date DateTime @db.Date
clockIn DateTime?
clockOut DateTime?
breakMinutes Int @default(0)
type TimeEntryType @default(REGULAR)
note String?
isLocked Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
employeeId String
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
correctedById String?
correctedBy User? @relation("TimeEntryCorrectedBy", fields: [correctedById], references: [id])
@@unique([employeeId, date])
@@index([employeeId])
@@index([date])
@@index([correctedById])
}
model Absence {
id String @id @default(cuid())
type AbsenceType
startDate DateTime @db.Date
endDate DateTime @db.Date
days Float
status ApprovalStatus @default(PENDING)
note String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
employeeId String
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
approvedById String?
approvedBy User? @relation("AbsenceApprovedBy", fields: [approvedById], references: [id])
@@index([employeeId])
@@index([startDate, endDate])
@@index([status])
@@index([approvedById])
}
model EmployeeReview {
id String @id @default(cuid())
date DateTime
type ReviewType
notes String? @db.Text
goals Json?
rating Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
employeeId String
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
reviewerId String
reviewer User @relation("ReviewConductedBy", fields: [reviewerId], references: [id])
@@index([employeeId])
@@index([reviewerId])
@@index([date])
@@index([type])
}
model OnboardingTask {
id String @id @default(cuid())
title String
description String? @db.Text
dueOffset Int @default(0) // Days from entry date
assignedTo String? // Role or specific user type
sortOrder Int @default(0)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
departmentId String?
department Department? @relation(fields: [departmentId], references: [id])
@@index([departmentId])
@@index([sortOrder])
}
// =============================================================================
// INTEGRATION MODELS
// =============================================================================
model IntegrationCredential {
id String @id @default(cuid())
type IntegrationType
name String
credentials String // Encrypted JSON (AES-256-GCM)
isActive Boolean @default(true)
lastUsed DateTime?
lastSync DateTime?
syncStatus SyncStatus @default(PENDING)
syncError String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdById String
createdBy User @relation(fields: [createdById], references: [id])
// Relations
syncHistory IntegrationSyncHistory[]
@@unique([type, name])
@@index([type])
@@index([isActive])
@@index([createdById])
}
model IntegrationSyncHistory {
id String @id @default(cuid())
credentialId String
credential IntegrationCredential @relation(fields: [credentialId], references: [id], onDelete: Cascade)
status SyncStatus
startedAt DateTime @default(now())
completedAt DateTime?
duration Int? // Duration in milliseconds
recordsProcessed Int?
error String?
metadata Json? // Additional sync metadata
createdAt DateTime @default(now())
@@index([credentialId])
@@index([status])
@@index([startedAt])
}
// =============================================================================
// LEAN MODELS - 3S / 5S
// =============================================================================
model S3Plan {
id String @id @default(cuid())
year Int
month Int
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
departmentId String
department Department @relation(fields: [departmentId], references: [id])
createdById String
createdBy User @relation("S3PlanCreatedBy", fields: [createdById], references: [id])
categories S3Category[]
@@unique([departmentId, year, month])
@@index([departmentId])
@@index([year, month])
@@index([createdById])
}
model S3Category {
id String @id @default(cuid())
name String
s3Type S3Type // SEIRI, SEITON, SEISO
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
planId String
plan S3Plan @relation(fields: [planId], references: [id], onDelete: Cascade)
statuses S3Status[]
@@index([planId])
@@index([sortOrder])
@@index([s3Type])
}
model S3Status {
id String @id @default(cuid())
week Int // 1-53
status S3StatusType
photo String? // URL to uploaded image
note String?
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
categoryId String
category S3Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
completedById String?
completedBy User? @relation("S3StatusCompletedBy", fields: [completedById], references: [id])
@@unique([categoryId, week])
@@index([categoryId])
@@index([week])
@@index([status])
@@index([completedById])
}
// =============================================================================
// LEAN MODELS - Morning Meeting (Shopfloor Management)
// =============================================================================
model MorningMeeting {
id String @id @default(cuid())
date DateTime @db.Date
startTime DateTime? // Actual start time
endTime DateTime? // Actual end time
duration Int? // Duration in minutes (calculated)
participants Json? // Array of participant user IDs
notes String? @db.Text
status MeetingStatus @default(SCHEDULED)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
departmentId String
department Department @relation(fields: [departmentId], references: [id])
conductorId String?
conductor User? @relation("MeetingConductor", fields: [conductorId], references: [id])
topics MorningMeetingTopic[]
actions MorningMeetingAction[]
@@unique([departmentId, date])
@@index([departmentId])
@@index([date])
@@index([status])
@@index([conductorId])
}
model MorningMeetingTopic {
id String @id @default(cuid())
category SQCDMCategory // SAFETY, QUALITY, COST, DELIVERY, MORALE
title String
value String? // Current value
target String? // Target value
unit String? // Unit (%, pieces, EUR, etc.)
status KPIStatus @default(NEUTRAL)
trend Trend? // UP, DOWN, STABLE
sortOrder Int @default(0)
note String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
meetingId String
meeting MorningMeeting @relation(fields: [meetingId], references: [id], onDelete: Cascade)
@@index([meetingId])
@@index([category])
@@index([sortOrder])
@@index([status])
}
model MorningMeetingAction {
id String @id @default(cuid())
title String
description String? @db.Text
dueDate DateTime? @db.Date
status ActionStatus @default(OPEN)
priority Priority @default(MEDIUM)
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
meetingId String
meeting MorningMeeting @relation(fields: [meetingId], references: [id], onDelete: Cascade)
assigneeId String?
assignee User? @relation("ActionAssignee", fields: [assigneeId], references: [id])
@@index([meetingId])
@@index([assigneeId])
@@index([status])
@@index([priority])
@@index([dueDate])
}
// =============================================================================
// LEAN MODELS - Skill Matrix
// =============================================================================
model Skill {
id String @id @default(cuid())
name String
description String?
category String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
departmentId String?
department Department? @relation(fields: [departmentId], references: [id])
entries SkillMatrixEntry[]
@@unique([name, departmentId])
@@index([departmentId])
@@index([category])
}
model SkillMatrixEntry {
id String @id @default(cuid())
level SkillLevel @default(NONE)
assessedAt DateTime @default(now())
note String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
employeeId String
employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade)
skillId String
skill Skill @relation(fields: [skillId], references: [id], onDelete: Cascade)
assessedById String?
assessedBy User? @relation("SkillAssessedBy", fields: [assessedById], references: [id])
@@unique([employeeId, skillId])
@@index([employeeId])
@@index([skillId])
@@index([assessedById])
@@index([level])
}
// =============================================================================
// USER PREFERENCES
// =============================================================================
model UserPreference {
id String @id @default(cuid())
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
theme String @default("system") // "light", "dark", "system"
language String @default("de") // "de", "en"
dashboardLayout Json? // Array of widget configurations
notifications Json? // Notification preferences
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
}
// =============================================================================
// AUDIT LOGGING
// =============================================================================
model AuditLog {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id])
action String // CREATE, UPDATE, DELETE, LOGIN, LOGOUT, etc.
entity String // User, Department, Employee, etc.
entityId String?
oldData Json?
newData Json?
ipAddress String?
userAgent String?
createdAt DateTime @default(now())
@@index([userId])
@@index([entity, entityId])
@@index([action])
@@index([createdAt])
}
// =============================================================================
// SYSTEM SETTINGS
// =============================================================================
model SystemSetting {
id String @id @default(cuid())
key String @unique
value String
category String
description String?
valueType String @default("string")
isSecret Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([category])
}