dashboard.ts 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188
  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. const Parse = FmodeParse.with('nova');
  11. // 项目数据接口
  12. interface ProjectData {
  13. id: string;
  14. title: string;
  15. customerName: string;
  16. customerPhone?: string;
  17. status: string;
  18. stage: string;
  19. assigneeName?: string;
  20. createdAt: Date;
  21. updatedAt: Date;
  22. deadline?: Date;
  23. priority?: string;
  24. description?: string;
  25. }
  26. // 任务数据接口
  27. interface Task {
  28. id: string;
  29. projectId: string;
  30. projectName: string;
  31. title: string;
  32. stage: string;
  33. deadline: Date;
  34. isOverdue: boolean;
  35. isCompleted: boolean;
  36. priority: 'high' | 'medium' | 'low';
  37. assignee: string;
  38. description?: string;
  39. status: string;
  40. }
  41. // 项目更新联合类型
  42. interface ProjectUpdate {
  43. id: string;
  44. name?: string;
  45. customerName: string;
  46. status: string;
  47. updatedAt?: Date;
  48. createdAt?: Date;
  49. }
  50. interface FeedbackUpdate {
  51. id: string;
  52. customerName: string;
  53. content: string;
  54. status: string;
  55. createdAt: Date;
  56. feedbackType: string;
  57. }
  58. // 项目类型(用于项目动态)
  59. interface Project {
  60. id: string;
  61. name: string;
  62. customerName: string;
  63. status: string;
  64. updatedAt?: Date;
  65. createdAt?: Date;
  66. deadline?: Date;
  67. }
  68. // 客户反馈类型
  69. interface CustomerFeedback {
  70. id: string;
  71. projectId: string;
  72. customerName: string;
  73. content: string;
  74. status: string;
  75. createdAt: Date;
  76. }
  77. @Component({
  78. selector: 'app-dashboard',
  79. standalone: true,
  80. imports: [CommonModule, FormsModule, RouterModule],
  81. templateUrl: './dashboard.html',
  82. styleUrls: ['./dashboard.scss', './dashboard-urgent-tasks-enhanced.scss', '../customer-service-styles.scss']
  83. })
  84. export class Dashboard implements OnInit, OnDestroy {
  85. // 数据看板统计
  86. stats = {
  87. totalProjects: signal(0), // 项目总数
  88. newConsultations: signal(0), // 新咨询数
  89. pendingAssignments: signal(0), // 待分配项目数(原待派单数)
  90. exceptionProjects: signal(0), // 异常项目数
  91. afterSalesCount: signal(0) // 售后服务数量
  92. };
  93. // 紧急任务列表
  94. urgentTasks = signal<Task[]>([]);
  95. // 任务处理状态
  96. taskProcessingState = signal<Partial<Record<string, { inProgress: boolean; progress: number }>>>({});
  97. // 新增:待跟进尾款项目列表(真实数据)
  98. pendingFinalPaymentProjects = signal<Array<{
  99. id: string;
  100. projectId: string;
  101. projectName: string;
  102. customerName: string;
  103. customerPhone: string;
  104. finalPaymentAmount: number;
  105. dueDate: Date;
  106. status: string;
  107. overdueDay: number;
  108. }>>([]);
  109. // 项目动态流
  110. projectUpdates = signal<(Project | CustomerFeedback)[]>([]);
  111. // 搜索关键词
  112. searchTerm = signal('');
  113. // 筛选后的项目更新
  114. filteredUpdates = computed(() => {
  115. if (!this.searchTerm()) return this.projectUpdates();
  116. return this.projectUpdates().filter(item => {
  117. if ('name' in item) {
  118. // 项目
  119. return item.name.toLowerCase().includes(this.searchTerm().toLowerCase()) ||
  120. item.customerName.toLowerCase().includes(this.searchTerm().toLowerCase()) ||
  121. item.status.toLowerCase().includes(this.searchTerm().toLowerCase());
  122. } else {
  123. // 反馈
  124. return 'content' in item && item.content.toLowerCase().includes(this.searchTerm().toLowerCase()) ||
  125. 'status' in item && item.status.toLowerCase().includes(this.searchTerm().toLowerCase());
  126. }
  127. });
  128. });
  129. currentDate = new Date();
  130. // 回到顶部按钮可见性信号
  131. showBackToTopSignal = signal(false);
  132. // 任务表单可见性
  133. isTaskFormVisible = signal(false);
  134. // 项目列表(用于下拉选择)
  135. projectList = signal<any[]>([]);
  136. // 空间列表(用于下拉选择)
  137. spaceList = signal<any[]>([]);
  138. // 团队成员列表(用于指派)
  139. teamMembers = signal<any[]>([]);
  140. // 新任务数据
  141. newTask: any = {
  142. title: '',
  143. description: '',
  144. projectId: '',
  145. spaceId: '',
  146. stage: '订单分配',
  147. region: '',
  148. priority: 'high',
  149. assigneeId: '',
  150. deadline: new Date()
  151. };
  152. // 用于日期时间输入的属性
  153. deadlineInput = '';
  154. // 预设快捷时长选项
  155. timePresets = [
  156. { label: '1小时内', hours: 1 },
  157. { label: '3小时内', hours: 3 },
  158. { label: '6小时内', hours: 6 },
  159. { label: '12小时内', hours: 12 },
  160. { label: '24小时内', hours: 24 }
  161. ];
  162. // 选中的预设时长
  163. selectedPreset = '';
  164. // 自定义时间弹窗可见性
  165. isCustomTimeVisible = false;
  166. // 自定义选择的日期和时间
  167. customDate = new Date();
  168. customTime = '';
  169. // 错误提示信息
  170. deadlineError = '';
  171. // 提交按钮是否禁用
  172. isSubmitDisabled = false;
  173. // 下拉框可见性
  174. deadlineDropdownVisible = false;
  175. // 日期范围限制
  176. get todayDate(): string {
  177. return new Date().toISOString().split('T')[0];
  178. }
  179. get sevenDaysLaterDate(): string {
  180. const date = new Date();
  181. date.setDate(date.getDate() + 7);
  182. return date.toISOString().split('T')[0];
  183. }
  184. constructor(
  185. private router: Router,
  186. private route: ActivatedRoute,
  187. private profileService: ProfileService,
  188. private urgentTaskService: UrgentTaskService,
  189. private activityLogService: ActivityLogService
  190. ) {}
  191. // 当前用户和公司信息
  192. currentUser = signal<any>(null);
  193. company = signal<any>(null);
  194. // 初始化用户和公司信息
  195. private async initializeUserAndCompany(): Promise<void> {
  196. try {
  197. const profile = await this.profileService.getCurrentProfile();
  198. this.currentUser.set(profile);
  199. // 获取公司信息 - 映三色帐套
  200. const companyQuery = new Parse.Query('Company');
  201. companyQuery.equalTo('objectId', 'cDL6R1hgSi');
  202. const company = await companyQuery.first();
  203. if (!company) {
  204. throw new Error('未找到公司信息');
  205. }
  206. this.company.set(company);
  207. console.log('✅ 用户和公司信息初始化完成');
  208. } catch (error) {
  209. console.error('❌ 用户和公司信息初始化失败:', error);
  210. throw error;
  211. }
  212. }
  213. // 获取公司指针
  214. private getCompanyPointer(): any {
  215. if (!this.company()) {
  216. throw new Error('公司信息未加载');
  217. }
  218. return {
  219. __type: 'Pointer',
  220. className: 'Company',
  221. objectId: this.company().id
  222. };
  223. }
  224. // 创建带公司过滤的查询
  225. private createQuery(className: string): any {
  226. const query = new Parse.Query(className);
  227. query.equalTo('company', this.getCompanyPointer());
  228. query.notEqualTo('isDeleted', true);
  229. return query;
  230. }
  231. async ngOnInit(): Promise<void> {
  232. try {
  233. await this.initializeUserAndCompany();
  234. await this.loadDashboardData();
  235. // 添加滚动事件监听
  236. window.addEventListener('scroll', this.onScroll.bind(this));
  237. } catch (error) {
  238. console.error('❌ 客服工作台初始化失败:', error);
  239. }
  240. }
  241. // 加载仪表板数据
  242. private async loadDashboardData(): Promise<void> {
  243. try {
  244. await Promise.all([
  245. this.loadConsultationStats(),
  246. this.loadUrgentTasks(),
  247. this.loadProjectUpdates(),
  248. this.loadCRMQueues(),
  249. this.loadPendingFinalPaymentProjects()
  250. ]);
  251. console.log('✅ 客服仪表板数据加载完成');
  252. } catch (error) {
  253. console.error('❌ 客服仪表板数据加载失败:', error);
  254. throw error;
  255. }
  256. }
  257. // 加载咨询统计数据
  258. private async loadConsultationStats(): Promise<void> {
  259. try {
  260. const todayStart = new Date();
  261. todayStart.setHours(0, 0, 0, 0);
  262. // 项目总数
  263. const totalProjectQuery = this.createQuery('Project');
  264. const totalProjects = await totalProjectQuery.count();
  265. this.stats.totalProjects.set(totalProjects);
  266. // 新咨询数(今日新增的项目)
  267. const consultationQuery = this.createQuery('Project');
  268. consultationQuery.greaterThanOrEqualTo('createdAt', todayStart);
  269. const newConsultations = await consultationQuery.count();
  270. this.stats.newConsultations.set(newConsultations);
  271. // 待分配项目数(阶段处于"订单分配"的项目)
  272. // 参考组长工作台的筛选逻辑:根据四大板块筛选
  273. // 订单分配阶段包括:order, pendingApproval, pendingAssignment, 订单分配, 待审批, 待分配
  274. // 查询所有项目,然后在客户端筛选(与组长工作台保持一致)
  275. const allProjectsQuery = this.createQuery('Project');
  276. allProjectsQuery.limit(1000); // 限制最多1000个项目
  277. const allProjects = await allProjectsQuery.find();
  278. // 使用与组长工作台相同的筛选逻辑
  279. const orderPhaseProjects = allProjects.filter(p => {
  280. const currentStage = p.get('currentStage') || '';
  281. const stage = p.get('stage') || '';
  282. const stageValue = (currentStage || stage).toString().trim().toLowerCase();
  283. // 订单分配阶段的所有变体(与组长工作台mapStageToCorePhase保持一致)
  284. const isOrderPhase = stageValue === 'order' ||
  285. stageValue === 'pendingapproval' ||
  286. stageValue === 'pendingassignment' ||
  287. stageValue === '订单分配' ||
  288. stageValue === '待审批' ||
  289. stageValue === '待分配';
  290. // 调试日志:输出每个项目的阶段信息
  291. if (isOrderPhase) {
  292. console.log(`📋 订单分配项目: ${p.get('title')}, currentStage="${currentStage}", stage="${stage}", 匹配值="${stageValue}"`);
  293. }
  294. return isOrderPhase;
  295. });
  296. const pendingAssignments = orderPhaseProjects.length;
  297. this.stats.pendingAssignments.set(pendingAssignments);
  298. console.log(`✅ 待分配项目统计: 总项目数=${allProjects.length}, 订单分配阶段项目数=${pendingAssignments}`);
  299. // 异常项目数(使用ProjectIssue表)
  300. const issueQuery = this.createQuery('ProjectIssue');
  301. issueQuery.equalTo('priority', 'high');
  302. issueQuery.equalTo('status', 'open');
  303. const exceptionProjects = await issueQuery.count();
  304. this.stats.exceptionProjects.set(exceptionProjects);
  305. // 售后服务数量(使用ProjectFeedback表,类型为投诉的待处理反馈)
  306. const feedbackQuery = this.createQuery('ProjectFeedback');
  307. feedbackQuery.equalTo('status', 'pending');
  308. feedbackQuery.equalTo('feedbackType', 'complaint');
  309. const afterSalesCount = await feedbackQuery.count();
  310. this.stats.afterSalesCount.set(afterSalesCount);
  311. console.log(`✅ 咨询统计: 项目总数${totalProjects}, 新咨询${newConsultations}, 待分配${pendingAssignments}, 异常${exceptionProjects}, 售后${afterSalesCount}`);
  312. } catch (error) {
  313. console.error('❌ 咨询统计加载失败:', error);
  314. // 不抛出错误,允许其他数据继续加载
  315. }
  316. }
  317. // 降级到模拟数据
  318. private loadMockData(): void {
  319. console.warn('⚠️ 使用模拟数据');
  320. this.loadUrgentTasks();
  321. this.loadProjectUpdates();
  322. this.loadCRMQueues();
  323. // loadPendingFinalPaymentProjects 已改为异步真实数据查询
  324. }
  325. // 添加滚动事件处理方法
  326. private onScroll(): void {
  327. this.showBackToTopSignal.set(window.scrollY > 300);
  328. }
  329. // 添加显示回到顶部按钮的计算属性
  330. showBackToTop = computed(() => this.showBackToTopSignal());
  331. // 清理事件监听器
  332. ngOnDestroy(): void {
  333. window.removeEventListener('scroll', this.onScroll.bind(this));
  334. }
  335. // 添加scrollToTop方法
  336. scrollToTop(): void {
  337. window.scrollTo({
  338. top: 0,
  339. behavior: 'smooth'
  340. });
  341. }
  342. // 查看人员考勤
  343. viewAttendance(): void {
  344. this.router.navigate(['/hr/attendance']);
  345. }
  346. // 加载紧急任务
  347. private async loadUrgentTasks(): Promise<void> {
  348. try {
  349. // 使用UrgentTaskService加载紧急事项
  350. const result = await this.urgentTaskService.findUrgentTasks({
  351. isCompleted: false
  352. }, 1, 20);
  353. // 转换数据格式以兼容现有UI
  354. const formattedTasks: Task[] = result.tasks.map(task => ({
  355. id: task.id,
  356. projectId: task.projectId,
  357. projectName: task.projectName,
  358. title: task.title,
  359. stage: task.stage,
  360. deadline: task.deadline,
  361. isOverdue: task.isOverdue,
  362. isCompleted: task.isCompleted,
  363. priority: task.priority as 'high' | 'medium' | 'low',
  364. assignee: task.assigneeName,
  365. description: task.description || '',
  366. status: task.status
  367. }));
  368. this.urgentTasks.set(formattedTasks);
  369. console.log(`✅ 紧急任务加载完成: ${formattedTasks.length} 个任务`);
  370. } catch (error) {
  371. console.error('❌ 紧急任务加载失败:', error);
  372. this.urgentTasks.set([]);
  373. }
  374. }
  375. // 加载CRM队列数据(已隐藏,暂不使用真实数据)
  376. private loadCRMQueues(): void {
  377. // CRM功能暂时隐藏,后续开发时再从Parse查询真实数据
  378. // 可以从ProjectFeedback表查询客户反馈和咨询记录
  379. console.log('⏸️ CRM队列功能暂时隐藏');
  380. }
  381. // 查看全部咨询列表
  382. goToConsultationList(): void {
  383. this.router.navigate(['/customer-service/consultation-list']);
  384. }
  385. // 加载项目动态
  386. private async loadProjectUpdates(): Promise<void> {
  387. try {
  388. const updates: (Project | CustomerFeedback)[] = [];
  389. // 1. 查询最新更新的项目
  390. const projectQuery = this.createQuery('Project');
  391. projectQuery.include(['contact', 'assignee']);
  392. projectQuery.descending('updatedAt');
  393. projectQuery.limit(10);
  394. const projects = await projectQuery.find();
  395. for (const project of projects) {
  396. const contact = project.get('contact');
  397. updates.push({
  398. id: project.id,
  399. name: project.get('title') || '未命名项目',
  400. customerName: contact?.get('name') || '未知客户',
  401. status: project.get('status') || '进行中',
  402. updatedAt: project.get('updatedAt'),
  403. createdAt: project.get('createdAt')
  404. });
  405. }
  406. // 2. 查询最新客户反馈
  407. const feedbackQuery = this.createQuery('ProjectFeedback');
  408. feedbackQuery.include(['contact', 'project']);
  409. feedbackQuery.descending('createdAt');
  410. feedbackQuery.limit(10);
  411. const feedbacks = await feedbackQuery.find();
  412. for (const feedback of feedbacks) {
  413. const contact = feedback.get('contact');
  414. updates.push({
  415. id: feedback.id,
  416. projectId: feedback.get('project')?.id || '',
  417. customerName: contact?.get('name') || '未知客户',
  418. content: feedback.get('content') || '无内容',
  419. status: feedback.get('status') || 'pending',
  420. createdAt: feedback.get('createdAt')
  421. });
  422. }
  423. // 按时间排序
  424. updates.sort((a, b) => {
  425. const aTime = ('updatedAt' in a && a.updatedAt) ? a.updatedAt.getTime() : (a.createdAt?.getTime() || 0);
  426. const bTime = ('updatedAt' in b && b.updatedAt) ? b.updatedAt.getTime() : (b.createdAt?.getTime() || 0);
  427. return bTime - aTime;
  428. });
  429. this.projectUpdates.set(updates);
  430. console.log(`✅ 项目动态加载完成: ${updates.length} 条动态`);
  431. } catch (error) {
  432. console.error('❌ 项目动态加载失败:', error);
  433. // 不抛出错误,允许其他数据继续加载
  434. }
  435. }
  436. // 处理任务完成
  437. async markTaskAsCompleted(taskId: string): Promise<void> {
  438. try {
  439. const task = this.urgentTasks().find(t => t.id === taskId);
  440. await this.urgentTaskService.markAsCompleted(taskId);
  441. // 记录活动日志
  442. if (task) {
  443. try {
  444. const user = this.currentUser();
  445. await this.activityLogService.logActivity({
  446. actorId: user?.id || 'unknown',
  447. actorName: user?.get('name') || '客服',
  448. actorRole: user?.get('roleName') || 'customer_service',
  449. actionType: 'complete',
  450. module: 'urgent_task',
  451. entityType: 'UrgentTask',
  452. entityId: taskId,
  453. entityName: task.title,
  454. description: '完成了紧急事项',
  455. metadata: {
  456. priority: task.priority,
  457. projectName: task.projectName
  458. }
  459. });
  460. } catch (logError) {
  461. console.error('记录活动日志失败:', logError);
  462. }
  463. }
  464. // 重新加载任务列表
  465. await this.loadUrgentTasks();
  466. console.log('✅ 任务标记为已完成');
  467. } catch (error) {
  468. console.error('❌ 标记任务完成失败:', error);
  469. alert('操作失败,请稍后重试');
  470. }
  471. }
  472. // 删除任务
  473. async deleteTask(taskId: string): Promise<void> {
  474. if (!await window?.fmode?.confirm('确定要删除这个紧急事项吗?')) {
  475. return;
  476. }
  477. try {
  478. await this.urgentTaskService.deleteUrgentTask(taskId);
  479. // 重新加载任务列表
  480. await this.loadUrgentTasks();
  481. console.log('✅ 任务删除成功');
  482. } catch (error) {
  483. console.error('❌ 删除任务失败:', error);
  484. alert('删除失败,请稍后重试');
  485. }
  486. }
  487. // 处理派单操作
  488. handleAssignment(taskId: string): void {
  489. // 标记任务为处理中
  490. const task = this.urgentTasks().find(t => t.id === taskId);
  491. if (task) {
  492. // 初始化处理状态
  493. this.taskProcessingState.update(state => ({
  494. ...state,
  495. [task.id]: { inProgress: true, progress: 0 }
  496. }));
  497. // 模拟处理进度
  498. let progress = 0;
  499. const interval = setInterval(() => {
  500. progress += 10;
  501. this.taskProcessingState.update(state => ({
  502. ...state,
  503. [task.id]: { inProgress: progress < 100, progress }
  504. }));
  505. if (progress >= 100) {
  506. clearInterval(interval);
  507. // 处理完成后从列表中移除该任务
  508. this.urgentTasks.set(
  509. this.urgentTasks().filter(t => t.id !== task.id)
  510. );
  511. // 清除处理状态
  512. this.taskProcessingState.update(state => {
  513. const newState = { ...state };
  514. delete newState[task.id];
  515. return newState;
  516. });
  517. }
  518. }, 300);
  519. }
  520. // 更新统计数据
  521. this.stats.pendingAssignments.set(this.stats.pendingAssignments() - 1);
  522. }
  523. // 显示任务表单
  524. async showTaskForm(): Promise<void> {
  525. // 重置表单数据
  526. this.newTask = {
  527. title: '',
  528. description: '',
  529. projectId: '',
  530. spaceId: '',
  531. stage: '订单分配',
  532. region: '',
  533. priority: 'high',
  534. assigneeId: '',
  535. deadline: new Date()
  536. };
  537. // 重置相关状态
  538. this.deadlineError = '';
  539. this.isSubmitDisabled = false;
  540. // 计算并设置默认预设时长
  541. this.setDefaultPreset();
  542. // 加载下拉列表数据
  543. try {
  544. const [projects, members] = await Promise.all([
  545. this.urgentTaskService.getProjects(),
  546. this.urgentTaskService.getTeamMembers()
  547. ]);
  548. this.projectList.set(projects);
  549. this.teamMembers.set(members);
  550. this.spaceList.set([]); // 初始为空,等待选择项目后加载
  551. } catch (error) {
  552. console.error('加载下拉列表数据失败:', error);
  553. }
  554. // 显示表单
  555. this.isTaskFormVisible.set(true);
  556. // 添加iOS风格的面板显示动画
  557. setTimeout(() => {
  558. document.querySelector('.ios-panel')?.classList.add('ios-panel-visible');
  559. }, 10);
  560. }
  561. // 项目选择变化时加载空间列表
  562. async onProjectChange(projectId: string): Promise<void> {
  563. if (!projectId) {
  564. this.spaceList.set([]);
  565. return;
  566. }
  567. try {
  568. const spaces = await this.urgentTaskService.getProjectSpaces(projectId);
  569. this.spaceList.set(spaces);
  570. } catch (error) {
  571. console.error('加载空间列表失败:', error);
  572. this.spaceList.set([]);
  573. }
  574. }
  575. // 设置默认预设时长
  576. private setDefaultPreset(): void {
  577. const now = new Date();
  578. const todayEnd = new Date(now);
  579. todayEnd.setHours(23, 59, 59, 999);
  580. // 检查3小时后是否超过当天24:00
  581. const threeHoursLater = new Date(now.getTime() + 3 * 60 * 60 * 1000);
  582. if (threeHoursLater <= todayEnd) {
  583. // 3小时后未超过当天24:00,默认选中3小时内
  584. this.selectedPreset = '3';
  585. this.updatePresetDeadline(3);
  586. } else {
  587. // 3小时后超过当天24:00,默认选中当天24:00前
  588. this.selectedPreset = 'today';
  589. this.deadlineInput = todayEnd.toISOString().slice(0, 16);
  590. this.newTask.deadline = todayEnd;
  591. }
  592. }
  593. // 处理预设时长选择
  594. handlePresetSelection(preset: string): void {
  595. this.selectedPreset = preset;
  596. this.deadlineError = '';
  597. if (preset === 'custom') {
  598. // 打开自定义时间选择器
  599. this.openCustomTimePicker();
  600. } else if (preset === 'today') {
  601. // 设置为当天24:00前
  602. const now = new Date();
  603. const todayEnd = new Date(now);
  604. todayEnd.setHours(23, 59, 59, 999);
  605. this.deadlineInput = todayEnd.toISOString().slice(0, 16);
  606. this.newTask.deadline = todayEnd;
  607. } else {
  608. // 计算预设时长的截止时间
  609. const hours = parseInt(preset);
  610. this.updatePresetDeadline(hours);
  611. }
  612. }
  613. // 更新预设时长的截止时间
  614. private updatePresetDeadline(hours: number): void {
  615. const now = new Date();
  616. const deadline = new Date(now.getTime() + hours * 60 * 60 * 1000);
  617. this.deadlineInput = deadline.toISOString().slice(0, 16);
  618. this.newTask.deadline = deadline;
  619. }
  620. // 打开自定义时间选择器
  621. openCustomTimePicker(): void {
  622. // 重置自定义时间
  623. this.customDate = new Date();
  624. const now = new Date();
  625. this.customTime = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
  626. // 显示自定义时间弹窗
  627. this.isCustomTimeVisible = true;
  628. // 添加iOS风格的弹窗动画
  629. setTimeout(() => {
  630. document.querySelector('.custom-time-modal')?.classList.add('modal-visible');
  631. }, 10);
  632. }
  633. // 关闭自定义时间选择器
  634. closeCustomTimePicker(): void {
  635. // 添加iOS风格的弹窗关闭动画
  636. const modal = document.querySelector('.custom-time-modal');
  637. if (modal) {
  638. modal.classList.remove('modal-visible');
  639. setTimeout(() => {
  640. this.isCustomTimeVisible = false;
  641. }, 300);
  642. } else {
  643. this.isCustomTimeVisible = false;
  644. }
  645. }
  646. // 处理自定义时间选择
  647. handleCustomTimeSelection(): void {
  648. const [hours, minutes] = this.customTime.split(':').map(Number);
  649. const selectedDateTime = new Date(this.customDate);
  650. selectedDateTime.setHours(hours, minutes, 0, 0);
  651. // 验证选择的时间是否有效
  652. if (this.validateDeadline(selectedDateTime)) {
  653. this.deadlineInput = selectedDateTime.toISOString().slice(0, 16);
  654. this.newTask.deadline = selectedDateTime;
  655. this.closeCustomTimePicker();
  656. }
  657. }
  658. // 验证截止时间是否有效
  659. validateDeadline(deadline: Date): boolean {
  660. const now = new Date();
  661. if (deadline < now) {
  662. this.deadlineError = '截止时间不能早于当前时间,请重新选择';
  663. this.isSubmitDisabled = true;
  664. return false;
  665. }
  666. this.deadlineError = '';
  667. this.isSubmitDisabled = false;
  668. return true;
  669. }
  670. // 获取显示的截止时间文本
  671. getDisplayDeadline(): string {
  672. if (!this.deadlineInput) return '';
  673. try {
  674. const date = new Date(this.deadlineInput);
  675. return date.toLocaleString('zh-CN', {
  676. year: 'numeric',
  677. month: '2-digit',
  678. day: '2-digit',
  679. hour: '2-digit',
  680. minute: '2-digit'
  681. });
  682. } catch (error) {
  683. return '';
  684. }
  685. }
  686. // 隐藏任务表单
  687. hideTaskForm(): void {
  688. // 添加iOS风格的面板隐藏动画
  689. const panel = document.querySelector('.ios-panel');
  690. if (panel) {
  691. panel.classList.remove('ios-panel-visible');
  692. setTimeout(() => {
  693. this.isTaskFormVisible.set(false);
  694. }, 300);
  695. } else {
  696. this.isTaskFormVisible.set(false);
  697. }
  698. }
  699. // 处理添加任务表单提交
  700. async handleAddTaskSubmit(): Promise<void> {
  701. // 验证表单数据
  702. if (!this.newTask.title.trim() || !this.newTask.projectName.trim() || !this.deadlineInput || this.isSubmitDisabled) {
  703. // 在实际应用中,这里应该显示错误提示
  704. window?.fmode?.alert('请填写必填字段(任务标题、项目名称、截止时间)');
  705. return;
  706. }
  707. try {
  708. // 创建紧急事项
  709. const task = await this.urgentTaskService.createUrgentTask({
  710. title: this.newTask.title,
  711. description: this.newTask.description,
  712. projectId: this.newTask.projectId,
  713. spaceId: this.newTask.spaceId || undefined,
  714. stage: this.newTask.stage,
  715. region: this.newTask.region,
  716. priority: this.newTask.priority,
  717. assigneeId: this.newTask.assigneeId || undefined,
  718. deadline: new Date(this.deadlineInput)
  719. });
  720. // 记录活动日志
  721. try {
  722. const user = this.currentUser();
  723. const projectName = this.projectList().find(p => p.id === this.newTask.projectId)?.get('title') || '未知项目';
  724. await this.activityLogService.logActivity({
  725. actorId: user?.id || 'unknown',
  726. actorName: user?.get('name') || '客服',
  727. actorRole: user?.get('roleName') || 'customer_service',
  728. actionType: 'create',
  729. module: 'urgent_task',
  730. entityType: 'UrgentTask',
  731. entityId: task.id,
  732. entityName: this.newTask.title,
  733. description: '创建了紧急事项',
  734. metadata: {
  735. priority: this.newTask.priority,
  736. projectName: projectName,
  737. stage: this.newTask.stage,
  738. region: this.newTask.region,
  739. deadline: this.deadlineInput
  740. }
  741. });
  742. } catch (logError) {
  743. console.error('记录活动日志失败:', logError);
  744. }
  745. // 重新加载任务列表
  746. await this.loadUrgentTasks();
  747. console.log('✅ 紧急事项创建成功');
  748. // 隐藏表单
  749. this.hideTaskForm();
  750. } catch (error) {
  751. console.error('❌ 创建紧急事项失败:', error);
  752. alert('创建失败,请稍后重试');
  753. }
  754. }
  755. // 添加新的紧急事项
  756. addUrgentTask(): void {
  757. // 调用显示表单方法
  758. this.showTaskForm();
  759. }
  760. // 项目总数图标点击处理
  761. handleTotalProjectsClick(): void {
  762. console.log('导航到项目列表 - 显示所有项目');
  763. this.router.navigate(['/customer-service/project-list'], {
  764. queryParams: { filter: 'all' }
  765. });
  766. }
  767. // 新咨询数图标点击处理
  768. handleNewConsultationsClick(): void {
  769. this.navigateToDetail('consultations');
  770. }
  771. // 待分配数图标点击处理
  772. handlePendingAssignmentsClick(): void {
  773. console.log('导航到项目列表 - 显示待分配项目');
  774. this.router.navigate(['/customer-service/project-list'], {
  775. queryParams: { filter: 'pending' }
  776. });
  777. }
  778. // 异常项目图标点击处理
  779. handleExceptionProjectsClick(): void {
  780. this.navigateToDetail('exceptions');
  781. }
  782. handleAfterSalesClick(): void {
  783. this.router.navigate(['/customer-service/after-sales']);
  784. }
  785. // 导航到详情页
  786. private navigateToDetail(type: 'consultations' | 'assignments' | 'exceptions'): void {
  787. const routeMap = {
  788. consultations: '/customer-service/consultation-list',
  789. assignments: '/customer-service/assignment-list',
  790. exceptions: '/customer-service/exception-list'
  791. };
  792. console.log('导航到:', routeMap[type]);
  793. console.log('当前路由:', this.router.url);
  794. // 添加iOS风格页面过渡动画
  795. document.body.classList.add('ios-page-transition');
  796. setTimeout(() => {
  797. this.router.navigateByUrl(routeMap[type])
  798. .then(navResult => {
  799. console.log('导航结果:', navResult);
  800. if (!navResult) {
  801. console.error('导航失败,检查路由配置');
  802. }
  803. })
  804. .catch(err => {
  805. console.error('导航错误:', err);
  806. });
  807. setTimeout(() => {
  808. document.body.classList.remove('ios-page-transition');
  809. }, 300);
  810. }, 100);
  811. }
  812. // 格式化日期
  813. formatDate(date: Date | string): string {
  814. if (!date) return '';
  815. try {
  816. return new Date(date).toLocaleString('zh-CN', {
  817. month: '2-digit',
  818. day: '2-digit',
  819. hour: '2-digit',
  820. minute: '2-digit'
  821. });
  822. } catch (error) {
  823. console.error('日期格式化错误:', error);
  824. return '';
  825. }
  826. }
  827. // 添加安全获取客户名称的方法
  828. getCustomerName(update: Project | CustomerFeedback): string {
  829. if ('customerName' in update && update.customerName) {
  830. return update.customerName;
  831. } else if ('projectId' in update) {
  832. // 查找相关项目获取客户名称
  833. return '客户反馈';
  834. }
  835. return '未知客户';
  836. }
  837. // 优化的日期格式化方法
  838. getFormattedDate(update: Project | CustomerFeedback): string {
  839. if (!update) return '';
  840. if ('createdAt' in update && update.createdAt) {
  841. return this.formatDate(update.createdAt);
  842. } else if ('updatedAt' in update && update.updatedAt) {
  843. return this.formatDate(update.updatedAt);
  844. } else if ('deadline' in update && update.deadline) {
  845. return this.formatDate(update.deadline);
  846. }
  847. return '';
  848. }
  849. // 添加获取状态的安全方法
  850. getUpdateStatus(update: Project | CustomerFeedback): string {
  851. if ('status' in update && update.status) {
  852. return update.status;
  853. }
  854. return '已更新';
  855. }
  856. // 检查是否是项目更新
  857. isProjectUpdate(update: Project | CustomerFeedback): update is Project {
  858. return 'name' in update && 'status' in update;
  859. }
  860. // 检查是否有内容字段
  861. hasContent(update: Project | CustomerFeedback): boolean {
  862. return 'content' in update;
  863. }
  864. // 获取更新内容
  865. getUpdateContent(update: Project | CustomerFeedback): string {
  866. if ('content' in update) {
  867. return (update as CustomerFeedback).content;
  868. }
  869. return '';
  870. }
  871. // 处理搜索输入事件
  872. onSearchInput(event: Event): void {
  873. const target = event.target as HTMLInputElement;
  874. if (target) {
  875. this.searchTerm.set(target.value);
  876. }
  877. }
  878. // 添加getTaskStatus方法的正确实现
  879. getTaskStatus(task: Task): string {
  880. if (!task) return '未知状态';
  881. if (task.isCompleted) return '已完成';
  882. if (task.isOverdue) return '已逾期';
  883. return '进行中';
  884. }
  885. // 添加getUpdateStatusClass方法的正确实现
  886. getUpdateStatusClass(update: Project | CustomerFeedback): string {
  887. if ('name' in update) {
  888. // 项目
  889. switch (update.status) {
  890. case '进行中': return 'status-active';
  891. case '已完成': return 'status-completed';
  892. case '已暂停': return 'status-paused';
  893. default: return 'status-pending';
  894. }
  895. } else {
  896. // 反馈
  897. switch (update.status) {
  898. case '已解决': return 'status-completed';
  899. case '处理中': return 'status-active';
  900. default: return 'status-pending';
  901. }
  902. }
  903. }
  904. // 新增:加载待跟进尾款项目(从Parse真实数据)
  905. private async loadPendingFinalPaymentProjects(): Promise<void> {
  906. try {
  907. const now = new Date();
  908. const pendingProjects: Array<{
  909. id: string;
  910. projectId: string;
  911. projectName: string;
  912. customerName: string;
  913. customerPhone: string;
  914. finalPaymentAmount: number;
  915. dueDate: Date;
  916. status: string;
  917. overdueDay: number;
  918. }> = [];
  919. // 查询所有待付款的尾款记录
  920. const paymentQuery = this.createQuery('ProjectPayment');
  921. paymentQuery.equalTo('type', 'final'); // 尾款类型
  922. paymentQuery.containedIn('status', ['pending', 'overdue']); // 待付款或逾期状态
  923. paymentQuery.include(['project', 'paidBy']); // 关联项目和付款人信息
  924. paymentQuery.descending('dueDate'); // 按应付时间倒序
  925. paymentQuery.limit(20);
  926. const payments = await paymentQuery.find();
  927. for (const payment of payments) {
  928. const project = payment.get('project');
  929. const paidBy = payment.get('paidBy');
  930. const dueDate = payment.get('dueDate');
  931. const amount = payment.get('amount');
  932. const status = payment.get('status');
  933. if (project && paidBy) {
  934. // 计算逾期天数
  935. const overdueDays = status === 'overdue'
  936. ? Math.floor((now.getTime() - dueDate.getTime()) / (1000 * 60 * 60 * 24))
  937. : 0;
  938. pendingProjects.push({
  939. id: payment.id,
  940. projectId: project.id,
  941. projectName: project.get('title') || '未命名项目',
  942. customerName: paidBy.get('name') || '未知客户',
  943. customerPhone: paidBy.get('mobile') || '无电话',
  944. finalPaymentAmount: amount || 0,
  945. dueDate: dueDate || new Date(),
  946. status: status === 'overdue' ? '已逾期' : '待付款',
  947. overdueDay: overdueDays
  948. });
  949. }
  950. }
  951. this.pendingFinalPaymentProjects.set(pendingProjects);
  952. console.log(`✅ 待跟进尾款项目加载完成: ${pendingProjects.length} 个项目`);
  953. } catch (error) {
  954. console.error('❌ 待跟进尾款项目加载失败:', error);
  955. // 不抛出错误,允许其他数据继续加载
  956. }
  957. }
  958. // 新增:格式化日期时间
  959. formatDateTime(date: Date): string {
  960. const now = new Date();
  961. const diffMs = now.getTime() - date.getTime();
  962. const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
  963. const diffMinutes = Math.floor(diffMs / (1000 * 60));
  964. if (diffMinutes < 60) {
  965. return `${diffMinutes}分钟前`;
  966. } else if (diffHours < 24) {
  967. return `${diffHours}小时前`;
  968. } else {
  969. return date.toLocaleDateString('zh-CN', {
  970. month: 'short',
  971. day: 'numeric',
  972. hour: '2-digit',
  973. minute: '2-digit'
  974. });
  975. }
  976. }
  977. // 新增:获取支付状态文本
  978. getPaymentStatusText(status: string): string {
  979. switch (status) {
  980. case 'pending_followup': return '待跟进';
  981. case 'following_up': return '跟进中';
  982. case 'payment_completed': return '已支付';
  983. default: return '未知状态';
  984. }
  985. }
  986. // 新增:开始跟进尾款
  987. followUpFinalPayment(projectId: string): void {
  988. console.log(`开始跟进项目 ${projectId} 的尾款`);
  989. // 这里可以添加实际的跟进逻辑,比如发送消息、创建任务等
  990. // 导航到项目详情页或打开跟进对话框
  991. this.router.navigate(['/customer-service/project-detail', projectId]);
  992. }
  993. // 新增:查看项目详情
  994. viewProjectDetail(projectId: string): void {
  995. this.router.navigate(['/customer-service/project-detail', projectId]);
  996. }
  997. // 新增:一键发送大图
  998. sendLargeImages(projectId: string): void {
  999. const projects = this.pendingFinalPaymentProjects();
  1000. const project = projects.find(p => p.projectId === projectId);
  1001. if (!project) return;
  1002. console.log(`正在为项目 ${projectId} 发送大图到企业微信...`);
  1003. // 模拟发送过程
  1004. setTimeout(() => {
  1005. const updatedProjects = projects.map(p => {
  1006. if (p.projectId === projectId) {
  1007. return { ...p, largeImagesSent: true };
  1008. }
  1009. return p;
  1010. });
  1011. this.pendingFinalPaymentProjects.set(updatedProjects);
  1012. console.log(`✅ 项目 ${projectId} 大图已成功发送到企业微信服务群`);
  1013. console.log(`📱 已同步发送支付成功与大图交付通知`);
  1014. window?.fmode?.alert(`🎉 大图发送成功!
  1015. ✅ 已完成操作:
  1016. • 大图已发送至企业微信服务群
  1017. • 已通知客户支付成功
  1018. • 已确认大图交付完成
  1019. 项目:${project.projectName}
  1020. 客户:${project.customerName}`);
  1021. }, 2000);
  1022. }
  1023. }