team-assign.component.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598
  1. import { Component, Input, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { FormsModule } from '@angular/forms';
  4. import { FmodeObject, FmodeParse } from 'fmode-ng/parse';
  5. import { ProductSpaceService, Project } from '../../services/product-space.service';
  6. import { DesignerTeamAssignmentModalComponent } from '../../../../app/pages/designer/project-detail/components/designer-team-assignment-modal/designer-team-assignment-modal.component';
  7. import type {
  8. ProjectTeam,
  9. Designer,
  10. SpaceScene,
  11. DesignerAssignmentResult
  12. } from '../../../../app/pages/designer/project-detail/components/designer-team-assignment-modal/designer-team-assignment-modal.component';
  13. const Parse = FmodeParse.with('nova');
  14. @Component({
  15. selector: 'app-team-assign',
  16. standalone: true,
  17. imports: [CommonModule, FormsModule, DesignerTeamAssignmentModalComponent],
  18. templateUrl: './team-assign.component.html',
  19. styleUrls: ['./team-assign.component.scss'],
  20. changeDetection: ChangeDetectionStrategy.OnPush
  21. })
  22. export class TeamAssignComponent implements OnInit {
  23. @Input() project: FmodeObject | null = null;
  24. @Input() canEdit: boolean = true; // 可选:未传入时默认允许编辑
  25. @Input() currentUser: FmodeObject | null = null; // 可选:未传入时为 null
  26. // 项目组(Department)列表
  27. departments: FmodeObject[] = [];
  28. selectedDepartment: FmodeObject | null = null;
  29. // 项目组成员(Profile)列表
  30. departmentMembers: FmodeObject[] = [];
  31. selectedDesigner: FmodeObject | null = null;
  32. // 已分配的项目团队成员
  33. projectTeams: FmodeObject[] = [];
  34. // 设计师分配对话框(旧的,保留以兼容)
  35. showAssignDialog: boolean = false;
  36. assigningDesigner: FmodeObject | null = null;
  37. selectedSpaces: string[] = [];
  38. editingTeam: FmodeObject | null = null; // 当前正在编辑的团队对象
  39. // 统一的设计师分配弹窗
  40. showDesignerModal: boolean = false;
  41. modalProjectTeams: ProjectTeam[] = [];
  42. modalSpaceScenes: SpaceScene[] = [];
  43. modalSelectedTeamId: string = '';
  44. // 加载状态
  45. loadingMembers: boolean = false;
  46. loadingTeams: boolean = false;
  47. loadingSpaces: boolean = false;
  48. saving: boolean = false;
  49. // 空间数据
  50. projectSpaces: Project[] = [];
  51. constructor(
  52. private productSpaceService: ProductSpaceService,
  53. private cdr: ChangeDetectorRef
  54. ) {}
  55. async ngOnInit() {
  56. await this.loadData();
  57. }
  58. async loadData() {
  59. if (!this.project) return;
  60. try {
  61. // 初始化已选择的项目组与设计师(若项目已有)
  62. const department = this.project.get('department');
  63. if (department) {
  64. this.selectedDepartment = department;
  65. await this.loadDepartmentMembers(department);
  66. }
  67. const assignee = this.project.get('assignee');
  68. if (assignee) {
  69. this.selectedDesigner = assignee;
  70. }
  71. // 加载项目组列表
  72. const deptQuery = new Parse.Query('Department');
  73. deptQuery.include('leader');
  74. deptQuery.equalTo('type', 'project');
  75. deptQuery.equalTo('company', localStorage.getItem('company'));
  76. deptQuery.notEqualTo('isDeleted', true);
  77. deptQuery.ascending('name');
  78. this.departments = await deptQuery.find();
  79. console.log("this.departments",this.departments)
  80. // 加载项目团队
  81. await this.loadProjectTeams();
  82. // 加载项目空间
  83. await this.loadProjectSpaces();
  84. } catch (err) {
  85. console.error('加载团队分配数据失败:', err);
  86. } finally {
  87. this.cdr.markForCheck();
  88. }
  89. }
  90. async loadProjectSpaces(): Promise<void> {
  91. if (!this.project) return;
  92. try {
  93. this.loadingSpaces = true;
  94. const projectId = this.project.id || '';
  95. this.projectSpaces = await this.productSpaceService.getProjectProductSpaces(projectId);
  96. } catch (err) {
  97. console.error('加载项目空间失败:', err);
  98. } finally {
  99. this.loadingSpaces = false;
  100. this.cdr.markForCheck();
  101. }
  102. }
  103. async loadProjectTeams() {
  104. if (!this.project) return;
  105. try {
  106. this.loadingTeams = true;
  107. const query = new Parse.Query('ProjectTeam');
  108. query.equalTo('project', this.project.toPointer());
  109. query.include('profile');
  110. query.notEqualTo('isDeleted', true);
  111. this.projectTeams = await query.find();
  112. } catch (err) {
  113. console.error('加载项目团队失败:', err);
  114. } finally {
  115. this.loadingTeams = false;
  116. }
  117. }
  118. async selectDepartment(department: FmodeObject) {
  119. this.selectedDepartment = department;
  120. this.selectedDesigner = null;
  121. this.departmentMembers = [];
  122. // ✅ 自动设置组长为项目负责人
  123. const leader = department.get('leader');
  124. if (leader && this.project) {
  125. try {
  126. // 更新项目的assignee字段为组长
  127. this.project.set('assignee', leader);
  128. this.project.set('department', department);
  129. await this.project.save();
  130. console.log('✅ 项目负责人已设置为组长:', leader.get('name'));
  131. // 触发界面更新
  132. this.cdr.markForCheck();
  133. } catch (error) {
  134. console.error('❌ 设置项目负责人失败:', error);
  135. }
  136. }
  137. await this.loadDepartmentMembers(department);
  138. }
  139. async loadDepartmentMembers(department: FmodeObject) {
  140. const departmentId = department?.id;
  141. if (!departmentId) return [];
  142. try {
  143. this.loadingMembers = true;
  144. const query = new Parse.Query('Profile');
  145. query.equalTo('department', departmentId);
  146. query.equalTo('roleName', '组员');
  147. query.notEqualTo('isDeleted', true);
  148. query.ascending('name');
  149. this.departmentMembers = await query.find();
  150. // 将组长置顶展示
  151. const leader = department?.get('leader');
  152. if (leader) {
  153. this.departmentMembers.unshift(leader);
  154. }
  155. this.loadingMembers = false;
  156. } catch (err) {
  157. console.error('加载项目组成员失败:', err);
  158. } finally {
  159. this.loadingMembers = false;
  160. }
  161. this.cdr.detectChanges()
  162. return this.departmentMembers;
  163. }
  164. selectDesigner(designer: FmodeObject) {
  165. // 检查是否已分配
  166. const isAssigned = this.projectTeams.some(team => team.get('profile')?.id === designer.id);
  167. if (isAssigned) {
  168. alert('该设计师已分配到此项目');
  169. return;
  170. }
  171. this.assigningDesigner = designer;
  172. this.selectedSpaces = [];
  173. // 如果只有一个空间,默认选中
  174. if (this.projectSpaces.length === 1) {
  175. const only = this.projectSpaces[0];
  176. this.selectedSpaces = [only.name];
  177. }
  178. this.showAssignDialog = true;
  179. }
  180. editAssignedDesigner(team: FmodeObject) {
  181. const designer = team.get('profile');
  182. if (!designer) return;
  183. this.assigningDesigner = designer;
  184. this.editingTeam = team;
  185. const currentSpaces = team.get('data')?.spaces || [];
  186. this.selectedSpaces = [...currentSpaces];
  187. this.showAssignDialog = true;
  188. }
  189. toggleSpaceSelection(spaceName: string) {
  190. const index = this.selectedSpaces.indexOf(spaceName);
  191. if (index > -1) {
  192. this.selectedSpaces.splice(index, 1);
  193. } else {
  194. this.selectedSpaces.push(spaceName);
  195. }
  196. }
  197. async confirmAssignDesigner() {
  198. if (!this.assigningDesigner || !this.project) return;
  199. if (this.selectedSpaces.length === 0) {
  200. alert('请至少选择一个空间场景');
  201. return;
  202. }
  203. try {
  204. this.saving = true;
  205. if (this.editingTeam) {
  206. // 更新现有团队成员的空间分配
  207. const data = this.editingTeam.get('data') || {};
  208. data.spaces = this.selectedSpaces;
  209. data.updatedAt = new Date();
  210. data.updatedBy = this.currentUser?.id;
  211. this.editingTeam.set('data', data);
  212. await this.editingTeam.save();
  213. alert('更新成功');
  214. } else {
  215. // 创建新的 ProjectTeam
  216. const ProjectTeam = Parse.Object.extend('ProjectTeam');
  217. const team = new ProjectTeam();
  218. team.set('project', this.project.toPointer());
  219. team.set('profile', this.assigningDesigner.toPointer());
  220. team.set('department', this.assigningDesigner.get("department"));
  221. team.set('role', '组员');
  222. team.set('data', {
  223. spaces: this.selectedSpaces,
  224. assignedAt: new Date(),
  225. assignedBy: this.currentUser?.id
  226. });
  227. await team.save();
  228. // 加入群聊(静默执行)
  229. await this.addMemberToGroupChat(this.assigningDesigner.get('userId'));
  230. alert('分配成功');
  231. }
  232. await this.loadProjectTeams();
  233. this.showAssignDialog = false;
  234. this.assigningDesigner = null;
  235. this.selectedSpaces = [];
  236. this.editingTeam = null;
  237. } catch (err) {
  238. console.error(this.editingTeam ? '更新失败:' : '分配设计师失败:', err);
  239. alert(this.editingTeam ? '更新失败' : '分配失败');
  240. } finally {
  241. this.saving = false;
  242. }
  243. }
  244. cancelAssignDialog() {
  245. this.showAssignDialog = false;
  246. this.assigningDesigner = null;
  247. this.selectedSpaces = [];
  248. this.editingTeam = null;
  249. }
  250. async addMemberToGroupChat(userId: string) {
  251. if (!userId) return;
  252. try {
  253. const groupChat = (this as any).groupChat;
  254. if (!groupChat) return;
  255. const chatId = groupChat.get('chat_id');
  256. if (!chatId) return;
  257. if (typeof (window as any).ww !== 'undefined') {
  258. await (window as any).ww.updateEnterpriseChat({
  259. chatId: chatId,
  260. userIdsToAdd: [userId]
  261. });
  262. }
  263. } catch (err) {
  264. console.warn('添加群成员失败:', err);
  265. }
  266. }
  267. async confirmDeleteMember() {
  268. if (!this.editingTeam) return;
  269. const ok = window.confirm('确定要删除该成员的项目分配吗?');
  270. if (!ok) return;
  271. try {
  272. this.saving = true;
  273. // 软删除 ProjectTeam
  274. this.editingTeam.set('isDeleted', true);
  275. const data = this.editingTeam.get('data') || {};
  276. data.deletedAt = new Date();
  277. data.deletedBy = this.currentUser?.id;
  278. this.editingTeam.set('data', data);
  279. await this.editingTeam.save();
  280. // 从群聊移除(静默尝试)
  281. const profile = this.editingTeam.get('profile');
  282. const userId = profile?.get?.('userId');
  283. if (userId) {
  284. await this.removeMemberFromGroupChat(userId);
  285. }
  286. alert('成员已删除');
  287. // 刷新列表并收起弹窗
  288. await this.loadProjectTeams();
  289. this.showAssignDialog = false;
  290. this.assigningDesigner = null;
  291. this.selectedSpaces = [];
  292. this.editingTeam = null;
  293. } catch (err) {
  294. console.error('删除成员失败:', err);
  295. alert('删除失败');
  296. } finally {
  297. this.saving = false;
  298. this.cdr.markForCheck();
  299. }
  300. }
  301. async removeMemberFromGroupChat(userId: string) {
  302. if (!userId) return;
  303. try {
  304. const groupChat = (this as any).groupChat;
  305. if (!groupChat) return;
  306. const chatId = groupChat.get('chat_id');
  307. if (!chatId) return;
  308. if (typeof (window as any).ww !== 'undefined') {
  309. await (window as any).ww.updateEnterpriseChat({
  310. chatId: chatId,
  311. userIdsToRemove: [userId]
  312. });
  313. }
  314. } catch (err) {
  315. console.warn('移除群成员失败或不支持:', err);
  316. }
  317. }
  318. getMemberSpaces(team: FmodeObject): string {
  319. const spaces = team.get('data')?.spaces || [];
  320. return spaces.join('、') || '未分配';
  321. }
  322. getDesignerWorkload(designer: FmodeObject): string {
  323. return '3个项目';
  324. }
  325. /**
  326. * 移除团队成员
  327. */
  328. async removeMember(team: FmodeObject) {
  329. if (!confirm(`确定要移除 ${team.get('profile')?.get('name')} 吗?`)) {
  330. return;
  331. }
  332. try {
  333. this.saving = true;
  334. // 删除ProjectTeam记录
  335. await team.destroy();
  336. // 重新加载项目团队
  337. await this.loadProjectTeams();
  338. this.cdr.markForCheck();
  339. } catch (err) {
  340. console.error('移除成员失败:', err);
  341. alert('移除失败');
  342. } finally {
  343. this.saving = false;
  344. this.cdr.markForCheck();
  345. }
  346. }
  347. // ===== 统一设计师分配弹窗相关方法 =====
  348. /**
  349. * 打开统一的设计师分配弹窗
  350. */
  351. openDesignerAssignmentModal() {
  352. // 设置当前选中的项目组
  353. // 项目组数据和空间数据都由弹窗自己加载真实数据
  354. this.modalSelectedTeamId = this.selectedDepartment?.id || '';
  355. this.showDesignerModal = true;
  356. this.cdr.markForCheck();
  357. }
  358. /**
  359. * 将Parse的Department对象转换为ProjectTeam
  360. */
  361. private convertToProjectTeam(dept: FmodeObject): ProjectTeam {
  362. const leader = dept.get('leader');
  363. const members = this.departments.find(d => d.id === dept.id)?.get('members') || [];
  364. return {
  365. id: dept.id,
  366. name: dept.get('name') || '',
  367. leaderId: leader?.id || '',
  368. leaderName: leader?.get('name') || '未指定',
  369. description: dept.get('description') || '',
  370. members: this.convertDepartmentMembers(dept.id)
  371. };
  372. }
  373. /**
  374. * 转换项目组成员为Designer格式
  375. */
  376. private convertDepartmentMembers(deptId: string): Designer[] {
  377. // 如果是当前选中的项目组,使用已加载的成员数据
  378. if (this.selectedDepartment?.id === deptId && this.departmentMembers.length > 0) {
  379. return this.departmentMembers
  380. .filter(member => member?.get)
  381. .map(member => this.convertToDesigner(member, deptId));
  382. }
  383. return [];
  384. }
  385. /**
  386. * 将Parse的Profile对象转换为Designer
  387. */
  388. private convertToDesigner(profile: FmodeObject, teamId: string): Designer {
  389. const data = profile.get('data') || {};
  390. const dept = this.departments.find(d => d.id === teamId);
  391. return {
  392. id: profile.id,
  393. name: profile.get('name') || '',
  394. avatar: data.avatar,
  395. teamId: teamId,
  396. teamName: dept?.get('name') || '',
  397. isTeamLeader: false, // 可以根据实际情况判断
  398. status: 'idle', // 默认空闲,可以根据实际工作量判断
  399. idleDays: 0,
  400. recentOrders: 0,
  401. lastOrderDate: undefined,
  402. reviewDates: [],
  403. workload: 0,
  404. skills: data.skills || [],
  405. isInStagnantProject: false,
  406. availableDates: [],
  407. groupId: teamId,
  408. groupName: dept?.get('name') || '',
  409. isLeader: false,
  410. currentProjects: 0
  411. };
  412. }
  413. /**
  414. * 关闭设计师分配弹窗
  415. */
  416. closeDesignerModal() {
  417. this.showDesignerModal = false;
  418. this.cdr.markForCheck();
  419. }
  420. /**
  421. * 确认设计师分配
  422. */
  423. async handleDesignerAssignment(result: DesignerAssignmentResult) {
  424. console.log('设计师分配结果:', result);
  425. try {
  426. this.saving = true;
  427. // 保存选中的设计师到项目团队
  428. for (const designer of result.selectedDesigners) {
  429. // 查找该设计师负责的空间
  430. const spaceAssignment = result.spaceAssignments.find(
  431. sa => sa.designerId === designer.id
  432. );
  433. await this.saveDesignerToTeam(designer, spaceAssignment?.spaceIds || []);
  434. }
  435. // 保存跨组合作者
  436. for (const collaborator of result.crossTeamCollaborators) {
  437. const spaceAssignment = result.spaceAssignments.find(
  438. sa => sa.designerId === collaborator.id
  439. );
  440. await this.saveDesignerToTeam(collaborator, spaceAssignment?.spaceIds || [], true);
  441. }
  442. // 重新加载项目团队数据
  443. await this.loadProjectTeams();
  444. // 关闭弹窗
  445. this.closeDesignerModal();
  446. alert('设计师分配成功!');
  447. } catch (err) {
  448. console.error('保存设计师分配失败:', err);
  449. alert('保存失败,请重试');
  450. } finally {
  451. this.saving = false;
  452. this.cdr.markForCheck();
  453. }
  454. }
  455. /**
  456. * 保存设计师到项目团队
  457. */
  458. private async saveDesignerToTeam(
  459. designer: Designer,
  460. spaceIds: string[],
  461. isCrossTeam: boolean = false
  462. ): Promise<void> {
  463. if (!this.project) return;
  464. // 查找对应的Profile对象
  465. const profileQuery = new Parse.Query('Profile');
  466. profileQuery.equalTo('objectId', designer.id);
  467. const profile = await profileQuery.first();
  468. if (!profile) {
  469. console.error('未找到设计师Profile:', designer.id);
  470. return;
  471. }
  472. // 查找是否已存在团队记录
  473. const existingTeamQuery = new Parse.Query('ProjectTeam');
  474. existingTeamQuery.equalTo('project', this.project);
  475. existingTeamQuery.equalTo('profile', profile);
  476. let teamObj = await existingTeamQuery.first();
  477. if (!teamObj) {
  478. // 创建新的团队记录
  479. const ProjectTeam = Parse.Object.extend('ProjectTeam');
  480. teamObj = new ProjectTeam();
  481. teamObj.set('project', this.project);
  482. teamObj.set('profile', profile);
  483. teamObj.set('role', 'designer');
  484. }
  485. // 转换空间ID为空间名称
  486. const spaceNames = spaceIds.map(id => {
  487. const space = this.projectSpaces.find(s => s.id === id);
  488. return space?.name || '';
  489. }).filter(name => name !== '');
  490. // 保存空间分配信息
  491. teamObj.set('data', {
  492. ...teamObj.get('data'),
  493. spaces: spaceNames,
  494. isCrossTeam: isCrossTeam,
  495. assignedAt: new Date().toISOString()
  496. });
  497. await teamObj.save();
  498. }
  499. }