/** * 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 }; }