dashboard.ts 69 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080
  1. // 客服工作台 - 对接Parse Server真实数据
  2. import { Component, OnInit, OnDestroy, signal, computed } from '@angular/core';
  3. import { CommonModule } from '@angular/common';
  4. import { FormsModule } from '@angular/forms';
  5. import { RouterModule, Router, ActivatedRoute } from '@angular/router';
  6. import { ProfileService } from '../../../services/profile.service';
  7. import { UrgentTaskService } from '../../../services/urgent-task.service';
  8. import { ActivityLogService } from '../../../services/activity-log.service';
  9. import { FmodeParse, FmodeObject } from 'fmode-ng/parse';
  10. // 问题板块服务与类型(复用组长端逻辑)
  11. import { ProjectIssueService, IssuePriority, IssueStatus, IssueType } from '../../../../modules/project/services/project-issue.service';
  12. // ⭐ 导入紧急事件类型定义(复用组长端)
  13. // 注意:UrgentEvent 类型与组长端保持一致,便于后续组件化
  14. interface UrgentEvent {
  15. id: string;
  16. title: string;
  17. description: string;
  18. eventType: 'review' | 'delivery' | 'phase_deadline'; // 事件类型
  19. phaseName?: string; // 阶段名称(如果是阶段截止)
  20. deadline: Date; // 截止时间
  21. projectId: string;
  22. projectName: string;
  23. designerName?: string;
  24. urgencyLevel: 'critical' | 'high' | 'medium'; // 紧急程度
  25. overdueDays?: number; // 逾期天数(负数表示还有几天)
  26. completionRate?: number; // 完成率(0-100)
  27. }
  28. const Parse = FmodeParse.with('nova');
  29. // 项目数据接口
  30. interface ProjectData {
  31. id: string;
  32. title: string;
  33. customerName: string;
  34. customerPhone?: string;
  35. status: string;
  36. stage: string;
  37. assigneeName?: string;
  38. createdAt: Date;
  39. updatedAt: Date;
  40. deadline?: Date;
  41. priority?: string;
  42. description?: string;
  43. }
  44. // 任务数据接口
  45. interface Task {
  46. id: string;
  47. projectId: string;
  48. projectName: string;
  49. title: string;
  50. stage: string;
  51. deadline: Date;
  52. isOverdue: boolean;
  53. isCompleted: boolean;
  54. priority: 'high' | 'medium' | 'low';
  55. assignee: string;
  56. description?: string;
  57. status: string;
  58. }
  59. // 项目更新联合类型
  60. interface ProjectUpdate {
  61. id: string;
  62. name?: string;
  63. customerName: string;
  64. status: string;
  65. updatedAt?: Date;
  66. createdAt?: Date;
  67. }
  68. interface FeedbackUpdate {
  69. id: string;
  70. customerName: string;
  71. content: string;
  72. status: string;
  73. createdAt: Date;
  74. feedbackType: string;
  75. }
  76. // 项目类型(用于项目动态)
  77. interface Project {
  78. id: string;
  79. name: string;
  80. customerName: string;
  81. status: string;
  82. updatedAt?: Date;
  83. createdAt?: Date;
  84. deadline?: Date;
  85. }
  86. // 客户反馈类型
  87. interface CustomerFeedback {
  88. id: string;
  89. projectId: string;
  90. customerName: string;
  91. content: string;
  92. status: string;
  93. createdAt: Date;
  94. }
  95. // 问题事件(用于项目动态与紧急待办复用)
  96. interface IssueUpdate {
  97. id: string;
  98. title: string;
  99. projectId: string;
  100. projectName: string;
  101. status: string; // 待处理/处理中/已解决/已关闭
  102. type?: IssueType | string;
  103. priority?: IssuePriority | string;
  104. assigneeName?: string;
  105. createdAt: Date;
  106. updatedAt: Date;
  107. }
  108. // 从问题板块映射的待办任务(复用组长端结构)
  109. interface TodoTaskFromIssue {
  110. id: string;
  111. title: string;
  112. description?: string;
  113. priority: IssuePriority;
  114. type: IssueType;
  115. status: IssueStatus;
  116. projectId: string;
  117. projectName: string;
  118. relatedSpace?: string;
  119. relatedStage?: string;
  120. assigneeName?: string;
  121. creatorName?: string;
  122. createdAt: Date;
  123. updatedAt: Date;
  124. dueDate?: Date;
  125. tags?: string[];
  126. }
  127. @Component({
  128. selector: 'app-dashboard',
  129. standalone: true,
  130. imports: [CommonModule, FormsModule, RouterModule],
  131. templateUrl: './dashboard.html',
  132. styleUrls: ['./dashboard.scss', './dashboard-urgent-tasks-enhanced.scss', '../customer-service-styles.scss']
  133. })
  134. export class Dashboard implements OnInit, OnDestroy {
  135. // 数据看板统计
  136. stats = {
  137. totalProjects: signal(0), // 项目总数
  138. newConsultations: signal(0), // 新咨询数
  139. pendingAssignments: signal(0), // 待分配项目数(原待派单数)
  140. exceptionProjects: signal(0), // 异常项目数
  141. afterSalesCount: signal(0) // 售后服务数量
  142. };
  143. // 紧急任务列表(从待办任务中筛选出紧急的)
  144. urgentTasks = signal<Task[]>([]);
  145. // 任务处理状态
  146. taskProcessingState = signal<Partial<Record<string, { inProgress: boolean; progress: number }>>>({});
  147. // 从问题板块加载的待办任务列表(复用组长端)
  148. todoTasksFromIssues = signal<TodoTaskFromIssue[]>([]);
  149. loadingTodoTasks = signal(false);
  150. todoTaskError = signal('');
  151. // ⭐ 紧急事件列表(复用组长端逻辑)
  152. urgentEventsList = signal<UrgentEvent[]>([]);
  153. loadingUrgentEvents = signal(false);
  154. // 项目时间轴数据(用于计算紧急事件)
  155. projectTimelineData: any[] = [];
  156. // 新增:待跟进尾款项目列表(真实数据)
  157. pendingFinalPaymentProjects = signal<Array<{
  158. id: string;
  159. projectId: string;
  160. projectName: string;
  161. customerName: string;
  162. customerPhone: string;
  163. finalPaymentAmount: number; // 剩余未付金额
  164. totalAmount: number; // 订单总金额
  165. paidAmount: number; // 已付金额
  166. dueDate: Date;
  167. status: string; // 已逾期/待创建/待付款
  168. overdueDay: number;
  169. }>>([]);
  170. // 项目动态流(扩展包含问题事件)
  171. projectUpdates = signal<(Project | CustomerFeedback | IssueUpdate)[]>([]);
  172. // 搜索关键词
  173. searchTerm = signal('');
  174. // 筛选后的项目更新(支持问题事件字段)
  175. filteredUpdates = computed(() => {
  176. if (!this.searchTerm()) return this.projectUpdates();
  177. return this.projectUpdates().filter(item => {
  178. if ('name' in item) {
  179. // 项目
  180. return item.name.toLowerCase().includes(this.searchTerm().toLowerCase()) ||
  181. item.customerName.toLowerCase().includes(this.searchTerm().toLowerCase()) ||
  182. item.status.toLowerCase().includes(this.searchTerm().toLowerCase());
  183. } else if ('content' in item) {
  184. // 反馈
  185. return 'content' in item && item.content.toLowerCase().includes(this.searchTerm().toLowerCase()) ||
  186. 'status' in item && item.status.toLowerCase().includes(this.searchTerm().toLowerCase());
  187. } else {
  188. // 问题事件
  189. const issue = item as IssueUpdate;
  190. const keyword = this.searchTerm().toLowerCase();
  191. return (
  192. (issue.title && issue.title.toLowerCase().includes(keyword)) ||
  193. (issue.projectName && issue.projectName.toLowerCase().includes(keyword)) ||
  194. (issue.assigneeName && issue.assigneeName.toLowerCase().includes(keyword)) ||
  195. (issue.status && issue.status.toLowerCase().includes(keyword))
  196. );
  197. }
  198. });
  199. });
  200. currentDate = new Date();
  201. // 回到顶部按钮可见性信号
  202. showBackToTopSignal = signal(false);
  203. // 任务表单可见性
  204. isTaskFormVisible = signal(false);
  205. // 项目列表(用于下拉选择)
  206. projectList = signal<any[]>([]);
  207. // 空间列表(用于下拉选择)
  208. spaceList = signal<any[]>([]);
  209. // 团队成员列表(用于指派)
  210. teamMembers = signal<any[]>([]);
  211. // 新任务数据
  212. newTask: any = {
  213. title: '',
  214. description: '',
  215. projectId: '',
  216. spaceId: '',
  217. stage: '订单分配',
  218. region: '',
  219. priority: 'high',
  220. assigneeId: '',
  221. deadline: new Date()
  222. };
  223. // 用于日期时间输入的属性
  224. deadlineInput = '';
  225. // 预设快捷时长选项
  226. timePresets = [
  227. { label: '1小时内', hours: 1 },
  228. { label: '3小时内', hours: 3 },
  229. { label: '6小时内', hours: 6 },
  230. { label: '12小时内', hours: 12 },
  231. { label: '24小时内', hours: 24 }
  232. ];
  233. // 选中的预设时长
  234. selectedPreset = '';
  235. // 自定义时间弹窗可见性
  236. isCustomTimeVisible = false;
  237. // 自定义选择的日期和时间
  238. customDate = new Date();
  239. customTime = '';
  240. // 错误提示信息
  241. deadlineError = '';
  242. // 提交按钮是否禁用
  243. isSubmitDisabled = false;
  244. // 下拉框可见性
  245. deadlineDropdownVisible = false;
  246. // 日期范围限制
  247. get todayDate(): string {
  248. return new Date().toISOString().split('T')[0];
  249. }
  250. get sevenDaysLaterDate(): string {
  251. const date = new Date();
  252. date.setDate(date.getDate() + 7);
  253. return date.toISOString().split('T')[0];
  254. }
  255. constructor(
  256. private router: Router,
  257. private route: ActivatedRoute,
  258. private profileService: ProfileService,
  259. private urgentTaskService: UrgentTaskService,
  260. private activityLogService: ActivityLogService,
  261. private issueService: ProjectIssueService
  262. ) {}
  263. // 当前用户和公司信息
  264. currentUser = signal<any>(null);
  265. company = signal<any>(null);
  266. // 初始化用户和公司信息
  267. private async initializeUserAndCompany(): Promise<void> {
  268. try {
  269. const profile = await this.profileService.getCurrentProfile();
  270. this.currentUser.set(profile);
  271. // 获取公司信息 - 映三色帐套
  272. const companyQuery = new Parse.Query('Company');
  273. companyQuery.equalTo('objectId', 'cDL6R1hgSi');
  274. const company = await companyQuery.first();
  275. if (!company) {
  276. throw new Error('未找到公司信息');
  277. }
  278. this.company.set(company);
  279. console.log('✅ 用户和公司信息初始化完成');
  280. } catch (error) {
  281. console.error('❌ 用户和公司信息初始化失败:', error);
  282. throw error;
  283. }
  284. }
  285. // 获取公司指针
  286. private getCompanyPointer(): any {
  287. if (!this.company()) {
  288. throw new Error('公司信息未加载');
  289. }
  290. return {
  291. __type: 'Pointer',
  292. className: 'Company',
  293. objectId: this.company().id
  294. };
  295. }
  296. // 创建带公司过滤的查询
  297. private createQuery(className: string): any {
  298. const query = new Parse.Query(className);
  299. query.equalTo('company', this.getCompanyPointer());
  300. query.notEqualTo('isDeleted', true);
  301. return query;
  302. }
  303. async ngOnInit(): Promise<void> {
  304. try {
  305. await this.initializeUserAndCompany();
  306. await this.loadDashboardData();
  307. // 添加滚动事件监听
  308. window.addEventListener('scroll', this.onScroll.bind(this));
  309. } catch (error) {
  310. console.error('❌ 客服工作台初始化失败:', error);
  311. }
  312. }
  313. // 加载仪表板数据
  314. private async loadDashboardData(): Promise<void> {
  315. try {
  316. await Promise.all([
  317. this.loadConsultationStats(),
  318. this.loadTodoTasksFromIssues(), // 先加载待办任务
  319. this.loadProjectUpdates(),
  320. this.loadCRMQueues(),
  321. this.loadPendingFinalPaymentProjects()
  322. ]);
  323. console.log('✅ 客服仪表板数据加载完成');
  324. } catch (error) {
  325. console.error('❌ 客服仪表板数据加载失败:', error);
  326. throw error;
  327. }
  328. }
  329. // 加载咨询统计数据
  330. private async loadConsultationStats(): Promise<void> {
  331. try {
  332. const todayStart = new Date();
  333. todayStart.setHours(0, 0, 0, 0);
  334. // 项目总数
  335. const totalProjectQuery = this.createQuery('Project');
  336. const totalProjects = await totalProjectQuery.count();
  337. this.stats.totalProjects.set(totalProjects);
  338. // 新咨询数(今日新增的项目)
  339. const consultationQuery = this.createQuery('Project');
  340. consultationQuery.greaterThanOrEqualTo('createdAt', todayStart);
  341. const newConsultations = await consultationQuery.count();
  342. this.stats.newConsultations.set(newConsultations);
  343. // 待分配项目数(阶段处于"订单分配"的项目)
  344. // 参考组长工作台的筛选逻辑:根据四大板块筛选
  345. // 订单分配阶段包括:order, pendingApproval, pendingAssignment, 订单分配, 待审批, 待分配
  346. // 查询所有项目,然后在客户端筛选(与组长工作台保持一致)
  347. const allProjectsQuery = this.createQuery('Project');
  348. allProjectsQuery.limit(1000); // 限制最多1000个项目
  349. const allProjects = await allProjectsQuery.find();
  350. // 使用与组长工作台相同的筛选逻辑
  351. const orderPhaseProjects = allProjects.filter(p => {
  352. const currentStage = p.get('currentStage') || '';
  353. const stage = p.get('stage') || '';
  354. const stageValue = (currentStage || stage).toString().trim().toLowerCase();
  355. // 订单分配阶段的所有变体(与组长工作台mapStageToCorePhase保持一致)
  356. const isOrderPhase = stageValue === 'order' ||
  357. stageValue === 'pendingapproval' ||
  358. stageValue === 'pendingassignment' ||
  359. stageValue === '订单分配' ||
  360. stageValue === '待审批' ||
  361. stageValue === '待分配';
  362. // 调试日志:输出每个项目的阶段信息
  363. if (isOrderPhase) {
  364. console.log(`📋 订单分配项目: ${p.get('title')}, currentStage="${currentStage}", stage="${stage}", 匹配值="${stageValue}"`);
  365. }
  366. return isOrderPhase;
  367. });
  368. const pendingAssignments = orderPhaseProjects.length;
  369. this.stats.pendingAssignments.set(pendingAssignments);
  370. console.log(`✅ 待分配项目统计: 总项目数=${allProjects.length}, 订单分配阶段项目数=${pendingAssignments}`);
  371. // 异常项目数(使用ProjectIssue表)
  372. const issueQuery = this.createQuery('ProjectIssue');
  373. issueQuery.equalTo('priority', 'high');
  374. issueQuery.equalTo('status', 'open');
  375. const exceptionProjects = await issueQuery.count();
  376. this.stats.exceptionProjects.set(exceptionProjects);
  377. // 售后服务数量(使用ProjectFeedback表,类型为投诉的待处理反馈)
  378. let afterSalesCount = 0;
  379. try {
  380. const feedbackQuery = this.createQuery('ProjectFeedback');
  381. feedbackQuery.equalTo('status', 'pending');
  382. feedbackQuery.equalTo('feedbackType', 'complaint');
  383. afterSalesCount = await feedbackQuery.count();
  384. this.stats.afterSalesCount.set(afterSalesCount);
  385. } catch (feedbackError) {
  386. console.warn('⚠️ ProjectFeedback表查询失败,可能表不存在,使用默认值0', feedbackError);
  387. this.stats.afterSalesCount.set(0);
  388. }
  389. console.log(`✅ 咨询统计: 项目总数${totalProjects}, 新咨询${newConsultations}, 待分配${pendingAssignments}, 异常${exceptionProjects}, 售后${afterSalesCount}`);
  390. } catch (error) {
  391. console.error('❌ 咨询统计加载失败:', error);
  392. // 不抛出错误,允许其他数据继续加载
  393. }
  394. }
  395. // 降级到模拟数据
  396. private loadMockData(): void {
  397. console.warn('⚠️ 使用模拟数据');
  398. this.loadUrgentTasks();
  399. this.loadProjectUpdates();
  400. this.loadCRMQueues();
  401. // loadPendingFinalPaymentProjects 已改为异步真实数据查询
  402. }
  403. // 添加滚动事件处理方法
  404. private onScroll(): void {
  405. this.showBackToTopSignal.set(window.scrollY > 300);
  406. }
  407. // 添加显示回到顶部按钮的计算属性
  408. showBackToTop = computed(() => this.showBackToTopSignal());
  409. // 清理事件监听器
  410. ngOnDestroy(): void {
  411. window.removeEventListener('scroll', this.onScroll.bind(this));
  412. }
  413. // 添加scrollToTop方法
  414. scrollToTop(): void {
  415. window.scrollTo({
  416. top: 0,
  417. behavior: 'smooth'
  418. });
  419. }
  420. // 查看人员考勤
  421. viewAttendance(): void {
  422. this.router.navigate(['/hr/attendance']);
  423. }
  424. // 加载紧急任务(已废弃,现在从loadTodoTasksFromIssues中同步)
  425. private async loadUrgentTasks(): Promise<void> {
  426. // 此方法已被 loadTodoTasksFromIssues 替代
  427. // 紧急任务现在从待办任务中自动筛选
  428. console.log('⚠️ loadUrgentTasks 已废弃,紧急任务从 loadTodoTasksFromIssues 中同步');
  429. return;
  430. /* 保留原代码用于参考
  431. try {
  432. // 使用UrgentTaskService加载紧急事项
  433. const result = await this.urgentTaskService.findUrgentTasks({
  434. isCompleted: false
  435. }, 1, 20);
  436. // 转换数据格式以兼容现有UI
  437. const formattedTasks: Task[] = result.tasks.map(task => ({
  438. id: task.id,
  439. projectId: task.projectId,
  440. projectName: task.projectName,
  441. title: task.title,
  442. stage: task.stage,
  443. deadline: task.deadline,
  444. isOverdue: task.isOverdue,
  445. isCompleted: task.isCompleted,
  446. priority: task.priority as 'high' | 'medium' | 'low',
  447. assignee: task.assigneeName,
  448. description: task.description || '',
  449. status: task.status
  450. }));
  451. */
  452. }
  453. // 加载CRM队列数据(已隐藏,暂不使用真实数据)
  454. private loadCRMQueues(): void {
  455. // CRM功能暂时隐藏,后续开发时再从Parse查询真实数据
  456. // 可以从ProjectFeedback表查询客户反馈和咨询记录
  457. console.log('⏸️ CRM队列功能暂时隐藏');
  458. }
  459. // 查看全部咨询列表
  460. goToConsultationList(): void {
  461. this.router.navigate(['/customer-service/consultation-list']);
  462. }
  463. // 加载项目动态
  464. private async loadProjectUpdates(): Promise<void> {
  465. try {
  466. const updates: (Project | CustomerFeedback | IssueUpdate)[] = [];
  467. // 1. 查询最新更新的项目
  468. const projectQuery = this.createQuery('Project');
  469. projectQuery.include(['contact', 'assignee']);
  470. projectQuery.descending('updatedAt');
  471. projectQuery.limit(10);
  472. const projects = await projectQuery.find();
  473. for (const project of projects) {
  474. const contact = project.get('contact');
  475. updates.push({
  476. id: project.id,
  477. name: project.get('title') || '未命名项目',
  478. customerName: contact?.get('name') || '未知客户',
  479. status: project.get('status') || '进行中',
  480. updatedAt: project.get('updatedAt'),
  481. createdAt: project.get('createdAt')
  482. });
  483. }
  484. // 2. 查询最新客户反馈
  485. let feedbacks: any[] = [];
  486. try {
  487. const feedbackQuery = this.createQuery('ProjectFeedback');
  488. feedbackQuery.include(['contact', 'project']);
  489. feedbackQuery.descending('createdAt');
  490. feedbackQuery.limit(10);
  491. feedbacks = await feedbackQuery.find();
  492. } catch (feedbackError) {
  493. console.warn('⚠️ ProjectFeedback表查询失败,可能表不存在,跳过反馈数据', feedbackError);
  494. feedbacks = [];
  495. }
  496. for (const feedback of feedbacks) {
  497. const contact = feedback.get('contact');
  498. updates.push({
  499. id: feedback.id,
  500. projectId: feedback.get('project')?.id || '',
  501. customerName: contact?.get('name') || '未知客户',
  502. content: feedback.get('content') || '无内容',
  503. status: feedback.get('status') || 'pending',
  504. createdAt: feedback.get('createdAt')
  505. });
  506. }
  507. // 3. 查询最新问题事件(ProjectIssue)
  508. try {
  509. const issueQuery = this.createQuery('ProjectIssue');
  510. issueQuery.include(['project', 'assignee']);
  511. issueQuery.notEqualTo('isDeleted', true);
  512. issueQuery.descending('updatedAt');
  513. issueQuery.limit(10);
  514. const issues = await issueQuery.find();
  515. for (const obj of issues) {
  516. const project = obj.get('project');
  517. const assignee = obj.get('assignee');
  518. const title = obj.get('title') || (obj.get('description') || '').slice(0, 40) || '未命名问题';
  519. const projectName = project?.get('title') || '未知项目';
  520. const statusZh = obj.get('status') || '待处理';
  521. const typeRaw = obj.get('issueType') || 'task';
  522. const priorityRaw = obj.get('priority') || 'medium';
  523. updates.push({
  524. id: obj.id,
  525. title,
  526. projectId: project?.id || '',
  527. projectName,
  528. status: statusZh,
  529. type: typeRaw,
  530. priority: priorityRaw,
  531. assigneeName: assignee?.get('name') || assignee?.get('realname') || '',
  532. createdAt: obj.createdAt || new Date(),
  533. updatedAt: obj.updatedAt || new Date()
  534. } as IssueUpdate);
  535. }
  536. } catch (e) {
  537. console.warn('⚠️ 加载问题事件失败(忽略):', e);
  538. }
  539. // 按时间排序
  540. updates.sort((a, b) => {
  541. const aTime = ('updatedAt' in a && a.updatedAt) ? a.updatedAt.getTime() : (a.createdAt?.getTime() || 0);
  542. const bTime = ('updatedAt' in b && b.updatedAt) ? b.updatedAt.getTime() : (b.createdAt?.getTime() || 0);
  543. return bTime - aTime;
  544. });
  545. this.projectUpdates.set(updates);
  546. console.log(`✅ 项目动态加载完成: ${updates.length} 条动态`);
  547. } catch (error) {
  548. console.error('❌ 项目动态加载失败:', error);
  549. // 不抛出错误,允许其他数据继续加载
  550. }
  551. }
  552. // 处理任务完成
  553. async markTaskAsCompleted(taskId: string): Promise<void> {
  554. try {
  555. const task = this.urgentTasks().find(t => t.id === taskId);
  556. if (task && task.id.startsWith('issue:')) {
  557. // 来自问题板块的任务:将问题状态置为已解决
  558. const issueId = task.id.replace('issue:', '');
  559. await this.issueService.setStatus(task.projectId, issueId, 'resolved');
  560. // 记录问题活动日志
  561. try {
  562. const user = this.currentUser();
  563. await this.activityLogService.logActivity({
  564. actorId: user?.id || 'unknown',
  565. actorName: user?.get('name') || '客服',
  566. actorRole: user?.get('roleName') || 'customer_service',
  567. actionType: 'complete',
  568. module: 'project_issue',
  569. entityType: 'ProjectIssue',
  570. entityId: issueId,
  571. entityName: task.title,
  572. description: '将问题标记为已解决',
  573. metadata: {
  574. priority: task.priority,
  575. projectName: task.projectName
  576. }
  577. });
  578. } catch (logError) {
  579. console.error('记录活动日志失败:', logError);
  580. }
  581. } else {
  582. // 原紧急任务逻辑
  583. await this.urgentTaskService.markAsCompleted(taskId);
  584. // 记录活动日志
  585. if (task) {
  586. try {
  587. const user = this.currentUser();
  588. await this.activityLogService.logActivity({
  589. actorId: user?.id || 'unknown',
  590. actorName: user?.get('name') || '客服',
  591. actorRole: user?.get('roleName') || 'customer_service',
  592. actionType: 'complete',
  593. module: 'urgent_task',
  594. entityType: 'UrgentTask',
  595. entityId: taskId,
  596. entityName: task.title,
  597. description: '完成了紧急事项',
  598. metadata: {
  599. priority: task.priority,
  600. projectName: task.projectName
  601. }
  602. });
  603. } catch (logError) {
  604. console.error('记录活动日志失败:', logError);
  605. }
  606. }
  607. }
  608. // 重新加载任务列表
  609. await this.loadUrgentTasks();
  610. console.log('✅ 任务标记为已完成');
  611. } catch (error) {
  612. console.error('❌ 标记任务完成失败:', error);
  613. alert('操作失败,请稍后重试');
  614. }
  615. }
  616. // 删除任务
  617. async deleteTask(taskId: string): Promise<void> {
  618. if (!await window?.fmode?.confirm('确定要删除这个紧急事项吗?')) {
  619. return;
  620. }
  621. try {
  622. const task = this.urgentTasks().find(t => t.id === taskId);
  623. if (task && task.id.startsWith('issue:')) {
  624. const issueId = task.id.replace('issue:', '');
  625. await this.issueService.deleteIssue(task.projectId, issueId);
  626. try {
  627. const user = this.currentUser();
  628. await this.activityLogService.logActivity({
  629. actorId: user?.id || 'unknown',
  630. actorName: user?.get('name') || '客服',
  631. actorRole: user?.get('roleName') || 'customer_service',
  632. actionType: 'delete',
  633. module: 'project_issue',
  634. entityType: 'ProjectIssue',
  635. entityId: issueId,
  636. entityName: task.title,
  637. description: '删除了问题',
  638. metadata: {
  639. priority: task.priority,
  640. projectName: task.projectName
  641. }
  642. });
  643. } catch {}
  644. } else {
  645. await this.urgentTaskService.deleteUrgentTask(taskId);
  646. }
  647. // 重新加载任务列表
  648. await this.loadUrgentTasks();
  649. console.log('✅ 任务删除成功');
  650. } catch (error) {
  651. console.error('❌ 删除任务失败:', error);
  652. alert('删除失败,请稍后重试');
  653. }
  654. }
  655. // 处理派单操作
  656. handleAssignment(taskId: string): void {
  657. // 标记任务为处理中
  658. const task = this.urgentTasks().find(t => t.id === taskId);
  659. if (task) {
  660. // 初始化处理状态
  661. this.taskProcessingState.update(state => ({
  662. ...state,
  663. [task.id]: { inProgress: true, progress: 0 }
  664. }));
  665. // 模拟处理进度
  666. let progress = 0;
  667. const interval = setInterval(() => {
  668. progress += 10;
  669. this.taskProcessingState.update(state => ({
  670. ...state,
  671. [task.id]: { inProgress: progress < 100, progress }
  672. }));
  673. if (progress >= 100) {
  674. clearInterval(interval);
  675. // 处理完成后从列表中移除该任务
  676. this.urgentTasks.set(
  677. this.urgentTasks().filter(t => t.id !== task.id)
  678. );
  679. // 清除处理状态
  680. this.taskProcessingState.update(state => {
  681. const newState = { ...state };
  682. delete newState[task.id];
  683. return newState;
  684. });
  685. }
  686. }, 300);
  687. }
  688. // 更新统计数据
  689. this.stats.pendingAssignments.set(this.stats.pendingAssignments() - 1);
  690. }
  691. // 显示任务表单
  692. async showTaskForm(): Promise<void> {
  693. // 重置表单数据
  694. this.newTask = {
  695. title: '',
  696. description: '',
  697. projectId: '',
  698. spaceId: '',
  699. stage: '订单分配',
  700. region: '',
  701. priority: 'high',
  702. assigneeId: '',
  703. deadline: new Date()
  704. };
  705. // 重置相关状态
  706. this.deadlineError = '';
  707. this.isSubmitDisabled = false;
  708. // 计算并设置默认预设时长
  709. this.setDefaultPreset();
  710. // 加载下拉列表数据
  711. try {
  712. const [projects, members] = await Promise.all([
  713. this.urgentTaskService.getProjects(),
  714. this.urgentTaskService.getTeamMembers()
  715. ]);
  716. this.projectList.set(projects);
  717. this.teamMembers.set(members);
  718. this.spaceList.set([]); // 初始为空,等待选择项目后加载
  719. } catch (error) {
  720. console.error('加载下拉列表数据失败:', error);
  721. }
  722. // 显示表单
  723. this.isTaskFormVisible.set(true);
  724. // 添加iOS风格的面板显示动画
  725. setTimeout(() => {
  726. document.querySelector('.ios-panel')?.classList.add('ios-panel-visible');
  727. }, 10);
  728. }
  729. // 项目选择变化时加载空间列表
  730. async onProjectChange(projectId: string): Promise<void> {
  731. if (!projectId) {
  732. this.spaceList.set([]);
  733. return;
  734. }
  735. try {
  736. const spaces = await this.urgentTaskService.getProjectSpaces(projectId);
  737. this.spaceList.set(spaces);
  738. } catch (error) {
  739. console.error('加载空间列表失败:', error);
  740. this.spaceList.set([]);
  741. }
  742. }
  743. // 设置默认预设时长
  744. private setDefaultPreset(): void {
  745. const now = new Date();
  746. const todayEnd = new Date(now);
  747. todayEnd.setHours(23, 59, 59, 999);
  748. // 检查3小时后是否超过当天24:00
  749. const threeHoursLater = new Date(now.getTime() + 3 * 60 * 60 * 1000);
  750. if (threeHoursLater <= todayEnd) {
  751. // 3小时后未超过当天24:00,默认选中3小时内
  752. this.selectedPreset = '3';
  753. this.updatePresetDeadline(3);
  754. } else {
  755. // 3小时后超过当天24:00,默认选中当天24:00前
  756. this.selectedPreset = 'today';
  757. this.deadlineInput = todayEnd.toISOString().slice(0, 16);
  758. this.newTask.deadline = todayEnd;
  759. }
  760. }
  761. // 处理预设时长选择
  762. handlePresetSelection(preset: string): void {
  763. this.selectedPreset = preset;
  764. this.deadlineError = '';
  765. if (preset === 'custom') {
  766. // 打开自定义时间选择器
  767. this.openCustomTimePicker();
  768. } else if (preset === 'today') {
  769. // 设置为当天24:00前
  770. const now = new Date();
  771. const todayEnd = new Date(now);
  772. todayEnd.setHours(23, 59, 59, 999);
  773. this.deadlineInput = todayEnd.toISOString().slice(0, 16);
  774. this.newTask.deadline = todayEnd;
  775. } else {
  776. // 计算预设时长的截止时间
  777. const hours = parseInt(preset);
  778. this.updatePresetDeadline(hours);
  779. }
  780. }
  781. // 更新预设时长的截止时间
  782. private updatePresetDeadline(hours: number): void {
  783. const now = new Date();
  784. const deadline = new Date(now.getTime() + hours * 60 * 60 * 1000);
  785. this.deadlineInput = deadline.toISOString().slice(0, 16);
  786. this.newTask.deadline = deadline;
  787. }
  788. // 打开自定义时间选择器
  789. openCustomTimePicker(): void {
  790. // 重置自定义时间
  791. this.customDate = new Date();
  792. const now = new Date();
  793. this.customTime = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
  794. // 显示自定义时间弹窗
  795. this.isCustomTimeVisible = true;
  796. // 添加iOS风格的弹窗动画
  797. setTimeout(() => {
  798. document.querySelector('.custom-time-modal')?.classList.add('modal-visible');
  799. }, 10);
  800. }
  801. // 关闭自定义时间选择器
  802. closeCustomTimePicker(): void {
  803. // 添加iOS风格的弹窗关闭动画
  804. const modal = document.querySelector('.custom-time-modal');
  805. if (modal) {
  806. modal.classList.remove('modal-visible');
  807. setTimeout(() => {
  808. this.isCustomTimeVisible = false;
  809. }, 300);
  810. } else {
  811. this.isCustomTimeVisible = false;
  812. }
  813. }
  814. // 处理自定义时间选择
  815. handleCustomTimeSelection(): void {
  816. const [hours, minutes] = this.customTime.split(':').map(Number);
  817. const selectedDateTime = new Date(this.customDate);
  818. selectedDateTime.setHours(hours, minutes, 0, 0);
  819. // 验证选择的时间是否有效
  820. if (this.validateDeadline(selectedDateTime)) {
  821. this.deadlineInput = selectedDateTime.toISOString().slice(0, 16);
  822. this.newTask.deadline = selectedDateTime;
  823. this.closeCustomTimePicker();
  824. }
  825. }
  826. // 验证截止时间是否有效
  827. validateDeadline(deadline: Date): boolean {
  828. const now = new Date();
  829. if (deadline < now) {
  830. this.deadlineError = '截止时间不能早于当前时间,请重新选择';
  831. this.isSubmitDisabled = true;
  832. return false;
  833. }
  834. this.deadlineError = '';
  835. this.isSubmitDisabled = false;
  836. return true;
  837. }
  838. // 获取显示的截止时间文本
  839. getDisplayDeadline(): string {
  840. if (!this.deadlineInput) return '';
  841. try {
  842. const date = new Date(this.deadlineInput);
  843. return date.toLocaleString('zh-CN', {
  844. year: 'numeric',
  845. month: '2-digit',
  846. day: '2-digit',
  847. hour: '2-digit',
  848. minute: '2-digit'
  849. });
  850. } catch (error) {
  851. return '';
  852. }
  853. }
  854. // 隐藏任务表单
  855. hideTaskForm(): void {
  856. // 添加iOS风格的面板隐藏动画
  857. const panel = document.querySelector('.ios-panel');
  858. if (panel) {
  859. panel.classList.remove('ios-panel-visible');
  860. setTimeout(() => {
  861. this.isTaskFormVisible.set(false);
  862. }, 300);
  863. } else {
  864. this.isTaskFormVisible.set(false);
  865. }
  866. }
  867. // 处理添加任务表单提交
  868. async handleAddTaskSubmit(): Promise<void> {
  869. // 验证表单数据
  870. if (!this.newTask.title.trim() || !this.newTask.projectName.trim() || !this.deadlineInput || this.isSubmitDisabled) {
  871. // 在实际应用中,这里应该显示错误提示
  872. window?.fmode?.alert('请填写必填字段(任务标题、项目名称、截止时间)');
  873. return;
  874. }
  875. try {
  876. // 创建紧急事项
  877. const task = await this.urgentTaskService.createUrgentTask({
  878. title: this.newTask.title,
  879. description: this.newTask.description,
  880. projectId: this.newTask.projectId,
  881. spaceId: this.newTask.spaceId || undefined,
  882. stage: this.newTask.stage,
  883. region: this.newTask.region,
  884. priority: this.newTask.priority,
  885. assigneeId: this.newTask.assigneeId || undefined,
  886. deadline: new Date(this.deadlineInput)
  887. });
  888. // 记录活动日志
  889. try {
  890. const user = this.currentUser();
  891. const projectName = this.projectList().find(p => p.id === this.newTask.projectId)?.get('title') || '未知项目';
  892. await this.activityLogService.logActivity({
  893. actorId: user?.id || 'unknown',
  894. actorName: user?.get('name') || '客服',
  895. actorRole: user?.get('roleName') || 'customer_service',
  896. actionType: 'create',
  897. module: 'urgent_task',
  898. entityType: 'UrgentTask',
  899. entityId: task.id,
  900. entityName: this.newTask.title,
  901. description: '创建了紧急事项',
  902. metadata: {
  903. priority: this.newTask.priority,
  904. projectName: projectName,
  905. stage: this.newTask.stage,
  906. region: this.newTask.region,
  907. deadline: this.deadlineInput
  908. }
  909. });
  910. } catch (logError) {
  911. console.error('记录活动日志失败:', logError);
  912. }
  913. // 重新加载任务列表
  914. await this.loadUrgentTasks();
  915. console.log('✅ 紧急事项创建成功');
  916. // 隐藏表单
  917. this.hideTaskForm();
  918. } catch (error) {
  919. console.error('❌ 创建紧急事项失败:', error);
  920. alert('创建失败,请稍后重试');
  921. }
  922. }
  923. // 添加新的紧急事项
  924. addUrgentTask(): void {
  925. // 调用显示表单方法
  926. this.showTaskForm();
  927. }
  928. // 项目总数图标点击处理
  929. handleTotalProjectsClick(): void {
  930. console.log('导航到项目列表 - 显示所有项目');
  931. this.router.navigate(['/customer-service/project-list'], {
  932. queryParams: { filter: 'all' }
  933. });
  934. }
  935. // 新咨询数图标点击处理
  936. handleNewConsultationsClick(): void {
  937. this.navigateToDetail('consultations');
  938. }
  939. // 待分配数图标点击处理
  940. handlePendingAssignmentsClick(): void {
  941. console.log('导航到项目列表 - 显示待分配项目');
  942. this.router.navigate(['/customer-service/project-list'], {
  943. queryParams: { filter: 'pending' }
  944. });
  945. }
  946. // 异常项目图标点击处理
  947. handleExceptionProjectsClick(): void {
  948. this.navigateToDetail('exceptions');
  949. }
  950. handleAfterSalesClick(): void {
  951. this.router.navigate(['/customer-service/after-sales']);
  952. }
  953. // 导航到详情页
  954. private navigateToDetail(type: 'consultations' | 'assignments' | 'exceptions'): void {
  955. const routeMap = {
  956. consultations: '/customer-service/consultation-list',
  957. assignments: '/customer-service/assignment-list',
  958. exceptions: '/customer-service/exception-list'
  959. };
  960. console.log('导航到:', routeMap[type]);
  961. console.log('当前路由:', this.router.url);
  962. // 添加iOS风格页面过渡动画
  963. document.body.classList.add('ios-page-transition');
  964. setTimeout(() => {
  965. this.router.navigateByUrl(routeMap[type])
  966. .then(navResult => {
  967. console.log('导航结果:', navResult);
  968. if (!navResult) {
  969. console.error('导航失败,检查路由配置');
  970. }
  971. })
  972. .catch(err => {
  973. console.error('导航错误:', err);
  974. });
  975. setTimeout(() => {
  976. document.body.classList.remove('ios-page-transition');
  977. }, 300);
  978. }, 100);
  979. }
  980. // 格式化日期
  981. formatDate(date: Date | string): string {
  982. if (!date) return '';
  983. try {
  984. return new Date(date).toLocaleString('zh-CN', {
  985. month: '2-digit',
  986. day: '2-digit',
  987. hour: '2-digit',
  988. minute: '2-digit'
  989. });
  990. } catch (error) {
  991. console.error('日期格式化错误:', error);
  992. return '';
  993. }
  994. }
  995. // 添加安全获取客户名称的方法
  996. getCustomerName(update: Project | CustomerFeedback | IssueUpdate): string {
  997. if ('customerName' in update && update.customerName) {
  998. return update.customerName;
  999. } else if ('projectId' in update) {
  1000. // 查找相关项目获取客户名称
  1001. // 如果是问题事件,优先展示项目名称
  1002. if ('title' in update && 'projectName' in update) {
  1003. return (update as IssueUpdate).projectName || '未知项目';
  1004. }
  1005. return '客户反馈';
  1006. }
  1007. return '未知客户';
  1008. }
  1009. // 优化的日期格式化方法
  1010. getFormattedDate(update: Project | CustomerFeedback | IssueUpdate): string {
  1011. if (!update) return '';
  1012. if ('createdAt' in update && update.createdAt) {
  1013. return this.formatDate(update.createdAt);
  1014. } else if ('updatedAt' in update && update.updatedAt) {
  1015. return this.formatDate(update.updatedAt);
  1016. } else if ('deadline' in update && update.deadline) {
  1017. return this.formatDate(update.deadline);
  1018. }
  1019. return '';
  1020. }
  1021. // 添加获取状态的安全方法
  1022. getUpdateStatus(update: Project | CustomerFeedback | IssueUpdate): string {
  1023. if ('status' in update && update.status) {
  1024. return update.status;
  1025. }
  1026. return '已更新';
  1027. }
  1028. // 检查是否是项目更新
  1029. isProjectUpdate(update: Project | CustomerFeedback | IssueUpdate): update is Project {
  1030. return 'name' in update && 'status' in update;
  1031. }
  1032. // 检查是否有内容字段
  1033. hasContent(update: Project | CustomerFeedback | IssueUpdate): boolean {
  1034. return 'content' in update;
  1035. }
  1036. // 获取更新内容
  1037. getUpdateContent(update: Project | CustomerFeedback | IssueUpdate): string {
  1038. if ('content' in update) {
  1039. return (update as CustomerFeedback).content;
  1040. }
  1041. return '';
  1042. }
  1043. // 处理搜索输入事件
  1044. onSearchInput(event: Event): void {
  1045. const target = event.target as HTMLInputElement;
  1046. if (target) {
  1047. this.searchTerm.set(target.value);
  1048. }
  1049. }
  1050. // 添加getTaskStatus方法的正确实现
  1051. getTaskStatus(task: Task): string {
  1052. if (!task) return '未知状态';
  1053. if (task.isCompleted) return '已完成';
  1054. if (task.isOverdue) return '已逾期';
  1055. return '进行中';
  1056. }
  1057. // 添加getUpdateStatusClass方法的正确实现
  1058. getUpdateStatusClass(update: Project | CustomerFeedback | IssueUpdate): string {
  1059. if ('name' in update) {
  1060. // 项目
  1061. switch (update.status) {
  1062. case '进行中': return 'status-active';
  1063. case '已完成': return 'status-completed';
  1064. case '已暂停': return 'status-paused';
  1065. default: return 'status-pending';
  1066. }
  1067. } else if ('title' in update) {
  1068. // 问题事件
  1069. const status = (update as IssueUpdate).status;
  1070. switch (status) {
  1071. case '待处理': return 'status-pending';
  1072. case '处理中': return 'status-active';
  1073. case '已解决': return 'status-completed';
  1074. case '已关闭': return 'status-completed';
  1075. default: return 'status-pending';
  1076. }
  1077. } else {
  1078. // 反馈
  1079. switch (update.status) {
  1080. case '已解决': return 'status-completed';
  1081. case '处理中': return 'status-active';
  1082. default: return 'status-pending';
  1083. }
  1084. }
  1085. }
  1086. // 新增:类型守卫与显示辅助(问题事件)
  1087. isIssueUpdate(update: Project | CustomerFeedback | IssueUpdate): update is IssueUpdate {
  1088. return 'title' in update && 'projectName' in update;
  1089. }
  1090. getIssueTitle(update: IssueUpdate): string {
  1091. return update?.title || '未命名问题';
  1092. }
  1093. getIssueProjectName(update: IssueUpdate): string {
  1094. return update?.projectName || '未知项目';
  1095. }
  1096. // 已移至底部统一管理(复用组长端方法)
  1097. // getIssueTypeLabel 和 getIssuePriorityLabel 已在待办任务模块中定义
  1098. // 新增:加载待跟进尾款项目(从Project.data读取,不使用ProjectPayment表)
  1099. private async loadPendingFinalPaymentProjects(): Promise<void> {
  1100. try {
  1101. console.log('🔍 开始加载待跟进尾款项目...');
  1102. const now = new Date();
  1103. const resultList: Array<{
  1104. id: string;
  1105. projectId: string;
  1106. projectName: string;
  1107. customerName: string;
  1108. customerPhone: string;
  1109. finalPaymentAmount: number;
  1110. totalAmount: number;
  1111. paidAmount: number;
  1112. dueDate: Date;
  1113. status: string;
  1114. overdueDay: number;
  1115. }> = [];
  1116. // 1) 查询处于"售后归档"相关阶段的项目(公司内)
  1117. const projectQuery = this.createQuery('Project');
  1118. projectQuery.containedIn('currentStage', [
  1119. '售后归档', '尾款结算', '客户评价', '投诉处理', '已归档', 'aftercare'
  1120. ]);
  1121. projectQuery.include(['contact', 'assignee']);
  1122. projectQuery.descending('updatedAt');
  1123. projectQuery.limit(100); // 增加限制以获取更多项目
  1124. projectQuery.notEqualTo('isDeleted', true);
  1125. const projects = await projectQuery.find();
  1126. console.log(`📊 找到 ${projects.length} 个售后阶段项目`);
  1127. // 2) 逐项目从Project.data统计尾款是否不足
  1128. for (const p of projects) {
  1129. try {
  1130. // 从项目数据中获取订单总金额和付款信息
  1131. const projectData = p.get('data') || {};
  1132. const quotation = projectData.quotation || {};
  1133. const aftercare = projectData.aftercare || {};
  1134. const finalPayment = aftercare.finalPayment || {};
  1135. // 订单总金额
  1136. const orderTotal = quotation.total || 0;
  1137. // 已付金额(从售后归档数据中获取)
  1138. let totalPaid = finalPayment.paidAmount || 0;
  1139. // 如果没有售后归档数据,尝试从 paymentVouchers 计算
  1140. if (totalPaid === 0 && finalPayment.paymentVouchers && finalPayment.paymentVouchers.length > 0) {
  1141. totalPaid = finalPayment.paymentVouchers.reduce((sum: number, v: any) => {
  1142. return sum + (v.amount || 0);
  1143. }, 0);
  1144. }
  1145. // 计算剩余未付款金额
  1146. const remaining = orderTotal - totalPaid;
  1147. console.log(`📋 项目 ${p.get('title') || p.get('name')}: 订单总额=¥${orderTotal}, 已付=¥${totalPaid}, 剩余=¥${remaining}`);
  1148. // 只有当剩余金额大于100元时才认为是待跟进项目(避免小额零头)
  1149. if (remaining > 100) {
  1150. const contact = p.get('contact');
  1151. const customerName = contact?.get?.('realname') || contact?.get?.('name') || p.get('customerName') || '未知客户';
  1152. const customerPhone = contact?.get?.('mobile') || contact?.get?.('phone') || p.get('customerPhone') || '无电话';
  1153. // 获取到期日期
  1154. let dueDate: Date | undefined = finalPayment.dueDate ? new Date(finalPayment.dueDate) : undefined;
  1155. let isOverdue = false;
  1156. let overdueDay = 0;
  1157. // 计算逾期天数
  1158. if (dueDate) {
  1159. const diff = now.getTime() - dueDate.getTime();
  1160. if (diff > 0) {
  1161. isOverdue = true;
  1162. overdueDay = Math.floor(diff / (1000 * 60 * 60 * 24));
  1163. }
  1164. }
  1165. // 如果没有到期日期,使用项目截止日期作为应付日期
  1166. if (!dueDate) {
  1167. dueDate = p.get('deadline') || new Date();
  1168. }
  1169. // 确定状态
  1170. const paymentStatus = isOverdue ? '已逾期' :
  1171. !finalPayment.dueDate ? '待创建' :
  1172. '待付款';
  1173. resultList.push({
  1174. id: p.id,
  1175. projectId: p.id,
  1176. projectName: p.get('title') || p.get('name') || '未命名项目',
  1177. customerName,
  1178. customerPhone,
  1179. finalPaymentAmount: remaining,
  1180. totalAmount: orderTotal,
  1181. paidAmount: totalPaid,
  1182. dueDate,
  1183. status: paymentStatus,
  1184. overdueDay
  1185. });
  1186. console.log(`✅ 添加待跟进项目: ${p.get('title') || p.get('name')}, 剩余¥${remaining}, 状态=${paymentStatus}`);
  1187. }
  1188. } catch (projectError) {
  1189. console.error(`❌ 处理项目 ${p.id} 时出错:`, projectError);
  1190. // 继续处理下一个项目
  1191. }
  1192. }
  1193. // 按逾期天数降序排序(逾期时间最长的排在前面)
  1194. resultList.sort((a, b) => {
  1195. if (a.status === '已逾期' && b.status !== '已逾期') return -1;
  1196. if (a.status !== '已逾期' && b.status === '已逾期') return 1;
  1197. return b.overdueDay - a.overdueDay;
  1198. });
  1199. this.pendingFinalPaymentProjects.set(resultList);
  1200. console.log(`✅ 待跟进尾款项目加载完成: ${resultList.length} 个项目(售后归档阶段)`);
  1201. console.log('详细列表:', resultList.map(p => ({
  1202. 项目: p.projectName,
  1203. 剩余金额: p.finalPaymentAmount,
  1204. 状态: p.status,
  1205. 逾期天数: p.overdueDay
  1206. })));
  1207. } catch (error) {
  1208. console.error('❌ 待跟进尾款项目加载失败:', error);
  1209. // 设置空列表,不影响其他功能
  1210. this.pendingFinalPaymentProjects.set([]);
  1211. }
  1212. }
  1213. // 新增:格式化日期时间
  1214. formatDateTime(date: Date): string {
  1215. const now = new Date();
  1216. const diffMs = now.getTime() - date.getTime();
  1217. const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
  1218. const diffMinutes = Math.floor(diffMs / (1000 * 60));
  1219. if (diffMinutes < 60) {
  1220. return `${diffMinutes}分钟前`;
  1221. } else if (diffHours < 24) {
  1222. return `${diffHours}小时前`;
  1223. } else {
  1224. return date.toLocaleDateString('zh-CN', {
  1225. month: 'short',
  1226. day: 'numeric',
  1227. hour: '2-digit',
  1228. minute: '2-digit'
  1229. });
  1230. }
  1231. }
  1232. // 新增:获取支付状态文本
  1233. getPaymentStatusText(status: string): string {
  1234. switch (status) {
  1235. case 'pending_followup': return '待跟进';
  1236. case 'following_up': return '跟进中';
  1237. case 'payment_completed': return '已支付';
  1238. default: return '未知状态';
  1239. }
  1240. }
  1241. // 新增:开始跟进尾款
  1242. async followUpFinalPayment(projectId: string): Promise<void> {
  1243. console.log(`🎯 开始跟进项目 ${projectId} 的尾款`);
  1244. try {
  1245. // 查找该项目的详细信息
  1246. const project = this.pendingFinalPaymentProjects().find(p => p.projectId === projectId);
  1247. if (!project) {
  1248. console.error('❌ 未找到项目信息');
  1249. return;
  1250. }
  1251. // 记录跟进日志到活动记录(ActivityLog表可能不存在,使用try-catch)
  1252. try {
  1253. const ActivityLog = Parse.Object.extend('ActivityLog');
  1254. const activityLog = new ActivityLog();
  1255. activityLog.set('company', this.getCompanyPointer());
  1256. activityLog.set('project', {
  1257. __type: 'Pointer',
  1258. className: 'Project',
  1259. objectId: projectId
  1260. });
  1261. activityLog.set('operator', Parse.User.current());
  1262. activityLog.set('action', '尾款跟进');
  1263. activityLog.set('description', `客服开始跟进尾款:剩余金额 ¥${project.finalPaymentAmount}`);
  1264. activityLog.set('type', 'payment_followup');
  1265. await activityLog.save();
  1266. console.log('✅ 跟进记录已保存');
  1267. } catch (logError) {
  1268. console.warn('⚠️ ActivityLog表不存在,跳过日志记录', logError);
  1269. // 继续执行,不阻塞跟进功能
  1270. }
  1271. // 获取当前公司ID
  1272. const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
  1273. // 导航到wxwork模块的项目详情页,并定位到售后归档阶段
  1274. this.router.navigate(['/wxwork', cid, 'project', projectId, 'aftercare'], {
  1275. queryParams: { focus: 'payment' }
  1276. });
  1277. } catch (error) {
  1278. console.error('❌ 开始跟进失败:', error);
  1279. window?.fmode?.alert('跳转失败,请稍后重试');
  1280. }
  1281. }
  1282. // 新增:查看项目详情
  1283. viewProjectDetail(projectId: string): void {
  1284. console.log(`📂 查看项目详情: ${projectId}`);
  1285. // 获取当前公司ID
  1286. const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
  1287. // 导航到wxwork模块的项目详情页
  1288. this.router.navigate(['/wxwork', cid, 'project', projectId]);
  1289. }
  1290. // ==================== 待办任务相关方法(复用组长端逻辑) ====================
  1291. /**
  1292. * 从问题板块加载待办任务(完全复用组长端逻辑)
  1293. */
  1294. async loadTodoTasksFromIssues(): Promise<void> {
  1295. this.loadingTodoTasks.set(true);
  1296. this.todoTaskError.set('');
  1297. try {
  1298. console.log('🔍 [客服-待办任务] 开始加载待办任务...');
  1299. // 使用 FmodeParse.with('nova') 直接创建查询,与组长端一致
  1300. const Parse: any = FmodeParse.with('nova');
  1301. const query = new Parse.Query('ProjectIssue');
  1302. // 筛选条件:待处理 + 处理中
  1303. query.containedIn('status', ['待处理', '处理中']);
  1304. query.notEqualTo('isDeleted', true);
  1305. // 关联数据
  1306. query.include(['project', 'creator', 'assignee']);
  1307. // 排序:更新时间倒序
  1308. query.descending('updatedAt');
  1309. // 限制数量
  1310. query.limit(50);
  1311. const results = await query.find();
  1312. console.log(`📊 [客服-待办任务] 找到 ${results.length} 个问题`);
  1313. // 数据转换(异步处理以支持 fetch,与组长端一致)
  1314. const tasks: TodoTaskFromIssue[] = await Promise.all(results.map(async (obj: any) => {
  1315. let project = obj.get('project');
  1316. const assignee = obj.get('assignee');
  1317. const creator = obj.get('creator');
  1318. const data = obj.get('data') || {};
  1319. let projectName = '未知项目';
  1320. let projectId = '';
  1321. // 如果 project 存在,尝试获取完整数据
  1322. if (project) {
  1323. projectId = project.id;
  1324. // 尝试从已加载的对象获取 name
  1325. projectName = project.get('name');
  1326. // 如果 name 为空,使用 Parse.Query 查询项目
  1327. if (!projectName && projectId) {
  1328. try {
  1329. console.log(`🔄 查询项目数据: ${projectId}`);
  1330. const projectQuery = new Parse.Query('Project');
  1331. const fetchedProject = await projectQuery.get(projectId);
  1332. projectName = fetchedProject.get('name') || fetchedProject.get('title') || '未知项目';
  1333. console.log(`✅ 项目名称: ${projectName}`);
  1334. } catch (error) {
  1335. console.warn(`⚠️ 无法查询项目 ${projectId}:`, error);
  1336. }
  1337. }
  1338. }
  1339. return {
  1340. id: obj.id,
  1341. title: obj.get('title') || obj.get('description')?.slice(0, 40) || '未命名问题',
  1342. description: obj.get('description'),
  1343. priority: obj.get('priority') as IssuePriority || 'medium',
  1344. type: obj.get('issueType') as IssueType || 'task',
  1345. status: this.zh2enStatus(obj.get('status')) as IssueStatus,
  1346. projectId,
  1347. projectName,
  1348. relatedSpace: obj.get('relatedSpace') || data.relatedSpace,
  1349. relatedStage: obj.get('relatedStage') || data.relatedStage,
  1350. assigneeName: assignee?.get('name') || assignee?.get('realname') || '未指派',
  1351. creatorName: creator?.get('name') || creator?.get('realname') || '未知',
  1352. createdAt: obj.get('createdAt') || new Date(),
  1353. updatedAt: obj.get('updatedAt') || new Date(),
  1354. dueDate: obj.get('dueDate'),
  1355. tags: (data.tags || []) as string[]
  1356. };
  1357. }));
  1358. // 按优先级排序
  1359. tasks.sort((a, b) => {
  1360. const priorityA = this.getPriorityOrder(a.priority);
  1361. const priorityB = this.getPriorityOrder(b.priority);
  1362. if (priorityA !== priorityB) {
  1363. return priorityA - priorityB;
  1364. }
  1365. return +new Date(b.updatedAt) - +new Date(a.updatedAt);
  1366. });
  1367. this.todoTasksFromIssues.set(tasks);
  1368. // ⭐ 计算紧急事件(复用组长端逻辑)
  1369. await this.loadProjectTimelineData();
  1370. this.calculateUrgentEvents();
  1371. console.log(`✅ [客服-待办任务] 加载完成: ${tasks.length} 个任务`);
  1372. } catch (error) {
  1373. console.error('❌ [客服-待办任务] 加载失败:', error);
  1374. this.todoTaskError.set('加载待办任务失败,请稍后重试');
  1375. this.todoTasksFromIssues.set([]);
  1376. this.urgentTasks.set([]);
  1377. } finally {
  1378. this.loadingTodoTasks.set(false);
  1379. }
  1380. }
  1381. /**
  1382. * 获取优先级顺序
  1383. */
  1384. private getPriorityOrder(priority: IssuePriority): number {
  1385. const order: Record<IssuePriority, number> = {
  1386. urgent: 0,
  1387. critical: 0,
  1388. high: 1,
  1389. medium: 2,
  1390. low: 3
  1391. };
  1392. return order[priority] || 999;
  1393. }
  1394. /**
  1395. * 状态映射(中文 -> 英文)
  1396. */
  1397. private zh2enStatus(status: string): IssueStatus {
  1398. const map: Record<string, IssueStatus> = {
  1399. '待处理': 'open',
  1400. '处理中': 'in_progress',
  1401. '已解决': 'resolved',
  1402. '已关闭': 'closed'
  1403. };
  1404. return map[status] || 'open';
  1405. }
  1406. /**
  1407. * ⭐ 加载项目时间轴数据(用于计算紧急事件)
  1408. * 复用组长端逻辑:从 ProjectTeam 表获取项目与设计师的关联关系
  1409. */
  1410. async loadProjectTimelineData(): Promise<void> {
  1411. try {
  1412. const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
  1413. // 查询当前公司的所有项目
  1414. const projectQuery = new Parse.Query('Project');
  1415. projectQuery.equalTo('company', cid);
  1416. projectQuery.notEqualTo('isDeleted', true);
  1417. projectQuery.limit(100);
  1418. const projects = await projectQuery.find();
  1419. console.log(`📊 [紧急事件] 查询到 ${projects.length} 个项目`);
  1420. // 转换为项目时间轴格式
  1421. this.projectTimelineData = projects.map((project: any) => {
  1422. const data = project.get('data') || {};
  1423. const phaseDeadlines = data.phaseDeadlines || {};
  1424. // 获取小图对图时间
  1425. const reviewDate = project.get('demoday') || project.get('reviewDate');
  1426. // 获取交付时间
  1427. const deliveryDate = project.get('deadline') ||
  1428. project.get('deliveryDate') ||
  1429. project.get('expectedDeliveryDate');
  1430. // 获取开始时间
  1431. const startDate = project.get('createdAt') || project.createdAt;
  1432. // 获取当前阶段
  1433. const currentStage = project.get('currentStage') || '建模阶段';
  1434. // 获取设计师名称
  1435. const assignee = project.get('assignee');
  1436. const designerName = assignee?.get('name') || assignee?.get('realname') || '未分配';
  1437. // 获取空间交付物汇总
  1438. const spaceDeliverableSummary = data.spaceDeliverableSummary;
  1439. return {
  1440. projectId: project.id,
  1441. projectName: project.get('name') || project.get('title') || '未命名项目',
  1442. designerId: assignee?.id,
  1443. designerName,
  1444. startDate: startDate ? new Date(startDate) : new Date(),
  1445. endDate: deliveryDate ? new Date(deliveryDate) : new Date(),
  1446. deliveryDate: deliveryDate ? new Date(deliveryDate) : undefined,
  1447. reviewDate: reviewDate ? new Date(reviewDate) : undefined,
  1448. currentStage,
  1449. stageName: currentStage,
  1450. stageProgress: 50,
  1451. status: 'normal' as const,
  1452. isStalled: false,
  1453. stalledDays: 0,
  1454. urgentCount: 0,
  1455. priority: 'medium' as const,
  1456. spaceName: '',
  1457. customerName: project.get('customerName') || '',
  1458. phaseDeadlines,
  1459. spaceDeliverableSummary
  1460. };
  1461. });
  1462. console.log(`✅ [紧急事件] 项目时间轴数据准备完成: ${this.projectTimelineData.length} 条`);
  1463. } catch (error) {
  1464. console.error('❌ [紧急事件] 加载项目时间轴数据失败:', error);
  1465. }
  1466. }
  1467. /**
  1468. * 🆕 从项目时间轴数据计算紧急事件
  1469. * 复用组长端逻辑:识别截止时间已到或即将到达但未完成的关键节点
  1470. */
  1471. calculateUrgentEvents(): void {
  1472. this.loadingUrgentEvents.set(true);
  1473. const events: UrgentEvent[] = [];
  1474. const now = new Date();
  1475. const oneDayMs = 24 * 60 * 60 * 1000;
  1476. try {
  1477. // 从 projectTimelineData 中提取数据
  1478. this.projectTimelineData.forEach(project => {
  1479. // 1. 检查小图对图事件
  1480. if (project.reviewDate) {
  1481. const reviewTime = project.reviewDate.getTime();
  1482. const timeDiff = reviewTime - now.getTime();
  1483. const daysDiff = Math.ceil(timeDiff / oneDayMs);
  1484. // 如果小图对图已经到期或即将到期(1天内),且不在已完成状态
  1485. if (daysDiff <= 1 && project.currentStage !== 'delivery' && project.currentStage !== '已完成' && project.currentStage !== '交付完成') {
  1486. events.push({
  1487. id: `${project.projectId}-review`,
  1488. title: `小图对图截止`,
  1489. description: `项目「${project.projectName}」的小图对图时间已${daysDiff < 0 ? '逾期' : '临近'}`,
  1490. eventType: 'review',
  1491. deadline: project.reviewDate,
  1492. projectId: project.projectId,
  1493. projectName: project.projectName,
  1494. designerName: project.designerName,
  1495. urgencyLevel: daysDiff < 0 ? 'critical' : daysDiff === 0 ? 'high' : 'medium',
  1496. overdueDays: -daysDiff
  1497. });
  1498. }
  1499. }
  1500. // 2. 检查交付事件
  1501. if (project.deliveryDate) {
  1502. const deliveryTime = project.deliveryDate.getTime();
  1503. const timeDiff = deliveryTime - now.getTime();
  1504. const daysDiff = Math.ceil(timeDiff / oneDayMs);
  1505. // 如果交付已经到期或即将到期(1天内),且不在已完成状态
  1506. if (daysDiff <= 1 && project.currentStage !== 'delivery' && project.currentStage !== '已完成' && project.currentStage !== '交付完成') {
  1507. const summary = project.spaceDeliverableSummary;
  1508. const completionRate = summary?.overallCompletionRate || 0;
  1509. events.push({
  1510. id: `${project.projectId}-delivery`,
  1511. title: `项目交付截止`,
  1512. description: `项目「${project.projectName}」需要在 ${project.deliveryDate.toLocaleDateString()} 交付(当前完成率 ${completionRate}%)`,
  1513. eventType: 'delivery',
  1514. deadline: project.deliveryDate,
  1515. projectId: project.projectId,
  1516. projectName: project.projectName,
  1517. designerName: project.designerName,
  1518. urgencyLevel: daysDiff < 0 ? 'critical' : daysDiff === 0 ? 'high' : 'medium',
  1519. overdueDays: -daysDiff,
  1520. completionRate
  1521. });
  1522. }
  1523. }
  1524. // 3. 检查各阶段截止时间
  1525. if (project.phaseDeadlines) {
  1526. const phaseMap: Record<string, string> = {
  1527. modeling: '建模',
  1528. softDecor: '软装',
  1529. rendering: '渲染',
  1530. postProcessing: '后期'
  1531. };
  1532. Object.entries(project.phaseDeadlines).forEach(([key, phaseInfo]: [string, any]) => {
  1533. if (phaseInfo && phaseInfo.deadline) {
  1534. const deadline = new Date(phaseInfo.deadline);
  1535. const phaseTime = deadline.getTime();
  1536. const timeDiff = phaseTime - now.getTime();
  1537. const daysDiff = Math.ceil(timeDiff / oneDayMs);
  1538. // 如果阶段已经到期或即将到期(1天内),且状态不是已完成
  1539. if (daysDiff <= 1 && phaseInfo.status !== 'completed') {
  1540. const phaseName = phaseMap[key] || key;
  1541. // 获取该阶段的完成率
  1542. const summary = project.spaceDeliverableSummary;
  1543. let completionRate = 0;
  1544. if (summary && summary.phaseProgress) {
  1545. const phaseProgress = summary.phaseProgress[key as keyof typeof summary.phaseProgress];
  1546. completionRate = phaseProgress?.completionRate || 0;
  1547. }
  1548. events.push({
  1549. id: `${project.projectId}-phase-${key}`,
  1550. title: `${phaseName}阶段截止`,
  1551. description: `项目「${project.projectName}」的${phaseName}阶段截止时间已${daysDiff < 0 ? '逾期' : '临近'}(完成率 ${completionRate}%)`,
  1552. eventType: 'phase_deadline',
  1553. phaseName,
  1554. deadline,
  1555. projectId: project.projectId,
  1556. projectName: project.projectName,
  1557. designerName: project.designerName,
  1558. urgencyLevel: daysDiff < 0 ? 'critical' : daysDiff === 0 ? 'high' : 'medium',
  1559. overdueDays: -daysDiff,
  1560. completionRate
  1561. });
  1562. }
  1563. }
  1564. });
  1565. }
  1566. });
  1567. // 按紧急程度和时间排序
  1568. events.sort((a, b) => {
  1569. // 首先按紧急程度排序
  1570. const urgencyOrder: Record<string, number> = { critical: 0, high: 1, medium: 2 };
  1571. const urgencyDiff = urgencyOrder[a.urgencyLevel] - urgencyOrder[b.urgencyLevel];
  1572. if (urgencyDiff !== 0) return urgencyDiff;
  1573. // 相同紧急程度,按截止时间排序(越早越靠前)
  1574. return a.deadline.getTime() - b.deadline.getTime();
  1575. });
  1576. this.urgentEventsList.set(events);
  1577. console.log(`✅ [客服-紧急事件] 计算完成,共 ${events.length} 个紧急事件`);
  1578. } catch (error) {
  1579. console.error('❌ [客服-紧急事件] 计算失败:', error);
  1580. } finally {
  1581. this.loadingUrgentEvents.set(false);
  1582. }
  1583. }
  1584. /**
  1585. * 手动刷新待办任务和紧急事件
  1586. */
  1587. async refreshTodoTasks(): Promise<void> {
  1588. console.log('🔄 [客服-待办任务] 手动刷新...');
  1589. await this.loadTodoTasksFromIssues();
  1590. // 紧急事件会在 loadTodoTasksFromIssues 中自动刷新
  1591. }
  1592. /**
  1593. * ⭐ 从紧急事件面板查看项目
  1594. */
  1595. onUrgentEventViewProject(projectId: string): void {
  1596. console.log('🔍 [紧急事件] 查看项目:', projectId);
  1597. // 跳转到项目详情页
  1598. const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
  1599. this.router.navigate(['/wxwork', cid, 'project', projectId, 'order'], {
  1600. queryParams: { roleName: 'customer-service' }
  1601. });
  1602. }
  1603. /**
  1604. * ⭐ 从待办任务面板查看详情
  1605. */
  1606. onTodoTaskViewDetails(task: TodoTaskFromIssue): void {
  1607. console.log('🔍 [待办任务] 查看详情:', task.title);
  1608. // 跳转到项目详情页,并打开问题板块
  1609. const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
  1610. this.router.navigate(['/wxwork', cid, 'project', task.projectId, 'order'], {
  1611. queryParams: {
  1612. openIssues: 'true',
  1613. highlightIssue: task.id,
  1614. roleName: 'customer-service'
  1615. }
  1616. });
  1617. }
  1618. /**
  1619. * ⭐ 从待办任务面板标记为已读
  1620. */
  1621. async onTodoTaskMarkAsRead(task: TodoTaskFromIssue): Promise<void> {
  1622. try {
  1623. console.log('✅ [待办任务] 标记为已读:', task.title);
  1624. // 从列表中移除
  1625. const currentTasks = this.todoTasksFromIssues();
  1626. const updatedTasks = currentTasks.filter(t => t.id !== task.id);
  1627. this.todoTasksFromIssues.set(updatedTasks);
  1628. // ⭐ 刷新紧急事件列表
  1629. await this.loadProjectTimelineData();
  1630. this.calculateUrgentEvents();
  1631. console.log(`✅ 标记问题为已读: ${task.title}`);
  1632. } catch (error) {
  1633. console.error('❌ 标记已读失败:', error);
  1634. }
  1635. }
  1636. /**
  1637. * ⭐ 刷新待办任务和紧急事件
  1638. */
  1639. async onRefreshTodoTasks(): Promise<void> {
  1640. console.log('🔄 [待办任务] 刷新...');
  1641. await this.refreshTodoTasks();
  1642. }
  1643. /**
  1644. * ⭐ 将紧急事件标记为已处理(用于紧急事件面板的checkbox)
  1645. */
  1646. async onUrgentEventMarkAsHandled(event: UrgentEvent): Promise<void> {
  1647. try {
  1648. console.log('✅ [紧急事件] 标记为已处理:', event.title);
  1649. // 从紧急事件列表中移除
  1650. const currentEvents = this.urgentEventsList();
  1651. const updatedEvents = currentEvents.filter(e => e.id !== event.id);
  1652. this.urgentEventsList.set(updatedEvents);
  1653. // ⭐ 刷新紧急事件列表
  1654. await this.loadProjectTimelineData();
  1655. this.calculateUrgentEvents();
  1656. } catch (error) {
  1657. console.error('❌ 标记已处理失败:', error);
  1658. }
  1659. }
  1660. /**
  1661. * 获取优先级配置
  1662. */
  1663. getPriorityConfig(priority: IssuePriority): { label: string; icon: string; color: string; order: number } {
  1664. const config: Record<IssuePriority, { label: string; icon: string; color: string; order: number }> = {
  1665. urgent: { label: '紧急', icon: '🔴', color: '#dc2626', order: 0 },
  1666. critical: { label: '紧急', icon: '🔴', color: '#dc2626', order: 0 },
  1667. high: { label: '高', icon: '🟠', color: '#ea580c', order: 1 },
  1668. medium: { label: '中', icon: '🟡', color: '#ca8a04', order: 2 },
  1669. low: { label: '低', icon: '⚪', color: '#9ca3af', order: 3 }
  1670. };
  1671. return config[priority] || config.medium;
  1672. }
  1673. /**
  1674. * 获取问题类型标签
  1675. * IssueType 定义: 'bug' | 'task' | 'feedback' | 'risk' | 'feature'
  1676. */
  1677. getIssueTypeLabel(type: IssueType): string {
  1678. const labels: Record<IssueType, string> = {
  1679. bug: '缺陷',
  1680. feature: '需求',
  1681. task: '任务',
  1682. feedback: '反馈',
  1683. risk: '风险'
  1684. };
  1685. return labels[type] || '其他';
  1686. }
  1687. /**
  1688. * 获取状态标签
  1689. * IssueStatus 定义: 'open' | 'in_progress' | 'resolved' | 'closed'
  1690. */
  1691. getIssueStatusLabel(status: IssueStatus): string {
  1692. const labels: Record<IssueStatus, string> = {
  1693. open: '待处理',
  1694. in_progress: '处理中',
  1695. resolved: '已解决',
  1696. closed: '已关闭'
  1697. };
  1698. return labels[status] || '待处理';
  1699. }
  1700. /**
  1701. * 跳转到项目问题详情
  1702. */
  1703. navigateToIssue(task: TodoTaskFromIssue): void {
  1704. console.log(`📋 跳转到问题详情: ${task.id}, 项目ID: ${task.projectId}`);
  1705. // 获取当前公司ID
  1706. const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
  1707. // 导航到wxwork模块的项目问题详情页
  1708. this.router.navigate(['/wxwork', cid, 'project', task.projectId, 'issues'], {
  1709. queryParams: { issueId: task.id }
  1710. });
  1711. }
  1712. // 新增:一键发送大图
  1713. sendLargeImages(projectId: string): void {
  1714. const projects = this.pendingFinalPaymentProjects();
  1715. const project = projects.find(p => p.projectId === projectId);
  1716. if (!project) return;
  1717. console.log(`正在为项目 ${projectId} 发送大图到企业微信...`);
  1718. // 模拟发送过程
  1719. setTimeout(() => {
  1720. const updatedProjects = projects.map(p => {
  1721. if (p.projectId === projectId) {
  1722. return { ...p, largeImagesSent: true };
  1723. }
  1724. return p;
  1725. });
  1726. this.pendingFinalPaymentProjects.set(updatedProjects);
  1727. console.log(`✅ 项目 ${projectId} 大图已成功发送到企业微信服务群`);
  1728. console.log(`📱 已同步发送支付成功与大图交付通知`);
  1729. window?.fmode?.alert(`🎉 大图发送成功!
  1730. ✅ 已完成操作:
  1731. • 大图已发送至企业微信服务群
  1732. • 已通知客户支付成功
  1733. • 已确认大图交付完成
  1734. 项目:${project.projectName}
  1735. 客户:${project.customerName}`);
  1736. }, 2000);
  1737. }
  1738. /**
  1739. * 标记问题为已读
  1740. */
  1741. async markAsRead(task: TodoTaskFromIssue): Promise<void> {
  1742. try {
  1743. // 本地隐藏(不修改数据库)
  1744. const currentTasks = this.todoTasksFromIssues();
  1745. const updatedTasks = currentTasks.filter(t => t.id !== task.id);
  1746. this.todoTasksFromIssues.set(updatedTasks);
  1747. // ⭐ 刷新紧急事件列表
  1748. await this.loadProjectTimelineData();
  1749. this.calculateUrgentEvents();
  1750. console.log(`✅ 标记问题为已读: ${task.title}`);
  1751. } catch (error) {
  1752. console.error('❌ 标记已读失败:', error);
  1753. }
  1754. }
  1755. /**
  1756. * 格式化相对时间(精确到秒)
  1757. */
  1758. formatRelativeTime(date: Date | string): string {
  1759. if (!date) {
  1760. return '未知时间';
  1761. }
  1762. try {
  1763. const targetDate = new Date(date);
  1764. const now = new Date();
  1765. const diff = now.getTime() - targetDate.getTime();
  1766. const seconds = Math.floor(diff / 1000);
  1767. const minutes = Math.floor(seconds / 60);
  1768. const hours = Math.floor(minutes / 60);
  1769. const days = Math.floor(hours / 24);
  1770. if (seconds < 60) {
  1771. return `${seconds}秒前`;
  1772. } else if (minutes < 60) {
  1773. return `${minutes}分钟前`;
  1774. } else if (hours < 24) {
  1775. return `${hours}小时前`;
  1776. } else if (days < 7) {
  1777. return `${days}天前`;
  1778. } else {
  1779. return targetDate.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' });
  1780. }
  1781. } catch (error) {
  1782. console.error('❌ formatRelativeTime 错误:', error, 'date:', date);
  1783. return '时间格式错误';
  1784. }
  1785. }
  1786. /**
  1787. * 格式化精确时间(用于 tooltip)
  1788. * 格式:YYYY-MM-DD HH:mm:ss
  1789. */
  1790. formatExactTime(date: Date | string): string {
  1791. if (!date) {
  1792. return '未知时间';
  1793. }
  1794. try {
  1795. const d = new Date(date);
  1796. const year = d.getFullYear();
  1797. const month = String(d.getMonth() + 1).padStart(2, '0');
  1798. const day = String(d.getDate()).padStart(2, '0');
  1799. const hours = String(d.getHours()).padStart(2, '0');
  1800. const minutes = String(d.getMinutes()).padStart(2, '0');
  1801. const seconds = String(d.getSeconds()).padStart(2, '0');
  1802. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
  1803. } catch (error) {
  1804. console.error('❌ formatExactTime 错误:', error, 'date:', date);
  1805. return '时间格式错误';
  1806. }
  1807. }
  1808. }