designer-assignment.component.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { FormsModule } from '@angular/forms';
  4. import { DesignerTeamAssignmentModalComponent } from '../designer-team-assignment-modal/designer-team-assignment-modal.component';
  5. export interface Designer {
  6. id: string;
  7. name: string;
  8. avatar?: string;
  9. teamId: string;
  10. teamName: string;
  11. isTeamLeader: boolean;
  12. status: 'idle' | 'busy' | 'reviewing';
  13. idleDays: number;
  14. recentOrders: number;
  15. lastOrderDate?: string;
  16. reviewDates: string[]; // 对图日期,这些日期不能安排其他工作
  17. workload: number; // 当前工作量 (0-100)
  18. skills: string[]; // 技能标签
  19. isOnStagnantProject?: boolean; // 是否在停滞期项目中
  20. isInStagnantProject: boolean; // 是否处于停滞期项目
  21. availableDates: string[]; // 空闲日期
  22. // 为了兼容团队分配弹窗与客户服务的日历组件,补充以下字段
  23. groupId: string; // 对应 teamId
  24. groupName: string; // 对应 teamName
  25. isLeader: boolean; // 对应 isTeamLeader
  26. currentProjects: number; // 当前项目数量
  27. }
  28. export interface ProjectTeam {
  29. id: string;
  30. name: string;
  31. leaderId: string;
  32. leaderName: string;
  33. members: Designer[];
  34. }
  35. export interface QuotationAssignment {
  36. quotationItemId: string;
  37. quotationItemName: string;
  38. assignedDesigners: string[];
  39. estimatedHours?: number;
  40. }
  41. export interface DesignerAssignmentData {
  42. primaryTeamId: string;
  43. quotationAssignments: QuotationAssignment[];
  44. crossTeamCollaborators: string[]; // 跨组合作的设计师ID
  45. notes?: string;
  46. }
  47. @Component({
  48. selector: 'app-designer-assignment',
  49. standalone: true,
  50. imports: [CommonModule, FormsModule, DesignerTeamAssignmentModalComponent],
  51. templateUrl: './designer-assignment.component.html',
  52. styleUrls: ['./designer-assignment.component.scss']
  53. })
  54. export class DesignerAssignmentComponent implements OnInit {
  55. @Input() quotationItems: any[] = [];
  56. @Input() initialAssignment?: DesignerAssignmentData;
  57. @Output() assignmentChange = new EventEmitter<DesignerAssignmentData>();
  58. @Output() designerClick = new EventEmitter<Designer>();
  59. showTeamAssignmentModal = false; // 控制弹窗显示
  60. showDesignerCalendar = false; // 控制设计师日历显示
  61. selectedDesignerForCalendar: Designer | null = null; // 当前查看日历的设计师
  62. // 模拟数据 - 实际项目中应该从服务获取
  63. projectTeams: ProjectTeam[] = [
  64. {
  65. id: 'team-1',
  66. name: '家装设计组',
  67. leaderId: 'designer-1',
  68. leaderName: '张组长',
  69. members: [
  70. {
  71. id: 'designer-1',
  72. name: '张组长',
  73. teamId: 'team-1',
  74. teamName: '家装设计组',
  75. isTeamLeader: true,
  76. // 为了兼容弹窗与日历组件,补充映射字段
  77. groupId: 'team-1',
  78. groupName: '家装设计组',
  79. isLeader: true,
  80. currentProjects: 3,
  81. status: 'busy',
  82. idleDays: 0,
  83. recentOrders: 3,
  84. lastOrderDate: '2024-01-15',
  85. reviewDates: ['2024-01-20', '2024-01-25'],
  86. workload: 85,
  87. skills: ['家装设计', '软装搭配', '项目管理'],
  88. isOnStagnantProject: false,
  89. isInStagnantProject: false,
  90. availableDates: ['2024-01-22', '2024-01-23', '2024-01-24']
  91. },
  92. {
  93. id: 'designer-2',
  94. name: '李设计师',
  95. teamId: 'team-1',
  96. teamName: '家装设计组',
  97. isTeamLeader: false,
  98. // 为了兼容弹窗与日历组件,补充映射字段
  99. groupId: 'team-1',
  100. groupName: '家装设计组',
  101. isLeader: false,
  102. currentProjects: 2,
  103. status: 'busy',
  104. idleDays: 0,
  105. recentOrders: 1,
  106. lastOrderDate: '2024-01-10',
  107. reviewDates: [],
  108. workload: 30,
  109. skills: ['家装设计', '3D建模'],
  110. isOnStagnantProject: false,
  111. isInStagnantProject: false,
  112. availableDates: ['2024-01-21', '2024-01-22', '2024-01-23', '2024-01-24', '2024-01-25']
  113. },
  114. {
  115. id: 'designer-5',
  116. name: '赵停滞',
  117. teamId: 'team-1',
  118. teamName: '家装设计组',
  119. isTeamLeader: false,
  120. // 为了兼容弹窗与日历组件,补充映射字段
  121. groupId: 'team-1',
  122. groupName: '家装设计组',
  123. isLeader: false,
  124. currentProjects: 4,
  125. status: 'busy',
  126. idleDays: 0,
  127. recentOrders: 1,
  128. lastOrderDate: '2023-12-20',
  129. reviewDates: [],
  130. workload: 90,
  131. skills: ['家装设计'],
  132. isOnStagnantProject: true,
  133. isInStagnantProject: true,
  134. availableDates: []
  135. }
  136. ]
  137. },
  138. {
  139. id: 'team-2',
  140. name: '工装设计组',
  141. leaderId: 'designer-3',
  142. leaderName: '王组长',
  143. members: [
  144. {
  145. id: 'designer-3',
  146. name: '王组长',
  147. teamId: 'team-2',
  148. teamName: '工装设计组',
  149. isTeamLeader: true,
  150. // 为了兼容弹窗与日历组件,补充映射字段
  151. groupId: 'team-2',
  152. groupName: '工装设计组',
  153. isLeader: true,
  154. currentProjects: 2,
  155. status: 'reviewing',
  156. idleDays: 0,
  157. recentOrders: 2,
  158. lastOrderDate: '2024-01-14',
  159. reviewDates: ['2024-01-18', '2024-01-22'],
  160. workload: 70,
  161. skills: ['工装设计', '商业空间', '项目管理'],
  162. isOnStagnantProject: false,
  163. isInStagnantProject: false,
  164. availableDates: ['2024-01-19', '2024-01-20', '2024-01-21']
  165. },
  166. {
  167. id: 'designer-4',
  168. name: '赵设计师',
  169. teamId: 'team-2',
  170. teamName: '工装设计组',
  171. isTeamLeader: false,
  172. // 为了兼容弹窗与日历组件,补充映射字段
  173. groupId: 'team-2',
  174. groupName: '工装设计组',
  175. isLeader: true,
  176. currentProjects: 2,
  177. status: 'reviewing',
  178. idleDays: 0,
  179. recentOrders: 0,
  180. lastOrderDate: '2024-01-03',
  181. reviewDates: [],
  182. workload: 10,
  183. skills: ['工装设计', '效果图制作'],
  184. isOnStagnantProject: false,
  185. isInStagnantProject: false,
  186. availableDates: ['2024-01-21', '2024-01-22', '2024-01-23', '2024-01-24', '2024-01-25', '2024-01-26']
  187. }
  188. ]
  189. }
  190. ];
  191. assignmentData: DesignerAssignmentData = {
  192. primaryTeamId: '',
  193. quotationAssignments: [],
  194. crossTeamCollaborators: [],
  195. notes: ''
  196. };
  197. selectedTeamId = '';
  198. showCrossTeamSelection = false;
  199. availableCrossTeamDesigners: Designer[] = [];
  200. constructor() {}
  201. ngOnInit() {
  202. if (this.initialAssignment) {
  203. this.assignmentData = { ...this.initialAssignment };
  204. this.selectedTeamId = this.assignmentData.primaryTeamId;
  205. }
  206. // 初始化报价分配
  207. this.initializeQuotationAssignments();
  208. }
  209. // 初始化报价分配
  210. initializeQuotationAssignments() {
  211. if (this.quotationItems.length > 0 && this.assignmentData.quotationAssignments.length === 0) {
  212. this.assignmentData.quotationAssignments = this.quotationItems.map(item => ({
  213. quotationItemId: item.id,
  214. quotationItemName: item.name,
  215. assignedDesigners: [],
  216. estimatedHours: 0
  217. }));
  218. }
  219. }
  220. // 选择主要项目组
  221. selectPrimaryTeam(teamId: string) {
  222. this.selectedTeamId = teamId;
  223. this.assignmentData.primaryTeamId = teamId;
  224. // 清空之前的分配
  225. this.assignmentData.quotationAssignments.forEach(assignment => {
  226. assignment.assignedDesigners = [];
  227. });
  228. this.updateAvailableCrossTeamDesigners();
  229. this.emitAssignmentChange();
  230. }
  231. // 获取选中的项目组
  232. getSelectedTeam(): ProjectTeam | undefined {
  233. return this.projectTeams.find(team => team.id === this.selectedTeamId);
  234. }
  235. // 分配设计师到报价项目
  236. assignDesignerToQuotation(quotationId: string, designerId: string) {
  237. const assignment = this.assignmentData.quotationAssignments.find(a => a.quotationItemId === quotationId);
  238. if (assignment) {
  239. if (!assignment.assignedDesigners.includes(designerId)) {
  240. assignment.assignedDesigners.push(designerId);
  241. this.emitAssignmentChange();
  242. }
  243. }
  244. }
  245. // 移除设计师分配
  246. removeDesignerFromQuotation(quotationId: string, designerId: string) {
  247. const assignment = this.assignmentData.quotationAssignments.find(a => a.quotationItemId === quotationId);
  248. if (assignment) {
  249. assignment.assignedDesigners = assignment.assignedDesigners.filter(id => id !== designerId);
  250. this.emitAssignmentChange();
  251. }
  252. }
  253. // 更新可用的跨组合作设计师
  254. updateAvailableCrossTeamDesigners() {
  255. this.availableCrossTeamDesigners = [];
  256. this.projectTeams.forEach(team => {
  257. if (team.id !== this.selectedTeamId) {
  258. this.availableCrossTeamDesigners.push(...team.members);
  259. }
  260. });
  261. }
  262. // 添加跨组合作设计师
  263. addCrossTeamCollaborator(designerId: string) {
  264. if (!this.assignmentData.crossTeamCollaborators.includes(designerId)) {
  265. this.assignmentData.crossTeamCollaborators.push(designerId);
  266. this.emitAssignmentChange();
  267. }
  268. }
  269. // 移除跨组合作设计师
  270. removeCrossTeamCollaborator(designerId: string) {
  271. this.assignmentData.crossTeamCollaborators = this.assignmentData.crossTeamCollaborators.filter(id => id !== designerId);
  272. this.emitAssignmentChange();
  273. }
  274. // 获取设计师信息
  275. getDesignerById(designerId: string): Designer | undefined {
  276. for (const team of this.projectTeams) {
  277. const designer = team.members.find(d => d.id === designerId);
  278. if (designer) return designer;
  279. }
  280. return undefined;
  281. }
  282. // 获取设计师状态颜色
  283. getDesignerStatusColor(status: string): string {
  284. switch (status) {
  285. case 'idle': return '#52c41a';
  286. case 'busy': return '#faad14';
  287. case 'reviewing': return '#1890ff';
  288. default: return '#d9d9d9';
  289. }
  290. }
  291. // 获取设计师状态文本
  292. getDesignerStatusText(status: string): string {
  293. switch (status) {
  294. case 'idle': return '空闲';
  295. case 'busy': return '忙碌';
  296. case 'reviewing': return '对图中';
  297. default: return '未知';
  298. }
  299. }
  300. // 获取工作量状态
  301. getWorkloadStatus(workload: number): 'low' | 'medium' | 'high' {
  302. if (workload < 30) return 'low';
  303. if (workload < 70) return 'medium';
  304. return 'high';
  305. }
  306. // 点击设计师
  307. onDesignerClick(designer: Designer) {
  308. this.designerClick.emit(designer);
  309. }
  310. // 发送分配变化事件
  311. emitAssignmentChange() {
  312. this.assignmentChange.emit({ ...this.assignmentData });
  313. }
  314. // 自动分配建议
  315. getAutoAssignmentSuggestion(): string {
  316. const selectedTeam = this.getSelectedTeam();
  317. if (!selectedTeam) return '';
  318. const idleDesigners = selectedTeam.members.filter(d => d.status === 'idle' && d.workload < 50);
  319. const busyDesigners = selectedTeam.members.filter(d => d.workload >= 70);
  320. let suggestion = '';
  321. if (idleDesigners.length > 0) {
  322. suggestion += `建议优先分配给空闲设计师:${idleDesigners.map(d => d.name).join('、')}。`;
  323. }
  324. if (busyDesigners.length > 0) {
  325. suggestion += `注意:${busyDesigners.map(d => d.name).join('、')} 工作量较重。`;
  326. }
  327. return suggestion;
  328. }
  329. // 检查是否有冲突的对图日期
  330. hasReviewDateConflict(designerId: string, projectStartDate?: string): boolean {
  331. const designer = this.getDesignerById(designerId);
  332. if (!designer || !projectStartDate) return false;
  333. // 简单的日期冲突检查逻辑
  334. const startDate = new Date(projectStartDate);
  335. const endDate = new Date(startDate.getTime() + 30 * 24 * 60 * 60 * 1000); // 假设项目周期30天
  336. return designer.reviewDates.some(reviewDate => {
  337. const review = new Date(reviewDate);
  338. return review >= startDate && review <= endDate;
  339. });
  340. }
  341. // 获取团队空闲人数的安全方法
  342. getIdleCount(team: ProjectTeam): number {
  343. if (!team || !team.members) return 0;
  344. return team.members.filter(m => m && m.status === 'idle').length;
  345. }
  346. // 获取团队平均工作量
  347. getAverageWorkload(team: ProjectTeam): number {
  348. if (team.members.length === 0) return 0;
  349. const totalWorkload = team.members.reduce((sum, member) => sum + member.workload, 0);
  350. return Math.round(totalWorkload / team.members.length);
  351. }
  352. // 获取工作量样式类
  353. getWorkloadClass(workload: number): string {
  354. if (workload < 50) return 'low';
  355. if (workload < 80) return 'medium';
  356. return 'high';
  357. }
  358. // 打开设计师组分配弹窗
  359. openTeamAssignmentModal(): void {
  360. this.showTeamAssignmentModal = true;
  361. }
  362. // 关闭设计师组分配弹窗
  363. closeTeamAssignmentModal(): void {
  364. this.showTeamAssignmentModal = false;
  365. }
  366. // 显示设计师详细日历
  367. showDesignerDetailCalendar(designer: Designer): void {
  368. this.selectedDesignerForCalendar = designer;
  369. this.showDesignerCalendar = true;
  370. }
  371. // 关闭设计师详细日历
  372. closeDesignerCalendar(): void {
  373. this.showDesignerCalendar = false;
  374. this.selectedDesignerForCalendar = null;
  375. }
  376. // 过滤停滞期项目的设计师
  377. filterStagnantDesigners(designers: Designer[]): Designer[] {
  378. return designers.filter(designer => !designer.isOnStagnantProject);
  379. }
  380. // 获取可用的设计师(过滤停滞期项目)
  381. getAvailableDesigners(team: ProjectTeam): Designer[] {
  382. return this.filterStagnantDesigners(team.members);
  383. }
  384. // 处理弹窗中的设计师分配确认
  385. onModalAssignmentConfirm(assignmentResult: any): void {
  386. // 更新分配数据
  387. this.selectedTeamId = assignmentResult.primaryTeamId;
  388. this.assignmentData.primaryTeamId = assignmentResult.primaryTeamId;
  389. this.assignmentData.crossTeamCollaborators = assignmentResult.crossTeamCollaborators;
  390. // 更新报价项目分配
  391. if (assignmentResult.quotationAssignments) {
  392. this.assignmentData.quotationAssignments = assignmentResult.quotationAssignments;
  393. }
  394. // 关闭弹窗
  395. this.closeTeamAssignmentModal();
  396. // 触发变更事件
  397. this.emitAssignmentChange();
  398. }
  399. // 获取总分配设计师数量
  400. getTotalAssignedDesigners(): number {
  401. return this.assignmentData.quotationAssignments.reduce((total, assignment) => total + assignment.assignedDesigners.length, 0);
  402. }
  403. }