project-detail.component.ts 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053
  1. import { Component, OnInit, Input, Output, EventEmitter, OnDestroy } 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 { FormsModule } from '@angular/forms';
  14. import { CustomerSelectorComponent } from '../../components/contact-selector/contact-selector.component';
  15. import { OrderApprovalPanelComponent } from '../../../../app/shared/components/order-approval-panel/order-approval-panel.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. CustomerSelectorComponent,
  40. OrderApprovalPanelComponent
  41. ],
  42. templateUrl: './project-detail.component.html',
  43. styleUrls: ['./project-detail.component.scss']
  44. })
  45. export class ProjectDetailComponent implements OnInit, OnDestroy {
  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. // 折叠:项目基本信息
  99. showProjectInfoCollapsed: boolean = true;
  100. // 事件监听器引用
  101. private stageCompletedListener: any = null;
  102. constructor(
  103. private router: Router,
  104. private route: ActivatedRoute,
  105. private profileService: ProfileService,
  106. private issueService: ProjectIssueService
  107. ) {}
  108. async ngOnInit() {
  109. // 获取路由参数
  110. this.cid = this.route.snapshot.paramMap.get('cid') || '';
  111. // 兼容:cid 在父级路由上
  112. if (!this.cid) {
  113. this.cid = this.route.parent?.snapshot.paramMap.get('cid') || '';
  114. }
  115. // 降级:从 localStorage 读取
  116. if (!this.cid) {
  117. this.cid = localStorage.getItem('company') || '';
  118. }
  119. this.projectId = this.route.snapshot.paramMap.get('projectId') || '';
  120. this.groupId = this.route.snapshot.queryParamMap.get('groupId') || '';
  121. this.profileId = this.route.snapshot.queryParamMap.get('profileId') || '';
  122. this.chatId = this.route.snapshot.queryParamMap.get('chatId') || '';
  123. console.log('📌 路由参数:', {
  124. cid: this.cid,
  125. projectId: this.projectId
  126. });
  127. // 监听路由变化
  128. this.route.firstChild?.url.subscribe((segments) => {
  129. if (segments.length > 0) {
  130. this.currentStage = segments[0].path;
  131. console.log('🔄 当前阶段已更新:', this.currentStage);
  132. }
  133. });
  134. // 初始化企微授权(不阻塞页面加载)
  135. await this.initWxworkAuth();
  136. await this.loadData();
  137. // 初始化工作流阶段(若缺失则根据已完成记录推断)
  138. this.ensureWorkflowStage();
  139. // 监听各阶段完成事件,自动推进到下一环节
  140. this.stageCompletedListener = async (e: any) => {
  141. console.log('🎯 [监听器] 事件触发', e?.detail);
  142. const stageId = e?.detail?.stage as string;
  143. if (!stageId) {
  144. console.error('❌ [监听器] 事件缺少 stage 参数');
  145. return;
  146. }
  147. console.log('✅ [监听器] 接收到阶段完成事件:', stageId);
  148. await this.advanceToNextStage(stageId);
  149. };
  150. console.log('📡 [初始化] 注册事件监听器: stage:completed');
  151. document.addEventListener('stage:completed', this.stageCompletedListener);
  152. console.log('✅ [初始化] 事件监听器注册成功');
  153. }
  154. /**
  155. * 组件销毁时清理事件监听器
  156. */
  157. ngOnDestroy() {
  158. if (this.stageCompletedListener) {
  159. document.removeEventListener('stage:completed', this.stageCompletedListener);
  160. console.log('🧹 已清理阶段完成事件监听器');
  161. }
  162. }
  163. /**
  164. * 初始化企微授权(不阻塞页面)
  165. */
  166. async initWxworkAuth() {
  167. try {
  168. let cid = this.cid || localStorage.getItem("company") || "";
  169. // 如果没有cid,记录警告但不抛出错误
  170. if (!cid) {
  171. console.warn('⚠️ 未找到company ID (cid),企微功能将不可用');
  172. return;
  173. }
  174. this.wxAuth = new WxworkAuth({ cid: cid });
  175. this.wxwork = new WxworkSDK({ cid: cid, appId: 'crm' });
  176. this.wecorp = new WxworkCorp(cid);
  177. console.log('✅ 企微SDK初始化成功,cid:', cid);
  178. } catch (error) {
  179. console.error('❌ 企微SDK初始化失败:', error);
  180. // 不阻塞页面加载
  181. }
  182. }
  183. /**
  184. * 折叠/展开 项目基本信息
  185. */
  186. toggleProjectInfo(): void {
  187. this.showProjectInfoCollapsed = !this.showProjectInfoCollapsed;
  188. }
  189. /**
  190. * 跳转到指定阶段(程序化跳转,用于阶段推进)
  191. */
  192. goToStage(stageId: 'order'|'requirements'|'delivery'|'aftercare') {
  193. console.log('🚀 [goToStage] 开始导航', {
  194. 目标阶段: stageId,
  195. 当前路由: this.router.url,
  196. cid: this.cid,
  197. projectId: this.projectId
  198. });
  199. // 更新本地状态
  200. this.currentStage = stageId;
  201. // 优先使用绝对路径导航(更可靠)
  202. if (this.cid && this.projectId) {
  203. console.log('🚀 [goToStage] 使用绝对路径导航');
  204. this.router.navigate(['/wxwork', this.cid, 'project', this.projectId, stageId])
  205. .then(success => {
  206. if (success) {
  207. console.log('✅ [goToStage] 导航成功:', stageId);
  208. }
  209. })
  210. .catch(err => {
  211. console.error('❌ [goToStage] 导航出错:', err);
  212. });
  213. } else {
  214. console.warn('⚠️ [goToStage] 缺少参数,使用相对路径', {
  215. cid: this.cid,
  216. projectId: this.projectId
  217. });
  218. // 降级:使用相对路径(直接切换子路由)
  219. this.router.navigate([stageId], { relativeTo: this.route })
  220. .then(success => {
  221. if (success) {
  222. console.log('✅ [goToStage] 相对路径导航成功');
  223. } else {
  224. console.error('❌ [goToStage] 相对路径导航失败');
  225. }
  226. })
  227. .catch(err => {
  228. console.error('❌ [goToStage] 相对路径导航出错:', err);
  229. });
  230. }
  231. }
  232. /**
  233. * 从给定阶段推进到下一个阶段
  234. */
  235. async advanceToNextStage(current: string) {
  236. console.log('🚀 [推进阶段] 开始', { current });
  237. const order = ['order','requirements','delivery','aftercare'];
  238. const idx = order.indexOf(current);
  239. console.log('🚀 [推进阶段] 阶段索引:', { current, idx });
  240. if (idx === -1) {
  241. console.error('❌ [推进阶段] 未找到当前阶段:', current);
  242. return;
  243. }
  244. if (idx >= order.length - 1) {
  245. console.log('✅ [推进阶段] 已到达最后阶段');
  246. window?.fmode?.alert('所有阶段已完成!');
  247. return;
  248. }
  249. const next = order[idx + 1];
  250. console.log('➡️ [推进阶段] 下一阶段:', next);
  251. // 持久化:标记当前阶段完成并设置下一阶段为当前
  252. console.log('💾 [推进阶段] 开始持久化');
  253. await this.persistStageProgress(current, next);
  254. console.log('✅ [推进阶段] 持久化完成');
  255. // 导航到下一阶段
  256. console.log('🚀 [推进阶段] 开始导航到:', next);
  257. this.goToStage(next as any);
  258. const nextStageName = this.stages.find(s => s.id === next)?.name || next;
  259. window?.fmode?.alert(`已自动跳转到下一阶段: ${nextStageName}`);
  260. console.log('✅ [推进阶段] 完成');
  261. }
  262. /**
  263. * 确保存在工作流当前阶段。如缺失则根据完成记录计算
  264. */
  265. ensureWorkflowStage() {
  266. if (!this.project) return;
  267. const order = ['order','requirements','delivery','aftercare'];
  268. const data = this.project.get('data') || {};
  269. const statuses = data.stageStatuses || {};
  270. let current = this.project.get('currentStage');
  271. if (!current) {
  272. // 找到第一个未完成的阶段
  273. current = order.find(s => statuses[s] !== 'completed') || 'aftercare';
  274. this.project.set('currentStage', current);
  275. }
  276. }
  277. /**
  278. * 持久化阶段推进(标记当前完成、设置下一阶段)
  279. */
  280. private async persistStageProgress(current: string, next: string) {
  281. if (!this.project) {
  282. console.warn('⚠️ 项目对象不存在,无法持久化');
  283. return;
  284. }
  285. console.log('💾 开始持久化阶段:', { current, next });
  286. const data = this.project.get('data') || {};
  287. data.stageStatuses = data.stageStatuses || {};
  288. data.stageStatuses[current] = 'completed';
  289. this.project.set('data', data);
  290. // 🔥 关键修复:将英文阶段ID映射为中文阶段名称
  291. const stageNameMap: Record<string, string> = {
  292. 'order': '订单分配',
  293. 'requirements': '确认需求',
  294. 'delivery': '白模', // 交付执行的第一个子阶段
  295. 'aftercare': '尾款结算'
  296. };
  297. const chineseStageName = stageNameMap[next] || next;
  298. this.project.set('currentStage', chineseStageName);
  299. console.log('💾 设置阶段状态:', {
  300. currentStage: chineseStageName,
  301. stageStatuses: data.stageStatuses
  302. });
  303. try {
  304. await this.project.save();
  305. console.log('✅ 阶段状态持久化成功');
  306. } catch (e) {
  307. console.warn('⚠️ 阶段状态持久化失败(忽略以保证流程可继续):', e);
  308. }
  309. }
  310. /**
  311. * 加载数据
  312. */
  313. async loadData() {
  314. try {
  315. this.loading = true;
  316. // 2. 获取当前用户(优先从全局服务获取)
  317. if (!this.currentUser?.id && this.wxAuth) {
  318. try {
  319. this.currentUser = await this.wxAuth.currentProfile();
  320. } catch (error) {
  321. console.warn('⚠️ 获取当前用户Profile失败:', error);
  322. }
  323. }
  324. // 设置权限
  325. this.role = this.currentUser?.get('roleName') || '';
  326. this.canEdit = ['客服', '组员', '组长', '管理员', '设计师', '客服主管'].includes(this.role);
  327. this.canViewCustomerPhone = ['客服', '组长', '管理员'].includes(this.role);
  328. const companyId = this.currentUser?.get('company')?.id || localStorage?.getItem("company");
  329. // 3. 加载项目
  330. if (!this.project) {
  331. if (this.projectId) {
  332. // 通过 projectId 加载(从后台进入)
  333. const query = new Parse.Query('Project');
  334. query.include('contact', 'assignee','department','department.leader');
  335. this.project = await query.get(this.projectId);
  336. } else if (this.chatId) {
  337. // 通过 chat_id 查找项目(从企微群聊进入)
  338. if (companyId) {
  339. // 先查找 GroupChat
  340. const gcQuery = new Parse.Query('GroupChat');
  341. gcQuery.equalTo('chat_id', this.chatId);
  342. gcQuery.equalTo('company', companyId);
  343. let groupChat = await gcQuery.first();
  344. if (groupChat) {
  345. this.groupChat = groupChat;
  346. const projectPointer = groupChat.get('project');
  347. if (projectPointer) {
  348. const pQuery = new Parse.Query('Project');
  349. pQuery.include('contact', 'assignee','department','department.leader');
  350. this.project = await pQuery.get(projectPointer.id);
  351. }
  352. }
  353. if (!this.project) {
  354. throw new Error('该群聊尚未关联项目,请先在后台创建项目');
  355. }
  356. }
  357. }
  358. }
  359. if(!this.groupChat?.id){
  360. const gcQuery2 = new Parse.Query('GroupChat');
  361. gcQuery2.equalTo('project', this.projectId);
  362. gcQuery2.equalTo('company', companyId);
  363. this.groupChat = await gcQuery2.first();
  364. }
  365. this.wxwork?.syncGroupChat(this.groupChat?.toJSON())
  366. if (!this.project) {
  367. throw new Error('无法加载项目信息');
  368. }
  369. this.contact = this.project.get('contact');
  370. this.assignee = this.project.get('assignee');
  371. // 加载问卷状态
  372. await this.loadSurveyStatus();
  373. // 更新问题计数
  374. try {
  375. if (this.project?.id) {
  376. this.issueService.seed(this.project.id!);
  377. const counts = this.issueService.getCounts(this.project.id!);
  378. this.issueCount = counts.total;
  379. }
  380. } catch (e) {
  381. console.warn('统计问题数量失败:', e);
  382. }
  383. // 4. 加载群聊(如果没有传入且有groupId)
  384. if (!this.groupChat && this.groupId) {
  385. try {
  386. const gcQuery = new Parse.Query('GroupChat');
  387. this.groupChat = await gcQuery.get(this.groupId);
  388. } catch (err) {
  389. console.warn('加载群聊失败:', err);
  390. }
  391. }
  392. // 5. 【已禁用】验证项目阶段 - 只显示警告,不自动回退
  393. // ⚠️ 原因:自动回退会覆盖组长审批通过后的阶段推进,导致反复回退
  394. let projectStage = this.project.get('currentStage');
  395. const data = this.project.get('data') || {};
  396. console.log('🔍 [项目详情] 当前项目阶段:', projectStage);
  397. console.log('🔍 [项目详情] 项目数据:', {
  398. title: this.project.get('title'),
  399. projectType: this.project.get('projectType'),
  400. demoday: this.project.get('demoday'),
  401. quotationTotal: data.quotation?.total,
  402. approvalStatus: data.approvalStatus,
  403. approvalHistory: data.approvalHistory,
  404. requirementsAnalysis: !!data.requirementsAnalysis,
  405. spaceRequirements: !!data.spaceRequirements
  406. });
  407. // ⚠️ 【已禁用】自动回退检查 - 改为只记录警告
  408. let needRollback = false;
  409. let correctStage = projectStage;
  410. let rollbackReason = '';
  411. // 【已禁用】检查1:订单分配阶段完成情况 - 只记录警告,不回退
  412. if (projectStage && ['确认需求', '方案确认', '方案深化', '交付执行', '白模', '软装', '渲染', '后期', '建模', '尾款结算', '客户评价', '投诉处理', '售后归档'].includes(projectStage)) {
  413. const title = this.project.get('title');
  414. const projectType = this.project.get('projectType');
  415. const demoday = this.project.get('demoday');
  416. const quotationTotal = data.quotation?.total || 0;
  417. const approvalStatus = data.approvalStatus;
  418. // 检查订单分配阶段是否完成
  419. const orderStageIncomplete = !title || !projectType || !demoday || quotationTotal <= 0;
  420. // ✅ 智能判断审批状态:检查审批历史
  421. const hasApprovedHistory = data.approvalHistory?.some((h: any) =>
  422. h.stage === '订单分配' && h.status === 'approved'
  423. );
  424. const notApproved = approvalStatus !== 'approved' && !hasApprovedHistory;
  425. // ⚠️ 【已禁用回退】只记录警告,不执行回退
  426. if (orderStageIncomplete || notApproved) {
  427. const reasons = [];
  428. if (!title) reasons.push('缺少项目名称');
  429. if (!projectType) reasons.push('缺少项目类型');
  430. if (!demoday) reasons.push('缺少小图日期');
  431. if (quotationTotal <= 0) reasons.push('缺少报价数据');
  432. if (notApproved) reasons.push('待组长审批');
  433. console.warn('⚠️ [数据警告] 订单分配阶段数据不完整,但不执行回退:', {
  434. currentStage: projectStage,
  435. 缺失项: reasons,
  436. approvalStatus,
  437. hasApprovedHistory
  438. });
  439. } else {
  440. console.log('✅ [数据验证] 订单分配阶段数据完整');
  441. }
  442. }
  443. // 【已禁用】检查2:确认需求阶段完成情况 - 只记录警告,不回退
  444. if (projectStage && ['交付执行', '白模', '软装', '渲染', '后期', '建模', '尾款结算', '客户评价', '投诉处理', '售后归档'].includes(projectStage)) {
  445. const hasRequirements = data.requirementsAnalysis || data.spaceRequirements;
  446. if (!hasRequirements) {
  447. console.warn('⚠️ [数据警告] 确认需求阶段数据不完整,但不执行回退:', {
  448. currentStage: projectStage,
  449. hasRequirementsAnalysis: !!data.requirementsAnalysis,
  450. hasSpaceRequirements: !!data.spaceRequirements
  451. });
  452. } else {
  453. console.log('✅ [数据验证] 确认需求阶段数据完整');
  454. }
  455. }
  456. // 【已禁用】自动回退功能 - 避免覆盖审批通过后的阶段推进
  457. console.log('ℹ️ [阶段验证] 自动回退功能已禁用,当前阶段:', projectStage);
  458. // 映射到路由ID
  459. const stageMap: any = {
  460. '订单分配': 'order',
  461. '确认需求': 'requirements',
  462. '方案确认': 'requirements',
  463. '方案深化': 'requirements',
  464. '交付执行': 'delivery',
  465. '白模': 'delivery', // 🔥 交付执行子阶段
  466. '软装': 'delivery',
  467. '渲染': 'delivery',
  468. '后期': 'delivery',
  469. '建模': 'delivery',
  470. '尾款结算': 'aftercare',
  471. '客户评价': 'aftercare',
  472. '投诉处理': 'aftercare'
  473. };
  474. const targetStage = stageMap[projectStage] || 'order';
  475. console.log('🎯 [项目详情] 目标路由阶段:', targetStage);
  476. // 🔥 修复:始终导航到正确的阶段,即使已有子路由
  477. const currentChildRoute = this.route.firstChild?.snapshot.url[0]?.path;
  478. console.log('📍 [项目详情] 当前子路由:', currentChildRoute);
  479. if (!currentChildRoute || currentChildRoute !== targetStage) {
  480. console.log('🚀 [项目详情] 导航到正确阶段:', targetStage);
  481. this.router.navigate([targetStage], { relativeTo: this.route, replaceUrl: true });
  482. } else {
  483. console.log('✅ [项目详情] 已在正确阶段,无需导航');
  484. }
  485. } catch (err: any) {
  486. console.error('加载失败:', err);
  487. this.error = err.message || '加载失败';
  488. } finally {
  489. this.loading = false;
  490. }
  491. }
  492. /**
  493. * 切换阶段(查看模式)
  494. * 允许查看所有阶段,但不改变项目的实际阶段(currentStage)
  495. * 只有通过各阶段的提交按钮(如"确认订单")才能推进项目阶段
  496. */
  497. switchStage(stageId: string) {
  498. console.log('🔄 用户点击切换阶段(查看模式):', stageId, {
  499. currentRoute: this.router.url,
  500. currentViewStage: this.currentStage,
  501. projectActualStage: this.project?.get('currentStage')
  502. });
  503. // ✅ 允许查看所有阶段(包括未开始的)
  504. const status = this.getStageStatus(stageId);
  505. console.log(`✅ 切换到阶段视图: ${stageId} (项目实际状态: ${status})`);
  506. // ⚠️ 如果查看未开始的阶段,给予友好提示
  507. if (status === 'pending') {
  508. const stageName = this.stages.find(s => s.id === stageId)?.name || stageId;
  509. console.warn(`⚠️ 正在查看未开始的阶段: ${stageName}`);
  510. console.warn(`💡 提示: 这是查看模式,项目实际阶段不会改变`);
  511. console.warn(`💡 需要完成当前阶段的必填项才能推进到 ${stageName}`);
  512. }
  513. // 更新本地视图状态(仅影响显示,不影响项目实际阶段)
  514. this.currentStage = stageId;
  515. // 导航到指定阶段(查看模式)
  516. this.router.navigate([stageId], { relativeTo: this.route })
  517. .then(success => {
  518. if (success) {
  519. console.log('✅ 导航成功(查看模式):', stageId);
  520. } else {
  521. console.warn('⚠️ 导航失败:', stageId);
  522. }
  523. })
  524. .catch(err => {
  525. console.error('❌ 导航出错:', err);
  526. });
  527. }
  528. /**
  529. * 获取阶段状态(参考设计师端 getSectionStatus 实现)
  530. * @returns 'completed' - 已完成(绿色)| 'active' - 当前进行中(红色)| 'pending' - 待开始(灰色)
  531. */
  532. getStageStatus(stageId: string): 'completed' | 'active' | 'pending' {
  533. // 颜色显示仅依据“工作流状态”,不受临时浏览路由影响
  534. const data = this.project?.get('data') || {};
  535. const statuses = data.stageStatuses || {};
  536. let workflowCurrent = this.project?.get('currentStage') || 'order';
  537. // 🔥 关键修复:将中文阶段名称映射为英文ID
  538. const stageNameToId: Record<string, string> = {
  539. '订单分配': 'order',
  540. '确认需求': 'requirements',
  541. // 设计师阶段(需求阶段)
  542. '方案深化': 'requirements',
  543. // 交付执行子阶段统一归为 delivery
  544. '交付执行': 'delivery',
  545. '交付': 'delivery',
  546. '白模': 'delivery',
  547. '建模': 'delivery',
  548. '软装': 'delivery',
  549. '渲染': 'delivery',
  550. '后期': 'delivery',
  551. // 售后归档
  552. '售后归档': 'aftercare',
  553. '尾款结算': 'aftercare',
  554. '已完成': 'aftercare'
  555. };
  556. // 如果是中文名称,转换为英文ID
  557. if (stageNameToId[workflowCurrent]) {
  558. workflowCurrent = stageNameToId[workflowCurrent];
  559. }
  560. // 如果没有当前阶段(新创建的项目),默认订单分配为active(红色)
  561. if (!workflowCurrent || workflowCurrent === 'order') {
  562. return stageId === 'order' ? 'active' : 'pending';
  563. }
  564. // 计算阶段索引
  565. const stageOrder = ['order', 'requirements', 'delivery', 'aftercare'];
  566. const currentIdx = stageOrder.indexOf(workflowCurrent);
  567. const idx = stageOrder.indexOf(stageId);
  568. if (idx === -1 || currentIdx === -1) return 'pending';
  569. // 已完成的阶段:当前阶段之前的所有阶段(绿色)
  570. if (idx < currentIdx) return 'completed';
  571. // 当前进行中的阶段:等于当前阶段(红色)
  572. if (idx === currentIdx) return 'active';
  573. // 未开始的阶段:当前阶段之后的所有阶段(灰色)
  574. return 'pending';
  575. }
  576. /**
  577. * 返回
  578. */
  579. goBack() {
  580. let ua = navigator.userAgent.toLowerCase();
  581. let isWeixin = ua.indexOf("micromessenger") != -1;
  582. if(isWeixin){
  583. this.router.navigate(['/wxwork', this.cid, 'project-loader']);
  584. }else{
  585. history.back();
  586. }
  587. }
  588. /**
  589. * 更新项目阶段
  590. */
  591. async updateProjectStage(stage: string) {
  592. if (!this.project || !this.canEdit) return;
  593. try {
  594. this.project.set('currentStage', stage);
  595. await this.project.save();
  596. // 添加阶段历史
  597. const data = this.project.get('data') || {};
  598. const stageHistory = data.stageHistory || [];
  599. stageHistory.push({
  600. stage,
  601. startTime: new Date(),
  602. status: 'current',
  603. operator: {
  604. id: this.currentUser!.id,
  605. name: this.currentUser!.get('name'),
  606. role: this.role
  607. }
  608. });
  609. this.project.set('data', { ...data, stageHistory });
  610. await this.project.save();
  611. } catch (err) {
  612. console.error('更新阶段失败:', err);
  613. window?.fmode?.alert('更新失败');
  614. }
  615. }
  616. /**
  617. * 发送企微消息
  618. */
  619. async sendWxMessage(message: string) {
  620. if (!this.groupChat || !this.wecorp) return;
  621. try {
  622. const chatId = this.groupChat.get('chat_id');
  623. await this.wecorp.appchat.sendText(chatId, message);
  624. } catch (err) {
  625. console.error('发送消息失败:', err);
  626. }
  627. }
  628. /**
  629. * 选择客户(从群聊成员中选择外部联系人)
  630. */
  631. async selectCustomer() {
  632. console.log(this.canEdit, this.groupChat)
  633. if (!this.groupChat) return;
  634. try {
  635. const memberList = this.groupChat.get('member_list') || [];
  636. const externalMembers = memberList.filter((m: any) => m.type === 2);
  637. if (externalMembers.length === 0) {
  638. window?.fmode?.alert('当前群聊中没有外部联系人');
  639. return;
  640. }
  641. console.log(externalMembers)
  642. // 简单实现:选择第一个外部联系人
  643. // TODO: 实现选择器UI
  644. const selectedMember = externalMembers[0];
  645. await this.setCustomerFromMember(selectedMember);
  646. } catch (err) {
  647. console.error('选择客户失败:', err);
  648. window?.fmode?.alert('选择客户失败');
  649. }
  650. }
  651. /**
  652. * 从群成员设置客户
  653. */
  654. async setCustomerFromMember(member: any) {
  655. if (!this.wecorp) return;
  656. try {
  657. const companyId = this.currentUser?.get('company')?.id || localStorage.getItem("company");
  658. if (!companyId) throw new Error('无法获取企业信息');
  659. // 1. 查询是否已存在 ContactInfo
  660. const query = new Parse.Query('ContactInfo');
  661. query.equalTo('external_userid', member.userid);
  662. query.equalTo('company', companyId);
  663. let contactInfo = await query.first();
  664. // 2. 如果不存在,通过企微API获取并创建
  665. if (!contactInfo) {
  666. contactInfo = new Parse.Object("ContactInfo");
  667. }
  668. const externalContactData = await this.wecorp.externalContact.get(member.userid);
  669. console.log("externalContactData",externalContactData)
  670. const ContactInfo = Parse.Object.extend('ContactInfo');
  671. contactInfo.set('name', externalContactData.name);
  672. contactInfo.set('external_userid', member.userid);
  673. const company = new Parse.Object('Company');
  674. company.id = companyId;
  675. const companyPointer = company.toPointer();
  676. contactInfo.set('company', companyPointer);
  677. contactInfo.set('data', externalContactData);
  678. await contactInfo.save();
  679. // 3. 设置为项目客户
  680. if (this.project) {
  681. this.project.set('contact', contactInfo.toPointer());
  682. await this.project.save();
  683. this.contact = contactInfo;
  684. window?.fmode?.alert('客户设置成功');
  685. }
  686. } catch (err) {
  687. console.error('设置客户失败:', err);
  688. throw err;
  689. }
  690. }
  691. /**
  692. * 显示文件模态框
  693. */
  694. showFiles() {
  695. this.showFilesModal = true;
  696. }
  697. /**
  698. * 显示成员模态框
  699. */
  700. showMembers() {
  701. this.showMembersModal = true;
  702. }
  703. /** 显示问题模态框 */
  704. showIssues() {
  705. this.showIssuesModal = true;
  706. }
  707. /**
  708. * 关闭文件模态框
  709. */
  710. closeFilesModal() {
  711. this.showFilesModal = false;
  712. }
  713. /**
  714. * 关闭成员模态框
  715. */
  716. closeMembersModal() {
  717. this.showMembersModal = false;
  718. }
  719. /** 显示客户详情面板 */
  720. openContactPanel() {
  721. if (this.contact) {
  722. this.showContactPanel = true;
  723. }
  724. }
  725. /** 关闭客户详情面板 */
  726. closeContactPanel() {
  727. this.showContactPanel = false;
  728. }
  729. /** 关闭问题模态框 */
  730. closeIssuesModal() {
  731. this.showIssuesModal = false;
  732. if (this.project?.id) {
  733. const counts = this.issueService.getCounts(this.project.id!);
  734. this.issueCount = counts.total;
  735. }
  736. }
  737. /** 客户选择事件回调(接收子组件输出) */
  738. onContactSelected(evt: { contact: FmodeObject; isNewCustomer: boolean; action: 'selected' | 'created' | 'updated' }) {
  739. this.contact = evt.contact;
  740. // 重新加载问卷状态
  741. this.loadSurveyStatus();
  742. }
  743. /**
  744. * 加载问卷状态
  745. */
  746. async loadSurveyStatus() {
  747. if (!this.project?.id) return;
  748. try {
  749. const query = new Parse.Query('SurveyLog');
  750. query.equalTo('project', this.project.toPointer());
  751. query.equalTo('type', 'survey-project');
  752. query.equalTo('isCompleted', true);
  753. query.include("contact")
  754. const surveyLog = await query.first();
  755. if (surveyLog) {
  756. this.surveyStatus = {
  757. filled: true,
  758. text: '查看问卷',
  759. icon: 'checkmark-circle',
  760. surveyLog,
  761. contact:surveyLog?.get("contact")
  762. };
  763. console.log('✅ 问卷已填写');
  764. } else {
  765. this.surveyStatus = {
  766. filled: false,
  767. text: '发送问卷',
  768. icon: 'document-text-outline'
  769. };
  770. console.log('✅ 问卷未填写');
  771. }
  772. } catch (err) {
  773. console.error('❌ 查询问卷状态失败:', err);
  774. }
  775. }
  776. /**
  777. * 发送问卷
  778. */
  779. async sendSurvey() {
  780. if (!this.groupChat || !this.wxwork) {
  781. window?.fmode?.alert('无法发送问卷:未找到群聊或企微SDK未初始化');
  782. return;
  783. }
  784. try {
  785. const chatId = this.groupChat.get('chat_id');
  786. const surveyUrl = `${document.baseURI}/wxwork/${this.cid}/survey/project/${this.project?.id}`;
  787. await this.wxwork.ww.openExistedChatWithMsg({
  788. chatId: chatId,
  789. msg: {
  790. msgtype: 'link',
  791. link: {
  792. title: '《家装效果图服务需求调查表》',
  793. desc: '为让本次服务更贴合您的需求,请花3-5分钟填写简短问卷,感谢支持!',
  794. url: surveyUrl,
  795. imgUrl: `${document.baseURI}/assets/logo.jpg`
  796. }
  797. }
  798. });
  799. window?.fmode?.alert('问卷已发送到群聊!');
  800. } catch (err) {
  801. console.error('❌ 发送问卷失败:', err);
  802. window?.fmode?.alert('发送失败,请重试');
  803. }
  804. }
  805. /**
  806. * 查看问卷结果
  807. */
  808. async viewSurvey() {
  809. if (!this.surveyStatus.surveyLog) return;
  810. // 跳转到问卷页面查看结果
  811. this.router.navigate(['/wxwork', this.cid, 'survey', 'project', this.project?.id]);
  812. }
  813. /**
  814. * 处理问卷点击
  815. */
  816. async handleSurveyClick(event: Event) {
  817. event.stopPropagation();
  818. if (this.surveyStatus.filled) {
  819. // 已填写,查看结果
  820. await this.viewSurvey();
  821. } else {
  822. // 未填写,发送问卷
  823. await this.sendSurvey();
  824. }
  825. }
  826. /**
  827. * 是否显示审批面板
  828. * 条件:当前用户是组长 + 项目处于订单分配阶段 + 审批状态为待审批
  829. */
  830. get showApprovalPanel(): boolean {
  831. if (!this.project || !this.currentUser) {
  832. console.log('🔍 审批面板检查: 缺少项目或用户数据');
  833. return false;
  834. }
  835. const userRole = this.currentUser.get('roleName') || '';
  836. // ✅ 恢复正确的角色检查:只有组长才能看到审批面板
  837. const isTeamLeader = userRole === '设计组长' || userRole === 'team-leader' || userRole === '组长';
  838. const currentStage = this.project.get('currentStage') || '';
  839. const isOrderStage = currentStage === '订单分配' || currentStage === 'order';
  840. const data = this.project.get('data') || {};
  841. const approvalStatus = data.approvalStatus;
  842. const isPending = approvalStatus === 'pending';
  843. // console.log('🔍 审批面板检查:', {
  844. // userRole,
  845. // isTeamLeader,
  846. // currentStage,
  847. // isOrderStage,
  848. // approvalStatus,
  849. // isPending,
  850. // result: isTeamLeader && isOrderStage && isPending
  851. // });
  852. return isTeamLeader && isOrderStage && isPending;
  853. }
  854. /**
  855. * 处理审批完成事件
  856. */
  857. async onApprovalCompleted(event: any) {
  858. if (!this.project) return;
  859. try {
  860. const data = this.project.get('data') || {};
  861. const approvalHistory = data.approvalHistory || [];
  862. const latestRecord = approvalHistory[approvalHistory.length - 1];
  863. if (latestRecord) {
  864. latestRecord.status = event.action;
  865. latestRecord.approver = {
  866. id: this.currentUser?.id,
  867. name: this.currentUser?.get('name'),
  868. role: this.currentUser?.get('roleName')
  869. };
  870. latestRecord.approvalTime = new Date();
  871. latestRecord.comment = event.comment;
  872. latestRecord.reason = event.reason;
  873. }
  874. if (event.action === 'approved') {
  875. // 通过审批:推进到确认需求阶段
  876. data.approvalStatus = 'approved';
  877. this.project.set('currentStage', '确认需求');
  878. this.project.set('data', data);
  879. await this.project.save();
  880. alert('✅ 审批通过,项目已进入确认需求阶段');
  881. // 刷新页面数据
  882. await this.loadData();
  883. } else {
  884. // 驳回:保持在订单分配阶段,记录驳回原因
  885. data.approvalStatus = 'rejected';
  886. data.lastRejectionReason = event.reason || '未提供原因';
  887. this.project.set('data', data);
  888. await this.project.save();
  889. alert('✅ 已驳回订单,客服将收到通知');
  890. // 刷新页面数据
  891. await this.loadData();
  892. }
  893. } catch (err) {
  894. console.error('处理审批失败:', err);
  895. alert('审批操作失败,请重试');
  896. }
  897. }
  898. }
  899. // duplicate inline CustomerSelectorComponent removed (we keep single declaration above)