hp-prod-tracker/src/lib/services/assignment-service.ts
2026-03-17 23:07:44 -05:00

250 lines
6.8 KiB
TypeScript

import { prisma } from "@/lib/prisma";
import type { AssignmentRole } from "@/generated/prisma/client";
import { emitAssignmentCreated } from "@/lib/automation/event-bus";
// Register automation handler (side-effect import)
import "@/lib/services/automation-service";
export async function assignUserToStage(
deliverableStageId: string,
userId: string,
role: AssignmentRole = "LEAD",
options: { dryRun?: boolean } = {}
) {
// Validate stage and user exist
const [stage, user] = await Promise.all([
prisma.deliverableStage.findUnique({
where: { id: deliverableStageId },
include: {
template: true,
deliverable: {
select: {
id: true,
name: true,
project: { select: { id: true, name: true, organizationId: true } },
},
},
},
}),
prisma.user.findUnique({
where: { id: userId },
select: { id: true, name: true, email: true, maxCapacity: true },
}),
]);
if (!stage) throw new Error("Stage not found");
if (!user) throw new Error("User not found");
// Check for existing assignment
const existing = await prisma.stageAssignment.findUnique({
where: { deliverableStageId_userId: { deliverableStageId, userId } },
});
if (options.dryRun) {
return {
dryRun: true,
action: existing ? "update_role" : "create_assignment",
stageName: stage.template.name,
userName: user.name || user.email,
role,
};
}
const result = await prisma.stageAssignment.upsert({
where: {
deliverableStageId_userId: { deliverableStageId, userId },
},
update: { role },
create: { deliverableStageId, userId, role },
include: { user: true, deliverableStage: { include: { template: true } } },
});
// Emit automation event (non-blocking)
emitAssignmentCreated(stage!.deliverable.project.organizationId, {
assignmentId: result.id,
stageId: deliverableStageId,
stageName: stage!.template.name,
userId,
userName: user!.name || user!.email,
deliverableId: stage!.deliverable.id,
projectId: stage!.deliverable.project.id,
}).catch((err) => {
console.error("[Automation] Failed to emit assignment.created:", err);
});
return result;
}
export async function removeAssignment(
deliverableStageId: string,
userId: string
) {
return prisma.stageAssignment.delete({
where: {
deliverableStageId_userId: { deliverableStageId, userId },
},
});
}
export async function getMyWork(userId: string) {
return prisma.stageAssignment.findMany({
where: { userId },
include: {
deliverableStage: {
include: {
template: true,
deliverable: {
include: {
project: { select: { id: true, name: true, projectCode: true } },
},
},
},
},
},
orderBy: { createdAt: "desc" },
});
}
// ─── Bulk Operations ────────────────────────────────────
export interface BulkAssignItem {
deliverableStageId: string;
userId: string;
role?: AssignmentRole;
}
export interface BulkAssignResult {
total: number;
succeeded: {
deliverableStageId: string;
stageName: string;
userId: string;
userName: string;
}[];
failed: {
deliverableStageId: string;
userId: string;
reason: string;
}[];
}
/**
* Assign multiple artists to stages in a single transaction.
* When dryRun is true, validates and returns preview without executing.
*/
export async function bulkAssignArtists(
items: BulkAssignItem[],
options: { dryRun?: boolean } = {}
): Promise<BulkAssignResult> {
if (items.length === 0) {
return { total: 0, succeeded: [], failed: [] };
}
// Fetch all referenced stages and users in bulk
const stageIds = [...new Set(items.map((i) => i.deliverableStageId))];
const userIds = [...new Set(items.map((i) => i.userId))];
const [stages, users] = await Promise.all([
prisma.deliverableStage.findMany({
where: { id: { in: stageIds } },
include: {
template: true,
deliverable: {
select: {
id: true,
name: true,
project: { select: { id: true, name: true, organizationId: true } },
},
},
},
}),
prisma.user.findMany({
where: { id: { in: userIds } },
select: { id: true, name: true, email: true },
}),
]);
const stageMap = new Map(stages.map((s) => [s.id, s]));
const userMap = new Map(users.map((u) => [u.id, u]));
const succeeded: BulkAssignResult["succeeded"] = [];
const failed: BulkAssignResult["failed"] = [];
for (const item of items) {
const stage = stageMap.get(item.deliverableStageId);
if (!stage) {
failed.push({
deliverableStageId: item.deliverableStageId,
userId: item.userId,
reason: "Stage not found",
});
continue;
}
const user = userMap.get(item.userId);
if (!user) {
failed.push({
deliverableStageId: item.deliverableStageId,
userId: item.userId,
reason: "User not found",
});
continue;
}
succeeded.push({
deliverableStageId: item.deliverableStageId,
stageName: stage.template.name,
userId: item.userId,
userName: user.name || user.email,
});
}
if (options.dryRun) {
return { total: items.length, succeeded, failed };
}
// Execute all valid assignments in a transaction
if (succeeded.length > 0) {
await prisma.$transaction(async (tx) => {
for (const item of items) {
if (!stageMap.has(item.deliverableStageId) || !userMap.has(item.userId))
continue;
await tx.stageAssignment.upsert({
where: {
deliverableStageId_userId: {
deliverableStageId: item.deliverableStageId,
userId: item.userId,
},
},
update: { role: item.role || "LEAD" },
create: {
deliverableStageId: item.deliverableStageId,
userId: item.userId,
role: item.role || "LEAD",
},
});
}
});
}
// Emit automation events AFTER transaction commits
if (succeeded.length > 0 && !options.dryRun) {
for (const item of succeeded) {
const stage = stageMap.get(item.deliverableStageId);
if (!stage) continue;
emitAssignmentCreated(stage.deliverable.project.organizationId, {
assignmentId: `${item.deliverableStageId}:${item.userId}`,
stageId: item.deliverableStageId,
stageName: item.stageName,
userId: item.userId,
userName: item.userName,
deliverableId: stage.deliverable.id,
projectId: stage.deliverable.project.id,
}).catch((err) => {
console.error("[Automation] Failed to emit assignment.created (bulk):", err);
});
}
}
return { total: items.length, succeeded, failed };
}