// 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 } 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") @@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) role Role // Fachbereiche, auf die sich diese Rolle beschränkt (leer = alle) fachbereiche String[] @default([]) createdAt DateTime @default(now()) @@unique([projectId, userId, role]) @@index([userId]) } // ═══════════════════════════════════════════════════════════════════ // 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]) }