dashboard.ts 83 KB

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