attendance.html 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. <div class="attendance-container hr-page">
  2. <header class="page-header">
  3. <h1>考勤统计</h1>
  4. <p class="page-description">管理和统计员工考勤数据,支持多维度查看和分析</p>
  5. </header>
  6. <!-- 时间维度切换栏 -->
  7. <div class="time-dimension-bar">
  8. <div class="date-navigation">
  9. <button mat-icon-button (click)="navigateDate('prev')" class="nav-btn">
  10. <mat-icon>chevron_left</mat-icon>
  11. </button>
  12. <div class="current-date" (click)="selectedView.set('month')">
  13. {{ selectedDate().getFullYear() }}年{{ selectedDate().getMonth() + 1 }}月
  14. <mat-icon class="calendar-icon">calendar_today</mat-icon>
  15. </div>
  16. <button mat-icon-button (click)="navigateDate('next')" class="nav-btn">
  17. <mat-icon>chevron_right</mat-icon>
  18. </button>
  19. </div>
  20. <div class="view-tabs">
  21. <button
  22. mat-raised-button
  23. [class.active]="selectedView() === 'day'"
  24. (click)="switchView('day')"
  25. class="view-btn"
  26. >
  27. </button>
  28. <button
  29. mat-raised-button
  30. [class.active]="selectedView() === 'week'"
  31. (click)="switchView('week')"
  32. class="view-btn"
  33. >
  34. </button>
  35. <button
  36. mat-raised-button
  37. [class.active]="selectedView() === 'month'"
  38. (click)="switchView('month')"
  39. class="view-btn"
  40. >
  41. </button>
  42. </div>
  43. <div class="action-buttons">
  44. <button
  45. mat-raised-button
  46. [class.active]="!showGanttView()"
  47. (click)="toggleView()"
  48. class="view-btn">
  49. 考勤视图
  50. </button>
  51. <button
  52. mat-raised-button
  53. [class.active]="showGanttView()"
  54. (click)="toggleView()"
  55. class="view-btn">
  56. 任务视图
  57. </button>
  58. <button
  59. mat-raised-button
  60. (click)="exportAttendanceData()"
  61. class="export-btn">
  62. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  63. <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
  64. <polyline points="7 10 12 15 17 10"></polyline>
  65. <line x1="12" y1="15" x2="12" y2="3"></line>
  66. </svg>
  67. 导出数据
  68. </button>
  69. </div>
  70. </div>
  71. <!-- 主内容区 -->
  72. <div class="main-content">
  73. <!-- 甘特图视图切换按钮 -->
  74. <div class="view-toggle-buttons">
  75. <button
  76. mat-raised-button
  77. [class.active]="!showGanttView()"
  78. (click)="toggleView('attendance')"
  79. class="view-btn">
  80. 考勤视图
  81. </button>
  82. <button
  83. mat-raised-button
  84. [class.active]="showGanttView()"
  85. (click)="toggleView('task')"
  86. class="view-btn">
  87. 任务视图
  88. </button>
  89. <button
  90. mat-raised-button
  91. (click)="exportAttendanceData()"
  92. class="export-btn">
  93. <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
  94. <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
  95. <polyline points="7 10 12 15 17 10"></polyline>
  96. <line x1="12" y1="15" x2="12" y2="3"></line>
  97. </svg>
  98. 导出数据
  99. </button>
  100. </div>
  101. @if (!showGanttView()) {
  102. <!-- 左侧:考勤日历 -->
  103. <div class="calendar-section">
  104. <div class="section-header">
  105. <h2>考勤日历</h2>
  106. </div>
  107. <!-- 日历表头 -->
  108. <div class="calendar-header">
  109. @for (i of [0, 1, 2, 3, 4, 5, 6]; track i) {
  110. <div class="weekday">
  111. {{ getWeekdayName(i) }}
  112. </div>
  113. }
  114. </div>
  115. <!-- 日历格子 -->
  116. <div class="calendar-grid">
  117. @for (day of getCalendarDays(); track day.date) {
  118. <div
  119. class="calendar-day"
  120. [class.current-month]="day.currentMonth"
  121. [class.other-month]="!day.currentMonth"
  122. [class.today]="isToday(day.date)"
  123. [class.has-attendance]="day.attendance"
  124. [class.absent]="day.attendance && day.attendance.status === '旷工'"
  125. [class.late]="day.attendance && day.attendance.status === '迟到'"
  126. [class.early]="day.attendance && day.attendance.status === '早退'"
  127. [class.leave]="day.attendance && day.attendance.status === '请假'"
  128. [class.normal]="day.attendance && day.attendance.status === '正常'"
  129. (click)="selectedDate.set(day.date)"
  130. matTooltip="{{ getDayTooltip(day) }}"
  131. matTooltipPosition="above"
  132. >
  133. <span class="day-number">{{ day.dayOfMonth }}</span>
  134. @if (day.attendance) {
  135. <div class="attendance-indicator"></div>
  136. }
  137. </div>
  138. }
  139. </div>
  140. <!-- 图例 -->
  141. <div class="calendar-legend">
  142. <div class="legend-item">
  143. <div class="legend-dot normal"></div>
  144. <span>正常</span>
  145. </div>
  146. <div class="legend-item">
  147. <div class="legend-dot late"></div>
  148. <span>迟到</span>
  149. </div>
  150. <div class="legend-item">
  151. <div class="legend-dot early"></div>
  152. <span>早退</span>
  153. </div>
  154. <div class="legend-item">
  155. <div class="legend-dot absent"></div>
  156. <span>旷工</span>
  157. </div>
  158. <div class="legend-item">
  159. <div class="legend-dot leave"></div>
  160. <span>请假</span>
  161. </div>
  162. </div>
  163. </div>
  164. <!-- 右侧:统计图表和异常列表 -->
  165. <div class="stats-section">
  166. <!-- 统计卡片 -->
  167. <div class="stats-cards">
  168. <div class="stat-card">
  169. <div class="stat-value">{{ attendanceStats().normalDays }}/{{ attendanceStats().totalDays }}</div>
  170. <div class="stat-label">正常出勤</div>
  171. <div class="stat-rate">{{ attendanceStats().complianceRate }}%</div>
  172. </div>
  173. <div class="stat-card warning">
  174. <div class="stat-value">{{ attendanceStats().lateDays }}</div>
  175. <div class="stat-label">迟到</div>
  176. </div>
  177. <div class="stat-card warning">
  178. <div class="stat-value">{{ attendanceStats().earlyLeaveDays }}</div>
  179. <div class="stat-label">早退</div>
  180. </div>
  181. <div class="stat-card danger">
  182. <div class="stat-value">{{ attendanceStats().absentDays }}</div>
  183. <div class="stat-label">旷工</div>
  184. </div>
  185. <div class="stat-card info">
  186. <div class="stat-value">{{ attendanceStats().leaveDays }}</div>
  187. <div class="stat-label">请假</div>
  188. </div>
  189. <div class="stat-card primary">
  190. <div class="stat-value">{{ attendanceStats().totalWorkHours }}h</div>
  191. <div class="stat-label">总工时</div>
  192. <div class="stat-sub">{{ attendanceStats().avgWorkHours }}h/天</div>
  193. </div>
  194. </div>
  195. <!-- 部门考勤对比 -->
  196. <div class="department-comparison">
  197. <div class="section-header">
  198. <h2>部门考勤对比</h2>
  199. </div>
  200. <div class="department-chart">
  201. @for (dept of departmentAttendanceData(); track dept.department) {
  202. <div class="dept-bar-container">
  203. <div class="dept-info">
  204. <span class="dept-name">{{ dept.department }}</span>
  205. <span class="dept-rate">{{ dept.complianceRate }}%</span>
  206. </div>
  207. <div class="progress-bar">
  208. <div
  209. class="progress-fill"
  210. [style.width]="dept.complianceRate + '%'"
  211. ></div>
  212. </div>
  213. <div class="dept-stats">
  214. 正常 {{ dept.compliant }} / 总计 {{ dept.total }}
  215. </div>
  216. </div>
  217. }
  218. </div>
  219. </div>
  220. <!-- 异常考勤列表 -->
  221. <div class="exceptions-section">
  222. <div class="section-header">
  223. <h2>考勤异常</h2>
  224. <span class="exception-count">({{ exceptionAttendance().length }})</span>
  225. </div>
  226. <div class="exceptions-table">
  227. <table mat-table [dataSource]="exceptionAttendance()" class="exception-table">
  228. <ng-container matColumnDef="date">
  229. <th mat-header-cell *matHeaderCellDef>日期</th>
  230. <td mat-cell *matCellDef="let item">{{ formatDate(item.date) }}</td>
  231. </ng-container>
  232. <ng-container matColumnDef="employeeName">
  233. <th mat-header-cell *matHeaderCellDef>员工</th>
  234. <td mat-cell *matCellDef="let item">{{ getEmployeeName(item.employeeId) }}</td>
  235. </ng-container>
  236. <ng-container matColumnDef="status">
  237. <th mat-header-cell *matHeaderCellDef>异常类型</th>
  238. <td mat-cell *matCellDef="let item">
  239. <span class="status-badge" [class]="getStatusClass(item.status)">
  240. {{ item.status }}
  241. </span>
  242. </td>
  243. </ng-container>
  244. <ng-container matColumnDef="workHours">
  245. <th mat-header-cell *matHeaderCellDef>工时</th>
  246. <td mat-cell *matCellDef="let item">{{ item.workHours }}h</td>
  247. </ng-container>
  248. <ng-container matColumnDef="projectName">
  249. <th mat-header-cell *matHeaderCellDef>项目</th>
  250. <td mat-cell *matCellDef="let item">{{ item.projectName }}</td>
  251. </ng-container>
  252. <ng-container matColumnDef="actions">
  253. <th mat-header-cell *matHeaderCellDef>操作</th>
  254. <td mat-cell *matCellDef="let item">
  255. <button
  256. mat-raised-button
  257. color="primary"
  258. size="small"
  259. (click)="openAttendanceDialog(item)"
  260. class="fix-btn"
  261. >
  262. 补卡申请
  263. </button>
  264. </td>
  265. </ng-container>
  266. <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
  267. <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
  268. @if (exceptionAttendance().length === 0) {
  269. <tr class="mat-row">
  270. <td class="mat-cell no-data" [attr.colspan]="displayedColumns.length">
  271. 暂无考勤异常记录
  272. </td>
  273. </tr>
  274. }
  275. </table>
  276. </div>
  277. </div>
  278. </div>
  279. } @else {
  280. <!-- 考勤甘特图视图 -->
  281. <div class="gantt-section">
  282. <div class="gantt-header">
  283. <h2>员工考勤甘特图</h2>
  284. <div class="gantt-controls">
  285. <!-- 显示模式切换 -->
  286. <div class="gantt-mode-buttons">
  287. <button
  288. mat-raised-button
  289. [class.active]="ganttMode() === 'attendance'"
  290. (click)="setGanttMode('attendance')"
  291. class="mode-btn"
  292. >
  293. 考勤状态
  294. </button>
  295. <button
  296. mat-raised-button
  297. [class.active]="ganttMode() === 'workload'"
  298. (click)="setGanttMode('workload')"
  299. class="mode-btn"
  300. >
  301. 工作负荷
  302. </button>
  303. </div>
  304. <!-- 时间尺度切换 -->
  305. <div class="gantt-scale-buttons">
  306. <button
  307. mat-raised-button
  308. [class.active]="ganttScale() === 'day'"
  309. (click)="setGanttScale('day')"
  310. class="scale-btn"
  311. >
  312. 日视图
  313. </button>
  314. <button
  315. mat-raised-button
  316. [class.active]="ganttScale() === 'week'"
  317. (click)="setGanttScale('week')"
  318. class="scale-btn"
  319. >
  320. 周视图
  321. </button>
  322. <button
  323. mat-raised-button
  324. [class.active]="ganttScale() === 'month'"
  325. (click)="setGanttScale('month')"
  326. class="scale-btn"
  327. >
  328. 月视图
  329. </button>
  330. </div>
  331. <!-- 部门筛选 -->
  332. <select
  333. [(ngModel)]="selectedDepartment"
  334. (change)="onDepartmentChange()"
  335. class="department-select"
  336. >
  337. <option value="all">全部部门</option>
  338. @for (dept of departments(); track dept) {
  339. <option [value]="dept">{{ dept }}</option>
  340. }
  341. </select>
  342. </div>
  343. </div>
  344. <div #ganttChartRef class="gantt-chart"></div>
  345. <!-- 图例说明 -->
  346. <div class="gantt-legend">
  347. @if (ganttMode() === 'attendance') {
  348. <div class="legend-item">
  349. <div class="legend-color normal"></div>
  350. <span>正常出勤</span>
  351. </div>
  352. <div class="legend-item">
  353. <div class="legend-color late"></div>
  354. <span>迟到</span>
  355. </div>
  356. <div class="legend-item">
  357. <div class="legend-color early"></div>
  358. <span>早退</span>
  359. </div>
  360. <div class="legend-item">
  361. <div class="legend-color absent"></div>
  362. <span>旷工</span>
  363. </div>
  364. <div class="legend-item">
  365. <div class="legend-color leave"></div>
  366. <span>请假</span>
  367. </div>
  368. <div class="legend-item">
  369. <div class="legend-color overtime"></div>
  370. <span>加班</span>
  371. </div>
  372. } @else {
  373. <div class="legend-item">
  374. <div class="legend-color" style="background-color: #22c55e;"></div>
  375. <span>工作负荷正常 (≤8h)</span>
  376. </div>
  377. <div class="legend-item">
  378. <div class="legend-color" style="background-color: #f59e0b;"></div>
  379. <span>工作负荷较高 (8-10h)</span>
  380. </div>
  381. <div class="legend-item">
  382. <div class="legend-color" style="background-color: #ef4444;"></div>
  383. <span>工作负荷过高 (>10h)</span>
  384. </div>
  385. }
  386. </div>
  387. <!-- 统计信息 -->
  388. <div class="gantt-stats">
  389. <div class="stat-item">
  390. <span class="stat-label">显示员工:</span>
  391. <span class="stat-value">{{ filteredEmployees().length }}人</span>
  392. </div>
  393. <div class="stat-item">
  394. <span class="stat-label">时间范围:</span>
  395. <span class="stat-value">{{ getTimeRangeText() }}</span>
  396. </div>
  397. @if (ganttMode() === 'attendance') {
  398. <div class="stat-item">
  399. <span class="stat-label">平均出勤率:</span>
  400. <span class="stat-value">{{ getAverageAttendanceRate() }}%</span>
  401. </div>
  402. } @else {
  403. <div class="stat-item">
  404. <span class="stat-label">平均工时:</span>
  405. <span class="stat-value">{{ getAverageWorkHours() }}h</span>
  406. </div>
  407. }
  408. </div>
  409. </div>
  410. }
  411. </div>
  412. </div>