import { PhaseDeadlines, PhaseInfo, PhaseName, PHASE_INFO } from '../models/project-phase.model'; import { addDays, normalizeDateInput } from './date-utils'; const DAY_MS = 24 * 60 * 60 * 1000; const DEFAULT_PHASE_RATIOS: Record = { modeling: 0.3, softDecor: 0.25, rendering: 0.3, postProcessing: 0.15 }; export function generatePhaseDeadlines( startDate: Date, endDate?: Date, ratios: Record = DEFAULT_PHASE_RATIOS ): PhaseDeadlines { const safeStart = new Date(startDate); const safeEnd = endDate ? new Date(endDate) : addDays(safeStart, 30); if (isNaN(safeEnd.getTime()) || safeEnd <= safeStart) { safeEnd.setTime(safeStart.getTime() + 30 * DAY_MS); } const totalDays = Math.max(4, Math.ceil((safeEnd.getTime() - safeStart.getTime()) / DAY_MS)); const durations = calculatePhaseDurations(totalDays, ratios); const deadlines: PhaseDeadlines = {}; let cursor = new Date(safeStart); (Object.keys(durations) as PhaseName[]).forEach((phase, index) => { const days = Math.max(1, durations[phase]); const deadline = addDays(cursor, days); deadlines[phase] = { startDate: cursor.toISOString(), deadline: deadline.toISOString(), estimatedDays: days, status: index === 0 ? 'in_progress' : 'not_started', priority: index === 0 ? 'high' : 'medium' }; cursor = new Date(deadline.getTime()); }); return deadlines; } export function ensurePhaseDeadlines( existing: PhaseDeadlines | undefined, startDate: Date, endDate?: Date ): PhaseDeadlines { if (existing) { return existing; } return generatePhaseDeadlines(startDate, endDate); } export function mapDeliveryTypeToPhase(deliveryType: string): PhaseName | null { const map: Record = { white_model: 'modeling', soft_decor: 'softDecor', rendering: 'rendering', post_process: 'postProcessing' }; return map[deliveryType] || null; } export function getNextPhaseName(current: PhaseName): PhaseName | null { const order: PhaseName[] = ['modeling', 'softDecor', 'rendering', 'postProcessing']; const index = order.indexOf(current); if (index === -1 || index === order.length - 1) { return null; } return order[index + 1]; } export function updatePhaseOnSubmission( deadlines: PhaseDeadlines, phase: PhaseName, submittedAt: Date ): void { const info = ensurePhaseInfo(deadlines, phase); const iso = submittedAt.toISOString(); if (!info.startDate) { info.startDate = iso; } if (!(info as any).firstUploadAt) { (info as any).firstUploadAt = iso; } (info as any).lastSubmissionAt = iso; if (!info.deadline) { const days = info.estimatedDays || getDefaultPhaseDays(phase); const start = normalizeDateInput(info.startDate, submittedAt); info.deadline = addDays(start, days).toISOString(); info.estimatedDays = days; } if (info.status !== 'completed') { info.status = 'in_progress'; } scheduleNextPhase(deadlines, phase, submittedAt); } export function markPhaseStatus( deadlines: PhaseDeadlines, phase: PhaseName, status: 'completed' | 'delayed' | 'in_progress', timestamp: Date ): void { const info = ensurePhaseInfo(deadlines, phase); info.status = status; if (status === 'completed') { info.completedAt = timestamp.toISOString(); } else if (info.completedAt) { delete info.completedAt; } } export function scheduleNextPhase( deadlines: PhaseDeadlines, currentPhase: PhaseName, anchorDate: Date ): void { const nextPhase = getNextPhaseName(currentPhase); if (!nextPhase) { return; } const nextInfo = ensurePhaseInfo(deadlines, nextPhase); if (!nextInfo.startDate) { nextInfo.startDate = anchorDate.toISOString(); } if (!nextInfo.deadline) { const days = nextInfo.estimatedDays || getDefaultPhaseDays(nextPhase); const start = normalizeDateInput(nextInfo.startDate, anchorDate); nextInfo.deadline = addDays(start, days).toISOString(); nextInfo.estimatedDays = days; } if (!nextInfo.status || nextInfo.status === 'not_started') { nextInfo.status = 'not_started'; } } function ensurePhaseInfo(deadlines: PhaseDeadlines, phase: PhaseName): PhaseInfo { if (!deadlines[phase]) { const start = new Date(); const defaultDays = getDefaultPhaseDays(phase); deadlines[phase] = { startDate: start.toISOString(), deadline: addDays(start, defaultDays).toISOString(), estimatedDays: defaultDays, status: 'not_started', priority: 'medium' }; } return deadlines[phase]!; } function calculatePhaseDurations( totalDays: number, ratios: Record ): Record { const result: Record = { modeling: 1, softDecor: 1, rendering: 1, postProcessing: 1 }; let allocated = 0; (Object.keys(result) as PhaseName[]).forEach((phase) => { const ratio = ratios[phase] ?? 0.25; const days = Math.max(1, Math.round(totalDays * ratio)); result[phase] = days; allocated += days; }); const diff = totalDays - allocated; if (diff !== 0) { result.postProcessing = Math.max(1, result.postProcessing + diff); } return result; } function getDefaultPhaseDays(phase: PhaseName): number { const meta = PHASE_INFO[phase]; if (!meta) { return 3; } return Math.max(1, meta.defaultDays || 3); }