dashboard.ts 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168
  1. import { CommonModule } from '@angular/common';
  2. import { FormsModule } from '@angular/forms';
  3. import { Router, RouterModule } from '@angular/router';
  4. import { firstValueFrom } from 'rxjs';
  5. import { Component, OnInit, OnDestroy, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
  6. import { ProjectService } from '../../../services/project.service';
  7. import { DesignerService } from '../services/designer.service';
  8. import { WxworkAuth } from 'fmode-ng/core';
  9. import { ProjectTimelineComponent } from '../project-timeline';
  10. import { EmployeeDetailPanelComponent } from '../employee-detail-panel';
  11. import { DashboardMetricsComponent } from './components/dashboard-metrics/dashboard-metrics.component';
  12. import { DashboardNavbarComponent } from './components/dashboard-navbar/dashboard-navbar.component';
  13. import { WorkloadGanttComponent } from './components/workload-gantt/workload-gantt.component';
  14. import { TodoSectionComponent } from './components/todo-section/todo-section.component';
  15. import { SmartMatchModalComponent } from './components/smart-match-modal/smart-match-modal.component';
  16. import { StagnationReasonModalComponent } from './components/stagnation-reason-modal/stagnation-reason-modal.component';
  17. import { DashboardFilterBarComponent } from './components/dashboard-filter-bar/dashboard-filter-bar.component';
  18. import { ProjectKanbanComponent } from './components/project-kanban/project-kanban.component';
  19. import { DashboardAlertsComponent } from './components/dashboard-alerts/dashboard-alerts.component';
  20. import type { ProjectTimeline } from '../project-timeline/project-timeline';
  21. import { UrgentEventService } from '../services/urgent-event.service';
  22. import { DesignerWorkloadService } from '../services/designer-workload.service';
  23. import { DashboardNavigationHelper } from '../services/dashboard-navigation.helper';
  24. import { TodoTaskService } from '../services/todo-task.service';
  25. import { DashboardFilterService } from '../services/dashboard-filter.service';
  26. import { PhaseDeadlines, PhaseName } from '../../../models/project-phase.model';
  27. import { PROJECT_STAGES, CORE_PHASES } from './dashboard.constants';
  28. import {
  29. ProjectStage,
  30. ProjectPhase,
  31. Project,
  32. TodoTaskFromIssue,
  33. UrgentEvent,
  34. LeaveRecord,
  35. EmployeeDetail,
  36. EmployeeCalendarData,
  37. EmployeeCalendarDay
  38. } from './dashboard.model';
  39. @Component({
  40. selector: 'app-dashboard',
  41. standalone: true,
  42. imports: [
  43. CommonModule,
  44. FormsModule,
  45. RouterModule,
  46. ProjectTimelineComponent,
  47. EmployeeDetailPanelComponent,
  48. DashboardMetricsComponent,
  49. DashboardNavbarComponent,
  50. WorkloadGanttComponent,
  51. TodoSectionComponent,
  52. SmartMatchModalComponent,
  53. StagnationReasonModalComponent,
  54. DashboardFilterBarComponent,
  55. ProjectKanbanComponent,
  56. DashboardAlertsComponent
  57. ],
  58. templateUrl: './dashboard.html',
  59. styleUrl: './dashboard.scss',
  60. changeDetection: ChangeDetectionStrategy.OnPush
  61. })
  62. export class Dashboard implements OnInit, OnDestroy {
  63. // 暴露 Array 给模板使用
  64. Array = Array;
  65. projects: Project[] = [];
  66. filteredProjects: Project[] = [];
  67. urgentPinnedProjects: Project[] = [];
  68. showAlert: boolean = false;
  69. selectedProjectId: string = '';
  70. // 待办任务数据(交给子组件处理显示)
  71. todoTasksFromIssues: TodoTaskFromIssue[] = [];
  72. loadingTodoTasks: boolean = false;
  73. todoTaskError: string = '';
  74. private todoTaskRefreshTimer: any;
  75. // 紧急事件数据(交给子组件处理显示)
  76. urgentEvents: UrgentEvent[] = [];
  77. loadingUrgentEvents: boolean = false;
  78. handledUrgentEventIds: Set<string> = new Set();
  79. mutedUrgentEventIds: Set<string> = new Set();
  80. // 新增:当前用户信息
  81. currentUser = {
  82. name: '组长',
  83. avatar: "data:image/svg+xml,%3Csvg width='40' height='40' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='100%25' height='100%25' fill='%23CCFFCC'/%3E%3Ctext x='50%25' y='50%25' font-family='Arial' font-size='13.333333333333334' font-weight='bold' text-anchor='middle' fill='%23555555' dy='0.3em'%3E组长%3C/text%3E%3C/svg%3E",
  84. roleName: '组长'
  85. };
  86. currentDate = new Date();
  87. // 真实设计师数据(从fmode-ng获取)
  88. realDesigners: any[] = [];
  89. // 设计师工作量映射(从 ProjectTeam 表)
  90. designerWorkloadMap: Map<string, any[]> = new Map(); // designerId/name -> projects[]
  91. // 智能推荐相关
  92. showSmartMatch: boolean = false;
  93. selectedProject: any = null;
  94. recommendations: any[] = [];
  95. // 🆕 停滞/改图原因弹窗
  96. showStagnationModal: boolean = false;
  97. stagnationModalType: 'stagnation' | 'modification' = 'stagnation';
  98. stagnationModalProject: Project | null = null;
  99. // 新增:关键词搜索
  100. searchTerm: string = '';
  101. // 临期项目与筛选状态
  102. selectedType: 'all' | 'soft' | 'hard' = 'all';
  103. selectedUrgency: 'all' | 'high' | 'medium' | 'low' = 'all';
  104. selectedStatus: 'all' | 'progress' | 'completed' | 'overdue' | 'pendingApproval' | 'pendingAssignment' | 'dueSoon' = 'all';
  105. selectedDesigner: string = 'all';
  106. selectedMemberType: 'all' | 'vip' | 'normal' = 'all';
  107. // 新增:时间窗筛选
  108. selectedTimeWindow: 'all' | 'today' | 'threeDays' | 'sevenDays' = 'all';
  109. designers: any[] = [];
  110. // 新增:四大板块筛选
  111. selectedCorePhase: 'all' | 'order' | 'requirements' | 'delivery' | 'aftercare' | 'stalled' | 'modification' = 'all';
  112. // 设计师画像(从fmode-ng动态获取,保留此字段作为兼容)
  113. designerProfiles: any[] = [];
  114. // 10个项目阶段
  115. projectStages = PROJECT_STAGES;
  116. // 5大核心阶段(聚合展示)
  117. allCorePhases = CORE_PHASES;
  118. // 是否显示前期阶段(订单/需求)
  119. showPreProductionPhases: boolean = false;
  120. get visibleCorePhases(): any[] {
  121. if (this.showPreProductionPhases) {
  122. return this.allCorePhases;
  123. }
  124. // 默认隐藏订单和需求阶段
  125. return this.allCorePhases.filter(p => p.id !== 'order' && p.id !== 'requirements');
  126. }
  127. // 视图开关
  128. showGanttView: boolean = true;
  129. // 个人详情面板相关属性
  130. showEmployeeDetailPanel: boolean = false;
  131. selectedEmployeeName: string = '';
  132. selectedEmployeeDetail: any | null = null;
  133. selectedEmployeeProjects: any[] = [];
  134. // 项目时间轴数据
  135. projectTimelineData: ProjectTimeline[] = [];
  136. private timelineDataCache: ProjectTimeline[] = [];
  137. private lastDesignerWorkloadMapSize: number = 0;
  138. // 员工请假数据
  139. // private leaveRecords: LeaveRecord[] = [];
  140. constructor(
  141. private projectService: ProjectService,
  142. private router: Router,
  143. private designerService: DesignerService,
  144. private cdr: ChangeDetectorRef,
  145. private urgentEventService: UrgentEventService,
  146. private designerWorkloadService: DesignerWorkloadService,
  147. private navigationHelper: DashboardNavigationHelper,
  148. private todoTaskService: TodoTaskService,
  149. private dashboardFilterService: DashboardFilterService
  150. ) {}
  151. async ngOnInit(): Promise<void> {
  152. // 新增:加载用户Profile信息
  153. await this.loadUserProfile();
  154. await this.loadProjects();
  155. await this.loadDesigners();
  156. // 加载待办任务(从问题板块)
  157. await this.loadTodoTasksFromIssues();
  158. // 🆕 计算紧急事件
  159. this.calculateUrgentEvents();
  160. // 启动自动刷新
  161. this.startAutoRefresh();
  162. }
  163. /**
  164. * 加载项目列表
  165. */
  166. async loadProjects(): Promise<void> {
  167. try {
  168. const sourceProjects = await firstValueFrom(this.projectService.getProjects()) as any[];
  169. // 获取项目分配信息(用于修正设计师信息,确保跟甘特图一致)
  170. const companyId = localStorage.getItem('company') || 'cDL6R1hgSi';
  171. const assignments = await this.designerWorkloadService.getProjectAssignments(companyId);
  172. // 数据转换与增强,确保符合Dashboard模型要求
  173. this.projects = sourceProjects.map(p => {
  174. const deadline = p.deadline ? new Date(p.deadline) : new Date();
  175. const now = new Date();
  176. const overdueDays = Math.ceil((now.getTime() - deadline.getTime()) / (1000 * 60 * 60 * 24));
  177. // 🔧 增强时间字段获取逻辑
  178. // 尝试从多种路径获取 createdAt
  179. let rawCreatedAt = p.createdAt;
  180. if (!rawCreatedAt && typeof p.get === 'function') {
  181. rawCreatedAt = p.get('createdAt');
  182. }
  183. // 如果还是没有,尝试从 data 字段
  184. if (!rawCreatedAt && p.data && p.data.createdAt) {
  185. rawCreatedAt = p.data.createdAt;
  186. }
  187. const createdAtDate = rawCreatedAt ? new Date(rawCreatedAt) : undefined;
  188. // 尝试获取 updatedAt
  189. let rawUpdatedAt = p.updatedAt;
  190. if (!rawUpdatedAt && typeof p.get === 'function') {
  191. rawUpdatedAt = p.get('updatedAt');
  192. }
  193. const updatedAtDate = rawUpdatedAt ? new Date(rawUpdatedAt) : undefined;
  194. // 简单的类型推断
  195. let type: 'soft' | 'hard' = 'soft';
  196. if (p.customerTags && Array.isArray(p.customerTags)) {
  197. if (p.customerTags.some((t: any) => t.needType === '硬装')) {
  198. type = 'hard';
  199. }
  200. }
  201. // 获取设计师分配信息
  202. const projectAssignments = assignments.get(p.id) || [];
  203. const designerIds = projectAssignments.length > 0 ? projectAssignments.map(d => d.id) : (p.assigneeId ? [p.assigneeId] : []);
  204. const displayDesignerName = projectAssignments.length > 0
  205. ? projectAssignments.map(d => d.name).join(', ')
  206. : (p.assigneeName || p.designerName || '未分配').trim();
  207. const primaryDesignerId = designerIds[0] || p.assigneeId || '';
  208. return {
  209. // 基础字段映射
  210. id: p.id,
  211. name: p.name,
  212. status: p.status,
  213. currentStage: p.currentStage || '订单分配',
  214. createdAt: createdAtDate, // ✅ 使用增强后的 createdAt
  215. updatedAt: updatedAtDate, // ✅ 传递 updatedAt
  216. deadline: deadline,
  217. // 字段名称转换
  218. designerName: displayDesignerName,
  219. designerId: primaryDesignerId,
  220. designerIds: designerIds,
  221. // 补充 Dashboard 模型必需的缺省字段
  222. type: type,
  223. memberType: 'normal',
  224. urgency: 'medium',
  225. phases: [],
  226. expectedEndDate: deadline,
  227. // 新增字段初始化
  228. isStalled: (p as any).isStalled || false,
  229. isModification: (p as any).isModification || false,
  230. // 计算字段
  231. isOverdue: p.status !== '已完成' && overdueDays > 0,
  232. overdueDays: overdueDays > 0 ? overdueDays : 0,
  233. dueSoon: p.status !== '已完成' && overdueDays <= 0 && overdueDays >= -3,
  234. searchIndex: `${p.name}|${p.assigneeName || p.designerName || ''}`.toLowerCase(),
  235. // 保留原始数据供其他用途
  236. data: p.data,
  237. contact: p.contact,
  238. customer: p.customerName
  239. } as Project;
  240. });
  241. this.buildSearchIndexes();
  242. this.applyFilters();
  243. console.log(`✅ 加载项目成功,共 ${this.projects.length} 个`);
  244. } catch (error) {
  245. console.error('加载项目失败:', error);
  246. this.projects = [];
  247. }
  248. }
  249. /**
  250. * 从fmode-ng加载真实设计师数据
  251. */
  252. async loadDesigners(): Promise<void> {
  253. try {
  254. this.realDesigners = await this.designerService.getDesigners();
  255. // 更新设计师列表(用于筛选下拉框)
  256. this.designers = this.realDesigners.map(d => ({
  257. id: d.id,
  258. name: (d.name || '').trim()
  259. })).filter(d => !!d.name);
  260. // 同时更新designerProfiles以保持兼容性
  261. this.designerProfiles = this.realDesigners.map(d => ({
  262. id: d.id,
  263. name: d.name,
  264. skills: d.tags.expertise.styles || [],
  265. workload: 0, // 后续动态计算
  266. avgRating: d.tags.history.avgRating || 0,
  267. experience: 0 // 暂无此字段
  268. }));
  269. // 加载设计师的实际工作量
  270. await this.loadDesignerWorkload();
  271. } catch (error) {
  272. console.error('加载设计师数据失败:', error);
  273. }
  274. }
  275. /**
  276. * 🔧 从 ProjectTeam 表加载每个设计师的实际工作量
  277. */
  278. async loadDesignerWorkload(): Promise<void> {
  279. try {
  280. const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
  281. // 使用服务加载工作量
  282. this.designerWorkloadMap = await this.designerWorkloadService.loadWorkload(cid);
  283. // 转换为时间轴数据
  284. this.projectTimelineData = this.designerWorkloadService.transformToTimeline(this.designerWorkloadMap);
  285. // 更新缓存
  286. this.timelineDataCache = this.projectTimelineData;
  287. // 统计信息 logging
  288. let totalProjectsInMap = 0;
  289. this.designerWorkloadMap.forEach(projects => totalProjectsInMap += projects.length);
  290. this.lastDesignerWorkloadMapSize = totalProjectsInMap;
  291. console.log(`� 加载完成: ${totalProjectsInMap} 个项目分布在 ${this.designerWorkloadMap.size} 个设计师中`);
  292. } catch (error) {
  293. console.error('加载设计师工作量失败:', error);
  294. }
  295. }
  296. /**
  297. * 将筛选后的项目转换为时间轴数据
  298. * ✅ 修复:正确处理多设计师项目,避免下拉列表出现合并的设计师名字
  299. */
  300. convertToProjectTimeline(): void {
  301. if (!this.designerWorkloadService) return;
  302. // 将筛选后的项目按设计师分组
  303. const groupedMap = new Map<string, any[]>();
  304. this.filteredProjects.forEach(p => {
  305. // ✅ 修复:如果 designerName 包含逗号,说明是多个设计师,需要拆分
  306. const designerNames = (p.designerName || '未分配')
  307. .split(',')
  308. .map(name => name.trim())
  309. .filter(name => name.length > 0);
  310. // 为每个设计师单独添加项目
  311. designerNames.forEach((designerName, index) => {
  312. if (!groupedMap.has(designerName)) {
  313. groupedMap.set(designerName, []);
  314. }
  315. // 创建项目副本,确保每个设计师都有独立的项目条目
  316. const projectCopy = {
  317. ...p,
  318. designerName: designerName,
  319. // 如果有多个设计师,使用对应的 designerId
  320. designerId: p.designerIds && p.designerIds[index] ? p.designerIds[index] : p.designerId
  321. };
  322. groupedMap.get(designerName)?.push(projectCopy);
  323. });
  324. });
  325. // 使用服务转换
  326. this.projectTimelineData = this.designerWorkloadService.transformToTimeline(groupedMap);
  327. this.cdr.markForCheck();
  328. }
  329. /**
  330. * 处理项目点击事件
  331. */
  332. onProjectTimelineClick(projectId: string): void {
  333. if (!projectId) {
  334. return;
  335. }
  336. const project = this.projects.find(p => p.id === projectId);
  337. const currentStage = project?.currentStage || '订单分配';
  338. this.navigationHelper.navigateToProject(projectId, currentStage);
  339. }
  340. /**
  341. * 构建搜索索引(如果需要)
  342. */
  343. private buildSearchIndexes(): void {
  344. this.projects.forEach(p => {
  345. if (!p.searchIndex) {
  346. p.searchIndex = `${p.name}|${p.designerName}`.toLowerCase();
  347. }
  348. });
  349. }
  350. // 筛选状态改变(由子组件触发)
  351. // 重置状态筛选
  352. resetStatusFilter(): void {
  353. this.selectedStatus = 'all';
  354. this.applyFilters();
  355. }
  356. onFilterChange(filterState: any): void {
  357. // 更新本地状态
  358. this.searchTerm = filterState.searchTerm;
  359. this.selectedType = filterState.type;
  360. this.selectedUrgency = filterState.urgency;
  361. this.selectedStatus = filterState.status;
  362. this.selectedDesigner = filterState.designer;
  363. this.selectedMemberType = filterState.memberType;
  364. this.selectedCorePhase = filterState.corePhase;
  365. this.selectedProjectId = filterState.projectId;
  366. this.selectedTimeWindow = filterState.timeWindow;
  367. // 应用筛选
  368. this.applyFilters();
  369. }
  370. // 状态筛选(由指标卡片点击触发)
  371. filterByStatus(status: string): void {
  372. this.selectedStatus = status as any;
  373. this.applyFilters();
  374. }
  375. // 统一筛选
  376. applyFilters(): void {
  377. const criteria = {
  378. searchTerm: this.searchTerm,
  379. type: this.selectedType,
  380. urgency: this.selectedUrgency,
  381. status: this.selectedStatus,
  382. designer: this.selectedDesigner,
  383. memberType: this.selectedMemberType,
  384. corePhase: this.selectedCorePhase,
  385. timeWindow: this.selectedTimeWindow
  386. };
  387. const result = this.dashboardFilterService.filterProjects(this.projects, criteria);
  388. this.filteredProjects = result.filteredProjects;
  389. this.urgentPinnedProjects = result.urgentPinnedProjects;
  390. // 当显示甘特卡片时,同步刷新时间轴
  391. if (this.showGanttView) {
  392. this.convertToProjectTimeline();
  393. }
  394. }
  395. /**
  396. * 计算项目加权值
  397. */
  398. calculateWorkloadWeight(project: any): number {
  399. return this.designerService.calculateProjectWeight(project);
  400. }
  401. /**
  402. * 获取设计师加权工作量
  403. */
  404. getDesignerWeightedWorkload(designerName: string): {
  405. weightedTotal: number;
  406. projectCount: number;
  407. overdueCount: number;
  408. loadRate: number;
  409. } {
  410. const designerProjects = this.filteredProjects.filter(p => p.designerName === designerName);
  411. const weightedTotal = designerProjects.reduce((sum, p) => sum + this.calculateWorkloadWeight(p), 0);
  412. const overdueCount = designerProjects.filter(p => p.isOverdue).length;
  413. // 从realDesigners获取设计师的单周处理量
  414. const designer = this.realDesigners.find(d => d.name === designerName);
  415. const weeklyCapacity = designer?.tags?.capacity?.weeklyProjects || 3;
  416. const loadRate = weeklyCapacity > 0 ? (weightedTotal / weeklyCapacity) * 100 : 0;
  417. return {
  418. weightedTotal,
  419. projectCount: designerProjects.length,
  420. overdueCount,
  421. loadRate
  422. };
  423. }
  424. /**
  425. * 工作量卡片数据(替代ECharts)
  426. */
  427. get designerWorkloadCards(): Array<{
  428. name: string;
  429. loadRate: number;
  430. weightedValue: number;
  431. projectCount: number;
  432. overdueCount: number;
  433. status: 'overload' | 'busy' | 'idle';
  434. }> {
  435. const designers = Array.from(new Set(this.filteredProjects.map(p => p.designerName).filter(n => n && n !== '未分配')));
  436. return designers.map(name => {
  437. const workload = this.getDesignerWeightedWorkload(name);
  438. let status: 'overload' | 'busy' | 'idle' = 'idle';
  439. if (workload.loadRate > 80) status = 'overload';
  440. else if (workload.loadRate > 50) status = 'busy';
  441. return {
  442. name,
  443. loadRate: workload.loadRate,
  444. weightedValue: workload.weightedTotal,
  445. projectCount: workload.projectCount,
  446. overdueCount: workload.overdueCount,
  447. status
  448. };
  449. }).sort((a, b) => b.loadRate - a.loadRate); // 按负载率降序
  450. }
  451. /**
  452. * 获取超负荷设计师数量
  453. */
  454. get overloadedDesignersCount(): number {
  455. return this.designerWorkloadCards.filter(d => d.status === 'overload').length;
  456. }
  457. /**
  458. * 获取平均负载率
  459. */
  460. get averageWorkloadRate(): number {
  461. const cards = this.designerWorkloadCards;
  462. if (cards.length === 0) return 0;
  463. const sum = cards.reduce((acc, card) => acc + card.loadRate, 0);
  464. return sum / cards.length;
  465. }
  466. /**
  467. * 获取预警汇总数据
  468. */
  469. getAlertSummary(): {
  470. totalAlerts: number;
  471. overdueHighRisk: Project[];
  472. overloadedDesigners: any[];
  473. dueSoonProjects: Project[];
  474. } {
  475. // 1. 超期高危项目(超期>=5天 或 高紧急度超期)
  476. const overdueHighRisk = this.filteredProjects
  477. .filter(p => p.isOverdue && (p.overdueDays >= 5 || p.urgency === 'high'))
  478. .sort((a, b) => b.overdueDays - a.overdueDays)
  479. .slice(0, 5);
  480. // 2. 超负荷设计师
  481. const overloadedDesigners = this.designerWorkloadCards
  482. .filter(d => d.loadRate > 80)
  483. .sort((a, b) => b.loadRate - a.loadRate)
  484. .slice(0, 5);
  485. // 3. 即将到期项目(1-2天内)
  486. const now = new Date();
  487. const dueSoonProjects = this.filteredProjects
  488. .filter(p => {
  489. if (p.isOverdue) return false;
  490. const daysLeft = Math.ceil((p.deadline.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
  491. return daysLeft >= 1 && daysLeft <= 2;
  492. })
  493. .sort((a, b) => a.deadline.getTime() - b.deadline.getTime())
  494. .slice(0, 5);
  495. const totalAlerts = overdueHighRisk.length + overloadedDesigners.length + dueSoonProjects.length;
  496. return {
  497. totalAlerts,
  498. overdueHighRisk,
  499. overloadedDesigners,
  500. dueSoonProjects
  501. };
  502. }
  503. /**
  504. * 打开智能推荐弹窗
  505. */
  506. async openSmartMatch(project: any): Promise<void> {
  507. this.selectedProject = project;
  508. this.showSmartMatch = true;
  509. try {
  510. this.recommendations = await this.designerService.getRecommendedDesigners(project, this.realDesigners);
  511. } catch (error) {
  512. console.error('智能推荐失败:', error);
  513. this.recommendations = [];
  514. }
  515. }
  516. /**
  517. * 关闭智能推荐弹窗
  518. */
  519. closeSmartMatch(): void {
  520. this.showSmartMatch = false;
  521. this.selectedProject = null;
  522. this.recommendations = [];
  523. }
  524. /**
  525. * 分配项目给设计师
  526. */
  527. async assignToDesigner(designerId: string): Promise<void> {
  528. if (!this.selectedProject) return;
  529. try {
  530. const success = await this.designerService.assignProject(this.selectedProject.id, designerId);
  531. if (success) {
  532. this.closeSmartMatch();
  533. await this.loadProjects(); // 重新加载项目数据
  534. }
  535. } catch (error) {
  536. console.error('❌ 分配项目失败:', error);
  537. window?.fmode?.alert('分配失败,请重试');
  538. }
  539. }
  540. // 切换视图
  541. toggleView(): void {
  542. this.showGanttView = !this.showGanttView;
  543. if (this.showGanttView) {
  544. this.convertToProjectTimeline();
  545. }
  546. }
  547. ngOnDestroy(): void {
  548. // 清理待办任务自动刷新定时器
  549. if (this.todoTaskRefreshTimer) {
  550. clearInterval(this.todoTaskRefreshTimer);
  551. }
  552. }
  553. // 🔥 已延期项目
  554. get overdueProjects(): Project[] {
  555. return this.projects.filter(p => p.isOverdue);
  556. }
  557. // ⏳ 临期项目(3天内)
  558. get dueSoonProjects(): Project[] {
  559. return this.projects.filter(p => p.dueSoon && !p.isOverdue);
  560. }
  561. // 📋 待审批项目(支持中文和英文阶段名称)
  562. get pendingApprovalProjects(): Project[] {
  563. const pending = this.projects.filter(p => {
  564. const stage = (p.currentStage || '').trim();
  565. const data = (p as any).data || {};
  566. const approvalStatus = data.approvalStatus;
  567. // 1. 阶段为"订单分配"且审批状态为 pending
  568. // 2. 或者阶段为"待确认"/"待审批"(兼容旧数据)
  569. return (stage === '订单分配' && approvalStatus === 'pending') ||
  570. stage === '待审批' ||
  571. stage === '待确认';
  572. });
  573. return pending;
  574. }
  575. // 检查项目是否待审批
  576. isPendingApproval(project: Project): boolean {
  577. const stage = (project.currentStage || '').trim();
  578. const stageEn = stage.toLowerCase();
  579. const data: any = (project as any).data || {};
  580. // 🔥 新增:检查顶层的 pendingApproval 字段(备用方案)
  581. const topLevelPending = (project as any).pendingApproval === true && (project as any).approvalStage === '订单分配';
  582. return (stage === '订单分配' && (data.approvalStatus === 'pending' || topLevelPending)) ||
  583. ((stage === '交付执行' || stageEn === 'delivery') &&
  584. (data.deliveryApproval?.status === 'pending' ||
  585. (Array.isArray(data.pendingDeliveryApprovals) && data.pendingDeliveryApprovals.some((x: any) => x?.status === 'pending'))));
  586. }
  587. // 🎯 待分配项目(支持中文和英文阶段名称)
  588. get pendingAssignmentProjects(): Project[] {
  589. return this.projects.filter(p => {
  590. const stage = (p.currentStage || '').trim().toLowerCase();
  591. return stage === 'pendingassignment' ||
  592. stage === '待分配' ||
  593. stage === '订单分配';
  594. });
  595. }
  596. // 智能推荐设计师
  597. private getRecommendedDesigner(projectType: 'soft' | 'hard') {
  598. if (!this.designerProfiles || !this.designerProfiles.length) return null;
  599. const scoreOf = (p: any) => {
  600. const workloadScore = 100 - (p.workload ?? 0); // 负载越低越好
  601. const ratingScore = (p.avgRating ?? 0) * 10; // 评分越高越好
  602. const expScore = (p.experience ?? 0) * 5; // 经验越高越好
  603. return workloadScore * 0.5 + ratingScore * 0.3 + expScore * 0.2;
  604. };
  605. const sorted = [...this.designerProfiles].sort((a, b) => scoreOf(b) - scoreOf(a));
  606. return sorted[0] || null;
  607. }
  608. // 项目质量评审(由子组件触发)
  609. reviewProjectQuality(data: {projectId: string, rating: 'excellent' | 'qualified' | 'unqualified'}): void {
  610. const project = this.projects.find(p => p.id === data.projectId);
  611. if (!project) return;
  612. project.qualityRating = data.rating;
  613. if (data.rating === 'unqualified') {
  614. project.currentStage = 'revision';
  615. }
  616. this.applyFilters();
  617. window?.fmode?.alert('质量评审已提交');
  618. }
  619. /**
  620. * 根据看板列跳转到项目详情(参考客服板块实现)
  621. * @param projectId 项目ID
  622. * @param corePhaseId 核心阶段ID (order/requirements/delivery/aftercare)
  623. */
  624. viewProjectDetailsByPhase(projectId: string, corePhaseId: string): void {
  625. this.navigationHelper.navigateToProjectByPhase(projectId, corePhaseId);
  626. }
  627. // 查看项目详情 - 跳转到纯净的项目详情页(无管理端UI)
  628. viewProjectDetails(projectId: string): void {
  629. if (!projectId) {
  630. return;
  631. }
  632. // 🔥 根据项目当前阶段决定跳转到哪个阶段页面
  633. const project = this.projects.find(p => p.id === projectId);
  634. const currentStage = project?.currentStage || '订单分配';
  635. this.navigationHelper.navigateToProject(projectId, currentStage);
  636. }
  637. // 快速分配项目(增强:加入智能推荐)
  638. async quickAssignProject(projectId: string): Promise<void> {
  639. const project = this.projects.find(p => p.id === projectId);
  640. if (!project) {
  641. window?.fmode?.alert('未找到对应项目');
  642. return;
  643. }
  644. const recommended = this.getRecommendedDesigner(project.type);
  645. if (recommended) {
  646. const reassigning = !!project.designerName;
  647. const message = `推荐设计师:${recommended.name}(工作负载:${recommended.workload}%,评分:${recommended.avgRating}分)` +
  648. (reassigning ? `\n\n该项目当前已由「${project.designerName}」负责,是否改为分配给「${recommended.name}」?` : '\n\n是否确认分配?');
  649. const confirmAssign = await window?.fmode?.confirm(message);
  650. if (confirmAssign) {
  651. project.designerName = recommended.name;
  652. if (project.currentStage === 'pendingAssignment' || project.currentStage === 'pendingApproval') {
  653. project.currentStage = 'requirement';
  654. }
  655. project.status = '进行中';
  656. this.applyFilters();
  657. window?.fmode?.alert(`项目已${reassigning ? '重新' : ''}分配给 ${recommended.name}`);
  658. return;
  659. }
  660. }
  661. // 无推荐或用户取消,跳转到详细分配页面
  662. // 跳转到项目详情页
  663. const current = this.projects.find(p => p.id === projectId)?.currentStage || '订单分配';
  664. this.navigationHelper.navigateToProject(projectId, current);
  665. }
  666. // 预警相关操作(由子组件触发)
  667. viewAllOverdueProjects(): void {
  668. this.filterByStatus('overdue');
  669. this.showAlert = false;
  670. }
  671. closeAlert(): void {
  672. this.showAlert = false;
  673. }
  674. // 处理甘特图员工点击事件
  675. async onEmployeeClick(employeeName: string): Promise<void> {
  676. if (!employeeName || employeeName === '未分配') {
  677. return;
  678. }
  679. this.selectedEmployeeName = employeeName;
  680. this.selectedEmployeeProjects = this.designerWorkloadMap.get(employeeName) || [];
  681. this.showEmployeeDetailPanel = true;
  682. // 🔥 修复:手动触发变更检测,确保弹窗立即显示
  683. this.cdr.markForCheck();
  684. }
  685. // 关闭员工详情面板
  686. closeEmployeeDetailPanel(): void {
  687. this.showEmployeeDetailPanel = false;
  688. this.selectedEmployeeName = '';
  689. this.selectedEmployeeProjects = [];
  690. this.selectedEmployeeDetail = null;
  691. }
  692. // 员工详情面板:日历月份切换
  693. changeEmployeeCalendarMonth(direction: number): void {
  694. // 由 EmployeeDetailPanelComponent 内部处理
  695. console.log('日历月份切换:', direction);
  696. }
  697. // 员工详情面板:日历日期点击
  698. onCalendarDayClick(day: any): void {
  699. // 由 EmployeeDetailPanelComponent 内部处理
  700. console.log('日历日期点击:', day);
  701. }
  702. // 员工详情面板:刷新问卷
  703. refreshEmployeeSurvey(): void {
  704. // 由 EmployeeDetailPanelComponent 内部处理
  705. console.log('刷新员工问卷');
  706. }
  707. // 从员工详情面板跳转到项目详情
  708. navigateToProjectFromPanel(projectId: string): void {
  709. if (!projectId) {
  710. return;
  711. }
  712. // 关闭员工详情面板
  713. this.closeEmployeeDetailPanel();
  714. // 🔥 根据项目当前阶段决定跳转到哪个阶段页面
  715. const project = this.projects.find(p => p.id === projectId);
  716. const currentStage = project?.currentStage || '订单分配';
  717. this.navigationHelper.navigateToProject(projectId, currentStage);
  718. }
  719. /**
  720. * 加载用户Profile信息
  721. */
  722. async loadUserProfile(): Promise<void> {
  723. try {
  724. const cid = localStorage.getItem("company");
  725. if (!cid) {
  726. console.warn('未找到公司ID,使用默认用户信息');
  727. return;
  728. }
  729. const wwAuth = new WxworkAuth({ cid });
  730. const profile = await wwAuth.currentProfile();
  731. if (profile) {
  732. const name = profile.get("name") || profile.get("mobile") || '组长';
  733. const avatar = profile.get("avatar");
  734. const roleName = profile.get("roleName") || '组长';
  735. this.currentUser = {
  736. name,
  737. avatar: avatar || this.generateDefaultAvatar(name),
  738. roleName
  739. };
  740. console.log('用户Profile加载成功:', this.currentUser);
  741. }
  742. } catch (error) {
  743. console.error('加载用户Profile失败:', error);
  744. // 保持默认值
  745. }
  746. }
  747. /**
  748. * 生成默认头像(SVG格式)
  749. * @param name 用户名
  750. * @returns Base64编码的SVG数据URL
  751. */
  752. generateDefaultAvatar(name: string): string {
  753. const initial = name ? name.substring(0, 2).toUpperCase() : '组长';
  754. const bgColor = '#CCFFCC';
  755. const textColor = '#555555';
  756. const svg = `<svg width='40' height='40' xmlns='http://www.w3.org/2000/svg'>
  757. <rect width='100%' height='100%' fill='${bgColor}'/>
  758. <text x='50%' y='50%' font-family='Arial' font-size='13.333333333333334' font-weight='bold' text-anchor='middle' fill='${textColor}' dy='0.3em'>${initial}</text>
  759. </svg>`;
  760. return `data:image/svg+xml,${encodeURIComponent(svg)}`;
  761. }
  762. // ==================== 新增:待办任务相关方法 ====================
  763. /**
  764. * 从问题板块加载待办任务
  765. */
  766. async loadTodoTasksFromIssues(): Promise<void> {
  767. this.loadingTodoTasks = true;
  768. this.todoTaskError = '';
  769. try {
  770. this.todoTasksFromIssues = await this.todoTaskService.getTodoTasks();
  771. console.log(`✅ 加载待办任务成功,共 ${this.todoTasksFromIssues.length} 条`);
  772. } catch (error) {
  773. console.error('❌ 加载待办任务失败:', error);
  774. this.todoTaskError = '加载失败,请稍后重试';
  775. } finally {
  776. this.loadingTodoTasks = false;
  777. }
  778. }
  779. /**
  780. * 启动自动刷新(每5分钟)
  781. */
  782. startAutoRefresh(): void {
  783. this.todoTaskRefreshTimer = setInterval(() => {
  784. console.log('🔄 自动刷新待办任务...');
  785. this.loadTodoTasksFromIssues();
  786. }, 5 * 60 * 1000); // 5分钟
  787. }
  788. /**
  789. * 手动刷新待办任务
  790. */
  791. refreshTodoTasks(): void {
  792. console.log('🔄 手动刷新待办任务...');
  793. this.loadTodoTasksFromIssues();
  794. this.calculateUrgentEvents(); // 🆕 同时刷新紧急事件
  795. }
  796. /**
  797. * 🆕 从项目时间轴数据计算紧急事件
  798. * 识别截止时间已到或即将到达但未完成的关键节点
  799. */
  800. calculateUrgentEvents(): void {
  801. this.loadingUrgentEvents = true;
  802. try {
  803. // 使用服务计算紧急事件
  804. this.urgentEvents = this.urgentEventService.calculateUrgentEvents(
  805. this.projectTimelineData,
  806. this.handledUrgentEventIds,
  807. this.mutedUrgentEventIds
  808. );
  809. } catch (error) {
  810. console.error('❌ 计算紧急事件失败:', error);
  811. // 发生错误时清空列表,避免渲染异常数据
  812. this.urgentEvents = [];
  813. } finally {
  814. this.loadingUrgentEvents = false;
  815. // 确保触发变更检测
  816. this.cdr.markForCheck();
  817. }
  818. }
  819. // 紧急事件处理方法(由子组件触发)
  820. confirmEventOnTime(event: UrgentEvent): void {
  821. this.mutedUrgentEventIds.add(event.id);
  822. this.urgentEvents = this.urgentEvents.filter(e => e.id !== event.id);
  823. this.cdr.markForCheck();
  824. }
  825. resolveUrgentEvent(event: UrgentEvent): void {
  826. this.handledUrgentEventIds.add(event.id);
  827. this.urgentEvents = this.urgentEvents.filter(e => e.id !== event.id);
  828. this.cdr.markForCheck();
  829. }
  830. markEventAsStagnant(payload: {event: UrgentEvent, reason: any}): void {
  831. const { event, reason } = payload;
  832. // 更新紧急事件
  833. this.urgentEvents = this.urgentEvents.map(item => {
  834. if (item.id !== event.id) return item;
  835. const labels = new Set(item.labels || []);
  836. labels.add('停滞期');
  837. return {
  838. ...item,
  839. category: 'customer' as const,
  840. statusType: 'stagnant' as const,
  841. isMarkedAsStagnant: true,
  842. stagnationReasonType: reason.reasonType,
  843. stagnationCustomReason: reason.customReason,
  844. estimatedResumeDate: reason.estimatedResumeDate,
  845. reasonNotes: reason.notes,
  846. markedAt: new Date(),
  847. markedBy: this.currentUser.name,
  848. stagnationDays: item.stagnationDays || 7,
  849. labels: Array.from(labels),
  850. followUpNeeded: true
  851. };
  852. });
  853. // 🆕 同步更新对应的项目对象
  854. this.updateProjectMarkStatus(event.projectId, 'stagnation', reason);
  855. this.cdr.markForCheck();
  856. // TODO: 持久化到后端数据库
  857. this.saveEventMarkToDatabase(event, 'stagnation', reason);
  858. }
  859. markEventAsModification(payload: {event: UrgentEvent, reason: any}): void {
  860. const { event, reason } = payload;
  861. // 更新紧急事件
  862. this.urgentEvents = this.urgentEvents.map(item => {
  863. if (item.id !== event.id) return item;
  864. const labels = new Set(item.labels || []);
  865. labels.add('改图期');
  866. return {
  867. ...item,
  868. statusType: 'modification' as const,
  869. isMarkedAsModification: true,
  870. modificationReasonType: reason.reasonType,
  871. modificationCustomReason: reason.customReason,
  872. reasonNotes: reason.notes,
  873. markedAt: new Date(),
  874. markedBy: this.currentUser.name,
  875. labels: Array.from(labels)
  876. };
  877. });
  878. // 🆕 同步更新对应的项目对象
  879. this.updateProjectMarkStatus(event.projectId, 'modification', reason);
  880. this.cdr.markForCheck();
  881. // TODO: 持久化到后端数据库
  882. this.saveEventMarkToDatabase(event, 'modification', reason);
  883. }
  884. /**
  885. * 更新项目的停滞/改图标记及原因信息
  886. */
  887. private updateProjectMarkStatus(projectId: string, type: 'stagnation' | 'modification', reason: any): void {
  888. this.projects = this.projects.map(project => {
  889. if (project.id !== projectId) return project;
  890. if (type === 'stagnation') {
  891. return {
  892. ...project,
  893. isStalled: true,
  894. isModification: false,
  895. stagnationReasonType: reason.reasonType,
  896. stagnationCustomReason: reason.customReason,
  897. estimatedResumeDate: reason.estimatedResumeDate,
  898. reasonNotes: reason.notes,
  899. markedAt: new Date(),
  900. markedBy: this.currentUser.name
  901. };
  902. } else {
  903. return {
  904. ...project,
  905. isModification: true,
  906. isStalled: false,
  907. modificationReasonType: reason.reasonType,
  908. modificationCustomReason: reason.customReason,
  909. reasonNotes: reason.notes,
  910. markedAt: new Date(),
  911. markedBy: this.currentUser.name
  912. };
  913. }
  914. });
  915. // 重新应用筛选
  916. this.applyFilters();
  917. }
  918. private saveEventMarkToDatabase(event: UrgentEvent, type: 'stagnation' | 'modification', reason: any): void {
  919. // TODO: 实现数据持久化逻辑
  920. // 可以保存到 Parse 数据库的 ProjectEvent 或 UrgentEventMark 表
  921. console.log(`💾 保存事件标记到数据库:`, {
  922. eventId: event.id,
  923. projectId: event.projectId,
  924. type,
  925. reason,
  926. timestamp: new Date()
  927. });
  928. }
  929. createTodoFromEvent(event: UrgentEvent): void {
  930. const now = new Date();
  931. const newTask: TodoTaskFromIssue = {
  932. id: `urgent-todo-${event.id}-${now.getTime()}`,
  933. title: `【紧急】${event.title}`,
  934. description: event.description,
  935. priority: event.urgencyLevel === 'critical' ? 'urgent' : event.urgencyLevel === 'high' ? 'high' : 'medium',
  936. type: 'feedback',
  937. status: 'open',
  938. projectId: event.projectId,
  939. projectName: event.projectName,
  940. relatedStage: event.phaseName,
  941. assigneeName: event.designerName || '待分配',
  942. creatorName: this.currentUser.name,
  943. createdAt: now,
  944. updatedAt: now,
  945. dueDate: event.deadline,
  946. tags: [...(event.labels || []), '来自紧急事件']
  947. };
  948. this.todoTasksFromIssues = [newTask, ...this.todoTasksFromIssues];
  949. this.resolveUrgentEvent(event);
  950. }
  951. // 待办任务操作(由子组件触发)
  952. navigateToIssue(task: TodoTaskFromIssue): void {
  953. this.navigationHelper.navigateToIssue(task.projectId, task.id);
  954. }
  955. markAsRead(task: TodoTaskFromIssue): void {
  956. this.todoTasksFromIssues = this.todoTasksFromIssues.filter(t => t.id !== task.id);
  957. console.log(`✅ 标记问题为已读: ${task.title}`);
  958. }
  959. // 标记项目为停滞(从看板触发)
  960. markProjectAsStalled(project: Project): void {
  961. // 🆕 弹出原因输入弹窗
  962. this.stagnationModalType = 'stagnation';
  963. this.stagnationModalProject = project;
  964. this.showStagnationModal = true;
  965. }
  966. // 标记项目为改图(从看板触发)
  967. markProjectAsModification(project: Project): void {
  968. // 🆕 弹出原因输入弹窗
  969. this.stagnationModalType = 'modification';
  970. this.stagnationModalProject = project;
  971. this.showStagnationModal = true;
  972. }
  973. // 🆕 确认标记停滞/改图原因
  974. onStagnationReasonConfirm(reason: any): void {
  975. if (!this.stagnationModalProject) return;
  976. // 直接调用 updateProjectMarkStatus 更新项目
  977. this.updateProjectMarkStatus(
  978. this.stagnationModalProject.id,
  979. this.stagnationModalType,
  980. reason
  981. );
  982. // 关闭弹窗
  983. this.closeStagnationModal();
  984. // 显示确认消息
  985. const message = this.stagnationModalType === 'stagnation' ? '已标记为停滞项目' : '已标记为改图项目';
  986. window?.fmode?.alert(message);
  987. }
  988. // 🆕 关闭停滞/改图原因弹窗
  989. closeStagnationModal(): void {
  990. this.showStagnationModal = false;
  991. this.stagnationModalProject = null;
  992. }
  993. // 切换前期阶段显示
  994. togglePreProductionPhases(): void {
  995. this.showPreProductionPhases = !this.showPreProductionPhases;
  996. }
  997. }