project-detail.component.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { Router, ActivatedRoute, RouterModule } from '@angular/router';
  4. import { IonicModule } from '@ionic/angular';
  5. import { WxworkSDK, WxworkCorp, WxworkAuth } from 'fmode-ng/core';
  6. import { FmodeParse, FmodeObject } from 'fmode-ng/parse';
  7. import { ProfileService } from '../../../../app/services/profile.service';
  8. import { ProjectBottomCardComponent } from '../../components/project-bottom-card/project-bottom-card.component';
  9. import { ProjectFilesModalComponent } from '../../components/project-files-modal/project-files-modal.component';
  10. import { ProjectMembersModalComponent } from '../../components/project-members-modal/project-members-modal.component';
  11. import { ProjectIssuesModalComponent } from '../../components/project-issues-modal/project-issues-modal.component';
  12. import { ProjectIssueService } from '../../services/project-issue.service';
  13. import { CustomerProfileComponent } from '../contact/contact.component';
  14. import { FormsModule } from '@angular/forms';
  15. import { CustomerSelectorComponent } from '../../components/contact-selector/contact-selector.component';
  16. const Parse = FmodeParse.with('nova');
  17. /**
  18. * 项目详情核心组件
  19. *
  20. * 功能:
  21. * 1. 展示四阶段导航(订单分配、确认需求、交付执行、售后归档)
  22. * 2. 根据角色控制权限
  23. * 3. 子路由切换阶段内容
  24. * 4. 支持@Input和路由参数两种数据加载方式
  25. *
  26. * 路由:/wxwork/:cid/project/:projectId
  27. */
  28. @Component({
  29. selector: 'app-project-detail',
  30. standalone: true,
  31. imports: [
  32. CommonModule,
  33. IonicModule,
  34. RouterModule,
  35. ProjectBottomCardComponent,
  36. ProjectFilesModalComponent,
  37. ProjectMembersModalComponent,
  38. ProjectIssuesModalComponent,
  39. CustomerProfileComponent,
  40. CustomerSelectorComponent
  41. ],
  42. templateUrl: './project-detail.component.html',
  43. styleUrls: ['./project-detail.component.scss']
  44. })
  45. export class ProjectDetailComponent implements OnInit {
  46. // 输入参数(支持组件复用)
  47. @Input() project: FmodeObject | null = null;
  48. @Input() groupChat: FmodeObject | null = null;
  49. @Input() currentUser: FmodeObject | null = null;
  50. // 问题统计
  51. issueCount: number = 0;
  52. // 路由参数
  53. cid: string = '';
  54. projectId: string = '';
  55. groupId: string = '';
  56. profileId: string = '';
  57. chatId: string = ''; // 从企微进入时的 chat_id
  58. // 企微SDK
  59. wxwork: WxworkSDK | null = null;
  60. wecorp: WxworkCorp | null = null;
  61. wxAuth: WxworkAuth | null = null; // WxworkAuth 实例
  62. // 加载状态
  63. loading: boolean = true;
  64. error: string | null = null;
  65. // 项目数据
  66. contact: FmodeObject | null = null;
  67. assignee: FmodeObject | null = null;
  68. // 当前阶段
  69. currentStage: string = 'order'; // order | requirements | delivery | aftercare
  70. stages = [
  71. { id: 'order', name: '订单分配', icon: 'document-text-outline', number: 1 },
  72. { id: 'requirements', name: '确认需求', icon: 'checkmark-circle-outline', number: 2 },
  73. { id: 'delivery', name: '交付执行', icon: 'rocket-outline', number: 3 },
  74. { id: 'aftercare', name: '售后归档', icon: 'archive-outline', number: 4 }
  75. ];
  76. // 权限
  77. canEdit: boolean = false;
  78. canViewCustomerPhone: boolean = false;
  79. role: string = '';
  80. // 模态框状态
  81. showFilesModal: boolean = false;
  82. showMembersModal: boolean = false;
  83. showIssuesModal: boolean = false;
  84. // 新增:客户详情侧栏面板状态
  85. showContactPanel: boolean = false;
  86. // 问卷状态
  87. surveyStatus: {
  88. filled: boolean;
  89. text: string;
  90. icon: string;
  91. surveyLog?: FmodeObject;
  92. contact?: FmodeObject;
  93. } = {
  94. filled: false,
  95. text: '发送问卷',
  96. icon: 'document-text-outline'
  97. };
  98. constructor(
  99. private router: Router,
  100. private route: ActivatedRoute,
  101. private profileService: ProfileService,
  102. private issueService: ProjectIssueService
  103. ) {}
  104. async ngOnInit() {
  105. // 获取路由参数
  106. this.cid = this.route.snapshot.paramMap.get('cid') || '';
  107. this.projectId = this.route.snapshot.paramMap.get('projectId') || '';
  108. this.groupId = this.route.snapshot.queryParamMap.get('groupId') || '';
  109. this.profileId = this.route.snapshot.queryParamMap.get('profileId') || '';
  110. this.chatId = this.route.snapshot.queryParamMap.get('chatId') || '';
  111. // 监听路由变化
  112. this.route.firstChild?.url.subscribe((segments) => {
  113. if (segments.length > 0) {
  114. this.currentStage = segments[0].path;
  115. }
  116. });
  117. // 初始化企微授权(不阻塞页面加载)
  118. await this.initWxworkAuth();
  119. await this.loadData();
  120. }
  121. /**
  122. * 初始化企微授权(不阻塞页面)
  123. */
  124. async initWxworkAuth() {
  125. try {
  126. let cid = this.cid || localStorage.getItem("company") || "";
  127. // 如果没有cid,记录警告但不抛出错误
  128. if (!cid) {
  129. console.warn('⚠️ 未找到company ID (cid),企微功能将不可用');
  130. return;
  131. }
  132. this.wxAuth = new WxworkAuth({ cid: cid });
  133. this.wxwork = new WxworkSDK({ cid: cid, appId: 'crm' });
  134. this.wecorp = new WxworkCorp(cid);
  135. console.log('✅ 企微SDK初始化成功,cid:', cid);
  136. } catch (error) {
  137. console.error('❌ 企微SDK初始化失败:', error);
  138. // 不阻塞页面加载
  139. }
  140. }
  141. /**
  142. * 加载数据
  143. */
  144. async loadData() {
  145. try {
  146. this.loading = true;
  147. // 2. 获取当前用户(优先从全局服务获取)
  148. if (!this.currentUser?.id && this.wxAuth) {
  149. try {
  150. this.currentUser = await this.wxAuth.currentProfile();
  151. } catch (error) {
  152. console.warn('⚠️ 获取当前用户Profile失败:', error);
  153. }
  154. }
  155. // 设置权限
  156. this.role = this.currentUser?.get('roleName') || '';
  157. this.canEdit = ['客服', '组员', '组长', '管理员'].includes(this.role);
  158. this.canViewCustomerPhone = ['客服', '组长', '管理员'].includes(this.role);
  159. const companyId = this.currentUser?.get('company')?.id || localStorage?.getItem("company");
  160. // 3. 加载项目
  161. if (!this.project) {
  162. if (this.projectId) {
  163. // 通过 projectId 加载(从后台进入)
  164. const query = new Parse.Query('Project');
  165. query.include('contact', 'assignee','department','department.leader');
  166. this.project = await query.get(this.projectId);
  167. } else if (this.chatId) {
  168. // 通过 chat_id 查找项目(从企微群聊进入)
  169. if (companyId) {
  170. // 先查找 GroupChat
  171. const gcQuery = new Parse.Query('GroupChat');
  172. gcQuery.equalTo('chat_id', this.chatId);
  173. gcQuery.equalTo('company', companyId);
  174. let groupChat = await gcQuery.first();
  175. if (groupChat) {
  176. this.groupChat = groupChat;
  177. const projectPointer = groupChat.get('project');
  178. if (projectPointer) {
  179. const pQuery = new Parse.Query('Project');
  180. pQuery.include('contact', 'assignee','department','department.leader');
  181. this.project = await pQuery.get(projectPointer.id);
  182. }
  183. }
  184. if (!this.project) {
  185. throw new Error('该群聊尚未关联项目,请先在后台创建项目');
  186. }
  187. }
  188. }
  189. }
  190. if(!this.groupChat?.id){
  191. const gcQuery2 = new Parse.Query('GroupChat');
  192. gcQuery2.equalTo('project', this.projectId);
  193. gcQuery2.equalTo('company', companyId);
  194. this.groupChat = await gcQuery2.first();
  195. }
  196. this.wxwork?.syncGroupChat(this.groupChat?.toJSON())
  197. if (!this.project) {
  198. throw new Error('无法加载项目信息');
  199. }
  200. this.contact = this.project.get('contact');
  201. this.assignee = this.project.get('assignee');
  202. // 加载问卷状态
  203. await this.loadSurveyStatus();
  204. // 更新问题计数
  205. try {
  206. if (this.project?.id) {
  207. this.issueService.seed(this.project.id!);
  208. const counts = this.issueService.getCounts(this.project.id!);
  209. this.issueCount = counts.total;
  210. }
  211. } catch (e) {
  212. console.warn('统计问题数量失败:', e);
  213. }
  214. // 4. 加载群聊(如果没有传入且有groupId)
  215. if (!this.groupChat && this.groupId) {
  216. try {
  217. const gcQuery = new Parse.Query('GroupChat');
  218. this.groupChat = await gcQuery.get(this.groupId);
  219. } catch (err) {
  220. console.warn('加载群聊失败:', err);
  221. }
  222. }
  223. // 5. 根据项目当前阶段设置默认路由
  224. const projectStage = this.project.get('currentStage');
  225. const stageMap: any = {
  226. '订单分配': 'order',
  227. '确认需求': 'requirements',
  228. '方案确认': 'requirements',
  229. '方案深化': 'requirements',
  230. '交付执行': 'delivery',
  231. '建模': 'delivery',
  232. '软装': 'delivery',
  233. '渲染': 'delivery',
  234. '后期': 'delivery',
  235. '尾款结算': 'aftercare',
  236. '客户评价': 'aftercare',
  237. '投诉处理': 'aftercare'
  238. };
  239. const targetStage = stageMap[projectStage] || 'order';
  240. // 如果当前没有子路由,跳转到对应阶段
  241. if (!this.route.firstChild) {
  242. this.router.navigate([targetStage], { relativeTo: this.route, replaceUrl: true });
  243. }
  244. } catch (err: any) {
  245. console.error('加载失败:', err);
  246. this.error = err.message || '加载失败';
  247. } finally {
  248. this.loading = false;
  249. }
  250. }
  251. /**
  252. * 切换阶段
  253. */
  254. switchStage(stageId: string) {
  255. this.currentStage = stageId;
  256. this.router.navigate([stageId], { relativeTo: this.route });
  257. }
  258. /**
  259. * 获取阶段状态
  260. */
  261. getStageStatus(stageId: string): 'completed' | 'active' | 'pending' {
  262. const projectStage = this.project?.get('currentStage') || '';
  263. const stageOrder = ['订单分配', '确认需求', '建模', '软装', '渲染', '后期', '尾款结算', '客户评价'];
  264. const currentIndex = stageOrder.indexOf(projectStage);
  265. const stageIndexMap: any = {
  266. 'order': 0,
  267. 'requirements': 1,
  268. 'delivery': 3,
  269. 'aftercare': 6
  270. };
  271. const targetIndex = stageIndexMap[stageId];
  272. if (currentIndex > targetIndex) {
  273. return 'completed';
  274. } else if (this.currentStage === stageId) {
  275. return 'active';
  276. } else {
  277. return 'pending';
  278. }
  279. }
  280. /**
  281. * 返回
  282. */
  283. goBack() {
  284. let ua = navigator.userAgent.toLowerCase();
  285. let isWeixin = ua.indexOf("micromessenger") != -1;
  286. if(isWeixin){
  287. this.router.navigate(['/wxwork', this.cid, 'project-loader']);
  288. }else{
  289. history.back();
  290. }
  291. }
  292. /**
  293. * 更新项目阶段
  294. */
  295. async updateProjectStage(stage: string) {
  296. if (!this.project || !this.canEdit) return;
  297. try {
  298. this.project.set('currentStage', stage);
  299. await this.project.save();
  300. // 添加阶段历史
  301. const data = this.project.get('data') || {};
  302. const stageHistory = data.stageHistory || [];
  303. stageHistory.push({
  304. stage,
  305. startTime: new Date(),
  306. status: 'current',
  307. operator: {
  308. id: this.currentUser!.id,
  309. name: this.currentUser!.get('name'),
  310. role: this.role
  311. }
  312. });
  313. this.project.set('data', { ...data, stageHistory });
  314. await this.project.save();
  315. } catch (err) {
  316. console.error('更新阶段失败:', err);
  317. alert('更新失败');
  318. }
  319. }
  320. /**
  321. * 发送企微消息
  322. */
  323. async sendWxMessage(message: string) {
  324. if (!this.groupChat || !this.wecorp) return;
  325. try {
  326. const chatId = this.groupChat.get('chat_id');
  327. await this.wecorp.appchat.sendText(chatId, message);
  328. } catch (err) {
  329. console.error('发送消息失败:', err);
  330. }
  331. }
  332. /**
  333. * 选择客户(从群聊成员中选择外部联系人)
  334. */
  335. async selectCustomer() {
  336. console.log(this.canEdit, this.groupChat)
  337. if (!this.groupChat) return;
  338. try {
  339. const memberList = this.groupChat.get('member_list') || [];
  340. const externalMembers = memberList.filter((m: any) => m.type === 2);
  341. if (externalMembers.length === 0) {
  342. alert('当前群聊中没有外部联系人');
  343. return;
  344. }
  345. console.log(externalMembers)
  346. // 简单实现:选择第一个外部联系人
  347. // TODO: 实现选择器UI
  348. const selectedMember = externalMembers[0];
  349. await this.setCustomerFromMember(selectedMember);
  350. } catch (err) {
  351. console.error('选择客户失败:', err);
  352. alert('选择客户失败');
  353. }
  354. }
  355. /**
  356. * 从群成员设置客户
  357. */
  358. async setCustomerFromMember(member: any) {
  359. if (!this.wecorp) return;
  360. try {
  361. const companyId = this.currentUser?.get('company')?.id || localStorage.getItem("company");
  362. if (!companyId) throw new Error('无法获取企业信息');
  363. // 1. 查询是否已存在 ContactInfo
  364. const query = new Parse.Query('ContactInfo');
  365. query.equalTo('external_userid', member.userid);
  366. query.equalTo('company', companyId);
  367. let contactInfo = await query.first();
  368. // 2. 如果不存在,通过企微API获取并创建
  369. if (!contactInfo) {
  370. contactInfo = new Parse.Object("ContactInfo");
  371. }
  372. const externalContactData = await this.wecorp.externalContact.get(member.userid);
  373. console.log("externalContactData",externalContactData)
  374. const ContactInfo = Parse.Object.extend('ContactInfo');
  375. contactInfo.set('name', externalContactData.name);
  376. contactInfo.set('external_userid', member.userid);
  377. const company = new Parse.Object('Company');
  378. company.id = companyId;
  379. const companyPointer = company.toPointer();
  380. contactInfo.set('company', companyPointer);
  381. contactInfo.set('data', externalContactData);
  382. await contactInfo.save();
  383. // 3. 设置为项目客户
  384. if (this.project) {
  385. this.project.set('contact', contactInfo.toPointer());
  386. await this.project.save();
  387. this.contact = contactInfo;
  388. alert('客户设置成功');
  389. }
  390. } catch (err) {
  391. console.error('设置客户失败:', err);
  392. throw err;
  393. }
  394. }
  395. /**
  396. * 显示文件模态框
  397. */
  398. showFiles() {
  399. this.showFilesModal = true;
  400. }
  401. /**
  402. * 显示成员模态框
  403. */
  404. showMembers() {
  405. this.showMembersModal = true;
  406. }
  407. /** 显示问题模态框 */
  408. showIssues() {
  409. this.showIssuesModal = true;
  410. }
  411. /**
  412. * 关闭文件模态框
  413. */
  414. closeFilesModal() {
  415. this.showFilesModal = false;
  416. }
  417. /**
  418. * 关闭成员模态框
  419. */
  420. closeMembersModal() {
  421. this.showMembersModal = false;
  422. }
  423. /** 显示客户详情面板 */
  424. openContactPanel() {
  425. if (this.contact) {
  426. this.showContactPanel = true;
  427. }
  428. }
  429. /** 关闭客户详情面板 */
  430. closeContactPanel() {
  431. this.showContactPanel = false;
  432. }
  433. /** 关闭问题模态框 */
  434. closeIssuesModal() {
  435. this.showIssuesModal = false;
  436. if (this.project?.id) {
  437. const counts = this.issueService.getCounts(this.project.id!);
  438. this.issueCount = counts.total;
  439. }
  440. }
  441. /** 客户选择事件回调(接收子组件输出) */
  442. onContactSelected(evt: { contact: FmodeObject; isNewCustomer: boolean; action: 'selected' | 'created' | 'updated' }) {
  443. this.contact = evt.contact;
  444. // 重新加载问卷状态
  445. this.loadSurveyStatus();
  446. }
  447. /**
  448. * 加载问卷状态
  449. */
  450. async loadSurveyStatus() {
  451. if (!this.project?.id) return;
  452. try {
  453. const query = new Parse.Query('SurveyLog');
  454. query.equalTo('project', this.project.toPointer());
  455. query.equalTo('type', 'survey-project');
  456. query.equalTo('isCompleted', true);
  457. query.include("contact")
  458. const surveyLog = await query.first();
  459. if (surveyLog) {
  460. this.surveyStatus = {
  461. filled: true,
  462. text: '查看问卷',
  463. icon: 'checkmark-circle',
  464. surveyLog,
  465. contact:surveyLog?.get("contact")
  466. };
  467. console.log('✅ 问卷已填写');
  468. } else {
  469. this.surveyStatus = {
  470. filled: false,
  471. text: '发送问卷',
  472. icon: 'document-text-outline'
  473. };
  474. console.log('✅ 问卷未填写');
  475. }
  476. } catch (err) {
  477. console.error('❌ 查询问卷状态失败:', err);
  478. }
  479. }
  480. /**
  481. * 发送问卷
  482. */
  483. async sendSurvey() {
  484. if (!this.groupChat || !this.wxwork) {
  485. alert('无法发送问卷:未找到群聊或企微SDK未初始化');
  486. return;
  487. }
  488. try {
  489. const chatId = this.groupChat.get('chat_id');
  490. const surveyUrl = `${document.baseURI}/wxwork/${this.cid}/survey/project/${this.project?.id}`;
  491. await this.wxwork.ww.openExistedChatWithMsg({
  492. chatId: chatId,
  493. msg: {
  494. msgtype: 'link',
  495. link: {
  496. title: '《家装效果图服务需求调查表》',
  497. desc: '为让本次服务更贴合您的需求,请花3-5分钟填写简短问卷,感谢支持!',
  498. url: surveyUrl,
  499. imgUrl: `${document.baseURI}/assets/logo.jpg`
  500. }
  501. }
  502. });
  503. alert('问卷已发送到群聊!');
  504. } catch (err) {
  505. console.error('❌ 发送问卷失败:', err);
  506. alert('发送失败,请重试');
  507. }
  508. }
  509. /**
  510. * 查看问卷结果
  511. */
  512. async viewSurvey() {
  513. if (!this.surveyStatus.surveyLog) return;
  514. // 跳转到问卷页面查看结果
  515. this.router.navigate(['/wxwork', this.cid, 'survey', 'project', this.project?.id]);
  516. }
  517. /**
  518. * 处理问卷点击
  519. */
  520. async handleSurveyClick(event: Event) {
  521. event.stopPropagation();
  522. if (this.surveyStatus.filled) {
  523. // 已填写,查看结果
  524. await this.viewSurvey();
  525. } else {
  526. // 未填写,发送问卷
  527. await this.sendSurvey();
  528. }
  529. }
  530. }
  531. // duplicate inline CustomerSelectorComponent removed (we keep single declaration above)