|
@@ -68,6 +68,7 @@ interface EmployeeDetail {
|
|
|
name: string;
|
|
name: string;
|
|
|
currentProjects: number; // 当前负责项目数
|
|
currentProjects: number; // 当前负责项目数
|
|
|
projectNames: string[]; // 项目名称列表(用于显示)
|
|
projectNames: string[]; // 项目名称列表(用于显示)
|
|
|
|
|
+ projectData: Array<{ id: string; name: string }>; // 项目数据(包含ID和名称,用于跳转)
|
|
|
leaveRecords: LeaveRecord[]; // 未来7天请假记录
|
|
leaveRecords: LeaveRecord[]; // 未来7天请假记录
|
|
|
redMarkExplanation: string; // 红色标记说明
|
|
redMarkExplanation: string; // 红色标记说明
|
|
|
}
|
|
}
|
|
@@ -85,7 +86,6 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
projects: Project[] = [];
|
|
projects: Project[] = [];
|
|
|
filteredProjects: Project[] = [];
|
|
filteredProjects: Project[] = [];
|
|
|
todoTasks: TodoTask[] = [];
|
|
todoTasks: TodoTask[] = [];
|
|
|
- overdueProjects: Project[] = [];
|
|
|
|
|
urgentPinnedProjects: Project[] = [];
|
|
urgentPinnedProjects: Project[] = [];
|
|
|
showAlert: boolean = false;
|
|
showAlert: boolean = false;
|
|
|
selectedProjectId: string = '';
|
|
selectedProjectId: string = '';
|
|
@@ -93,6 +93,9 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
// 真实设计师数据(从fmode-ng获取)
|
|
// 真实设计师数据(从fmode-ng获取)
|
|
|
realDesigners: any[] = [];
|
|
realDesigners: any[] = [];
|
|
|
|
|
|
|
|
|
|
+ // 设计师工作量映射(从 ProjectTeam 表)
|
|
|
|
|
+ designerWorkloadMap: Map<string, any[]> = new Map(); // designerId/name -> projects[]
|
|
|
|
|
+
|
|
|
// 智能推荐相关
|
|
// 智能推荐相关
|
|
|
showSmartMatch: boolean = false;
|
|
showSmartMatch: boolean = false;
|
|
|
selectedProject: any = null;
|
|
selectedProject: any = null;
|
|
@@ -110,7 +113,6 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
private readonly MAX_SUGGESTIONS = 8; // 建议最大条数
|
|
private readonly MAX_SUGGESTIONS = 8; // 建议最大条数
|
|
|
private isSearchFocused: boolean = false; // 是否处于输入聚焦态
|
|
private isSearchFocused: boolean = false; // 是否处于输入聚焦态
|
|
|
// 新增:临期项目与筛选状态
|
|
// 新增:临期项目与筛选状态
|
|
|
- dueSoonProjects: Project[] = [];
|
|
|
|
|
selectedType: 'all' | 'soft' | 'hard' = 'all';
|
|
selectedType: 'all' | 'soft' | 'hard' = 'all';
|
|
|
selectedUrgency: 'all' | 'high' | 'medium' | 'low' = 'all';
|
|
selectedUrgency: 'all' | 'high' | 'medium' | 'low' = 'all';
|
|
|
selectedStatus: 'all' | 'progress' | 'completed' | 'overdue' | 'pendingApproval' | 'pendingAssignment' | 'dueSoon' = 'all';
|
|
selectedStatus: 'all' | 'progress' | 'completed' | 'overdue' | 'pendingApproval' | 'pendingAssignment' | 'dueSoon' = 'all';
|
|
@@ -228,9 +230,142 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
avgRating: d.tags.history.avgRating || 0,
|
|
avgRating: d.tags.history.avgRating || 0,
|
|
|
experience: 0 // 暂无此字段
|
|
experience: 0 // 暂无此字段
|
|
|
}));
|
|
}));
|
|
|
- console.log('✅ 加载设计师数据成功:', this.realDesigners.length, '人');
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 加载设计师的实际工作量
|
|
|
|
|
+ await this.loadDesignerWorkload();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('加载设计师数据失败:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 🔧 从 ProjectTeam 表加载每个设计师的实际工作量
|
|
|
|
|
+ */
|
|
|
|
|
+ async loadDesignerWorkload(): Promise<void> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const Parse = await import('fmode-ng/parse').then(m => m.FmodeParse.with('nova'));
|
|
|
|
|
+
|
|
|
|
|
+ // 查询所有 ProjectTeam 记录
|
|
|
|
|
+ const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
|
|
|
|
|
+
|
|
|
|
|
+ // 先查询当前公司的所有项目
|
|
|
|
|
+ const projectQuery = new Parse.Query('Project');
|
|
|
|
|
+ projectQuery.equalTo('company', cid);
|
|
|
|
|
+ projectQuery.notEqualTo('isDeleted', true);
|
|
|
|
|
+
|
|
|
|
|
+ // 查询当前公司项目的 ProjectTeam
|
|
|
|
|
+ const teamQuery = new Parse.Query('ProjectTeam');
|
|
|
|
|
+ teamQuery.matchesQuery('project', projectQuery);
|
|
|
|
|
+ teamQuery.notEqualTo('isDeleted', true);
|
|
|
|
|
+ teamQuery.include('project');
|
|
|
|
|
+ teamQuery.include('profile');
|
|
|
|
|
+ teamQuery.limit(1000);
|
|
|
|
|
+
|
|
|
|
|
+ const teamRecords = await teamQuery.find();
|
|
|
|
|
+
|
|
|
|
|
+ // 如果 ProjectTeam 表为空,使用降级方案
|
|
|
|
|
+ if (teamRecords.length === 0) {
|
|
|
|
|
+ await this.loadDesignerWorkloadFromProjects();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 构建设计师工作量映射
|
|
|
|
|
+ this.designerWorkloadMap.clear();
|
|
|
|
|
+
|
|
|
|
|
+ teamRecords.forEach((record: any) => {
|
|
|
|
|
+ const profile = record.get('profile');
|
|
|
|
|
+ const project = record.get('project');
|
|
|
|
|
+
|
|
|
|
|
+ if (!profile || !project) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const profileId = profile.id;
|
|
|
|
|
+ const profileName = profile.get('name') || profile.get('user')?.get?.('name') || `设计师-${profileId.slice(-4)}`;
|
|
|
|
|
+
|
|
|
|
|
+ // 提取项目信息
|
|
|
|
|
+ const projectData = {
|
|
|
|
|
+ id: project.id,
|
|
|
|
|
+ name: project.get('title') || '未命名项目',
|
|
|
|
|
+ status: project.get('status') || '进行中',
|
|
|
|
|
+ currentStage: project.get('currentStage') || '未知阶段',
|
|
|
|
|
+ deadline: project.get('deadline'),
|
|
|
|
|
+ createdAt: project.get('createdAt'),
|
|
|
|
|
+ designerName: profileName // 设置为组员的名字
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 添加到映射 (by ID)
|
|
|
|
|
+ if (!this.designerWorkloadMap.has(profileId)) {
|
|
|
|
|
+ this.designerWorkloadMap.set(profileId, []);
|
|
|
|
|
+ }
|
|
|
|
|
+ this.designerWorkloadMap.get(profileId)!.push(projectData);
|
|
|
|
|
+
|
|
|
|
|
+ // 同时建立 name -> projects 的映射(用于甘特图)
|
|
|
|
|
+ if (!this.designerWorkloadMap.has(profileName)) {
|
|
|
|
|
+ this.designerWorkloadMap.set(profileName, []);
|
|
|
|
|
+ }
|
|
|
|
|
+ this.designerWorkloadMap.get(profileName)!.push(projectData);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('加载设计师工作量失败:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 🔧 降级方案:从 Project.assignee 统计工作量
|
|
|
|
|
+ * 当 ProjectTeam 表为空时使用
|
|
|
|
|
+ */
|
|
|
|
|
+ async loadDesignerWorkloadFromProjects(): Promise<void> {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const Parse = await import('fmode-ng/parse').then(m => m.FmodeParse.with('nova'));
|
|
|
|
|
+ const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
|
|
|
|
|
+
|
|
|
|
|
+ // 查询所有项目
|
|
|
|
|
+ const projectQuery = new Parse.Query('Project');
|
|
|
|
|
+ projectQuery.equalTo('company', cid);
|
|
|
|
|
+ projectQuery.equalTo('isDeleted', false);
|
|
|
|
|
+ projectQuery.include('assignee');
|
|
|
|
|
+ projectQuery.include('department');
|
|
|
|
|
+ projectQuery.limit(1000);
|
|
|
|
|
+
|
|
|
|
|
+ const projects = await projectQuery.find();
|
|
|
|
|
+
|
|
|
|
|
+ // 构建设计师工作量映射
|
|
|
|
|
+ this.designerWorkloadMap.clear();
|
|
|
|
|
+
|
|
|
|
|
+ projects.forEach((project: any) => {
|
|
|
|
|
+ const assignee = project.get('assignee');
|
|
|
|
|
+ if (!assignee) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 只统计组员角色的项目
|
|
|
|
|
+ const assigneeRole = assignee.get('roleName');
|
|
|
|
|
+ if (assigneeRole !== '组员') {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const assigneeName = assignee.get('name') || assignee.get('user')?.get?.('name') || `设计师-${assignee.id.slice(-4)}`;
|
|
|
|
|
+
|
|
|
|
|
+ // 提取项目信息
|
|
|
|
|
+ const projectData = {
|
|
|
|
|
+ id: project.id,
|
|
|
|
|
+ name: project.get('title') || '未命名项目',
|
|
|
|
|
+ status: project.get('status') || '进行中',
|
|
|
|
|
+ currentStage: project.get('currentStage') || '未知阶段',
|
|
|
|
|
+ deadline: project.get('deadline'),
|
|
|
|
|
+ createdAt: project.get('createdAt'),
|
|
|
|
|
+ designerName: assigneeName
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 添加到映射
|
|
|
|
|
+ if (!this.designerWorkloadMap.has(assigneeName)) {
|
|
|
|
|
+ this.designerWorkloadMap.set(assigneeName, []);
|
|
|
|
|
+ }
|
|
|
|
|
+ this.designerWorkloadMap.get(assigneeName)!.push(projectData);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- console.error('❌ 加载设计师数据失败:', error);
|
|
|
|
|
|
|
+ console.error('[降级方案] 加载工作量失败:', error);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -244,14 +379,12 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
// 如果有真实数据,使用真实数据
|
|
// 如果有真实数据,使用真实数据
|
|
|
if (realProjects && realProjects.length > 0) {
|
|
if (realProjects && realProjects.length > 0) {
|
|
|
this.projects = realProjects;
|
|
this.projects = realProjects;
|
|
|
- console.log('✅ 加载真实项目数据成功:', this.projects.length, '个项目');
|
|
|
|
|
} else {
|
|
} else {
|
|
|
- // 如果没有真实数据,使用模拟数据(便于开发测试)
|
|
|
|
|
- console.warn('⚠️ 未找到真实项目数据,使用模拟数据');
|
|
|
|
|
|
|
+ // 如果没有真实数据,使用模拟数据
|
|
|
this.projects = this.getMockProjects();
|
|
this.projects = this.getMockProjects();
|
|
|
}
|
|
}
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- console.error('❌ 加载项目数据失败,使用模拟数据:', error);
|
|
|
|
|
|
|
+ console.error('加载项目数据失败:', error);
|
|
|
this.projects = this.getMockProjects();
|
|
this.projects = this.getMockProjects();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -537,15 +670,13 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
return { ...p, deadline, createdAt } as Project;
|
|
return { ...p, deadline, createdAt } as Project;
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- // 筛选超期与临期项目
|
|
|
|
|
- this.overdueProjects = this.projects.filter(project => project.isOverdue);
|
|
|
|
|
- this.dueSoonProjects = this.projects.filter(project => project.dueSoon && !project.isOverdue);
|
|
|
|
|
|
|
+ // 筛选结果初始化为全部项目
|
|
|
this.filteredProjects = [...this.projects];
|
|
this.filteredProjects = [...this.projects];
|
|
|
|
|
|
|
|
// 供筛选用的设计师列表
|
|
// 供筛选用的设计师列表
|
|
|
this.designers = Array.from(new Set(this.projects.map(p => p.designerName).filter(n => !!n)));
|
|
this.designers = Array.from(new Set(this.projects.map(p => p.designerName).filter(n => !!n)));
|
|
|
|
|
|
|
|
- // 显示超期提醒
|
|
|
|
|
|
|
+ // 显示超期提醒(使用 getter)
|
|
|
if (this.overdueProjects.length > 0) {
|
|
if (this.overdueProjects.length > 0) {
|
|
|
this.showAlert = true;
|
|
this.showAlert = true;
|
|
|
}
|
|
}
|
|
@@ -946,9 +1077,8 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
this.recommendations = await this.designerService.getRecommendedDesigners(project, this.realDesigners);
|
|
this.recommendations = await this.designerService.getRecommendedDesigners(project, this.realDesigners);
|
|
|
- console.log('✅ 智能推荐结果:', this.recommendations);
|
|
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- console.error('❌ 智能推荐失败:', error);
|
|
|
|
|
|
|
+ console.error('智能推荐失败:', error);
|
|
|
this.recommendations = [];
|
|
this.recommendations = [];
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -1848,9 +1978,9 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
if (this.workloadGanttScale === 'week') {
|
|
if (this.workloadGanttScale === 'week') {
|
|
|
// 周视图:显示未来7天
|
|
// 周视图:显示未来7天
|
|
|
xMin = todayTs;
|
|
xMin = todayTs;
|
|
|
- xMax = todayTs + 7 * DAY - 1;
|
|
|
|
|
|
|
+ xMax = todayTs + 7 * DAY;
|
|
|
xSplitNumber = 7;
|
|
xSplitNumber = 7;
|
|
|
- xLabelFormatter = (val) => {
|
|
|
|
|
|
|
+ xLabelFormatter = (val: any) => {
|
|
|
const date = new Date(val);
|
|
const date = new Date(val);
|
|
|
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
|
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
|
|
|
return `${date.getMonth() + 1}/${date.getDate()}\n${weekDays[date.getDay()]}`;
|
|
return `${date.getMonth() + 1}/${date.getDate()}\n${weekDays[date.getDay()]}`;
|
|
@@ -1858,26 +1988,23 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
} else {
|
|
} else {
|
|
|
// 月视图:显示未来30天
|
|
// 月视图:显示未来30天
|
|
|
xMin = todayTs;
|
|
xMin = todayTs;
|
|
|
- xMax = todayTs + 30 * DAY - 1;
|
|
|
|
|
|
|
+ xMax = todayTs + 30 * DAY;
|
|
|
xSplitNumber = 30;
|
|
xSplitNumber = 30;
|
|
|
- xLabelFormatter = (val) => {
|
|
|
|
|
|
|
+ xLabelFormatter = (val: any) => {
|
|
|
const date = new Date(val);
|
|
const date = new Date(val);
|
|
|
return `${date.getMonth() + 1}/${date.getDate()}`;
|
|
return `${date.getMonth() + 1}/${date.getDate()}`;
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // ✅ 获取所有真实设计师(优先使用realDesigners)
|
|
|
|
|
|
|
+ // 获取所有真实设计师
|
|
|
let designers: string[] = [];
|
|
let designers: string[] = [];
|
|
|
|
|
|
|
|
if (this.realDesigners && this.realDesigners.length > 0) {
|
|
if (this.realDesigners && this.realDesigners.length > 0) {
|
|
|
- // 使用真实的设计师列表
|
|
|
|
|
designers = this.realDesigners.map(d => d.name);
|
|
designers = this.realDesigners.map(d => d.name);
|
|
|
- console.log('✅ 使用真实设计师列表:', designers.length, '人');
|
|
|
|
|
} else {
|
|
} else {
|
|
|
- // 降级:从已分配的项目中提取设计师(过滤掉"未分配")
|
|
|
|
|
|
|
+ // 降级:从已分配的项目中提取设计师
|
|
|
const assigned = this.filteredProjects.filter(p => p.designerName && p.designerName !== '未分配');
|
|
const assigned = this.filteredProjects.filter(p => p.designerName && p.designerName !== '未分配');
|
|
|
designers = Array.from(new Set(assigned.map(p => p.designerName)));
|
|
designers = Array.from(new Set(assigned.map(p => p.designerName)));
|
|
|
- console.warn('⚠️ 使用项目中提取的设计师列表:', designers.length, '人');
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (designers.length === 0) {
|
|
if (designers.length === 0) {
|
|
@@ -1896,10 +2023,7 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 获取所有已分配的项目(过滤掉"未分配")
|
|
|
|
|
- const assigned = this.filteredProjects.filter(p => p.designerName && p.designerName !== '未分配');
|
|
|
|
|
-
|
|
|
|
|
- // 计算每个设计师的每日工作状态
|
|
|
|
|
|
|
+ // 🔧 使用 ProjectTeam 表的数据(实际执行人)
|
|
|
const workloadByDesigner: Record<string, any[]> = {};
|
|
const workloadByDesigner: Record<string, any[]> = {};
|
|
|
designers.forEach(name => {
|
|
designers.forEach(name => {
|
|
|
workloadByDesigner[name] = [];
|
|
workloadByDesigner[name] = [];
|
|
@@ -1908,7 +2032,7 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
// 计算每个设计师的总负载(用于排序)
|
|
// 计算每个设计师的总负载(用于排序)
|
|
|
const designerTotalLoad: Record<string, number> = {};
|
|
const designerTotalLoad: Record<string, number> = {};
|
|
|
designers.forEach(name => {
|
|
designers.forEach(name => {
|
|
|
- const projects = assigned.filter(p => p.designerName === name);
|
|
|
|
|
|
|
+ const projects = this.designerWorkloadMap.get(name) || [];
|
|
|
designerTotalLoad[name] = projects.length;
|
|
designerTotalLoad[name] = projects.length;
|
|
|
});
|
|
});
|
|
|
|
|
|
|
@@ -1919,7 +2043,7 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
|
|
|
|
|
// 为每个设计师生成时间段数据
|
|
// 为每个设计师生成时间段数据
|
|
|
sortedDesigners.forEach((designerName, yIndex) => {
|
|
sortedDesigners.forEach((designerName, yIndex) => {
|
|
|
- const designerProjects = assigned.filter(p => p.designerName === designerName);
|
|
|
|
|
|
|
+ const designerProjects = this.designerWorkloadMap.get(designerName) || [];
|
|
|
|
|
|
|
|
// 计算每一天的状态
|
|
// 计算每一天的状态
|
|
|
const days = this.workloadGanttScale === 'week' ? 7 : 30;
|
|
const days = this.workloadGanttScale === 'week' ? 7 : 30;
|
|
@@ -1929,8 +2053,31 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
|
|
|
|
|
// 查找该天有哪些项目
|
|
// 查找该天有哪些项目
|
|
|
const dayProjects = designerProjects.filter(p => {
|
|
const dayProjects = designerProjects.filter(p => {
|
|
|
- const pStart = p.createdAt ? new Date(p.createdAt).getTime() : dayStart;
|
|
|
|
|
|
|
+ // 如果项目没有 deadline,则认为项目一直在进行中
|
|
|
|
|
+ if (!p.deadline) {
|
|
|
|
|
+ return true; // 没有截止日期的项目始终显示
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const pEnd = new Date(p.deadline).getTime();
|
|
const pEnd = new Date(p.deadline).getTime();
|
|
|
|
|
+
|
|
|
|
|
+ // 检查时间是否有效
|
|
|
|
|
+ if (isNaN(pEnd)) {
|
|
|
|
|
+ return true; // 如果截止日期无效,认为项目在进行中
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 🔧 修复:对于进行中的项目(状态不是"已完成"),即使过期也显示
|
|
|
|
|
+ // 这样可以在甘特图中看到超期的项目
|
|
|
|
|
+ const isCompleted = p.status === '已完成' || p.status === '已交付';
|
|
|
|
|
+ if (!isCompleted) {
|
|
|
|
|
+ // 进行中的项目:只要截止日期还没到很久之前(比如30天前),就显示
|
|
|
|
|
+ const thirtyDaysAgo = todayTs - 30 * DAY;
|
|
|
|
|
+ if (pEnd >= thirtyDaysAgo) {
|
|
|
|
|
+ return true; // 30天内的项目都显示
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 已完成的项目:正常时间范围判断
|
|
|
|
|
+ const pStart = p.createdAt ? new Date(p.createdAt).getTime() : dayStart;
|
|
|
return !(pEnd < dayStart || pStart > dayEnd);
|
|
return !(pEnd < dayStart || pStart > dayEnd);
|
|
|
});
|
|
});
|
|
|
|
|
|
|
@@ -2043,17 +2190,29 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
bottom: 60
|
|
bottom: 60
|
|
|
},
|
|
},
|
|
|
xAxis: {
|
|
xAxis: {
|
|
|
- type: 'value',
|
|
|
|
|
|
|
+ type: 'time',
|
|
|
min: xMin,
|
|
min: xMin,
|
|
|
max: xMax,
|
|
max: xMax,
|
|
|
- splitNumber: xSplitNumber,
|
|
|
|
|
|
|
+ boundaryGap: false,
|
|
|
axisLine: { lineStyle: { color: '#e5e7eb' } },
|
|
axisLine: { lineStyle: { color: '#e5e7eb' } },
|
|
|
axisLabel: {
|
|
axisLabel: {
|
|
|
color: '#6b7280',
|
|
color: '#6b7280',
|
|
|
formatter: xLabelFormatter,
|
|
formatter: xLabelFormatter,
|
|
|
- interval: this.workloadGanttScale === 'week' ? 0 : 4
|
|
|
|
|
|
|
+ interval: 0,
|
|
|
|
|
+ rotate: this.workloadGanttScale === 'week' ? 0 : 45,
|
|
|
|
|
+ showMinLabel: true,
|
|
|
|
|
+ showMaxLabel: true
|
|
|
},
|
|
},
|
|
|
- splitLine: { lineStyle: { color: '#f1f5f9' } }
|
|
|
|
|
|
|
+ axisTick: {
|
|
|
|
|
+ alignWithLabel: true,
|
|
|
|
|
+ interval: 0
|
|
|
|
|
+ },
|
|
|
|
|
+ splitLine: {
|
|
|
|
|
+ show: true,
|
|
|
|
|
+ lineStyle: { color: '#f1f5f9' }
|
|
|
|
|
+ },
|
|
|
|
|
+ splitNumber: xSplitNumber,
|
|
|
|
|
+ minInterval: DAY
|
|
|
},
|
|
},
|
|
|
yAxis: {
|
|
yAxis: {
|
|
|
type: 'category',
|
|
type: 'category',
|
|
@@ -2143,14 +2302,65 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
|
|
|
|
|
// 新增:阶段到核心阶段的映射
|
|
// 新增:阶段到核心阶段的映射
|
|
|
private mapStageToCorePhase(stageId: string): 'order' | 'requirements' | 'delivery' | 'aftercare' {
|
|
private mapStageToCorePhase(stageId: string): 'order' | 'requirements' | 'delivery' | 'aftercare' {
|
|
|
- // 订单分配:立项初期
|
|
|
|
|
- if (stageId === 'pendingApproval' || stageId === 'pendingAssignment') return 'order';
|
|
|
|
|
- // 确认需求:需求沟通 + 方案规划
|
|
|
|
|
- if (stageId === 'requirement' || stageId === 'planning') return 'requirements';
|
|
|
|
|
- // 交付执行:制作与评审修订过程
|
|
|
|
|
- if (stageId === 'modeling' || stageId === 'rendering' || stageId === 'postProduction' || stageId === 'review' || stageId === 'revision') return 'delivery';
|
|
|
|
|
- // 售后:交付完成后的跟进(当前数据以交付完成代表进入售后)
|
|
|
|
|
- return 'aftercare';
|
|
|
|
|
|
|
+ if (!stageId) return 'order'; // 空值默认为订单分配
|
|
|
|
|
+
|
|
|
|
|
+ // 标准化阶段名称(去除空格,转小写)
|
|
|
|
|
+ const normalizedStage = stageId.trim().toLowerCase();
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 订单分配阶段(英文ID + 中文名称)
|
|
|
|
|
+ if (normalizedStage === 'order' ||
|
|
|
|
|
+ normalizedStage === 'pendingapproval' ||
|
|
|
|
|
+ normalizedStage === 'pendingassignment' ||
|
|
|
|
|
+ normalizedStage === '订单分配' ||
|
|
|
|
|
+ normalizedStage === '待审批' ||
|
|
|
|
|
+ normalizedStage === '待分配') {
|
|
|
|
|
+ return 'order';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 确认需求阶段(英文ID + 中文名称)
|
|
|
|
|
+ if (normalizedStage === 'requirements' ||
|
|
|
|
|
+ normalizedStage === 'requirement' ||
|
|
|
|
|
+ normalizedStage === 'planning' ||
|
|
|
|
|
+ normalizedStage === '确认需求' ||
|
|
|
|
|
+ normalizedStage === '需求沟通' ||
|
|
|
|
|
+ normalizedStage === '方案规划') {
|
|
|
|
|
+ return 'requirements';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 交付执行阶段(英文ID + 中文名称)
|
|
|
|
|
+ if (normalizedStage === 'delivery' ||
|
|
|
|
|
+ normalizedStage === 'modeling' ||
|
|
|
|
|
+ normalizedStage === 'rendering' ||
|
|
|
|
|
+ normalizedStage === 'postproduction' ||
|
|
|
|
|
+ normalizedStage === 'review' ||
|
|
|
|
|
+ normalizedStage === 'revision' ||
|
|
|
|
|
+ normalizedStage === '交付执行' ||
|
|
|
|
|
+ normalizedStage === '建模' ||
|
|
|
|
|
+ normalizedStage === '建模阶段' ||
|
|
|
|
|
+ normalizedStage === '渲染' ||
|
|
|
|
|
+ normalizedStage === '渲染阶段' ||
|
|
|
|
|
+ normalizedStage === '后期制作' ||
|
|
|
|
|
+ normalizedStage === '评审' ||
|
|
|
|
|
+ normalizedStage === '修改' ||
|
|
|
|
|
+ normalizedStage === '修订') {
|
|
|
|
|
+ return 'delivery';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 售后归档阶段(英文ID + 中文名称)
|
|
|
|
|
+ if (normalizedStage === 'aftercare' ||
|
|
|
|
|
+ normalizedStage === 'completed' ||
|
|
|
|
|
+ normalizedStage === 'archived' ||
|
|
|
|
|
+ normalizedStage === '售后归档' ||
|
|
|
|
|
+ normalizedStage === '售后' ||
|
|
|
|
|
+ normalizedStage === '归档' ||
|
|
|
|
|
+ normalizedStage === '已完成' ||
|
|
|
|
|
+ normalizedStage === '已交付') {
|
|
|
|
|
+ return 'aftercare';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 未匹配的阶段:默认为交付执行(因为大部分时间项目都在执行中)
|
|
|
|
|
+ console.warn(`⚠️ 未识别的阶段: "${stageId}" → 默认归类为交付执行`);
|
|
|
|
|
+ return 'delivery';
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 新增:获取核心阶段的项目
|
|
// 新增:获取核心阶段的项目
|
|
@@ -2168,16 +2378,34 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
return this.getProjectsByStage(stageId).length;
|
|
return this.getProjectsByStage(stageId).length;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 待审批项目:currentStage === 'pendingApproval'
|
|
|
|
|
|
|
+ // 🔥 已延期项目
|
|
|
|
|
+ get overdueProjects(): Project[] {
|
|
|
|
|
+ return this.projects.filter(p => p.isOverdue);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ⏳ 临期项目(3天内)
|
|
|
|
|
+ get dueSoonProjects(): Project[] {
|
|
|
|
|
+ return this.projects.filter(p => p.dueSoon && !p.isOverdue);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 📋 待审批项目(支持中文和英文阶段名称)
|
|
|
get pendingApprovalProjects(): Project[] {
|
|
get pendingApprovalProjects(): Project[] {
|
|
|
- const src = (this.filteredProjects && this.filteredProjects.length) ? this.filteredProjects : this.projects;
|
|
|
|
|
- return src.filter(p => p.currentStage === 'pendingApproval');
|
|
|
|
|
|
|
+ return this.projects.filter(p => {
|
|
|
|
|
+ const stage = (p.currentStage || '').trim().toLowerCase();
|
|
|
|
|
+ return stage === 'pendingapproval' ||
|
|
|
|
|
+ stage === '待审批' ||
|
|
|
|
|
+ stage === '待确认';
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 待指派项目:currentStage === 'pendingAssignment'
|
|
|
|
|
|
|
+ // 🎯 待分配项目(支持中文和英文阶段名称)
|
|
|
get pendingAssignmentProjects(): Project[] {
|
|
get pendingAssignmentProjects(): Project[] {
|
|
|
- const src = (this.filteredProjects && this.filteredProjects.length) ? this.filteredProjects : this.projects;
|
|
|
|
|
- return src.filter(p => p.currentStage === 'pendingAssignment');
|
|
|
|
|
|
|
+ return this.projects.filter(p => {
|
|
|
|
|
+ const stage = (p.currentStage || '').trim().toLowerCase();
|
|
|
|
|
+ return stage === 'pendingassignment' ||
|
|
|
|
|
+ stage === '待分配' ||
|
|
|
|
|
+ stage === '订单分配';
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 智能推荐设计师
|
|
// 智能推荐设计师
|
|
@@ -2219,7 +2447,6 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
// 查看项目详情 - 跳转到纯净的项目详情页(无管理端UI)
|
|
// 查看项目详情 - 跳转到纯净的项目详情页(无管理端UI)
|
|
|
viewProjectDetails(projectId: string): void {
|
|
viewProjectDetails(projectId: string): void {
|
|
|
if (!projectId) {
|
|
if (!projectId) {
|
|
|
- console.warn('⚠️ 项目ID为空,无法跳转');
|
|
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -2227,10 +2454,7 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
|
|
const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
|
|
|
|
|
|
|
|
// 跳转到wxwork路由的项目详情页(纯净页面,无管理端侧边栏)
|
|
// 跳转到wxwork路由的项目详情页(纯净页面,无管理端侧边栏)
|
|
|
- // 路由格式:/wxwork/:cid/project/:projectId/order
|
|
|
|
|
this.router.navigate(['/wxwork', cid, 'project', projectId, 'order']);
|
|
this.router.navigate(['/wxwork', cid, 'project', projectId, 'order']);
|
|
|
-
|
|
|
|
|
- console.log('✅ 组长端跳转到纯净项目详情页:', projectId);
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 快速分配项目(增强:加入智能推荐)
|
|
// 快速分配项目(增强:加入智能推荐)
|
|
@@ -2346,10 +2570,17 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
|
|
|
|
|
// 生成员工详情数据
|
|
// 生成员工详情数据
|
|
|
private generateEmployeeDetail(employeeName: string): EmployeeDetail {
|
|
private generateEmployeeDetail(employeeName: string): EmployeeDetail {
|
|
|
- // 获取该员工负责的项目
|
|
|
|
|
- const employeeProjects = this.filteredProjects.filter(p => p.designerName === employeeName);
|
|
|
|
|
|
|
+ // 🔧 从 ProjectTeam 表获取该员工负责的项目
|
|
|
|
|
+ const employeeProjects = this.designerWorkloadMap.get(employeeName) || [];
|
|
|
const currentProjects = employeeProjects.length;
|
|
const currentProjects = employeeProjects.length;
|
|
|
- const projectNames = employeeProjects.slice(0, 3).map(p => p.name); // 最多显示3个项目名称
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 保存完整的项目数据(最多显示3个)
|
|
|
|
|
+ const projectData = employeeProjects.slice(0, 3).map(p => ({
|
|
|
|
|
+ id: p.id,
|
|
|
|
|
+ name: p.name
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ const projectNames = projectData.map(p => p.name); // 项目名称列表
|
|
|
|
|
|
|
|
// 获取该员工的请假记录(未来7天)
|
|
// 获取该员工的请假记录(未来7天)
|
|
|
const today = new Date();
|
|
const today = new Date();
|
|
@@ -2370,6 +2601,7 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
name: employeeName,
|
|
name: employeeName,
|
|
|
currentProjects,
|
|
currentProjects,
|
|
|
projectNames,
|
|
projectNames,
|
|
|
|
|
+ projectData,
|
|
|
leaveRecords: employeeLeaveRecords,
|
|
leaveRecords: employeeLeaveRecords,
|
|
|
redMarkExplanation
|
|
redMarkExplanation
|
|
|
};
|
|
};
|
|
@@ -2409,6 +2641,20 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
this.selectedEmployeeDetail = null;
|
|
this.selectedEmployeeDetail = null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 从员工详情面板跳转到项目详情
|
|
|
|
|
+ navigateToProjectFromPanel(projectId: string): void {
|
|
|
|
|
+ if (!projectId) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 关闭员工详情面板
|
|
|
|
|
+ this.closeEmployeeDetailPanel();
|
|
|
|
|
+
|
|
|
|
|
+ // 跳转到项目详情页(使用纯净的wxwork路由)
|
|
|
|
|
+ const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
|
|
|
|
|
+ this.router.navigate(['/wxwork', cid, 'project', projectId, 'order']);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// 获取请假类型显示文本
|
|
// 获取请假类型显示文本
|
|
|
getLeaveTypeText(leaveType?: string): string {
|
|
getLeaveTypeText(leaveType?: string): string {
|
|
|
const typeMap: Record<string, string> = {
|
|
const typeMap: Record<string, string> = {
|
|
@@ -2454,7 +2700,7 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// 检查项目繁忙情况,如果项目数>=3,也添加红色标记
|
|
// 检查项目繁忙情况,如果项目数>=3,也添加红色标记
|
|
|
- const employeeProjects = this.filteredProjects.filter(p => p.designerName === employeeName);
|
|
|
|
|
|
|
+ const employeeProjects = this.designerWorkloadMap.get(employeeName) || [];
|
|
|
if (employeeProjects.length >= 3) {
|
|
if (employeeProjects.length >= 3) {
|
|
|
// 在当前日期添加繁忙标记
|
|
// 在当前日期添加繁忙标记
|
|
|
const today = new Date();
|
|
const today = new Date();
|