Files
duffyduckandClaude Opus 5 84eb93c59f Rolleneditor: eigene Rollen neben den Standardrollen
Rechte sind jetzt die Grundgröße, eine Rolle ist ein benanntes Bündel davon.
Die elf Standardrollen stehen weiterhin im Code und sind unveränderlich –
Prüfvermerke und Bescheinigungen berufen sich auf sie. Daneben lassen sich
eigene Rollen anlegen.

Rolleneditor unter Verwaltung > Rollen & Rechte
- Rolle aus einer Standardrolle als Vorlage ableiten oder von Grund auf anlegen
- Rechte nach Modul gruppiert ankreuzen, mit Sammelschaltern je Gruppe
- Seite des Bauvertrags festlegen (Auftraggeber / Auftragnehmer / Zuarbeit) –
  bestimmt die Datensicht, unabhängig von der Rechteliste
- bearbeiten, stilllegen, löschen; die Rechtematrix zeigt eigene Rollen mit an

Grenzen
- Administrationsrechte sind nicht über eine Rolle vergebbar, sonst ließe sich
  über eine selbst gebaute Rolle die Benutzerverwaltung erreichen
- eine Rolle ohne jedes Recht wird abgelehnt
- eine vergebene Rolle lässt sich weder stilllegen noch löschen
- die Vier-Augen-Trennung bleibt unabhängig von der Rolle bestehen

Umbau der Rechteauswertung
- Rechte werden an genau einer Stelle ermittelt: rechteImProjekt() führt
  Standardrollen und eigene Rollen zu einer Rechtelage zusammen, die Seiten und
  Server Actions über rechte.darf() bzw. rechte.pruefe() befragen
- die Firmenschranke hängt nun an der Seite des Bauvertrags, die jede Rolle
  mitbringt, statt an einer festen Liste privilegierter Standardrollen
- ProjectMember trägt wahlweise eine Standardrolle oder eine eigene Rolle

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 14:13:38 +02:00

