250 lines
6.8 KiB
TypeScript
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 };
|
|
}
|