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>
675 lines
18 KiB
Plaintext
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])
|
|
}
|