import { Component, OnInit, Input, Output, EventEmitter, OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Router, ActivatedRoute, RouterModule } from '@angular/router'; import { IonicModule } from '@ionic/angular'; import { WxworkSDK, WxworkCorp, WxworkAuth } from 'fmode-ng/core'; import { FmodeParse, FmodeObject } from 'fmode-ng/parse'; import { ProfileService } from '../../../../app/services/profile.service'; import { ProjectBottomCardComponent } from '../../components/project-bottom-card/project-bottom-card.component'; import { ProjectFilesModalComponent } from '../../components/project-files-modal/project-files-modal.component'; import { ProjectMembersModalComponent } from '../../components/project-members-modal/project-members-modal.component'; import { ProjectIssuesModalComponent } from '../../components/project-issues-modal/project-issues-modal.component'; import { ProjectIssueService } from '../../services/project-issue.service'; import { FormsModule } from '@angular/forms'; import { CustomerSelectorComponent } from '../../components/contact-selector/contact-selector.component'; import { OrderApprovalPanelComponent } from '../../../../app/shared/components/order-approval-panel/order-approval-panel.component'; import { GroupChatSummaryComponent } from '../../components/group-chat-summary/group-chat-summary.component'; const Parse = FmodeParse.with('nova'); /** * 项目详情核心组件 * * 功能? * 1. 展示四阶段导航(订单分配、确认需求、交付执行、售后归档) * 2. 根据角色控制权限 * 3. 子路由切换阶段内? * 4. 支持@Input和路由参数两种数据加载方? * * 路由?wxwork/:cid/project/:projectId */ @Component({ selector: 'app-project-detail', standalone: true, imports: [ CommonModule, IonicModule, RouterModule, ProjectBottomCardComponent, ProjectFilesModalComponent, ProjectMembersModalComponent, ProjectIssuesModalComponent, CustomerSelectorComponent, OrderApprovalPanelComponent, GroupChatSummaryComponent ], templateUrl: './project-detail.component.html', styleUrls: ['./project-detail.component.scss'] }) export class ProjectDetailComponent implements OnInit, OnDestroy { // 输入参数(支持组件复用) @Input() project: FmodeObject | null = null; @Input() groupChat: FmodeObject | null = null; @Input() currentUser: FmodeObject | null = null; // 问题统计 issueCount: number = 0; // 路由参数 cid: string = ''; projectId: string = ''; groupId: string = ''; profileId: string = ''; chatId: string = ''; // 从企微进入时?chat_id // 企微SDK wxwork: WxworkSDK | null = null; wecorp: WxworkCorp | null = null; wxAuth: WxworkAuth | null = null; // WxworkAuth 实例 // 加载状? loading: boolean = true; error: string | null = null; // 项目数据 contact: FmodeObject | null = null; assignee: FmodeObject | null = null; // 当前阶段 currentStage: string = 'order'; // order | requirements | delivery | aftercare stages = [ { id: 'order', name: '订单分配', icon: 'document-text-outline', number: 1 }, { id: 'requirements', name: '确认需?, icon: 'checkmark-circle-outline', number: 2 }, { id: 'delivery', name: '交付执行', icon: 'rocket-outline', number: 3 }, { id: 'aftercare', name: '售后归档', icon: 'archive-outline', number: 4 } ]; // 权限 canEdit: boolean = false; canViewCustomerPhone: boolean = false; role: string = ''; // 模态框状? showFilesModal: boolean = false; showMembersModal: boolean = false; showIssuesModal: boolean = false; // 新增:客户详情侧栏面板状? showContactPanel: boolean = false; // 问卷状? surveyStatus: { filled: boolean; text: string; icon: string; surveyLog?: FmodeObject; contact?: FmodeObject; } = { filled: false, text: '发送问?, icon: 'document-text-outline' }; // 折叠:项目基本信? showProjectInfoCollapsed: boolean = true; // 事件监听器引? private stageCompletedListener: any = null; constructor( private router: Router, private route: ActivatedRoute, private profileService: ProfileService, private issueService: ProjectIssueService ) {} async ngOnInit() { // 获取路由参数 this.cid = this.route.snapshot.paramMap.get('cid') || ''; this.projectId = this.route.snapshot.paramMap.get('projectId') || ''; this.groupId = this.route.snapshot.queryParamMap.get('groupId') || ''; this.profileId = this.route.snapshot.queryParamMap.get('profileId') || ''; this.chatId = this.route.snapshot.queryParamMap.get('chatId') || ''; // 监听路由变化 this.route.firstChild?.url.subscribe((segments) => { if (segments.length > 0) { this.currentStage = segments[0].path; console.log('🔄 当前阶段已更?', this.currentStage); } }); // 初始化企微授权(不阻塞页面加载) await this.initWxworkAuth(); await this.loadData(); // 初始化工作流阶段(若缺失则根据已完成记录推断? this.ensureWorkflowStage(); // 监听各阶段完成事件,自动推进到下一环节 this.stageCompletedListener = async (e: any) => { const stageId = e?.detail?.stage as string; if (!stageId) return; console.log('?接收到阶段完成事?', stageId); await this.advanceToNextStage(stageId); }; document.addEventListener('stage:completed', this.stageCompletedListener); } /** * 组件销毁时清理事件监听? */ ngOnDestroy() { if (this.stageCompletedListener) { document.removeEventListener('stage:completed', this.stageCompletedListener); console.log('🧹 已清理阶段完成事件监听器'); } } /** * 初始化企微授权(不阻塞页面) */ async initWxworkAuth() { try { let cid = this.cid || localStorage.getItem("company") || ""; // 如果没有cid,记录警告但不抛出错? if (!cid) { console.warn('⚠️ 未找到company ID (cid),企微功能将不可?); return; } this.wxAuth = new WxworkAuth({ cid: cid }); this.wxwork = new WxworkSDK({ cid: cid, appId: 'crm' }); this.wecorp = new WxworkCorp(cid); console.log('?企微SDK初始化成功,cid:', cid); } catch (error) { console.error('?企微SDK初始化失?', error); // 不阻塞页面加? } } /** * 折叠/展开 项目基本信息 */ toggleProjectInfo(): void { this.showProjectInfoCollapsed = !this.showProjectInfoCollapsed; } /** * 跳转到指定阶? */ goToStage(stageId: 'order'|'requirements'|'delivery'|'aftercare') { // 子路由:当前?/.../project-detail/:projectId/:stage this.currentStage = stageId; // 使用上级相对路径,确保在任何嵌套路由下都能正确切? this.router.navigate(['../', stageId], { relativeTo: this.route }); } /** * 从给定阶段推进到下一个阶? */ async advanceToNextStage(current: string) { const order = ['order','requirements','delivery','aftercare']; const idx = order.indexOf(current); console.log('🚀 推进阶段:', { current, idx, currentStage: this.currentStage }); if (idx === -1) { console.warn('⚠️ 未找到当前阶?', current); return; } if (idx >= order.length - 1) { console.log('?已到达最后阶?); window?.fmode?.alert('所有阶段已完成?); return; } const next = order[idx + 1]; console.log('➡️ 跳转到下一阶段:', next); // 持久化:标记当前阶段完成并设置下一阶段为当? await this.persistStageProgress(current, next); // 导航到下一阶段(不改变工作流颜色判定,仅切换内容) this.goToStage(next as any); const nextStageName = this.stages.find(s => s.id === next)?.name || next; window?.fmode?.alert(`已自动跳转到下一阶段: ${nextStageName}`); } /** * 确保存在工作流当前阶段。如缺失则根据完成记录计? */ ensureWorkflowStage() { if (!this.project) return; const order = ['order','requirements','delivery','aftercare']; const data = this.project.get('data') || {}; const statuses = data.stageStatuses || {}; let current = this.project.get('currentStage'); if (!current) { // 找到第一个未完成的阶? current = order.find(s => statuses[s] !== 'completed') || 'aftercare'; this.project.set('currentStage', current); } } /** * 持久化阶段推进(标记当前完成、设置下一阶段? */ private async persistStageProgress(current: string, next: string) { if (!this.project) { console.warn('⚠️ 项目对象不存在,无法持久?); return; } console.log('💾 开始持久化阶段:', { current, next }); const data = this.project.get('data') || {}; data.stageStatuses = data.stageStatuses || {}; data.stageStatuses[current] = 'completed'; this.project.set('data', data); this.project.set('currentStage', next); console.log('💾 设置阶段状?', { currentStage: next, stageStatuses: data.stageStatuses }); try { await this.project.save(); console.log('?阶段状态持久化成功'); } catch (e) { console.warn('⚠️ 阶段状态持久化失败(忽略以保证流程可继续):', e); } } /** * 加载数据 */ async loadData() { try { this.loading = true; // 2. 获取当前用户(优先从全局服务获取? if (!this.currentUser?.id && this.wxAuth) { try { this.currentUser = await this.wxAuth.currentProfile(); } catch (error) { console.warn('⚠️ 获取当前用户Profile失败:', error); } } // 设置权限 this.role = this.currentUser?.get('roleName') || ''; this.canEdit = ['客服', '组员', '组长', '管理?, '设计?, '客服主管'].includes(this.role); this.canViewCustomerPhone = ['客服', '组长', '管理?].includes(this.role); const companyId = this.currentUser?.get('company')?.id || localStorage?.getItem("company"); // 3. 加载项目 if (!this.project) { if (this.projectId) { // 通过 projectId 加载(从后台进入? const query = new Parse.Query('Project'); query.include('contact', 'assignee','department','department.leader'); this.project = await query.get(this.projectId); } else if (this.chatId) { // 通过 chat_id 查找项目(从企微群聊进入? if (companyId) { // 先查?GroupChat const gcQuery = new Parse.Query('GroupChat'); gcQuery.equalTo('chat_id', this.chatId); gcQuery.equalTo('company', companyId); let groupChat = await gcQuery.first(); if (groupChat) { this.groupChat = groupChat; const projectPointer = groupChat.get('project'); if (projectPointer) { const pQuery = new Parse.Query('Project'); pQuery.include('contact', 'assignee','department','department.leader'); this.project = await pQuery.get(projectPointer.id); } } if (!this.project) { throw new Error('该群聊尚未关联项目,请先在后台创建项?); } } } } if(!this.groupChat?.id){ const gcQuery2 = new Parse.Query('GroupChat'); gcQuery2.equalTo('project', this.projectId); gcQuery2.equalTo('company', companyId); this.groupChat = await gcQuery2.first(); } this.wxwork?.syncGroupChat(this.groupChat?.toJSON()) if (!this.project) { throw new Error('无法加载项目信息'); } this.contact = this.project.get('contact'); this.assignee = this.project.get('assignee'); // 加载问卷状? await this.loadSurveyStatus(); // 更新问题计数 try { if (this.project?.id) { this.issueService.seed(this.project.id!); const counts = this.issueService.getCounts(this.project.id!); this.issueCount = counts.total; } } catch (e) { console.warn('统计问题数量失败:', e); } // 4. 加载群聊(如果没有传入且有groupId? if (!this.groupChat && this.groupId) { try { const gcQuery = new Parse.Query('GroupChat'); this.groupChat = await gcQuery.get(this.groupId); } catch (err) { console.warn('加载群聊失败:', err); } } // 5. 根据项目当前阶段设置默认路由 const projectStage = this.project.get('currentStage'); const stageMap: any = { '订单分配': 'order', '确认需?: 'requirements', '方案确认': 'requirements', '方案深化': 'requirements', '交付执行': 'delivery', '建模': 'delivery', '软装': 'delivery', '渲染': 'delivery', '后期': 'delivery', '尾款结算': 'aftercare', '客户评价': 'aftercare', '投诉处理': 'aftercare' }; const targetStage = stageMap[projectStage] || 'order'; // 如果当前没有子路由,跳转到对应阶? if (!this.route.firstChild) { this.router.navigate([targetStage], { relativeTo: this.route, replaceUrl: true }); } } catch (err: any) { console.error('加载失败:', err); this.error = err.message || '加载失败'; } finally { this.loading = false; } } /** * 切换阶段 */ switchStage(stageId: string) { this.currentStage = stageId; this.router.navigate([stageId], { relativeTo: this.route }); } /** * 获取阶段状? */ getStageStatus(stageId: string): 'completed' | 'active' | 'pending' { // 颜色显示仅依?工作流状?,不受临时浏览路由影? const data = this.project?.get('data') || {}; const statuses = data.stageStatuses || {}; const workflowCurrent = this.project?.get('currentStage') || 'order'; console.log('🎨 计算阶段状?', { stageId, workflowCurrent, statuses, result: statuses[stageId] === 'completed' ? 'completed' : (workflowCurrent === stageId ? 'active' : 'pending') }); if (statuses[stageId] === 'completed') return 'completed'; if (workflowCurrent === stageId) return 'active'; return 'pending'; } /** * 返回 */ goBack() { let ua = navigator.userAgent.toLowerCase(); let isWeixin = ua.indexOf("micromessenger") != -1; if(isWeixin){ this.router.navigate(['/wxwork', this.cid, 'project-loader']); }else{ history.back(); } } /** * 更新项目阶段 */ async updateProjectStage(stage: string) { if (!this.project || !this.canEdit) return; try { this.project.set('currentStage', stage); await this.project.save(); // 添加阶段历史 const data = this.project.get('data') || {}; const stageHistory = data.stageHistory || []; stageHistory.push({ stage, startTime: new Date(), status: 'current', operator: { id: this.currentUser!.id, name: this.currentUser!.get('name'), role: this.role } }); this.project.set('data', { ...data, stageHistory }); await this.project.save(); } catch (err) { console.error('更新阶段失败:', err); window?.fmode?.alert('更新失败'); } } /** * 发送企微消? */ async sendWxMessage(message: string) { if (!this.groupChat || !this.wecorp) return; try { const chatId = this.groupChat.get('chat_id'); await this.wecorp.appchat.sendText(chatId, message); } catch (err) { console.error('发送消息失?', err); } } /** * 选择客户(从群聊成员中选择外部联系人) */ async selectCustomer() { console.log(this.canEdit, this.groupChat) if (!this.groupChat) return; try { const memberList = this.groupChat.get('member_list') || []; const externalMembers = memberList.filter((m: any) => m.type === 2); if (externalMembers.length === 0) { window?.fmode?.alert('当前群聊中没有外部联系人'); return; } console.log(externalMembers) // 简单实现:选择第一个外部联系人 // TODO: 实现选择器UI const selectedMember = externalMembers[0]; await this.setCustomerFromMember(selectedMember); } catch (err) { console.error('选择客户失败:', err); window?.fmode?.alert('选择客户失败'); } } /** * 从群成员设置客户 */ async setCustomerFromMember(member: any) { if (!this.wecorp) return; try { const companyId = this.currentUser?.get('company')?.id || localStorage.getItem("company"); if (!companyId) throw new Error('无法获取企业信息'); // 1. 查询是否已存?ContactInfo const query = new Parse.Query('ContactInfo'); query.equalTo('external_userid', member.userid); query.equalTo('company', companyId); let contactInfo = await query.first(); // 2. 如果不存在,通过企微API获取并创? if (!contactInfo) { contactInfo = new Parse.Object("ContactInfo"); } const externalContactData = await this.wecorp.externalContact.get(member.userid); console.log("externalContactData",externalContactData) const ContactInfo = Parse.Object.extend('ContactInfo'); contactInfo.set('name', externalContactData.name); contactInfo.set('external_userid', member.userid); const company = new Parse.Object('Company'); company.id = companyId; const companyPointer = company.toPointer(); contactInfo.set('company', companyPointer); contactInfo.set('data', externalContactData); await contactInfo.save(); // 3. 设置为项目客? if (this.project) { this.project.set('contact', contactInfo.toPointer()); await this.project.save(); this.contact = contactInfo; window?.fmode?.alert('客户设置成功'); } } catch (err) { console.error('设置客户失败:', err); throw err; } } /** * 显示文件模态框 */ showFiles() { this.showFilesModal = true; } /** * 显示成员模态框 */ showMembers() { this.showMembersModal = true; } /** 显示问题模态框 */ showIssues() { this.showIssuesModal = true; } /** * 关闭文件模态框 */ closeFilesModal() { this.showFilesModal = false; } /** * 关闭成员模态框 */ closeMembersModal() { this.showMembersModal = false; } /** 显示客户详情面板 */ openContactPanel() { if (this.contact) { this.showContactPanel = true; } } /** 关闭客户详情面板 */ closeContactPanel() { this.showContactPanel = false; } /** 关闭问题模态框 */ closeIssuesModal() { this.showIssuesModal = false; if (this.project?.id) { const counts = this.issueService.getCounts(this.project.id!); this.issueCount = counts.total; } } /** 客户选择事件回调(接收子组件输出?*/ onContactSelected(evt: { contact: FmodeObject; isNewCustomer: boolean; action: 'selected' | 'created' | 'updated' }) { this.contact = evt.contact; // 重新加载问卷状? this.loadSurveyStatus(); } /** * 加载问卷状? */ async loadSurveyStatus() { if (!this.project?.id) return; try { const query = new Parse.Query('SurveyLog'); query.equalTo('project', this.project.toPointer()); query.equalTo('type', 'survey-project'); query.equalTo('isCompleted', true); query.include("contact") const surveyLog = await query.first(); if (surveyLog) { this.surveyStatus = { filled: true, text: '查看问卷', icon: 'checkmark-circle', surveyLog, contact:surveyLog?.get("contact") }; console.log('?问卷已填?); } else { this.surveyStatus = { filled: false, text: '发送问?, icon: 'document-text-outline' }; console.log('?问卷未填?); } } catch (err) { console.error('?查询问卷状态失?', err); } } /** * 发送问? */ async sendSurvey() { if (!this.groupChat || !this.wxwork) { window?.fmode?.alert('无法发送问?未找到群聊或企微SDK未初始化'); return; } try { const chatId = this.groupChat.get('chat_id'); const surveyUrl = `${document.baseURI}/wxwork/${this.cid}/survey/project/${this.project?.id}`; await this.wxwork.ww.openExistedChatWithMsg({ chatId: chatId, msg: { msgtype: 'link', link: { title: '《家装效果图服务需求调查表?, desc: '为让本次服务更贴合您的需?请花3-5分钟填写简短问?感谢支持!', url: surveyUrl, imgUrl: `${document.baseURI}/assets/logo.jpg` } } }); window?.fmode?.alert('问卷已发送到群聊!'); } catch (err) { console.error('?发送问卷失?', err); window?.fmode?.alert('发送失?请重?); } } /** * 查看问卷结果 */ async viewSurvey() { if (!this.surveyStatus.surveyLog) return; // 跳转到问卷页面查看结? this.router.navigate(['/wxwork', this.cid, 'survey', 'project', this.project?.id]); } /** * 处理问卷点击 */ async handleSurveyClick(event: Event) { event.stopPropagation(); if (this.surveyStatus.filled) { // 已填?查看结果 await this.viewSurvey(); } else { // 未填?发送问? await this.sendSurvey(); } } /** * 是否显示审批面板 * 条件:当前用户是组长 + 项目处于订单分配阶段 + 审批状态为待审? * ⚠️ 临时放开权限:允许所有角色查看审批面板(测试用) */ get showApprovalPanel(): boolean { if (!this.project || !this.currentUser) { console.log('🔍 审批面板检? 缺少项目或用户数?); return false; } const userRole = this.currentUser.get('roleName') || ''; // ⚠️ 临时注释角色检查,允许所有角色访? // const isTeamLeader = userRole === '设计组长' || userRole === 'team-leader'; const isTeamLeader = true; // 临时放开权限 const currentStage = this.project.get('currentStage') || ''; const isOrderStage = currentStage === '订单分配' || currentStage === 'order'; const data = this.project.get('data') || {}; const approvalStatus = data.approvalStatus; const isPending = approvalStatus === 'pending'; console.log('🔍 审批面板检?[临时放开权限]:', { userRole, isTeamLeader, currentStage, isOrderStage, approvalStatus, isPending, result: isTeamLeader && isOrderStage && isPending }); return isTeamLeader && isOrderStage && isPending; } /** * 处理审批完成事件 */ async onApprovalCompleted(event: { action: 'approved' | 'rejected'; reason?: string; comment?: string }) { if (!this.project) return; try { const data = this.project.get('data') || {}; const approvalHistory = data.approvalHistory || []; const latestRecord = approvalHistory[approvalHistory.length - 1]; if (latestRecord) { latestRecord.status = event.action; latestRecord.approver = { id: this.currentUser?.id, name: this.currentUser?.get('name'), role: this.currentUser?.get('roleName') }; latestRecord.approvalTime = new Date(); latestRecord.comment = event.comment; latestRecord.reason = event.reason; } if (event.action === 'approved') { // 通过审批:推进到确认需求阶? data.approvalStatus = 'approved'; this.project.set('currentStage', '确认需?); this.project.set('data', data); await this.project.save(); alert('?审批通过,项目已进入确认需求阶?); // 刷新页面数据 await this.loadData(); } else { // 驳回:保持在订单分配阶段,记录驳回原? data.approvalStatus = 'rejected'; data.lastRejectionReason = event.reason || '未提供原?; this.project.set('data', data); await this.project.save(); alert('?已驳回订单,客服将收到通知'); // 刷新页面数据 await this.loadData(); } } catch (err) { console.error('处理审批失败:', err); alert('审批操作失败,请重试'); } } } // duplicate inline CustomerSelectorComponent removed (we keep single declaration above)