L13 Planung: Plantafel-, Einplan-, Empfehlungs-, Live- und Verzugsdienste mit API und Meldungen

Services board/schedule (L2 assign+update in inTransaction, Versionskonflikt, Audit), recommend (Luftlinie + freie Kolonnenzeit), live (Kolonnen, ohne Geräte-Koordinaten), watch (Verzug ab 80 %, Überschreitung, gefährdete Folgeaufträge, früher fertig mit Vorschlägen), Kolonnenkapazität. Job planning-watch alle 5 min, Events planning.capacity_freed/overrun/followup_at_risk (In-App an Backoffice + Teamleiter, Dedupe im Audit-Log), L12-Adapter für source/approvalStatus/manual. API /api/v1/planning/{board,schedule,recommendations,live} + OpenAPI. Tests core, recommend, live, watch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 10:13:39 +02:00
co-authored by Claude Opus 5
parent 213bcca3a1
commit 5e10523df6
29 changed files with 2579 additions and 6 deletions
+38
View File
@@ -0,0 +1,38 @@
import { can, ServiceError, type ServiceCtx } from "@/server/services/context";
/**
* Who may use the planning views (L13):
* - `work_order:read_all` (backoffice, admin): all teams; scheduling needs `work_order:assign` + `work_order:write`.
* - `work_order:read_team` + leader of at least one active team (Teamleiter): read-only, own teams only.
* - everyone else (Monteur): forbidden.
*/
export type PlanningAccess = {
all: boolean;
/** null = all teams of the tenant */
teamIds: string[] | null;
canSchedule: boolean;
canManageTeams: boolean;
};
export async function planningAccess(ctx: ServiceCtx): Promise<PlanningAccess> {
if (can(ctx, "work_order:read_all")) {
return {
all: true,
teamIds: null,
canSchedule: can(ctx, "work_order:assign") && can(ctx, "work_order:write"),
canManageTeams: can(ctx, "team:manage"),
};
}
if (can(ctx, "work_order:read_team")) {
const led = await ctx.db.team.findMany({ where: { leaderUserId: ctx.userId, status: "active", deletedAt: null }, select: { id: true } });
if (led.length > 0) return { all: false, teamIds: led.map((t) => t.id), canSchedule: false, canManageTeams: false };
}
throw new ServiceError("forbidden", "planning not allowed");
}
/** Recommendations and scheduling are backoffice functions (all teams + assign right). */
export async function assertCanPlanAll(ctx: ServiceCtx): Promise<PlanningAccess> {
const access = await planningAccess(ctx);
if (!access.all || !can(ctx, "work_order:assign")) throw new ServiceError("forbidden", "missing permission work_order:assign");
return access;
}