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:
@@ -0,0 +1,24 @@
|
||||
import { requireApiContext } from "@/server/api/context";
|
||||
import { json, withApi } from "@/server/api/respond";
|
||||
import { getPlanningBoard, weekRange } from "@/server/services/planning/board";
|
||||
|
||||
/**
|
||||
* GET /api/v1/planning/board?from=YYYY-MM-DD&to=YYYY-MM-DD&teamId=…&orderTypeId=…&priority=…
|
||||
* Default range: current week (Mon–Sun, tenant timezone), max. 42 days. Backoffice: all teams;
|
||||
* team leads: own teams (read-only). Access is checked in the service.
|
||||
*/
|
||||
export const GET = withApi(async (req: Request) => {
|
||||
const ctx = await requireApiContext("work_orders");
|
||||
const sp = new URL(req.url).searchParams;
|
||||
const week = await weekRange(ctx, sp.get("from"));
|
||||
const teamIds = sp.getAll("teamId").filter(Boolean);
|
||||
return json(
|
||||
await getPlanningBoard(ctx, {
|
||||
from: sp.get("from") ?? week.from,
|
||||
to: sp.get("to") ?? week.to,
|
||||
teamIds: teamIds.length ? teamIds : undefined,
|
||||
orderTypeId: sp.get("orderTypeId") || undefined,
|
||||
priority: (sp.get("priority") || undefined) as "low" | "normal" | "high" | "urgent" | undefined,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import { requireApiContext } from "@/server/api/context";
|
||||
import { json, withApi } from "@/server/api/respond";
|
||||
import { getLiveSituation, LIVE_STATUSES, type LiveStatus } from "@/server/services/planning/live";
|
||||
|
||||
/**
|
||||
* GET /api/v1/planning/live?teamId=…&status=en_route|working|paused|free — polled every 30 s by
|
||||
* /planning/live. Location = site of the running order; never device coordinates.
|
||||
*/
|
||||
export const GET = withApi(async (req: Request) => {
|
||||
const ctx = await requireApiContext("work_orders");
|
||||
const sp = new URL(req.url).searchParams;
|
||||
const status = sp.get("status");
|
||||
return json(
|
||||
await getLiveSituation(ctx, {
|
||||
teamId: sp.get("teamId") || undefined,
|
||||
status: status && (LIVE_STATUSES as readonly string[]).includes(status) ? (status as LiveStatus) : undefined,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { requireApiContext } from "@/server/api/context";
|
||||
import { ApiError, json, withApi } from "@/server/api/respond";
|
||||
import { findNearbyUnplanned, recommendAssignments } from "@/server/services/planning/recommend";
|
||||
|
||||
/**
|
||||
* GET /api/v1/planning/recommendations?workOrderId=…&days=10&radiusKm=25&nearbyRadiusKm=5&locale=de
|
||||
* Suggestions only (straight-line distance + free capacity); nothing is saved.
|
||||
*/
|
||||
export const GET = withApi(async (req: Request) => {
|
||||
const ctx = await requireApiContext("work_orders", "work_order:assign");
|
||||
const sp = new URL(req.url).searchParams;
|
||||
const workOrderId = sp.get("workOrderId");
|
||||
if (!workOrderId) throw new ApiError("invalid", "workOrderId required");
|
||||
const locale = sp.get("locale") === "en" ? "en" : "de";
|
||||
const [recommendations, nearby] = await Promise.all([
|
||||
recommendAssignments(ctx, {
|
||||
workOrderId,
|
||||
days: sp.get("days") ?? undefined,
|
||||
radiusKm: sp.get("radiusKm") ?? undefined,
|
||||
locale,
|
||||
}),
|
||||
findNearbyUnplanned(ctx, { workOrderId, radiusKm: sp.get("nearbyRadiusKm") ?? undefined }),
|
||||
]);
|
||||
return json({ ...recommendations, nearby });
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { requireApiContext } from "@/server/api/context";
|
||||
import { json, readJsonObject, withApi } from "@/server/api/respond";
|
||||
import { scheduleWorkOrder, type ScheduleInput } from "@/server/services/planning/schedule";
|
||||
|
||||
/**
|
||||
* POST /api/v1/planning/schedule — body { workOrderId, teamId, plannedStart, plannedEnd?, plannedDurationMinutes?, baseVersion }.
|
||||
* Assign + dates in one transaction (L2 services, event work_order.assigned, audit). 409 conflict when
|
||||
* the order changed meanwhile ("Auftrag wurde zwischenzeitlich geändert"). Response contains the
|
||||
* conflicts of the target team on the target day(s).
|
||||
*/
|
||||
export const POST = withApi(async (req: Request) => {
|
||||
const ctx = await requireApiContext("work_orders", "work_order:assign");
|
||||
const body = await readJsonObject(req);
|
||||
return json(await scheduleWorkOrder(ctx, body as ScheduleInput));
|
||||
});
|
||||
Reference in New Issue
Block a user