project-detail.component.ts 30 KB

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