// Backoffice / team lead records time for staff (field:correct_time, User 2026-09-15): // (1) back office records for a technician → approved directly, source manual, audited // (2) team lead of ANOTHER team → forbidden (user_not_in_team); own team member → approved // (3) technician cannot record for a colleague → forbidden // (4) other tenant's back office → not_found for the order // (5) back office role carries field:correct_time // // Lauf: npx tsx scripts/test-backoffice-time.ts (lokale Postgres-DB aus .env) import "dotenv/config"; import { randomUUID } from "node:crypto"; import { prisma } from "../src/server/db"; import { ROLE_DEFS } from "../src/server/rbac"; import { addManualTimeEntry } from "../src/server/services/field/time-entries"; import { berlinAt, createTenant, expectCode, ok, runSuite, section, type TenantFixture } from "./lib/e2e-fixture"; const SLUG_A = "zz-bo-time-a"; const SLUG_B = "zz-bo-time-b"; async function order(A: TenantFixture) { return prisma.workOrder.create({ data: { tenantId: A.tenantId, number: `BT-${randomUUID().slice(0, 6)}`, customerId: A.customerId, siteId: A.siteId, title: "Heizungswartung", status: "in_progress", assignedTeamId: A.teamId, }, }); } void runSuite("Backoffice erfasst Zeiten", [SLUG_A, SLUG_B], async () => { const A = await createTenant(SLUG_A); const B = await createTenant(SLUG_B); const wo = await order(A); section("Rolle"); ok(ROLE_DEFS.backoffice.permissions.includes("field:correct_time"), "(5) Backoffice-Rolle hat field:correct_time"); section("Backoffice"); const entry = await addManualTimeEntry(A.ctx.backoffice, { workOrderId: wo.id, forUserId: A.users.tech.id, type: "work", startedAt: berlinAt(-1, 8), endedAt: berlinAt(-1, 10), reason: "Stundenzettel nachgetragen", }); ok(entry.userId === A.users.tech.id, "(1) Eintrag gehört dem Monteur"); ok(entry.approvalStatus === "approved" && entry.source === "manual", "(1) direkt freigegeben, Quelle manuell"); const audited = await prisma.auditLog.count({ where: { tenantId: A.tenantId, entity: "time_entry", entityId: entry.id } }); ok(audited >= 1, "(1) Audit-Eintrag vorhanden"); section("Teamleiter / Monteur"); await expectCode( () => addManualTimeEntry(A.ctx.lead2, { workOrderId: wo.id, forUserId: A.users.tech.id, type: "work", startedAt: berlinAt(-1, 11), endedAt: berlinAt(-1, 12), reason: "fremdes Team" }), ["forbidden", "not_found"], "(2) Teamleiter eines anderen Teams → abgewiesen", ); const leadEntry = await addManualTimeEntry(A.ctx.lead, { workOrderId: wo.id, forUserId: A.users.tech2.id, type: "travel", startedAt: berlinAt(-1, 7), durationMinutes: 30, reason: "Anfahrt nachgetragen", }); ok(leadEntry.approvalStatus === "approved", "(2) Teamleiter für eigenes Teammitglied → freigegeben"); await expectCode( () => addManualTimeEntry(A.ctx.tech, { workOrderId: wo.id, forUserId: A.users.tech2.id, type: "work", startedAt: berlinAt(-2, 8), endedAt: berlinAt(-2, 9), reason: "für Kollegen" }), "forbidden", "(3) Monteur für Kollegen → forbidden", ); section("Mandantentrennung"); await expectCode( () => addManualTimeEntry(B.ctx.backoffice, { workOrderId: wo.id, forUserId: A.users.tech.id, type: "work", startedAt: berlinAt(-1, 13), endedAt: berlinAt(-1, 14), reason: "fremder Mandant" }), ["not_found", "forbidden"], "(4) Backoffice anderer Mandant → abgewiesen", ); });