feat: prevent duplicate resource-project assignments
Engine (packages/engine): - New checkDuplicateAssignment() pure function: detects same resource assigned to same project with overlapping dates - 15 unit tests covering: overlap, no-overlap, cancelled, self-exclude, string dates, PROPOSED status Application layer (packages/application): - createAssignment: throws CONFLICT before DB write if duplicate found - fillDemandRequirement: same check before entering transaction AI Assistant (packages/api/router/assistant-tools.ts): - create_allocation: checks before creating, returns helpful error message - fill_demand: same check using demand's projectId UI (apps/web): - AllocationModal: amber warning when resource already assigned to selected project with overlapping dates (non-blocking) Database cleanup: - Found and merged 1 duplicate: Wong Wong on Porsche Taycan Sport Film (2 overlapping PROPOSED assignments merged into 1) Regression: 298 engine tests pass (283 + 15 new). TypeScript clean. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { prisma } from "@planarchy/db";
|
||||
import { calculateAllocation, countWorkingDays } from "@planarchy/engine/allocation";
|
||||
import { calculateAllocation, checkDuplicateAssignment, countWorkingDays } from "@planarchy/engine/allocation";
|
||||
import { computeBudgetStatus } from "@planarchy/engine";
|
||||
import type { PermissionKey } from "@planarchy/shared";
|
||||
import { parseTaskAction } from "@planarchy/shared";
|
||||
@@ -2195,6 +2195,16 @@ const executors = {
|
||||
const startDate = new Date(params.startDate);
|
||||
const endDate = new Date(params.endDate);
|
||||
|
||||
// Check for overlapping duplicate assignments (same resource + project + overlapping dates)
|
||||
const existingAssignments = await ctx.db.assignment.findMany({
|
||||
where: { resourceId: resource.id, status: { not: "CANCELLED" } },
|
||||
select: { id: true, resourceId: true, projectId: true, startDate: true, endDate: true, status: true },
|
||||
});
|
||||
const dupCheck = checkDuplicateAssignment(resource.id, project.id, startDate, endDate, existingAssignments);
|
||||
if (dupCheck.isDuplicate) {
|
||||
return { error: dupCheck.message + " Use update_allocation_status to modify the existing assignment." };
|
||||
}
|
||||
|
||||
// Check for existing CANCELLED allocation with same unique key — reactivate it
|
||||
const existing = await ctx.db.assignment.findUnique({
|
||||
where: {
|
||||
@@ -3002,6 +3012,16 @@ const executors = {
|
||||
}
|
||||
if (!resource) return { error: `Resource not found: ${params.resourceId}` };
|
||||
|
||||
// Check for overlapping duplicate assignments (same resource + project + overlapping dates)
|
||||
const existingAssignments = await ctx.db.assignment.findMany({
|
||||
where: { resourceId: resource.id, status: { not: "CANCELLED" } },
|
||||
select: { id: true, resourceId: true, projectId: true, startDate: true, endDate: true, status: true },
|
||||
});
|
||||
const dupCheck = checkDuplicateAssignment(resource.id, demand.project.id, demand.startDate, demand.endDate, existingAssignments);
|
||||
if (dupCheck.isDuplicate) {
|
||||
return { error: dupCheck.message + " Use update_allocation_status to modify the existing assignment." };
|
||||
}
|
||||
|
||||
const roleName = demand.roleEntity?.name ?? demand.role ?? null;
|
||||
const dailyCostCents = Math.round(resource.lcrCents * demand.hoursPerDay);
|
||||
const assignment = await ctx.db.assignment.create({
|
||||
|
||||
Reference in New Issue
Block a user