employees.ts 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931
  1. import { Component, OnInit, signal } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { FormsModule } from '@angular/forms';
  4. import { EmployeeService } from '../services/employee.service';
  5. import { DepartmentService } from '../services/department.service';
  6. import { EmployeeInfoPanelComponent, EmployeeFullInfo } from '../../../shared/components/employee-info-panel';
  7. interface Employee {
  8. id: string;
  9. name: string; // 昵称(内部沟通用)
  10. realname?: string; // 真实姓名
  11. mobile: string;
  12. userid: string;
  13. roleName: string;
  14. department: string;
  15. departmentId?: string;
  16. isDisabled?: boolean;
  17. createdAt?: Date;
  18. // 新增展示字段
  19. avatar?: string;
  20. email?: string;
  21. position?: string;
  22. gender?: string;
  23. level?: string;
  24. skills?: string[];
  25. joinDate?: Date | string;
  26. workload?: { currentProjects?: number; completedProjects?: number; averageQuality?: number };
  27. }
  28. interface Department {
  29. id: string;
  30. name: string;
  31. }
  32. @Component({
  33. selector: 'app-employees',
  34. standalone: true,
  35. imports: [CommonModule, FormsModule, EmployeeInfoPanelComponent],
  36. templateUrl: './employees.html',
  37. styleUrls: ['./employees.scss']
  38. })
  39. export class Employees implements OnInit {
  40. // 数据
  41. employees = signal<Employee[]>([]);
  42. departments = signal<Department[]>([]);
  43. loading = signal(false);
  44. // 筛选
  45. keyword = signal('');
  46. roleFilter = signal<string>('all');
  47. // 侧边面板(原有的,保留用于向后兼容)
  48. showPanel = false;
  49. panelMode: 'detail' | 'edit' = 'detail';
  50. currentEmployee: Employee | null = null;
  51. formModel: Partial<Employee> = {};
  52. // 新的员工信息面板
  53. showEmployeeInfoPanel = false;
  54. selectedEmployeeForPanel: EmployeeFullInfo | null = null;
  55. // 统计 - 按身份统计
  56. stats = {
  57. total: signal(0),
  58. service: signal(0), // 客服
  59. designer: signal(0), // 组员(设计师)
  60. leader: signal(0), // 组长
  61. hr: signal(0), // 人事
  62. finance: signal(0) // 财务
  63. };
  64. // 角色列表
  65. roles = ['客服', '组员', '组长', '人事', '财务','管理员'];
  66. constructor(
  67. private employeeService: EmployeeService,
  68. private departmentService: DepartmentService
  69. ) {}
  70. ngOnInit(): void {
  71. this.loadEmployees();
  72. this.loadDepartments();
  73. }
  74. async loadEmployees(): Promise<void> {
  75. this.loading.set(true);
  76. try {
  77. const emps = await this.employeeService.findEmployees();
  78. console.log(`🔍 [Employees] 开始加载 ${emps.length} 个员工的详细信息...`);
  79. const empList: Employee[] = await Promise.all(emps.map(async (e) => {
  80. const json = this.employeeService.toJSON(e);
  81. const data = (e as any).get ? ((e as any).get('data') || {}) : {};
  82. const workload = data.workload || {};
  83. const wxwork = data.wxworkInfo || {};
  84. // 优先级说明:
  85. // 1. name(昵称):优先使用企微昵称 wxwork.name,其次 json.name,最后 data.name
  86. // 2. realname(真实姓名):优先使用用户填写的 data.realname
  87. // 3. mobile(手机号):优先使用企微手机号 wxwork.mobile,其次 data.mobile,最后 json.mobile
  88. const wxworkName = wxwork.name || ''; // 企微昵称
  89. const wxworkMobile = wxwork.mobile || ''; // 企微手机号
  90. const dataMobile = data.mobile || ''; // data字段中的手机号
  91. const jsonMobile = json.mobile || ''; // Parse表字段的手机号
  92. // 手机号优先级:企微手机号 > data.mobile > json.mobile
  93. let finalMobile = wxworkMobile || dataMobile || jsonMobile || '';
  94. // 如果手机号为空或格式不对,尝试从其他字段获取
  95. if (!finalMobile || !/^1[3-9]\d{9}$/.test(finalMobile)) {
  96. // 尝试从 data 中的其他可能字段获取
  97. finalMobile = data.phone || data.telephone || wxwork.telephone || jsonMobile || '';
  98. }
  99. // 🔥 获取员工的实际项目负载数据
  100. let actualWorkload = {
  101. currentProjects: 0,
  102. completedProjects: 0,
  103. averageQuality: 0
  104. };
  105. // 只为设计师(组员)和组长查询项目负载
  106. if (json.roleName === '组员' || json.roleName === '组长') {
  107. try {
  108. console.log(`🔍 [Employees] 开始查询员工项目负载:`, {
  109. 员工姓名: wxworkName || json.name,
  110. 员工ID: json.objectId,
  111. 角色: json.roleName
  112. });
  113. const projectData = await this.employeeService.getEmployeeWorkload(json.objectId);
  114. actualWorkload = {
  115. currentProjects: projectData.currentProjects,
  116. completedProjects: projectData.completedProjects,
  117. averageQuality: workload.averageQuality || 0
  118. };
  119. if (projectData.currentProjects > 0 || projectData.completedProjects > 0) {
  120. console.log(`✅ [Employees] 员工 ${wxworkName || json.name} 项目负载:`, {
  121. 当前项目数: projectData.currentProjects,
  122. 已完成项目数: projectData.completedProjects,
  123. 进行中项目: projectData.ongoingProjects.map(p => `${p.name} (${p.id})`),
  124. 已完成项目: projectData.completedProjectsList.map(p => `${p.name} (${p.id})`)
  125. });
  126. } else {
  127. console.warn(`⚠️ [Employees] 员工 ${wxworkName || json.name} (${json.objectId}) 没有查询到项目数据`);
  128. }
  129. } catch (error) {
  130. console.error(`❌ [Employees] 获取员工 ${json.objectId} 项目负载失败:`, error);
  131. }
  132. }
  133. return {
  134. id: json.objectId,
  135. name: wxworkName || json.name || data.name || '未知', // 优先企微昵称
  136. realname: data.realname || '', // 用户填写的真实姓名
  137. mobile: finalMobile, // 优先企微手机号
  138. userid: json.userid || wxwork.userid || '',
  139. roleName: json.roleName || '未分配',
  140. department: e.get("department")?.get("name") || '未分配',
  141. departmentId: e.get("department")?.id,
  142. isDisabled: json.isDisabled || false,
  143. createdAt: json.createdAt,
  144. avatar: data.avatar || wxwork.avatar || '',
  145. email: data.email || wxwork.email || '',
  146. position: wxwork.position || '',
  147. gender: data.gender || wxwork.gender || '',
  148. level: data.level || '',
  149. skills: Array.isArray(data.skills) ? data.skills : [],
  150. joinDate: data.joinDate || '',
  151. workload: actualWorkload // 使用实际查询的负载数据
  152. };
  153. }));
  154. console.log(`✅ [Employees] 所有员工数据加载完成,共 ${empList.length} 人`);
  155. this.employees.set(empList);
  156. // 更新统计
  157. this.stats.total.set(empList.length);
  158. this.stats.service.set(empList.filter(e => e.roleName === '客服').length);
  159. this.stats.designer.set(empList.filter(e => e.roleName === '组员').length);
  160. this.stats.leader.set(empList.filter(e => e.roleName === '组长').length);
  161. this.stats.hr.set(empList.filter(e => e.roleName === '人事').length);
  162. this.stats.finance.set(empList.filter(e => e.roleName === '财务').length);
  163. } catch (error) {
  164. console.error('加载员工列表失败:', error);
  165. } finally {
  166. this.loading.set(false);
  167. }
  168. }
  169. async loadDepartments(): Promise<void> {
  170. try {
  171. const depts = await this.departmentService.findDepartments();
  172. this.departments.set(
  173. depts.map(d => {
  174. const json = this.departmentService.toJSON(d);
  175. return {
  176. id: json.objectId,
  177. name: json.name
  178. };
  179. })
  180. );
  181. } catch (error) {
  182. console.error('加载部门列表失败:', error);
  183. }
  184. }
  185. get filtered(): Employee[] {
  186. const kw = this.keyword().trim().toLowerCase();
  187. const role = this.roleFilter();
  188. let list = this.employees();
  189. if (role !== 'all') {
  190. list = list.filter(e => e.roleName === role);
  191. }
  192. if (kw) {
  193. list = list.filter(
  194. e =>
  195. (e.name || '').toLowerCase().includes(kw) ||
  196. (e.realname || '').toLowerCase().includes(kw) ||
  197. (e.mobile || '').includes(kw) ||
  198. (e.userid || '').toLowerCase().includes(kw) ||
  199. (e.email || '').toLowerCase().includes(kw) ||
  200. (e.position || '').toLowerCase().includes(kw)
  201. );
  202. }
  203. return list;
  204. }
  205. setRoleFilter(role: string) {
  206. this.roleFilter.set(role);
  207. }
  208. resetFilters() {
  209. this.keyword.set('');
  210. this.roleFilter.set('all');
  211. }
  212. // 查看详情(使用新的员工信息面板)
  213. // ⭐ 优化:数据预加载后再显示面板,避免数据闪烁
  214. async viewEmployee(emp: Employee) {
  215. console.log(`🚀 [Employees] 开始打开员工信息面板: ${emp.realname || emp.name} (${emp.id})`);
  216. // 准备基础数据
  217. const baseData: EmployeeFullInfo = {
  218. id: emp.id,
  219. name: emp.name,
  220. realname: emp.realname,
  221. mobile: emp.mobile,
  222. userid: emp.userid,
  223. roleName: emp.roleName,
  224. department: emp.department,
  225. departmentId: emp.departmentId,
  226. isDisabled: emp.isDisabled,
  227. createdAt: emp.createdAt,
  228. avatar: emp.avatar,
  229. email: emp.email,
  230. position: emp.position,
  231. gender: emp.gender,
  232. level: emp.level,
  233. skills: emp.skills,
  234. joinDate: emp.joinDate,
  235. workload: emp.workload,
  236. currentProjects: 0,
  237. projectData: [],
  238. projectNames: [],
  239. leaveRecords: [],
  240. redMarkExplanation: ''
  241. };
  242. // ⭐ 关键修复:如果是设计师,先加载完整数据再显示面板(避免数据闪烁)
  243. if (emp.roleName === '组员' || emp.roleName === '组长') {
  244. try {
  245. console.log(`🔄 [Employees] 预加载员工 ${emp.id} 的完整数据...`);
  246. // 1️⃣ 加载项目数据
  247. const wl = await this.employeeService.getEmployeeWorkload(emp.id);
  248. console.log(`✅ [Employees] 项目数据加载完成:`, {
  249. currentProjects: wl.currentProjects,
  250. ongoingProjects: wl.ongoingProjects.length,
  251. 项目列表: wl.ongoingProjects.map(p => p.name)
  252. });
  253. // 2️⃣ 准备项目数据(前3个核心项目)
  254. const coreProjects = (wl.ongoingProjects || []).slice(0, 3).map(p => ({
  255. id: p.id,
  256. name: p.name
  257. }));
  258. // 3️⃣ 保存项目数据(用于月份切换)
  259. this.currentEmployeeProjects = wl.ongoingProjects || [];
  260. // 4️⃣ 生成日历数据(使用修复后的算法)
  261. const calendarData = this.buildCalendarData(this.currentEmployeeProjects);
  262. console.log(`📅 [Employees] 日历数据生成完成:`, {
  263. days: calendarData.days.length,
  264. 有项目的天数: calendarData.days.filter(d => d.projectCount > 0).length
  265. });
  266. // 5️⃣ 加载问卷数据
  267. const surveyInfo = await this.loadEmployeeSurvey(emp.id, emp.realname || emp.name);
  268. console.log(`📝 [Employees] 问卷数据加载完成:`, {
  269. completed: surveyInfo.completed,
  270. answers: surveyInfo.data?.answers?.length || 0
  271. });
  272. // 6️⃣ 组装完整数据
  273. this.selectedEmployeeForPanel = {
  274. ...baseData,
  275. currentProjects: wl.currentProjects || 0,
  276. projectData: coreProjects,
  277. projectNames: coreProjects.map(p => p.name),
  278. calendarData: calendarData,
  279. surveyCompleted: surveyInfo.completed,
  280. surveyData: surveyInfo.data,
  281. profileId: surveyInfo.profileId
  282. };
  283. console.log(`🎯 [Employees] 完整数据准备完成,打开面板:`, {
  284. currentProjects: this.selectedEmployeeForPanel.currentProjects,
  285. projectData: this.selectedEmployeeForPanel.projectData?.length,
  286. calendarData: '✅',
  287. surveyData: surveyInfo.completed ? '✅' : '❌'
  288. });
  289. } catch (err) {
  290. console.error(`❌ [Employees] 加载员工数据失败:`, err);
  291. // 失败时使用基础数据
  292. this.selectedEmployeeForPanel = baseData;
  293. alert('加载员工项目数据失败,仅显示基础信息');
  294. }
  295. } else {
  296. // ⭐ 修复:非设计师角色也需要加载问卷数据
  297. console.log(`📦 [Employees] 非设计师角色,加载基础数据和问卷...`);
  298. try {
  299. // 加载问卷数据
  300. const surveyInfo = await this.loadEmployeeSurvey(emp.id, emp.realname || emp.name);
  301. console.log(`📝 [Employees] 问卷数据加载完成:`, {
  302. completed: surveyInfo.completed,
  303. answers: surveyInfo.data?.answers?.length || 0
  304. });
  305. // 组装数据(包含问卷)
  306. this.selectedEmployeeForPanel = {
  307. ...baseData,
  308. surveyCompleted: surveyInfo.completed,
  309. surveyData: surveyInfo.data,
  310. profileId: surveyInfo.profileId
  311. };
  312. console.log(`🎯 [Employees] 非设计师数据准备完成:`, {
  313. surveyData: surveyInfo.completed ? '✅' : '❌'
  314. });
  315. } catch (err) {
  316. console.error(`❌ [Employees] 加载问卷数据失败:`, err);
  317. // 失败时使用基础数据(不包含问卷)
  318. this.selectedEmployeeForPanel = baseData;
  319. }
  320. }
  321. // ⭐ 关键修复:数据准备完成后才显示面板
  322. this.showEmployeeInfoPanel = true;
  323. console.log(`✅ [Employees] 面板已显示`);
  324. }
  325. /**
  326. * 根据项目的整个生命周期生成日历数据(与组长端对齐)
  327. * ⭐ 关键修复:基于项目的 [createdAt, deadline] 范围填充所有天数
  328. * @param projects 项目列表
  329. * @param targetMonth 目标月份(可选,默认为当前月)
  330. */
  331. private buildCalendarData(
  332. projects: Array<{ id: string; name: string; deadline?: any; createdAt?: any }>,
  333. targetMonth?: Date
  334. ): { currentMonth: Date; days: any[] } {
  335. const now = targetMonth || new Date();
  336. const year = now.getFullYear();
  337. const month = now.getMonth(); // 0-11
  338. const firstOfMonth = new Date(year, month, 1);
  339. const lastOfMonth = new Date(year, month + 1, 0);
  340. const firstWeekday = firstOfMonth.getDay(); // 0(日)-6(六)
  341. const daysInMonth = lastOfMonth.getDate();
  342. // 辅助函数:解析日期
  343. const parseDate = (d: any): Date | null => {
  344. if (!d) return null;
  345. if (d instanceof Date) return d;
  346. if (d.toDate && typeof d.toDate === 'function') return d.toDate(); // Parse Date 对象
  347. const t = new Date(d);
  348. return isNaN(t.getTime()) ? null : t;
  349. };
  350. const sameDay = (a: Date, b: Date): boolean => {
  351. return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
  352. };
  353. // ⭐ 计算"今天"和"明天"
  354. const today = new Date();
  355. today.setHours(0, 0, 0, 0);
  356. const tomorrow = new Date(today);
  357. tomorrow.setDate(today.getDate() + 1);
  358. const days: any[] = [];
  359. // 前置填充(上月尾巴),保持从周日开始的网格对齐
  360. for (let i = 0; i < firstWeekday; i++) {
  361. const d = new Date(year, month, 1 - (firstWeekday - i));
  362. days.push({
  363. date: d,
  364. projectCount: 0,
  365. projects: [],
  366. isToday: sameDay(d, today),
  367. isTomorrow: sameDay(d, tomorrow),
  368. isCurrentMonth: false
  369. });
  370. }
  371. // ⭐ 关键修复:本月每一天,找出该天在项目生命周期内的所有项目
  372. for (let day = 1; day <= daysInMonth; day++) {
  373. const date = new Date(year, month, day);
  374. date.setHours(0, 0, 0, 0);
  375. // 找出该日期相关的项目(项目在 [startDate, endDate] 范围内)
  376. const dayProjects = projects.filter(p => {
  377. const createdAt = parseDate((p as any).createdAt);
  378. const deadline = parseDate((p as any).deadline);
  379. // ⭐ 智能处理:如果项目既没有 deadline 也没有 createdAt,则跳过
  380. if (!deadline && !createdAt) {
  381. return false;
  382. }
  383. // ⭐ 智能处理日期范围(与组长端对齐)
  384. let startDate: Date;
  385. let endDate: Date;
  386. if (deadline && createdAt) {
  387. // 情况1:两个日期都有
  388. startDate = new Date(createdAt);
  389. endDate = new Date(deadline);
  390. } else if (deadline) {
  391. // 情况2:只有deadline,往前推30天
  392. startDate = new Date(deadline.getTime() - 30 * 24 * 60 * 60 * 1000);
  393. endDate = new Date(deadline);
  394. } else {
  395. // 情况3:只有createdAt,往后推30天
  396. startDate = new Date(createdAt!);
  397. endDate = new Date(createdAt!.getTime() + 30 * 24 * 60 * 60 * 1000);
  398. }
  399. startDate.setHours(0, 0, 0, 0);
  400. endDate.setHours(0, 0, 0, 0);
  401. // ⭐ 关键:项目在 [startDate, endDate] 范围内的所有天都显示
  402. const inRange = date >= startDate && date <= endDate;
  403. return inRange;
  404. });
  405. days.push({
  406. date,
  407. projectCount: dayProjects.length,
  408. projects: dayProjects.map(p => ({
  409. id: p.id,
  410. name: p.name,
  411. deadline: parseDate((p as any).deadline)
  412. })),
  413. isToday: sameDay(date, today),
  414. isTomorrow: sameDay(date, tomorrow), // ⭐ 标记明天
  415. isCurrentMonth: true
  416. });
  417. }
  418. // 后置填充到 6 行 * 7 列 = 42 格
  419. while (days.length % 7 !== 0) {
  420. const last = days[days.length - 1].date as Date;
  421. const d = new Date(last.getFullYear(), last.getMonth(), last.getDate() + 1);
  422. days.push({
  423. date: d,
  424. projectCount: 0,
  425. projects: [],
  426. isToday: sameDay(d, today),
  427. isTomorrow: sameDay(d, tomorrow),
  428. isCurrentMonth: d.getMonth() === month
  429. });
  430. }
  431. // ⭐ 详细的调试日志
  432. console.log(`📅 [buildCalendarData] 日历生成完成:`, {
  433. 总天数: days.length,
  434. 本月天数: daysInMonth,
  435. 有项目的天数: days.filter(d => d.isCurrentMonth && d.projectCount > 0).length,
  436. 项目总数: projects.length,
  437. 项目详情: projects.map(p => ({
  438. name: p.name,
  439. createdAt: (p as any).createdAt,
  440. deadline: (p as any).deadline
  441. }))
  442. });
  443. // 输出每一天的项目统计(只输出有项目的天)
  444. const daysWithProjects = days.filter(d => d.isCurrentMonth && d.projectCount > 0);
  445. if (daysWithProjects.length > 0) {
  446. console.log(`📅 [buildCalendarData] 有项目的日期:`, daysWithProjects.map(d => ({
  447. 日期: d.date.toISOString().split('T')[0],
  448. 项目数: d.projectCount,
  449. 项目: d.projects.map((p: any) => p.name)
  450. })));
  451. }
  452. return { currentMonth: new Date(year, month, 1), days };
  453. }
  454. /**
  455. * 加载员工问卷数据(与组长端对齐)
  456. */
  457. private async loadEmployeeSurvey(employeeId: string, employeeName: string): Promise<{ completed: boolean; data: any; profileId: string }> {
  458. try {
  459. const Parse = await import('fmode-ng/parse').then(m => m.FmodeParse.with('nova'));
  460. // ⭐ 与组长端完全一致的查询逻辑
  461. // 通过员工名字查找Profile(同时查询 realname 和 name 字段)
  462. const realnameQuery = new Parse.Query('Profile');
  463. realnameQuery.equalTo('realname', employeeName);
  464. const nameQuery = new Parse.Query('Profile');
  465. nameQuery.equalTo('name', employeeName);
  466. // 使用 or 查询
  467. const profileQuery = Parse.Query.or(realnameQuery, nameQuery);
  468. profileQuery.limit(1);
  469. const profileResults = await profileQuery.find();
  470. console.log(`🔍 查找员工 ${employeeName},找到 ${profileResults.length} 个结果`);
  471. if (profileResults.length > 0) {
  472. const profile = profileResults[0];
  473. const profileId = profile.id;
  474. // ⭐ 详细输出 Profile 的所有字段,帮助诊断
  475. console.log(`📋 Profile 详细信息:`, {
  476. id: profileId,
  477. realname: profile.get('realname'),
  478. name: profile.get('name'),
  479. surveyCompleted: profile.get('surveyCompleted')
  480. });
  481. const surveyCompleted = profile.get('surveyCompleted') || false;
  482. console.log(`📋 Profile ID: ${profileId}, surveyCompleted: ${surveyCompleted}`);
  483. // 如果已完成问卷,加载问卷答案
  484. if (surveyCompleted) {
  485. const surveyQuery = new Parse.Query('SurveyLog');
  486. surveyQuery.equalTo('profile', profile.toPointer());
  487. surveyQuery.equalTo('type', 'survey-profile');
  488. surveyQuery.descending('createdAt');
  489. surveyQuery.limit(1);
  490. console.log(`📝 开始查询 SurveyLog,查询条件:`, {
  491. profileId: profileId,
  492. type: 'survey-profile'
  493. });
  494. const surveyResults = await surveyQuery.find();
  495. console.log(`📝 找到 ${surveyResults.length} 条问卷记录`);
  496. // ⭐ 如果没找到 'survey-profile',尝试查询所有类型
  497. if (surveyResults.length === 0) {
  498. console.warn(`⚠️ 未找到 type='survey-profile' 的记录,尝试查询所有类型...`);
  499. const allTypeQuery = new Parse.Query('SurveyLog');
  500. allTypeQuery.equalTo('profile', profile.toPointer());
  501. allTypeQuery.descending('createdAt');
  502. allTypeQuery.limit(5);
  503. const allResults = await allTypeQuery.find();
  504. console.log(`📝 该员工的所有问卷记录 (${allResults.length} 条):`,
  505. allResults.map(s => ({
  506. id: s.id,
  507. type: s.get('type'),
  508. answersCount: s.get('answers')?.length || 0,
  509. createdAt: s.get('createdAt')
  510. }))
  511. );
  512. }
  513. if (surveyResults.length > 0) {
  514. const survey = surveyResults[0];
  515. const surveyData = {
  516. answers: survey.get('answers') || [],
  517. createdAt: survey.get('createdAt'),
  518. updatedAt: survey.get('updatedAt')
  519. };
  520. console.log(`✅ 加载问卷数据成功,共 ${surveyData.answers.length} 道题`);
  521. return {
  522. completed: true,
  523. data: surveyData,
  524. profileId
  525. };
  526. }
  527. }
  528. console.log(`📋 员工 ${employeeName} 问卷状态:`, surveyCompleted ? '已完成' : '未完成');
  529. return {
  530. completed: false,
  531. data: null,
  532. profileId
  533. };
  534. } else {
  535. console.warn(`⚠️ 未找到员工 ${employeeName} 的 Profile`);
  536. return {
  537. completed: false,
  538. data: null,
  539. profileId: ''
  540. };
  541. }
  542. } catch (error) {
  543. console.error(`❌ [loadEmployeeSurvey] 加载员工 ${employeeName} 问卷数据失败:`, error);
  544. return {
  545. completed: false,
  546. data: null,
  547. profileId: ''
  548. };
  549. }
  550. }
  551. // 查看详情(旧版本,保留用于向后兼容)
  552. viewEmployeeOld(emp: Employee) {
  553. this.currentEmployee = emp;
  554. this.panelMode = 'detail';
  555. this.showPanel = true;
  556. }
  557. // 编辑 (员工从企微同步,只能编辑部分字段,不能删除)
  558. editEmployee(emp: Employee) {
  559. this.currentEmployee = emp;
  560. // 复制所有需要编辑的字段
  561. this.formModel = {
  562. name: emp.name, // 昵称
  563. realname: emp.realname, // 真实姓名
  564. mobile: emp.mobile,
  565. userid: emp.userid, // 企微ID只读,但需要显示
  566. roleName: emp.roleName,
  567. departmentId: emp.departmentId,
  568. isDisabled: emp.isDisabled || false
  569. };
  570. this.panelMode = 'edit';
  571. this.showPanel = true;
  572. }
  573. // ⭐ 保存当前员工的项目数据(用于切换月份)
  574. currentEmployeeProjects: Array<{ id: string; name: string; deadline?: any; createdAt?: any }> = [];
  575. // 关闭新的员工信息面板
  576. closeEmployeeInfoPanel() {
  577. this.showEmployeeInfoPanel = false;
  578. this.selectedEmployeeForPanel = null;
  579. this.currentEmployeeProjects = []; // 清空项目数据
  580. }
  581. /**
  582. * 切换日历月份(与组长端对齐)
  583. * @param direction -1=上月, 1=下月
  584. */
  585. onChangeMonth(direction: number): void {
  586. if (!this.selectedEmployeeForPanel?.calendarData) {
  587. console.warn(`⚠️ [onChangeMonth] 日历数据不存在`);
  588. return;
  589. }
  590. console.log(`📅 [onChangeMonth] 切换月份: ${direction > 0 ? '下月' : '上月'}`);
  591. const currentMonth = this.selectedEmployeeForPanel.calendarData.currentMonth;
  592. const newMonth = new Date(currentMonth);
  593. newMonth.setMonth(newMonth.getMonth() + direction);
  594. // 重新生成指定月份的日历数据
  595. const newCalendarData = this.buildCalendarData(this.currentEmployeeProjects, newMonth);
  596. console.log(`📅 [onChangeMonth] 新月份日历生成完成:`, {
  597. 月份: `${newMonth.getFullYear()}年${newMonth.getMonth() + 1}月`,
  598. 有项目的天数: newCalendarData.days.filter(d => d.isCurrentMonth && d.projectCount > 0).length
  599. });
  600. // 更新员工详情中的日历数据
  601. this.selectedEmployeeForPanel = {
  602. ...this.selectedEmployeeForPanel,
  603. calendarData: newCalendarData
  604. };
  605. }
  606. /**
  607. * 处理日历日期点击事件
  608. */
  609. onCalendarDayClick(day: any): void {
  610. console.log(`📅 [onCalendarDayClick] 点击日期:`, {
  611. 日期: day.date,
  612. 项目数: day.projectCount,
  613. 项目列表: day.projects
  614. });
  615. // TODO: 可以显示当天的项目详情弹窗
  616. }
  617. /**
  618. * 处理项目点击事件
  619. */
  620. onProjectClick(projectId: string): void {
  621. console.log(`🔗 [onProjectClick] 点击项目: ${projectId}`);
  622. // TODO: 导航到项目详情页
  623. // this.router.navigate(['/project', projectId]);
  624. }
  625. /**
  626. * 刷新问卷数据
  627. */
  628. async onRefreshSurvey(): Promise<void> {
  629. if (!this.selectedEmployeeForPanel) {
  630. return;
  631. }
  632. console.log(`🔄 [onRefreshSurvey] 刷新问卷数据...`);
  633. try {
  634. const employeeId = this.selectedEmployeeForPanel.id;
  635. const employeeName = this.selectedEmployeeForPanel.realname || this.selectedEmployeeForPanel.name;
  636. const surveyInfo = await this.loadEmployeeSurvey(employeeId, employeeName);
  637. // 更新问卷数据
  638. this.selectedEmployeeForPanel = {
  639. ...this.selectedEmployeeForPanel,
  640. surveyCompleted: surveyInfo.completed,
  641. surveyData: surveyInfo.data,
  642. profileId: surveyInfo.profileId
  643. };
  644. console.log(`✅ [onRefreshSurvey] 问卷数据刷新完成:`, {
  645. completed: surveyInfo.completed,
  646. answersCount: surveyInfo.data?.answers?.length || 0
  647. });
  648. } catch (error) {
  649. console.error(`❌ [onRefreshSurvey] 刷新失败:`, error);
  650. }
  651. }
  652. // 更新员工信息(从新面板触发)
  653. async updateEmployeeInfo(updates: Partial<EmployeeFullInfo>) {
  654. try {
  655. await this.employeeService.updateEmployee(updates.id!, {
  656. name: updates.name,
  657. mobile: updates.mobile,
  658. roleName: updates.roleName,
  659. departmentId: updates.departmentId,
  660. isDisabled: updates.isDisabled,
  661. data: {
  662. realname: updates.realname
  663. }
  664. });
  665. console.log('✅ 员工信息已更新(从新面板)', updates);
  666. // 重新加载员工列表
  667. await this.loadEmployees();
  668. // 关闭面板
  669. this.closeEmployeeInfoPanel();
  670. alert('员工信息更新成功!');
  671. } catch (error) {
  672. console.error('更新员工失败:', error);
  673. alert('更新员工失败,请重试');
  674. }
  675. }
  676. // 关闭面板(旧版本)
  677. closePanel() {
  678. this.showPanel = false;
  679. this.panelMode = 'detail';
  680. this.currentEmployee = null;
  681. this.formModel = {};
  682. }
  683. // 提交编辑
  684. async updateEmployee() {
  685. if (!this.currentEmployee) return;
  686. // 表单验证
  687. if (!this.formModel.name?.trim()) {
  688. alert('请输入员工姓名');
  689. return;
  690. }
  691. if (!this.formModel.mobile?.trim()) {
  692. alert('请输入手机号');
  693. return;
  694. }
  695. // 手机号格式验证
  696. const mobileRegex = /^1[3-9]\d{9}$/;
  697. if (!mobileRegex.test(this.formModel.mobile)) {
  698. alert('请输入正确的手机号格式');
  699. return;
  700. }
  701. if (!this.formModel.roleName) {
  702. alert('请选择员工身份');
  703. return;
  704. }
  705. try {
  706. // 保存所有可编辑字段到后端数据库
  707. await this.employeeService.updateEmployee(this.currentEmployee.id, {
  708. name: this.formModel.name.trim(),
  709. mobile: this.formModel.mobile.trim(),
  710. roleName: this.formModel.roleName,
  711. departmentId: this.formModel.departmentId,
  712. isDisabled: this.formModel.isDisabled || false,
  713. data: {
  714. realname: this.formModel.realname?.trim() || ''
  715. }
  716. });
  717. console.log('✅ 员工信息已保存到Parse数据库', {
  718. id: this.currentEmployee.id,
  719. name: this.formModel.name,
  720. realname: this.formModel.realname,
  721. mobile: this.formModel.mobile,
  722. roleName: this.formModel.roleName,
  723. departmentId: this.formModel.departmentId,
  724. isDisabled: this.formModel.isDisabled
  725. });
  726. await this.loadEmployees();
  727. this.closePanel();
  728. // 显示成功提示
  729. alert('员工信息更新成功!');
  730. } catch (error) {
  731. console.error('更新员工失败:', error);
  732. window?.fmode?.alert('更新员工失败,请重试');
  733. }
  734. }
  735. // 禁用/启用员工(从新面板触发)
  736. async toggleEmployeeFromPanel(emp: EmployeeFullInfo) {
  737. const action = emp.isDisabled ? '启用' : '禁用';
  738. if (!confirm(`确定要${action}员工 "${emp.name}" 吗?`)) {
  739. return;
  740. }
  741. try {
  742. await this.employeeService.toggleEmployee(emp.id, !emp.isDisabled);
  743. await this.loadEmployees();
  744. // 更新面板中的员工信息
  745. if (this.selectedEmployeeForPanel?.id === emp.id) {
  746. this.selectedEmployeeForPanel = {
  747. ...this.selectedEmployeeForPanel,
  748. isDisabled: !emp.isDisabled
  749. };
  750. }
  751. alert(`${action}成功!`);
  752. } catch (error) {
  753. console.error(`${action}员工失败:`, error);
  754. alert(`${action}员工失败,请重试`);
  755. }
  756. }
  757. // 禁用/启用员工
  758. async toggleEmployee(emp: Employee) {
  759. const action = emp.isDisabled ? '启用' : '禁用';
  760. if (!await window?.fmode?.confirm(`确定要${action}员工 "${emp.name}" 吗?`)) {
  761. return;
  762. }
  763. try {
  764. await this.employeeService.toggleEmployee(emp.id, !emp.isDisabled);
  765. await this.loadEmployees();
  766. } catch (error) {
  767. console.error(`${action}员工失败:`, error);
  768. window?.fmode?.alert(`${action}员工失败,请重试`);
  769. }
  770. }
  771. // 导出
  772. exportEmployees() {
  773. const header = ['姓名', '手机号', '企微ID', '身份', '部门', '状态', '创建时间'];
  774. const rows = this.filtered.map(e => [
  775. e.name,
  776. e.mobile,
  777. e.userid,
  778. e.roleName,
  779. e.department,
  780. e.isDisabled ? '已禁用' : '正常',
  781. e.createdAt instanceof Date
  782. ? e.createdAt.toISOString().slice(0, 10)
  783. : String(e.createdAt || '')
  784. ]);
  785. this.downloadCSV('员工列表.csv', [header, ...rows]);
  786. }
  787. private downloadCSV(filename: string, rows: (string | number)[][]) {
  788. const escape = (val: string | number) => {
  789. const s = String(val ?? '');
  790. if (/[",\n]/.test(s)) return '"' + s.replace(/"/g, '""') + '"';
  791. return s;
  792. };
  793. const csv = rows.map(r => r.map(escape).join(',')).join('\n');
  794. const blob = new Blob(['\ufeff', csv], {
  795. type: 'text/csv;charset=utf-8;'
  796. });
  797. const url = URL.createObjectURL(blob);
  798. const a = document.createElement('a');
  799. a.href = url;
  800. a.download = filename;
  801. a.click();
  802. URL.revokeObjectURL(url);
  803. }
  804. }