dashboard.ts 47 KB

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