dashboard.ts 58 KB

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