|
|
@@ -10,6 +10,9 @@ import { FmodeParse } from 'fmode-ng/parse';
|
|
|
import { ProjectTimelineComponent } from '../project-timeline';
|
|
|
import type { ProjectTimeline } from '../project-timeline/project-timeline';
|
|
|
import { EmployeeDetailPanelComponent } from '../employee-detail-panel';
|
|
|
+import { normalizeDateInput, addDays } from '../../../utils/date-utils';
|
|
|
+import { generatePhaseDeadlines } from '../../../utils/phase-deadline.utils';
|
|
|
+import { PhaseDeadlines, PhaseName } from '../../../models/project-phase.model';
|
|
|
|
|
|
// 项目阶段定义
|
|
|
interface ProjectStage {
|
|
|
@@ -569,21 +572,6 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
* 将项目数据转换为ProjectTimeline格式(带缓存优化 + 去重)
|
|
|
*/
|
|
|
private convertToProjectTimeline(): void {
|
|
|
- // 计算当前数据大小
|
|
|
- let currentSize = 0;
|
|
|
- this.designerWorkloadMap.forEach((projects) => {
|
|
|
- currentSize += projects.length;
|
|
|
- });
|
|
|
-
|
|
|
- console.log(`📊 convertToProjectTimeline: 当前数据大小 = ${currentSize}, 缓存大小 = ${this.lastDesignerWorkloadMapSize}`);
|
|
|
-
|
|
|
- // 如果数据没有变化,使用缓存
|
|
|
- if (currentSize === this.lastDesignerWorkloadMapSize && this.timelineDataCache.length > 0) {
|
|
|
- console.log(`📊 使用缓存数据,共 ${this.timelineDataCache.length} 个项目`);
|
|
|
- this.projectTimelineData = this.timelineDataCache;
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
// 🔧 不去重,保留所有项目-设计师关联关系(一个项目可能有多个设计师)
|
|
|
const allDesignerProjects: any[] = [];
|
|
|
|
|
|
@@ -626,57 +614,66 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
const projectData = project.data || {};
|
|
|
|
|
|
// 1. 获取真实的项目开始时间
|
|
|
- // 优先使用 phaseDeadlines.modeling.startDate,其次使用 requirementsConfirmedAt,最后使用 createdAt
|
|
|
- let realStartDate: Date;
|
|
|
- if (projectData.phaseDeadlines?.modeling?.startDate) {
|
|
|
- realStartDate = projectData.phaseDeadlines.modeling.startDate instanceof Date
|
|
|
- ? projectData.phaseDeadlines.modeling.startDate
|
|
|
- : new Date(projectData.phaseDeadlines.modeling.startDate);
|
|
|
- } else if (projectData.requirementsConfirmedAt) {
|
|
|
- realStartDate = new Date(projectData.requirementsConfirmedAt);
|
|
|
- } else if (project.createdAt) {
|
|
|
- realStartDate = project.createdAt instanceof Date ? project.createdAt : new Date(project.createdAt);
|
|
|
- } else {
|
|
|
- // 降级:如果没有开始时间,使用当前时间
|
|
|
- realStartDate = new Date();
|
|
|
- }
|
|
|
+ const realStartDate = normalizeDateInput(
|
|
|
+ projectData.phaseDeadlines?.modeling?.startDate ||
|
|
|
+ projectData.requirementsConfirmedAt ||
|
|
|
+ project.createdAt,
|
|
|
+ new Date()
|
|
|
+ );
|
|
|
|
|
|
// 2. 获取真实的交付日期
|
|
|
+ // ✅ 修复:确保 deadline 是未来的日期(不使用过去的初始值或未初始化的值)
|
|
|
+ let proposedEndDate = project.deadline || projectData.phaseDeadlines?.postProcessing?.deadline;
|
|
|
let realEndDate: Date;
|
|
|
- if (project.deadline) {
|
|
|
- realEndDate = project.deadline instanceof Date ? project.deadline : new Date(project.deadline);
|
|
|
- } else {
|
|
|
- // ✅ 修复:如果没有交付日期,使用开始时间 + 30天(更合理的默认值)
|
|
|
- // 避免新项目因为默认7天导致结束日期在过去而被过滤
|
|
|
- realEndDate = new Date(realStartDate.getTime() + 30 * 24 * 60 * 60 * 1000);
|
|
|
- }
|
|
|
|
|
|
- // ✅ 调试:检查新项目
|
|
|
- if (project.id === 'qCV9QHROSH') {
|
|
|
- console.log(`📊 新项目 qCV9QHROSH 日期计算:`, {
|
|
|
- projectName: project.name,
|
|
|
- hasDeadline: !!project.deadline,
|
|
|
- deadline: project.deadline ? (project.deadline instanceof Date ? project.deadline.toLocaleString('zh-CN') : new Date(project.deadline).toLocaleString('zh-CN')) : '无',
|
|
|
- realStartDate: realStartDate.toLocaleString('zh-CN'),
|
|
|
- realEndDate: realEndDate.toLocaleString('zh-CN'),
|
|
|
- calculatedEndDate: realEndDate.toLocaleString('zh-CN')
|
|
|
- });
|
|
|
+ // 如果提议的结束日期在过去,或者日期无效,使用默认值
|
|
|
+ if (proposedEndDate) {
|
|
|
+ const proposed = normalizeDateInput(proposedEndDate, realStartDate);
|
|
|
+ // 只有当提议的日期在未来时才使用它
|
|
|
+ if (proposed.getTime() > now.getTime()) {
|
|
|
+ realEndDate = proposed;
|
|
|
+ } else {
|
|
|
+ // 日期在过去,使用默认值(从开始日期起30天)
|
|
|
+ realEndDate = addDays(realStartDate, 30);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 没有提议的日期,使用默认值
|
|
|
+ realEndDate = addDays(realStartDate, 30);
|
|
|
}
|
|
|
|
|
|
- // 3. 获取真实的对图时间
|
|
|
- // 优先使用 demoday,其次使用 phaseDeadlines.softDecor.deadline,最后计算
|
|
|
+ // 3. 获取真实的对图时间(小图对图)
|
|
|
+ // ✅ 逻辑:优先使用 project.demoday,否则在软装截止时间后半天
|
|
|
let realReviewDate: Date;
|
|
|
+ let reviewDateSource = 'default';
|
|
|
+
|
|
|
if (project.demoday) {
|
|
|
- realReviewDate = project.demoday instanceof Date ? project.demoday : new Date(project.demoday);
|
|
|
+ // 如果有显式设置的小图对图日期,使用它
|
|
|
+ realReviewDate = normalizeDateInput(project.demoday, new Date());
|
|
|
+ reviewDateSource = 'demoday';
|
|
|
} else if (projectData.phaseDeadlines?.softDecor?.deadline) {
|
|
|
- const softDecorDeadline = projectData.phaseDeadlines.softDecor.deadline;
|
|
|
- realReviewDate = softDecorDeadline instanceof Date ? softDecorDeadline : new Date(softDecorDeadline);
|
|
|
+ // 软装截止时间后半天作为小图对图时间
|
|
|
+ const softDecorDeadline = normalizeDateInput(projectData.phaseDeadlines.softDecor.deadline, new Date());
|
|
|
+ realReviewDate = new Date(softDecorDeadline.getTime() + 12 * 60 * 60 * 1000); // 加12小时
|
|
|
+ reviewDateSource = 'softDecor + 12h';
|
|
|
} else {
|
|
|
- // 计算:设置在软装和渲染之间(项目周期的 60% 位置)
|
|
|
- const projectDuration = realEndDate.getTime() - realStartDate.getTime();
|
|
|
- const projectMidPoint = realStartDate.getTime() + (projectDuration * 0.6);
|
|
|
- realReviewDate = new Date(projectMidPoint);
|
|
|
- realReviewDate.setHours(14, 0, 0, 0);
|
|
|
+ // 默认值:项目进度的60%位置,下午2点
|
|
|
+ const defaultReviewPoint = new Date(
|
|
|
+ realStartDate.getTime() + (realEndDate.getTime() - realStartDate.getTime()) * 0.6
|
|
|
+ );
|
|
|
+ defaultReviewPoint.setHours(14, 0, 0, 0);
|
|
|
+ realReviewDate = defaultReviewPoint;
|
|
|
+ reviewDateSource = 'default 60%';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 调试日志
|
|
|
+ if (project.name?.includes('紫云') || project.name?.includes('自建')) {
|
|
|
+ console.log(`📸 [${project.name}] 小图对图时间计算:`, {
|
|
|
+ source: reviewDateSource,
|
|
|
+ reviewDate: realReviewDate.toLocaleString('zh-CN'),
|
|
|
+ demoday: project.demoday,
|
|
|
+ softDecorDeadline: projectData.phaseDeadlines?.softDecor?.deadline,
|
|
|
+ hasPhaseDeadlines: !!projectData.phaseDeadlines
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
// 4. 计算距离交付还有几天(使用真实日期)
|
|
|
@@ -709,59 +706,12 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
const currentStage = stageMap[project.currentStage || '建模阶段'] || 'model';
|
|
|
const stageName = project.currentStage || '建模阶段';
|
|
|
|
|
|
- // 7. 计算真实的阶段进度(基于 phaseDeadlines)
|
|
|
- let stageProgress = 50; // 默认值
|
|
|
- if (projectData.phaseDeadlines) {
|
|
|
- const phaseDeadlines = projectData.phaseDeadlines;
|
|
|
-
|
|
|
- // 根据当前阶段计算进度
|
|
|
- if (currentStage === 'model' && phaseDeadlines.modeling) {
|
|
|
- const start = phaseDeadlines.modeling.startDate instanceof Date
|
|
|
- ? phaseDeadlines.modeling.startDate
|
|
|
- : new Date(phaseDeadlines.modeling.startDate);
|
|
|
- const end = phaseDeadlines.modeling.deadline instanceof Date
|
|
|
- ? phaseDeadlines.modeling.deadline
|
|
|
- : new Date(phaseDeadlines.modeling.deadline);
|
|
|
- const total = end.getTime() - start.getTime();
|
|
|
- const elapsed = now.getTime() - start.getTime();
|
|
|
- stageProgress = total > 0 ? Math.min(100, Math.max(0, (elapsed / total) * 100)) : 50;
|
|
|
- } else if (currentStage === 'decoration' && phaseDeadlines.softDecor) {
|
|
|
- const start = phaseDeadlines.softDecor.startDate instanceof Date
|
|
|
- ? phaseDeadlines.softDecor.startDate
|
|
|
- : new Date(phaseDeadlines.softDecor.startDate);
|
|
|
- const end = phaseDeadlines.softDecor.deadline instanceof Date
|
|
|
- ? phaseDeadlines.softDecor.deadline
|
|
|
- : new Date(phaseDeadlines.softDecor.deadline);
|
|
|
- const total = end.getTime() - start.getTime();
|
|
|
- const elapsed = now.getTime() - start.getTime();
|
|
|
- stageProgress = total > 0 ? Math.min(100, Math.max(0, (elapsed / total) * 100)) : 50;
|
|
|
- } else if (currentStage === 'render' && phaseDeadlines.rendering) {
|
|
|
- const start = phaseDeadlines.rendering.startDate instanceof Date
|
|
|
- ? phaseDeadlines.rendering.startDate
|
|
|
- : new Date(phaseDeadlines.rendering.startDate);
|
|
|
- const end = phaseDeadlines.rendering.deadline instanceof Date
|
|
|
- ? phaseDeadlines.rendering.deadline
|
|
|
- : new Date(phaseDeadlines.rendering.deadline);
|
|
|
- const total = end.getTime() - start.getTime();
|
|
|
- const elapsed = now.getTime() - start.getTime();
|
|
|
- stageProgress = total > 0 ? Math.min(100, Math.max(0, (elapsed / total) * 100)) : 50;
|
|
|
- } else if (currentStage === 'delivery' && phaseDeadlines.postProcessing) {
|
|
|
- const start = phaseDeadlines.postProcessing.startDate instanceof Date
|
|
|
- ? phaseDeadlines.postProcessing.startDate
|
|
|
- : new Date(phaseDeadlines.postProcessing.startDate);
|
|
|
- const end = phaseDeadlines.postProcessing.deadline instanceof Date
|
|
|
- ? phaseDeadlines.postProcessing.deadline
|
|
|
- : new Date(phaseDeadlines.postProcessing.deadline);
|
|
|
- const total = end.getTime() - start.getTime();
|
|
|
- const elapsed = now.getTime() - start.getTime();
|
|
|
- stageProgress = total > 0 ? Math.min(100, Math.max(0, (elapsed / total) * 100)) : 50;
|
|
|
- }
|
|
|
- } else {
|
|
|
- // 降级:使用整体项目进度
|
|
|
- const totalDuration = realEndDate.getTime() - realStartDate.getTime();
|
|
|
- const elapsed = now.getTime() - realStartDate.getTime();
|
|
|
- stageProgress = totalDuration > 0 ? Math.min(100, Math.max(0, (elapsed / totalDuration) * 100)) : 50;
|
|
|
- }
|
|
|
+ // 7. 🆕 阶段任务完成度(由时间轴组件的 getProjectCompletionRate 计算)
|
|
|
+ // ✅ 重要变更:进度条现在表示"任务完成度"而不是"时间百分比"
|
|
|
+ // - 时间轴组件会优先使用交付物完成率(overallCompletionRate)
|
|
|
+ // - 若无交付物数据,则根据 phaseDeadlines.status 推断任务完成度
|
|
|
+ // - stageProgress 保留作为兼容字段,但已弃用
|
|
|
+ let stageProgress = 50; // 默认兼容值(实际进度由时间轴组件计算)
|
|
|
|
|
|
// 8. 检查是否停滞(基于 updatedAt)
|
|
|
let isStalled = false;
|
|
|
@@ -790,76 +740,24 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
}
|
|
|
|
|
|
// 11. 获取或生成阶段截止时间数据
|
|
|
- let phaseDeadlines = projectData.phaseDeadlines;
|
|
|
-
|
|
|
- // 如果项目没有阶段数据,从交付日期往前推算
|
|
|
- if (!phaseDeadlines && realEndDate) {
|
|
|
- const deliveryTime = realEndDate.getTime();
|
|
|
- const startTime = realStartDate.getTime();
|
|
|
- const totalDays = Math.ceil((deliveryTime - startTime) / (24 * 60 * 60 * 1000));
|
|
|
-
|
|
|
- // 按比例分配:建模30%,软装25%,渲染30%,后期15%
|
|
|
- const modelingDays = Math.ceil(totalDays * 0.3);
|
|
|
- const softDecorDays = Math.ceil(totalDays * 0.25);
|
|
|
- const renderingDays = Math.ceil(totalDays * 0.3);
|
|
|
- const postProcessDays = totalDays - modelingDays - softDecorDays - renderingDays;
|
|
|
-
|
|
|
- let currentDate = new Date(startTime);
|
|
|
-
|
|
|
- const modelingDeadline = new Date(currentDate);
|
|
|
- modelingDeadline.setDate(modelingDeadline.getDate() + modelingDays);
|
|
|
-
|
|
|
- currentDate = new Date(modelingDeadline);
|
|
|
- const softDecorDeadline = new Date(currentDate);
|
|
|
- softDecorDeadline.setDate(softDecorDeadline.getDate() + softDecorDays);
|
|
|
-
|
|
|
- currentDate = new Date(softDecorDeadline);
|
|
|
- const renderingDeadline = new Date(currentDate);
|
|
|
- renderingDeadline.setDate(renderingDeadline.getDate() + renderingDays);
|
|
|
-
|
|
|
- currentDate = new Date(renderingDeadline);
|
|
|
- const postProcessingDeadline = new Date(currentDate);
|
|
|
- postProcessingDeadline.setDate(postProcessingDeadline.getDate() + postProcessDays);
|
|
|
-
|
|
|
- // 更新对图时间(软装和渲染之间)
|
|
|
- const reviewTime = softDecorDeadline.getTime() + (renderingDeadline.getTime() - softDecorDeadline.getTime()) * 0.5;
|
|
|
- realReviewDate = new Date(reviewTime);
|
|
|
- realReviewDate.setHours(14, 0, 0, 0);
|
|
|
-
|
|
|
- phaseDeadlines = {
|
|
|
- modeling: {
|
|
|
- startDate: realStartDate,
|
|
|
- deadline: modelingDeadline,
|
|
|
- estimatedDays: modelingDays,
|
|
|
- status: now.getTime() >= modelingDeadline.getTime() ? 'completed' :
|
|
|
- now.getTime() >= realStartDate.getTime() ? 'in_progress' : 'not_started',
|
|
|
- priority: 'high'
|
|
|
- },
|
|
|
- softDecor: {
|
|
|
- startDate: modelingDeadline,
|
|
|
- deadline: softDecorDeadline,
|
|
|
- estimatedDays: softDecorDays,
|
|
|
- status: now.getTime() >= softDecorDeadline.getTime() ? 'completed' :
|
|
|
- now.getTime() >= modelingDeadline.getTime() ? 'in_progress' : 'not_started',
|
|
|
- priority: 'medium'
|
|
|
- },
|
|
|
- rendering: {
|
|
|
- startDate: softDecorDeadline,
|
|
|
- deadline: renderingDeadline,
|
|
|
- estimatedDays: renderingDays,
|
|
|
- status: now.getTime() >= renderingDeadline.getTime() ? 'completed' :
|
|
|
- now.getTime() >= softDecorDeadline.getTime() ? 'in_progress' : 'not_started',
|
|
|
- priority: 'high'
|
|
|
- },
|
|
|
- postProcessing: {
|
|
|
- startDate: renderingDeadline,
|
|
|
- deadline: postProcessingDeadline,
|
|
|
- estimatedDays: postProcessDays,
|
|
|
- status: now.getTime() >= postProcessingDeadline.getTime() ? 'completed' :
|
|
|
- now.getTime() >= renderingDeadline.getTime() ? 'in_progress' : 'not_started',
|
|
|
- priority: 'medium'
|
|
|
+ let phaseDeadlines: PhaseDeadlines | undefined = projectData.phaseDeadlines;
|
|
|
+ if (!phaseDeadlines) {
|
|
|
+ phaseDeadlines = generatePhaseDeadlines(realStartDate, realEndDate);
|
|
|
+ }
|
|
|
+ if (phaseDeadlines) {
|
|
|
+ (Object.keys(phaseDeadlines) as PhaseName[]).forEach((phaseKey) => {
|
|
|
+ const info = phaseDeadlines![phaseKey];
|
|
|
+ if (!info) return;
|
|
|
+ const phaseStart = normalizeDateInput(info.startDate, realStartDate);
|
|
|
+ const phaseEnd = normalizeDateInput(info.deadline, realEndDate);
|
|
|
+ if (now >= phaseEnd) {
|
|
|
+ info.status = 'completed';
|
|
|
+ } else if (now >= phaseStart) {
|
|
|
+ info.status = 'in_progress';
|
|
|
+ } else {
|
|
|
+ info.status = info.status || 'not_started';
|
|
|
}
|
|
|
- };
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
// 12. 获取空间和客户信息
|
|
|
@@ -892,7 +790,7 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
|
|
|
// 更新缓存
|
|
|
this.timelineDataCache = this.projectTimelineData;
|
|
|
- this.lastDesignerWorkloadMapSize = currentSize;
|
|
|
+ this.lastDesignerWorkloadMapSize = totalProjectsInMap;
|
|
|
|
|
|
console.log(`📊 convertToProjectTimeline 完成: 转换了 ${this.projectTimelineData.length} 个项目`);
|
|
|
if (this.projectTimelineData.length > 0) {
|