L2 Aufträge & Backoffice: Service-Schicht Aufträge + Tests
Statusmaschine (transitionWorkOrder inkl. eventData), Zuweisung, Completion-Guards, Materialvorgabe, Checklisten/Pflichtfotos, Liste/Dashboard-Presets, Suche, Sync-Konflikte, Einstellungen (Auftragsarten, Vorlagen, Nummernkreise). Tests: Übergangsmatrix je Rolle, Kernlogik, Scope/Mandantentrennung, Nummernkreis-Parallelität. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import { canTransition, type WorkOrderStatus } from "@/lib/work-orders/status";
|
||||
import { assignSchema, type AssignInput } from "@/lib/work-orders/schemas";
|
||||
import { emitEvent } from "@/server/events";
|
||||
import { assertCan, ServiceError, type ServiceCtx } from "@/server/services/context";
|
||||
import {
|
||||
assertBaseVersion,
|
||||
assertNotLocked,
|
||||
auditWorkOrder,
|
||||
loadVisibleWorkOrder,
|
||||
parseInput,
|
||||
PLANNING_LOCKED,
|
||||
writeWithVersion,
|
||||
} from "@/server/services/work-orders/_shared";
|
||||
|
||||
/**
|
||||
* Assign a team (+ optional individual technicians and a responsible team lead) — US-004.
|
||||
* draft/review_required/planned → assigned; accepted with a changed team → back to assigned.
|
||||
*/
|
||||
export async function assignWorkOrder(
|
||||
ctx: ServiceCtx,
|
||||
raw: AssignInput,
|
||||
): Promise<{ id: string; version: number; status: WorkOrderStatus }> {
|
||||
assertCan(ctx, "work_order:assign");
|
||||
const input = parseInput(assignSchema, raw);
|
||||
const wo = await loadVisibleWorkOrder(ctx, input.workOrderId);
|
||||
assertNotLocked(wo, PLANNING_LOCKED);
|
||||
assertBaseVersion(wo, input.baseVersion);
|
||||
|
||||
const team = await ctx.db.team.findFirst({
|
||||
where: { id: input.teamId, deletedAt: null, status: "active" },
|
||||
select: { id: true, name: true, leaderUserId: true },
|
||||
});
|
||||
if (!team) throw new ServiceError("invalid", "team_not_found");
|
||||
|
||||
const userIds = [...new Set(input.userIds)];
|
||||
const leadId = input.teamLeadUserId ?? team.leaderUserId ?? null;
|
||||
const toCheck = [...new Set([...userIds, ...(leadId ? [leadId] : [])])];
|
||||
if (toCheck.length) {
|
||||
const found = await ctx.db.user.count({ where: { id: { in: toCheck }, status: "ACTIVE" } });
|
||||
if (found !== toCheck.length) throw new ServiceError("invalid", "user_not_found");
|
||||
}
|
||||
|
||||
const previous = await ctx.db.workOrderAssignee.findMany({ where: { workOrderId: wo.id }, select: { userId: true } });
|
||||
|
||||
let to: WorkOrderStatus | null = null;
|
||||
if (["draft", "review_required", "planned"].includes(wo.status)) to = "assigned";
|
||||
else if (wo.status === "accepted" && wo.assignedTeamId !== team.id) to = "assigned";
|
||||
if (to && !canTransition(wo.status, to)) to = null;
|
||||
|
||||
const version = await writeWithVersion(ctx, wo, {
|
||||
assignedTeamId: team.id,
|
||||
teamLeadUserId: leadId,
|
||||
...(to ? { status: to } : {}),
|
||||
});
|
||||
await ctx.db.workOrderAssignee.deleteMany({ where: { workOrderId: wo.id, userId: { notIn: userIds } } });
|
||||
if (userIds.length) {
|
||||
await ctx.db.workOrderAssignee.createMany({
|
||||
data: userIds.map((userId) => ({ tenantId: ctx.tenantId, workOrderId: wo.id, userId })),
|
||||
skipDuplicates: true,
|
||||
});
|
||||
}
|
||||
if (to) {
|
||||
await ctx.db.workOrderStatusChange.create({
|
||||
data: { tenantId: ctx.tenantId, workOrderId: wo.id, fromStatus: wo.status, toStatus: to, actorId: ctx.userId },
|
||||
});
|
||||
}
|
||||
|
||||
await auditWorkOrder(ctx, {
|
||||
action: "update",
|
||||
workOrderId: wo.id,
|
||||
before: { assignedTeamId: wo.assignedTeamId, teamLeadUserId: wo.teamLeadUserId, userIds: previous.map((p) => p.userId), status: wo.status },
|
||||
after: { assignedTeamId: team.id, teamLeadUserId: leadId, userIds, status: to ?? wo.status, version },
|
||||
});
|
||||
await emitEvent(ctx, {
|
||||
type: "work_order.assigned",
|
||||
entityType: "work_order",
|
||||
entityId: wo.id,
|
||||
data: { number: wo.number, teamId: team.id, teamName: team.name },
|
||||
});
|
||||
return { id: wo.id, version, status: to ?? wo.status };
|
||||
}
|
||||
Reference in New Issue
Block a user