Erstimport. Fachliche Grundlage ist die EIU-Ablagestruktur des Auftraggebers (2-EIU-Ablagestruktur.xlsx): 1.106 Zeilen, aufbereitet zu 159 Ordnern und 944 Soll-Unterlagen mit Fachbereichsrelevanz, Aufbewahrungsfrist, Medium und Übergabezeitpunkt. Module - Dokumentationsstand: Soll-Ist je Projekt, Fristen aus dem Übergabezeitpunkt und den Projektterminen, Ampel, CSV-Rückexport im Format der Ursprungsdatei - Dokumentenmanagement mit Versionen, Freigabelauf, SHA-256-Prüfsummen - Bautagebuch nach Ril 809.0301, nach Abschluss gesperrt - Meldungen und Mängel (Behinderung, Bedenken, Mangel, AvL, Unfall) - Verträge, Nachträge, Aufmaße, Stundenlohnzettel mit zeilenweiser Anerkennung - Rechnungsprüfung: automatischer Abgleich gegen Vertrag, anerkanntes Aufmaß, bestätigte Stundenzettel und Vorrechnungen; getrennte Bescheinigung sachlich richtig / rechnerisch richtig / Zahlungsfreigabe - Kommunikation mit Projektkanälen, Erwähnungen und E-Mail-Benachrichtigung - Revisionssicheres Protokoll aller Vorgänge Rechte Rollen nach dem Vorgabeprozess Bauüberwachung (PL, BHV, BÜB, FBÜ, kaufmännische Steuerung, Planung, Auftragnehmer, Prüfer, Betreiber), Zuordnung je Projekt. Auftragnehmer sehen ausschließlich Daten der eigenen Firma; diese Schranke hängt allein an der Rolle. Welche Bereiche ihnen überhaupt angezeigt werden, entscheidet der Bauherr je Projekt über Schalter in den Einstellungen. Betrieb Docker Compose mit Anwendung, PostgreSQL und Mailpit. Keine Named Volumes – alle persistenten Daten liegen als Bind-Mount unter data/ im Compose- Verzeichnis, damit ein Backup ein Kopiervorgang bleibt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
657 lines
34 KiB
TypeScript
657 lines
34 KiB
TypeScript
/**
|
||
* Seed: EIU-Ablagestruktur aus der Excel des Auftraggebers + Demo-Projekt.
|
||
*
|
||
* Der Katalog (FilingNode / DocumentRequirement) wird bei jedem Start
|
||
* idempotent aktualisiert. Demo-Daten werden nur angelegt, wenn die
|
||
* Datenbank noch leer ist.
|
||
*/
|
||
import { PrismaClient, type Akte, type Medium, type Uebergabezeitpunkt } from "@prisma/client";
|
||
import bcrypt from "bcryptjs";
|
||
import { readFileSync } from "fs";
|
||
import path from "path";
|
||
import { slotsErzeugen } from "../src/lib/projekte";
|
||
|
||
const prisma = new PrismaClient();
|
||
|
||
type RohKnoten = { code: string; parent: string | null; level: number; title: string; ril: string; akte: string };
|
||
type RohItem = {
|
||
seq: number; folder: string; akte: string; title: string; bullet: boolean; ril: string;
|
||
netz: boolean; sus: boolean; fbNetz: string[]; fbSuS: string[];
|
||
aufbNutzungsdauer: boolean; aufbJahre: string; medien: string[]; uebergabe: string[]; bemerkung: string;
|
||
};
|
||
|
||
const tage = (d: number) => new Date(Date.now() + d * 864e5);
|
||
|
||
async function ablagestrukturImportieren() {
|
||
const datei = path.join(process.cwd(), "prisma", "data", "ablagestruktur.json");
|
||
const roh = JSON.parse(readFileSync(datei, "utf-8")) as { nodes: RohKnoten[]; items: RohItem[] };
|
||
|
||
// Knoten nach Ebene, damit Elternknoten zuerst existieren
|
||
const sortiert = [...roh.nodes].sort((a, b) => a.level - b.level);
|
||
let i = 0;
|
||
for (const k of sortiert) {
|
||
await prisma.filingNode.upsert({
|
||
where: { code: k.code },
|
||
create: {
|
||
code: k.code,
|
||
parentCode: k.parent,
|
||
akte: k.akte as Akte,
|
||
level: k.level,
|
||
titel: k.title,
|
||
ril: k.ril || null,
|
||
sortIndex: i++,
|
||
},
|
||
update: { titel: k.title, ril: k.ril || null, parentCode: k.parent, level: k.level, sortIndex: i },
|
||
});
|
||
}
|
||
|
||
for (const it of roh.items) {
|
||
const fachbereiche = [...new Set([...it.fbNetz, ...it.fbSuS])];
|
||
await prisma.documentRequirement.upsert({
|
||
where: { ordnung: it.seq },
|
||
create: {
|
||
ordnung: it.seq,
|
||
nodeCode: it.folder,
|
||
titel: it.title,
|
||
ril: it.ril || null,
|
||
istUnterpunkt: it.bullet,
|
||
relevantNetz: it.netz,
|
||
relevantBahnhof: it.sus,
|
||
fachbereiche,
|
||
aufbewahrungNutzungsdauer: it.aufbNutzungsdauer,
|
||
aufbewahrungJahre: it.aufbJahre || null,
|
||
medien: it.medien as Medium[],
|
||
uebergabe: it.uebergabe as Uebergabezeitpunkt[],
|
||
bemerkung: it.bemerkung || null,
|
||
},
|
||
update: {
|
||
nodeCode: it.folder,
|
||
titel: it.title,
|
||
ril: it.ril || null,
|
||
relevantNetz: it.netz,
|
||
relevantBahnhof: it.sus,
|
||
fachbereiche,
|
||
medien: it.medien as Medium[],
|
||
uebergabe: it.uebergabe as Uebergabezeitpunkt[],
|
||
},
|
||
});
|
||
}
|
||
|
||
console.log(` Ablagestruktur: ${sortiert.length} Ordner, ${roh.items.length} Soll-Unterlagen.`);
|
||
}
|
||
|
||
async function demodatenAnlegen() {
|
||
const pw = await bcrypt.hash("baudoc2026", 11);
|
||
|
||
// ── Firmen ────────────────────────────────────────────────────────
|
||
const eiu = await prisma.company.create({
|
||
data: { name: "DB InfraGO AG – Regionalbereich Nord", shortName: "DB InfraGO", type: "EIU", city: "Hannover" },
|
||
});
|
||
const anGleisbau = await prisma.company.create({
|
||
data: { name: "Nordbahn Gleisbau GmbH", shortName: "Nordbahn", type: "AUFTRAGNEHMER", city: "Lüneburg", kreditorNr: "K-100482", ustId: "DE812345678" },
|
||
});
|
||
const anElt = await prisma.company.create({
|
||
data: { name: "Elektro Bahntechnik Weser GmbH", shortName: "EBW", type: "AUFTRAGNEHMER", city: "Bremen", kreditorNr: "K-100731" },
|
||
});
|
||
const planer = await prisma.company.create({
|
||
data: { name: "Ingenieurbüro Bahnplan Nord GmbH", shortName: "IB Bahnplan", type: "PLANUNGSBUERO", city: "Hamburg" },
|
||
});
|
||
const sicherung = await prisma.company.create({
|
||
data: { name: "RailSafe Sicherungsdienste GmbH", shortName: "RailSafe", type: "SICHERUNGSUNTERNEHMEN", city: "Celle" },
|
||
});
|
||
await prisma.company.create({
|
||
data: { name: "Eisenbahn-Bundesamt, Außenstelle Hannover", shortName: "EBA", type: "BEHOERDE", city: "Hannover" },
|
||
});
|
||
|
||
// ── Benutzer ──────────────────────────────────────────────────────
|
||
const mk = (email: string, name: string, companyId: string, position: string, isAdmin = false) =>
|
||
prisma.user.create({ data: { email, name, passwordHash: pw, companyId, position, isAdmin } });
|
||
|
||
const admin = await mk("admin@baudoc.local", "System Administration", eiu.id, "IT-Administration", true);
|
||
const pl = await mk("m.kruse@infrago.local", "Martina Kruse", eiu.id, "Projektleitung (PL)");
|
||
const bhv = await mk("t.albrecht@infrago.local", "Thorben Albrecht", eiu.id, "Bauherrenvertretung (BHV)");
|
||
const buew = await mk("j.hoffmann@infrago.local", "Jens Hoffmann", eiu.id, "Bauüberwacher Bahn – Oberbau/KIB");
|
||
const fbue = await mk("s.linnemann@infrago.local", "Sabine Linnemann", eiu.id, "Fachbauüberwacherin – Elektrotechnik/OLA");
|
||
const kfm = await mk("r.pauls@infrago.local", "Rieke Pauls", eiu.id, "Kaufmännische Projektsteuerung");
|
||
const plan = await mk("d.wegener@bahnplan.local", "Dominik Wegener", planer.id, "Projektingenieur Planung");
|
||
const anUser = await mk("bauleitung@nordbahn.local", "Kai Petersen", anGleisbau.id, "Bauleitung Nordbahn Gleisbau");
|
||
const anUser2 = await mk("technik@ebw.local", "Yasemin Öztürk", anElt.id, "Bauleitung EBW");
|
||
const sich = await mk("dispo@railsafe.local", "Marco Dennig", sicherung.id, "Disposition Sicherungsleistungen");
|
||
|
||
// ── Projekt ───────────────────────────────────────────────────────
|
||
const fachbereiche = [
|
||
"Oberbau", "Tiefbau", "Konstruktiver Ingenieurbau",
|
||
"Elektrotechnik 50 Hz", "LST", "Oberleitung 16,7 Hz",
|
||
];
|
||
|
||
const heute = new Date();
|
||
const projekt = await prisma.project.create({
|
||
data: {
|
||
nummer: "P-2026-0147",
|
||
name: "Streckenertüchtigung Lüneburg – Uelzen, Abschnitt Bad Bevensen",
|
||
kurzname: "SE Bad Bevensen",
|
||
bauherr: "DB_INFRAGO_FAHRWEG",
|
||
status: "BAU",
|
||
regionalbereich: "Nord",
|
||
fachbereiche,
|
||
budgetPlan: 14_850_000,
|
||
beschreibung:
|
||
"Erneuerung Oberbau km 42,300 – 46,900 inkl. Weichen 1–6, Ersatzneubau EÜ Ilmenau (km 44,120), " +
|
||
"Anpassung Oberleitung 16,7 Hz, Kabeltiefbau und LST-Anpassung im Zuge der ESTW-Anbindung.",
|
||
startgespraech: tage(-420),
|
||
baubeginn: tage(-180),
|
||
bauende: tage(120),
|
||
inbetriebnahme: tage(95),
|
||
bauschlussmeldung: tage(150),
|
||
projektabschluss: tage(330),
|
||
abschnitte: {
|
||
create: [
|
||
{ streckenNr: "1720", streckenName: "Lehrte – Cuxhaven", kmVon: "42,300", kmBis: "46,900", betriebsstelle: "Bad Bevensen (ABBV)" },
|
||
{ streckenNr: "1720", streckenName: "Lehrte – Cuxhaven", kmVon: "44,100", kmBis: "44,150", betriebsstelle: "EÜ Ilmenau" },
|
||
],
|
||
},
|
||
bauwerke: {
|
||
create: [
|
||
{ bauwerksNr: "1720-044-120", bezeichnung: "EÜ Ilmenau", typ: "Eisenbahnüberführung", fachbereich: "Konstruktiver Ingenieurbau", kmLage: "44,120", ueberwachungspflichtig: true },
|
||
{ bauwerksNr: "1720-045-880", bezeichnung: "Stützbauwerk Nordrampe", typ: "Stützbauwerk", fachbereich: "Konstruktiver Ingenieurbau", kmLage: "45,880", ueberwachungspflichtig: true },
|
||
{ bezeichnung: "Weichen W1 – W6 Bad Bevensen", typ: "Gleisanlage", fachbereich: "Oberbau", kmLage: "43,100 – 43,650" },
|
||
{ bezeichnung: "Oberleitungsanlage Abschnitt Nord", typ: "Oberleitung", fachbereich: "Oberleitung 16,7 Hz", kmLage: "42,300 – 46,900" },
|
||
],
|
||
},
|
||
milestones: {
|
||
create: [
|
||
{ typ: "STARTGESPRAECH", titel: "Startgespräch Bauüberwachung", geplant: tage(-420), ist: tage(-418) },
|
||
{ typ: "BAUBEGINN", titel: "Baubeginn / Bauanlaufberatung", geplant: tage(-180), ist: tage(-180) },
|
||
{ typ: "ZWISCHEN_IBN", titel: "Zwischeninbetriebnahme Gleis 1", geplant: tage(-30), ist: tage(-28) },
|
||
{ typ: "INBETRIEBNAHME", titel: "Inbetriebnahme Gesamtmaßnahme", geplant: tage(95) },
|
||
{ typ: "BAUENDE", titel: "Bauende", geplant: tage(120) },
|
||
{ typ: "BAUSCHLUSSMELDUNG", titel: "Bauschlussmeldung", geplant: tage(150) },
|
||
{ typ: "PROJEKTABSCHLUSS", titel: "Projektabschluss (PA)", geplant: tage(330) },
|
||
],
|
||
},
|
||
members: {
|
||
create: [
|
||
{ userId: admin.id, role: "ADMIN" },
|
||
{ userId: pl.id, role: "PROJEKTLEITER" },
|
||
{ userId: bhv.id, role: "BAUHERRENVERTRETER" },
|
||
{ userId: buew.id, role: "BAUUEBERWACHER" },
|
||
{ userId: fbue.id, role: "FACHBAUUEBERWACHER", fachbereiche: ["Elektrotechnik 50 Hz", "Oberleitung 16,7 Hz", "LST"] },
|
||
{ userId: kfm.id, role: "KAUFMANN" },
|
||
{ userId: plan.id, role: "PLANER" },
|
||
{ userId: anUser.id, role: "AUFTRAGNEHMER" },
|
||
{ userId: anUser2.id, role: "AUFTRAGNEHMER" },
|
||
{ userId: sich.id, role: "AUFTRAGNEHMER" },
|
||
],
|
||
},
|
||
},
|
||
});
|
||
|
||
// ── Soll-Dokumentenkatalog für das Projekt erzeugen ────────────────
|
||
const { neu } = await slotsErzeugen(projekt.id);
|
||
console.log(` Soll-Dokumentenkatalog: ${neu} Positionen für ${projekt.nummer}.`);
|
||
|
||
// Realistischer Erfüllungsstand: Planungsakte weitgehend erledigt,
|
||
// Bauakte teilweise, Betreiberakte noch offen.
|
||
const slots = await prisma.projectDocumentSlot.findMany({
|
||
where: { projectId: projekt.id },
|
||
include: { requirement: { select: { nodeCode: true } } },
|
||
orderBy: { requirement: { ordnung: "asc" } },
|
||
});
|
||
let idx = 0;
|
||
for (const s of slots) {
|
||
const akte = s.requirement.nodeCode[0];
|
||
const wurf = (idx * 37) % 100;
|
||
idx++;
|
||
let status: "OFFEN" | "IN_ARBEIT" | "EINGEREICHT" | "IN_PRUEFUNG" | "FREIGEGEBEN" | "ENTFAELLT" = "OFFEN";
|
||
if (akte === "P") status = wurf < 78 ? "FREIGEGEBEN" : wurf < 88 ? "IN_PRUEFUNG" : wurf < 94 ? "ENTFAELLT" : "OFFEN";
|
||
else if (akte === "K") status = wurf < 46 ? "FREIGEGEBEN" : wurf < 60 ? "EINGEREICHT" : wurf < 70 ? "IN_ARBEIT" : "OFFEN";
|
||
else status = wurf < 34 ? "FREIGEGEBEN" : wurf < 46 ? "IN_PRUEFUNG" : wurf < 58 ? "IN_ARBEIT" : "OFFEN";
|
||
|
||
if (status !== "OFFEN") {
|
||
await prisma.projectDocumentSlot.update({
|
||
where: { id: s.id },
|
||
data: {
|
||
status,
|
||
eingangAm: status === "FREIGEGEBEN" || status === "EINGEREICHT" ? tage(-((wurf % 120) + 5)) : null,
|
||
responsibleId: akte === "K" ? kfm.id : akte === "P" ? plan.id : buew.id,
|
||
entfalltGrund: status === "ENTFAELLT" ? "Im Projektumfang nicht einschlägig (Vermerk PL)." : null,
|
||
},
|
||
});
|
||
}
|
||
}
|
||
|
||
// ── Vertrag Oberbau/KIB mit LV ────────────────────────────────────
|
||
const vertrag = await prisma.contract.create({
|
||
data: {
|
||
projectId: projekt.id,
|
||
companyId: anGleisbau.id,
|
||
nummer: "BV-2026-0147-01",
|
||
bezeichnung: "Oberbau- und Ingenieurbauleistungen Bad Bevensen",
|
||
typ: "BAUVERTRAG",
|
||
status: "IN_ABWICKLUNG",
|
||
vergabeart: "Offenes Verfahren (VOB/A EU)",
|
||
auftragssumme: 7_420_000,
|
||
beauftragtAm: tage(-210),
|
||
ausfuehrungVon: tage(-180),
|
||
ausfuehrungBis: tage(110),
|
||
gewaehrleistungJahre: 4,
|
||
sicherheitseinbehaltProzent: 5,
|
||
skontoProzent: 2,
|
||
skontoTage: 14,
|
||
zahlungszielTage: 30,
|
||
vertragsverantwortlich: "Rieke Pauls",
|
||
positionen: {
|
||
create: [
|
||
{ oz: "01.01.0010", kurztext: "Gleis ausbauen, Schotteroberbau", einheit: "m", menge: 4600, einheitspreis: 38.5 },
|
||
{ oz: "01.01.0020", kurztext: "Planumsschutzschicht herstellen, d = 30 cm", einheit: "m²", menge: 21800, einheitspreis: 24.9 },
|
||
{ oz: "01.02.0010", kurztext: "Gleis neu bauen, S 54, Betonschwelle B 70", einheit: "m", menge: 4600, einheitspreis: 412.0 },
|
||
{ oz: "01.02.0040", kurztext: "Weiche EW 60-500-1:12 einbauen", einheit: "St", menge: 6, einheitspreis: 148500.0 },
|
||
{ oz: "02.01.0010", kurztext: "EÜ Ilmenau – Überbau ausbauen", einheit: "psch", menge: 1, einheitspreis: 96000.0 },
|
||
{ oz: "02.01.0030", kurztext: "EÜ Ilmenau – Überbau einheben und lagern", einheit: "psch", menge: 1, einheitspreis: 384000.0 },
|
||
{ oz: "03.01.0010", kurztext: "Kabeltiefbau, Kabeltrog beidseitig", einheit: "m", menge: 4600, einheitspreis: 62.4 },
|
||
{ oz: "09.01.0010", kurztext: "Stundenlohnarbeiten Facharbeiter", einheit: "h", menge: 800, einheitspreis: 58.5, istStundenlohn: true },
|
||
{ oz: "09.01.0020", kurztext: "Stundenlohnarbeiten Polier", einheit: "h", menge: 200, einheitspreis: 74.0, istStundenlohn: true },
|
||
{ oz: "09.02.0010", kurztext: "Vorhaltung Zweiwegebagger im Stundenlohn", einheit: "h", menge: 300, einheitspreis: 132.0, istStundenlohn: true },
|
||
],
|
||
},
|
||
},
|
||
include: { positionen: true },
|
||
});
|
||
|
||
const p = (oz: string) => vertrag.positionen.find((x) => x.oz === oz)!;
|
||
|
||
await prisma.contract.create({
|
||
data: {
|
||
projectId: projekt.id,
|
||
companyId: anElt.id,
|
||
nummer: "BV-2026-0147-02",
|
||
bezeichnung: "Oberleitungs- und 50-Hz-Anlagen",
|
||
typ: "BAUVERTRAG",
|
||
status: "IN_ABWICKLUNG",
|
||
auftragssumme: 3_180_000,
|
||
beauftragtAm: tage(-195),
|
||
ausfuehrungVon: tage(-150),
|
||
ausfuehrungBis: tage(100),
|
||
positionen: {
|
||
create: [
|
||
{ oz: "05.01.0010", kurztext: "Oberleitungsmast setzen inkl. Fundament", einheit: "St", menge: 118, einheitspreis: 8940.0 },
|
||
{ oz: "05.02.0010", kurztext: "Kettenwerk Re200 montieren und regulieren", einheit: "m", menge: 4600, einheitspreis: 148.0 },
|
||
],
|
||
},
|
||
},
|
||
});
|
||
|
||
const nachtrag = await prisma.changeOrder.create({
|
||
data: {
|
||
contractId: vertrag.id,
|
||
nummer: "N-01",
|
||
titel: "Mehraufwand Bodenaustausch km 45,100 – 45,400 (kontaminiertes Material)",
|
||
begruendung:
|
||
"Bei Aushub wurde nicht ausgeschriebenes, mit Altöl belastetes Material angetroffen (Zuordnung Z 2). " +
|
||
"Entsorgung und Ersatz durch Frostschutzschicht erforderlich.",
|
||
rechtsgrundlage: "§ 2 Abs. 6 VOB/B",
|
||
angebotssumme: 214_800,
|
||
anerkannteSumme: 186_400,
|
||
status: "BEAUFTRAGT",
|
||
angemeldetAm: tage(-96),
|
||
geprueftAm: tage(-84),
|
||
beauftragtAm: tage(-72),
|
||
stellungnahme:
|
||
"Dem Grunde nach anerkannt. Mengenvordersätze auf Basis Abrechnungszeichnung geprüft. " +
|
||
"Einheitspreis Entsorgung auf marktübliches Niveau gekürzt (Nachtragsbewertungsmatrix).",
|
||
},
|
||
});
|
||
|
||
await prisma.changeOrder.create({
|
||
data: {
|
||
contractId: vertrag.id,
|
||
nummer: "N-02",
|
||
titel: "Zusätzliche Sperrpause wegen verschobener Betra KW 18",
|
||
begruendung: "Verschiebung der Betra durch Kapazitätsmanagement, Mehrkosten Vorhaltung und Sicherung.",
|
||
rechtsgrundlage: "§ 6 Abs. 6 VOB/B",
|
||
angebotssumme: 88_400,
|
||
status: "IN_PRUEFUNG_BUEW",
|
||
angemeldetAm: tage(-21),
|
||
},
|
||
});
|
||
|
||
// ── Aufmaße ───────────────────────────────────────────────────────
|
||
const aufmass1 = await prisma.measurement.create({
|
||
data: {
|
||
projectId: projekt.id, contractId: vertrag.id, nummer: "AM-2026-014",
|
||
art: "ABRECHNUNGSZEICHNUNG", datum: tage(-38), ortBauteil: "km 42,300 – 44,100",
|
||
status: "ANERKANNT", createdById: buew.id, geprueftAm: tage(-34),
|
||
bemerkung: "Mengenermittlung nach REB 23.003, gemeinsam mit AN festgestellt.",
|
||
zeilen: {
|
||
create: [
|
||
{ contractItemId: p("01.01.0010").id, beschreibung: "Gleis 1 Ausbau", formel: "1800,00", menge: 1800 },
|
||
{ contractItemId: p("01.02.0010").id, beschreibung: "Gleis 1 Neubau", formel: "1800,00", menge: 1800 },
|
||
{ contractItemId: p("01.01.0020").id, beschreibung: "PSS Gleis 1", formel: "1800,00 * 4,70", menge: 8460 },
|
||
{ contractItemId: p("01.02.0040").id, beschreibung: "W1, W2", formel: "2", menge: 2 },
|
||
],
|
||
},
|
||
},
|
||
});
|
||
|
||
await prisma.measurement.create({
|
||
data: {
|
||
projectId: projekt.id, contractId: vertrag.id, nummer: "AM-2026-021",
|
||
art: "OERTLICHES_AUFMASS", datum: tage(-12), ortBauteil: "EÜ Ilmenau km 44,120",
|
||
status: "TEILWEISE_ANERKANNT", createdById: buew.id, geprueftAm: tage(-9),
|
||
bemerkung: "Überbau Ausbau anerkannt. Einheben noch nicht erbracht – Position zurückgestellt.",
|
||
zeilen: {
|
||
create: [
|
||
{ contractItemId: p("02.01.0010").id, beschreibung: "Überbau Ausbau", formel: "1", menge: 1, mengeAnerkannt: 1 },
|
||
{ contractItemId: p("02.01.0030").id, beschreibung: "Überbau einheben", formel: "1", menge: 1, mengeAnerkannt: 0 },
|
||
{ contractItemId: p("03.01.0010").id, beschreibung: "Kabeltrog Nordseite", formel: "1800,00", menge: 1800, mengeAnerkannt: 1650 },
|
||
],
|
||
},
|
||
},
|
||
});
|
||
|
||
// ── Stundenlohnzettel ─────────────────────────────────────────────
|
||
// Zettel 1: vollständig anerkannt
|
||
const zettel1 = await prisma.timeSheet.create({
|
||
data: {
|
||
projectId: projekt.id, contractId: vertrag.id, nummer: "SLZ-2026-088",
|
||
datum: tage(-26), leistung: "Freilegen und Sichern eines nicht dokumentierten Kabels km 43,480",
|
||
ortKm: "43,480", bezugPosition: "09.01.0010", status: "ANERKANNT",
|
||
filedById: anUser.id, checkedById: buew.id, geprueftAm: tage(-24),
|
||
pruefkommentar: "Leistung war angeordnet (AvL vom 12.), Stunden vor Ort bestätigt.",
|
||
zeilen: {
|
||
create: [
|
||
{ name: "M. Sobczak", art: "FACHARBEITER", stundenGemeldet: 8, stundenAnerkannt: 8, stundensatz: 58.5 },
|
||
{ name: "T. Ehlert", art: "FACHARBEITER", stundenGemeldet: 8, stundenAnerkannt: 8, stundensatz: 58.5 },
|
||
{ name: "R. Danneberg", art: "POLIER", stundenGemeldet: 4, stundenAnerkannt: 4, stundensatz: 74.0 },
|
||
],
|
||
},
|
||
},
|
||
});
|
||
|
||
// Zettel 2: gekürzt (An- und Abfahrt nicht anerkannt) – klassischer Streitfall
|
||
const zettel2 = await prisma.timeSheet.create({
|
||
data: {
|
||
projectId: projekt.id, contractId: vertrag.id, nummer: "SLZ-2026-091",
|
||
datum: tage(-19), leistung: "Handschachtung im Bereich Bestandskabel W4, Sperrpause 22:00 – 05:00",
|
||
ortKm: "43,610", bezugPosition: "09.01.0010", status: "TEILWEISE_ANERKANNT",
|
||
filedById: anUser.id, checkedById: buew.id, geprueftAm: tage(-17),
|
||
pruefkommentar:
|
||
"An- und Abfahrtszeiten sind mit den Einheitspreisen abgegolten und wurden abgesetzt. " +
|
||
"Nachtzuschlag 25 % für die Sperrpause anerkannt.",
|
||
zeilen: {
|
||
create: [
|
||
{ name: "M. Sobczak", art: "FACHARBEITER", stundenGemeldet: 9, stundenAnerkannt: 7, stundensatz: 58.5, zuschlagProzent: 25, zuschlagGrund: "Nachtarbeit in Sperrpause" },
|
||
{ name: "T. Ehlert", art: "FACHARBEITER", stundenGemeldet: 9, stundenAnerkannt: 7, stundensatz: 58.5, zuschlagProzent: 25, zuschlagGrund: "Nachtarbeit in Sperrpause" },
|
||
{ name: "Zweiwegebagger ZW-14", art: "GERAET", stundenGemeldet: 7, stundenAnerkannt: 7, stundensatz: 132.0 },
|
||
],
|
||
},
|
||
},
|
||
});
|
||
|
||
// Zettel 3: noch nicht geprüft – darf nicht abgerechnet werden
|
||
const zettel3 = await prisma.timeSheet.create({
|
||
data: {
|
||
projectId: projekt.id, contractId: vertrag.id, nummer: "SLZ-2026-097",
|
||
datum: tage(-6), leistung: "Nacharbeiten Entwässerungsrinne km 45,900",
|
||
ortKm: "45,900", status: "EINGEREICHT", filedById: anUser.id,
|
||
zeilen: {
|
||
create: [
|
||
{ name: "K. Wiese", art: "FACHARBEITER", stundenGemeldet: 6, stundensatz: 58.5 },
|
||
{ name: "L. Brandt", art: "HELFER", stundenGemeldet: 6, stundensatz: 46.0 },
|
||
],
|
||
},
|
||
},
|
||
});
|
||
|
||
// ── Rechnung mit bewusst eingebauten Prüfbefunden ─────────────────
|
||
const rechnung = await prisma.invoice.create({
|
||
data: {
|
||
projectId: projekt.id, contractId: vertrag.id, companyId: anGleisbau.id,
|
||
rechnungsNr: "2026-AR-0431", typ: "ABSCHLAGSRECHNUNG", status: "EINGEGANGEN",
|
||
rechnungsdatum: tage(-4), eingangsdatum: tage(-3),
|
||
leistungVon: tage(-60), leistungBis: tage(-5),
|
||
nettoBetrag: 2_166_283.0, ustSatz: 19, ustBetrag: 411_593.77, bruttoBetrag: 2_577_876.77,
|
||
zahlbarBis: tage(27),
|
||
zeilen: {
|
||
create: [
|
||
// korrekt
|
||
{ position: 1, art: "LV_POSITION", oz: "01.01.0010", bezeichnung: "Gleis ausbauen, Schotteroberbau",
|
||
einheit: "m", menge: 1800, einheitspreis: 38.5, betrag: 69_300, contractItemId: p("01.01.0010").id },
|
||
// korrekt
|
||
{ position: 2, art: "LV_POSITION", oz: "01.02.0010", bezeichnung: "Gleis neu bauen S 54",
|
||
einheit: "m", menge: 1800, einheitspreis: 412.0, betrag: 741_600, contractItemId: p("01.02.0010").id },
|
||
// Befund: Einheitspreis über Vertrag
|
||
{ position: 3, art: "LV_POSITION", oz: "01.01.0020", bezeichnung: "Planumsschutzschicht d = 30 cm",
|
||
einheit: "m²", menge: 8460, einheitspreis: 26.4, betrag: 223_344, contractItemId: p("01.01.0020").id },
|
||
// Befund: Menge über anerkanntem Aufmaß (3 statt 2 Weichen)
|
||
{ position: 4, art: "LV_POSITION", oz: "01.02.0040", bezeichnung: "Weiche EW 60-500-1:12 einbauen",
|
||
einheit: "St", menge: 3, einheitspreis: 148_500, betrag: 445_500, contractItemId: p("01.02.0040").id },
|
||
// Befund: Position ohne anerkanntes Aufmaß (Überbau noch nicht eingehoben)
|
||
{ position: 5, art: "LV_POSITION", oz: "02.01.0030", bezeichnung: "EÜ Ilmenau – Überbau einheben",
|
||
einheit: "psch", menge: 1, einheitspreis: 384_000, betrag: 384_000, contractItemId: p("02.01.0030").id },
|
||
// korrekt
|
||
{ position: 6, art: "STUNDENLOHN", oz: "SLZ-2026-088", bezeichnung: "Stundenlohn SLZ-2026-088",
|
||
einheit: "h", menge: 20, einheitspreis: 61.6, betrag: 1_232, timeSheetId: zettel1.id },
|
||
// Befund: gemeldete statt anerkannte Stunden
|
||
{ position: 7, art: "STUNDENLOHN", oz: "SLZ-2026-091", bezeichnung: "Stundenlohn SLZ-2026-091",
|
||
einheit: "h", menge: 25, einheitspreis: 78.4, betrag: 1_960, timeSheetId: zettel2.id },
|
||
// Befund: Zettel noch nicht bestätigt
|
||
{ position: 8, art: "STUNDENLOHN", oz: "SLZ-2026-097", bezeichnung: "Stundenlohn SLZ-2026-097",
|
||
einheit: "h", menge: 12, einheitspreis: 52.25, betrag: 627, timeSheetId: zettel3.id },
|
||
// Befund: Nachtragsposition ohne beauftragten Nachtrag ist hier korrekt zugeordnet
|
||
{ position: 9, art: "NACHTRAG", bezeichnung: "N-01 Bodenaustausch km 45,100 – 45,400",
|
||
einheit: "psch", menge: 1, einheitspreis: 186_400, betrag: 186_400, changeOrderId: nachtrag.id },
|
||
// Befund: Kabeltrog über anerkanntem Aufmaß (1800 statt 1650 m)
|
||
{ position: 10, art: "LV_POSITION", oz: "03.01.0010", bezeichnung: "Kabeltiefbau, Kabeltrog",
|
||
einheit: "m", menge: 1800, einheitspreis: 62.4, betrag: 112_320, contractItemId: p("03.01.0010").id },
|
||
],
|
||
},
|
||
},
|
||
});
|
||
|
||
// ── Bautagebuch ───────────────────────────────────────────────────
|
||
const wetter = [
|
||
["heiter", "sonnig", "bewölkt", 8, 19],
|
||
["Nebel", "bedeckt", "Regen", 5, 12],
|
||
["bedeckt", "Regen", "Regen", 6, 11],
|
||
["klar", "heiter", "klar", 3, 16],
|
||
["bewölkt", "bewölkt", "heiter", 7, 17],
|
||
];
|
||
for (let i = 0; i < 5; i++) {
|
||
const w = wetter[i];
|
||
const eintrag = await prisma.siteDiaryEntry.create({
|
||
data: {
|
||
projectId: projekt.id, datum: tage(-(i + 1)), lfdNr: 178 - i, authorId: buew.id,
|
||
wetterFrueh: String(w[0]), wetterMittag: String(w[1]), wetterAbend: String(w[2]),
|
||
tempMin: Number(w[3]), tempMax: Number(w[4]),
|
||
arbeitszeitVon: "06:30", arbeitszeitBis: "16:00",
|
||
betra: i < 2 ? "Betra 2026/0412, Gleis 2 gesperrt 22:00 – 05:00" : "Betra 2026/0398",
|
||
sperrpause: i < 2 ? "22:00 – 05:00, Gleis 2" : "keine",
|
||
leistungen:
|
||
i === 0
|
||
? "Gleisneubau km 43,900 – 44,050 (150 m), Schotterverteilung und erstes Stopfen. Kabeltrogmontage Nordseite fortgesetzt.\nEÜ Ilmenau: Vorbereitung Lagerversetzung, Traggerüst geprüft."
|
||
: "Gleisneubau fortgesetzt, PSS eingebaut und verdichtet. Weiche W4 vormontiert.",
|
||
besondereVorkommnisse:
|
||
i === 0
|
||
? "Bei Aushub km 43,480 nicht dokumentiertes Kabel angetroffen. Arbeiten örtlich unterbrochen, DB Kommunikationstechnik verständigt (14:20 Uhr, Hr. Lembke)."
|
||
: i === 2
|
||
? "Starkregen ab 11:30, Verdichtungsarbeiten eingestellt."
|
||
: null,
|
||
einweisungen: i === 4 ? "Ersteinweisung 6 Mitarbeiter Nordbahn Gleisbau, Baustellenbelehrung nach Ril 132.0108V11." : null,
|
||
sicherungsmassnahmen: "Sicherung durch automatisches Warnsystem (AWS), Sicherungsplan Rev. 3, RailSafe.",
|
||
status: i === 0 ? "ENTWURF" : "ABGESCHLOSSEN",
|
||
signedById: i === 0 ? null : buew.id,
|
||
signedAt: i === 0 ? null : tage(-(i + 1)),
|
||
gesperrtAm: i > 1 ? tage(-(i)) : null,
|
||
personal: {
|
||
create: [
|
||
{ companyId: anGleisbau.id, gewerk: "Oberbau", taetigkeit: "Gleisbaukolonne", anzahl: 12, stunden: 108 },
|
||
{ companyId: anGleisbau.id, gewerk: "Konstruktiver Ingenieurbau", taetigkeit: "EÜ Ilmenau", anzahl: 5, stunden: 45 },
|
||
{ companyId: sicherung.id, gewerk: "Sicherung", taetigkeit: "Sicherungsposten / AWS", anzahl: 3, stunden: 27 },
|
||
],
|
||
},
|
||
geraete: {
|
||
create: [
|
||
{ bezeichnung: "Zweiwegebagger ZW-14", anzahl: 1, stunden: 9 },
|
||
{ bezeichnung: "Stopfmaschine 09-32", anzahl: 1, stunden: 6 },
|
||
{ bezeichnung: "Autokran 200 t", anzahl: i === 0 ? 1 : 0, stunden: i === 0 ? 8 : 0 },
|
||
],
|
||
},
|
||
},
|
||
});
|
||
if (i === 0) {
|
||
await prisma.timeSheet.update({ where: { id: zettel1.id }, data: { diaryEntryId: eintrag.id } });
|
||
}
|
||
}
|
||
|
||
// ── Meldungen / Vorgänge ──────────────────────────────────────────
|
||
await prisma.issue.createMany({
|
||
data: [
|
||
{
|
||
projectId: projekt.id, nr: 1, typ: "BEHINDERUNGSANZEIGE",
|
||
titel: "Nicht dokumentiertes Kabel km 43,480 – Behinderung Gleisbau",
|
||
beschreibung:
|
||
"Beim Aushub wurde ein im Bestandsplan nicht enthaltenes Kabel angetroffen. Der Gleisbau in diesem Bereich ruht " +
|
||
"bis zur Freigabe durch DB Kommunikationstechnik. Bauzeitverzug wird angezeigt.",
|
||
status: "IN_BEARBEITUNG", schwere: "HOCH", ortKm: "43,480", fachbereich: "Oberbau",
|
||
raisedById: anUser.id, assigneeId: buew.id, gegenFirmaId: null, contractId: vertrag.id,
|
||
gemeldetAm: tage(-26), fristAm: tage(2), kostenrelevant: true, terminrelevant: true, geschaetzteKosten: 42_000,
|
||
},
|
||
{
|
||
projectId: projekt.id, nr: 2, typ: "MANGELANZEIGE",
|
||
titel: "Schweißnaht SK 41 – Ultraschallprüfung ohne Nachweis",
|
||
beschreibung:
|
||
"Für die Schweißstelle SK 41 (km 43,215) liegt kein Prüfprotokoll nach Ril 824.9006 vor. " +
|
||
"Nachweis oder Nacharbeit ist bis zum Fristende beizubringen.",
|
||
status: "OFFEN", schwere: "SICHERHEITSRELEVANT", ortKm: "43,215", fachbereich: "Oberbau",
|
||
raisedById: buew.id, assigneeId: anUser.id, gegenFirmaId: anGleisbau.id, contractId: vertrag.id,
|
||
gemeldetAm: tage(-8), fristAm: tage(-1),
|
||
},
|
||
{
|
||
projectId: projekt.id, nr: 3, typ: "BEDENKENANZEIGE",
|
||
titel: "Bedenken gegen vorgesehene Verdichtungsleistung bei anhaltender Nässe",
|
||
beschreibung:
|
||
"Nach Starkregen ist die geforderte Verdichtung der PSS im Abschnitt km 45,2 – 45,6 nach unserer Einschätzung " +
|
||
"nicht erreichbar. Bedenken werden gemäß § 4 Abs. 3 VOB/B angemeldet.",
|
||
status: "ERLEDIGT", schwere: "MITTEL", ortKm: "45,200 – 45,600", fachbereich: "Tiefbau",
|
||
raisedById: anUser.id, assigneeId: buew.id, contractId: vertrag.id,
|
||
gemeldetAm: tage(-40), erledigtAm: tage(-31),
|
||
},
|
||
{
|
||
projectId: projekt.id, nr: 4, typ: "MEHRKOSTEN_AVL",
|
||
titel: "Anordnung von Leistungen: zusätzliche Kabelsuchschachtungen",
|
||
beschreibung: "Anordnung des BÜB vom 12., Ausführung im Stundenlohn nach SLZ-2026-088.",
|
||
status: "ERLEDIGT", schwere: "GERING", raisedById: buew.id, contractId: vertrag.id,
|
||
gemeldetAm: tage(-27), erledigtAm: tage(-24), kostenrelevant: true,
|
||
},
|
||
{
|
||
projectId: projekt.id, nr: 5, typ: "VERTRAGSABWEICHUNG",
|
||
titel: "Abweichende Schwellenlieferung – Zulassung prüfen",
|
||
beschreibung:
|
||
"Angeliefert wurden Betonschwellen einer nicht im Vertrag benannten Bauart. " +
|
||
"Verwendbarkeitsnachweis und Zulassung sind vor Einbau vorzulegen.",
|
||
status: "IN_BEARBEITUNG", schwere: "HOCH", fachbereich: "Oberbau",
|
||
raisedById: fbue.id, assigneeId: anUser.id, gegenFirmaId: anGleisbau.id, contractId: vertrag.id,
|
||
gemeldetAm: tage(-3), fristAm: tage(4),
|
||
},
|
||
],
|
||
});
|
||
|
||
await prisma.warranty.createMany({
|
||
data: [
|
||
{
|
||
projectId: projekt.id, contractId: vertrag.id, gegenstand: "Zwischeninbetriebnahme Gleis 1, km 42,300 – 44,100",
|
||
abnahmeAm: tage(-28), fristJahre: 4, fristEndeAm: tage(-28 + 4 * 365),
|
||
verantwortlich: "Jens Hoffmann (BÜB)", buergschaft: 371_000, status: "LAUFEND",
|
||
},
|
||
],
|
||
});
|
||
|
||
// ── Kommunikation ─────────────────────────────────────────────────
|
||
const kanalAllgemein = await prisma.channel.create({
|
||
data: {
|
||
projectId: projekt.id, name: "Baustelle allgemein", kind: "PROJEKT",
|
||
beschreibung: "Tagesabstimmung aller Projektbeteiligten.",
|
||
mitglieder: { create: [pl, bhv, buew, fbue, kfm, plan, anUser, anUser2, sich].map((u) => ({ userId: u.id })) },
|
||
},
|
||
});
|
||
const kanalAbrechnung = await prisma.channel.create({
|
||
data: {
|
||
projectId: projekt.id, name: "Abrechnung & Nachträge", kind: "THEMA", privat: true,
|
||
beschreibung: "Nur Bauherrenseite: Aufmaße, Stundenzettel, Rechnungsprüfung.",
|
||
mitglieder: { create: [pl, bhv, buew, kfm].map((u) => ({ userId: u.id })) },
|
||
},
|
||
});
|
||
await prisma.channel.create({
|
||
data: {
|
||
projectId: projekt.id, name: "Oberleitung / 50 Hz", kind: "FACHBEREICH",
|
||
beschreibung: "Fachliche Abstimmung Elektrotechnik.",
|
||
mitglieder: { create: [fbue, pl, anUser2, buew].map((u) => ({ userId: u.id })) },
|
||
},
|
||
});
|
||
|
||
await prisma.message.createMany({
|
||
data: [
|
||
{ channelId: kanalAllgemein.id, authorId: buew.id, createdAt: tage(-1.4),
|
||
text: "Betra 2026/0412 ist für heute Nacht bestätigt, Sperrung Gleis 2 von 22:00 bis 05:00. Einweisung um 21:30 am Container Nord." },
|
||
{ channelId: kanalAllgemein.id, authorId: anUser.id, createdAt: tage(-1.3),
|
||
text: "Verstanden. Wir sind mit 12 Mann und dem Zweiwegebagger vor Ort. @Hoffmann bitte kurz bestätigen, dass der Autokran bis 06:00 stehen bleiben darf." },
|
||
{ channelId: kanalAllgemein.id, authorId: buew.id, createdAt: tage(-1.25),
|
||
text: "Bestätigt, der Kran kann bis 06:00 stehen bleiben. Standsicherheitsnachweis liegt in der Ablage unter B.I.1.3." },
|
||
{ channelId: kanalAllgemein.id, authorId: fbue.id, createdAt: tage(-0.6),
|
||
text: "Hinweis für alle: ab Montag ist der Abschnitt km 45,2 – 45,6 unter Spannung. Arbeiten im Gefahrenbereich nur nach Schalt- und Erdungsprotokoll." },
|
||
{ channelId: kanalAbrechnung.id, authorId: kfm.id, createdAt: tage(-0.4),
|
||
text: "Die Abschlagsrechnung 2026-AR-0431 von Nordbahn ist eingegangen. Automatische Prüfung hat mehrere kritische Befunde ausgeworfen – @Hoffmann magst du sachlich draufschauen, bevor ich rechnerisch prüfe?" },
|
||
{ channelId: kanalAbrechnung.id, authorId: buew.id, createdAt: tage(-0.3),
|
||
text: "Schaue ich mir heute Nachmittag an. Position 5 (Überbau einheben) kann definitiv noch nicht abgerechnet werden, der Überbau liegt noch auf dem Lagerplatz." },
|
||
],
|
||
});
|
||
|
||
await prisma.task.createMany({
|
||
data: [
|
||
{ projectId: projekt.id, titel: "Prüfprotokoll Schweißstelle SK 41 nachfordern", assigneeId: buew.id, faelligAm: tage(1), status: "OFFEN" },
|
||
{ projectId: projekt.id, titel: "Nachtrag N-02 bewerten (Nachtragsbewertungsmatrix)", assigneeId: buew.id, faelligAm: tage(5), status: "IN_ARBEIT" },
|
||
{ projectId: projekt.id, titel: "Bestandsunterlagen Kabeltrasse in DB-GIS einarbeiten lassen", assigneeId: plan.id, faelligAm: tage(30), status: "OFFEN" },
|
||
{ projectId: projekt.id, titel: "Abschlagsrechnung 2026-AR-0431 rechnerisch prüfen", assigneeId: kfm.id, faelligAm: tage(6), status: "OFFEN" },
|
||
],
|
||
});
|
||
|
||
return { projekt, rechnung };
|
||
}
|
||
|
||
async function main() {
|
||
console.log("[seed] Ablagestruktur importieren ...");
|
||
await ablagestrukturImportieren();
|
||
|
||
const vorhanden = await prisma.project.count();
|
||
if (vorhanden > 0) {
|
||
console.log("[seed] Projekte vorhanden – Demo-Daten werden übersprungen.");
|
||
return;
|
||
}
|
||
|
||
console.log("[seed] Demo-Daten anlegen ...");
|
||
const { rechnung } = await demodatenAnlegen();
|
||
|
||
console.log("[seed] Rechnungsprüfung ausführen ...");
|
||
const { pruefeRechnung } = await import("../src/lib/rechnungspruefung");
|
||
const bericht = await pruefeRechnung(rechnung.id);
|
||
console.log(
|
||
` ${bericht.befunde} Befunde (${bericht.kritisch} kritisch), ` +
|
||
`Kürzung ${bericht.kuerzungNetto.toLocaleString("de-DE")} EUR.`,
|
||
);
|
||
|
||
console.log("\n[seed] Fertig. Zugänge (Kennwort jeweils: baudoc2026)");
|
||
console.log(" admin@baudoc.local Administration");
|
||
console.log(" m.kruse@infrago.local Projektleitung");
|
||
console.log(" j.hoffmann@infrago.local Bauüberwacher Bahn");
|
||
console.log(" r.pauls@infrago.local Kaufmännische Steuerung");
|
||
console.log(" bauleitung@nordbahn.local Auftragnehmer");
|
||
}
|
||
|
||
main()
|
||
.catch((e) => {
|
||
console.error(e);
|
||
process.exit(1);
|
||
})
|
||
.finally(() => prisma.$disconnect());
|