departments.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. import { Component, OnInit, signal } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { FormsModule } from '@angular/forms';
  4. import { DepartmentService } from '../services/department.service';
  5. import { EmployeeService } from '../services/employee.service';
  6. import { FmodeObject } from 'fmode-ng/core';
  7. interface Department {
  8. id: string;
  9. name: string;
  10. leader: FmodeObject;
  11. leaderName: string;
  12. leaderId?: string;
  13. type: string;
  14. memberCount: number;
  15. createdAt?: Date;
  16. }
  17. interface Employee {
  18. id: string;
  19. name: string;
  20. roleName: string;
  21. }
  22. @Component({
  23. selector: 'app-departments',
  24. standalone: true,
  25. imports: [CommonModule, FormsModule],
  26. templateUrl: './departments.html',
  27. styleUrls: ['./departments.scss']
  28. })
  29. export class Departments implements OnInit {
  30. // 数据
  31. departments = signal<Department[]>([]);
  32. employees = signal<Employee[]>([]); // 可选择的组长列表
  33. loading = signal(false);
  34. // 筛选
  35. keyword = signal('');
  36. // 侧边面板
  37. showPanel = false;
  38. panelMode: 'add' | 'detail' | 'edit' = 'add';
  39. currentDepartment: Department | null = null;
  40. formModel: Partial<Department> = {};
  41. // 统计
  42. total = signal(0);
  43. constructor(
  44. private departmentService: DepartmentService,
  45. private employeeService: EmployeeService
  46. ) {}
  47. ngOnInit(): void {
  48. this.loadDepartments();
  49. this.loadLeaders();
  50. }
  51. async loadDepartments(): Promise<void> {
  52. this.loading.set(true);
  53. try {
  54. const depts = await this.departmentService.findDepartments({
  55. type: 'project'
  56. });
  57. const deptList: Department[] = await Promise.all(
  58. depts.map(async d => {
  59. const json = this.departmentService.toJSON(d);
  60. const memberCount = await this.departmentService.countDepartmentMembers(json.objectId);
  61. return {
  62. id: json.objectId,
  63. name: json.name || '未命名项目组',
  64. leader: d?.get("leader"),
  65. leaderName: d?.get("leader")?.get("name") || '未分配',
  66. leaderId: d?.get("leader").id,
  67. type: json.type || 'project',
  68. memberCount,
  69. createdAt: json.createdAt?.iso || json.createdAt
  70. };
  71. })
  72. );
  73. this.departments.set(deptList);
  74. this.total.set(deptList.length);
  75. } catch (error) {
  76. console.error('加载项目组失败:', error);
  77. } finally {
  78. this.loading.set(false);
  79. }
  80. }
  81. async loadLeaders(): Promise<void> {
  82. try {
  83. // 加载所有可以担任组长的员工(组长角色)
  84. const leaders = await this.employeeService.findEmployees({
  85. roleName: '组长'
  86. });
  87. this.employees.set(
  88. leaders.map(l => {
  89. const json = this.employeeService.toJSON(l);
  90. return {
  91. id: json.objectId,
  92. name: json.name,
  93. roleName: json.roleName
  94. };
  95. })
  96. );
  97. } catch (error) {
  98. console.error('加载组长列表失败:', error);
  99. }
  100. }
  101. get filtered() {
  102. const kw = this.keyword().trim().toLowerCase();
  103. if (!kw) return this.departments();
  104. return this.departments().filter(
  105. d =>
  106. d.name.toLowerCase().includes(kw) ||
  107. d.leaderName.toLowerCase().includes(kw)
  108. );
  109. }
  110. resetFilters() {
  111. this.keyword.set('');
  112. }
  113. // 新建项目组
  114. addDepartment() {
  115. this.formModel = {
  116. name: '',
  117. leaderId: undefined,
  118. type: 'project'
  119. };
  120. this.currentDepartment = null;
  121. this.panelMode = 'add';
  122. this.showPanel = true;
  123. }
  124. // 查看详情
  125. viewDepartment(dept: Department) {
  126. this.currentDepartment = dept;
  127. this.panelMode = 'detail';
  128. this.showPanel = true;
  129. }
  130. // 编辑
  131. editDepartment(dept: Department) {
  132. this.currentDepartment = dept;
  133. this.formModel = { ...dept };
  134. this.panelMode = 'edit';
  135. this.showPanel = true;
  136. }
  137. // 关闭面板
  138. closePanel() {
  139. this.showPanel = false;
  140. this.panelMode = 'add';
  141. this.currentDepartment = null;
  142. this.formModel = {};
  143. }
  144. // 保存新增
  145. async saveDepartment() {
  146. const name = (this.formModel.name || '').trim();
  147. if (!name) {
  148. window?.fmode?.alert('请输入项目组名称');
  149. return;
  150. }
  151. if (!this.formModel.leaderId) {
  152. window?.fmode?.alert('请选择组长');
  153. return;
  154. }
  155. try {
  156. await this.departmentService.createDepartment({
  157. name,
  158. leaderId: this.formModel.leaderId,
  159. type: 'project'
  160. });
  161. await this.loadDepartments();
  162. this.closePanel();
  163. } catch (error) {
  164. console.error('创建项目组失败:', error);
  165. window?.fmode?.alert('创建项目组失败,请重试');
  166. }
  167. }
  168. // 提交编辑
  169. async updateDepartment() {
  170. if (!this.currentDepartment) return;
  171. const name = (this.formModel.name || '').trim();
  172. if (!name) {
  173. window?.fmode?.alert('请输入项目组名称');
  174. return;
  175. }
  176. if (!this.formModel.leaderId) {
  177. window?.fmode?.alert('请选择组长');
  178. return;
  179. }
  180. try {
  181. await this.departmentService.updateDepartment(this.currentDepartment.id, {
  182. name,
  183. leaderId: this.formModel.leaderId
  184. });
  185. await this.loadDepartments();
  186. this.closePanel();
  187. } catch (error) {
  188. console.error('更新项目组失败:', error);
  189. window?.fmode?.alert('更新项目组失败,请重试');
  190. }
  191. }
  192. // 删除
  193. async deleteDepartment(dept: Department) {
  194. if (!await window?.fmode?.confirm(`确定要删除项目组 "${dept.name}" 吗?`)) {
  195. return;
  196. }
  197. try {
  198. await this.departmentService.deleteDepartment(dept.id);
  199. await this.loadDepartments();
  200. } catch (error) {
  201. console.error('删除项目组失败:', error);
  202. window?.fmode?.alert('删除项目组失败,请重试');
  203. }
  204. }
  205. // 导出
  206. exportDepartments() {
  207. const header = ['项目组名称', '组长', '成员数', '创建时间'];
  208. const rows = this.filtered.map(d => [
  209. d.name,
  210. d.leaderName,
  211. String(d.memberCount),
  212. d.createdAt instanceof Date
  213. ? d.createdAt.toISOString().slice(0, 10)
  214. : String(d.createdAt || '')
  215. ]);
  216. this.downloadCSV('项目组列表.csv', [header, ...rows]);
  217. }
  218. private downloadCSV(filename: string, rows: (string | number)[][]) {
  219. const escape = (val: string | number) => {
  220. const s = String(val ?? '');
  221. if (/[",\n]/.test(s)) return '"' + s.replace(/"/g, '""') + '"';
  222. return s;
  223. };
  224. const csv = rows.map(r => r.map(escape).join(',')).join('\n');
  225. const blob = new Blob(['\ufeff', csv], {
  226. type: 'text/csv;charset=utf-8;'
  227. });
  228. const url = URL.createObjectURL(blob);
  229. const a = document.createElement('a');
  230. a.href = url;
  231. a.download = filename;
  232. a.click();
  233. URL.revokeObjectURL(url);
  234. }
  235. }