project-management.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. import { Component, OnInit, signal } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { RouterModule } from '@angular/router';
  4. import { FormsModule } from '@angular/forms';
  5. import { MatButtonModule } from '@angular/material/button';
  6. import { MatIconModule } from '@angular/material/icon';
  7. import { MatTableModule } from '@angular/material/table';
  8. import { MatInputModule } from '@angular/material/input';
  9. import { MatSelectModule } from '@angular/material/select';
  10. import { MatPaginatorModule } from '@angular/material/paginator';
  11. import { MatDialogModule, MatDialog } from '@angular/material/dialog';
  12. import { MatSortModule } from '@angular/material/sort';
  13. import { ProjectDialogComponent } from './project-dialog/project-dialog'; // @ts-ignore: Component used in code but not in template
  14. import { ProjectService } from '../services/project.service';
  15. import { ProjectAutoCaseService } from '../services/project-auto-case.service';
  16. import { DesignerTeamAssignmentModalComponent, Designer, ProjectTeam } from '../../designer/project-detail/components/designer-team-assignment-modal/designer-team-assignment-modal.component';
  17. import { mapStageToCorePhase, getCorePhaseName, getProjectStatusByStage, normalizeStage } from '../../../utils/project-stage-mapper';
  18. interface Project {
  19. id: string;
  20. title: string;
  21. customer: string;
  22. customerId?: string;
  23. status: string;
  24. assignee: string;
  25. assigneeId?: string;
  26. createdAt?: Date;
  27. updatedAt?: Date;
  28. deadline?: Date;
  29. currentStage?: string;
  30. }
  31. @Component({
  32. selector: 'app-project-management',
  33. standalone: true,
  34. imports: [
  35. CommonModule,
  36. RouterModule,
  37. FormsModule,
  38. MatButtonModule,
  39. MatIconModule,
  40. MatTableModule,
  41. MatInputModule,
  42. MatSelectModule,
  43. MatPaginatorModule,
  44. MatDialogModule,
  45. MatSortModule,
  46. DesignerTeamAssignmentModalComponent
  47. ],
  48. templateUrl: './project-management.html',
  49. styleUrl: './project-management.scss'
  50. })
  51. export class ProjectManagement implements OnInit {
  52. projects = signal<Project[]>([]);
  53. filteredProjects = signal<Project[]>([]);
  54. searchTerm = '';
  55. statusFilter = '';
  56. sortColumn = 'updatedAt';
  57. sortDirection = 'desc';
  58. pageSize = 10;
  59. currentPage = 0;
  60. loading = signal(false);
  61. // 团队分配相关属性
  62. showTeamAssignmentModal = false;
  63. selectedProject: Project | null = null;
  64. projectTeams: ProjectTeam[] = [];
  65. currentTeamAssignment: any = {
  66. primaryTeamId: null,
  67. quotationAssignments: [],
  68. crossTeamCollaborators: []
  69. };
  70. currentQuotationItems: any[] = [];
  71. // 模拟项目团队数据
  72. private mockProjectTeams: ProjectTeam[] = [
  73. {
  74. id: 'team-1',
  75. name: '野生项目组',
  76. leaderId: 'designer-1',
  77. leaderName: '张佳乐',
  78. description: '专注于家庭装修设计,包括客厅、卧室、厨房等空间设计',
  79. members: [
  80. {
  81. id: 'designer-1',
  82. name: '张佳乐',
  83. avatar: '/assets/avatars/zhang.jpg',
  84. teamId: 'team-1',
  85. teamName: '野生项目组',
  86. isTeamLeader: true,
  87. status: 'busy',
  88. idleDays: 0,
  89. recentOrders: 3,
  90. lastOrderDate: '2024-10-20',
  91. reviewDates: ['2024-10-25', '2024-10-28'],
  92. workload: 85,
  93. skills: ['空间设计', '建模', '渲染'],
  94. isInStagnantProject: false,
  95. availableDates: [],
  96. groupId: 'team-1',
  97. groupName: '野生项目组',
  98. isLeader: true,
  99. currentProjects: 3
  100. },
  101. {
  102. id: 'designer-2',
  103. name: '未知设计师',
  104. avatar: '/assets/avatars/unknown.jpg',
  105. teamId: 'team-1',
  106. teamName: '野生项目组',
  107. isTeamLeader: false,
  108. status: 'idle',
  109. idleDays: 6,
  110. recentOrders: 0,
  111. lastOrderDate: '2024-10-14',
  112. reviewDates: [],
  113. workload: 0,
  114. skills: ['软装', '后期'],
  115. isInStagnantProject: false,
  116. availableDates: ['2024-10-25', '2024-10-26', '2024-10-27'],
  117. groupId: 'team-1',
  118. groupName: '野生项目组',
  119. isLeader: false,
  120. currentProjects: 0
  121. }
  122. ]
  123. },
  124. {
  125. id: 'team-2',
  126. name: '无常项目组',
  127. leaderId: 'designer-3',
  128. leaderName: '江集',
  129. description: '专业商业空间设计,办公室、店铺等',
  130. members: [
  131. {
  132. id: 'designer-3',
  133. name: '江集',
  134. avatar: '/assets/avatars/jiang.jpg',
  135. teamId: 'team-2',
  136. teamName: '无常项目组',
  137. isTeamLeader: true,
  138. status: 'busy',
  139. idleDays: 0,
  140. recentOrders: 2,
  141. lastOrderDate: '2024-10-21',
  142. reviewDates: ['2024-10-26'],
  143. workload: 70,
  144. skills: ['空间设计', '软装', '项目管理'],
  145. isInStagnantProject: false,
  146. availableDates: [],
  147. groupId: 'team-2',
  148. groupName: '无常项目组',
  149. isLeader: true,
  150. currentProjects: 2
  151. }
  152. ]
  153. }
  154. ];
  155. // 提供Math对象给模板使用
  156. readonly Math = Math;
  157. // 状态颜色映射
  158. statusColors: Record<string, string> = {
  159. '待分配': '#FFAA00',
  160. '进行中': '#165DFF',
  161. '已完成': '#00B42A',
  162. '已暂停': '#FF7D00',
  163. '已延期': '#F53F3F',
  164. '已取消': '#86909C'
  165. };
  166. // 状态文本映射
  167. statusTexts: Record<string, string> = {
  168. '待分配': '待分配',
  169. '进行中': '进行中',
  170. '已完成': '已完成',
  171. '已暂停': '已暂停',
  172. '已延期': '已延期',
  173. '已取消': '已取消'
  174. };
  175. constructor(
  176. private dialog: MatDialog,
  177. private projectService: ProjectService,
  178. private projectAutoCaseService: ProjectAutoCaseService
  179. ) {}
  180. ngOnInit(): void {
  181. this.loadProjects();
  182. }
  183. async loadProjects(): Promise<void> {
  184. this.loading.set(true);
  185. try {
  186. const projects = await this.projectService.findProjects({
  187. limit: 100
  188. });
  189. const projectList: Project[] = projects.map(p => {
  190. const json = this.projectService.toJSON(p);
  191. // 获取原始阶段
  192. const rawStage = json.currentStage || json.stage || '订单分配';
  193. // 🔄 规范化阶段名称(统一为四大核心阶段)
  194. const normalizedStage = normalizeStage(rawStage);
  195. // 🔄 根据阶段自动判断状态(与组长端逻辑保持一致)
  196. const autoStatus = getProjectStatusByStage(rawStage, json.status);
  197. console.log(`📊 [项目管理] "${json.title}":`, {
  198. 客户: json.customerName,
  199. 负责人: json.assigneeName,
  200. 角色: json.assigneeRole,
  201. 原始阶段: rawStage,
  202. 规范化阶段: normalizedStage,
  203. 状态: `${json.status} → ${autoStatus}`
  204. });
  205. return {
  206. id: json.objectId,
  207. title: json.title || '未命名项目',
  208. customer: json.customerName || '未知客户',
  209. customerId: json.customerId,
  210. status: autoStatus, // 使用根据阶段自动判断的状态
  211. assignee: json.assigneeName || '未分配',
  212. assigneeId: json.assigneeId,
  213. createdAt: json.createdAt?.iso || json.createdAt,
  214. updatedAt: json.updatedAt?.iso || json.updatedAt,
  215. deadline: json.deadline,
  216. currentStage: normalizedStage // 使用规范化后的阶段名称
  217. };
  218. });
  219. this.projects.set(projectList);
  220. this.applyFilters();
  221. } catch (error) {
  222. console.error('加载项目列表失败:', error);
  223. this.projects.set([]);
  224. } finally {
  225. this.loading.set(false);
  226. }
  227. }
  228. applyFilters(): void {
  229. let result = [...this.projects()];
  230. // 搜索过滤
  231. if (this.searchTerm) {
  232. const term = this.searchTerm.toLowerCase();
  233. result = result.filter(project =>
  234. project.title.toLowerCase().includes(term) ||
  235. project.customer.toLowerCase().includes(term) ||
  236. project.assignee.toLowerCase().includes(term)
  237. );
  238. }
  239. // 状态过滤
  240. if (this.statusFilter) {
  241. result = result.filter(project => project.status === this.statusFilter);
  242. }
  243. // 排序 - 按更新时间或创建时间
  244. result.sort((a, b) => {
  245. if (this.sortColumn === 'updatedAt' || this.sortColumn === 'createdAt') {
  246. const dateA = a[this.sortColumn] ? new Date(a[this.sortColumn] as Date).getTime() : 0;
  247. const dateB = b[this.sortColumn] ? new Date(b[this.sortColumn] as Date).getTime() : 0;
  248. return this.sortDirection === 'asc' ? dateA - dateB : dateB - dateA;
  249. } else {
  250. const valueA = String((a as any)[this.sortColumn] || '').toLowerCase();
  251. const valueB = String((b as any)[this.sortColumn] || '').toLowerCase();
  252. return this.sortDirection === 'asc' ? valueA.localeCompare(valueB) : valueB.localeCompare(valueA);
  253. }
  254. });
  255. this.filteredProjects.set(result);
  256. }
  257. onSearch(): void {
  258. this.applyFilters();
  259. }
  260. onStatusFilterChange(): void {
  261. this.applyFilters();
  262. }
  263. onSort(column: string): void {
  264. if (this.sortColumn === column) {
  265. this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
  266. } else {
  267. this.sortColumn = column;
  268. this.sortDirection = 'asc';
  269. }
  270. this.applyFilters();
  271. }
  272. // 简化的编辑对话框(只允许修改名字和分配组员)
  273. openEditDialog(project: Project): void {
  274. // TODO: 实现简单的编辑对话框,使用设计师分配组件
  275. // 暂时使用alert提示
  276. window?.fmode?.alert('编辑功能将在设计师分配组件对接完成后实现');
  277. }
  278. // 打开团队分配弹窗
  279. openTeamAssignmentModal(project: Project): void {
  280. this.selectedProject = project;
  281. // 加载项目团队数据
  282. this.projectTeams = this.mockProjectTeams;
  283. // 初始化当前团队分配信息
  284. this.currentTeamAssignment = {
  285. primaryTeamId: project.assigneeId || null,
  286. quotationAssignments: [],
  287. crossTeamCollaborators: []
  288. };
  289. // TODO: 从Parse Server加载报价项数据
  290. this.currentQuotationItems = [];
  291. this.showTeamAssignmentModal = true;
  292. console.log('打开团队分配弹窗:', project, '团队数据:', this.projectTeams);
  293. }
  294. // 关闭团队分配弹窗
  295. closeTeamAssignmentModal(): void {
  296. this.showTeamAssignmentModal = false;
  297. this.selectedProject = null;
  298. this.currentTeamAssignment = {
  299. primaryTeamId: null,
  300. quotationAssignments: [],
  301. crossTeamCollaborators: []
  302. };
  303. this.currentQuotationItems = [];
  304. }
  305. // 确认团队分配
  306. confirmTeamAssignment(event: any): void {
  307. console.log('确认团队分配:', event);
  308. // TODO: 保存团队分配数据到Parse Server
  309. this.closeTeamAssignmentModal();
  310. // 重新加载项目列表
  311. this.loadProjects();
  312. }
  313. formatCurrency(amount: number): string {
  314. return new Intl.NumberFormat('zh-CN', {
  315. style: 'currency',
  316. currency: 'CNY',
  317. minimumFractionDigits: 0
  318. }).format(amount);
  319. }
  320. get paginatedProjects(): Project[] {
  321. const startIndex = this.currentPage * this.pageSize;
  322. return this.filteredProjects().slice(startIndex, startIndex + this.pageSize);
  323. }
  324. get totalPages(): number {
  325. return Math.ceil(this.filteredProjects().length / this.pageSize);
  326. }
  327. // 项目状态统计计算属性
  328. get inProgressProjectsCount(): number {
  329. return this.projects().filter(p => p.status === '进行中').length;
  330. }
  331. get completedProjectsCount(): number {
  332. return this.projects().filter(p => p.status === '已完成').length;
  333. }
  334. get pendingProjectsCount(): number {
  335. return this.projects().filter(p => p.status === '待分配').length;
  336. }
  337. // 项目总数
  338. get totalProjectsCount(): number {
  339. return this.projects().length;
  340. }
  341. onPageChange(page: number): void {
  342. this.currentPage = page;
  343. }
  344. // 生成页码数组
  345. getPageNumbers(): number[] {
  346. const totalPages = this.totalPages;
  347. const currentPage = this.currentPage;
  348. const pageNumbers: number[] = [];
  349. // 简单实现:显示所有页码
  350. for (let i = 0; i < totalPages; i++) {
  351. pageNumbers.push(i);
  352. }
  353. return pageNumbers;
  354. }
  355. }