phase-deadline.utils.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. import { PhaseDeadlines, PhaseInfo, PhaseName, PHASE_INFO } from '../models/project-phase.model';
  2. import { addDays, normalizeDateInput } from './date-utils';
  3. const DAY_MS = 24 * 60 * 60 * 1000;
  4. const DEFAULT_PHASE_RATIOS: Record<PhaseName, number> = {
  5. modeling: 0.3,
  6. softDecor: 0.25,
  7. rendering: 0.3,
  8. postProcessing: 0.15
  9. };
  10. export function generatePhaseDeadlines(
  11. startDate: Date,
  12. endDate?: Date,
  13. ratios: Record<PhaseName, number> = DEFAULT_PHASE_RATIOS
  14. ): PhaseDeadlines {
  15. const safeStart = new Date(startDate);
  16. const safeEnd = endDate ? new Date(endDate) : addDays(safeStart, 30);
  17. if (isNaN(safeEnd.getTime()) || safeEnd <= safeStart) {
  18. safeEnd.setTime(safeStart.getTime() + 30 * DAY_MS);
  19. }
  20. const totalDays = Math.max(4, Math.ceil((safeEnd.getTime() - safeStart.getTime()) / DAY_MS));
  21. const durations = calculatePhaseDurations(totalDays, ratios);
  22. const deadlines: PhaseDeadlines = {};
  23. let cursor = new Date(safeStart);
  24. (Object.keys(durations) as PhaseName[]).forEach((phase, index) => {
  25. const days = Math.max(1, durations[phase]);
  26. const deadline = addDays(cursor, days);
  27. deadlines[phase] = {
  28. startDate: cursor.toISOString(),
  29. deadline: deadline.toISOString(),
  30. estimatedDays: days,
  31. status: index === 0 ? 'in_progress' : 'not_started',
  32. priority: index === 0 ? 'high' : 'medium'
  33. };
  34. cursor = new Date(deadline.getTime());
  35. });
  36. return deadlines;
  37. }
  38. export function ensurePhaseDeadlines(
  39. existing: PhaseDeadlines | undefined,
  40. startDate: Date,
  41. endDate?: Date
  42. ): PhaseDeadlines {
  43. if (existing) {
  44. return existing;
  45. }
  46. return generatePhaseDeadlines(startDate, endDate);
  47. }
  48. export function mapDeliveryTypeToPhase(deliveryType: string): PhaseName | null {
  49. const map: Record<string, PhaseName> = {
  50. white_model: 'modeling',
  51. soft_decor: 'softDecor',
  52. rendering: 'rendering',
  53. post_process: 'postProcessing'
  54. };
  55. return map[deliveryType] || null;
  56. }
  57. export function getNextPhaseName(current: PhaseName): PhaseName | null {
  58. const order: PhaseName[] = ['modeling', 'softDecor', 'rendering', 'postProcessing'];
  59. const index = order.indexOf(current);
  60. if (index === -1 || index === order.length - 1) {
  61. return null;
  62. }
  63. return order[index + 1];
  64. }
  65. export function updatePhaseOnSubmission(
  66. deadlines: PhaseDeadlines,
  67. phase: PhaseName,
  68. submittedAt: Date
  69. ): void {
  70. const info = ensurePhaseInfo(deadlines, phase);
  71. const iso = submittedAt.toISOString();
  72. if (!info.startDate) {
  73. info.startDate = iso;
  74. }
  75. if (!(info as any).firstUploadAt) {
  76. (info as any).firstUploadAt = iso;
  77. }
  78. (info as any).lastSubmissionAt = iso;
  79. if (!info.deadline) {
  80. const days = info.estimatedDays || getDefaultPhaseDays(phase);
  81. const start = normalizeDateInput(info.startDate, submittedAt);
  82. info.deadline = addDays(start, days).toISOString();
  83. info.estimatedDays = days;
  84. }
  85. if (info.status !== 'completed') {
  86. info.status = 'in_progress';
  87. }
  88. scheduleNextPhase(deadlines, phase, submittedAt);
  89. }
  90. export function markPhaseStatus(
  91. deadlines: PhaseDeadlines,
  92. phase: PhaseName,
  93. status: 'completed' | 'delayed' | 'in_progress',
  94. timestamp: Date
  95. ): void {
  96. const info = ensurePhaseInfo(deadlines, phase);
  97. info.status = status;
  98. if (status === 'completed') {
  99. info.completedAt = timestamp.toISOString();
  100. } else if (info.completedAt) {
  101. delete info.completedAt;
  102. }
  103. }
  104. export function scheduleNextPhase(
  105. deadlines: PhaseDeadlines,
  106. currentPhase: PhaseName,
  107. anchorDate: Date
  108. ): void {
  109. const nextPhase = getNextPhaseName(currentPhase);
  110. if (!nextPhase) {
  111. return;
  112. }
  113. const nextInfo = ensurePhaseInfo(deadlines, nextPhase);
  114. if (!nextInfo.startDate) {
  115. nextInfo.startDate = anchorDate.toISOString();
  116. }
  117. if (!nextInfo.deadline) {
  118. const days = nextInfo.estimatedDays || getDefaultPhaseDays(nextPhase);
  119. const start = normalizeDateInput(nextInfo.startDate, anchorDate);
  120. nextInfo.deadline = addDays(start, days).toISOString();
  121. nextInfo.estimatedDays = days;
  122. }
  123. if (!nextInfo.status || nextInfo.status === 'not_started') {
  124. nextInfo.status = 'not_started';
  125. }
  126. }
  127. function ensurePhaseInfo(deadlines: PhaseDeadlines, phase: PhaseName): PhaseInfo {
  128. if (!deadlines[phase]) {
  129. const start = new Date();
  130. const defaultDays = getDefaultPhaseDays(phase);
  131. deadlines[phase] = {
  132. startDate: start.toISOString(),
  133. deadline: addDays(start, defaultDays).toISOString(),
  134. estimatedDays: defaultDays,
  135. status: 'not_started',
  136. priority: 'medium'
  137. };
  138. }
  139. return deadlines[phase]!;
  140. }
  141. function calculatePhaseDurations(
  142. totalDays: number,
  143. ratios: Record<PhaseName, number>
  144. ): Record<PhaseName, number> {
  145. const result: Record<PhaseName, number> = {
  146. modeling: 1,
  147. softDecor: 1,
  148. rendering: 1,
  149. postProcessing: 1
  150. };
  151. let allocated = 0;
  152. (Object.keys(result) as PhaseName[]).forEach((phase) => {
  153. const ratio = ratios[phase] ?? 0.25;
  154. const days = Math.max(1, Math.round(totalDays * ratio));
  155. result[phase] = days;
  156. allocated += days;
  157. });
  158. const diff = totalDays - allocated;
  159. if (diff !== 0) {
  160. result.postProcessing = Math.max(1, result.postProcessing + diff);
  161. }
  162. return result;
  163. }
  164. function getDefaultPhaseDays(phase: PhaseName): number {
  165. const meta = PHASE_INFO[phase];
  166. if (!meta) {
  167. return 3;
  168. }
  169. return Math.max(1, meta.defaultDays || 3);
  170. }