| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080 |
- // 客服工作台 - 对接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<Task[]>([]);
- // 任务处理状态
- taskProcessingState = signal<Partial<Record<string, { inProgress: boolean; progress: number }>>>({});
-
- // 从问题板块加载的待办任务列表(复用组长端)
- todoTasksFromIssues = signal<TodoTaskFromIssue[]>([]);
- loadingTodoTasks = signal(false);
- todoTaskError = signal('');
-
- // ⭐ 紧急事件列表(复用组长端逻辑)
- urgentEventsList = signal<UrgentEvent[]>([]);
- loadingUrgentEvents = signal(false);
-
- // 项目时间轴数据(用于计算紧急事件)
- projectTimelineData: any[] = [];
-
- // 新增:待跟进尾款项目列表(真实数据)
- pendingFinalPaymentProjects = signal<Array<{
- id: string;
- projectId: string;
- projectName: string;
- customerName: string;
- customerPhone: string;
- finalPaymentAmount: number; // 剩余未付金额
- totalAmount: number; // 订单总金额
- paidAmount: number; // 已付金额
- dueDate: Date;
- status: string; // 已逾期/待创建/待付款
- overdueDay: number;
- }>>([]);
-
- // 项目动态流(扩展包含问题事件)
- 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<any[]>([]);
-
- // 空间列表(用于下拉选择)
- spaceList = signal<any[]>([]);
-
- // 团队成员列表(用于指派)
- teamMembers = signal<any[]>([]);
-
- // 新任务数据
- 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<any>(null);
- company = signal<any>(null);
- // 初始化用户和公司信息
- private async initializeUserAndCompany(): Promise<void> {
- 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<void> {
- try {
- await this.initializeUserAndCompany();
- await this.loadDashboardData();
- // 添加滚动事件监听
- window.addEventListener('scroll', this.onScroll.bind(this));
- } catch (error) {
- console.error('❌ 客服工作台初始化失败:', error);
- }
- }
- // 加载仪表板数据
- private async loadDashboardData(): Promise<void> {
- 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<void> {
- 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<void> {
- // 此方法已被 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<void> {
- 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<void> {
- 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<void> {
- 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<void> {
- // 重置表单数据
- 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<void> {
- 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<void> {
- // 验证表单数据
- 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<void> {
- 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<void> {
- 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<void> {
- 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<IssuePriority, number> = {
- urgent: 0,
- critical: 0,
- high: 1,
- medium: 2,
- low: 3
- };
- return order[priority] || 999;
- }
-
- /**
- * 状态映射(中文 -> 英文)
- */
- private zh2enStatus(status: string): IssueStatus {
- const map: Record<string, IssueStatus> = {
- '待处理': 'open',
- '处理中': 'in_progress',
- '已解决': 'resolved',
- '已关闭': 'closed'
- };
- return map[status] || 'open';
- }
-
- /**
- * ⭐ 加载项目时间轴数据(用于计算紧急事件)
- * 复用组长端逻辑:从 ProjectTeam 表获取项目与设计师的关联关系
- */
- async loadProjectTimelineData(): Promise<void> {
- try {
- const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
-
- // 查询当前公司的所有项目
- const projectQuery = new Parse.Query('Project');
- projectQuery.equalTo('company', cid);
- projectQuery.notEqualTo('isDeleted', true);
- 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 || {};
-
- // 获取小图对图时间
- const 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');
- const designerName = assignee?.get('name') || assignee?.get('realname') || '未分配';
-
- // 获取空间交付物汇总
- const spaceDeliverableSummary = data.spaceDeliverableSummary;
-
- return {
- projectId: project.id,
- projectName: project.get('name') || project.get('title') || '未命名项目',
- designerId: assignee?.id,
- 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,
- 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' && project.currentStage !== '已完成' && project.currentStage !== '交付完成') {
- 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' && project.currentStage !== '已完成' && project.currentStage !== '交付完成') {
- 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<string, string> = {
- 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<string, number> = { 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<void> {
- 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' }
- });
- }
-
- /**
- * ⭐ 从待办任务面板查看详情
- */
- 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<void> {
- 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<void> {
- console.log('🔄 [待办任务] 刷新...');
- await this.refreshTodoTasks();
- }
-
- /**
- * ⭐ 将紧急事件标记为已处理(用于紧急事件面板的checkbox)
- */
- async onUrgentEventMarkAsHandled(event: UrgentEvent): Promise<void> {
- 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<IssuePriority, { label: string; icon: string; color: string; order: number }> = {
- 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<IssueType, string> = {
- bug: '缺陷',
- feature: '需求',
- task: '任务',
- feedback: '反馈',
- risk: '风险'
- };
- return labels[type] || '其他';
- }
-
- /**
- * 获取状态标签
- * IssueStatus 定义: 'open' | 'in_progress' | 'resolved' | 'closed'
- */
- getIssueStatusLabel(status: IssueStatus): string {
- const labels: Record<IssueStatus, string> = {
- open: '待处理',
- in_progress: '处理中',
- resolved: '已解决',
- closed: '已关闭'
- };
- return labels[status] || '待处理';
- }
-
- /**
- * 跳转到项目问题详情
- */
- navigateToIssue(task: TodoTaskFromIssue): void {
- console.log(`📋 跳转到问题详情: ${task.id}, 项目ID: ${task.projectId}`);
-
- // 获取当前公司ID
- const cid = localStorage.getItem('company') || 'cDL6R1hgSi';
-
- // 导航到wxwork模块的项目问题详情页
- this.router.navigate(['/wxwork', cid, 'project', task.projectId, 'issues'], {
- queryParams: { issueId: task.id }
- });
- }
- // 新增:一键发送大图
- 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<void> {
- 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 '时间格式错误';
- }
- }
- }
|