| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200 |
- 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<PhaseName, number> = {
- modeling: 0.3,
- softDecor: 0.25,
- rendering: 0.3,
- postProcessing: 0.15
- };
- export function generatePhaseDeadlines(
- startDate: Date,
- endDate?: Date,
- ratios: Record<PhaseName, number> = 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<string, PhaseName> = {
- 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<PhaseName, number>
- ): Record<PhaseName, number> {
- const result: Record<PhaseName, number> = {
- 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);
- }
|