// 客服工作台 - 对接Parse Server真实数据 import { Component, OnInit, OnDestroy, signal, computed } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { RouterModule, Router, ActivatedRoute } from '@angular/router'; import { ProfileService } from '../../../services/profile.service'; import { UrgentTaskService } from '../../../services/urgent-task.service'; import { ActivityLogService } from '../../../services/activity-log.service'; import { FmodeParse, FmodeObject } from 'fmode-ng/parse'; // 问题板块服务与类型(复用组长端逻辑) import { ProjectIssueService, IssuePriority, IssueStatus, IssueType } from '../../../../modules/project/services/project-issue.service'; // ⭐ 导入紧急事件类型定义(复用组长端) // 注意:UrgentEvent 类型与组长端保持一致,便于后续组件化 interface UrgentEvent { id: string; title: string; description: string; eventType: 'review' | 'delivery' | 'phase_deadline'; // 事件类型 phaseName?: string; // 阶段名称(如果是阶段截止) deadline: Date; // 截止时间 projectId: string; projectName: string; designerName?: string; urgencyLevel: 'critical' | 'high' | 'medium'; // 紧急程度 overdueDays?: number; // 逾期天数(负数表示还有几天) completionRate?: number; // 完成率(0-100) } const Parse = FmodeParse.with('nova'); // 项目数据接口 interface ProjectData { id: string; title: string; customerName: string; customerPhone?: string; status: string; stage: string; assigneeName?: string; createdAt: Date; updatedAt: Date; deadline?: Date; priority?: string; description?: string; } // 任务数据接口 interface Task { id: string; projectId: string; projectName: string; title: string; stage: string; deadline: Date; isOverdue: boolean; isCompleted: boolean; priority: 'high' | 'medium' | 'low'; assignee: string; description?: string; status: string; } // 项目更新联合类型 interface ProjectUpdate { id: string; name?: string; customerName: string; status: string; updatedAt?: Date; createdAt?: Date; } interface FeedbackUpdate { id: string; customerName: string; content: string; status: string; createdAt: Date; feedbackType: string; } // 项目类型(用于项目动态) interface Project { id: string; name: string; customerName: string; status: string; updatedAt?: Date; createdAt?: Date; deadline?: Date; } // 客户反馈类型 interface CustomerFeedback { id: string; projectId: string; customerName: string; content: string; status: string; createdAt: Date; } // 问题事件(用于项目动态与紧急待办复用) interface IssueUpdate { id: string; title: string; projectId: string; projectName: string; status: string; // 待处理/处理中/已解决/已关闭 type?: IssueType | string; priority?: IssuePriority | string; assigneeName?: string; createdAt: Date; updatedAt: Date; } // 从问题板块映射的待办任务(复用组长端结构) interface TodoTaskFromIssue { id: string; title: string; description?: string; priority: IssuePriority; type: IssueType; status: IssueStatus; projectId: string; projectName: string; relatedSpace?: string; relatedStage?: string; assigneeName?: string; creatorName?: string; createdAt: Date; updatedAt: Date; dueDate?: Date; tags?: string[]; } @Component({ selector: 'app-dashboard', standalone: true, imports: [CommonModule, FormsModule, RouterModule], templateUrl: './dashboard.html', styleUrls: ['./dashboard.scss', './dashboard-urgent-tasks-enhanced.scss', '../customer-service-styles.scss'] }) export class Dashboard implements OnInit, OnDestroy { // 数据看板统计 stats = { totalProjects: signal(0), // 项目总数 newConsultations: signal(0), // 新咨询数 pendingAssignments: signal(0), // 待分配项目数(原待派单数) exceptionProjects: signal(0), // 异常项目数 afterSalesCount: signal(0) // 售后服务数量 }; // 紧急任务列表(从待办任务中筛选出紧急的) urgentTasks = signal([]); // 任务处理状态 taskProcessingState = signal>>({}); // 从问题板块加载的待办任务列表(复用组长端) todoTasksFromIssues = signal([]); loadingTodoTasks = signal(false); todoTaskError = signal(''); // ⭐ 紧急事件列表(复用组长端逻辑) urgentEventsList = signal([]); loadingUrgentEvents = signal(false); // 🆕 待办事项标签筛选功能 urgentEventTagFilter = signal<'all' | 'customer' | 'phase' | 'review' | 'delivery'>('all'); // 标签统计(根据优先级计算) urgentEventTags = computed(() => { const events = this.urgentEventsList(); // 按类型分类统计 let customerCount = 0; let phaseCount = 0; let reviewCount = 0; let deliveryCount = 0; events.forEach(event => { if (event.eventType === 'review') { reviewCount++; } else if (event.eventType === 'delivery') { deliveryCount++; } else if (event.eventType === 'phase_deadline') { phaseCount++; } }); // 根据优先级:客户 > 工作阶段 > 小图截止 > 交付延期 // 这里构建一个有序的标签列表 const tags = [ { id: 'customer', label: '客户服务', count: customerCount, icon: '👥' }, { id: 'phase', label: '工作阶段', count: phaseCount, icon: '🔧' }, { id: 'review', label: '小图截止', count: reviewCount, icon: '📐' }, { id: 'delivery', label: '交付延期', count: deliveryCount, icon: '📦' } ]; return tags.filter(tag => tag.count > 0); }); // 筛选后的紧急事件列表 filteredUrgentEvents = computed(() => { const events = this.urgentEventsList(); const filter = this.urgentEventTagFilter(); if (filter === 'all') { return events; } // 按标签过滤 switch (filter) { case 'customer': // 客户相关事件:可以根据具体需求判断 return events.filter(e => e.eventType === 'review'); // 或其他条件 case 'phase': return events.filter(e => e.eventType === 'phase_deadline'); case 'review': return events.filter(e => e.eventType === 'review'); case 'delivery': return events.filter(e => e.eventType === 'delivery'); default: return events; } }); // 项目时间轴数据(用于计算紧急事件) projectTimelineData: any[] = []; // 新增:待跟进尾款项目列表(真实数据) pendingFinalPaymentProjects = signal>([]); // 项目动态流(扩展包含问题事件) projectUpdates = signal<(Project | CustomerFeedback | IssueUpdate)[]>([]); // 搜索关键词 searchTerm = signal(''); // 筛选后的项目更新(支持问题事件字段) filteredUpdates = computed(() => { if (!this.searchTerm()) return this.projectUpdates(); return this.projectUpdates().filter(item => { if ('name' in item) { // 项目 return item.name.toLowerCase().includes(this.searchTerm().toLowerCase()) || item.customerName.toLowerCase().includes(this.searchTerm().toLowerCase()) || item.status.toLowerCase().includes(this.searchTerm().toLowerCase()); } else if ('content' in item) { // 反馈 return 'content' in item && item.content.toLowerCase().includes(this.searchTerm().toLowerCase()) || 'status' in item && item.status.toLowerCase().includes(this.searchTerm().toLowerCase()); } else { // 问题事件 const issue = item as IssueUpdate; const keyword = this.searchTerm().toLowerCase(); return ( (issue.title && issue.title.toLowerCase().includes(keyword)) || (issue.projectName && issue.projectName.toLowerCase().includes(keyword)) || (issue.assigneeName && issue.assigneeName.toLowerCase().includes(keyword)) || (issue.status && issue.status.toLowerCase().includes(keyword)) ); } }); }); currentDate = new Date(); // 回到顶部按钮可见性信号 showBackToTopSignal = signal(false); // 任务表单可见性 isTaskFormVisible = signal(false); // 项目列表(用于下拉选择) projectList = signal([]); // 空间列表(用于下拉选择) spaceList = signal([]); // 团队成员列表(用于指派) teamMembers = signal([]); // 新任务数据 newTask: any = { title: '', description: '', projectId: '', spaceId: '', stage: '订单分配', region: '', priority: 'high', assigneeId: '', deadline: new Date() }; // 用于日期时间输入的属性 deadlineInput = ''; // 预设快捷时长选项 timePresets = [ { label: '1小时内', hours: 1 }, { label: '3小时内', hours: 3 }, { label: '6小时内', hours: 6 }, { label: '12小时内', hours: 12 }, { label: '24小时内', hours: 24 } ]; // 选中的预设时长 selectedPreset = ''; // 自定义时间弹窗可见性 isCustomTimeVisible = false; // 自定义选择的日期和时间 customDate = new Date(); customTime = ''; // 错误提示信息 deadlineError = ''; // 提交按钮是否禁用 isSubmitDisabled = false; // 下拉框可见性 deadlineDropdownVisible = false; // 日期范围限制 get todayDate(): string { return new Date().toISOString().split('T')[0]; } get sevenDaysLaterDate(): string { const date = new Date(); date.setDate(date.getDate() + 7); return date.toISOString().split('T')[0]; } constructor( private router: Router, private route: ActivatedRoute, private profileService: ProfileService, private urgentTaskService: UrgentTaskService, private activityLogService: ActivityLogService, private issueService: ProjectIssueService ) {} // 当前用户和公司信息 currentUser = signal(null); company = signal(null); // 初始化用户和公司信息 private async initializeUserAndCompany(): Promise { try { const profile = await this.profileService.getCurrentProfile(); this.currentUser.set(profile); // 获取公司信息 - 映三色帐套 const companyQuery = new Parse.Query('Company'); companyQuery.equalTo('objectId', 'cDL6R1hgSi'); const company = await companyQuery.first(); if (!company) { throw new Error('未找到公司信息'); } this.company.set(company); console.log('✅ 用户和公司信息初始化完成'); } catch (error) { console.error('❌ 用户和公司信息初始化失败:', error); throw error; } } // 获取公司指针 private getCompanyPointer(): any { if (!this.company()) { throw new Error('公司信息未加载'); } return { __type: 'Pointer', className: 'Company', objectId: this.company().id }; } // 创建带公司过滤的查询 private createQuery(className: string): any { const query = new Parse.Query(className); query.equalTo('company', this.getCompanyPointer()); query.notEqualTo('isDeleted', true); return query; } async ngOnInit(): Promise { try { await this.initializeUserAndCompany(); await this.loadDashboardData(); // 添加滚动事件监听 window.addEventListener('scroll', this.onScroll.bind(this)); } catch (error) { console.error('❌ 客服工作台初始化失败:', error); } } // 加载仪表板数据 private async loadDashboardData(): Promise { try { await Promise.all([ this.loadConsultationStats(), this.loadTodoTasksFromIssues(), // 先加载待办任务 this.loadProjectUpdates(), this.loadCRMQueues(), this.loadPendingFinalPaymentProjects() ]); console.log('✅ 客服仪表板数据加载完成'); } catch (error) { console.error('❌ 客服仪表板数据加载失败:', error); throw error; } } // 加载咨询统计数据 private async loadConsultationStats(): Promise { try { const todayStart = new Date(); todayStart.setHours(0, 0, 0, 0); // 项目总数 const totalProjectQuery = this.createQuery('Project'); const totalProjects = await totalProjectQuery.count(); this.stats.totalProjects.set(totalProjects); // 新咨询数(今日新增的项目) const consultationQuery = this.createQuery('Project'); consultationQuery.greaterThanOrEqualTo('createdAt', todayStart); const newConsultations = await consultationQuery.count(); this.stats.newConsultations.set(newConsultations); // 待分配项目数(阶段处于"订单分配"的项目) // 参考组长工作台的筛选逻辑:根据四大板块筛选 // 订单分配阶段包括:order, pendingApproval, pendingAssignment, 订单分配, 待审批, 待分配 // 查询所有项目,然后在客户端筛选(与组长工作台保持一致) const allProjectsQuery = this.createQuery('Project'); allProjectsQuery.limit(1000); // 限制最多1000个项目 const allProjects = await allProjectsQuery.find(); // 使用与组长工作台相同的筛选逻辑 const orderPhaseProjects = allProjects.filter(p => { const currentStage = p.get('currentStage') || ''; const stage = p.get('stage') || ''; const stageValue = (currentStage || stage).toString().trim().toLowerCase(); // 订单分配阶段的所有变体(与组长工作台mapStageToCorePhase保持一致) const isOrderPhase = stageValue === 'order' || stageValue === 'pendingapproval' || stageValue === 'pendingassignment' || stageValue === '订单分配' || stageValue === '待审批' || stageValue === '待分配'; // 调试日志:输出每个项目的阶段信息 if (isOrderPhase) { console.log(`📋 订单分配项目: ${p.get('title')}, currentStage="${currentStage}", stage="${stage}", 匹配值="${stageValue}"`); } return isOrderPhase; }); const pendingAssignments = orderPhaseProjects.length; this.stats.pendingAssignments.set(pendingAssignments); console.log(`✅ 待分配项目统计: 总项目数=${allProjects.length}, 订单分配阶段项目数=${pendingAssignments}`); // 异常项目数(使用ProjectIssue表) const issueQuery = this.createQuery('ProjectIssue'); issueQuery.equalTo('priority', 'high'); issueQuery.equalTo('status', 'open'); const exceptionProjects = await issueQuery.count(); this.stats.exceptionProjects.set(exceptionProjects); // 售后服务数量(使用ProjectFeedback表,类型为投诉的待处理反馈) let afterSalesCount = 0; try { const feedbackQuery = this.createQuery('ProjectFeedback'); feedbackQuery.equalTo('status', 'pending'); feedbackQuery.equalTo('feedbackType', 'complaint'); afterSalesCount = await feedbackQuery.count(); this.stats.afterSalesCount.set(afterSalesCount); } catch (feedbackError) { console.warn('⚠️ ProjectFeedback表查询失败,可能表不存在,使用默认值0', feedbackError); this.stats.afterSalesCount.set(0); } console.log(`✅ 咨询统计: 项目总数${totalProjects}, 新咨询${newConsultations}, 待分配${pendingAssignments}, 异常${exceptionProjects}, 售后${afterSalesCount}`); } catch (error) { console.error('❌ 咨询统计加载失败:', error); // 不抛出错误,允许其他数据继续加载 } } // 降级到模拟数据 private loadMockData(): void { console.warn('⚠️ 使用模拟数据'); this.loadUrgentTasks(); this.loadProjectUpdates(); this.loadCRMQueues(); // loadPendingFinalPaymentProjects 已改为异步真实数据查询 } // 添加滚动事件处理方法 private onScroll(): void { this.showBackToTopSignal.set(window.scrollY > 300); } // 添加显示回到顶部按钮的计算属性 showBackToTop = computed(() => this.showBackToTopSignal()); // 清理事件监听器 ngOnDestroy(): void { window.removeEventListener('scroll', this.onScroll.bind(this)); } // 添加scrollToTop方法 scrollToTop(): void { window.scrollTo({ top: 0, behavior: 'smooth' }); } // 查看人员考勤 viewAttendance(): void { this.router.navigate(['/hr/attendance']); } // 加载紧急任务(已废弃,现在从loadTodoTasksFromIssues中同步) private async loadUrgentTasks(): Promise { // 此方法已被 loadTodoTasksFromIssues 替代 // 紧急任务现在从待办任务中自动筛选 console.log('⚠️ loadUrgentTasks 已废弃,紧急任务从 loadTodoTasksFromIssues 中同步'); return; /* 保留原代码用于参考 try { // 使用UrgentTaskService加载紧急事项 const result = await this.urgentTaskService.findUrgentTasks({ isCompleted: false }, 1, 20); // 转换数据格式以兼容现有UI const formattedTasks: Task[] = result.tasks.map(task => ({ id: task.id, projectId: task.projectId, projectName: task.projectName, title: task.title, stage: task.stage, deadline: task.deadline, isOverdue: task.isOverdue, isCompleted: task.isCompleted, priority: task.priority as 'high' | 'medium' | 'low', assignee: task.assigneeName, description: task.description || '', status: task.status })); */ } // 加载CRM队列数据(已隐藏,暂不使用真实数据) private loadCRMQueues(): void { // CRM功能暂时隐藏,后续开发时再从Parse查询真实数据 // 可以从ProjectFeedback表查询客户反馈和咨询记录 console.log('⏸️ CRM队列功能暂时隐藏'); } // 查看全部咨询列表 goToConsultationList(): void { this.router.navigate(['/customer-service/consultation-list']); } // 加载项目动态 private async loadProjectUpdates(): Promise { try { const updates: (Project | CustomerFeedback | IssueUpdate)[] = []; // 1. 查询最新更新的项目 const projectQuery = this.createQuery('Project'); projectQuery.include(['contact', 'assignee']); projectQuery.descending('updatedAt'); projectQuery.limit(10); const projects = await projectQuery.find(); for (const project of projects) { const contact = project.get('contact'); updates.push({ id: project.id, name: project.get('title') || '未命名项目', customerName: contact?.get('name') || '未知客户', status: project.get('status') || '进行中', updatedAt: project.get('updatedAt'), createdAt: project.get('createdAt') }); } // 2. 查询最新客户反馈 let feedbacks: any[] = []; try { const feedbackQuery = this.createQuery('ProjectFeedback'); feedbackQuery.include(['contact', 'project']); feedbackQuery.descending('createdAt'); feedbackQuery.limit(10); feedbacks = await feedbackQuery.find(); } catch (feedbackError) { console.warn('⚠️ ProjectFeedback表查询失败,可能表不存在,跳过反馈数据', feedbackError); feedbacks = []; } for (const feedback of feedbacks) { const contact = feedback.get('contact'); updates.push({ id: feedback.id, projectId: feedback.get('project')?.id || '', customerName: contact?.get('name') || '未知客户', content: feedback.get('content') || '无内容', status: feedback.get('status') || 'pending', createdAt: feedback.get('createdAt') }); } // 3. 查询最新问题事件(ProjectIssue) try { const issueQuery = this.createQuery('ProjectIssue'); issueQuery.include(['project', 'assignee']); issueQuery.notEqualTo('isDeleted', true); issueQuery.descending('updatedAt'); issueQuery.limit(10); const issues = await issueQuery.find(); for (const obj of issues) { const project = obj.get('project'); const assignee = obj.get('assignee'); const title = obj.get('title') || (obj.get('description') || '').slice(0, 40) || '未命名问题'; const projectName = project?.get('title') || '未知项目'; const statusZh = obj.get('status') || '待处理'; const typeRaw = obj.get('issueType') || 'task'; const priorityRaw = obj.get('priority') || 'medium'; updates.push({ id: obj.id, title, projectId: project?.id || '', projectName, status: statusZh, type: typeRaw, priority: priorityRaw, assigneeName: assignee?.get('name') || assignee?.get('realname') || '', createdAt: obj.createdAt || new Date(), updatedAt: obj.updatedAt || new Date() } as IssueUpdate); } } catch (e) { console.warn('⚠️ 加载问题事件失败(忽略):', e); } // 按时间排序 updates.sort((a, b) => { const aTime = ('updatedAt' in a && a.updatedAt) ? a.updatedAt.getTime() : (a.createdAt?.getTime() || 0); const bTime = ('updatedAt' in b && b.updatedAt) ? b.updatedAt.getTime() : (b.createdAt?.getTime() || 0); return bTime - aTime; }); this.projectUpdates.set(updates); console.log(`✅ 项目动态加载完成: ${updates.length} 条动态`); } catch (error) { console.error('❌ 项目动态加载失败:', error); // 不抛出错误,允许其他数据继续加载 } } // 处理任务完成 async markTaskAsCompleted(taskId: string): Promise { try { const task = this.urgentTasks().find(t => t.id === taskId); if (task && task.id.startsWith('issue:')) { // 来自问题板块的任务:将问题状态置为已解决 const issueId = task.id.replace('issue:', ''); await this.issueService.setStatus(task.projectId, issueId, 'resolved'); // 记录问题活动日志 try { const user = this.currentUser(); await this.activityLogService.logActivity({ actorId: user?.id || 'unknown', actorName: user?.get('name') || '客服', actorRole: user?.get('roleName') || 'customer_service', actionType: 'complete', module: 'project_issue', entityType: 'ProjectIssue', entityId: issueId, entityName: task.title, description: '将问题标记为已解决', metadata: { priority: task.priority, projectName: task.projectName } }); } catch (logError) { console.error('记录活动日志失败:', logError); } } else { // 原紧急任务逻辑 await this.urgentTaskService.markAsCompleted(taskId); // 记录活动日志 if (task) { try { const user = this.currentUser(); await this.activityLogService.logActivity({ actorId: user?.id || 'unknown', actorName: user?.get('name') || '客服', actorRole: user?.get('roleName') || 'customer_service', actionType: 'complete', module: 'urgent_task', entityType: 'UrgentTask', entityId: taskId, entityName: task.title, description: '完成了紧急事项', metadata: { priority: task.priority, projectName: task.projectName } }); } catch (logError) { console.error('记录活动日志失败:', logError); } } } // 重新加载任务列表 await this.loadUrgentTasks(); console.log('✅ 任务标记为已完成'); } catch (error) { console.error('❌ 标记任务完成失败:', error); alert('操作失败,请稍后重试'); } } // 删除任务 async deleteTask(taskId: string): Promise { if (!await window?.fmode?.confirm('确定要删除这个紧急事项吗?')) { return; } try { const task = this.urgentTasks().find(t => t.id === taskId); if (task && task.id.startsWith('issue:')) { const issueId = task.id.replace('issue:', ''); await this.issueService.deleteIssue(task.projectId, issueId); try { const user = this.currentUser(); await this.activityLogService.logActivity({ actorId: user?.id || 'unknown', actorName: user?.get('name') || '客服', actorRole: user?.get('roleName') || 'customer_service', actionType: 'delete', module: 'project_issue', entityType: 'ProjectIssue', entityId: issueId, entityName: task.title, description: '删除了问题', metadata: { priority: task.priority, projectName: task.projectName } }); } catch {} } else { await this.urgentTaskService.deleteUrgentTask(taskId); } // 重新加载任务列表 await this.loadUrgentTasks(); console.log('✅ 任务删除成功'); } catch (error) { console.error('❌ 删除任务失败:', error); alert('删除失败,请稍后重试'); } } // 处理派单操作 handleAssignment(taskId: string): void { // 标记任务为处理中 const task = this.urgentTasks().find(t => t.id === taskId); if (task) { // 初始化处理状态 this.taskProcessingState.update(state => ({ ...state, [task.id]: { inProgress: true, progress: 0 } })); // 模拟处理进度 let progress = 0; const interval = setInterval(() => { progress += 10; this.taskProcessingState.update(state => ({ ...state, [task.id]: { inProgress: progress < 100, progress } })); if (progress >= 100) { clearInterval(interval); // 处理完成后从列表中移除该任务 this.urgentTasks.set( this.urgentTasks().filter(t => t.id !== task.id) ); // 清除处理状态 this.taskProcessingState.update(state => { const newState = { ...state }; delete newState[task.id]; return newState; }); } }, 300); } // 更新统计数据 this.stats.pendingAssignments.set(this.stats.pendingAssignments() - 1); } // 显示任务表单 async showTaskForm(): Promise { // 重置表单数据 this.newTask = { title: '', description: '', projectId: '', spaceId: '', stage: '订单分配', region: '', priority: 'high', assigneeId: '', deadline: new Date() }; // 重置相关状态 this.deadlineError = ''; this.isSubmitDisabled = false; // 计算并设置默认预设时长 this.setDefaultPreset(); // 加载下拉列表数据 try { const [projects, members] = await Promise.all([ this.urgentTaskService.getProjects(), this.urgentTaskService.getTeamMembers() ]); this.projectList.set(projects); this.teamMembers.set(members); this.spaceList.set([]); // 初始为空,等待选择项目后加载 } catch (error) { console.error('加载下拉列表数据失败:', error); } // 显示表单 this.isTaskFormVisible.set(true); // 添加iOS风格的面板显示动画 setTimeout(() => { document.querySelector('.ios-panel')?.classList.add('ios-panel-visible'); }, 10); } // 项目选择变化时加载空间列表 async onProjectChange(projectId: string): Promise { if (!projectId) { this.spaceList.set([]); return; } try { const spaces = await this.urgentTaskService.getProjectSpaces(projectId); this.spaceList.set(spaces); } catch (error) { console.error('加载空间列表失败:', error); this.spaceList.set([]); } } // 设置默认预设时长 private setDefaultPreset(): void { const now = new Date(); const todayEnd = new Date(now); todayEnd.setHours(23, 59, 59, 999); // 检查3小时后是否超过当天24:00 const threeHoursLater = new Date(now.getTime() + 3 * 60 * 60 * 1000); if (threeHoursLater <= todayEnd) { // 3小时后未超过当天24:00,默认选中3小时内 this.selectedPreset = '3'; this.updatePresetDeadline(3); } else { // 3小时后超过当天24:00,默认选中当天24:00前 this.selectedPreset = 'today'; this.deadlineInput = todayEnd.toISOString().slice(0, 16); this.newTask.deadline = todayEnd; } } // 处理预设时长选择 handlePresetSelection(preset: string): void { this.selectedPreset = preset; this.deadlineError = ''; if (preset === 'custom') { // 打开自定义时间选择器 this.openCustomTimePicker(); } else if (preset === 'today') { // 设置为当天24:00前 const now = new Date(); const todayEnd = new Date(now); todayEnd.setHours(23, 59, 59, 999); this.deadlineInput = todayEnd.toISOString().slice(0, 16); this.newTask.deadline = todayEnd; } else { // 计算预设时长的截止时间 const hours = parseInt(preset); this.updatePresetDeadline(hours); } } // 更新预设时长的截止时间 private updatePresetDeadline(hours: number): void { const now = new Date(); const deadline = new Date(now.getTime() + hours * 60 * 60 * 1000); this.deadlineInput = deadline.toISOString().slice(0, 16); this.newTask.deadline = deadline; } // 打开自定义时间选择器 openCustomTimePicker(): void { // 重置自定义时间 this.customDate = new Date(); const now = new Date(); this.customTime = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`; // 显示自定义时间弹窗 this.isCustomTimeVisible = true; // 添加iOS风格的弹窗动画 setTimeout(() => { document.querySelector('.custom-time-modal')?.classList.add('modal-visible'); }, 10); } // 关闭自定义时间选择器 closeCustomTimePicker(): void { // 添加iOS风格的弹窗关闭动画 const modal = document.querySelector('.custom-time-modal'); if (modal) { modal.classList.remove('modal-visible'); setTimeout(() => { this.isCustomTimeVisible = false; }, 300); } else { this.isCustomTimeVisible = false; } } // 处理自定义时间选择 handleCustomTimeSelection(): void { const [hours, minutes] = this.customTime.split(':').map(Number); const selectedDateTime = new Date(this.customDate); selectedDateTime.setHours(hours, minutes, 0, 0); // 验证选择的时间是否有效 if (this.validateDeadline(selectedDateTime)) { this.deadlineInput = selectedDateTime.toISOString().slice(0, 16); this.newTask.deadline = selectedDateTime; this.closeCustomTimePicker(); } } // 验证截止时间是否有效 validateDeadline(deadline: Date): boolean { const now = new Date(); if (deadline < now) { this.deadlineError = '截止时间不能早于当前时间,请重新选择'; this.isSubmitDisabled = true; return false; } this.deadlineError = ''; this.isSubmitDisabled = false; return true; } // 获取显示的截止时间文本 getDisplayDeadline(): string { if (!this.deadlineInput) return ''; try { const date = new Date(this.deadlineInput); return date.toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); } catch (error) { return ''; } } // 隐藏任务表单 hideTaskForm(): void { // 添加iOS风格的面板隐藏动画 const panel = document.querySelector('.ios-panel'); if (panel) { panel.classList.remove('ios-panel-visible'); setTimeout(() => { this.isTaskFormVisible.set(false); }, 300); } else { this.isTaskFormVisible.set(false); } } // 处理添加任务表单提交 async handleAddTaskSubmit(): Promise { // 验证表单数据 if (!this.newTask.title.trim() || !this.newTask.projectName.trim() || !this.deadlineInput || this.isSubmitDisabled) { // 在实际应用中,这里应该显示错误提示 window?.fmode?.alert('请填写必填字段(任务标题、项目名称、截止时间)'); return; } try { // 创建紧急事项 const task = await this.urgentTaskService.createUrgentTask({ title: this.newTask.title, description: this.newTask.description, projectId: this.newTask.projectId, spaceId: this.newTask.spaceId || undefined, stage: this.newTask.stage, region: this.newTask.region, priority: this.newTask.priority, assigneeId: this.newTask.assigneeId || undefined, deadline: new Date(this.deadlineInput) }); // 记录活动日志 try { const user = this.currentUser(); const projectName = this.projectList().find(p => p.id === this.newTask.projectId)?.get('title') || '未知项目'; await this.activityLogService.logActivity({ actorId: user?.id || 'unknown', actorName: user?.get('name') || '客服', actorRole: user?.get('roleName') || 'customer_service', actionType: 'create', module: 'urgent_task', entityType: 'UrgentTask', entityId: task.id, entityName: this.newTask.title, description: '创建了紧急事项', metadata: { priority: this.newTask.priority, projectName: projectName, stage: this.newTask.stage, region: this.newTask.region, deadline: this.deadlineInput } }); } catch (logError) { console.error('记录活动日志失败:', logError); } // 重新加载任务列表 await this.loadUrgentTasks(); console.log('✅ 紧急事项创建成功'); // 隐藏表单 this.hideTaskForm(); } catch (error) { console.error('❌ 创建紧急事项失败:', error); alert('创建失败,请稍后重试'); } } // 添加新的紧急事项 addUrgentTask(): void { // 调用显示表单方法 this.showTaskForm(); } // 项目总数图标点击处理 handleTotalProjectsClick(): void { console.log('导航到项目列表 - 显示所有项目'); this.router.navigate(['/customer-service/project-list'], { queryParams: { filter: 'all' } }); } // 新咨询数图标点击处理 handleNewConsultationsClick(): void { this.navigateToDetail('consultations'); } // 待分配数图标点击处理 handlePendingAssignmentsClick(): void { console.log('导航到项目列表 - 显示待分配项目'); this.router.navigate(['/customer-service/project-list'], { queryParams: { filter: 'pending' } }); } // 异常项目图标点击处理 handleExceptionProjectsClick(): void { this.navigateToDetail('exceptions'); } handleAfterSalesClick(): void { this.router.navigate(['/customer-service/after-sales']); } // 导航到详情页 private navigateToDetail(type: 'consultations' | 'assignments' | 'exceptions'): void { const routeMap = { consultations: '/customer-service/consultation-list', assignments: '/customer-service/assignment-list', exceptions: '/customer-service/exception-list' }; console.log('导航到:', routeMap[type]); console.log('当前路由:', this.router.url); // 添加iOS风格页面过渡动画 document.body.classList.add('ios-page-transition'); setTimeout(() => { this.router.navigateByUrl(routeMap[type]) .then(navResult => { console.log('导航结果:', navResult); if (!navResult) { console.error('导航失败,检查路由配置'); } }) .catch(err => { console.error('导航错误:', err); }); setTimeout(() => { document.body.classList.remove('ios-page-transition'); }, 300); }, 100); } // 格式化日期 formatDate(date: Date | string): string { if (!date) return ''; try { return new Date(date).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); } catch (error) { console.error('日期格式化错误:', error); return ''; } } // 添加安全获取客户名称的方法 getCustomerName(update: Project | CustomerFeedback | IssueUpdate): string { if ('customerName' in update && update.customerName) { return update.customerName; } else if ('projectId' in update) { // 查找相关项目获取客户名称 // 如果是问题事件,优先展示项目名称 if ('title' in update && 'projectName' in update) { return (update as IssueUpdate).projectName || '未知项目'; } return '客户反馈'; } return '未知客户'; } // 优化的日期格式化方法 getFormattedDate(update: Project | CustomerFeedback | IssueUpdate): string { if (!update) return ''; if ('createdAt' in update && update.createdAt) { return this.formatDate(update.createdAt); } else if ('updatedAt' in update && update.updatedAt) { return this.formatDate(update.updatedAt); } else if ('deadline' in update && update.deadline) { return this.formatDate(update.deadline); } return ''; } // 添加获取状态的安全方法 getUpdateStatus(update: Project | CustomerFeedback | IssueUpdate): string { if ('status' in update && update.status) { return update.status; } return '已更新'; } // 检查是否是项目更新 isProjectUpdate(update: Project | CustomerFeedback | IssueUpdate): update is Project { return 'name' in update && 'status' in update; } // 检查是否有内容字段 hasContent(update: Project | CustomerFeedback | IssueUpdate): boolean { return 'content' in update; } // 获取更新内容 getUpdateContent(update: Project | CustomerFeedback | IssueUpdate): string { if ('content' in update) { return (update as CustomerFeedback).content; } return ''; } // 处理搜索输入事件 onSearchInput(event: Event): void { const target = event.target as HTMLInputElement; if (target) { this.searchTerm.set(target.value); } } // 添加getTaskStatus方法的正确实现 getTaskStatus(task: Task): string { if (!task) return '未知状态'; if (task.isCompleted) return '已完成'; if (task.isOverdue) return '已逾期'; return '进行中'; } // 添加getUpdateStatusClass方法的正确实现 getUpdateStatusClass(update: Project | CustomerFeedback | IssueUpdate): string { if ('name' in update) { // 项目 switch (update.status) { case '进行中': return 'status-active'; case '已完成': return 'status-completed'; case '已暂停': return 'status-paused'; default: return 'status-pending'; } } else if ('title' in update) { // 问题事件 const status = (update as IssueUpdate).status; switch (status) { case '待处理': return 'status-pending'; case '处理中': return 'status-active'; case '已解决': return 'status-completed'; case '已关闭': return 'status-completed'; default: return 'status-pending'; } } else { // 反馈 switch (update.status) { case '已解决': return 'status-completed'; case '处理中': return 'status-active'; default: return 'status-pending'; } } } // 新增:类型守卫与显示辅助(问题事件) isIssueUpdate(update: Project | CustomerFeedback | IssueUpdate): update is IssueUpdate { return 'title' in update && 'projectName' in update; } getIssueTitle(update: IssueUpdate): string { return update?.title || '未命名问题'; } getIssueProjectName(update: IssueUpdate): string { return update?.projectName || '未知项目'; } // 已移至底部统一管理(复用组长端方法) // getIssueTypeLabel 和 getIssuePriorityLabel 已在待办任务模块中定义 // 新增:加载待跟进尾款项目(从Project.data读取,不使用ProjectPayment表) private async loadPendingFinalPaymentProjects(): Promise { try { console.log('🔍 开始加载待跟进尾款项目...'); const now = new Date(); const resultList: Array<{ id: string; projectId: string; projectName: string; customerName: string; customerPhone: string; finalPaymentAmount: number; totalAmount: number; paidAmount: number; dueDate: Date; status: string; overdueDay: number; }> = []; // 1) 查询处于"售后归档"相关阶段的项目(公司内) const projectQuery = this.createQuery('Project'); projectQuery.containedIn('currentStage', [ '售后归档', '尾款结算', '客户评价', '投诉处理', '已归档', 'aftercare' ]); projectQuery.include(['contact', 'assignee']); projectQuery.descending('updatedAt'); projectQuery.limit(100); // 增加限制以获取更多项目 projectQuery.notEqualTo('isDeleted', true); const projects = await projectQuery.find(); console.log(`📊 找到 ${projects.length} 个售后阶段项目`); // 2) 逐项目从Project.data统计尾款是否不足 for (const p of projects) { try { // 从项目数据中获取订单总金额和付款信息 const projectData = p.get('data') || {}; const quotation = projectData.quotation || {}; const aftercare = projectData.aftercare || {}; const finalPayment = aftercare.finalPayment || {}; // 订单总金额 const orderTotal = quotation.total || 0; // 已付金额(从售后归档数据中获取) let totalPaid = finalPayment.paidAmount || 0; // 如果没有售后归档数据,尝试从 paymentVouchers 计算 if (totalPaid === 0 && finalPayment.paymentVouchers && finalPayment.paymentVouchers.length > 0) { totalPaid = finalPayment.paymentVouchers.reduce((sum: number, v: any) => { return sum + (v.amount || 0); }, 0); } // 计算剩余未付款金额 const remaining = orderTotal - totalPaid; console.log(`📋 项目 ${p.get('title') || p.get('name')}: 订单总额=¥${orderTotal}, 已付=¥${totalPaid}, 剩余=¥${remaining}`); // 只有当剩余金额大于100元时才认为是待跟进项目(避免小额零头) if (remaining > 100) { const contact = p.get('contact'); const customerName = contact?.get?.('realname') || contact?.get?.('name') || p.get('customerName') || '未知客户'; const customerPhone = contact?.get?.('mobile') || contact?.get?.('phone') || p.get('customerPhone') || '无电话'; // 获取到期日期 let dueDate: Date | undefined = finalPayment.dueDate ? new Date(finalPayment.dueDate) : undefined; let isOverdue = false; let overdueDay = 0; // 计算逾期天数 if (dueDate) { const diff = now.getTime() - dueDate.getTime(); if (diff > 0) { isOverdue = true; overdueDay = Math.floor(diff / (1000 * 60 * 60 * 24)); } } // 如果没有到期日期,使用项目截止日期作为应付日期 if (!dueDate) { dueDate = p.get('deadline') || new Date(); } // 确定状态 const paymentStatus = isOverdue ? '已逾期' : !finalPayment.dueDate ? '待创建' : '待付款'; resultList.push({ id: p.id, projectId: p.id, projectName: p.get('title') || p.get('name') || '未命名项目', customerName, customerPhone, finalPaymentAmount: remaining, totalAmount: orderTotal, paidAmount: totalPaid, dueDate, status: paymentStatus, overdueDay }); console.log(`✅ 添加待跟进项目: ${p.get('title') || p.get('name')}, 剩余¥${remaining}, 状态=${paymentStatus}`); } } catch (projectError) { console.error(`❌ 处理项目 ${p.id} 时出错:`, projectError); // 继续处理下一个项目 } } // 按逾期天数降序排序(逾期时间最长的排在前面) resultList.sort((a, b) => { if (a.status === '已逾期' && b.status !== '已逾期') return -1; if (a.status !== '已逾期' && b.status === '已逾期') return 1; return b.overdueDay - a.overdueDay; }); this.pendingFinalPaymentProjects.set(resultList); console.log(`✅ 待跟进尾款项目加载完成: ${resultList.length} 个项目(售后归档阶段)`); console.log('详细列表:', resultList.map(p => ({ 项目: p.projectName, 剩余金额: p.finalPaymentAmount, 状态: p.status, 逾期天数: p.overdueDay }))); } catch (error) { console.error('❌ 待跟进尾款项目加载失败:', error); // 设置空列表,不影响其他功能 this.pendingFinalPaymentProjects.set([]); } } // 新增:格式化日期时间 formatDateTime(date: Date): string { const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); const diffMinutes = Math.floor(diffMs / (1000 * 60)); if (diffMinutes < 60) { return `${diffMinutes}分钟前`; } else if (diffHours < 24) { return `${diffHours}小时前`; } else { return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); } } // 新增:获取支付状态文本 getPaymentStatusText(status: string): string { switch (status) { case 'pending_followup': return '待跟进'; case 'following_up': return '跟进中'; case 'payment_completed': return '已支付'; default: return '未知状态'; } } // 新增:开始跟进尾款 async followUpFinalPayment(projectId: string): Promise { console.log(`🎯 开始跟进项目 ${projectId} 的尾款`); try { // 查找该项目的详细信息 const project = this.pendingFinalPaymentProjects().find(p => p.projectId === projectId); if (!project) { console.error('❌ 未找到项目信息'); return; } // 记录跟进日志到活动记录(ActivityLog表可能不存在,使用try-catch) try { const ActivityLog = Parse.Object.extend('ActivityLog'); const activityLog = new ActivityLog(); activityLog.set('company', this.getCompanyPointer()); activityLog.set('project', { __type: 'Pointer', className: 'Project', objectId: projectId }); activityLog.set('operator', Parse.User.current()); activityLog.set('action', '尾款跟进'); activityLog.set('description', `客服开始跟进尾款:剩余金额 ¥${project.finalPaymentAmount}`); activityLog.set('type', 'payment_followup'); await activityLog.save(); console.log('✅ 跟进记录已保存'); } catch (logError) { console.warn('⚠️ ActivityLog表不存在,跳过日志记录', logError); // 继续执行,不阻塞跟进功能 } // 获取当前公司ID const cid = localStorage.getItem('company') || 'cDL6R1hgSi'; // 导航到wxwork模块的项目详情页,并定位到售后归档阶段 this.router.navigate(['/wxwork', cid, 'project', projectId, 'aftercare'], { queryParams: { focus: 'payment' } }); } catch (error) { console.error('❌ 开始跟进失败:', error); window?.fmode?.alert('跳转失败,请稍后重试'); } } // 新增:查看项目详情 viewProjectDetail(projectId: string): void { console.log(`📂 查看项目详情: ${projectId}`); // 获取当前公司ID const cid = localStorage.getItem('company') || 'cDL6R1hgSi'; // 导航到wxwork模块的项目详情页 this.router.navigate(['/wxwork', cid, 'project', projectId]); } // ==================== 待办任务相关方法(复用组长端逻辑) ==================== /** * 从问题板块加载待办任务(完全复用组长端逻辑) */ async loadTodoTasksFromIssues(): Promise { this.loadingTodoTasks.set(true); this.todoTaskError.set(''); try { console.log('🔍 [客服-待办任务] 开始加载待办任务...'); // 使用 FmodeParse.with('nova') 直接创建查询,与组长端一致 const Parse: any = FmodeParse.with('nova'); const query = new Parse.Query('ProjectIssue'); // 筛选条件:待处理 + 处理中 query.containedIn('status', ['待处理', '处理中']); query.notEqualTo('isDeleted', true); // 关联数据 query.include(['project', 'creator', 'assignee']); // 排序:更新时间倒序 query.descending('updatedAt'); // 限制数量 query.limit(50); const results = await query.find(); console.log(`📊 [客服-待办任务] 找到 ${results.length} 个问题`); // 数据转换(异步处理以支持 fetch,与组长端一致) const tasks: TodoTaskFromIssue[] = await Promise.all(results.map(async (obj: any) => { let project = obj.get('project'); const assignee = obj.get('assignee'); const creator = obj.get('creator'); const data = obj.get('data') || {}; let projectName = '未知项目'; let projectId = ''; // 如果 project 存在,尝试获取完整数据 if (project) { projectId = project.id; // 尝试从已加载的对象获取 name projectName = project.get('name'); // 如果 name 为空,使用 Parse.Query 查询项目 if (!projectName && projectId) { try { console.log(`🔄 查询项目数据: ${projectId}`); const projectQuery = new Parse.Query('Project'); const fetchedProject = await projectQuery.get(projectId); projectName = fetchedProject.get('name') || fetchedProject.get('title') || '未知项目'; console.log(`✅ 项目名称: ${projectName}`); } catch (error) { console.warn(`⚠️ 无法查询项目 ${projectId}:`, error); } } } return { id: obj.id, title: obj.get('title') || obj.get('description')?.slice(0, 40) || '未命名问题', description: obj.get('description'), priority: obj.get('priority') as IssuePriority || 'medium', type: obj.get('issueType') as IssueType || 'task', status: this.zh2enStatus(obj.get('status')) as IssueStatus, projectId, projectName, relatedSpace: obj.get('relatedSpace') || data.relatedSpace, relatedStage: obj.get('relatedStage') || data.relatedStage, assigneeName: assignee?.get('name') || assignee?.get('realname') || '未指派', creatorName: creator?.get('name') || creator?.get('realname') || '未知', createdAt: obj.get('createdAt') || new Date(), updatedAt: obj.get('updatedAt') || new Date(), dueDate: obj.get('dueDate'), tags: (data.tags || []) as string[] }; })); // 按优先级排序 tasks.sort((a, b) => { const priorityA = this.getPriorityOrder(a.priority); const priorityB = this.getPriorityOrder(b.priority); if (priorityA !== priorityB) { return priorityA - priorityB; } return +new Date(b.updatedAt) - +new Date(a.updatedAt); }); this.todoTasksFromIssues.set(tasks); // ⭐ 计算紧急事件(复用组长端逻辑) await this.loadProjectTimelineData(); this.calculateUrgentEvents(); console.log(`✅ [客服-待办任务] 加载完成: ${tasks.length} 个任务`); } catch (error) { console.error('❌ [客服-待办任务] 加载失败:', error); this.todoTaskError.set('加载待办任务失败,请稍后重试'); this.todoTasksFromIssues.set([]); this.urgentTasks.set([]); } finally { this.loadingTodoTasks.set(false); } } /** * 获取优先级顺序 */ private getPriorityOrder(priority: IssuePriority): number { const order: Record = { urgent: 0, critical: 0, high: 1, medium: 2, low: 3 }; return order[priority] || 999; } /** * 状态映射(中文 -> 英文) */ private zh2enStatus(status: string): IssueStatus { const map: Record = { '待处理': 'open', '处理中': 'in_progress', '已解决': 'resolved', '已关闭': 'closed' }; return map[status] || 'open'; } /** * ⭐ 加载项目时间轴数据(用于计算紧急事件) * 复用组长端逻辑:从 ProjectTeam 表获取项目与设计师的关联关系 */ async loadProjectTimelineData(): Promise { try { const cid = localStorage.getItem('company') || 'cDL6R1hgSi'; // 查询当前公司的所有项目 const projectQuery = new Parse.Query('Project'); projectQuery.equalTo('company', cid); projectQuery.notEqualTo('isDeleted', true); // 关键:包含 assignee 指针,避免出现 assignee.get 不是函数 projectQuery.include('assignee'); projectQuery.limit(100); const projects = await projectQuery.find(); console.log(`📊 [紧急事件] 查询到 ${projects.length} 个项目`); // 转换为项目时间轴格式 this.projectTimelineData = projects.map((project: any) => { const data = project.get('data') || {}; const phaseDeadlines = data.phaseDeadlines || {}; // 获取小图对图时间 let reviewDate = project.get('demoday') || project.get('reviewDate'); // 获取交付时间 const deliveryDate = project.get('deadline') || project.get('deliveryDate') || project.get('expectedDeliveryDate'); // 获取开始时间 const startDate = project.get('createdAt') || project.createdAt; // 获取当前阶段 const currentStage = project.get('currentStage') || '建模阶段'; // 获取设计师名称 const assignee = project.get('assignee'); let designerName: string = '未分配'; let designerId: string | undefined = undefined; if (assignee) { // 兼容 Parse.Object / 普通对象 / 字符串ID // eslint-disable-next-line @typescript-eslint/no-explicit-any const anyAssignee: any = assignee; designerId = anyAssignee?.id || (typeof anyAssignee === 'string' ? anyAssignee : undefined); if (typeof anyAssignee?.get === 'function') { designerName = anyAssignee.get('name') || anyAssignee.get('realname') || anyAssignee.get('realName') || '未分配'; } else if (typeof anyAssignee === 'object') { designerName = anyAssignee.name || anyAssignee.realname || anyAssignee.realName || '未分配'; } else if (typeof anyAssignee === 'string') { // 如果后端直接存ID字符串,这里先显示ID占位,避免崩溃 designerName = anyAssignee; } } // 获取空间交付物汇总 const spaceDeliverableSummary = data.spaceDeliverableSummary; // ===== 复用组长端的回退计算逻辑,补齐缺失的 reviewDate 与 phaseDeadlines ===== // 计算today,以便生成合理的回退时间 const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); // 若 reviewDate 缺失且存在交付日期,则将其设置在项目周期60%位置(下午2点) if (!reviewDate && deliveryDate && startDate) { const end = new Date(deliveryDate).getTime(); const start = new Date(startDate).getTime(); const mid = start + Math.max(0, Math.floor((end - start) * 0.6)); const midDate = new Date(mid); midDate.setHours(14, 0, 0, 0); reviewDate = midDate; } // 若缺少阶段截止时间,按交付日期向前推算(后期=交付日,渲染=交付-1天,软装=交付-2天,建模=交付-3天) let phaseDeadlinesFallback = phaseDeadlines; if ((!phaseDeadlinesFallback || Object.keys(phaseDeadlinesFallback).length === 0) && deliveryDate && startDate) { const deliveryTime = new Date(deliveryDate).getTime(); const postProcessingDeadline = new Date(deliveryTime); const renderingDeadline = new Date(deliveryTime - 1 * 24 * 60 * 60 * 1000); const softDecorDeadline = new Date(deliveryTime - 2 * 24 * 60 * 60 * 1000); const modelingDeadline = new Date(deliveryTime - 3 * 24 * 60 * 60 * 1000); phaseDeadlinesFallback = { modeling: { startDate: new Date(startDate), deadline: modelingDeadline, estimatedDays: 1, status: now.getTime() >= modelingDeadline.getTime() && now.getTime() < softDecorDeadline.getTime() ? 'in_progress' : now.getTime() >= softDecorDeadline.getTime() ? 'completed' : 'not_started', priority: 'high' }, softDecor: { startDate: modelingDeadline, deadline: softDecorDeadline, estimatedDays: 1, status: now.getTime() >= softDecorDeadline.getTime() && now.getTime() < renderingDeadline.getTime() ? 'in_progress' : now.getTime() >= renderingDeadline.getTime() ? 'completed' : 'not_started', priority: 'medium' }, rendering: { startDate: softDecorDeadline, deadline: renderingDeadline, estimatedDays: 1, status: now.getTime() >= renderingDeadline.getTime() && now.getTime() < postProcessingDeadline.getTime() ? 'in_progress' : now.getTime() >= postProcessingDeadline.getTime() ? 'completed' : 'not_started', priority: 'high' }, postProcessing: { startDate: renderingDeadline, deadline: postProcessingDeadline, estimatedDays: 1, status: now.getTime() >= postProcessingDeadline.getTime() ? 'completed' : now.getTime() >= renderingDeadline.getTime() ? 'in_progress' : 'not_started', priority: 'medium' } } as any; } return { projectId: project.id, projectName: project.get('name') || project.get('title') || '未命名项目', designerId, designerName, startDate: startDate ? new Date(startDate) : new Date(), endDate: deliveryDate ? new Date(deliveryDate) : new Date(), deliveryDate: deliveryDate ? new Date(deliveryDate) : undefined, reviewDate: reviewDate ? new Date(reviewDate) : undefined, currentStage, stageName: currentStage, stageProgress: 50, status: 'normal' as const, isStalled: false, stalledDays: 0, urgentCount: 0, priority: 'medium' as const, spaceName: '', customerName: project.get('customerName') || '', phaseDeadlines: phaseDeadlinesFallback, spaceDeliverableSummary }; }); console.log(`✅ [紧急事件] 项目时间轴数据准备完成: ${this.projectTimelineData.length} 条`); } catch (error) { console.error('❌ [紧急事件] 加载项目时间轴数据失败:', error); } } /** * 🆕 从项目时间轴数据计算紧急事件 * 复用组长端逻辑:识别截止时间已到或即将到达但未完成的关键节点 */ calculateUrgentEvents(): void { this.loadingUrgentEvents.set(true); const events: UrgentEvent[] = []; const now = new Date(); const oneDayMs = 24 * 60 * 60 * 1000; try { // 从 projectTimelineData 中提取数据 this.projectTimelineData.forEach(project => { // 1. 检查小图对图事件(与组长端一致) if (project.reviewDate) { const reviewTime = project.reviewDate.getTime(); const timeDiff = reviewTime - now.getTime(); const daysDiff = Math.ceil(timeDiff / oneDayMs); // 如果小图对图已经到期或即将到期(1天内),且不在交付完成阶段 if (daysDiff <= 1 && project.currentStage !== 'delivery') { events.push({ id: `${project.projectId}-review`, title: `小图对图截止`, description: `项目「${project.projectName}」的小图对图时间已${daysDiff < 0 ? '逾期' : '临近'}`, eventType: 'review', deadline: project.reviewDate, projectId: project.projectId, projectName: project.projectName, designerName: project.designerName, urgencyLevel: daysDiff < 0 ? 'critical' : daysDiff === 0 ? 'high' : 'medium', overdueDays: -daysDiff }); } } // 2. 检查交付事件(与组长端一致) if (project.deliveryDate) { const deliveryTime = project.deliveryDate.getTime(); const timeDiff = deliveryTime - now.getTime(); const daysDiff = Math.ceil(timeDiff / oneDayMs); // 如果交付已经到期或即将到期(1天内),且不在交付完成阶段 if (daysDiff <= 1 && project.currentStage !== 'delivery') { const summary = project.spaceDeliverableSummary; const completionRate = summary?.overallCompletionRate || 0; events.push({ id: `${project.projectId}-delivery`, title: `项目交付截止`, description: `项目「${project.projectName}」需要在 ${project.deliveryDate.toLocaleDateString()} 交付(当前完成率 ${completionRate}%)`, eventType: 'delivery', deadline: project.deliveryDate, projectId: project.projectId, projectName: project.projectName, designerName: project.designerName, urgencyLevel: daysDiff < 0 ? 'critical' : daysDiff === 0 ? 'high' : 'medium', overdueDays: -daysDiff, completionRate }); } } // 3. 检查各阶段截止时间 if (project.phaseDeadlines) { const phaseMap: Record = { modeling: '建模', softDecor: '软装', rendering: '渲染', postProcessing: '后期' }; Object.entries(project.phaseDeadlines).forEach(([key, phaseInfo]: [string, any]) => { if (phaseInfo && phaseInfo.deadline) { const deadline = new Date(phaseInfo.deadline); const phaseTime = deadline.getTime(); const timeDiff = phaseTime - now.getTime(); const daysDiff = Math.ceil(timeDiff / oneDayMs); // 如果阶段已经到期或即将到期(1天内),且状态不是已完成 if (daysDiff <= 1 && phaseInfo.status !== 'completed') { const phaseName = phaseMap[key] || key; // 获取该阶段的完成率 const summary = project.spaceDeliverableSummary; let completionRate = 0; if (summary && summary.phaseProgress) { const phaseProgress = summary.phaseProgress[key as keyof typeof summary.phaseProgress]; completionRate = phaseProgress?.completionRate || 0; } events.push({ id: `${project.projectId}-phase-${key}`, title: `${phaseName}阶段截止`, description: `项目「${project.projectName}」的${phaseName}阶段截止时间已${daysDiff < 0 ? '逾期' : '临近'}(完成率 ${completionRate}%)`, eventType: 'phase_deadline', phaseName, deadline, projectId: project.projectId, projectName: project.projectName, designerName: project.designerName, urgencyLevel: daysDiff < 0 ? 'critical' : daysDiff === 0 ? 'high' : 'medium', overdueDays: -daysDiff, completionRate }); } } }); } }); // 按紧急程度和时间排序 events.sort((a, b) => { // 首先按紧急程度排序 const urgencyOrder: Record = { critical: 0, high: 1, medium: 2 }; const urgencyDiff = urgencyOrder[a.urgencyLevel] - urgencyOrder[b.urgencyLevel]; if (urgencyDiff !== 0) return urgencyDiff; // 相同紧急程度,按截止时间排序(越早越靠前) return a.deadline.getTime() - b.deadline.getTime(); }); this.urgentEventsList.set(events); console.log(`✅ [客服-紧急事件] 计算完成,共 ${events.length} 个紧急事件`); } catch (error) { console.error('❌ [客服-紧急事件] 计算失败:', error); } finally { this.loadingUrgentEvents.set(false); } } /** * 手动刷新待办任务和紧急事件 */ async refreshTodoTasks(): Promise { console.log('🔄 [客服-待办任务] 手动刷新...'); await this.loadTodoTasksFromIssues(); // 紧急事件会在 loadTodoTasksFromIssues 中自动刷新 } /** * ⭐ 从紧急事件面板查看项目 */ onUrgentEventViewProject(projectId: string): void { console.log('🔍 [紧急事件] 查看项目:', projectId); // 跳转到项目详情页 const cid = localStorage.getItem('company') || 'cDL6R1hgSi'; this.router.navigate(['/wxwork', cid, 'project', projectId, 'order'], { queryParams: { roleName: 'customer-service' } }); } /** * 🆕 待办事项标签筛选 */ filterUrgentEventsByTag(tag: 'all' | 'customer' | 'phase' | 'review' | 'delivery'): void { console.log(`🏷️ [紧急事件] 应用标签筛选: ${tag}`); this.urgentEventTagFilter.set(tag); } /** * 获取指定标签的计数 */ getTagCount(tagId: string): number { const tag = this.urgentEventTags().find(t => t.id === tagId); return tag?.count || 0; } /** * ⭐ 从待办任务面板查看详情(跳转到项目并显示问题弹窗) */ navigateToIssue(task: TodoTaskFromIssue): void { console.log('🔍 [待办任务] 查看详情:', task.title); // 跳转到项目详情页,并打开问题板块 const cid = localStorage.getItem('company') || 'cDL6R1hgSi'; this.router.navigate(['/wxwork', cid, 'project', task.projectId, 'order'], { queryParams: { openIssues: 'true', highlightIssue: task.id, roleName: 'customer-service' } }); } /** * ⭐ 从待办任务面板查看详情 */ onTodoTaskViewDetails(task: TodoTaskFromIssue): void { console.log('🔍 [待办任务] 查看详情:', task.title); // 跳转到项目详情页,并打开问题板块 const cid = localStorage.getItem('company') || 'cDL6R1hgSi'; this.router.navigate(['/wxwork', cid, 'project', task.projectId, 'order'], { queryParams: { openIssues: 'true', highlightIssue: task.id, roleName: 'customer-service' } }); } /** * ⭐ 从待办任务面板标记为已读 */ async onTodoTaskMarkAsRead(task: TodoTaskFromIssue): Promise { try { console.log('✅ [待办任务] 标记为已读:', task.title); // 从列表中移除 const currentTasks = this.todoTasksFromIssues(); const updatedTasks = currentTasks.filter(t => t.id !== task.id); this.todoTasksFromIssues.set(updatedTasks); // ⭐ 刷新紧急事件列表 await this.loadProjectTimelineData(); this.calculateUrgentEvents(); console.log(`✅ 标记问题为已读: ${task.title}`); } catch (error) { console.error('❌ 标记已读失败:', error); } } /** * ⭐ 刷新待办任务和紧急事件 */ async onRefreshTodoTasks(): Promise { console.log('🔄 [待办任务] 刷新...'); await this.refreshTodoTasks(); } /** * ⭐ 将紧急事件标记为已处理(用于紧急事件面板的checkbox) */ async onUrgentEventMarkAsHandled(event: UrgentEvent): Promise { try { console.log('✅ [紧急事件] 标记为已处理:', event.title); // 从紧急事件列表中移除 const currentEvents = this.urgentEventsList(); const updatedEvents = currentEvents.filter(e => e.id !== event.id); this.urgentEventsList.set(updatedEvents); // ⭐ 刷新紧急事件列表 await this.loadProjectTimelineData(); this.calculateUrgentEvents(); } catch (error) { console.error('❌ 标记已处理失败:', error); } } /** * 获取优先级配置 */ getPriorityConfig(priority: IssuePriority): { label: string; icon: string; color: string; order: number } { const config: Record = { urgent: { label: '紧急', icon: '🔴', color: '#dc2626', order: 0 }, critical: { label: '紧急', icon: '🔴', color: '#dc2626', order: 0 }, high: { label: '高', icon: '🟠', color: '#ea580c', order: 1 }, medium: { label: '中', icon: '🟡', color: '#ca8a04', order: 2 }, low: { label: '低', icon: '⚪', color: '#9ca3af', order: 3 } }; return config[priority] || config.medium; } /** * 获取问题类型标签 * IssueType 定义: 'bug' | 'task' | 'feedback' | 'risk' | 'feature' */ getIssueTypeLabel(type: IssueType): string { const labels: Record = { bug: '缺陷', feature: '需求', task: '任务', feedback: '反馈', risk: '风险' }; return labels[type] || '其他'; } /** * 获取状态标签 * IssueStatus 定义: 'open' | 'in_progress' | 'resolved' | 'closed' */ getIssueStatusLabel(status: IssueStatus): string { const labels: Record = { open: '待处理', in_progress: '处理中', resolved: '已解决', closed: '已关闭' }; return labels[status] || '待处理'; } // 新增:一键发送大图 sendLargeImages(projectId: string): void { const projects = this.pendingFinalPaymentProjects(); const project = projects.find(p => p.projectId === projectId); if (!project) return; console.log(`正在为项目 ${projectId} 发送大图到企业微信...`); // 模拟发送过程 setTimeout(() => { const updatedProjects = projects.map(p => { if (p.projectId === projectId) { return { ...p, largeImagesSent: true }; } return p; }); this.pendingFinalPaymentProjects.set(updatedProjects); console.log(`✅ 项目 ${projectId} 大图已成功发送到企业微信服务群`); console.log(`📱 已同步发送支付成功与大图交付通知`); window?.fmode?.alert(`🎉 大图发送成功! ✅ 已完成操作: • 大图已发送至企业微信服务群 • 已通知客户支付成功 • 已确认大图交付完成 项目:${project.projectName} 客户:${project.customerName}`); }, 2000); } /** * 标记问题为已读 */ async markAsRead(task: TodoTaskFromIssue): Promise { try { // 本地隐藏(不修改数据库) const currentTasks = this.todoTasksFromIssues(); const updatedTasks = currentTasks.filter(t => t.id !== task.id); this.todoTasksFromIssues.set(updatedTasks); // ⭐ 刷新紧急事件列表 await this.loadProjectTimelineData(); this.calculateUrgentEvents(); console.log(`✅ 标记问题为已读: ${task.title}`); } catch (error) { console.error('❌ 标记已读失败:', error); } } /** * 格式化相对时间(精确到秒) */ formatRelativeTime(date: Date | string): string { if (!date) { return '未知时间'; } try { const targetDate = new Date(date); const now = new Date(); const diff = now.getTime() - targetDate.getTime(); const seconds = Math.floor(diff / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); if (seconds < 60) { return `${seconds}秒前`; } else if (minutes < 60) { return `${minutes}分钟前`; } else if (hours < 24) { return `${hours}小时前`; } else if (days < 7) { return `${days}天前`; } else { return targetDate.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' }); } } catch (error) { console.error('❌ formatRelativeTime 错误:', error, 'date:', date); return '时间格式错误'; } } /** * 格式化精确时间(用于 tooltip) * 格式:YYYY-MM-DD HH:mm:ss */ formatExactTime(date: Date | string): string { if (!date) { return '未知时间'; } try { const d = new Date(date); const year = d.getFullYear(); const month = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); const hours = String(d.getHours()).padStart(2, '0'); const minutes = String(d.getMinutes()).padStart(2, '0'); const seconds = String(d.getSeconds()).padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; } catch (error) { console.error('❌ formatExactTime 错误:', error, 'date:', date); return '时间格式错误'; } } }