47b2aeec72
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>
79 lines
2.5 KiB
TypeScript
79 lines
2.5 KiB
TypeScript
/**
|
|
* Checks whether a resource is already assigned to the same project
|
|
* with overlapping dates. Used to prevent duplicate bookings.
|
|
*/
|
|
|
|
export interface ExistingAssignment {
|
|
id: string;
|
|
resourceId: string;
|
|
projectId: string;
|
|
startDate: Date | string;
|
|
endDate: Date | string;
|
|
status: string;
|
|
}
|
|
|
|
export interface DuplicateCheckResult {
|
|
isDuplicate: boolean;
|
|
conflictingAssignment?: ExistingAssignment;
|
|
message?: string;
|
|
}
|
|
|
|
const ACTIVE_STATUSES = new Set(["CONFIRMED", "ACTIVE", "PROPOSED"]);
|
|
|
|
function toTime(d: Date | string): number {
|
|
const dt = typeof d === "string" ? new Date(d) : d;
|
|
dt.setHours(0, 0, 0, 0);
|
|
return dt.getTime();
|
|
}
|
|
|
|
/**
|
|
* Check if assigning a resource to a project with the given dates
|
|
* would create a duplicate (same resource + same project + overlapping dates).
|
|
*
|
|
* @param resourceId - The resource being assigned
|
|
* @param projectId - The project being assigned to
|
|
* @param startDate - Start of the new assignment
|
|
* @param endDate - End of the new assignment
|
|
* @param existingAssignments - All current assignments for this resource
|
|
* @param excludeAssignmentId - Exclude this ID (for updates — don't conflict with self)
|
|
*/
|
|
export function checkDuplicateAssignment(
|
|
resourceId: string,
|
|
projectId: string,
|
|
startDate: Date | string,
|
|
endDate: Date | string,
|
|
existingAssignments: ExistingAssignment[],
|
|
excludeAssignmentId?: string,
|
|
): DuplicateCheckResult {
|
|
const newStart = toTime(startDate);
|
|
const newEnd = toTime(endDate);
|
|
|
|
for (const existing of existingAssignments) {
|
|
// Skip self (for updates)
|
|
if (excludeAssignmentId && existing.id === excludeAssignmentId) continue;
|
|
|
|
// Skip different resource or project
|
|
if (existing.resourceId !== resourceId) continue;
|
|
if (existing.projectId !== projectId) continue;
|
|
|
|
// Skip cancelled/inactive
|
|
if (!ACTIVE_STATUSES.has(existing.status)) continue;
|
|
|
|
// Check date overlap: existingStart <= newEnd && existingEnd >= newStart
|
|
const existStart = toTime(existing.startDate);
|
|
const existEnd = toTime(existing.endDate);
|
|
|
|
if (existStart <= newEnd && existEnd >= newStart) {
|
|
const startStr = new Date(existing.startDate).toISOString().slice(0, 10);
|
|
const endStr = new Date(existing.endDate).toISOString().slice(0, 10);
|
|
return {
|
|
isDuplicate: true,
|
|
conflictingAssignment: existing,
|
|
message: `Resource is already assigned to this project from ${startStr} to ${endStr}. Consider updating the existing assignment instead.`,
|
|
};
|
|
}
|
|
}
|
|
|
|
return { isDuplicate: false };
|
|
}
|