Zeiterfassung: Backoffice erfasst Zeiten für Mitarbeiter
- Backoffice-Rolle erhält field:correct_time (User-Entscheidung): Zeiten für Monteure/Teamleiter direkt freigegeben anlegen und korrigieren - Eigene Zeiten erfordern weiterhin field:record_own_time (Backoffice erfasst keine eigenen Zeiten) - Auftragsdetail › Zeiten: Formular „Zeit für Mitarbeiter erfassen“ (Mitarbeiter, Art, Datum, von–bis oder Dauer, Begründung; Mandanten-Zeitzone) - Server Action recordTimeForUserAction (moduleGuard work_orders) - Test test-backoffice-time (Backoffice, fremdes Team, Monteur, Mandantentrennung) Demo-Daten lokal bereinigt: je Monteur nur noch eine aktive Uhr. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
// 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",
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user