1285 lines
39 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// BauDoc Bahn Datenmodell
// Fachliche Grundlage: EIU-Ablagestruktur (DB), Vorgabeprozess Bauüberwachung,
// VV BAU / VV BAU-STE, VOB/B, Ril 809 (Projektmanagement Bau).
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ═══════════════════════════════════════════════════════════════════
// 1 Identität, Organisationen, Rechte
// ═══════════════════════════════════════════════════════════════════
enum CompanyType {
EIU // Eisenbahninfrastrukturunternehmen / Bauherr (DB InfraGO etc.)
AUFTRAGNEHMER // Baufirma
PLANUNGSBUERO
GUTACHTER // Prüfingenieur, Sachverständiger, TÜV
SICHERUNGSUNTERNEHMEN
BEHOERDE // EBA, Kommune, Landesbehörde
BETREIBER // Anlagenbetreiber / Instandhaltung
SONSTIGE
}
enum Role {
ADMIN // Systemadministration
PROJEKTLEITER // PL
BAUHERRENVERTRETER // BHV
BAUUEBERWACHER // BÜB Bauüberwacher Bahn
FACHBAUUEBERWACHER // FBÜ
KAUFMANN // kaufmännische Projektsteuerung, "rechnerisch richtig"
PLANER
AUFTRAGNEHMER // Baufirma nur eigene Vorgänge
PRUEFER // Gutachter / Sachverständiger
BETREIBER // liest Betreiberakte
LESER
}
/// Auf welcher Seite des Bauvertrags eine Rolle steht. Bestimmt, ob die
/// Trägerin oder der Träger das ganze Projekt oder nur die eigene Firma sieht.
enum RolleSeite {
AG // Bauherrenseite sieht das gesamte Projekt
AN // Auftragnehmerseite sieht ausschließlich die eigene Firma
NEUTRAL // Zuarbeit, Prüfung, Mitlesen ebenfalls auf die eigene Firma beschränkt
}
/// Frei definierbare Rolle. Die elf Standardrollen stehen im Code
/// (src/lib/rbac.ts) und sind unveränderlich; hier entstehen zusätzliche
/// Rollen, wahlweise auf Grundlage einer Standardrolle als Vorlage.
model CustomRole {
id String @id @default(cuid())
schluessel String @unique // technischer Name, z.B. "fbue-oberleitung"
name String
beschreibung String?
seite RolleSeite @default(NEUTRAL)
vorlage Role? // Standardrolle, von der abgeleitet wurde
rechte String[] @default([]) // Schlüssel aus PERMISSIONS
aktiv Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdById String?
createdBy User? @relation("CustomRoleAutor", fields: [createdById], references: [id])
mitglieder ProjectMember[]
}
model Company {
id String @id @default(cuid())
name String
shortName String?
type CompanyType
street String?
zip String?
city String?
country String @default("DE")
ustId String?
kreditorNr String? // Kreditorennummer für die Rechnungsprüfung
contactEmail String?
contactPhone String?
notes String?
active Boolean @default(true)
createdAt DateTime @default(now())
users User[]
contracts Contract[]
invoices Invoice[]
issues Issue[] @relation("IssueAgainstCompany")
diaryCrews DiaryPersonnel[]
@@index([type])
}
model User {
id String @id @default(cuid())
email String @unique
name String
passwordHash String
phone String?
title String? // z.B. "Dipl.-Ing."
position String? // Freitext, z.B. "Bauüberwacher Bahn KIB"
active Boolean @default(true)
isAdmin Boolean @default(false)
mustChangePw Boolean @default(false)
lastLoginAt DateTime?
createdAt DateTime @default(now())
companyId String?
company Company? @relation(fields: [companyId], references: [id])
// Benachrichtigungen
notifyMode NotifyMode @default(SOFORT)
notifyMentions Boolean @default(true)
notifyAssignments Boolean @default(true)
notifyDeadlines Boolean @default(true)
notifyDigestHour Int @default(7)
memberships ProjectMember[]
sessions Session[]
uploadedVersions DocumentVersion[]
reviews DocumentReview[]
diaryEntries SiteDiaryEntry[] @relation("DiaryAuthor")
diarySignatures SiteDiaryEntry[] @relation("DiarySignedBy")
issuesRaised Issue[] @relation("IssueRaisedBy")
issuesAssigned Issue[] @relation("IssueAssignee")
issueComments IssueComment[]
timeSheetsFiled TimeSheet[] @relation("TimeSheetFiledBy")
timeSheetsChecked TimeSheet[] @relation("TimeSheetCheckedBy")
measurements Measurement[] @relation("MeasurementCreatedBy")
invoicesSachlich Invoice[] @relation("InvoiceSachlich")
invoicesRechn Invoice[] @relation("InvoiceRechnerisch")
invoicesFreigabe Invoice[] @relation("InvoiceFreigabe")
messages Message[]
channelMembers ChannelMember[]
notifications Notification[]
auditLogs AuditLog[]
slotResponsible ProjectDocumentSlot[] @relation("SlotResponsible")
tasks Task[] @relation("TaskAssignee")
eigeneRollen CustomRole[] @relation("CustomRoleAutor")
@@index([companyId])
}
enum NotifyMode {
SOFORT
TAGESZUSAMMENFASSUNG
AUS
}
model Session {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
token String @unique
userAgent String?
ip String?
createdAt DateTime @default(now())
expiresAt DateTime
@@index([userId])
}
model ProjectMember {
id String @id @default(cuid())
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// Entweder eine Standardrolle aus dem Code oder eine selbst angelegte.
// Genau eines von beiden ist gesetzt; darauf achtet die Anwendungsschicht.
role Role?
customRoleId String?
customRole CustomRole? @relation(fields: [customRoleId], references: [id])
// Fachbereiche, auf die sich diese Rolle beschränkt (leer = alle)
fachbereiche String[] @default([])
createdAt DateTime @default(now())
@@unique([projectId, userId, role, customRoleId])
@@index([userId])
@@index([customRoleId])
}
// ═══════════════════════════════════════════════════════════════════
// 2 Projekt / Baustelle
// ═══════════════════════════════════════════════════════════════════
enum Bauherr {
DB_INFRAGO_FAHRWEG // vormals DB Netz
DB_INFRAGO_BAHNHOF // vormals DB Station&Service
DB_ENERGIE
SONSTIGE
}
enum ProjectStatus {
ANLAGE
PLANUNG
VERGABE
BAU
INBETRIEBNAHME
ABNAHME
GEWAEHRLEISTUNG
ABGESCHLOSSEN
RUHEND
}
model Project {
id String @id @default(cuid())
nummer String @unique // PSP-Element / Projektnummer
name String
kurzname String?
bauherr Bauherr @default(DB_INFRAGO_FAHRWEG)
status ProjectStatus @default(ANLAGE)
regionalbereich String?
beschreibung String?
// Fachbereiche, die im Projekt vorkommen steuert den Soll-Dokumentenkatalog
fachbereiche String[] @default([])
budgetPlan Decimal? @db.Decimal(14, 2)
// Terminanker daraus werden die Fristen der Ablagestruktur berechnet
startgespraech DateTime?
baubeginn DateTime?
bauende DateTime?
inbetriebnahme DateTime?
bauschlussmeldung DateTime?
projektabschluss DateTime?
// ── Projekteinstellungen ──────────────────────────────────────
// Was Auftragnehmer in diesem Projekt sehen dürfen. Immer nur die
// eigene Firma diese Schalter entscheiden nur, OB der Bereich
// überhaupt sichtbar ist. Voreinstellung: das, was die Baufirma für
// ihre eigene Arbeit braucht.
anSichtVertrag Boolean @default(true) // eigenes LV und eigene Nachträge
anSichtRechnungen Boolean @default(true) // Prüfstand der eigenen Rechnungen
anSichtBautagebuch Boolean @default(false) // Bautagebuch des Bauherrn
anSichtDokumentation Boolean @default(false) // Soll-Ist der Ablagestruktur
anDarfDokumente Boolean @default(true) // Unterlagen einstellen
// Ablauf
freigabeTrotzBefund Boolean @default(true) // sachlich richtig trotz kritischer Befunde (mit Begründung)
archiviert Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
abschnitte ProjectSection[]
bauwerke Bauwerk[]
members ProjectMember[]
slots ProjectDocumentSlot[]
documents Document[]
diary SiteDiaryEntry[]
issues Issue[]
contracts Contract[]
timeSheets TimeSheet[]
measurements Measurement[]
invoices Invoice[]
channels Channel[]
milestones Milestone[]
tasks Task[]
warranties Warranty[]
@@index([status])
}
model ProjectSection {
id String @id @default(cuid())
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
streckenNr String? // VzG-Streckennummer
streckenName String?
kmVon String?
kmBis String?
betriebsstelle String?
bemerkung String?
@@index([projectId])
}
model Bauwerk {
id String @id @default(cuid())
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
bauwerksNr String? // ASB/BWV-Nummer
bezeichnung String
typ String? // EÜ, SÜ, Tunnel, Bahnsteig, Stellwerk, Stützbauwerk ...
fachbereich String?
kmLage String?
baujahr Int?
bemerkung String?
ueberwachungspflichtig Boolean @default(false)
documents Document[]
warranties Warranty[]
@@index([projectId])
}
enum MilestoneType {
STARTGESPRAECH
BAUANLAUF
BAUBEGINN
ZWISCHEN_IBN
INBETRIEBNAHME
BAUENDE
ABNAHME
BAUSCHLUSSMELDUNG
PROJEKTABSCHLUSS
SONSTIGES
}
model Milestone {
id String @id @default(cuid())
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
typ MilestoneType
titel String
geplant DateTime?
ist DateTime?
bemerkung String?
@@index([projectId])
}
// ═══════════════════════════════════════════════════════════════════
// 3 Ablagestruktur (Katalog) und Dokumentenmanagement
// ═══════════════════════════════════════════════════════════════════
enum Akte {
P // Planungsakte
B // Baudokumentation / Bauakten
K // Kaufmännische Akte
}
/// Ordnerknoten der EIU-Ablagestruktur aus der Excel des Auftraggebers.
model FilingNode {
code String @id // z.B. "B.I.2.2"
parentCode String?
parent FilingNode? @relation("FilingTree", fields: [parentCode], references: [code])
children FilingNode[] @relation("FilingTree")
akte Akte
level Int
titel String
ril String?
sortIndex Int
requirements DocumentRequirement[]
documents Document[]
@@index([parentCode])
@@index([akte])
}
enum Uebergabezeitpunkt {
INBETRIEBNAHME
BAUAKTE_II_IBN
IBN_PLUS_3M
IBN_PLUS_6M
BAUAKTE_I_BAUENDE_6M
PROJEKTABSCHLUSS
}
enum Medium {
MIKROFICHE
DIGITAL
PAPIERAKTE
}
/// Einzelne Soll-Unterlage aus der Ablagestruktur (945 Positionen).
model DocumentRequirement {
id String @id @default(cuid())
ordnung Int @unique // laufende Nummer aus der Excel
nodeCode String
node FilingNode @relation(fields: [nodeCode], references: [code], onDelete: Cascade)
titel String
ril String?
istUnterpunkt Boolean @default(false)
relevantNetz Boolean @default(false)
relevantBahnhof Boolean @default(false)
fachbereiche String[] @default([]) // vereinigte Fachbereichsliste
aufbewahrungNutzungsdauer Boolean @default(false)
aufbewahrungJahre String?
medien Medium[] @default([])
uebergabe Uebergabezeitpunkt[] @default([])
bemerkung String?
slots ProjectDocumentSlot[]
@@index([nodeCode])
}
enum SlotStatus {
OFFEN
ANGEFORDERT
IN_ARBEIT
EINGEREICHT
IN_PRUEFUNG
FREIGEGEBEN
ABGELEHNT
ENTFAELLT
}
/// Soll-Ist-Abgleich: pro Projekt eine Zeile je relevanter Soll-Unterlage.
/// Das ist die lebende Version der Excel-Prüfliste.
model ProjectDocumentSlot {
id String @id @default(cuid())
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
requirementId String
requirement DocumentRequirement @relation(fields: [requirementId], references: [id], onDelete: Cascade)
status SlotStatus @default(OFFEN)
responsibleId String?
responsible User? @relation("SlotResponsible", fields: [responsibleId], references: [id])
frist DateTime? // berechnet aus Übergabezeitpunkt + Projekttermine
eingangAm DateTime? // "Datum Eintreffen der Unterlagen"
entfalltGrund String?
bemerkung String?
updatedAt DateTime @updatedAt
documents Document[]
@@unique([projectId, requirementId])
@@index([projectId, status])
}
enum DocumentStatus {
ENTWURF
EINGEREICHT
IN_PRUEFUNG
FREIGEGEBEN
ABGELEHNT
ZURUECKGEZOGEN
ARCHIVIERT
}
enum Vertraulichkeit {
OFFEN // alle Projektbeteiligten
INTERN // nur Bauherrenseite (EIU)
VERTRAULICH // nur explizit Berechtigte
}
model Document {
id String @id @default(cuid())
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
nodeCode String?
node FilingNode? @relation(fields: [nodeCode], references: [code])
slotId String?
slot ProjectDocumentSlot? @relation(fields: [slotId], references: [id])
bauwerkId String?
bauwerk Bauwerk? @relation(fields: [bauwerkId], references: [id])
titel String
dokumentNr String?
fachbereich String?
status DocumentStatus @default(ENTWURF)
vertraulich Vertraulichkeit @default(OFFEN)
beschreibung String?
schlagworte String[] @default([])
planNr String?
planStand String?
gueltigAb DateTime?
aufbewahrungBis DateTime?
createdById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
versions DocumentVersion[]
reviews DocumentReview[]
invoices Invoice[]
links DocumentLink[] @relation("LinkFrom")
linkedBy DocumentLink[] @relation("LinkTo")
@@index([projectId, status])
@@index([nodeCode])
@@index([slotId])
}
model DocumentVersion {
id String @id @default(cuid())
documentId String
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
version Int
dateiname String
pfad String // relativ zu STORAGE_DIR
mimeType String
groesse Int
sha256 String
kommentar String?
uploadedById String?
uploadedBy User? @relation(fields: [uploadedById], references: [id])
uploadedAt DateTime @default(now())
@@unique([documentId, version])
@@index([sha256])
}
enum PruefArt {
FACHTECHNISCHE_PRUEFUNG
FORMALE_PRUEFUNG
FREIGABE
KENNTNISNAHME
SACHLICHE_PRUEFUNG
}
enum PruefErgebnis {
OFFEN
FREIGEGEBEN
FREIGEGEBEN_MIT_AUFLAGEN
ABGELEHNT
}
model DocumentReview {
id String @id @default(cuid())
documentId String
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
version Int?
art PruefArt
ergebnis PruefErgebnis @default(OFFEN)
reviewerId String
reviewer User @relation(fields: [reviewerId], references: [id])
faelligAm DateTime?
erledigtAm DateTime?
kommentar String?
auflagen String?
createdAt DateTime @default(now())
@@index([documentId])
@@index([reviewerId, ergebnis])
}
model DocumentLink {
id String @id @default(cuid())
fromId String
from Document @relation("LinkFrom", fields: [fromId], references: [id], onDelete: Cascade)
toId String
to Document @relation("LinkTo", fields: [toId], references: [id], onDelete: Cascade)
art String? // "ersetzt", "gehört zu", "Prüfbericht zu" ...
@@unique([fromId, toId])
}
// ═══════════════════════════════════════════════════════════════════
// 4 Bautagebuch (angelehnt an eBTB / Ril 809.0301V01)
// ═══════════════════════════════════════════════════════════════════
enum DiaryStatus {
ENTWURF
ABGESCHLOSSEN
GEGENGEZEICHNET
}
model SiteDiaryEntry {
id String @id @default(cuid())
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
datum DateTime @db.Date
lfdNr Int
authorId String
author User @relation("DiaryAuthor", fields: [authorId], references: [id])
wetterFrueh String?
wetterMittag String?
wetterAbend String?
tempMin Int?
tempMax Int?
windRegen String?
arbeitszeitVon String?
arbeitszeitBis String?
betra String? // Betriebs- und Bauanweisung
sperrpause String?
sperrungVon DateTime?
sperrungBis DateTime?
leistungen String? // ausgeführte Arbeiten
besondereVorkommnisse String?
behinderungen String?
anordnungen String?
einweisungen String? // Ersteinweisung / Baustellenbelehrung
sicherungsmassnahmen String?
bemerkungen String?
status DiaryStatus @default(ENTWURF)
signedById String?
signedBy User? @relation("DiarySignedBy", fields: [signedById], references: [id])
signedAt DateTime?
gesperrtAm DateTime? // revisionssicher: danach nur noch Nachträge
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
personal DiaryPersonnel[]
geraete DiaryEquipment[]
issues Issue[]
timeSheets TimeSheet[]
@@unique([projectId, datum, lfdNr])
@@index([projectId, datum])
}
model DiaryPersonnel {
id String @id @default(cuid())
entryId String
entry SiteDiaryEntry @relation(fields: [entryId], references: [id], onDelete: Cascade)
companyId String?
company Company? @relation(fields: [companyId], references: [id])
gewerk String?
taetigkeit String?
anzahl Int @default(0)
stunden Decimal @default(0) @db.Decimal(8, 2)
@@index([entryId])
}
model DiaryEquipment {
id String @id @default(cuid())
entryId String
entry SiteDiaryEntry @relation(fields: [entryId], references: [id], onDelete: Cascade)
bezeichnung String
anzahl Int @default(1)
stunden Decimal @default(0) @db.Decimal(8, 2)
bemerkung String?
@@index([entryId])
}
// ═══════════════════════════════════════════════════════════════════
// 5 Meldungen / Vorgänge / Mängel
// ═══════════════════════════════════════════════════════════════════
enum IssueType {
BEHINDERUNGSANZEIGE
BEDENKENANZEIGE
MANGELANZEIGE
MEHRKOSTEN_AVL // Anordnung von Leistungen
SCHADENSMELDUNG
UNFALLANZEIGE
SOFORTMELDUNG
VERTRAGSABWEICHUNG
BETREIBERHINWEIS
SONSTIGES
}
enum IssueStatus {
OFFEN
IN_BEARBEITUNG
BEHOBEN_GEMELDET
NACHKONTROLLE
ERLEDIGT
ABGELEHNT
}
enum Severity {
GERING
MITTEL
HOCH
SICHERHEITSRELEVANT
}
model Issue {
id String @id @default(cuid())
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
nr Int
typ IssueType
titel String
beschreibung String?
status IssueStatus @default(OFFEN)
schwere Severity @default(MITTEL)
ortKm String?
bauteil String?
fachbereich String?
raisedById String
raisedBy User @relation("IssueRaisedBy", fields: [raisedById], references: [id])
assigneeId String?
assignee User? @relation("IssueAssignee", fields: [assigneeId], references: [id])
gegenFirmaId String?
gegenFirma Company? @relation("IssueAgainstCompany", fields: [gegenFirmaId], references: [id])
contractId String?
contract Contract? @relation(fields: [contractId], references: [id])
diaryEntryId String?
diaryEntry SiteDiaryEntry? @relation(fields: [diaryEntryId], references: [id])
gemeldetAm DateTime @default(now())
fristAm DateTime?
erledigtAm DateTime?
kostenrelevant Boolean @default(false)
terminrelevant Boolean @default(false)
geschaetzteKosten Decimal? @db.Decimal(12, 2)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
kommentare IssueComment[]
@@unique([projectId, nr])
@@index([projectId, status])
}
model IssueComment {
id String @id @default(cuid())
issueId String
issue Issue @relation(fields: [issueId], references: [id], onDelete: Cascade)
authorId String
author User @relation(fields: [authorId], references: [id])
text String
createdAt DateTime @default(now())
@@index([issueId])
}
/// Fristenblatt zu Mängelansprüchen (Ablagestruktur B 6.6)
model Warranty {
id String @id @default(cuid())
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
bauwerkId String?
bauwerk Bauwerk? @relation(fields: [bauwerkId], references: [id])
contractId String?
contract Contract? @relation(fields: [contractId], references: [id])
gegenstand String
abnahmeAm DateTime
fristJahre Int @default(4)
fristEndeAm DateTime
verantwortlich String?
buergschaft Decimal? @db.Decimal(12, 2)
status String @default("LAUFEND")
bemerkung String?
@@index([projectId, fristEndeAm])
}
// ═══════════════════════════════════════════════════════════════════
// 6 Verträge, Nachträge, LV
// ═══════════════════════════════════════════════════════════════════
enum ContractType {
BAUVERTRAG
INGENIEURVERTRAG
RAHMENVERTRAG
SICHERUNGSLEISTUNG
LIEFERVERTRAG
SONSTIGER
}
enum ContractStatus {
ENTWURF
BEAUFTRAGT
IN_ABWICKLUNG
ABGENOMMEN
SCHLUSSGERECHNET
ABGESCHLOSSEN
GEKUENDIGT
}
model Contract {
id String @id @default(cuid())
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
companyId String
company Company @relation(fields: [companyId], references: [id])
nummer String
bezeichnung String
typ ContractType @default(BAUVERTRAG)
status ContractStatus @default(BEAUFTRAGT)
vergabeart String?
auftragssumme Decimal @default(0) @db.Decimal(14, 2)
waehrung String @default("EUR")
beauftragtAm DateTime?
ausfuehrungVon DateTime?
ausfuehrungBis DateTime?
gewaehrleistungJahre Int @default(4)
sicherheitseinbehaltProzent Decimal @default(0) @db.Decimal(5, 2)
skontoProzent Decimal @default(0) @db.Decimal(5, 2)
skontoTage Int @default(0)
zahlungszielTage Int @default(30)
vertragsverantwortlich String?
bemerkung String?
createdAt DateTime @default(now())
positionen ContractItem[]
nachtraege ChangeOrder[]
timeSheets TimeSheet[]
measurements Measurement[]
invoices Invoice[]
issues Issue[]
warranties Warranty[]
@@unique([projectId, nummer])
@@index([companyId])
}
model ContractItem {
id String @id @default(cuid())
contractId String
contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
changeOrderId String?
changeOrder ChangeOrder? @relation(fields: [changeOrderId], references: [id])
oz String // Ordnungszahl der LV-Position
kurztext String
langtext String?
einheit String @default("St")
menge Decimal @default(0) @db.Decimal(14, 3)
einheitspreis Decimal @default(0) @db.Decimal(12, 2)
istStundenlohn Boolean @default(false)
istEventual Boolean @default(false)
measurementLines MeasurementLine[]
invoiceLines InvoiceLine[]
@@unique([contractId, oz])
@@index([contractId])
}
enum ChangeOrderStatus {
ANGEMELDET
ANGEBOT_EINGEGANGEN
IN_PRUEFUNG_BUEW
STELLUNGNAHME_ABGEGEBEN
BEAUFTRAGT
ABGELEHNT
ZURUECKGEZOGEN
}
model ChangeOrder {
id String @id @default(cuid())
contractId String
contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
nummer String
titel String
begruendung String?
rechtsgrundlage String? // § 2 Abs. 5 / 6 VOB/B, § 1 Abs. 3 / 4 VOB/B
angebotssumme Decimal? @db.Decimal(14, 2)
anerkannteSumme Decimal? @db.Decimal(14, 2)
status ChangeOrderStatus @default(ANGEMELDET)
angemeldetAm DateTime?
geprueftAm DateTime?
beauftragtAm DateTime?
stellungnahme String?
positionen ContractItem[]
invoiceLines InvoiceLine[]
@@unique([contractId, nummer])
}
// ═══════════════════════════════════════════════════════════════════
// 7 Aufmaß und Stundenlohnzettel
// ═══════════════════════════════════════════════════════════════════
enum MeasurementArt {
ABRECHNUNGSZEICHNUNG
OERTLICHES_AUFMASS
AUFMASSBLATT
}
enum PruefStatus {
EINGEREICHT
IN_PRUEFUNG
ANERKANNT
TEILWEISE_ANERKANNT
ABGELEHNT
}
model Measurement {
id String @id @default(cuid())
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
contractId String
contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
nummer String
art MeasurementArt @default(AUFMASSBLATT)
datum DateTime
ortBauteil String?
status PruefStatus @default(EINGEREICHT)
createdById String
createdBy User @relation("MeasurementCreatedBy", fields: [createdById], references: [id])
geprueftAm DateTime?
bemerkung String?
createdAt DateTime @default(now())
zeilen MeasurementLine[]
@@unique([contractId, nummer])
@@index([projectId])
}
model MeasurementLine {
id String @id @default(cuid())
measurementId String
measurement Measurement @relation(fields: [measurementId], references: [id], onDelete: Cascade)
contractItemId String?
contractItem ContractItem? @relation(fields: [contractItemId], references: [id])
beschreibung String?
formel String? // REB-konforme Berechnung, z.B. "12,50 * 3,20"
menge Decimal @default(0) @db.Decimal(14, 3)
mengeAnerkannt Decimal? @db.Decimal(14, 3)
@@index([measurementId])
}
enum LohnArt {
FACHARBEITER
VORARBEITER
POLIER
HELFER
MASCHINIST
SICHERUNGSPOSTEN
GERAET
SONSTIGES
}
/// Stundenlohnzettel (Ablagestruktur K 3.4) Grundlage der Rechnungsprüfung.
model TimeSheet {
id String @id @default(cuid())
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
contractId String
contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade)
nummer String
datum DateTime @db.Date
leistung String
ortKm String?
bezugNachtrag String?
bezugPosition String?
diaryEntryId String?
diaryEntry SiteDiaryEntry? @relation(fields: [diaryEntryId], references: [id])
status PruefStatus @default(EINGEREICHT)
filedById String
filedBy User @relation("TimeSheetFiledBy", fields: [filedById], references: [id])
checkedById String?
checkedBy User? @relation("TimeSheetCheckedBy", fields: [checkedById], references: [id])
geprueftAm DateTime?
pruefkommentar String?
createdAt DateTime @default(now())
zeilen TimeSheetLine[]
invoiceLines InvoiceLine[]
@@unique([contractId, nummer])
@@index([projectId, datum])
@@index([status])
}
model TimeSheetLine {
id String @id @default(cuid())
timeSheetId String
timeSheet TimeSheet @relation(fields: [timeSheetId], references: [id], onDelete: Cascade)
name String? // Person oder Gerätebezeichnung
art LohnArt @default(FACHARBEITER)
stundenGemeldet Decimal @default(0) @db.Decimal(8, 2)
stundenAnerkannt Decimal? @db.Decimal(8, 2)
stundensatz Decimal @default(0) @db.Decimal(10, 2)
zuschlagProzent Decimal @default(0) @db.Decimal(5, 2)
zuschlagGrund String? // Nacht, Sonntag, Feiertag, Sperrpause
bemerkung String?
@@index([timeSheetId])
}
// ═══════════════════════════════════════════════════════════════════
// 8 Rechnungsprüfung
// ═══════════════════════════════════════════════════════════════════
enum InvoiceType {
ABSCHLAGSRECHNUNG
TEILSCHLUSSRECHNUNG
SCHLUSSRECHNUNG
NACHTRAGSRECHNUNG
STUNDENLOHNRECHNUNG
GUTSCHRIFT
}
enum InvoiceStatus {
EINGEGANGEN
IN_PRUEFUNG
RUECKFRAGE
SACHLICH_RICHTIG // Bauüberwachung
RECHNERISCH_RICHTIG // kaufmännisch
FREIGEGEBEN
ZURUECKGEWIESEN
BEZAHLT
}
model Invoice {
id String @id @default(cuid())
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
contractId String?
contract Contract? @relation(fields: [contractId], references: [id])
companyId String
company Company @relation(fields: [companyId], references: [id])
rechnungsNr String
typ InvoiceType @default(ABSCHLAGSRECHNUNG)
status InvoiceStatus @default(EINGEGANGEN)
rechnungsdatum DateTime
eingangsdatum DateTime @default(now())
leistungVon DateTime?
leistungBis DateTime?
nettoBetrag Decimal @default(0) @db.Decimal(14, 2)
ustSatz Decimal @default(19) @db.Decimal(5, 2)
ustBetrag Decimal @default(0) @db.Decimal(14, 2)
bruttoBetrag Decimal @default(0) @db.Decimal(14, 2)
// kumulierte Vorauszahlungen bei Abschlagsrechnungen
bereitsGezahlt Decimal @default(0) @db.Decimal(14, 2)
einbehalt Decimal @default(0) @db.Decimal(14, 2)
skonto Decimal @default(0) @db.Decimal(14, 2)
zahlbarBis DateTime?
// anerkannte Summe nach Prüfung
anerkanntNetto Decimal? @db.Decimal(14, 2)
kuerzungNetto Decimal @default(0) @db.Decimal(14, 2)
kuerzungGrund String?
sachlichById String?
sachlichBy User? @relation("InvoiceSachlich", fields: [sachlichById], references: [id])
sachlichAm DateTime?
rechnerischById String?
rechnerischBy User? @relation("InvoiceRechnerisch", fields: [rechnerischById], references: [id])
rechnerischAm DateTime?
freigabeById String?
freigabeBy User? @relation("InvoiceFreigabe", fields: [freigabeById], references: [id])
freigabeAm DateTime?
documentId String?
document Document? @relation(fields: [documentId], references: [id])
letzterPrueflauf DateTime?
bemerkung String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
zeilen InvoiceLine[]
befunde InvoiceFinding[]
@@unique([companyId, rechnungsNr])
@@index([projectId, status])
}
enum InvoiceLineKind {
LV_POSITION
STUNDENLOHN
NACHTRAG
MATERIAL
SONSTIGES
}
model InvoiceLine {
id String @id @default(cuid())
invoiceId String
invoice Invoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade)
position Int
art InvoiceLineKind @default(LV_POSITION)
oz String?
bezeichnung String
einheit String @default("St")
menge Decimal @default(0) @db.Decimal(14, 3)
einheitspreis Decimal @default(0) @db.Decimal(12, 2)
betrag Decimal @default(0) @db.Decimal(14, 2)
contractItemId String?
contractItem ContractItem? @relation(fields: [contractItemId], references: [id])
timeSheetId String?
timeSheet TimeSheet? @relation(fields: [timeSheetId], references: [id])
changeOrderId String?
changeOrder ChangeOrder? @relation(fields: [changeOrderId], references: [id])
mengeAnerkannt Decimal? @db.Decimal(14, 3)
betragAnerkannt Decimal? @db.Decimal(14, 2)
befunde InvoiceFinding[]
@@index([invoiceId])
}
enum FindingKind {
KEIN_VERTRAGSBEZUG
PREIS_ABWEICHUNG
MENGE_OHNE_AUFMASS
MENGE_UEBER_AUFMASS
STUNDEN_OHNE_ZETTEL
STUNDEN_UEBER_ZETTEL
STUNDENSATZ_ABWEICHUNG
ZETTEL_NICHT_ANERKANNT
DOPPELABRECHNUNG
NACHTRAG_NICHT_BEAUFTRAGT
RECHENFEHLER
SUMME_UEBER_AUFTRAG
ZUSCHLAG_UNBELEGT
HINWEIS
}
enum FindingSeverity {
INFO
WARNUNG
KRITISCH
}
/// Ergebnis des automatischen Abgleichs Rechnung ↔ Vertrag/Aufmaß/Stundenzettel.
model InvoiceFinding {
id String @id @default(cuid())
invoiceId String
invoice Invoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade)
lineId String?
line InvoiceLine? @relation(fields: [lineId], references: [id], onDelete: Cascade)
art FindingKind
schwere FindingSeverity @default(WARNUNG)
meldung String
erwartet String?
gefunden String?
differenz Decimal? @db.Decimal(14, 2)
erledigt Boolean @default(false)
erledigtGrund String?
createdAt DateTime @default(now())
@@index([invoiceId])
}
// ═══════════════════════════════════════════════════════════════════
// 9 Kommunikation und Benachrichtigung
// ═══════════════════════════════════════════════════════════════════
enum ChannelKind {
PROJEKT
FACHBEREICH
THEMA
DIREKT
}
model Channel {
id String @id @default(cuid())
projectId String?
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
name String
beschreibung String?
kind ChannelKind @default(THEMA)
privat Boolean @default(false)
createdAt DateTime @default(now())
mitglieder ChannelMember[]
nachrichten Message[]
@@index([projectId])
}
model ChannelMember {
id String @id @default(cuid())
channelId String
channel Channel @relation(fields: [channelId], references: [id], onDelete: Cascade)
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
gelesenBis DateTime?
stumm Boolean @default(false)
@@unique([channelId, userId])
@@index([userId])
}
model Message {
id String @id @default(cuid())
channelId String
channel Channel @relation(fields: [channelId], references: [id], onDelete: Cascade)
authorId String
author User @relation(fields: [authorId], references: [id])
text String
parentId String?
parent Message? @relation("Thread", fields: [parentId], references: [id])
antworten Message[] @relation("Thread")
// Verknüpfung mit einem Fachobjekt: DOKUMENT | VORGANG | RECHNUNG | BAUTAGEBUCH
refTyp String?
refId String?
mentions String[] @default([]) // User-IDs
anhaenge MessageAttachment[]
bearbeitetAm DateTime?
geloeschtAm DateTime?
createdAt DateTime @default(now())
@@index([channelId, createdAt])
}
model MessageAttachment {
id String @id @default(cuid())
messageId String
message Message @relation(fields: [messageId], references: [id], onDelete: Cascade)
dateiname String
pfad String
mimeType String
groesse Int
}
enum NotificationType {
ERWAEHNUNG
NACHRICHT
ZUWEISUNG
FRIST
FREIGABE_ANGEFORDERT
DOKUMENT_NEU
RECHNUNG_BEFUND
MANGEL
SYSTEM
}
model Notification {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
typ NotificationType
titel String
text String?
link String?
projectId String?
gelesenAm DateTime?
gemailtAm DateTime?
createdAt DateTime @default(now())
@@index([userId, gelesenAm])
}
// ═══════════════════════════════════════════════════════════════════
// 10 Aufgaben und Protokollierung
// ═══════════════════════════════════════════════════════════════════
enum TaskStatus {
OFFEN
IN_ARBEIT
ERLEDIGT
VERWORFEN
}
model Task {
id String @id @default(cuid())
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
titel String
beschreibung String?
status TaskStatus @default(OFFEN)
faelligAm DateTime?
assigneeId String?
assignee User? @relation("TaskAssignee", fields: [assigneeId], references: [id])
refTyp String?
refId String?
createdAt DateTime @default(now())
erledigtAm DateTime?
@@index([projectId, status])
}
model AuditLog {
id String @id @default(cuid())
userId String?
user User? @relation(fields: [userId], references: [id])
aktion String // CREATE | UPDATE | DELETE | LOGIN | DOWNLOAD | FREIGABE ...
entitaet String
entitaetId String?
projectId String?
details Json?
ip String?
createdAt DateTime @default(now())
@@index([entitaet, entitaetId])
@@index([projectId, createdAt])
}