|
|
@@ -5,6 +5,11 @@ import { Component, OnInit, OnDestroy, ElementRef, ViewChild } from '@angular/co
|
|
|
import { ProjectService } from '../../../services/project.service';
|
|
|
import { DesignerService } from '../services/designer.service';
|
|
|
import { WxworkAuth } from 'fmode-ng/core';
|
|
|
+import { ProjectIssueService, ProjectIssue, IssueStatus, IssuePriority, IssueType } from '../../../../modules/project/services/project-issue.service';
|
|
|
+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';
|
|
|
|
|
|
// 项目阶段定义
|
|
|
interface ProjectStage {
|
|
|
@@ -54,6 +59,26 @@ interface TodoTask {
|
|
|
targetId: string;
|
|
|
}
|
|
|
|
|
|
+// 新增:从问题板块映射的待办任务
|
|
|
+interface TodoTaskFromIssue {
|
|
|
+ id: string;
|
|
|
+ title: string;
|
|
|
+ description?: string;
|
|
|
+ priority: IssuePriority;
|
|
|
+ type: IssueType;
|
|
|
+ status: IssueStatus;
|
|
|
+ projectId: string;
|
|
|
+ projectName: string;
|
|
|
+ relatedSpace?: string;
|
|
|
+ relatedStage?: string;
|
|
|
+ assigneeName?: string;
|
|
|
+ creatorName?: string;
|
|
|
+ createdAt: Date;
|
|
|
+ updatedAt: Date;
|
|
|
+ dueDate?: Date;
|
|
|
+ tags?: string[];
|
|
|
+}
|
|
|
+
|
|
|
// 员工请假记录接口
|
|
|
interface LeaveRecord {
|
|
|
id: string;
|
|
|
@@ -98,7 +123,7 @@ declare const echarts: any;
|
|
|
@Component({
|
|
|
selector: 'app-dashboard',
|
|
|
standalone: true,
|
|
|
- imports: [CommonModule, FormsModule, RouterModule],
|
|
|
+ imports: [CommonModule, FormsModule, RouterModule, ProjectTimelineComponent, EmployeeDetailPanelComponent],
|
|
|
templateUrl: './dashboard.html',
|
|
|
styleUrl: './dashboard.scss'
|
|
|
})
|
|
|
@@ -114,6 +139,12 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
showAlert: boolean = false;
|
|
|
selectedProjectId: string = '';
|
|
|
|
|
|
+ // 新增:从问题板块加载的待办任务
|
|
|
+ todoTasksFromIssues: TodoTaskFromIssue[] = [];
|
|
|
+ loadingTodoTasks: boolean = false;
|
|
|
+ todoTaskError: string = '';
|
|
|
+ private todoTaskRefreshTimer: any;
|
|
|
+
|
|
|
// 新增:当前用户信息
|
|
|
currentUser = {
|
|
|
name: '组长',
|
|
|
@@ -204,6 +235,15 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
selectedDayProjects: Array<{ id: string; name: string; deadline?: Date }> = [];
|
|
|
selectedDate: Date | null = null;
|
|
|
|
|
|
+ // 当前员工日历相关数据(用于切换月份)
|
|
|
+ private currentEmployeeName: string = '';
|
|
|
+ private currentEmployeeProjects: any[] = [];
|
|
|
+
|
|
|
+ // 项目时间轴数据
|
|
|
+ projectTimelineData: ProjectTimeline[] = [];
|
|
|
+ private timelineDataCache: ProjectTimeline[] = [];
|
|
|
+ private lastDesignerWorkloadMapSize: number = 0;
|
|
|
+
|
|
|
// 员工请假数据(模拟数据)
|
|
|
private leaveRecords: LeaveRecord[] = [
|
|
|
{ id: '1', employeeName: '张三', date: '2024-01-20', isLeave: true, leaveType: 'personal', reason: '事假' },
|
|
|
@@ -241,7 +281,8 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
constructor(
|
|
|
private projectService: ProjectService,
|
|
|
private router: Router,
|
|
|
- private designerService: DesignerService
|
|
|
+ private designerService: DesignerService,
|
|
|
+ private issueService: ProjectIssueService
|
|
|
) {}
|
|
|
|
|
|
async ngOnInit(): Promise<void> {
|
|
|
@@ -253,6 +294,11 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
this.loadTodoTasks();
|
|
|
// 首次微任务后尝试初始化一次,确保容器已渲染
|
|
|
setTimeout(() => this.updateWorkloadGantt(), 0);
|
|
|
+
|
|
|
+ // 新增:加载待办任务(从问题板块)
|
|
|
+ await this.loadTodoTasksFromIssues();
|
|
|
+ // 启动自动刷新
|
|
|
+ this.startAutoRefresh();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -366,6 +412,9 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
this.designerWorkloadMap.get(profileName)!.push(projectData);
|
|
|
});
|
|
|
|
|
|
+ // 更新项目时间轴数据
|
|
|
+ this.convertToProjectTimeline();
|
|
|
+
|
|
|
} catch (error) {
|
|
|
console.error('加载设计师工作量失败:', error);
|
|
|
}
|
|
|
@@ -467,6 +516,257 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
this.applyFilters();
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 将项目数据转换为ProjectTimeline格式(带缓存优化 + 去重)
|
|
|
+ */
|
|
|
+ private convertToProjectTimeline(): void {
|
|
|
+ // 计算当前数据大小
|
|
|
+ let currentSize = 0;
|
|
|
+ this.designerWorkloadMap.forEach((projects) => {
|
|
|
+ currentSize += projects.length;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 如果数据没有变化,使用缓存
|
|
|
+ if (currentSize === this.lastDesignerWorkloadMapSize && this.timelineDataCache.length > 0) {
|
|
|
+ console.log('📊 使用缓存的项目时间轴数据:', this.timelineDataCache.length, '个项目');
|
|
|
+ this.projectTimelineData = this.timelineDataCache;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('📊 重新计算项目时间轴数据...');
|
|
|
+
|
|
|
+ // 从 designerWorkloadMap 获取所有组员的项目数据(去重)
|
|
|
+ const projectMap = new Map<string, any>(); // 使用Map去重,key为projectId
|
|
|
+ const allDesignerProjects: any[] = [];
|
|
|
+
|
|
|
+ // 调试:打印所有的 designerKey
|
|
|
+ const allKeys: string[] = [];
|
|
|
+ this.designerWorkloadMap.forEach((projects, designerKey) => {
|
|
|
+ allKeys.push(designerKey);
|
|
|
+ });
|
|
|
+ console.log('📊 designerWorkloadMap所有key:', allKeys);
|
|
|
+
|
|
|
+ this.designerWorkloadMap.forEach((projects, designerKey) => {
|
|
|
+ // 只处理真实的设计师名称(中文姓名),跳过ID形式的key
|
|
|
+ // 判断条件:
|
|
|
+ // 1. 是字符串
|
|
|
+ // 2. 长度在2-10之间(中文姓名通常2-4个字)
|
|
|
+ // 3. 包含中文字符(最可靠的判断)
|
|
|
+ const isChineseName = typeof designerKey === 'string'
|
|
|
+ && designerKey.length >= 2
|
|
|
+ && designerKey.length <= 10
|
|
|
+ && /[\u4e00-\u9fa5]/.test(designerKey); // 包含中文字符
|
|
|
+
|
|
|
+ if (isChineseName) {
|
|
|
+ console.log('✅ 使用设计师名称:', designerKey, '项目数:', projects.length);
|
|
|
+ projects.forEach(proj => {
|
|
|
+ const projectId = proj.id;
|
|
|
+ // 使用projectId去重
|
|
|
+ if (!projectMap.has(projectId)) {
|
|
|
+ const projectWithDesigner = {
|
|
|
+ ...proj,
|
|
|
+ designerName: designerKey
|
|
|
+ };
|
|
|
+ projectMap.set(projectId, projectWithDesigner);
|
|
|
+ allDesignerProjects.push(projectWithDesigner);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ console.log('⏭️ 跳过key:', designerKey, '(不是中文姓名)');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ console.log('📊 从designerWorkloadMap转换项目数据:', allDesignerProjects.length, '个项目(已去重)');
|
|
|
+
|
|
|
+ this.projectTimelineData = allDesignerProjects.map((project, index) => {
|
|
|
+ const now = new Date();
|
|
|
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
|
+
|
|
|
+ // 🔧 调整项目时间到当前周内(便于查看效果)
|
|
|
+ // 根据索引分配不同的天数偏移,让项目分散在7天内
|
|
|
+ const dayOffset = (index % 7) + 1; // 1-7天后
|
|
|
+ const adjustedEndDate = new Date(today.getTime() + dayOffset * 24 * 60 * 60 * 1000);
|
|
|
+
|
|
|
+ // 项目开始时间:交付前3-7天
|
|
|
+ const projectDuration = 3 + (index % 5); // 3-7天的项目周期
|
|
|
+ const adjustedStartDate = new Date(adjustedEndDate.getTime() - projectDuration * 24 * 60 * 60 * 1000);
|
|
|
+
|
|
|
+ // 对图时间:交付前1-2天
|
|
|
+ const reviewDaysBefore = 1 + (index % 2); // 交付前1-2天
|
|
|
+ const adjustedReviewDate = new Date(adjustedEndDate.getTime() - reviewDaysBefore * 24 * 60 * 60 * 1000);
|
|
|
+
|
|
|
+ // 计算距离交付还有几天
|
|
|
+ const daysUntilDeadline = Math.ceil((adjustedEndDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
|
|
+
|
|
|
+ // 计算项目状态
|
|
|
+ let status: 'normal' | 'warning' | 'urgent' | 'overdue' = 'normal';
|
|
|
+ if (daysUntilDeadline < 0) {
|
|
|
+ status = 'overdue';
|
|
|
+ } else if (daysUntilDeadline <= 1) {
|
|
|
+ status = 'urgent';
|
|
|
+ } else if (daysUntilDeadline <= 3) {
|
|
|
+ status = 'warning';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 映射阶段
|
|
|
+ const stageMap: Record<string, 'plan' | 'model' | 'decoration' | 'render' | 'delivery'> = {
|
|
|
+ '方案设计': 'plan',
|
|
|
+ '方案规划': 'plan',
|
|
|
+ '建模': 'model',
|
|
|
+ '建模阶段': 'model',
|
|
|
+ '软装': 'decoration',
|
|
|
+ '软装设计': 'decoration',
|
|
|
+ '渲染': 'render',
|
|
|
+ '渲染阶段': 'render',
|
|
|
+ '后期': 'render',
|
|
|
+ '交付': 'delivery',
|
|
|
+ '已完成': 'delivery'
|
|
|
+ };
|
|
|
+ const currentStage = stageMap[project.currentStage || '建模阶段'] || 'model';
|
|
|
+ const stageName = project.currentStage || '建模阶段';
|
|
|
+
|
|
|
+ // 计算阶段进度
|
|
|
+ const totalDuration = adjustedEndDate.getTime() - adjustedStartDate.getTime();
|
|
|
+ const elapsed = now.getTime() - adjustedStartDate.getTime();
|
|
|
+ const stageProgress = totalDuration > 0 ? Math.min(100, Math.max(0, (elapsed / totalDuration) * 100)) : 50;
|
|
|
+
|
|
|
+ // 检查是否停滞
|
|
|
+ const isStalled = false; // 调整后的项目都是进行中
|
|
|
+ const stalledDays = 0;
|
|
|
+
|
|
|
+ // 催办次数
|
|
|
+ const urgentCount = status === 'overdue' ? 2 : status === 'urgent' ? 1 : 0;
|
|
|
+
|
|
|
+ // 优先级
|
|
|
+ let priority: 'low' | 'medium' | 'high' | 'critical' = 'medium';
|
|
|
+ if (status === 'overdue') {
|
|
|
+ priority = 'critical';
|
|
|
+ } else if (status === 'urgent') {
|
|
|
+ priority = 'high';
|
|
|
+ } else if (status === 'warning') {
|
|
|
+ priority = 'medium';
|
|
|
+ } else {
|
|
|
+ priority = 'low';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 🆕 生成阶段截止时间数据(从交付日期往前推,每个阶段1天)
|
|
|
+ let phaseDeadlines = project.data?.phaseDeadlines;
|
|
|
+
|
|
|
+ // 如果项目没有阶段数据,动态生成(用于演示效果)
|
|
|
+ if (!phaseDeadlines) {
|
|
|
+ // ✅ 关键修复:从交付日期往前推算各阶段截止时间
|
|
|
+ const deliveryTime = adjustedEndDate.getTime();
|
|
|
+
|
|
|
+ // 后期截止 = 交付日期
|
|
|
+ const postProcessingDeadline = new Date(deliveryTime);
|
|
|
+ // 渲染截止 = 交付日期 - 1天
|
|
|
+ const renderingDeadline = new Date(deliveryTime - 1 * 24 * 60 * 60 * 1000);
|
|
|
+ // 软装截止 = 交付日期 - 2天
|
|
|
+ const softDecorDeadline = new Date(deliveryTime - 2 * 24 * 60 * 60 * 1000);
|
|
|
+ // 建模截止 = 交付日期 - 3天
|
|
|
+ const modelingDeadline = new Date(deliveryTime - 3 * 24 * 60 * 60 * 1000);
|
|
|
+
|
|
|
+ phaseDeadlines = {
|
|
|
+ modeling: {
|
|
|
+ startDate: adjustedStartDate,
|
|
|
+ deadline: modelingDeadline,
|
|
|
+ estimatedDays: 1,
|
|
|
+ status: now.getTime() >= modelingDeadline.getTime() && now.getTime() < softDecorDeadline.getTime() ? 'in_progress' :
|
|
|
+ now.getTime() >= softDecorDeadline.getTime() ? 'completed' : 'not_started',
|
|
|
+ priority: 'high'
|
|
|
+ },
|
|
|
+ softDecor: {
|
|
|
+ startDate: modelingDeadline,
|
|
|
+ deadline: softDecorDeadline,
|
|
|
+ estimatedDays: 1,
|
|
|
+ status: now.getTime() >= softDecorDeadline.getTime() && now.getTime() < renderingDeadline.getTime() ? 'in_progress' :
|
|
|
+ now.getTime() >= renderingDeadline.getTime() ? 'completed' : 'not_started',
|
|
|
+ priority: 'medium'
|
|
|
+ },
|
|
|
+ rendering: {
|
|
|
+ startDate: softDecorDeadline,
|
|
|
+ deadline: renderingDeadline,
|
|
|
+ estimatedDays: 1,
|
|
|
+ status: now.getTime() >= renderingDeadline.getTime() && now.getTime() < postProcessingDeadline.getTime() ? 'in_progress' :
|
|
|
+ now.getTime() >= postProcessingDeadline.getTime() ? 'completed' : 'not_started',
|
|
|
+ priority: 'high'
|
|
|
+ },
|
|
|
+ postProcessing: {
|
|
|
+ startDate: renderingDeadline,
|
|
|
+ deadline: postProcessingDeadline,
|
|
|
+ estimatedDays: 1,
|
|
|
+ status: now.getTime() >= postProcessingDeadline.getTime() ? 'completed' :
|
|
|
+ now.getTime() >= renderingDeadline.getTime() ? 'in_progress' : 'not_started',
|
|
|
+ priority: 'medium'
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ projectId: project.id || `proj-${Math.random().toString(36).slice(2, 9)}`,
|
|
|
+ projectName: project.name || '未命名项目',
|
|
|
+ designerId: project.designerName || '未分配',
|
|
|
+ designerName: project.designerName || '未分配',
|
|
|
+ startDate: adjustedStartDate,
|
|
|
+ endDate: adjustedEndDate,
|
|
|
+ deliveryDate: adjustedEndDate,
|
|
|
+ reviewDate: adjustedReviewDate,
|
|
|
+ currentStage,
|
|
|
+ stageName,
|
|
|
+ stageProgress: Math.round(stageProgress),
|
|
|
+ status,
|
|
|
+ isStalled,
|
|
|
+ stalledDays,
|
|
|
+ urgentCount,
|
|
|
+ priority,
|
|
|
+ spaceName: project.space || '',
|
|
|
+ customerName: project.customer || '',
|
|
|
+ phaseDeadlines: phaseDeadlines // 🆕 阶段截止时间
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ // 更新缓存
|
|
|
+ this.timelineDataCache = this.projectTimelineData;
|
|
|
+ this.lastDesignerWorkloadMapSize = currentSize;
|
|
|
+
|
|
|
+ console.log('📊 项目时间轴数据已转换:', this.projectTimelineData.length, '个项目');
|
|
|
+
|
|
|
+ // 调试:打印前3个项目的时间信息
|
|
|
+ if (this.projectTimelineData.length > 0) {
|
|
|
+ console.log('📅 示例项目时间:');
|
|
|
+ this.projectTimelineData.slice(0, 3).forEach(p => {
|
|
|
+ console.log(` - ${p.projectName}:`, {
|
|
|
+ 开始: p.startDate.toLocaleDateString(),
|
|
|
+ 对图: p.reviewDate.toLocaleDateString(),
|
|
|
+ 交付: p.deliveryDate.toLocaleDateString(),
|
|
|
+ 状态: p.status,
|
|
|
+ 阶段: p.stageName
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理项目点击事件
|
|
|
+ */
|
|
|
+ onProjectTimelineClick(projectId: string): void {
|
|
|
+ if (!projectId) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取公司ID(与 viewProjectDetails 保持一致)
|
|
|
+ const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
|
|
|
+
|
|
|
+ // 跳转到企微认证项目详情页(正确路由)
|
|
|
+ this.router.navigate(['/wxwork', cid, 'project', projectId]);
|
|
|
+
|
|
|
+ console.log('🔗 项目时间轴跳转:', {
|
|
|
+ projectId,
|
|
|
+ companyId: cid,
|
|
|
+ route: `/wxwork/${cid}/project/${projectId}`
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 构建搜索索引(如果需要)
|
|
|
*/
|
|
|
@@ -1201,7 +1501,10 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
toggleView(): void {
|
|
|
this.showGanttView = !this.showGanttView;
|
|
|
if (this.showGanttView) {
|
|
|
- setTimeout(() => this.initOrUpdateGantt(), 0);
|
|
|
+ // 切换到时间轴视图时,延迟加载数据(性能优化)
|
|
|
+ setTimeout(() => {
|
|
|
+ this.convertToProjectTimeline();
|
|
|
+ }, 0);
|
|
|
} else {
|
|
|
if (this.ganttChart) {
|
|
|
this.ganttChart.dispose();
|
|
|
@@ -2362,11 +2665,16 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
this.workloadGanttChart.dispose();
|
|
|
this.workloadGanttChart = null;
|
|
|
}
|
|
|
+ // 清理待办任务自动刷新定时器
|
|
|
+ if (this.todoTaskRefreshTimer) {
|
|
|
+ clearInterval(this.todoTaskRefreshTimer);
|
|
|
+ }
|
|
|
}
|
|
|
// 选择单个项目
|
|
|
selectProject(): void {
|
|
|
if (this.selectedProjectId) {
|
|
|
- this.router.navigate(['/team-leader/project-detail', this.selectedProjectId]);
|
|
|
+ const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
|
|
|
+ this.router.navigate(['/wxwork', cid, 'project', this.selectedProjectId]);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -2588,8 +2896,9 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
}
|
|
|
}
|
|
|
// 无推荐或用户取消,跳转到详细分配页面
|
|
|
- // 改为跳转到复用详情(组长视图),通过 query 参数标记 assign 模式
|
|
|
- this.router.navigate(['/team-leader/project-detail', projectId], { queryParams: { mode: 'assign' } });
|
|
|
+ // 跳转到项目详情页
|
|
|
+ const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
|
|
|
+ this.router.navigate(['/wxwork', cid, 'project', projectId]);
|
|
|
}
|
|
|
|
|
|
// 导航到待办任务
|
|
|
@@ -2636,10 +2945,11 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
// 打开工作量预估工具(已迁移)
|
|
|
openWorkloadEstimator(): void {
|
|
|
// 工具迁移至详情页:引导前往当前选中项目详情
|
|
|
+ const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
|
|
|
if (this.selectedProjectId) {
|
|
|
- this.router.navigate(['/team-leader/project-detail', this.selectedProjectId], { queryParams: { tool: 'estimator' } });
|
|
|
+ this.router.navigate(['/wxwork', cid, 'project', this.selectedProjectId]);
|
|
|
} else {
|
|
|
- this.router.navigate(['/team-leader/dashboard']);
|
|
|
+ this.router.navigate(['/wxwork', cid, 'team-leader']);
|
|
|
}
|
|
|
window?.fmode?.alert('工作量预估工具已迁移至项目详情页,您可以在建模阶段之前使用该工具进行工作量计算。');
|
|
|
}
|
|
|
@@ -2700,6 +3010,10 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
// 生成红色标记说明
|
|
|
const redMarkExplanation = this.generateRedMarkExplanation(employeeName, employeeLeaveRecords, currentProjects);
|
|
|
|
|
|
+ // 保存当前员工信息和项目数据(用于切换月份)
|
|
|
+ this.currentEmployeeName = employeeName;
|
|
|
+ this.currentEmployeeProjects = employeeProjects;
|
|
|
+
|
|
|
// 生成日历数据
|
|
|
const calendarData = this.generateEmployeeCalendar(employeeName, employeeProjects);
|
|
|
|
|
|
@@ -2779,10 +3093,10 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 生成员工日历数据(当前月份)
|
|
|
+ * 生成员工日历数据(支持指定月份)
|
|
|
*/
|
|
|
- private generateEmployeeCalendar(employeeName: string, employeeProjects: any[]): EmployeeCalendarData {
|
|
|
- const currentMonth = new Date();
|
|
|
+ private generateEmployeeCalendar(employeeName: string, employeeProjects: any[], targetMonth?: Date): EmployeeCalendarData {
|
|
|
+ const currentMonth = targetMonth || new Date();
|
|
|
const year = currentMonth.getFullYear();
|
|
|
const month = currentMonth.getMonth();
|
|
|
|
|
|
@@ -2922,6 +3236,33 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
this.showCalendarProjectList = true;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 切换员工日历月份
|
|
|
+ * @param direction -1=上月, 1=下月
|
|
|
+ */
|
|
|
+ changeEmployeeCalendarMonth(direction: number): void {
|
|
|
+ if (!this.selectedEmployeeDetail?.calendarData) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const currentMonth = this.selectedEmployeeDetail.calendarData.currentMonth;
|
|
|
+ const newMonth = new Date(currentMonth);
|
|
|
+ newMonth.setMonth(newMonth.getMonth() + direction);
|
|
|
+
|
|
|
+ // 重新生成日历数据
|
|
|
+ const newCalendarData = this.generateEmployeeCalendar(
|
|
|
+ this.currentEmployeeName,
|
|
|
+ this.currentEmployeeProjects,
|
|
|
+ newMonth
|
|
|
+ );
|
|
|
+
|
|
|
+ // 更新员工详情中的日历数据
|
|
|
+ this.selectedEmployeeDetail = {
|
|
|
+ ...this.selectedEmployeeDetail,
|
|
|
+ calendarData: newCalendarData
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 关闭项目列表弹窗
|
|
|
*/
|
|
|
@@ -3165,4 +3506,269 @@ export class Dashboard implements OnInit, OnDestroy {
|
|
|
|
|
|
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
|
|
}
|
|
|
+
|
|
|
+ // ==================== 新增:待办任务相关方法 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从问题板块加载待办任务
|
|
|
+ */
|
|
|
+ async loadTodoTasksFromIssues(): Promise<void> {
|
|
|
+ this.loadingTodoTasks = true;
|
|
|
+ this.todoTaskError = '';
|
|
|
+
|
|
|
+ try {
|
|
|
+ const Parse: any = FmodeParse.with('nova');
|
|
|
+ const query = new Parse.Query('ProjectIssue');
|
|
|
+
|
|
|
+ // 筛选条件:待处理 + 处理中
|
|
|
+ query.containedIn('status', ['待处理', '处理中']);
|
|
|
+ query.notEqualTo('isDeleted', true);
|
|
|
+
|
|
|
+ // 关联数据
|
|
|
+ query.include(['project', 'creator', 'assignee']);
|
|
|
+
|
|
|
+ // 排序:更新时间倒序
|
|
|
+ query.descending('updatedAt');
|
|
|
+
|
|
|
+ // 限制数量
|
|
|
+ query.limit(50);
|
|
|
+
|
|
|
+ const results = await query.find();
|
|
|
+
|
|
|
+ console.log(`📥 查询到 ${results.length} 条问题记录`);
|
|
|
+
|
|
|
+ // 数据转换(异步处理以支持 fetch)
|
|
|
+ const tasks = await Promise.all(results.map(async (obj: any) => {
|
|
|
+ let project = obj.get('project');
|
|
|
+ const assignee = obj.get('assignee');
|
|
|
+ const creator = obj.get('creator');
|
|
|
+ const data = obj.get('data') || {};
|
|
|
+
|
|
|
+ let projectName = '未知项目';
|
|
|
+ let projectId = '';
|
|
|
+
|
|
|
+ // 如果 project 存在,尝试获取完整数据
|
|
|
+ if (project) {
|
|
|
+ projectId = project.id;
|
|
|
+
|
|
|
+ // 尝试从已加载的对象获取 name
|
|
|
+ projectName = project.get('name');
|
|
|
+
|
|
|
+ // 如果 name 为空,使用 Parse.Query 查询项目
|
|
|
+ if (!projectName && projectId) {
|
|
|
+ try {
|
|
|
+ console.log(`🔄 查询项目数据: ${projectId}`);
|
|
|
+ const projectQuery = new Parse.Query('Project');
|
|
|
+ const fetchedProject = await projectQuery.get(projectId);
|
|
|
+ projectName = fetchedProject.get('name') || fetchedProject.get('title') || '未知项目';
|
|
|
+ console.log(`✅ 项目名称: ${projectName}`);
|
|
|
+ } catch (error) {
|
|
|
+ console.warn(`⚠️ 无法加载项目 ${projectId}:`, error);
|
|
|
+ projectName = `项目-${projectId.slice(0, 6)}`;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ console.warn('⚠️ 问题缺少关联项目:', {
|
|
|
+ issueId: obj.id,
|
|
|
+ title: obj.get('title')
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ id: obj.id,
|
|
|
+ title: obj.get('title') || obj.get('description')?.slice(0, 40) || '未命名问题',
|
|
|
+ description: obj.get('description'),
|
|
|
+ priority: obj.get('priority') as IssuePriority || 'medium',
|
|
|
+ type: obj.get('issueType') as IssueType || 'task',
|
|
|
+ status: this.zh2enStatus(obj.get('status')) as IssueStatus,
|
|
|
+ projectId,
|
|
|
+ projectName,
|
|
|
+ relatedSpace: obj.get('relatedSpace') || data.relatedSpace,
|
|
|
+ relatedStage: obj.get('relatedStage') || data.relatedStage,
|
|
|
+ assigneeName: assignee?.get('name') || assignee?.get('realname') || '未指派',
|
|
|
+ creatorName: creator?.get('name') || creator?.get('realname') || '未知',
|
|
|
+ createdAt: obj.createdAt || new Date(),
|
|
|
+ updatedAt: obj.updatedAt || new Date(),
|
|
|
+ dueDate: obj.get('dueDate'),
|
|
|
+ tags: (data.tags || []) as string[]
|
|
|
+ };
|
|
|
+ }));
|
|
|
+
|
|
|
+ this.todoTasksFromIssues = tasks;
|
|
|
+
|
|
|
+ // 排序:优先级 -> 时间
|
|
|
+ this.todoTasksFromIssues.sort((a, b) => {
|
|
|
+ const priorityA = this.getPriorityOrder(a.priority);
|
|
|
+ const priorityB = this.getPriorityOrder(b.priority);
|
|
|
+
|
|
|
+ if (priorityA !== priorityB) {
|
|
|
+ return priorityA - priorityB;
|
|
|
+ }
|
|
|
+
|
|
|
+ return +new Date(b.updatedAt) - +new Date(a.updatedAt);
|
|
|
+ });
|
|
|
+
|
|
|
+ console.log(`✅ 加载待办任务成功,共 ${this.todoTasksFromIssues.length} 条`);
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 加载待办任务失败:', error);
|
|
|
+ this.todoTaskError = '加载失败,请稍后重试';
|
|
|
+ } finally {
|
|
|
+ this.loadingTodoTasks = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 启动自动刷新(每5分钟)
|
|
|
+ */
|
|
|
+ startAutoRefresh(): void {
|
|
|
+ this.todoTaskRefreshTimer = setInterval(() => {
|
|
|
+ console.log('🔄 自动刷新待办任务...');
|
|
|
+ this.loadTodoTasksFromIssues();
|
|
|
+ }, 5 * 60 * 1000); // 5分钟
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 手动刷新待办任务
|
|
|
+ */
|
|
|
+ refreshTodoTasks(): void {
|
|
|
+ console.log('🔄 手动刷新待办任务...');
|
|
|
+ this.loadTodoTasksFromIssues();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 跳转到项目问题详情
|
|
|
+ */
|
|
|
+ navigateToIssue(task: TodoTaskFromIssue): void {
|
|
|
+ const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
|
|
|
+ // 跳转到项目详情页,并打开问题板块
|
|
|
+ this.router.navigate(
|
|
|
+ ['/wxwork', cid, 'project', task.projectId, 'order'],
|
|
|
+ {
|
|
|
+ queryParams: {
|
|
|
+ openIssues: 'true',
|
|
|
+ highlightIssue: task.id
|
|
|
+ }
|
|
|
+ }
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 标记问题为已读
|
|
|
+ */
|
|
|
+ async markAsRead(task: TodoTaskFromIssue): Promise<void> {
|
|
|
+ try {
|
|
|
+ // 方式1: 本地隐藏(不修改数据库)
|
|
|
+ this.todoTasksFromIssues = this.todoTasksFromIssues.filter(t => t.id !== task.id);
|
|
|
+ console.log(`✅ 标记问题为已读: ${task.title}`);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 标记已读失败:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取优先级配置
|
|
|
+ */
|
|
|
+ getPriorityConfig(priority: IssuePriority): { label: string; icon: string; color: string; order: number } {
|
|
|
+ const config: Record<IssuePriority, { label: string; icon: string; color: string; order: number }> = {
|
|
|
+ urgent: { label: '紧急', icon: '🔴', color: '#dc2626', order: 0 },
|
|
|
+ critical: { label: '紧急', icon: '🔴', color: '#dc2626', order: 0 },
|
|
|
+ high: { label: '高', icon: '🟠', color: '#ea580c', order: 1 },
|
|
|
+ medium: { label: '中', icon: '🟡', color: '#ca8a04', order: 2 },
|
|
|
+ low: { label: '低', icon: '⚪', color: '#9ca3af', order: 3 }
|
|
|
+ };
|
|
|
+ return config[priority] || config.medium;
|
|
|
+ }
|
|
|
+
|
|
|
+ getPriorityOrder(priority: IssuePriority): number {
|
|
|
+ return this.getPriorityConfig(priority).order;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取问题类型中文名
|
|
|
+ */
|
|
|
+ getIssueTypeLabel(type: IssueType): string {
|
|
|
+ const map: Record<IssueType, string> = {
|
|
|
+ bug: '问题',
|
|
|
+ task: '任务',
|
|
|
+ feedback: '反馈',
|
|
|
+ risk: '风险',
|
|
|
+ feature: '需求'
|
|
|
+ };
|
|
|
+ return map[type] || '任务';
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 格式化相对时间(精确到秒)
|
|
|
+ */
|
|
|
+ formatRelativeTime(date: Date | string): string {
|
|
|
+ if (!date) {
|
|
|
+ return '未知时间';
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const targetDate = new Date(date);
|
|
|
+ const now = new Date();
|
|
|
+ const diff = now.getTime() - targetDate.getTime();
|
|
|
+ const seconds = Math.floor(diff / 1000);
|
|
|
+ const minutes = Math.floor(seconds / 60);
|
|
|
+ const hours = Math.floor(minutes / 60);
|
|
|
+ const days = Math.floor(hours / 24);
|
|
|
+
|
|
|
+ if (seconds < 10) {
|
|
|
+ return '刚刚';
|
|
|
+ } else if (seconds < 60) {
|
|
|
+ return `${seconds}秒前`;
|
|
|
+ } else if (minutes < 60) {
|
|
|
+ return `${minutes}分钟前`;
|
|
|
+ } else if (hours < 24) {
|
|
|
+ return `${hours}小时前`;
|
|
|
+ } else if (days < 7) {
|
|
|
+ return `${days}天前`;
|
|
|
+ } else {
|
|
|
+ return targetDate.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' });
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ formatRelativeTime 错误:', error, 'date:', date);
|
|
|
+ return '时间格式错误';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 格式化精确时间(用于 tooltip)
|
|
|
+ * 格式:YYYY-MM-DD HH:mm:ss
|
|
|
+ */
|
|
|
+ formatExactTime(date: Date | string): string {
|
|
|
+ if (!date) {
|
|
|
+ return '未知时间';
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const d = new Date(date);
|
|
|
+ const year = d.getFullYear();
|
|
|
+ const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
|
+ const day = String(d.getDate()).padStart(2, '0');
|
|
|
+ const hours = String(d.getHours()).padStart(2, '0');
|
|
|
+ const minutes = String(d.getMinutes()).padStart(2, '0');
|
|
|
+ const seconds = String(d.getSeconds()).padStart(2, '0');
|
|
|
+
|
|
|
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ formatExactTime 错误:', error, 'date:', date);
|
|
|
+ return '时间格式错误';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 状态映射(中文 -> 英文)
|
|
|
+ */
|
|
|
+ private zh2enStatus(status: string): IssueStatus {
|
|
|
+ const map: Record<string, IssueStatus> = {
|
|
|
+ '待处理': 'open',
|
|
|
+ '处理中': 'in_progress',
|
|
|
+ '已解决': 'resolved',
|
|
|
+ '已关闭': 'closed'
|
|
|
+ };
|
|
|
+ return map[status] || 'open';
|
|
|
+ }
|
|
|
}
|