dashboard.ts 76 KB

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