project-list.ts 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987
  1. import { Component, OnInit, OnDestroy, signal, computed, Inject } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { FormsModule } from '@angular/forms';
  4. import { Router, RouterModule, ActivatedRoute } from '@angular/router';
  5. import { MatDialog, MatDialogModule } from '@angular/material/dialog';
  6. import { ProjectService } from '../../../services/project.service';
  7. import { ConsultationOrderDialogComponent } from '../consultation-order/consultation-order-dialog.component';
  8. import { Project, ProjectStatus, ProjectStage } from '../../../models/project.model';
  9. import { FmodeParse, FmodeObject } from 'fmode-ng/parse';
  10. import { ProfileService } from '../../../services/profile.service';
  11. import { normalizeStage, getProjectStatusByStage } from '../../../utils/project-stage-mapper';
  12. const Parse = FmodeParse.with('nova');
  13. // 定义项目列表项接口,包含计算后的属性
  14. interface ProjectListItem extends Project {
  15. progress: number;
  16. daysUntilDeadline: number;
  17. isUrgent: boolean;
  18. tagDisplayText: string;
  19. }
  20. @Component({
  21. selector: 'app-project-list',
  22. standalone: true,
  23. imports: [CommonModule, FormsModule, RouterModule, MatDialogModule],
  24. templateUrl: './project-list.html',
  25. styleUrls: ['./project-list.scss', '../customer-service-styles.scss']
  26. })
  27. export class ProjectList implements OnInit, OnDestroy {
  28. // 项目列表数据
  29. projects = signal<ProjectListItem[]>([]);
  30. // 原始项目数据(用于筛选)
  31. allProjects = signal<Project[]>([]);
  32. // 视图模式:卡片 / 列表 / 监控大盘(默认卡片)
  33. viewMode = signal<'card' | 'list' | 'dashboard'>('card');
  34. // 看板列配置 - 按照订单分配、确认需求、交付执行、售后四个阶段
  35. columns = [
  36. { id: 'order', name: '订单分配' },
  37. { id: 'requirements', name: '确认需求' },
  38. { id: 'delivery', name: '交付执行' },
  39. { id: 'aftercare', name: '售后' }
  40. ] as const;
  41. // 基础项目集合(服务端返回 + 本地生成),用于二次处理
  42. private baseProjects: Project[] = [];
  43. // 消息监听器
  44. private messageListener?: (event: MessageEvent) => void;
  45. // 添加toggleSidebar方法
  46. toggleSidebar(): void {
  47. // 侧边栏切换逻辑
  48. console.log('Toggle sidebar');
  49. }
  50. // 筛选和排序状态
  51. searchTerm = signal('');
  52. statusFilter = signal<string>('all');
  53. stageFilter = signal<string>('all');
  54. sortBy = signal<string>('deadline');
  55. // 当前页码
  56. currentPage = signal(1);
  57. // 每页显示数量
  58. pageSize = 8;
  59. // 分页后的项目列表(列表模式下可用)
  60. paginatedProjects = computed(() => {
  61. const filteredProjects = this.projects();
  62. const startIndex = (this.currentPage() - 1) * this.pageSize;
  63. return filteredProjects.slice(startIndex, startIndex + this.pageSize);
  64. });
  65. // 总页数
  66. totalPages = computed(() => {
  67. return Math.ceil(this.projects().length / this.pageSize);
  68. });
  69. // 筛选和排序选项
  70. statusOptions = [
  71. { value: 'all', label: '全部' },
  72. { value: 'order', label: '订单分配' },
  73. { value: 'requirements', label: '确认需求' },
  74. { value: 'delivery', label: '交付执行' },
  75. { value: 'aftercare', label: '售后' }
  76. ];
  77. stageOptions = [
  78. { value: 'all', label: '全部阶段' },
  79. { value: '需求沟通', label: '需求沟通' },
  80. { value: '建模', label: '建模' },
  81. { value: '软装', label: '软装' },
  82. { value: '渲染', label: '渲染' },
  83. { value: '尾款结算', label: '尾款结算' },
  84. { value: '投诉处理', label: '投诉处理' }
  85. ];
  86. sortOptions = [
  87. { value: 'deadline', label: '截止日期' },
  88. { value: 'createdAt', label: '创建时间' },
  89. { value: 'name', label: '项目名称' }
  90. ];
  91. // Parse相关
  92. company: FmodeObject | null = null;
  93. currentProfile: FmodeObject | null = null;
  94. isLoading = signal(false);
  95. loadError = signal<string | null>(null);
  96. constructor(
  97. private projectService: ProjectService,
  98. private router: Router,
  99. private route: ActivatedRoute,
  100. private dialog: MatDialog,
  101. private profileService: ProfileService
  102. ) {}
  103. async ngOnInit(): Promise<void> {
  104. // 读取上次的视图记忆
  105. const saved = localStorage.getItem('cs.viewMode');
  106. if (saved === 'card' || saved === 'list' || saved === 'dashboard') {
  107. this.viewMode.set(saved as 'card' | 'list' | 'dashboard');
  108. }
  109. // 初始化用户和公司信息
  110. await this.initializeUserAndCompany();
  111. // 清理重复的Product记录(确保每个项目在每个阶段只出现一次)
  112. await this.cleanupDuplicateProducts();
  113. // 加载真实项目数据
  114. await this.loadProjects();
  115. // 处理来自dashboard的查询参数
  116. this.route.queryParams.subscribe(params => {
  117. const filter = params['filter'];
  118. if (filter === 'all') {
  119. // 显示所有项目 - 重置筛选
  120. this.statusFilter.set('all');
  121. console.log('✅ 显示所有项目');
  122. } else if (filter === 'pending') {
  123. // 筛选待分配项目 - 使用'order'列ID
  124. this.statusFilter.set('order');
  125. console.log('✅ 筛选待分配项目(订单分配阶段)');
  126. }
  127. });
  128. // 添加消息监听器,处理来自iframe的导航请求
  129. this.messageListener = (event: MessageEvent) => {
  130. // 验证消息来源(可以根据需要添加更严格的验证)
  131. if (event.data && event.data.type === 'navigate' && event.data.route) {
  132. this.router.navigate([event.data.route]);
  133. }
  134. };
  135. window.addEventListener('message', this.messageListener);
  136. // 🔍 添加全局调试方法
  137. (window as any).debugCustomerServiceProjects = () => {
  138. console.log('🔍 [客服端项目调试] 当前项目列表:');
  139. const projects = this.allProjects();
  140. projects.forEach((project, index) => {
  141. console.log(`项目${index + 1}: "${project.name}"`);
  142. console.log(` - currentStage: ${project.currentStage}`);
  143. console.log(` - stage: ${project.stage}`);
  144. console.log(` - status: ${project.status}`);
  145. console.log(` - 看板列: ${this.getColumnIdForProject(project as ProjectListItem)}`);
  146. console.log(' ---');
  147. });
  148. };
  149. console.log('🔍 调试方法已添加到全局: window.debugCustomerServiceProjects()');
  150. }
  151. // 🎯 判断是否为交付执行阶段(用于统一显示)
  152. private isDeliveryExecutionStage(stage: string): boolean {
  153. if (!stage) return false;
  154. const trimmedStage = stage.trim();
  155. const lowerStage = trimmedStage.toLowerCase();
  156. return trimmedStage === '交付执行' ||
  157. lowerStage === 'delivery' ||
  158. // 建模相关
  159. trimmedStage === '建模' ||
  160. trimmedStage === '建模阶段' ||
  161. trimmedStage === '白模' ||
  162. trimmedStage === '白膜' ||
  163. lowerStage === 'modeling' ||
  164. // 软装相关
  165. trimmedStage === '软装' ||
  166. trimmedStage === '软装阶段' ||
  167. lowerStage === 'soft_decor' ||
  168. lowerStage === 'decoration' ||
  169. // 渲染相关
  170. trimmedStage === '渲染' ||
  171. trimmedStage === '渲染阶段' ||
  172. lowerStage === 'rendering' ||
  173. // 后期相关
  174. trimmedStage === '后期制作' ||
  175. trimmedStage === '后期处理' ||
  176. trimmedStage === '后期' ||
  177. lowerStage === 'postproduction' ||
  178. // 评审修改相关
  179. trimmedStage === '评审' ||
  180. trimmedStage === '方案评审' ||
  181. trimmedStage === '修改' ||
  182. trimmedStage === '方案修改' ||
  183. trimmedStage === '修订' ||
  184. lowerStage === 'review' ||
  185. lowerStage === 'revision' ||
  186. // 其他可能的交付执行子阶段
  187. trimmedStage === '设计' ||
  188. trimmedStage === '设计阶段' ||
  189. trimmedStage === '制作' ||
  190. trimmedStage === '制作阶段' ||
  191. trimmedStage === '完善' ||
  192. trimmedStage === '优化' ||
  193. trimmedStage === '调整';
  194. }
  195. // 🔍 验证项目分配统计
  196. private validateProjectDistribution(projects: Project[]): void {
  197. const orderCount = projects.filter(p => this.isOrderAssignment(p)).length;
  198. const requirementsCount = projects.filter(p => this.isRequirementsConfirmation(p)).length;
  199. const deliveryCount = projects.filter(p => this.isDeliveryExecution(p)).length;
  200. const aftercareCount = projects.filter(p => this.isAftercare(p)).length;
  201. console.log('🔍 [客服端项目分配统计]:');
  202. console.log(` 订单分配: ${orderCount} 个项目`);
  203. console.log(` 确认需求: ${requirementsCount} 个项目`);
  204. console.log(` 交付执行: ${deliveryCount} 个项目`);
  205. console.log(` 售后: ${aftercareCount} 个项目`);
  206. console.log(` 总计: ${projects.length} 个项目`);
  207. // 🎯 与期望值对比
  208. const expected = { order: 3, requirements: 4, delivery: 8, aftercare: 5 };
  209. console.log('🎯 [期望值对比]:');
  210. console.log(` 订单分配: 实际${orderCount} vs 期望${expected.order} ${orderCount === expected.order ? '✅' : '❌'}`);
  211. console.log(` 确认需求: 实际${requirementsCount} vs 期望${expected.requirements} ${requirementsCount === expected.requirements ? '✅' : '❌'}`);
  212. console.log(` 交付执行: 实际${deliveryCount} vs 期望${expected.delivery} ${deliveryCount === expected.delivery ? '✅' : '❌'}`);
  213. console.log(` 售后: 实际${aftercareCount} vs 期望${expected.aftercare} ${aftercareCount === expected.aftercare ? '✅' : '❌'}`);
  214. // 🔍 如果数量不匹配,显示详细信息
  215. if (orderCount !== expected.order) {
  216. console.log('🔍 [订单分配阶段项目详情]:');
  217. projects.filter(p => this.isOrderAssignment(p)).forEach(p => {
  218. console.log(` - "${p.name}": currentStage="${p.currentStage}"`);
  219. });
  220. }
  221. if (deliveryCount !== expected.delivery) {
  222. console.log('🔍 [交付执行阶段项目详情]:');
  223. projects.filter(p => this.isDeliveryExecution(p)).forEach(p => {
  224. console.log(` - "${p.name}": currentStage="${p.currentStage}"`);
  225. });
  226. // 🔍 显示所有项目的原始阶段,帮助识别遗漏的阶段
  227. console.log('🔍 [所有项目的原始阶段]:');
  228. projects.forEach(p => {
  229. const isDelivery = this.isDeliveryExecution(p);
  230. console.log(` - "${p.name}": "${p.currentStage}" ${isDelivery ? '✅交付执行' : ''}`);
  231. });
  232. }
  233. }
  234. ngOnDestroy(): void {
  235. // 清理消息监听器
  236. if (this.messageListener) {
  237. window.removeEventListener('message', this.messageListener);
  238. }
  239. }
  240. // 视图切换
  241. toggleView(mode: 'card' | 'list' | 'dashboard') {
  242. if (this.viewMode() !== mode) {
  243. this.viewMode.set(mode);
  244. localStorage.setItem('cs.viewMode', mode);
  245. }
  246. }
  247. // 初始化用户和公司信息
  248. private async initializeUserAndCompany(): Promise<void> {
  249. try {
  250. // 方法1: 从localStorage获取公司ID(参考team-leader的实现)
  251. const companyId = localStorage.getItem('company');
  252. if (companyId) {
  253. // 创建公司指针对象
  254. const CompanyClass = Parse.Object.extend('Company');
  255. this.company = new CompanyClass();
  256. this.company.id = companyId;
  257. console.log('✅ 从localStorage加载公司ID:', companyId);
  258. } else {
  259. // 方法2: 从Profile获取公司信息
  260. this.currentProfile = await this.profileService.getCurrentProfile();
  261. if (!this.currentProfile) {
  262. throw new Error('无法获取用户信息');
  263. }
  264. // 获取公司信息
  265. this.company = this.currentProfile.get('company');
  266. if (!this.company) {
  267. throw new Error('无法获取公司信息');
  268. }
  269. console.log('✅ 从Profile加载公司信息:', this.company.get('name'));
  270. }
  271. } catch (error) {
  272. console.error('❌ 初始化用户和公司信息失败:', error);
  273. this.loadError.set('加载用户信息失败,请刷新页面重试');
  274. }
  275. }
  276. // 获取公司指针
  277. private getCompanyPointer() {
  278. if (!this.company) {
  279. throw new Error('公司信息未初始化');
  280. }
  281. return {
  282. __type: 'Pointer',
  283. className: 'Company',
  284. objectId: this.company.id
  285. };
  286. }
  287. /**
  288. * 清理重复的Product记录
  289. * 对于同一个项目中相同名称的Product,只保留最早创建的,删除其他重复的
  290. */
  291. private async cleanupDuplicateProducts(): Promise<void> {
  292. if (!this.company) {
  293. console.warn('公司信息未加载,跳过重复清理');
  294. return;
  295. }
  296. try {
  297. console.log('🔍 开始检查重复的Product记录...');
  298. // 查询所有Product
  299. const ProductQuery = new Parse.Query('Product');
  300. ProductQuery.equalTo('company', this.getCompanyPointer());
  301. ProductQuery.notEqualTo('isDeleted', true);
  302. ProductQuery.limit(1000);
  303. const allProducts = await ProductQuery.find();
  304. console.log(`📦 找到 ${allProducts.length} 个Product记录`);
  305. // 按项目分组,然后按产品名称检测重复
  306. const projectMap = new Map<string, Map<string, any[]>>();
  307. for (const product of allProducts) {
  308. const projectId = product.get('project')?.id;
  309. const productName = (product.get('productName') || '').trim().toLowerCase();
  310. if (!projectId || !productName) continue;
  311. if (!projectMap.has(projectId)) {
  312. projectMap.set(projectId, new Map());
  313. }
  314. const productsByName = projectMap.get(projectId)!;
  315. if (!productsByName.has(productName)) {
  316. productsByName.set(productName, []);
  317. }
  318. productsByName.get(productName)!.push(product);
  319. }
  320. // 找出并删除重复的Product
  321. let duplicateCount = 0;
  322. const duplicatesToDelete: any[] = [];
  323. for (const [projectId, productsByName] of projectMap.entries()) {
  324. for (const [productName, products] of productsByName.entries()) {
  325. if (products.length > 1) {
  326. console.log(`⚠️ 项目 ${projectId} 中发现重复空间: "${productName}" (${products.length}个)`);
  327. // 按创建时间排序,保留最早的,删除其他的
  328. products.sort((a, b) => {
  329. const timeA = a.get('createdAt')?.getTime() || 0;
  330. const timeB = b.get('createdAt')?.getTime() || 0;
  331. return timeA - timeB;
  332. });
  333. // 保留第一个,删除其他的
  334. for (let i = 1; i < products.length; i++) {
  335. duplicatesToDelete.push(products[i]);
  336. duplicateCount++;
  337. console.log(` 🗑️ 标记删除: ${products[i].get('productName')} (${products[i].id})`);
  338. }
  339. }
  340. }
  341. }
  342. // 批量删除重复的Product
  343. if (duplicatesToDelete.length > 0) {
  344. console.log(`🗑️ 准备删除 ${duplicatesToDelete.length} 个重复Product...`);
  345. for (const product of duplicatesToDelete) {
  346. try {
  347. product.set('isDeleted', true);
  348. product.set('data', {
  349. ...product.get('data'),
  350. deletedAt: new Date(),
  351. deletedReason: '重复产品,自动清理'
  352. });
  353. await product.save();
  354. console.log(` ✅ 已删除: ${product.get('productName')} (${product.id})`);
  355. } catch (error) {
  356. console.error(` ❌ 删除失败: ${product.id}`, error);
  357. }
  358. }
  359. console.log(`✅ 重复Product清理完成,共删除 ${duplicateCount} 个`);
  360. } else {
  361. console.log('✅ 未发现重复的Product记录');
  362. }
  363. } catch (error) {
  364. console.error('❌ 清理重复Product失败:', error);
  365. // 不阻塞主流程
  366. }
  367. }
  368. // 加载项目列表(从Parse Server)
  369. async loadProjects(): Promise<void> {
  370. if (!this.company) {
  371. console.warn('公司信息未加载,跳过项目加载');
  372. return;
  373. }
  374. this.isLoading.set(true);
  375. this.loadError.set(null);
  376. try {
  377. const ProjectQuery = new Parse.Query('Project');
  378. ProjectQuery.equalTo('company', this.getCompanyPointer());
  379. // 不强制要求isDeleted字段,兼容没有该字段的数据
  380. ProjectQuery.notEqualTo('isDeleted', true);
  381. ProjectQuery.include('contact', 'assignee', 'owner');
  382. ProjectQuery.descending('updatedAt');
  383. ProjectQuery.limit(500); // 获取最多500个项目
  384. const projectObjects = await ProjectQuery.find();
  385. console.log(`✅ 从Parse Server加载了 ${projectObjects.length} 个项目`);
  386. // 🔍 调试:检查前5个项目的阶段数据
  387. if (projectObjects.length > 0) {
  388. console.log('🔍 [客服端调试] 检查前5个项目的阶段数据:');
  389. for (let i = 0; i < Math.min(5, projectObjects.length); i++) {
  390. const proj = projectObjects[i];
  391. const title = proj.get('title') || '未命名项目';
  392. const currentStage = proj.get('currentStage');
  393. const stage = proj.get('stage');
  394. const status = proj.get('status');
  395. console.log(` 项目${i + 1}: "${title}"`);
  396. console.log(` - currentStage: ${currentStage}`);
  397. console.log(` - stage: ${stage}`);
  398. console.log(` - status: ${status}`);
  399. console.log(` - 最终使用阶段: ${currentStage || stage || '订单分配'}`);
  400. console.log(' ---');
  401. }
  402. }
  403. // 如果没有数据,打印调试信息
  404. if (projectObjects.length === 0) {
  405. console.warn('⚠️ 未找到项目数据,请检查:');
  406. console.warn('1. Parse Server中是否有Project数据');
  407. console.warn('2. 当前公司ID:', this.company.id);
  408. console.warn('3. 数据是否正确关联到当前公司');
  409. }
  410. // 转换为Project接口格式(并从Product表同步最新阶段)
  411. const projects: Project[] = await Promise.all(projectObjects.map(async (obj: FmodeObject) => {
  412. const contact = obj.get('contact');
  413. const assignee = obj.get('assignee');
  414. // 🔄 直接从Project表读取阶段(与组长端保持严格一致)
  415. let rawStage = obj.get('currentStage') || obj.get('stage') || '订单分配';
  416. console.log(`📊 项目 ${obj.get('title')} 原始阶段数据: currentStage=${obj.get('currentStage')}, stage=${obj.get('stage')}`);
  417. // 🔥 关键修复:统一交付执行子阶段的显示
  418. let finalStage = rawStage;
  419. // 🎯 将交付执行的所有子阶段统一显示为"交付执行"
  420. const tempProject = { currentStage: rawStage, status: obj.get('status') };
  421. if (this.isDeliveryExecutionStage(rawStage)) {
  422. finalStage = '交付执行';
  423. console.log(`🔄 统一阶段显示: "${obj.get('title')}" 从 "${rawStage}" → "交付执行"`);
  424. }
  425. // 🔄 根据阶段自动判断状态(与组长端、管理端保持一致)
  426. const projectStatus = obj.get('status');
  427. const autoStatus = getProjectStatusByStage(rawStage, projectStatus);
  428. console.log(`📊 客服项目 "${obj.get('title')}": 原始阶段=${rawStage}, 最终阶段=${finalStage}, 原状态=${projectStatus}, 自动状态=${autoStatus}`);
  429. // 确保updatedAt是Date对象
  430. const updatedAt = obj.get('updatedAt');
  431. const createdAt = obj.get('createdAt');
  432. return {
  433. id: obj.id,
  434. name: obj.get('title') || '未命名项目',
  435. customerName: contact?.get('name') || '未知客户',
  436. customerId: contact?.id || '',
  437. status: autoStatus as ProjectStatus, // 使用根据阶段自动判断的状态
  438. currentStage: finalStage as ProjectStage,
  439. stage: finalStage as ProjectStage, // stage和currentStage保持一致
  440. assigneeId: assignee?.id || '',
  441. assigneeName: assignee?.get('name') || '未分配',
  442. deadline: obj.get('deadline') || new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
  443. createdAt: createdAt instanceof Date ? createdAt : (createdAt ? new Date(createdAt) : new Date()),
  444. updatedAt: updatedAt instanceof Date ? updatedAt : (updatedAt ? new Date(updatedAt) : new Date()),
  445. description: obj.get('description') || '',
  446. priority: obj.get('priority') || 'medium',
  447. customerTags: [],
  448. highPriorityNeeds: [],
  449. skillsRequired: [],
  450. contact: contact
  451. };
  452. }));
  453. this.allProjects.set(projects);
  454. this.baseProjects = projects;
  455. this.processProjects(projects);
  456. // 🔍 验证项目分配统计
  457. this.validateProjectDistribution(projects);
  458. console.log('项目数据处理完成');
  459. } catch (error) {
  460. console.error('加载项目列表失败:', error);
  461. this.loadError.set('加载项目列表失败,请刷新页面重试');
  462. this.projects.set([]);
  463. } finally {
  464. this.isLoading.set(false);
  465. }
  466. }
  467. // 映射Parse Server状态到前端状态
  468. private mapStatus(parseStatus: string): ProjectStatus {
  469. const statusMap: Record<string, ProjectStatus> = {
  470. '进行中': '进行中',
  471. '已完成': '已完成',
  472. '已暂停': '已暂停',
  473. '已延期': '已延期'
  474. };
  475. return statusMap[parseStatus] || '进行中';
  476. }
  477. // 映射Parse Server阶段到前端阶段
  478. private mapStage(parseStage: string): ProjectStage {
  479. // 直接返回Parse Server的阶段,不做转换
  480. // Parse Server的currentStage字段包含:订单分配、需求沟通、建模、软装、渲染、后期、尾款结算、投诉处理等
  481. if (!parseStage) {
  482. return '需求沟通'; // 默认阶段
  483. }
  484. return parseStage as ProjectStage;
  485. }
  486. // 处理项目数据,添加计算属性
  487. processProjects(projects: Project[]): void {
  488. const processedProjects = projects.map(project => {
  489. // 计算项目进度(模拟)
  490. const progress = this.calculateProjectProgress(project);
  491. // 计算距离截止日期的天数
  492. const daysUntilDeadline = this.calculateDaysUntilDeadline(project.deadline);
  493. // 判断是否紧急(截止日期前3天或已逾期)
  494. const isUrgent = daysUntilDeadline <= 3 && project.status === '进行中';
  495. // 生成标签显示文本
  496. const tagDisplayText = this.generateTagDisplayText(project);
  497. return {
  498. ...project,
  499. progress,
  500. daysUntilDeadline,
  501. isUrgent,
  502. tagDisplayText
  503. };
  504. });
  505. this.projects.set(this.applyFiltersAndSorting(processedProjects));
  506. }
  507. // 应用筛选和排序
  508. applyFiltersAndSorting(projects: ProjectListItem[]): ProjectListItem[] {
  509. let filteredProjects = [...projects];
  510. // 搜索筛选
  511. if (this.searchTerm().trim()) {
  512. const searchLower = this.searchTerm().toLowerCase().trim();
  513. filteredProjects = filteredProjects.filter(project =>
  514. project.name.toLowerCase().includes(searchLower) ||
  515. project.customerName.toLowerCase().includes(searchLower)
  516. );
  517. }
  518. // 状态筛选(按看板列映射)
  519. if (this.statusFilter() !== 'all') {
  520. const col = this.statusFilter() as 'order' | 'requirements' | 'delivery' | 'aftercare';
  521. filteredProjects = filteredProjects.filter(project =>
  522. this.getColumnIdForProject(project) === col
  523. );
  524. }
  525. // 阶段筛选
  526. if (this.stageFilter() !== 'all') {
  527. filteredProjects = filteredProjects.filter(project =>
  528. project.currentStage === this.stageFilter()
  529. );
  530. }
  531. // 排序
  532. filteredProjects.sort((a, b) => {
  533. switch (this.sortBy()) {
  534. case 'deadline':
  535. return new Date(a.deadline).getTime() - new Date(b.deadline).getTime();
  536. case 'createdAt':
  537. return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
  538. case 'name':
  539. return a.name.localeCompare(b.name);
  540. default:
  541. return 0;
  542. }
  543. });
  544. return filteredProjects;
  545. }
  546. // 生成标签显示文本
  547. generateTagDisplayText(project: Project): string {
  548. if (!project.customerTags || project.customerTags.length === 0) {
  549. return '普通项目';
  550. }
  551. const tag = project.customerTags[0];
  552. return `${tag.preference}${tag.needType}`;
  553. }
  554. // 计算项目进度(模拟)
  555. calculateProjectProgress(project: Project): number {
  556. if (project.status === '已完成') return 100;
  557. if (project.status === '已暂停' || project.status === '已延期') return 0;
  558. // 基于当前阶段计算进度(包含四大核心阶段和细分阶段)
  559. const stageProgress: Record<ProjectStage, number> = {
  560. // 四大核心阶段
  561. '订单分配': 0,
  562. '确认需求': 25,
  563. '交付执行': 60,
  564. '售后归档': 95,
  565. // 细分阶段(向后兼容)
  566. '需求沟通': 20,
  567. '方案确认': 30,
  568. '建模': 40,
  569. '软装': 50,
  570. '渲染': 70,
  571. '后期': 85,
  572. '尾款结算': 90,
  573. '客户评价': 100,
  574. '投诉处理': 100
  575. };
  576. return stageProgress[project.currentStage] || 0;
  577. }
  578. // 计算距离截止日期的天数
  579. calculateDaysUntilDeadline(deadline: Date): number {
  580. const now = new Date();
  581. const deadlineDate = new Date(deadline);
  582. const diffTime = deadlineDate.getTime() - now.getTime();
  583. return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
  584. }
  585. // 列表/筛选交互(保留已有实现)
  586. onSearch(): void {
  587. // 搜索后重算
  588. this.processProjects(this.baseProjects);
  589. }
  590. onStatusChange(event: Event): void {
  591. const value = (event.target as HTMLSelectElement).value;
  592. this.statusFilter.set(value);
  593. this.processProjects(this.baseProjects);
  594. }
  595. onStageChange(event: Event): void {
  596. const value = (event.target as HTMLSelectElement).value;
  597. this.stageFilter.set(value);
  598. this.processProjects(this.baseProjects);
  599. }
  600. onSortChange(event: Event): void {
  601. const value = (event.target as HTMLSelectElement).value;
  602. this.sortBy.set(value);
  603. this.processProjects(this.baseProjects);
  604. }
  605. goToPage(page: number): void {
  606. if (page >= 1 && page <= this.totalPages()) {
  607. this.currentPage.set(page);
  608. }
  609. }
  610. prevPage(): void {
  611. if (this.currentPage() > 1) {
  612. this.currentPage.update(v => v - 1);
  613. }
  614. }
  615. nextPage(): void {
  616. if (this.currentPage() < this.totalPages()) {
  617. this.currentPage.update(v => v + 1);
  618. }
  619. }
  620. pageNumbers = computed(() => {
  621. const total = this.totalPages();
  622. const pages: number[] = [];
  623. const maxToShow = Math.min(total, 5);
  624. for (let i = 1; i <= maxToShow; i++) pages.push(i);
  625. return pages;
  626. });
  627. getAbsValue(value: number): number {
  628. return Math.abs(value);
  629. }
  630. formatDate(date: Date): string {
  631. const d = new Date(date);
  632. const y = d.getFullYear();
  633. const m = String(d.getMonth() + 1).padStart(2, '0');
  634. const day = String(d.getDate()).padStart(2, '0');
  635. return `${y}-${m}-${day}`;
  636. }
  637. getStatusClass(status: string): string {
  638. switch (status) {
  639. case '进行中': return 'status-in-progress';
  640. case '已完成': return 'status-completed';
  641. case '已暂停': return 'status-paused';
  642. case '已延期': return 'status-overdue';
  643. default: return '';
  644. }
  645. }
  646. getStageClass(stage: string): string {
  647. switch (stage) {
  648. case '需求沟通': return 'stage-communication';
  649. case '建模': return 'stage-modeling';
  650. case '软装': return 'stage-decoration';
  651. case '渲染': return 'stage-rendering';
  652. case '投诉处理': return 'stage-completed';
  653. case '订单分配': return 'stage-active';
  654. case '方案确认': return 'stage-active';
  655. case '尾款结算': return 'stage-completed';
  656. case '客户评价': return 'stage-completed';
  657. default: return '';
  658. }
  659. }
  660. // 看板分组逻辑 - 按照订单分配、确认需求、交付执行、售后四个阶段
  661. // 🔥 修复:直接使用原始阶段名称进行匹配(与组长端完全一致)
  662. private isOrderAssignment(p: Project): boolean {
  663. const stage = (p.currentStage as string)?.trim();
  664. if (!stage) return false;
  665. const lowerStage = stage.toLowerCase();
  666. return stage === '订单分配' ||
  667. lowerStage === 'order' ||
  668. stage === '待分配' ||
  669. stage === '待审批';
  670. }
  671. private isRequirementsConfirmation(p: Project): boolean {
  672. const stage = (p.currentStage as string)?.trim();
  673. if (!stage) return false;
  674. const lowerStage = stage.toLowerCase();
  675. return stage === '确认需求' ||
  676. lowerStage === 'requirements' ||
  677. stage === '需求沟通' ||
  678. stage === '需求确认' ||
  679. stage === '方案规划' ||
  680. stage === '方案确认' ||
  681. stage === '方案深化';
  682. }
  683. private isDeliveryExecution(p: Project): boolean {
  684. const stage = (p.currentStage as string)?.trim();
  685. if (!stage) return false;
  686. const lowerStage = stage.toLowerCase();
  687. // 🔥 扩展交付执行阶段的识别范围,包含所有子阶段
  688. return stage === '交付执行' ||
  689. lowerStage === 'delivery' ||
  690. // 建模相关
  691. stage === '建模' ||
  692. stage === '建模阶段' ||
  693. stage === '白模' ||
  694. stage === '白膜' ||
  695. lowerStage === 'modeling' ||
  696. // 软装相关
  697. stage === '软装' ||
  698. stage === '软装阶段' ||
  699. lowerStage === 'soft_decor' ||
  700. lowerStage === 'decoration' ||
  701. // 渲染相关
  702. stage === '渲染' ||
  703. stage === '渲染阶段' ||
  704. lowerStage === 'rendering' ||
  705. // 后期相关
  706. stage === '后期制作' ||
  707. stage === '后期处理' ||
  708. stage === '后期' ||
  709. lowerStage === 'postproduction' ||
  710. // 评审修改相关
  711. stage === '评审' ||
  712. stage === '方案评审' ||
  713. stage === '修改' ||
  714. stage === '方案修改' ||
  715. stage === '修订' ||
  716. lowerStage === 'review' ||
  717. lowerStage === 'revision' ||
  718. // 其他可能的交付执行子阶段
  719. stage === '设计' ||
  720. stage === '设计阶段' ||
  721. stage === '制作' ||
  722. stage === '制作阶段' ||
  723. stage === '完善' ||
  724. stage === '优化' ||
  725. stage === '调整';
  726. }
  727. private isAftercare(p: Project): boolean {
  728. const stage = (p.currentStage as string)?.trim();
  729. if (!stage) return false;
  730. const lowerStage = stage.toLowerCase();
  731. return stage === '售后归档' ||
  732. lowerStage === 'aftercare' ||
  733. stage === '售后' ||
  734. stage === '归档' ||
  735. stage === '尾款结算' ||
  736. stage === '客户评价' ||
  737. stage === '投诉处理' ||
  738. stage === '已归档' ||
  739. p.status === '已完成';
  740. }
  741. getProjectsByColumn(columnId: 'order' | 'requirements' | 'delivery' | 'aftercare'): ProjectListItem[] {
  742. const list = this.projects();
  743. let result: ProjectListItem[] = [];
  744. switch (columnId) {
  745. case 'order':
  746. result = list.filter(p => this.isOrderAssignment(p));
  747. break;
  748. case 'requirements':
  749. result = list.filter(p => this.isRequirementsConfirmation(p));
  750. break;
  751. case 'delivery':
  752. result = list.filter(p => this.isDeliveryExecution(p));
  753. break;
  754. case 'aftercare':
  755. result = list.filter(p => this.isAftercare(p));
  756. break;
  757. default:
  758. result = [];
  759. }
  760. // 🔍 调试日志
  761. console.log(`🔍 [getProjectsByColumn] ${columnId}: ${result.length} 个项目`);
  762. if (result.length > 0) {
  763. result.forEach(p => {
  764. console.log(` - "${p.name}": currentStage="${p.currentStage}"`);
  765. });
  766. }
  767. return result;
  768. }
  769. // 新增:根据项目状态与阶段推断所在看板列
  770. getColumnIdForProject(project: ProjectListItem): 'order' | 'requirements' | 'delivery' | 'aftercare' {
  771. if (this.isOrderAssignment(project)) return 'order';
  772. if (this.isRequirementsConfirmation(project)) return 'requirements';
  773. if (this.isDeliveryExecution(project)) return 'delivery';
  774. if (this.isAftercare(project)) return 'aftercare';
  775. return 'requirements'; // 默认为确认需求阶段
  776. }
  777. // 详情跳转到wxwork项目详情页面(与组长、管理员保持一致)
  778. navigateToProject(project: ProjectListItem) {
  779. // 获取公司ID
  780. const cid = localStorage.getItem('company') || '';
  781. if (!cid) {
  782. console.error('未找到公司ID,无法跳转到项目详情页');
  783. return;
  784. }
  785. // ✅ 根据项目实际阶段决定路由(不使用columnId)
  786. // wxwork路由支持的阶段:order, requirements, delivery, aftercare, issues
  787. const stageRouteMap: Record<string, string> = {
  788. '订单分配': 'order',
  789. '确认需求': 'requirements',
  790. '方案确认': 'requirements',
  791. '方案深化': 'requirements',
  792. '交付执行': 'delivery',
  793. '建模': 'delivery',
  794. '软装': 'delivery',
  795. '渲染': 'delivery',
  796. '后期': 'delivery',
  797. '白模': 'delivery',
  798. '白膜': 'delivery',
  799. '售后归档': 'aftercare',
  800. '售后': 'aftercare',
  801. '尾款结算': 'aftercare',
  802. '客户评价': 'aftercare',
  803. '投诉处理': 'aftercare'
  804. };
  805. const currentStage = project.currentStage || '订单分配';
  806. const stagePath = stageRouteMap[currentStage] || 'order';
  807. console.log(`🎯 [客服端跳转] 项目"${project.name}"`, {
  808. currentStage,
  809. stagePath,
  810. projectId: project.id
  811. });
  812. // ✅ 标记从客服板块进入(用于控制"确认订单"按钮权限)
  813. try {
  814. localStorage.setItem('enterFromCustomerService', '1');
  815. localStorage.setItem('customerServiceMode', 'true');
  816. console.log('✅ 已标记从客服板块进入,允许确认订单');
  817. } catch (e) {
  818. console.warn('无法设置 localStorage 标记:', e);
  819. }
  820. // 跳转到wxwork路由的项目详情页(纯净页面,无管理端侧边栏)
  821. // 路由格式:/wxwork/:cid/project/:projectId/:stage
  822. this.router.navigate(['/wxwork', cid, 'project', project.id, stagePath]);
  823. }
  824. // 新增:直接进入沟通管理(消息)标签
  825. navigateToMessages(project: ProjectListItem) {
  826. this.router.navigate(['/customer-service/messages'], { queryParams: { projectId: project.id } });
  827. }
  828. // 导航到创建订单页面
  829. navigateToCreateOrder() {
  830. // 打开咨询订单弹窗
  831. const dialogRef = this.dialog.open(ConsultationOrderDialogComponent, {
  832. width: '900px',
  833. maxWidth: '95vw',
  834. maxHeight: '90vh',
  835. panelClass: 'consultation-order-dialog'
  836. });
  837. // 监听订单分配成功事件
  838. dialogRef.componentInstance.orderCreated.subscribe((orderData: any) => {
  839. // 关闭弹窗
  840. dialogRef.close();
  841. // 准备同步数据
  842. const syncData = {
  843. customerInfo: orderData.customerInfo,
  844. requirementInfo: orderData.requirementInfo,
  845. preferenceTags: orderData.preferenceTags,
  846. assignedDesigner: orderData.assignedDesigner
  847. };
  848. // 跳转到新创建的项目详情页面,传递同步数据
  849. this.router.navigate([
  850. '/designer/project-detail',
  851. orderData.orderId
  852. ], {
  853. queryParams: {
  854. role: 'customer-service',
  855. activeTab: 'overview',
  856. syncData: JSON.stringify(syncData)
  857. }
  858. });
  859. });
  860. }
  861. }