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:
@@ -1,4 +1,4 @@
|
||||
import { calculateAllocation, validateAvailability } from "@planarchy/engine";
|
||||
import { calculateAllocation, validateAvailability, checkDuplicateAssignment } from "@planarchy/engine";
|
||||
import type { PrismaClient, Prisma } from "@planarchy/db";
|
||||
import {
|
||||
type Allocation,
|
||||
@@ -105,6 +105,28 @@ export async function createAssignment(
|
||||
},
|
||||
);
|
||||
|
||||
const duplicateResult = checkDuplicateAssignment(
|
||||
input.resourceId,
|
||||
input.projectId,
|
||||
input.startDate,
|
||||
input.endDate,
|
||||
existingBookings.map(b => ({
|
||||
id: b.id,
|
||||
resourceId: b.resourceId ?? "",
|
||||
projectId: b.projectId,
|
||||
startDate: b.startDate,
|
||||
endDate: b.endDate,
|
||||
status: b.status,
|
||||
})),
|
||||
);
|
||||
|
||||
if (duplicateResult.isDuplicate) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: duplicateResult.message ?? "Resource is already assigned to this project with overlapping dates",
|
||||
});
|
||||
}
|
||||
|
||||
const availability = resource.availability as unknown as WeekdayAvailability;
|
||||
const availabilityWindows = existingBookings.map((booking) => ({
|
||||
startDate: booking.startDate,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { PrismaClient } from "@planarchy/db";
|
||||
import { AllocationStatus, type FillDemandRequirementInput } from "@planarchy/shared";
|
||||
import { checkDuplicateAssignment } from "@planarchy/engine";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { type AssignmentWithRelations } from "./create-assignment.js";
|
||||
import { fillDemandRequirementWithLegacySync } from "./fill-demand-requirement-with-legacy-sync.js";
|
||||
import { listAssignmentBookings } from "./list-assignment-bookings.js";
|
||||
|
||||
export interface FillDemandRequirementResult {
|
||||
assignment: AssignmentWithRelations;
|
||||
@@ -54,5 +56,34 @@ export async function fillDemandRequirement(
|
||||
});
|
||||
}
|
||||
|
||||
const existingBookings = await listAssignmentBookings(
|
||||
db as unknown as Parameters<typeof listAssignmentBookings>[0],
|
||||
{
|
||||
resourceIds: [input.resourceId],
|
||||
},
|
||||
);
|
||||
|
||||
const duplicateResult = checkDuplicateAssignment(
|
||||
input.resourceId,
|
||||
demandRequirement.projectId,
|
||||
demandRequirement.startDate,
|
||||
demandRequirement.endDate,
|
||||
existingBookings.map(b => ({
|
||||
id: b.id,
|
||||
resourceId: b.resourceId ?? "",
|
||||
projectId: b.projectId,
|
||||
startDate: b.startDate,
|
||||
endDate: b.endDate,
|
||||
status: b.status,
|
||||
})),
|
||||
);
|
||||
|
||||
if (duplicateResult.isDuplicate) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: duplicateResult.message ?? "Resource is already assigned to this project with overlapping dates",
|
||||
});
|
||||
}
|
||||
|
||||
return fillDemandRequirementWithLegacySync(db, demandRequirement, input);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user