QPAT commited on
Commit
401bceb
·
1 Parent(s): 0bef0b7

add approve and reject leave

Browse files
src/main/java/com/attendenceSystem/constant/Routes.java CHANGED
@@ -14,6 +14,8 @@ public final class Routes {
14
  public static final String DELETE = "/delete";
15
  public static final String DEACTIVATE = "/deactivate";
16
  public static final String ACTIVATE = "/activate";
 
 
17
  }
18
 
19
  public static final class Role {
 
14
  public static final String DELETE = "/delete";
15
  public static final String DEACTIVATE = "/deactivate";
16
  public static final String ACTIVATE = "/activate";
17
+ public static final String ACCEPT = "/accept";
18
+ public static final String REJECT = "/reject";
19
  }
20
 
21
  public static final class Role {
src/main/java/com/attendenceSystem/module/attendance/controller/AttendanceController.java CHANGED
@@ -8,6 +8,7 @@ import org.springframework.validation.BindingResult;
8
  import org.springframework.web.bind.annotation.ExceptionHandler;
9
  import org.springframework.web.bind.annotation.GetMapping;
10
  import org.springframework.web.bind.annotation.ModelAttribute;
 
11
  import org.springframework.web.bind.annotation.PostMapping;
12
  import org.springframework.web.bind.annotation.RequestMapping;
13
  import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@@ -21,6 +22,7 @@ import com.attendenceSystem.module.attendance.exception.AlreadyCheckedOutExcepti
21
  import com.attendenceSystem.module.attendance.exception.InvalidAttendanceStateException;
22
  import com.attendenceSystem.module.attendance.exception.NotCheckedInException;
23
  import com.attendenceSystem.module.attendance.service.AttendanceService;
 
24
 
25
  import jakarta.validation.Valid;
26
  import lombok.RequiredArgsConstructor;
@@ -32,6 +34,7 @@ import lombok.extern.slf4j.Slf4j;
32
  @RequestMapping(Routes.Attendance.ROOT)
33
  public class AttendanceController {
34
  private final AttendanceService attendanceService;
 
35
 
36
  @GetMapping
37
  public String toAttendanceListPage(@PageableDefault(size = 10) Pageable pageable, Model model) {
@@ -55,7 +58,7 @@ public class AttendanceController {
55
 
56
  @GetMapping(Routes.Attendance.HISTORY)
57
  public String attendanceHistory(@PageableDefault(size = 10) Pageable pageable, Model model) {
58
-
59
  model.addAttribute("attendanceHistory", attendanceService.getAttendanceHistory(pageable));
60
  return Views.Attendance.HISTORY;
61
  }
@@ -84,7 +87,25 @@ public class AttendanceController {
84
  }
85
  attendanceService.createLeaveRequest(createLeaveRequest);
86
  redirectAttributes.addFlashAttribute("successMessage", "Yêu cầu nghỉ phép đã được gửi.");
87
- return Routes.REDIRECT + Routes.Attendance.ROOT + Routes.Attendance.LEAVE + Routes.Action.CREATE;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  }
89
 
90
  @ExceptionHandler({
 
8
  import org.springframework.web.bind.annotation.ExceptionHandler;
9
  import org.springframework.web.bind.annotation.GetMapping;
10
  import org.springframework.web.bind.annotation.ModelAttribute;
11
+ import org.springframework.web.bind.annotation.PathVariable;
12
  import org.springframework.web.bind.annotation.PostMapping;
13
  import org.springframework.web.bind.annotation.RequestMapping;
14
  import org.springframework.web.servlet.mvc.support.RedirectAttributes;
 
22
  import com.attendenceSystem.module.attendance.exception.InvalidAttendanceStateException;
23
  import com.attendenceSystem.module.attendance.exception.NotCheckedInException;
24
  import com.attendenceSystem.module.attendance.service.AttendanceService;
25
+ import com.attendenceSystem.module.attendance.service.LeaveService;
26
 
27
  import jakarta.validation.Valid;
28
  import lombok.RequiredArgsConstructor;
 
34
  @RequestMapping(Routes.Attendance.ROOT)
35
  public class AttendanceController {
36
  private final AttendanceService attendanceService;
37
+ private final LeaveService leaveService;
38
 
39
  @GetMapping
40
  public String toAttendanceListPage(@PageableDefault(size = 10) Pageable pageable, Model model) {
 
58
 
59
  @GetMapping(Routes.Attendance.HISTORY)
60
  public String attendanceHistory(@PageableDefault(size = 10) Pageable pageable, Model model) {
61
+
62
  model.addAttribute("attendanceHistory", attendanceService.getAttendanceHistory(pageable));
63
  return Views.Attendance.HISTORY;
64
  }
 
87
  }
88
  attendanceService.createLeaveRequest(createLeaveRequest);
89
  redirectAttributes.addFlashAttribute("successMessage", "Yêu cầu nghỉ phép đã được gửi.");
90
+ return Routes.REDIRECT + Routes.Attendance.ROOT + Routes.Attendance.LEAVE + Routes.Action.CREATE;
91
+ }
92
+
93
+ @PostMapping(Routes.Attendance.LEAVE + Routes.Action.ACCEPT + "/{id}")
94
+ public String acceptLeaveRequest(
95
+ @PathVariable("id") Long id,
96
+ RedirectAttributes redirectAttributes) {
97
+ leaveService.acceptLeave(id);
98
+ redirectAttributes.addFlashAttribute("successMessage", "Đã duyệt đơn nghỉ phép.");
99
+ return Routes.REDIRECT + Routes.Attendance.ROOT + Routes.Attendance.LEAVE;
100
+ }
101
+
102
+ @PostMapping(Routes.Attendance.LEAVE + Routes.Action.REJECT + "/{id}")
103
+ public String rejectLeaveRequest(
104
+ @PathVariable("id") Long id,
105
+ RedirectAttributes redirectAttributes) {
106
+ leaveService.rejectLeave(id);
107
+ redirectAttributes.addFlashAttribute("successMessage", "Đã từ chối đơn nghỉ phép.");
108
+ return Routes.REDIRECT + Routes.Attendance.ROOT + Routes.Attendance.LEAVE;
109
  }
110
 
111
  @ExceptionHandler({
src/main/java/com/attendenceSystem/module/attendance/repository/LeaveRequestRepository.java CHANGED
@@ -1,5 +1,7 @@
1
  package com.attendenceSystem.module.attendance.repository;
2
 
 
 
3
  import org.springframework.data.domain.Page;
4
  import org.springframework.data.domain.Pageable;
5
  import org.springframework.data.jpa.repository.JpaRepository;
@@ -9,4 +11,6 @@ import com.attendenceSystem.module.user.entity.User;
9
 
10
  public interface LeaveRequestRepository extends JpaRepository<LeaveRequest, Long> {
11
  Page<LeaveRequest> findByUser(User user, Pageable pageable);
 
 
12
  }
 
1
  package com.attendenceSystem.module.attendance.repository;
2
 
3
+ import java.util.Optional;
4
+
5
  import org.springframework.data.domain.Page;
6
  import org.springframework.data.domain.Pageable;
7
  import org.springframework.data.jpa.repository.JpaRepository;
 
11
 
12
  public interface LeaveRequestRepository extends JpaRepository<LeaveRequest, Long> {
13
  Page<LeaveRequest> findByUser(User user, Pageable pageable);
14
+
15
+ Optional<LeaveRequest> findById(long id);
16
  }
src/main/java/com/attendenceSystem/module/attendance/service/LeaveService.java ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.attendance.service;
2
+
3
+ public interface LeaveService {
4
+ void acceptLeave(Long id);
5
+ void rejectLeave(Long id);
6
+ }
src/main/java/com/attendenceSystem/module/attendance/service/impl/LeaveServiceImpl.java ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.attendance.service.impl;
2
+
3
+ import org.springframework.stereotype.Service;
4
+ import org.springframework.transaction.annotation.Transactional;
5
+
6
+ import com.attendenceSystem.module.attendance.entity.LeaveRequest;
7
+ import com.attendenceSystem.module.attendance.entity.enums.LeaveStatus;
8
+ import com.attendenceSystem.module.attendance.repository.LeaveRequestRepository;
9
+ import com.attendenceSystem.module.attendance.service.LeaveService;
10
+ import com.attendenceSystem.module.user.entity.enums.Role;
11
+ import com.attendenceSystem.util.SecurityUtil;
12
+
13
+ import lombok.RequiredArgsConstructor;
14
+
15
+ @Service
16
+ @RequiredArgsConstructor
17
+ public class LeaveServiceImpl implements LeaveService {
18
+ private final LeaveRequestRepository leaveRequestRepository;
19
+
20
+ @Transactional
21
+ @Override
22
+ public void acceptLeave(Long id) {
23
+ validateManagerAction();
24
+ LeaveRequest leaveRequest = findByIdWithStatusPending(id);
25
+ leaveRequest.setStatus(LeaveStatus.APPROVED);
26
+ }
27
+
28
+ @Transactional
29
+ @Override
30
+ public void rejectLeave(Long id) {
31
+ validateManagerAction();
32
+ LeaveRequest leaveRequest = findByIdWithStatusPending(id);
33
+ leaveRequest.setStatus(LeaveStatus.REJECTED);
34
+ }
35
+
36
+ private void validateManagerAction() {
37
+ if (SecurityUtil.getCurrentUserRole() != Role.MANAGER) {
38
+ throw new IllegalStateException("Bạn không có quyền thực hiện hành động này");
39
+ }
40
+ }
41
+
42
+ private LeaveRequest findByIdWithStatusPending(Long id) {
43
+ LeaveRequest leaveRequest = leaveRequestRepository.findById(id)
44
+ .orElseThrow(() -> new IllegalStateException("Không tìm thấy đơn xin nghỉ phép"));
45
+ if (leaveRequest.getStatus() != LeaveStatus.PENDING) {
46
+ throw new IllegalStateException("Đơn nghỉ phép đã được xử lý");
47
+ }
48
+ return leaveRequest;
49
+ }
50
+
51
+ }
src/main/java/com/attendenceSystem/module/dashboard/dto/response/EmployeeDashboardResponse.java CHANGED
@@ -7,8 +7,6 @@ import com.attendenceSystem.module.attendance.dto.response.AttendanceResponse;
7
  public record EmployeeDashboardResponse(
8
 
9
  Long totalReports,
10
- Long acceptedReports,
11
- Long rejectedReports,
12
  String attendanceRate,
13
  Page<AttendanceResponse> attendanceHistory
14
 
 
7
  public record EmployeeDashboardResponse(
8
 
9
  Long totalReports,
 
 
10
  String attendanceRate,
11
  Page<AttendanceResponse> attendanceHistory
12
 
src/main/java/com/attendenceSystem/module/dashboard/mapper/response/DashboardResponseMapper.java CHANGED
@@ -15,22 +15,43 @@ import com.attendenceSystem.module.dashboard.dto.response.DailyAttendanceStats;
15
  @Component
16
  public class DashboardResponseMapper {
17
 
18
- public AdminDashboardResponse toAdminDashboardResponse(long totalAccounts, long activeAccounts,
19
- long inactiveAccounts, long pendingAccounts,
 
 
 
20
  List<AccountTypeDistributionResponse> accountTypeDistribution) {
21
- return new AdminDashboardResponse(totalAccounts, activeAccounts, inactiveAccounts, pendingAccounts,
 
 
 
 
22
  accountTypeDistribution);
23
  }
24
 
25
- public ManagerDashboardResponse toManagerDashboardResponse(long totalEmployees, long attendedEmployees,
26
- long lateEmployees, long absentEmployees, Page<AttendanceResponse> attendanceHistory,
 
 
 
 
27
  List<DailyAttendanceStats> weeklyStats) {
28
- return new ManagerDashboardResponse(totalEmployees, attendedEmployees, lateEmployees, absentEmployees, attendanceHistory, weeklyStats);
 
 
 
 
 
 
29
  }
30
 
31
- public EmployeeDashboardResponse toEmployeeDashboardResponse(long totalReports, long acceptedReports,
32
- long rejectedReports, String attendanceRate, Page<AttendanceResponse> attendanceHistory) {
33
- return new EmployeeDashboardResponse(totalReports, acceptedReports, rejectedReports, attendanceRate,
 
 
 
 
34
  attendanceHistory);
35
  }
36
  }
 
15
  @Component
16
  public class DashboardResponseMapper {
17
 
18
+ public AdminDashboardResponse toAdminDashboardResponse(
19
+ long totalAccounts,
20
+ long activeAccounts,
21
+ long inactiveAccounts,
22
+ long pendingAccounts,
23
  List<AccountTypeDistributionResponse> accountTypeDistribution) {
24
+ return new AdminDashboardResponse(
25
+ totalAccounts,
26
+ activeAccounts,
27
+ inactiveAccounts,
28
+ pendingAccounts,
29
  accountTypeDistribution);
30
  }
31
 
32
+ public ManagerDashboardResponse toManagerDashboardResponse(
33
+ long totalEmployees,
34
+ long attendedEmployees,
35
+ long lateEmployees,
36
+ long absentEmployees,
37
+ Page<AttendanceResponse> attendanceHistory,
38
  List<DailyAttendanceStats> weeklyStats) {
39
+ return new ManagerDashboardResponse(
40
+ totalEmployees,
41
+ attendedEmployees,
42
+ lateEmployees,
43
+ absentEmployees,
44
+ attendanceHistory,
45
+ weeklyStats);
46
  }
47
 
48
+ public EmployeeDashboardResponse toEmployeeDashboardResponse(
49
+ long totalReports,
50
+ String attendanceRate,
51
+ Page<AttendanceResponse> attendanceHistory) {
52
+ return new EmployeeDashboardResponse(
53
+ totalReports,
54
+ attendanceRate,
55
  attendanceHistory);
56
  }
57
  }
src/main/java/com/attendenceSystem/module/dashboard/service/impl/DashboardServiceImpl.java CHANGED
@@ -23,7 +23,6 @@ import com.attendenceSystem.module.dashboard.dto.response.ManagerDashboardRespon
23
  import com.attendenceSystem.module.dashboard.mapper.response.DashboardResponseMapper;
24
  import com.attendenceSystem.module.dashboard.service.DashboardService;
25
  import com.attendenceSystem.module.dashboard.util.DashboardCalculator;
26
- import com.attendenceSystem.module.report.entity.enums.ReportStatus;
27
  import com.attendenceSystem.module.report.repository.ReportRepository;
28
  import com.attendenceSystem.module.user.entity.User;
29
  import com.attendenceSystem.module.user.entity.enums.Role;
@@ -64,8 +63,9 @@ public class DashboardServiceImpl implements DashboardService {
64
 
65
  @Override
66
  public ManagerDashboardResponse getManagerDashboard() {
67
- long totalEmployees = userRepository.countByRoleNot(Role.ADMIN);
68
  LocalDate today = LocalDate.now();
 
 
69
  long presentToday = attendanceRecordRepository.countByAttendanceDateAndStatus(
70
  today,
71
  AttendanceStatus.PRESENT);
@@ -110,8 +110,6 @@ public class DashboardServiceImpl implements DashboardService {
110
  public EmployeeDashboardResponse getEmployeeDashboard() {
111
  User user = getCurrentUser();
112
  long totalReports = reportRepository.countByEmployee(user);
113
- long acceptedReports = reportRepository.countByEmployeeAndStatus(user, ReportStatus.ACCEPTED);
114
- long rejectedReports = reportRepository.countByEmployeeAndStatus(user, ReportStatus.REJECTED);
115
  long totalDays = attendanceRecordRepository.count();
116
  long attendedDays = attendanceRecordRepository.countByCheckInTimeNotNullAndCheckOutTimeNotNull();
117
  String attendenceRate = DashboardCalculator.showResultStr(attendedDays, totalDays);
@@ -120,8 +118,6 @@ public class DashboardServiceImpl implements DashboardService {
120
  .map(attendanceResponseMapper::fromEntity);
121
  return dashboardResponseMapper.toEmployeeDashboardResponse(
122
  totalReports,
123
- acceptedReports,
124
- rejectedReports,
125
  attendenceRate,
126
  attendanceHistory);
127
  }
 
23
  import com.attendenceSystem.module.dashboard.mapper.response.DashboardResponseMapper;
24
  import com.attendenceSystem.module.dashboard.service.DashboardService;
25
  import com.attendenceSystem.module.dashboard.util.DashboardCalculator;
 
26
  import com.attendenceSystem.module.report.repository.ReportRepository;
27
  import com.attendenceSystem.module.user.entity.User;
28
  import com.attendenceSystem.module.user.entity.enums.Role;
 
63
 
64
  @Override
65
  public ManagerDashboardResponse getManagerDashboard() {
 
66
  LocalDate today = LocalDate.now();
67
+
68
+ long totalEmployees = userRepository.countByRoleNot(Role.ADMIN);
69
  long presentToday = attendanceRecordRepository.countByAttendanceDateAndStatus(
70
  today,
71
  AttendanceStatus.PRESENT);
 
110
  public EmployeeDashboardResponse getEmployeeDashboard() {
111
  User user = getCurrentUser();
112
  long totalReports = reportRepository.countByEmployee(user);
 
 
113
  long totalDays = attendanceRecordRepository.count();
114
  long attendedDays = attendanceRecordRepository.countByCheckInTimeNotNullAndCheckOutTimeNotNull();
115
  String attendenceRate = DashboardCalculator.showResultStr(attendedDays, totalDays);
 
118
  .map(attendanceResponseMapper::fromEntity);
119
  return dashboardResponseMapper.toEmployeeDashboardResponse(
120
  totalReports,
 
 
121
  attendenceRate,
122
  attendanceHistory);
123
  }
src/main/java/com/attendenceSystem/module/user/api/AuthApiController.java CHANGED
@@ -13,6 +13,7 @@ import com.attendenceSystem.module.user.dto.request.RegisterRequest;
13
  import com.attendenceSystem.module.user.dto.response.UserResponse;
14
  import com.attendenceSystem.module.user.service.AuthService;
15
 
 
16
  import lombok.RequiredArgsConstructor;
17
 
18
  @RestController
@@ -23,13 +24,13 @@ public class AuthApiController {
23
  private final AuthService authService;
24
 
25
  @PostMapping(Routes.Auth.LOGIN)
26
- public ResponseEntity<UserResponse> login(@RequestBody LoginRequest request) {
27
  UserResponse response = authService.login(request);
28
  return ResponseEntity.ok(response);
29
  }
30
 
31
  @PostMapping(Routes.Auth.REGISTER)
32
- public ResponseEntity<Void> register(@RequestBody RegisterRequest request) {
33
  authService.register(request);
34
  return ResponseEntity.ok().build();
35
  }
@@ -44,7 +45,7 @@ public class AuthApiController {
44
  return ResponseEntity.ok().build();
45
  }
46
  @PostMapping(Routes.Auth.VERIFY_OTP)
47
- public ResponseEntity<Boolean> verifyOtp(@RequestBody VerifyOtpRequest request) {
48
  boolean isValid = authService.verifyOtp(request.getDestination(), request.getCode());
49
  return ResponseEntity.ok(isValid);
50
  }
 
13
  import com.attendenceSystem.module.user.dto.response.UserResponse;
14
  import com.attendenceSystem.module.user.service.AuthService;
15
 
16
+ import jakarta.validation.Valid;
17
  import lombok.RequiredArgsConstructor;
18
 
19
  @RestController
 
24
  private final AuthService authService;
25
 
26
  @PostMapping(Routes.Auth.LOGIN)
27
+ public ResponseEntity<UserResponse> login(@Valid @RequestBody LoginRequest request) {
28
  UserResponse response = authService.login(request);
29
  return ResponseEntity.ok(response);
30
  }
31
 
32
  @PostMapping(Routes.Auth.REGISTER)
33
+ public ResponseEntity<Void> register(@Valid @RequestBody RegisterRequest request) {
34
  authService.register(request);
35
  return ResponseEntity.ok().build();
36
  }
 
45
  return ResponseEntity.ok().build();
46
  }
47
  @PostMapping(Routes.Auth.VERIFY_OTP)
48
+ public ResponseEntity<Boolean> verifyOtp(@Valid @RequestBody VerifyOtpRequest request) {
49
  boolean isValid = authService.verifyOtp(request.getDestination(), request.getCode());
50
  return ResponseEntity.ok(isValid);
51
  }
src/main/java/com/attendenceSystem/util/SecurityUtil.java CHANGED
@@ -41,7 +41,7 @@ public class SecurityUtil {
41
  .map(GrantedAuthority::getAuthority)
42
  .filter(authority -> authority.startsWith("ROLE_"))
43
  .findFirst()
44
- .map(authority -> Role.valueOf(authority.replace("ROLE_", "")))
45
  .orElse(null);
46
  }
47
  }
 
41
  .map(GrantedAuthority::getAuthority)
42
  .filter(authority -> authority.startsWith("ROLE_"))
43
  .findFirst()
44
+ .map(authority -> Role.valueOf(authority.substring(5)))
45
  .orElse(null);
46
  }
47
  }
src/main/resources/templates/cms/absent/absent-list.html CHANGED
@@ -117,10 +117,13 @@
117
  <tr th:each="leave : ${leaveRequests.content}">
118
  <td th:text="${leave.fullName()}">Nhân viên</td>
119
  <td th:text="${leave.departmentDisplay()}">Phòng ban</td>
120
- <td th:text="${leave.startDate()}">Từ ngày</td>
121
- <td th:text="${leave.endDate()}">Đến ngày</td>
122
  <td>
123
- <span class="status pending">Trạng thái</span>
 
 
 
124
  </td>
125
  <td>
126
  <button th:data-id="${leave.id}"
@@ -133,20 +136,18 @@
133
 
134
 
135
  <td>
136
- <div class="action-group">
137
-
138
- <form method="post">
139
- <button class="btn-primary btn-small">
140
  Phê duyệt
141
  </button>
142
  </form>
143
 
144
- <form method="post">
145
- <button class="btn-secondary btn-small">
146
  Từ chối
147
  </button>
148
  </form>
149
-
150
  </div>
151
  </td>
152
  </tr>
 
117
  <tr th:each="leave : ${leaveRequests.content}">
118
  <td th:text="${leave.fullName()}">Nhân viên</td>
119
  <td th:text="${leave.departmentDisplay()}">Phòng ban</td>
120
+ <td th:text="${#temporals.format(leave.startDate(), 'dd/MM/yyyy')}">Từ ngày</td>
121
+ <td th:text="${#temporals.format(leave.endDate(), 'dd/MM/yyyy')}">Đến ngày</td>
122
  <td>
123
+ <span th:classappend="${leave.status()} ? 'status-' + ${leave.status().name().toLowerCase()}"
124
+ class="status"
125
+ th:text="${leave.status()}">
126
+ </span>
127
  </td>
128
  <td>
129
  <button th:data-id="${leave.id}"
 
136
 
137
 
138
  <td>
139
+ <div class="action-group" th:if="${leave.status().toString() == 'PENDING'}">
140
+ <form th:action="@{'/attendance/leave/accept/' + ${leave.id()}}" method="post" style="display:inline;">
141
+ <button class="btn-primary btn-small" type="submit" onclick="return confirm('Bạn có chắc phê duyệt đơn nghỉ phép này?')">
 
142
  Phê duyệt
143
  </button>
144
  </form>
145
 
146
+ <form th:action="@{'/attendance/leave/reject/' + ${leave.id()}}" method="post" style="display:inline;">
147
+ <button class="btn-secondary btn-small" type="submit" onclick="return confirm('Bạn có chắc từ chối đơn nghỉ phép này?')">
148
  Từ chối
149
  </button>
150
  </form>
 
151
  </div>
152
  </td>
153
  </tr>
src/main/resources/templates/cms/dashboard/dashboard-employee.html CHANGED
@@ -36,7 +36,7 @@
36
  </div>
37
 
38
  <div class="employee-stat-card__content">
39
- <h3 class="employee-stat-card__number">12</h3>
40
  <p class="employee-stat-card__label">Báo cáo đã nộp</p>
41
  </div>
42
 
@@ -48,7 +48,7 @@
48
  </div>
49
 
50
  <div class="employee-stat-card__content">
51
- <h3 class="employee-stat-card__number">95%</h3>
52
  <p class="employee-stat-card__label">Tỷ lệ chuyên cần</p>
53
  </div>
54
 
@@ -147,51 +147,23 @@
147
  </thead>
148
 
149
  <tbody>
150
-
151
- <tr>
152
- <td>2/6/2026</td>
153
- <td>07:58</td>
154
- <td>17:05</td>
155
- <td>
156
- <span class="status-badge status-success">
157
- Đúng giờ
158
- </span>
159
- </td>
160
- </tr>
161
-
162
- <tr>
163
- <td>1/6/2026</td>
164
- <td>08:12</td>
165
- <td>17:02</td>
166
- <td>
167
- <span class="status-badge status-warning">
168
- Đi muộn
169
- </span>
170
- </td>
171
- </tr>
172
-
173
- <tr>
174
- <td>31/5/2026</td>
175
- <td>08:00</td>
176
- <td>17:00</td>
177
  <td>
178
- <span class="status-badge status-success">
179
- Đúng giờ
 
180
  </span>
181
  </td>
182
  </tr>
183
 
184
- <tr>
185
- <td>30/5/2026</td>
186
- <td>07:55</td>
187
- <td>16:45</td>
188
- <td>
189
- <span class="status-badge status-danger">
190
- Về sớm
191
- </span>
192
  </td>
193
  </tr>
194
-
195
  </tbody>
196
 
197
  </table>
 
36
  </div>
37
 
38
  <div class="employee-stat-card__content">
39
+ <h3 class="employee-stat-card__number" th:text="${dashboard.totalReports}">0</h3>
40
  <p class="employee-stat-card__label">Báo cáo đã nộp</p>
41
  </div>
42
 
 
48
  </div>
49
 
50
  <div class="employee-stat-card__content">
51
+ <h3 class="employee-stat-card__number" th:text="${dashboard.attendanceRate}">0%</h3>
52
  <p class="employee-stat-card__label">Tỷ lệ chuyên cần</p>
53
  </div>
54
 
 
147
  </thead>
148
 
149
  <tbody>
150
+ <tr th:each="record : ${dashboard.attendanceHistory.content}">
151
+ <td th:text="${#temporals.format(record.attendanceDate, 'dd/MM/yyyy')}"></td>
152
+ <td th:text="${record.checkInTime != null ? #temporals.format(record.checkInTime, 'HH:mm') : '--'}"></td>
153
+ <td th:text="${record.checkOutTime != null ? #temporals.format(record.checkOutTime, 'HH:mm') : '--'}"></td>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  <td>
155
+ <span th:classappend="${record.late} ? 'status-warning' : (${record.earlyLeave} ? 'status-danger' : 'status-success')"
156
+ class="status-badge"
157
+ th:text="${record.late} ? 'Đi muộn' : (${record.earlyLeave} ? 'Về sớm' : 'Đúng giờ')">
158
  </span>
159
  </td>
160
  </tr>
161
 
162
+ <tr th:if="${dashboard.attendanceHistory.content.empty}">
163
+ <td colspan="4" style="text-align: center; padding: 20px;">
164
+ Chưa có dữ liệu điểm danh
 
 
 
 
 
165
  </td>
166
  </tr>
 
167
  </tbody>
168
 
169
  </table>
uploads/reports/07cb5f5fe436beb6e2382138612a94f58987f57de20801eaece9e7e39c4b1f3a.jpg ADDED