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 }; }