| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707 | import { Component, OnInit, Input, Output, EventEmitter } 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 {  // 输入参数(支持组件复用)  @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'  };  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;      }    });    // 初始化企微授权(不阻塞页面加载)    await this.initWxworkAuth();    await this.loadData();  }  /**   * 初始化企微授权(不阻塞页面)   */  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);      // 不阻塞页面加载    }  }  /**   * 加载数据   */  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 projectStage = this.project?.get('currentStage') || '';    const stageOrder = ['订单分配', '确认需求', '建模', '软装', '渲染', '后期', '尾款结算', '客户评价'];    const currentIndex = stageOrder.indexOf(projectStage);    const stageIndexMap: any = {      'order': 0,      'requirements': 1,      'delivery': 3,      'aftercare': 6    };    const targetIndex = stageIndexMap[stageId];    if (currentIndex > targetIndex) {      return 'completed';    } else if (this.currentStage === stageId) {      return 'active';    } else {      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)
 |