123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433 |
- <div class="attendance-container hr-page">
- <header class="page-header">
- <h1>考勤统计</h1>
- <p class="page-description">管理和统计员工考勤数据,支持多维度查看和分析</p>
- </header>
- <!-- 时间维度切换栏 -->
- <div class="time-dimension-bar">
- <div class="date-navigation">
- <button mat-icon-button (click)="navigateDate('prev')" class="nav-btn">
- <mat-icon>chevron_left</mat-icon>
- </button>
- <div class="current-date" (click)="selectedView.set('month')">
- {{ selectedDate().getFullYear() }}年{{ selectedDate().getMonth() + 1 }}月
- <mat-icon class="calendar-icon">calendar_today</mat-icon>
- </div>
- <button mat-icon-button (click)="navigateDate('next')" class="nav-btn">
- <mat-icon>chevron_right</mat-icon>
- </button>
- </div>
-
- <div class="view-tabs">
- <button
- mat-raised-button
- [class.active]="selectedView() === 'day'"
- (click)="switchView('day')"
- class="view-btn"
- >
- 日
- </button>
- <button
- mat-raised-button
- [class.active]="selectedView() === 'week'"
- (click)="switchView('week')"
- class="view-btn"
- >
- 周
- </button>
- <button
- mat-raised-button
- [class.active]="selectedView() === 'month'"
- (click)="switchView('month')"
- class="view-btn"
- >
- 月
- </button>
- </div>
-
- <div class="action-buttons">
- <button
- mat-raised-button
- [class.active]="!showGanttView()"
- (click)="toggleView()"
- class="view-btn">
- 考勤视图
- </button>
- <button
- mat-raised-button
- [class.active]="showGanttView()"
- (click)="toggleView()"
- class="view-btn">
- 任务视图
- </button>
- <button
- mat-raised-button
- (click)="exportAttendanceData()"
- class="export-btn">
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
- <polyline points="7 10 12 15 17 10"></polyline>
- <line x1="12" y1="15" x2="12" y2="3"></line>
- </svg>
- 导出数据
- </button>
- </div>
- </div>
- <!-- 主内容区 -->
- <div class="main-content">
- <!-- 甘特图视图切换按钮 -->
- <div class="view-toggle-buttons">
- <button
- mat-raised-button
- [class.active]="!showGanttView()"
- (click)="toggleView('attendance')"
- class="view-btn">
- 考勤视图
- </button>
- <button
- mat-raised-button
- [class.active]="showGanttView()"
- (click)="toggleView('task')"
- class="view-btn">
- 任务视图
- </button>
- <button
- mat-raised-button
- (click)="exportAttendanceData()"
- class="export-btn">
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
- <polyline points="7 10 12 15 17 10"></polyline>
- <line x1="12" y1="15" x2="12" y2="3"></line>
- </svg>
- 导出数据
- </button>
- </div>
- @if (!showGanttView()) {
- <!-- 左侧:考勤日历 -->
- <div class="calendar-section">
- <div class="section-header">
- <h2>考勤日历</h2>
- </div>
-
- <!-- 日历表头 -->
- <div class="calendar-header">
- @for (i of [0, 1, 2, 3, 4, 5, 6]; track i) {
- <div class="weekday">
- {{ getWeekdayName(i) }}
- </div>
- }
- </div>
-
- <!-- 日历格子 -->
- <div class="calendar-grid">
- @for (day of getCalendarDays(); track day.date) {
- <div
- class="calendar-day"
- [class.current-month]="day.currentMonth"
- [class.other-month]="!day.currentMonth"
- [class.today]="isToday(day.date)"
- [class.has-attendance]="day.attendance"
- [class.absent]="day.attendance && day.attendance.status === '旷工'"
- [class.late]="day.attendance && day.attendance.status === '迟到'"
- [class.early]="day.attendance && day.attendance.status === '早退'"
- [class.leave]="day.attendance && day.attendance.status === '请假'"
- [class.normal]="day.attendance && day.attendance.status === '正常'"
- (click)="selectedDate.set(day.date)"
- matTooltip="{{ getDayTooltip(day) }}"
- matTooltipPosition="above"
- >
- <span class="day-number">{{ day.dayOfMonth }}</span>
- @if (day.attendance) {
- <div class="attendance-indicator"></div>
- }
- </div>
- }
- </div>
-
- <!-- 图例 -->
- <div class="calendar-legend">
- <div class="legend-item">
- <div class="legend-dot normal"></div>
- <span>正常</span>
- </div>
- <div class="legend-item">
- <div class="legend-dot late"></div>
- <span>迟到</span>
- </div>
- <div class="legend-item">
- <div class="legend-dot early"></div>
- <span>早退</span>
- </div>
- <div class="legend-item">
- <div class="legend-dot absent"></div>
- <span>旷工</span>
- </div>
- <div class="legend-item">
- <div class="legend-dot leave"></div>
- <span>请假</span>
- </div>
- </div>
- </div>
-
- <!-- 右侧:统计图表和异常列表 -->
- <div class="stats-section">
- <!-- 统计卡片 -->
- <div class="stats-cards">
- <div class="stat-card">
- <div class="stat-value">{{ attendanceStats().normalDays }}/{{ attendanceStats().totalDays }}</div>
- <div class="stat-label">正常出勤</div>
- <div class="stat-rate">{{ attendanceStats().complianceRate }}%</div>
- </div>
- <div class="stat-card warning">
- <div class="stat-value">{{ attendanceStats().lateDays }}</div>
- <div class="stat-label">迟到</div>
- </div>
- <div class="stat-card warning">
- <div class="stat-value">{{ attendanceStats().earlyLeaveDays }}</div>
- <div class="stat-label">早退</div>
- </div>
- <div class="stat-card danger">
- <div class="stat-value">{{ attendanceStats().absentDays }}</div>
- <div class="stat-label">旷工</div>
- </div>
- <div class="stat-card info">
- <div class="stat-value">{{ attendanceStats().leaveDays }}</div>
- <div class="stat-label">请假</div>
- </div>
- <div class="stat-card primary">
- <div class="stat-value">{{ attendanceStats().totalWorkHours }}h</div>
- <div class="stat-label">总工时</div>
- <div class="stat-sub">{{ attendanceStats().avgWorkHours }}h/天</div>
- </div>
- </div>
-
- <!-- 部门考勤对比 -->
- <div class="department-comparison">
- <div class="section-header">
- <h2>部门考勤对比</h2>
- </div>
- <div class="department-chart">
- @for (dept of departmentAttendanceData(); track dept.department) {
- <div class="dept-bar-container">
- <div class="dept-info">
- <span class="dept-name">{{ dept.department }}</span>
- <span class="dept-rate">{{ dept.complianceRate }}%</span>
- </div>
- <div class="progress-bar">
- <div
- class="progress-fill"
- [style.width]="dept.complianceRate + '%'"
- ></div>
- </div>
- <div class="dept-stats">
- 正常 {{ dept.compliant }} / 总计 {{ dept.total }}
- </div>
- </div>
- }
- </div>
- </div>
-
- <!-- 异常考勤列表 -->
- <div class="exceptions-section">
- <div class="section-header">
- <h2>考勤异常</h2>
- <span class="exception-count">({{ exceptionAttendance().length }})</span>
- </div>
- <div class="exceptions-table">
- <table mat-table [dataSource]="exceptionAttendance()" class="exception-table">
- <ng-container matColumnDef="date">
- <th mat-header-cell *matHeaderCellDef>日期</th>
- <td mat-cell *matCellDef="let item">{{ formatDate(item.date) }}</td>
- </ng-container>
- <ng-container matColumnDef="employeeName">
- <th mat-header-cell *matHeaderCellDef>员工</th>
- <td mat-cell *matCellDef="let item">{{ getEmployeeName(item.employeeId) }}</td>
- </ng-container>
- <ng-container matColumnDef="status">
- <th mat-header-cell *matHeaderCellDef>异常类型</th>
- <td mat-cell *matCellDef="let item">
- <span class="status-badge" [class]="getStatusClass(item.status)">
- {{ item.status }}
- </span>
- </td>
- </ng-container>
- <ng-container matColumnDef="workHours">
- <th mat-header-cell *matHeaderCellDef>工时</th>
- <td mat-cell *matCellDef="let item">{{ item.workHours }}h</td>
- </ng-container>
- <ng-container matColumnDef="projectName">
- <th mat-header-cell *matHeaderCellDef>项目</th>
- <td mat-cell *matCellDef="let item">{{ item.projectName }}</td>
- </ng-container>
- <ng-container matColumnDef="actions">
- <th mat-header-cell *matHeaderCellDef>操作</th>
- <td mat-cell *matCellDef="let item">
- <button
- mat-raised-button
- color="primary"
- size="small"
- (click)="openAttendanceDialog(item)"
- class="fix-btn"
- >
- 补卡申请
- </button>
- </td>
- </ng-container>
-
- <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
- <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
-
- @if (exceptionAttendance().length === 0) {
- <tr class="mat-row">
- <td class="mat-cell no-data" [attr.colspan]="displayedColumns.length">
- 暂无考勤异常记录
- </td>
- </tr>
- }
- </table>
- </div>
- </div>
- </div>
- } @else {
- <!-- 考勤甘特图视图 -->
- <div class="gantt-section">
- <div class="gantt-header">
- <h2>员工考勤甘特图</h2>
- <div class="gantt-controls">
- <!-- 显示模式切换 -->
- <div class="gantt-mode-buttons">
- <button
- mat-raised-button
- [class.active]="ganttMode() === 'attendance'"
- (click)="setGanttMode('attendance')"
- class="mode-btn"
- >
- 考勤状态
- </button>
- <button
- mat-raised-button
- [class.active]="ganttMode() === 'workload'"
- (click)="setGanttMode('workload')"
- class="mode-btn"
- >
- 工作负荷
- </button>
- </div>
-
- <!-- 时间尺度切换 -->
- <div class="gantt-scale-buttons">
- <button
- mat-raised-button
- [class.active]="ganttScale() === 'day'"
- (click)="setGanttScale('day')"
- class="scale-btn"
- >
- 日视图
- </button>
- <button
- mat-raised-button
- [class.active]="ganttScale() === 'week'"
- (click)="setGanttScale('week')"
- class="scale-btn"
- >
- 周视图
- </button>
- <button
- mat-raised-button
- [class.active]="ganttScale() === 'month'"
- (click)="setGanttScale('month')"
- class="scale-btn"
- >
- 月视图
- </button>
- </div>
-
- <!-- 部门筛选 -->
- <select
- [(ngModel)]="selectedDepartment"
- (change)="onDepartmentChange()"
- class="department-select"
- >
- <option value="all">全部部门</option>
- @for (dept of departments(); track dept) {
- <option [value]="dept">{{ dept }}</option>
- }
- </select>
- </div>
- </div>
-
- <div #ganttChartRef class="gantt-chart"></div>
-
- <!-- 图例说明 -->
- <div class="gantt-legend">
- @if (ganttMode() === 'attendance') {
- <div class="legend-item">
- <div class="legend-color normal"></div>
- <span>正常出勤</span>
- </div>
- <div class="legend-item">
- <div class="legend-color late"></div>
- <span>迟到</span>
- </div>
- <div class="legend-item">
- <div class="legend-color early"></div>
- <span>早退</span>
- </div>
- <div class="legend-item">
- <div class="legend-color absent"></div>
- <span>旷工</span>
- </div>
- <div class="legend-item">
- <div class="legend-color leave"></div>
- <span>请假</span>
- </div>
- <div class="legend-item">
- <div class="legend-color overtime"></div>
- <span>加班</span>
- </div>
- } @else {
- <div class="legend-item">
- <div class="legend-color" style="background-color: #22c55e;"></div>
- <span>工作负荷正常 (≤8h)</span>
- </div>
- <div class="legend-item">
- <div class="legend-color" style="background-color: #f59e0b;"></div>
- <span>工作负荷较高 (8-10h)</span>
- </div>
- <div class="legend-item">
- <div class="legend-color" style="background-color: #ef4444;"></div>
- <span>工作负荷过高 (>10h)</span>
- </div>
- }
- </div>
-
- <!-- 统计信息 -->
- <div class="gantt-stats">
- <div class="stat-item">
- <span class="stat-label">显示员工:</span>
- <span class="stat-value">{{ filteredEmployees().length }}人</span>
- </div>
- <div class="stat-item">
- <span class="stat-label">时间范围:</span>
- <span class="stat-value">{{ getTimeRangeText() }}</span>
- </div>
- @if (ganttMode() === 'attendance') {
- <div class="stat-item">
- <span class="stat-label">平均出勤率:</span>
- <span class="stat-value">{{ getAverageAttendanceRate() }}%</span>
- </div>
- } @else {
- <div class="stat-item">
- <span class="stat-label">平均工时:</span>
- <span class="stat-value">{{ getAverageWorkHours() }}h</span>
- </div>
- }
- </div>
- </div>
- }
- </div>
- </div>
|