QPAT commited on
Commit
4aa64c5
·
1 Parent(s): f75eb78

Add module attendance show list and filter, fix bugs

Browse files
Files changed (49) hide show
  1. spring-boot.log +0 -0
  2. src/main/java/com/attendenceSystem/module/attendance/api/AttendanceManagerApiController.java +62 -0
  3. src/main/java/com/attendenceSystem/module/attendance/controller/AttendanceController.java +5 -5
  4. src/main/java/com/attendenceSystem/module/attendance/dto/response/AttendanceResponse.java +3 -1
  5. src/main/java/com/attendenceSystem/module/attendance/dto/response/ManagerStatsResponse.java +18 -0
  6. src/main/java/com/attendenceSystem/module/attendance/mapper/response/AttendanceResponseMapper.java +1 -0
  7. src/main/java/com/attendenceSystem/module/attendance/repository/AttendanceRecordRepository.java +11 -0
  8. src/main/java/com/attendenceSystem/module/attendance/service/AttendanceService.java +12 -0
  9. src/main/java/com/attendenceSystem/module/attendance/service/impl/AttendanceServiceImpl.java +139 -5
  10. src/main/java/com/attendenceSystem/module/dashboard/dto/response/DailyAttendanceStats.java +9 -0
  11. src/main/java/com/attendenceSystem/module/dashboard/dto/response/ManagerDashboardResponse.java +8 -3
  12. src/main/java/com/attendenceSystem/module/dashboard/mapper/response/DashboardResponseMapper.java +4 -2
  13. src/main/java/com/attendenceSystem/module/dashboard/service/impl/DashboardServiceImpl.java +27 -4
  14. src/main/java/com/attendenceSystem/module/faceid/api/FaceIdApiController.java +2 -1
  15. src/main/java/com/attendenceSystem/module/report/api/ReportApiController.java +7 -4
  16. src/main/java/com/attendenceSystem/module/report/controller/ReportController.java +4 -4
  17. src/main/java/com/attendenceSystem/module/storage/provider/LocalStorageProvider.java +33 -7
  18. src/main/java/com/attendenceSystem/module/user/entity/converter/DepartmentConverter.java +2 -6
  19. src/main/java/com/attendenceSystem/module/user/entity/enums/Department.java +9 -0
  20. src/main/java/com/attendenceSystem/module/user/repository/UserRepository.java +3 -0
  21. src/main/java/com/attendenceSystem/security/SecurityConfig.java +2 -1
  22. src/main/resources/static/js/attendance-manager.js +164 -0
  23. src/main/resources/static/js/charts/Chart.js +49 -15
  24. src/main/resources/templates/cms/attendance/attendance-check.html +104 -102
  25. src/main/resources/templates/cms/attendance/attendance.html +5 -5
  26. src/main/resources/templates/cms/dashboard/dashboard-manager.html +10 -5
  27. src/main/resources/templates/cms/document/document-create.html +1 -1
  28. src/main/resources/templates/cms/sidebar/sidebar-manage.html +1 -1
  29. src/main/resources/templates/cms/user/user-information.html +1 -1
  30. uploads/face_samples/2/face_2_1.jpg +0 -0
  31. uploads/face_samples/2/face_2_2.jpg +0 -0
  32. uploads/face_samples/2/face_2_3.jpg +0 -0
  33. uploads/face_samples/2/face_2_4.jpg +0 -0
  34. uploads/face_samples/2/face_2_5.jpg +0 -0
  35. uploads/face_samples/4/face_4_1.jpg +0 -0
  36. uploads/face_samples/4/face_4_2.jpg +0 -0
  37. uploads/face_samples/4/face_4_3.jpg +0 -0
  38. uploads/face_samples/4/face_4_4.jpg +0 -0
  39. uploads/face_samples/4/face_4_5.jpg +0 -0
  40. uploads/face_samples/5/face_5_1.jpg +0 -0
  41. uploads/face_samples/5/face_5_2.jpg +0 -0
  42. uploads/face_samples/5/face_5_3.jpg +0 -0
  43. uploads/face_samples/5/face_5_4.jpg +0 -0
  44. uploads/face_samples/5/face_5_5.jpg +0 -0
  45. uploads/face_samples/6/face_6_1.jpg +0 -0
  46. uploads/face_samples/6/face_6_2.jpg +0 -0
  47. uploads/face_samples/6/face_6_3.jpg +0 -0
  48. uploads/face_samples/6/face_6_4.jpg +0 -0
  49. uploads/face_samples/6/face_6_5.jpg +0 -0
spring-boot.log CHANGED
The diff for this file is too large to render. See raw diff
 
src/main/java/com/attendenceSystem/module/attendance/api/AttendanceManagerApiController.java ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.attendance.api;
2
+
3
+ import java.time.LocalDate;
4
+ import java.util.List;
5
+ import java.util.Map;
6
+
7
+ import org.springframework.http.ResponseEntity;
8
+ import org.springframework.web.bind.annotation.GetMapping;
9
+ import org.springframework.web.bind.annotation.RequestMapping;
10
+ import org.springframework.web.bind.annotation.RequestParam;
11
+ import org.springframework.web.bind.annotation.RestController;
12
+
13
+ import com.attendenceSystem.constant.Routes;
14
+ import com.attendenceSystem.module.attendance.dto.response.AttendanceResponse;
15
+ import com.attendenceSystem.module.attendance.dto.response.ManagerStatsResponse;
16
+ import com.attendenceSystem.module.attendance.service.AttendanceService;
17
+
18
+ import lombok.RequiredArgsConstructor;
19
+
20
+ @RestController
21
+ @RequestMapping(Routes.API + Routes.Attendance.ROOT + "/manager")
22
+ @RequiredArgsConstructor
23
+ public class AttendanceManagerApiController {
24
+
25
+ private final AttendanceService attendanceService;
26
+
27
+ @GetMapping("/stats")
28
+ public ResponseEntity<ManagerStatsResponse> getStats(
29
+ @RequestParam(required = false) String departmentId,
30
+ @RequestParam(required = false) LocalDate startDate,
31
+ @RequestParam(required = false) LocalDate endDate,
32
+ @RequestParam(required = false) String status) {
33
+ com.attendenceSystem.module.attendance.entity.enums.AttendanceStatus attendanceStatus = null;
34
+ if (status != null && !status.isEmpty()) {
35
+ try {
36
+ attendanceStatus = com.attendenceSystem.module.attendance.entity.enums.AttendanceStatus.valueOf(status);
37
+ } catch (IllegalArgumentException e) {
38
+ // Invalid status, ignore
39
+ }
40
+ }
41
+ ManagerStatsResponse stats = attendanceService.getManagerStats(departmentId, startDate, endDate, attendanceStatus);
42
+ return ResponseEntity.ok(stats);
43
+ }
44
+
45
+ @GetMapping("/list")
46
+ public ResponseEntity<List<AttendanceResponse>> getList(
47
+ @RequestParam(required = false) String departmentId,
48
+ @RequestParam(required = false) LocalDate startDate,
49
+ @RequestParam(required = false) LocalDate endDate,
50
+ @RequestParam(required = false) String status) {
51
+ com.attendenceSystem.module.attendance.entity.enums.AttendanceStatus attendanceStatus = null;
52
+ if (status != null && !status.isEmpty()) {
53
+ try {
54
+ attendanceStatus = com.attendenceSystem.module.attendance.entity.enums.AttendanceStatus.valueOf(status);
55
+ } catch (IllegalArgumentException e) {
56
+ // Invalid status, ignore
57
+ }
58
+ }
59
+ List<AttendanceResponse> list = attendanceService.getManagerAttendanceList(departmentId, startDate, endDate, attendanceStatus);
60
+ return ResponseEntity.ok(list);
61
+ }
62
+ }
src/main/java/com/attendenceSystem/module/attendance/controller/AttendanceController.java CHANGED
@@ -43,14 +43,14 @@ public class AttendanceController {
43
  public String checkIn(RedirectAttributes redirectAttributes) {
44
  AttendanceResponse attendance = attendanceService.checkIn();
45
  redirectAttributes.addFlashAttribute("successMessage", "Điểm danh thành công cho " + attendance.fullName());
46
- return Routes.REDIRECT + Routes.Attendance.ROOT;
47
  }
48
 
49
  @PostMapping(Routes.Attendance.CHECK_OUT)
50
  public String checkOut(RedirectAttributes redirectAttributes) {
51
  AttendanceResponse attendance = attendanceService.checkOut();
52
  redirectAttributes.addFlashAttribute("successMessage", "Checkout thành công cho " + attendance.fullName());
53
- return Routes.REDIRECT + Routes.Attendance.ROOT;
54
  }
55
 
56
  @GetMapping(Routes.Attendance.HISTORY)
@@ -83,7 +83,7 @@ public class AttendanceController {
83
  }
84
  attendanceService.createLeaveRequest(createLeaveRequest);
85
  redirectAttributes.addFlashAttribute("successMessage", "Yêu cầu nghỉ phép đã được gửi.");
86
- return Routes.REDIRECT + Routes.Attendance.ROOT + Routes.Attendance.LEAVE;
87
  }
88
 
89
  @ExceptionHandler({
@@ -95,13 +95,13 @@ public class AttendanceController {
95
  })
96
  public String handleBadRequest(RuntimeException ex, RedirectAttributes redirectAttributes) {
97
  redirectAttributes.addFlashAttribute("errorMessage", ex.getMessage());
98
- return Routes.REDIRECT + Routes.Attendance.LEAVE;
99
  }
100
 
101
  @ExceptionHandler(Exception.class)
102
  public String handleUnexpectedError(Exception ex, RedirectAttributes redirectAttributes) {
103
  log.error("Unexpected error in AttendanceController", ex);
104
  redirectAttributes.addFlashAttribute("errorMessage", "Có lỗi xảy ra, vui lòng thử lại sau");
105
- return Routes.REDIRECT + Routes.Attendance.LEAVE;
106
  }
107
  }
 
43
  public String checkIn(RedirectAttributes redirectAttributes) {
44
  AttendanceResponse attendance = attendanceService.checkIn();
45
  redirectAttributes.addFlashAttribute("successMessage", "Điểm danh thành công cho " + attendance.fullName());
46
+ return Views.Attendance.LIST;
47
  }
48
 
49
  @PostMapping(Routes.Attendance.CHECK_OUT)
50
  public String checkOut(RedirectAttributes redirectAttributes) {
51
  AttendanceResponse attendance = attendanceService.checkOut();
52
  redirectAttributes.addFlashAttribute("successMessage", "Checkout thành công cho " + attendance.fullName());
53
+ return Views.Attendance.LIST;
54
  }
55
 
56
  @GetMapping(Routes.Attendance.HISTORY)
 
83
  }
84
  attendanceService.createLeaveRequest(createLeaveRequest);
85
  redirectAttributes.addFlashAttribute("successMessage", "Yêu cầu nghỉ phép đã được gửi.");
86
+ return Views.Attendance.LEAVE_LIST;
87
  }
88
 
89
  @ExceptionHandler({
 
95
  })
96
  public String handleBadRequest(RuntimeException ex, RedirectAttributes redirectAttributes) {
97
  redirectAttributes.addFlashAttribute("errorMessage", ex.getMessage());
98
+ return Views.Attendance.LIST;
99
  }
100
 
101
  @ExceptionHandler(Exception.class)
102
  public String handleUnexpectedError(Exception ex, RedirectAttributes redirectAttributes) {
103
  log.error("Unexpected error in AttendanceController", ex);
104
  redirectAttributes.addFlashAttribute("errorMessage", "Có lỗi xảy ra, vui lòng thử lại sau");
105
+ return Views.Attendance.LIST;
106
  }
107
  }
src/main/java/com/attendenceSystem/module/attendance/dto/response/AttendanceResponse.java CHANGED
@@ -4,6 +4,7 @@ import java.time.Instant;
4
  import java.time.LocalDate;
5
 
6
  import com.attendenceSystem.module.attendance.entity.enums.AttendanceStatus;
 
7
 
8
  import lombok.Builder;
9
 
@@ -12,6 +13,7 @@ public record AttendanceResponse(
12
  Long id,
13
  Long userId,
14
  String fullName,
 
15
  LocalDate attendanceDate,
16
  Instant checkInTime,
17
  Instant checkOutTime,
@@ -20,4 +22,4 @@ public record AttendanceResponse(
20
  boolean earlyLeave,
21
  long workingMinutes,
22
  String note) {
23
- }
 
4
  import java.time.LocalDate;
5
 
6
  import com.attendenceSystem.module.attendance.entity.enums.AttendanceStatus;
7
+ import com.attendenceSystem.module.user.entity.enums.Department;
8
 
9
  import lombok.Builder;
10
 
 
13
  Long id,
14
  Long userId,
15
  String fullName,
16
+ Department department,
17
  LocalDate attendanceDate,
18
  Instant checkInTime,
19
  Instant checkOutTime,
 
22
  boolean earlyLeave,
23
  long workingMinutes,
24
  String note) {
25
+ }
src/main/java/com/attendenceSystem/module/attendance/dto/response/ManagerStatsResponse.java ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.attendance.dto.response;
2
+
3
+ import lombok.AllArgsConstructor;
4
+ import lombok.Builder;
5
+ import lombok.Data;
6
+ import lombok.NoArgsConstructor;
7
+
8
+ @Data
9
+ @Builder
10
+ @NoArgsConstructor
11
+ @AllArgsConstructor
12
+ public class ManagerStatsResponse {
13
+ private long totalEmployees;
14
+ private long checkedIn; // Đã điểm danh (PRESENT + LATE)
15
+ private long checkedOut; // Đã checkout về sớm hoặc đúng giờ
16
+ private long lateArrivals; // Điểm danh muộn
17
+ private long absent; // Vắng mặt (không có bản ghi)
18
+ }
src/main/java/com/attendenceSystem/module/attendance/mapper/response/AttendanceResponseMapper.java CHANGED
@@ -22,6 +22,7 @@ public class AttendanceResponseMapper {
22
  .id(attendance.getId())
23
  .userId(attendance.getUser().getId())
24
  .fullName(attendance.getUser().getFullName())
 
25
  .attendanceDate(attendance.getAttendanceDate())
26
  .checkInTime(attendance.getCheckInTime())
27
  .checkOutTime(attendance.getCheckOutTime())
 
22
  .id(attendance.getId())
23
  .userId(attendance.getUser().getId())
24
  .fullName(attendance.getUser().getFullName())
25
+ .department(attendance.getUser().getDepartment())
26
  .attendanceDate(attendance.getAttendanceDate())
27
  .checkInTime(attendance.getCheckInTime())
28
  .checkOutTime(attendance.getCheckOutTime())
src/main/java/com/attendenceSystem/module/attendance/repository/AttendanceRecordRepository.java CHANGED
@@ -1,6 +1,7 @@
1
  package com.attendenceSystem.module.attendance.repository;
2
 
3
  import java.time.LocalDate;
 
4
  import java.util.Optional;
5
 
6
  import org.springframework.data.domain.Page;
@@ -29,4 +30,14 @@ public interface AttendanceRecordRepository extends JpaRepository<AttendanceReco
29
 
30
  long countByCheckInTimeNotNullAndCheckOutTimeNotNull();
31
 
 
 
 
 
 
 
 
 
 
 
32
  }
 
1
  package com.attendenceSystem.module.attendance.repository;
2
 
3
  import java.time.LocalDate;
4
+ import java.util.List;
5
  import java.util.Optional;
6
 
7
  import org.springframework.data.domain.Page;
 
30
 
31
  long countByCheckInTimeNotNullAndCheckOutTimeNotNull();
32
 
33
+ List<AttendanceRecord> findByAttendanceDate(LocalDate attendanceDate);
34
+
35
+ List<AttendanceRecord> findByAttendanceDateBetween(LocalDate startDate, LocalDate endDate);
36
+
37
+ List<AttendanceRecord> findByAttendanceDateBetweenAndStatus(LocalDate startDate, LocalDate endDate, com.attendenceSystem.module.attendance.entity.enums.AttendanceStatus status);
38
+
39
+ List<AttendanceRecord> findAllByOrderByAttendanceDateDesc();
40
+
41
+ long countByAttendanceDateAndStatus(LocalDate attendanceDate, com.attendenceSystem.module.attendance.entity.enums.AttendanceStatus status);
42
+
43
  }
src/main/java/com/attendenceSystem/module/attendance/service/AttendanceService.java CHANGED
@@ -3,10 +3,14 @@ package com.attendenceSystem.module.attendance.service;
3
  import org.springframework.data.domain.Page;
4
  import org.springframework.data.domain.Pageable;
5
 
 
 
 
6
  import com.attendenceSystem.module.attendance.dto.request.CreateLeaveRequest;
7
  import com.attendenceSystem.module.attendance.dto.response.AttendanceResponse;
8
  import com.attendenceSystem.module.attendance.dto.response.LeaveDetailResponse;
9
  import com.attendenceSystem.module.attendance.dto.response.LeaveRequestResponse;
 
10
 
11
  public interface AttendanceService {
12
 
@@ -16,6 +20,14 @@ public interface AttendanceService {
16
 
17
  Page<AttendanceResponse> getAttendanceHistory(Pageable pageable);
18
 
 
 
 
 
 
 
 
 
19
  LeaveRequestResponse createLeaveRequest(CreateLeaveRequest request);
20
 
21
  Page<LeaveRequestResponse> getLeaveRequests(Pageable pageable);
 
3
  import org.springframework.data.domain.Page;
4
  import org.springframework.data.domain.Pageable;
5
 
6
+ import java.time.LocalDate;
7
+ import java.util.List;
8
+
9
  import com.attendenceSystem.module.attendance.dto.request.CreateLeaveRequest;
10
  import com.attendenceSystem.module.attendance.dto.response.AttendanceResponse;
11
  import com.attendenceSystem.module.attendance.dto.response.LeaveDetailResponse;
12
  import com.attendenceSystem.module.attendance.dto.response.LeaveRequestResponse;
13
+ import com.attendenceSystem.module.attendance.dto.response.ManagerStatsResponse;
14
 
15
  public interface AttendanceService {
16
 
 
20
 
21
  Page<AttendanceResponse> getAttendanceHistory(Pageable pageable);
22
 
23
+ ManagerStatsResponse getManagerStats(String departmentId, LocalDate startDate, LocalDate endDate);
24
+
25
+ ManagerStatsResponse getManagerStats(String departmentId, LocalDate startDate, LocalDate endDate, com.attendenceSystem.module.attendance.entity.enums.AttendanceStatus status);
26
+
27
+ List<AttendanceResponse> getManagerAttendanceList(String departmentId, LocalDate startDate, LocalDate endDate);
28
+
29
+ List<AttendanceResponse> getManagerAttendanceList(String departmentId, LocalDate startDate, LocalDate endDate, com.attendenceSystem.module.attendance.entity.enums.AttendanceStatus status);
30
+
31
  LeaveRequestResponse createLeaveRequest(CreateLeaveRequest request);
32
 
33
  Page<LeaveRequestResponse> getLeaveRequests(Pageable pageable);
src/main/java/com/attendenceSystem/module/attendance/service/impl/AttendanceServiceImpl.java CHANGED
@@ -2,11 +2,11 @@ package com.attendenceSystem.module.attendance.service.impl;
2
 
3
  import java.time.Instant;
4
  import java.time.LocalDate;
 
5
  import java.util.Optional;
6
 
 
7
  import org.springframework.dao.DataIntegrityViolationException;
8
- import org.springframework.data.domain.Page;
9
- import org.springframework.data.domain.Pageable;
10
  import org.springframework.orm.ObjectOptimisticLockingFailureException;
11
  import org.springframework.stereotype.Service;
12
  import org.springframework.transaction.annotation.Transactional;
@@ -15,6 +15,7 @@ import com.attendenceSystem.module.attendance.dto.request.CreateLeaveRequest;
15
  import com.attendenceSystem.module.attendance.dto.response.AttendanceResponse;
16
  import com.attendenceSystem.module.attendance.dto.response.LeaveDetailResponse;
17
  import com.attendenceSystem.module.attendance.dto.response.LeaveRequestResponse;
 
18
  import com.attendenceSystem.module.attendance.entity.AttendanceRecord;
19
  import com.attendenceSystem.module.attendance.entity.enums.AttendanceStatus;
20
  import com.attendenceSystem.module.attendance.exception.AlreadyCheckedInException;
@@ -32,6 +33,8 @@ import com.attendenceSystem.module.attendance.service.AttendanceService;
32
  import com.attendenceSystem.module.attendance.util.AttendanceCalculator;
33
  import com.attendenceSystem.module.attendance.util.TimeZoneProvider;
34
  import com.attendenceSystem.module.user.entity.User;
 
 
35
  import com.attendenceSystem.module.user.repository.UserRepository;
36
  import com.attendenceSystem.util.SecurityUtil;
37
 
@@ -49,6 +52,12 @@ public class AttendanceServiceImpl implements AttendanceService {
49
  private final AttendanceCalculator attendanceCalculator;
50
  private final TimeZoneProvider timeZoneProvider;
51
 
 
 
 
 
 
 
52
  @Transactional
53
  @Override
54
  public AttendanceResponse checkIn() {
@@ -110,7 +119,7 @@ public class AttendanceServiceImpl implements AttendanceService {
110
  }
111
 
112
  @Override
113
- public Page<AttendanceResponse> getAttendanceHistory(final Pageable pageable) {
114
  User user = getCurrentUser();
115
 
116
  return attendanceRecordRepository
@@ -118,6 +127,131 @@ public class AttendanceServiceImpl implements AttendanceService {
118
  .map(attendanceResponseMapper::fromEntity);
119
  }
120
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  @Transactional
122
  @Override
123
  public LeaveRequestResponse createLeaveRequest(final CreateLeaveRequest request) {
@@ -137,14 +271,14 @@ public class AttendanceServiceImpl implements AttendanceService {
137
  }
138
 
139
  @Override
140
- public Page<LeaveRequestResponse> getLeaveRequests(final Pageable pageable) {
141
  User user = getCurrentUser();
142
  return leaveRequestRepository.findByUser(user, pageable)
143
  .map(leaveRequestResponseMapper::fromEntity);
144
  }
145
 
146
  @Override
147
- public Page<LeaveRequestResponse> getAllLeaveRequests(final Pageable pageable) {
148
  return leaveRequestRepository.findAll(pageable).map(leaveRequestResponseMapper::fromEntity);
149
  }
150
 
 
2
 
3
  import java.time.Instant;
4
  import java.time.LocalDate;
5
+ import java.util.List;
6
  import java.util.Optional;
7
 
8
+ import org.springframework.beans.factory.annotation.Value;
9
  import org.springframework.dao.DataIntegrityViolationException;
 
 
10
  import org.springframework.orm.ObjectOptimisticLockingFailureException;
11
  import org.springframework.stereotype.Service;
12
  import org.springframework.transaction.annotation.Transactional;
 
15
  import com.attendenceSystem.module.attendance.dto.response.AttendanceResponse;
16
  import com.attendenceSystem.module.attendance.dto.response.LeaveDetailResponse;
17
  import com.attendenceSystem.module.attendance.dto.response.LeaveRequestResponse;
18
+ import com.attendenceSystem.module.attendance.dto.response.ManagerStatsResponse;
19
  import com.attendenceSystem.module.attendance.entity.AttendanceRecord;
20
  import com.attendenceSystem.module.attendance.entity.enums.AttendanceStatus;
21
  import com.attendenceSystem.module.attendance.exception.AlreadyCheckedInException;
 
33
  import com.attendenceSystem.module.attendance.util.AttendanceCalculator;
34
  import com.attendenceSystem.module.attendance.util.TimeZoneProvider;
35
  import com.attendenceSystem.module.user.entity.User;
36
+ import com.attendenceSystem.module.user.entity.enums.Department;
37
+ import com.attendenceSystem.module.user.entity.enums.Role;
38
  import com.attendenceSystem.module.user.repository.UserRepository;
39
  import com.attendenceSystem.util.SecurityUtil;
40
 
 
52
  private final AttendanceCalculator attendanceCalculator;
53
  private final TimeZoneProvider timeZoneProvider;
54
 
55
+ @Value("${attendance.start-work:08:00}")
56
+ private String startWork;
57
+
58
+ @Value("${attendance.end-work:17:00}")
59
+ private String endWork;
60
+
61
  @Transactional
62
  @Override
63
  public AttendanceResponse checkIn() {
 
119
  }
120
 
121
  @Override
122
+ public org.springframework.data.domain.Page<AttendanceResponse> getAttendanceHistory(final org.springframework.data.domain.Pageable pageable) {
123
  User user = getCurrentUser();
124
 
125
  return attendanceRecordRepository
 
127
  .map(attendanceResponseMapper::fromEntity);
128
  }
129
 
130
+ @Override
131
+ public ManagerStatsResponse getManagerStats(String departmentId, LocalDate startDate, LocalDate endDate) {
132
+ return getManagerStats(departmentId, startDate, endDate, null);
133
+ }
134
+
135
+ @Override
136
+ public List<AttendanceResponse> getManagerAttendanceList(String departmentId, LocalDate startDate, LocalDate endDate) {
137
+ return getManagerAttendanceList(departmentId, startDate, endDate, null);
138
+ }
139
+
140
+ private List<AttendanceRecord> getFilteredRecords(LocalDate startDate, LocalDate endDate, AttendanceStatus status) {
141
+ if (status != null) {
142
+ return attendanceRecordRepository.findByAttendanceDateBetweenAndStatus(startDate, endDate, status);
143
+ }
144
+ return attendanceRecordRepository.findByAttendanceDateBetween(startDate, endDate);
145
+ }
146
+
147
+ public ManagerStatsResponse getManagerStats(String departmentId, LocalDate startDate, LocalDate endDate, AttendanceStatus status) {
148
+ List<User> employees;
149
+ if (departmentId != null && !departmentId.isEmpty()) {
150
+ Department dept = Department.fromValue(departmentId);
151
+ if (dept == null) {
152
+ employees = userRepository.findByRoleNot(Role.ADMIN);
153
+ } else {
154
+ employees = userRepository.findByDepartmentAndRoleNot(dept, Role.ADMIN);
155
+ }
156
+ } else {
157
+ employees = userRepository.findByRoleNot(Role.ADMIN);
158
+ }
159
+
160
+ long totalEmployees = employees.size();
161
+ if (totalEmployees == 0) {
162
+ return ManagerStatsResponse.builder()
163
+ .totalEmployees(0)
164
+ .checkedIn(0)
165
+ .checkedOut(0)
166
+ .lateArrivals(0)
167
+ .absent(0)
168
+ .build();
169
+ }
170
+
171
+ LocalDate effectiveStartDate = startDate;
172
+ LocalDate effectiveEndDate = endDate;
173
+ if (effectiveStartDate == null && effectiveEndDate == null) {
174
+ effectiveStartDate = LocalDate.now(timeZoneProvider.getZoneId()).minusMonths(6);
175
+ effectiveEndDate = LocalDate.now(timeZoneProvider.getZoneId());
176
+ } else if (effectiveStartDate != null && effectiveEndDate == null) {
177
+ effectiveEndDate = effectiveStartDate;
178
+ } else if (effectiveStartDate == null && effectiveEndDate != null) {
179
+ effectiveStartDate = effectiveEndDate;
180
+ }
181
+
182
+ List<AttendanceRecord> records = getFilteredRecords(effectiveStartDate, effectiveEndDate, status);
183
+
184
+ long checkedIn = 0;
185
+ long checkedOut = 0;
186
+ long lateArrivals = 0;
187
+ long absent = 0;
188
+
189
+ for (User emp : employees) {
190
+ boolean hasRecord = false;
191
+ for (AttendanceRecord record : records) {
192
+ if (record.getUser() != null && record.getUser().getId().equals(emp.getId())) {
193
+ hasRecord = true;
194
+ if (record.getStatus() == AttendanceStatus.LATE) {
195
+ lateArrivals++;
196
+ }
197
+ checkedIn++;
198
+ if (record.getCheckOutTime() != null) {
199
+ checkedOut++;
200
+ }
201
+ break;
202
+ }
203
+ }
204
+ if (!hasRecord) {
205
+ absent++;
206
+ }
207
+ }
208
+
209
+ return ManagerStatsResponse.builder()
210
+ .totalEmployees(totalEmployees)
211
+ .checkedIn(checkedIn)
212
+ .checkedOut(checkedOut)
213
+ .lateArrivals(lateArrivals)
214
+ .absent(absent)
215
+ .build();
216
+ }
217
+
218
+ public List<AttendanceResponse> getManagerAttendanceList(String departmentId, LocalDate startDate, LocalDate endDate, AttendanceStatus status) {
219
+ List<User> employees;
220
+ if (departmentId != null && !departmentId.isEmpty()) {
221
+ Department dept = Department.fromValue(departmentId);
222
+ if (dept == null) {
223
+ employees = userRepository.findByRoleNot(Role.ADMIN);
224
+ } else {
225
+ employees = userRepository.findByDepartmentAndRoleNot(dept, Role.ADMIN);
226
+ }
227
+ } else {
228
+ employees = userRepository.findByRoleNot(Role.ADMIN);
229
+ }
230
+
231
+ LocalDate effectiveStartDate = startDate;
232
+ LocalDate effectiveEndDate = endDate;
233
+ if (effectiveStartDate == null && effectiveEndDate == null) {
234
+ effectiveStartDate = LocalDate.now(timeZoneProvider.getZoneId()).minusMonths(6);
235
+ effectiveEndDate = LocalDate.now(timeZoneProvider.getZoneId());
236
+ } else if (effectiveStartDate != null && effectiveEndDate == null) {
237
+ effectiveEndDate = effectiveStartDate;
238
+ } else if (effectiveStartDate == null && effectiveEndDate != null) {
239
+ effectiveStartDate = effectiveEndDate;
240
+ }
241
+
242
+ List<AttendanceRecord> records = getFilteredRecords(effectiveStartDate, effectiveEndDate, status);
243
+
244
+ return records.stream()
245
+ .map(attendanceResponseMapper::fromEntity)
246
+ .sorted((a, b) -> {
247
+ if (a.attendanceDate() == null && b.attendanceDate() == null) return 0;
248
+ if (a.attendanceDate() == null) return 1;
249
+ if (b.attendanceDate() == null) return -1;
250
+ return b.attendanceDate().compareTo(a.attendanceDate());
251
+ })
252
+ .toList();
253
+ }
254
+
255
  @Transactional
256
  @Override
257
  public LeaveRequestResponse createLeaveRequest(final CreateLeaveRequest request) {
 
271
  }
272
 
273
  @Override
274
+ public org.springframework.data.domain.Page<LeaveRequestResponse> getLeaveRequests(final org.springframework.data.domain.Pageable pageable) {
275
  User user = getCurrentUser();
276
  return leaveRequestRepository.findByUser(user, pageable)
277
  .map(leaveRequestResponseMapper::fromEntity);
278
  }
279
 
280
  @Override
281
+ public org.springframework.data.domain.Page<LeaveRequestResponse> getAllLeaveRequests(final org.springframework.data.domain.Pageable pageable) {
282
  return leaveRequestRepository.findAll(pageable).map(leaveRequestResponseMapper::fromEntity);
283
  }
284
 
src/main/java/com/attendenceSystem/module/dashboard/dto/response/DailyAttendanceStats.java ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.dashboard.dto.response;
2
+
3
+ public record DailyAttendanceStats(
4
+ String dayName,
5
+ Long present,
6
+ Long late,
7
+ Long absent
8
+ ) {
9
+ }
src/main/java/com/attendenceSystem/module/dashboard/dto/response/ManagerDashboardResponse.java CHANGED
@@ -4,15 +4,20 @@ import org.springframework.data.domain.Page;
4
 
5
  import com.attendenceSystem.module.attendance.dto.response.AttendanceResponse;
6
 
7
- public record ManagerDashboardResponse(
8
 
 
9
  Long totalEmployees,
10
 
11
  Long attendedEmployees,
12
 
 
 
13
  Long absentEmployees,
14
 
15
- Page<AttendanceResponse> attendanceHistory
 
 
16
 
17
  ) {
18
- }
 
4
 
5
  import com.attendenceSystem.module.attendance.dto.response.AttendanceResponse;
6
 
7
+ import java.util.List;
8
 
9
+ public record ManagerDashboardResponse(
10
  Long totalEmployees,
11
 
12
  Long attendedEmployees,
13
 
14
+ Long lateEmployees,
15
+
16
  Long absentEmployees,
17
 
18
+ Page<AttendanceResponse> attendanceHistory,
19
+
20
+ List<DailyAttendanceStats> weeklyStats
21
 
22
  ) {
23
+ }
src/main/java/com/attendenceSystem/module/dashboard/mapper/response/DashboardResponseMapper.java CHANGED
@@ -10,6 +10,7 @@ import com.attendenceSystem.module.attendance.dto.response.AttendanceResponse;
10
  import com.attendenceSystem.module.dashboard.dto.response.AdminDashboardResponse;
11
  import com.attendenceSystem.module.dashboard.dto.response.ManagerDashboardResponse;
12
  import com.attendenceSystem.module.dashboard.dto.response.EmployeeDashboardResponse;
 
13
 
14
  @Component
15
  public class DashboardResponseMapper {
@@ -22,8 +23,9 @@ public class DashboardResponseMapper {
22
  }
23
 
24
  public ManagerDashboardResponse toManagerDashboardResponse(long totalEmployees, long attendedEmployees,
25
- long absentEmployees, Page<AttendanceResponse> attendanceHistory) {
26
- return new ManagerDashboardResponse(totalEmployees, attendedEmployees, absentEmployees, attendanceHistory);
 
27
  }
28
 
29
  public EmployeeDashboardResponse toEmployeeDashboardResponse(long totalReports, long acceptedReports,
 
10
  import com.attendenceSystem.module.dashboard.dto.response.AdminDashboardResponse;
11
  import com.attendenceSystem.module.dashboard.dto.response.ManagerDashboardResponse;
12
  import com.attendenceSystem.module.dashboard.dto.response.EmployeeDashboardResponse;
13
+ import com.attendenceSystem.module.dashboard.dto.response.DailyAttendanceStats;
14
 
15
  @Component
16
  public class DashboardResponseMapper {
 
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,
src/main/java/com/attendenceSystem/module/dashboard/service/impl/DashboardServiceImpl.java CHANGED
@@ -1,7 +1,10 @@
1
  package com.attendenceSystem.module.dashboard.service.impl;
2
 
 
3
  import java.time.LocalDate;
 
4
  import java.util.Locale;
 
5
 
6
  import org.springframework.data.domain.Page;
7
  import org.springframework.data.domain.PageRequest;
@@ -61,13 +64,33 @@ public class DashboardServiceImpl implements DashboardService {
61
  public ManagerDashboardResponse getManagerDashboard() {
62
  long totalEmployees = userRepository.countByRoleNot(Role.ADMIN);
63
  LocalDate today = LocalDate.now();
64
- long attendedEmployees = attendanceRecordRepository.countByAttendanceDate(today);
65
- long absentEmployees = Math.max(0, totalEmployees - attendedEmployees);
 
 
66
  Page<AttendanceResponse> attendanceHistory = attendanceRecordRepository
67
  .findAllByOrderByAttendanceDateDesc(PageRequest.of(0, 10))
68
  .map(attendanceResponseMapper::fromEntity);
69
- return dashboardResponseMapper.toManagerDashboardResponse(totalEmployees, attendedEmployees, absentEmployees,
70
- attendanceHistory);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  }
72
 
73
  @Override
 
1
  package com.attendenceSystem.module.dashboard.service.impl;
2
 
3
+ import java.time.DayOfWeek;
4
  import java.time.LocalDate;
5
+ import java.time.temporal.TemporalAdjusters;
6
  import java.util.Locale;
7
+ import java.util.stream.IntStream;
8
 
9
  import org.springframework.data.domain.Page;
10
  import org.springframework.data.domain.PageRequest;
 
64
  public ManagerDashboardResponse getManagerDashboard() {
65
  long totalEmployees = userRepository.countByRoleNot(Role.ADMIN);
66
  LocalDate today = LocalDate.now();
67
+ long presentToday = attendanceRecordRepository.countByAttendanceDateAndStatus(today, com.attendenceSystem.module.attendance.entity.enums.AttendanceStatus.PRESENT);
68
+ long lateToday = attendanceRecordRepository.countByAttendanceDateAndStatus(today, com.attendenceSystem.module.attendance.entity.enums.AttendanceStatus.LATE);
69
+ long attendedToday = presentToday + lateToday;
70
+ long absentToday = Math.max(0, totalEmployees - attendedToday);
71
  Page<AttendanceResponse> attendanceHistory = attendanceRecordRepository
72
  .findAllByOrderByAttendanceDateDesc(PageRequest.of(0, 10))
73
  .map(attendanceResponseMapper::fromEntity);
74
+
75
+ var monday = TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY).adjustInto(today);
76
+ var weeklyStats = IntStream.rangeClosed(2, 6)
77
+ .mapToObj(dayOfWeek -> {
78
+ LocalDate date = ((LocalDate) monday).plusDays(dayOfWeek - DayOfWeek.MONDAY.getValue());
79
+ long present = attendanceRecordRepository.countByAttendanceDateAndStatus(date, com.attendenceSystem.module.attendance.entity.enums.AttendanceStatus.PRESENT);
80
+ long late = attendanceRecordRepository.countByAttendanceDateAndStatus(date, com.attendenceSystem.module.attendance.entity.enums.AttendanceStatus.LATE);
81
+ long attended = present + late;
82
+ long absent = Math.max(0, totalEmployees - attended);
83
+ return new com.attendenceSystem.module.dashboard.dto.response.DailyAttendanceStats(
84
+ "T" + (dayOfWeek - 1),
85
+ present,
86
+ late,
87
+ absent
88
+ );
89
+ })
90
+ .toList();
91
+
92
+ return dashboardResponseMapper.toManagerDashboardResponse(totalEmployees, presentToday, lateToday, absentToday,
93
+ attendanceHistory, weeklyStats);
94
  }
95
 
96
  @Override
src/main/java/com/attendenceSystem/module/faceid/api/FaceIdApiController.java CHANGED
@@ -242,7 +242,8 @@ public class FaceIdApiController {
242
  String directory = "face_samples";
243
  String relativePath = directory + "/" + userId + "/" + fileName;
244
 
245
- Path targetPath = Paths.get(relativePath);
 
246
 
247
  // Tạo MultipartFile ảo
248
  MultipartFile multipartFile = createMultipartFile(fileName, imageBytes);
 
242
  String directory = "face_samples";
243
  String relativePath = directory + "/" + userId + "/" + fileName;
244
 
245
+ // Sử dụng Paths.get với nhiều tham số để tránh vấn đề với đường dẫn tương đối
246
+ Path targetPath = Paths.get(directory, String.valueOf(userId), fileName);
247
 
248
  // Tạo MultipartFile ảo
249
  MultipartFile multipartFile = createMultipartFile(fileName, imageBytes);
src/main/java/com/attendenceSystem/module/report/api/ReportApiController.java CHANGED
@@ -13,7 +13,7 @@ import com.attendenceSystem.module.report.dto.response.ReportDetailResponse;
13
  import com.attendenceSystem.module.report.dto.response.ReportResponse;
14
  import com.attendenceSystem.module.report.service.ReportService;
15
  import com.attendenceSystem.module.user.entity.User;
16
- import com.attendenceSystem.module.user.entity.enums.Specialization;
17
  import com.attendenceSystem.module.user.entity.enums.Role;
18
  import com.attendenceSystem.module.user.repository.UserRepository;
19
 
@@ -55,9 +55,12 @@ public class ReportApiController {
55
  }
56
 
57
  @GetMapping("/users/by-department/{departmentId}")
58
- public ResponseEntity<List<User>> getUsersByDepartment(@PathVariable Integer departmentId) {
59
- Specialization specialization = Specialization.fromValue(departmentId);
60
- List<User> users = userRepository.findBySpecializationAndRoleNot(specialization, Role.ADMIN);
 
 
 
61
  return ResponseEntity.ok(users);
62
  }
63
  }
 
13
  import com.attendenceSystem.module.report.dto.response.ReportResponse;
14
  import com.attendenceSystem.module.report.service.ReportService;
15
  import com.attendenceSystem.module.user.entity.User;
16
+ import com.attendenceSystem.module.user.entity.enums.Department;
17
  import com.attendenceSystem.module.user.entity.enums.Role;
18
  import com.attendenceSystem.module.user.repository.UserRepository;
19
 
 
55
  }
56
 
57
  @GetMapping("/users/by-department/{departmentId}")
58
+ public ResponseEntity<List<User>> getUsersByDepartment(@PathVariable String departmentId) {
59
+ Department department = Department.fromValue(departmentId);
60
+ if (department == null) {
61
+ return ResponseEntity.ok(List.of());
62
+ }
63
+ List<User> users = userRepository.findByDepartmentAndRoleNot(department, Role.ADMIN);
64
  return ResponseEntity.ok(users);
65
  }
66
  }
src/main/java/com/attendenceSystem/module/report/controller/ReportController.java CHANGED
@@ -12,7 +12,7 @@ import com.attendenceSystem.constant.Routes;
12
  import com.attendenceSystem.constant.Views;
13
  import com.attendenceSystem.module.report.dto.request.CreateReportRequest;
14
  import com.attendenceSystem.module.report.service.ReportService;
15
- import com.attendenceSystem.module.user.entity.enums.Specialization;
16
 
17
  import lombok.RequiredArgsConstructor;
18
  import org.springframework.ui.Model;
@@ -31,7 +31,7 @@ public class ReportController {
31
 
32
  @GetMapping(Routes.Action.CREATE)
33
  public String reportCreate(Model model) {
34
- model.addAttribute("departments", Specialization.values());
35
  return Views.Document.CREATE;
36
  }
37
 
@@ -39,7 +39,7 @@ public class ReportController {
39
  public String submitReport(
40
  @RequestParam("title") String title,
41
  @RequestParam("content") String content,
42
- @RequestParam("departmentId") Integer departmentId,
43
  @RequestParam(value = "sharedUserIds", required = false) Long[] sharedUserIds,
44
  @RequestParam(value = "files", required = false) MultipartFile[] files,
45
  @RequestParam(value = "link", required = false) String link,
@@ -58,7 +58,7 @@ public class ReportController {
58
  return Routes.REDIRECT + Routes.Report.ROOT + "?success=true";
59
  } catch (Exception e) {
60
  model.addAttribute("errorMessage", e.getMessage());
61
- model.addAttribute("departments", Specialization.values());
62
  return Views.Document.CREATE;
63
  }
64
  }
 
12
  import com.attendenceSystem.constant.Views;
13
  import com.attendenceSystem.module.report.dto.request.CreateReportRequest;
14
  import com.attendenceSystem.module.report.service.ReportService;
15
+ import com.attendenceSystem.module.user.entity.enums.Department;
16
 
17
  import lombok.RequiredArgsConstructor;
18
  import org.springframework.ui.Model;
 
31
 
32
  @GetMapping(Routes.Action.CREATE)
33
  public String reportCreate(Model model) {
34
+ model.addAttribute("departments", Department.values());
35
  return Views.Document.CREATE;
36
  }
37
 
 
39
  public String submitReport(
40
  @RequestParam("title") String title,
41
  @RequestParam("content") String content,
42
+ @RequestParam("departmentId") String departmentId,
43
  @RequestParam(value = "sharedUserIds", required = false) Long[] sharedUserIds,
44
  @RequestParam(value = "files", required = false) MultipartFile[] files,
45
  @RequestParam(value = "link", required = false) String link,
 
58
  return Routes.REDIRECT + Routes.Report.ROOT + "?success=true";
59
  } catch (Exception e) {
60
  model.addAttribute("errorMessage", e.getMessage());
61
+ model.addAttribute("departments", Department.values());
62
  return Views.Document.CREATE;
63
  }
64
  }
src/main/java/com/attendenceSystem/module/storage/provider/LocalStorageProvider.java CHANGED
@@ -5,6 +5,8 @@ import java.nio.file.Files;
5
  import java.nio.file.Path;
6
  import java.nio.file.Paths;
7
 
 
 
8
  import org.springframework.beans.factory.annotation.Value;
9
  import org.springframework.core.io.FileSystemResource;
10
  import org.springframework.core.io.Resource;
@@ -14,6 +16,8 @@ import org.springframework.web.multipart.MultipartFile;
14
  @Component
15
  public class LocalStorageProvider implements StorageProvider {
16
 
 
 
17
  private final Path rootDir;
18
 
19
  public LocalStorageProvider(@Value("${app.storage.upload-dir}") String uploadDir) {
@@ -25,24 +29,44 @@ public class LocalStorageProvider implements StorageProvider {
25
  }
26
  }
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  @Override
29
  public void save(Path targetPath, MultipartFile file) throws IOException {
30
- Path fullPath = rootDir.resolve(targetPath).normalize();
 
 
 
31
 
32
- // Kiểm tra path traversal
33
  if (!fullPath.startsWith(rootDir)) {
 
34
  throw new SecurityException("Path traversal không hợp lệ: " + targetPath);
35
  }
36
 
 
37
  Files.createDirectories(fullPath.getParent());
38
  file.transferTo(fullPath);
39
  }
40
 
41
  @Override
42
  public Resource load(String path) throws IOException {
43
- Path fullPath = rootDir.resolve(path).normalize();
 
44
 
45
- // Kiểm tra path traversal
46
  if (!fullPath.startsWith(rootDir)) {
47
  throw new SecurityException("Path traversal không hợp lệ: " + path);
48
  }
@@ -56,9 +80,10 @@ public class LocalStorageProvider implements StorageProvider {
56
 
57
  @Override
58
  public void delete(String path) throws IOException {
59
- Path fullPath = rootDir.resolve(path).normalize();
 
60
 
61
- // Kiểm tra path traversal
62
  if (!fullPath.startsWith(rootDir)) {
63
  throw new SecurityException("Path traversal không hợp lệ: " + path);
64
  }
@@ -73,7 +98,8 @@ public class LocalStorageProvider implements StorageProvider {
73
 
74
  @Override
75
  public Path resolvePath(String relativePath) {
76
- Path fullPath = rootDir.resolve(relativePath).normalize();
 
77
  if (!fullPath.startsWith(rootDir)) {
78
  throw new SecurityException("Path traversal không hợp lệ: " + relativePath);
79
  }
 
5
  import java.nio.file.Path;
6
  import java.nio.file.Paths;
7
 
8
+ import org.slf4j.Logger;
9
+ import org.slf4j.LoggerFactory;
10
  import org.springframework.beans.factory.annotation.Value;
11
  import org.springframework.core.io.FileSystemResource;
12
  import org.springframework.core.io.Resource;
 
16
  @Component
17
  public class LocalStorageProvider implements StorageProvider {
18
 
19
+ private static final Logger log = LoggerFactory.getLogger(LocalStorageProvider.class);
20
+
21
  private final Path rootDir;
22
 
23
  public LocalStorageProvider(@Value("${app.storage.upload-dir}") String uploadDir) {
 
29
  }
30
  }
31
 
32
+ /**
33
+ * Chuẩn hóa đường dẫn relative: loại bỏ dấu "/" ở đầu để tránh
34
+ * rootDir.resolve() coi là đường dẫn tuyệt đối.
35
+ */
36
+ private String normalizeRelativePath(String path) {
37
+ if (path == null) return null;
38
+ String normalized = path;
39
+ // Loại bỏ dấu "/" hoặc "\" ở đầu để tránh path traversal false positive
40
+ while (normalized.startsWith("/") || normalized.startsWith("\\")) {
41
+ normalized = normalized.substring(1);
42
+ }
43
+ return normalized;
44
+ }
45
+
46
  @Override
47
  public void save(Path targetPath, MultipartFile file) throws IOException {
48
+ // Chuẩn hóa path để tránh dấu "/" ở đầu gây lỗi path traversal
49
+ String pathStr = normalizeRelativePath(targetPath.toString());
50
+ Path normalizedPath = Paths.get(pathStr);
51
+ Path fullPath = rootDir.resolve(normalizedPath).normalize();
52
 
53
+ // Kiểm tra path traversal thực sự (chứa ..)
54
  if (!fullPath.startsWith(rootDir)) {
55
+ log.warn("Path traversal detected! rootDir={}, targetPath={}, fullPath={}", rootDir, targetPath, fullPath);
56
  throw new SecurityException("Path traversal không hợp lệ: " + targetPath);
57
  }
58
 
59
+ log.debug("Saving file: rootDir={}, targetPath={}, fullPath={}", rootDir, targetPath, fullPath);
60
  Files.createDirectories(fullPath.getParent());
61
  file.transferTo(fullPath);
62
  }
63
 
64
  @Override
65
  public Resource load(String path) throws IOException {
66
+ String normalizedPath = normalizeRelativePath(path);
67
+ Path fullPath = rootDir.resolve(normalizedPath).normalize();
68
 
69
+ // Kiểm tra path traversal thực sự (chứa ..)
70
  if (!fullPath.startsWith(rootDir)) {
71
  throw new SecurityException("Path traversal không hợp lệ: " + path);
72
  }
 
80
 
81
  @Override
82
  public void delete(String path) throws IOException {
83
+ String normalizedPath = normalizeRelativePath(path);
84
+ Path fullPath = rootDir.resolve(normalizedPath).normalize();
85
 
86
+ // Kiểm tra path traversal thực sự (chứa ..)
87
  if (!fullPath.startsWith(rootDir)) {
88
  throw new SecurityException("Path traversal không hợp lệ: " + path);
89
  }
 
98
 
99
  @Override
100
  public Path resolvePath(String relativePath) {
101
+ String normalizedPath = normalizeRelativePath(relativePath);
102
+ Path fullPath = rootDir.resolve(normalizedPath).normalize();
103
  if (!fullPath.startsWith(rootDir)) {
104
  throw new SecurityException("Path traversal không hợp lệ: " + relativePath);
105
  }
src/main/java/com/attendenceSystem/module/user/entity/converter/DepartmentConverter.java CHANGED
@@ -18,11 +18,7 @@ public class DepartmentConverter implements AttributeConverter<Department, Strin
18
  if (dbData == null) {
19
  return null;
20
  }
21
- for (Department dept : Department.values()) {
22
- if (dept.getRoomCode().equals(dbData)) {
23
- return dept;
24
- }
25
- }
26
- return null;
27
  }
 
28
  }
 
18
  if (dbData == null) {
19
  return null;
20
  }
21
+ return Department.valueOf(dbData);
 
 
 
 
 
22
  }
23
+
24
  }
src/main/java/com/attendenceSystem/module/user/entity/enums/Department.java CHANGED
@@ -13,4 +13,13 @@ public enum Department {
13
  private final String roomCode;
14
  private final String joinCode;
15
  private final String displayName;
 
 
 
 
 
 
 
 
 
16
  }
 
13
  private final String roomCode;
14
  private final String joinCode;
15
  private final String displayName;
16
+
17
+ public static Department fromValue(String value) {
18
+ for (Department d : Department.values()) {
19
+ if (d.getRoomCode().equals(value)) {
20
+ return d;
21
+ }
22
+ }
23
+ return null;
24
+ }
25
  }
src/main/java/com/attendenceSystem/module/user/repository/UserRepository.java CHANGED
@@ -9,6 +9,7 @@ import org.springframework.data.jpa.repository.JpaRepository;
9
  import org.springframework.stereotype.Repository;
10
 
11
  import com.attendenceSystem.module.user.entity.User;
 
12
  import com.attendenceSystem.module.user.entity.enums.Specialization;
13
  import com.attendenceSystem.module.user.entity.enums.Role;
14
  import com.attendenceSystem.module.user.entity.enums.Status;
@@ -29,5 +30,7 @@ public interface UserRepository extends JpaRepository<User, Long> {
29
  Optional<User> findByUsername(String username);
30
  boolean existsByPhone(String phone);
31
  List<User> findBySpecializationAndRoleNot(Specialization specialization, Role role);
 
32
  Page<User> findByRole(Role role, Pageable pageable);
 
33
  }
 
9
  import org.springframework.stereotype.Repository;
10
 
11
  import com.attendenceSystem.module.user.entity.User;
12
+ import com.attendenceSystem.module.user.entity.enums.Department;
13
  import com.attendenceSystem.module.user.entity.enums.Specialization;
14
  import com.attendenceSystem.module.user.entity.enums.Role;
15
  import com.attendenceSystem.module.user.entity.enums.Status;
 
30
  Optional<User> findByUsername(String username);
31
  boolean existsByPhone(String phone);
32
  List<User> findBySpecializationAndRoleNot(Specialization specialization, Role role);
33
+ List<User> findByDepartmentAndRoleNot(Department department, Role role);
34
  Page<User> findByRole(Role role, Pageable pageable);
35
+ List<User> findByRoleNot(Role role);
36
  }
src/main/java/com/attendenceSystem/security/SecurityConfig.java CHANGED
@@ -47,7 +47,8 @@ public class SecurityConfig {
47
  http
48
  .authorizeHttpRequests(auth -> auth
49
  .requestMatchers("/auth/**", "/login/**", "/css/**", "/js/**").permitAll()
50
- .requestMatchers("/dashboard/**").authenticated()
 
51
  .anyRequest().permitAll())
52
  .exceptionHandling(ex -> ex
53
  .authenticationEntryPoint((request, response, authException) -> {
 
47
  http
48
  .authorizeHttpRequests(auth -> auth
49
  .requestMatchers("/auth/**", "/login/**", "/css/**", "/js/**").permitAll()
50
+ .requestMatchers("/dashboard/**", "/attendance/**", "/attendance", "/attendanceCheck", "/attendanceHistory", "/attendanceLists").permitAll()
51
+ .requestMatchers("/api/attendance/**").permitAll()
52
  .anyRequest().permitAll())
53
  .exceptionHandling(ex -> ex
54
  .authenticationEntryPoint((request, response, authException) -> {
src/main/resources/static/js/attendance-manager.js ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ (function () {
2
+ const API_BASE = '/api';
3
+
4
+ async function fetchJSON(url) {
5
+ const res = await fetch(url);
6
+ if (!res.ok) {
7
+ const text = await res.text();
8
+ throw new Error(text || 'Request failed');
9
+ }
10
+ return res.json();
11
+ }
12
+
13
+ function el(id) { return document.getElementById(id); }
14
+
15
+ function getListTarget() {
16
+ return el('attendanceList') || el('attendanceTable');
17
+ }
18
+
19
+ function formatTime(iso) {
20
+ if (!iso) return '--';
21
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?Z$/.exec(iso);
22
+ if (match) {
23
+ const [, year, month, day, hour, minute] = match;
24
+ return `${hour}:${minute}`;
25
+ }
26
+ const d = new Date(iso);
27
+ if (isNaN(d.getTime())) return iso;
28
+ return d.toLocaleString('vi-VN', {
29
+ timeZone: 'Asia/Ho_Chi_Minh',
30
+ hour: '2-digit',
31
+ minute: '2-digit',
32
+ hour12: false
33
+ });
34
+ }
35
+
36
+ function formatWorkingHours(minutes) {
37
+ if (minutes === null || minutes === undefined || minutes === '') return '--';
38
+ const numericMinutes = Number(minutes);
39
+ if (!Number.isFinite(numericMinutes) || numericMinutes <= 0) return '0.0';
40
+ const hours = numericMinutes / 60;
41
+ return hours.toFixed(1);
42
+ }
43
+
44
+ async function loadStats() {
45
+ const params = new URLSearchParams();
46
+ const dept = el('departmentFilter')?.value || el('departmentId')?.value;
47
+ const startDate = el('attendanceDateFrom')?.value || el('dateFilter')?.value;
48
+ const endDate = el('attendanceDateTo')?.value || el('dateFilter')?.value;
49
+ const status = el('attendanceStatusFilter')?.value;
50
+ if (dept) params.set('departmentId', dept);
51
+ if (startDate) params.set('startDate', startDate);
52
+ if (endDate) params.set('endDate', endDate);
53
+ if (status) params.set('status', status);
54
+ try {
55
+ const data = await fetchJSON(`${API_BASE}/attendance/manager/stats?${params.toString()}`);
56
+ const totalEl = el('statTotal') || el('totalCount');
57
+ const presentEl = el('statPresent') || el('presentCount');
58
+ const lateEl = el('statLate') || el('lateCount');
59
+ const absentEl = el('statAbsent') || el('absentCount');
60
+ const checkedOutEl = el('statCheckedOut') || el('checkedOutCount');
61
+ const pendingEl = el('pendingCount');
62
+ if (totalEl) totalEl.textContent = data.totalEmployees ?? 0;
63
+ if (presentEl) presentEl.textContent = data.checkedIn ?? 0;
64
+ if (checkedOutEl) checkedOutEl.textContent = data.checkedOut ?? 0;
65
+ if (lateEl) lateEl.textContent = data.lateArrivals ?? 0;
66
+ if (absentEl) absentEl.textContent = data.absent ?? 0;
67
+ if (pendingEl) pendingEl.textContent = Math.max((data.totalEmployees ?? 0) - (data.checkedIn ?? 0), 0);
68
+ } catch (e) {
69
+ console.error('Failed to load stats', e);
70
+ }
71
+ }
72
+
73
+ async function loadList() {
74
+ const tbody = getListTarget();
75
+ if (!tbody) return;
76
+ const params = new URLSearchParams();
77
+ const dept = el('departmentFilter')?.value || el('departmentId')?.value;
78
+ const startDate = el('attendanceDateFrom')?.value || el('dateFilter')?.value;
79
+ const endDate = el('attendanceDateTo')?.value || el('dateFilter')?.value;
80
+ const status = el('attendanceStatusFilter')?.value;
81
+ if (dept) params.set('departmentId', dept);
82
+ if (startDate) params.set('startDate', startDate);
83
+ if (endDate) params.set('endDate', endDate);
84
+ if (status) params.set('status', status);
85
+ try {
86
+ const list = await fetchJSON(`${API_BASE}/attendance/manager/list?${params.toString()}`);
87
+ tbody.innerHTML = '';
88
+ if (!Array.isArray(list) || list.length === 0) {
89
+ tbody.innerHTML = '<tr><td colspan="10" class="text-center">Không có dữ liệu</td></tr>';
90
+ return;
91
+ }
92
+ for (const item of list) {
93
+ const tr = document.createElement('tr');
94
+ const name = item?.fullName || '--';
95
+ const department = item?.department || '--';
96
+ const checkIn = formatTime(item?.checkInTime);
97
+ const checkOut = formatTime(item?.checkOutTime);
98
+ const workingHours = formatWorkingHours(item?.workingMinutes);
99
+ const statusValue = item?.status;
100
+ let statusClass = 'status-absent';
101
+ let statusText = 'Vắng';
102
+ if (statusValue === 'PRESENT' || statusValue === 'LATE') {
103
+ if (statusValue === 'LATE') {
104
+ statusClass = 'status-late';
105
+ statusText = 'Đi muộn';
106
+ } else if (item.checkOutTime) {
107
+ statusClass = 'status-checked-out';
108
+ statusText = 'Đã checkout';
109
+ } else {
110
+ statusClass = 'status-present';
111
+ statusText = 'Đã điểm danh';
112
+ }
113
+ }
114
+ tr.innerHTML = `
115
+ <td>${item?.attendanceDate ?? '--'}</td>
116
+ <td>${name}</td>
117
+ <td>${checkIn}</td>
118
+ <td>${checkOut}</td>
119
+ <td><span class="history-status ${statusClass}">${statusText}</span></td>
120
+ <td>${item?.department ?? '--'}</td>
121
+ <td>${item?.late ? 'Có' : 'Không'}</td>
122
+ <td>${item?.earlyLeave ? 'Có' : 'Không'}</td>
123
+ <td>${workingHours}</td>
124
+ <td>${item?.note ?? '--'}</td>
125
+ `;
126
+ tbody.appendChild(tr);
127
+ }
128
+ } catch (e) {
129
+ console.error('Failed to load list', e);
130
+ const target = getListTarget();
131
+ if (target) target.innerHTML = '<tr><td colspan="10" class="text-center">Lỗi tải dữ liệu</td></tr>';
132
+ }
133
+ }
134
+
135
+ window.loadAttendanceRecords = loadList;
136
+
137
+ window.resetAttendanceFilters = function () {
138
+ const today = new Date().toISOString().slice(0, 10);
139
+ const dateFrom = el('attendanceDateFrom');
140
+ const dateTo = el('attendanceDateTo');
141
+ const statusFilter = el('attendanceStatusFilter');
142
+ const deptFilter = el('departmentFilter');
143
+
144
+ if (dateFrom) dateFrom.value = today;
145
+ if (dateTo) dateTo.value = today;
146
+ if (statusFilter) statusFilter.value = '';
147
+ if (deptFilter) deptFilter.value = '';
148
+ loadStats();
149
+ loadList();
150
+ };
151
+
152
+ document.addEventListener('DOMContentLoaded', () => {
153
+ loadStats();
154
+ loadList();
155
+ const filterForm = el('filterForm');
156
+ if (filterForm) {
157
+ filterForm.addEventListener('submit', (e) => {
158
+ e.preventDefault();
159
+ loadStats();
160
+ loadList();
161
+ });
162
+ }
163
+ });
164
+ })();
src/main/resources/static/js/charts/Chart.js CHANGED
@@ -1,16 +1,50 @@
1
- var options = {
2
- chart: {
3
- type: 'line'
4
- },
5
- series: [{
6
- data: [93, 95, 94, 96, 97, 96]
7
- }],
8
- xaxis: {
9
- categories: ['T1', 'T2', 'T3', 'T4', 'T5', 'T6']
10
- }
11
- };
12
 
13
- new ApexCharts(
14
- document.querySelector("#weeklyAttendanceChart"),
15
- options
16
- ).render();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener("DOMContentLoaded", function () {
 
 
 
 
 
 
 
 
 
 
2
 
3
+ const ctx = document.getElementById("weeklyAttendanceChart");
4
+
5
+ new Chart(ctx, {
6
+ type: "bar",
7
+ data: {
8
+ labels: weeklyStatsData.map(item => item.dayName),
9
+ datasets: [
10
+ {
11
+ label: "Có mặt",
12
+ data: weeklyStatsData.map(item => item.present),
13
+ backgroundColor: "#10b981",
14
+ borderRadius: 4
15
+ },
16
+ {
17
+ label: "Đi muộn",
18
+ data: weeklyStatsData.map(item => item.late),
19
+ backgroundColor: "#f59e0b",
20
+ borderRadius: 4
21
+ },
22
+ {
23
+ label: "Vắng mặt",
24
+ data: weeklyStatsData.map(item => item.absent),
25
+ backgroundColor: "#ef4444",
26
+ borderRadius: 4
27
+ }
28
+ ]
29
+ },
30
+ options: {
31
+ responsive: true,
32
+ maintainAspectRatio: false,
33
+ plugins: {
34
+ legend: {
35
+ display: true,
36
+ position: "top"
37
+ }
38
+ },
39
+ scales: {
40
+ y: {
41
+ beginAtZero: true,
42
+ ticks: {
43
+ stepSize: 1
44
+ }
45
+ }
46
+ }
47
+ }
48
+ });
49
+
50
+ });
src/main/resources/templates/cms/attendance/attendance-check.html CHANGED
@@ -19,134 +19,136 @@
19
  </div>
20
 
21
  <div class="header-info">
22
- <h1 class="header-title">Home Attendance 1</h1>
23
- <div class="header-time">22/22/22</div>
24
  </div>
25
  </div>
26
 
27
  <div class="header-right">
28
-
29
  <div class="header-clock" id="clock">
30
  00:00:00
31
  </div>
32
-
33
  <button class="header-btn theme-toggle" id="themeToggle">
34
  🌙
35
  </button>
36
-
37
-
38
-
39
  </div>
40
  </header>
 
41
  <!-- =======================================================================
42
  BODY
43
  ==========================================================================-->
44
  <div class="container">
45
- <div class="container-attend-left history">
46
- <h3 class="history-title">Lịch sử chấm công</h3>
47
-
48
- <ul class="history-list">
49
-
50
- <li class="history-item">
51
- <div class="history-card">
52
- <span class="history-date">
53
- Ngày 01/01/2024
54
- </span>
55
-
56
- <span class="history-status status-in">
57
- Vào lúc 08:00
58
- </span>
59
-
60
- <span class="history-status status-out">
61
- Ra lúc 17:00
62
- </span>
63
- </div>
64
- </li>
65
-
66
- </ul>
67
- </div>
68
- <div class="container-attend-main">
69
-
70
- <div class="face-scanner scanner-card">
71
-
72
- <!-- <video id="camera" autoplay playsinline></video> -->
73
-
74
- <div class="scan-overlay">
75
- <div class="scan-frame"></div>
76
- <div class="scan-line"></div>
77
- </div>
78
-
79
- <div class="result-card success-result">
80
- ✓ Nhận diện thành công
81
- </div>
82
-
83
  </div>
84
-
85
- <div class="face-guide guide-card">
86
- <p class="guide-title">
87
- Hướng dẫn chấm công bằng nhận diện khuôn mặt:
88
- </p>
89
-
90
- <ol class="guide-list">
91
- <li>Đứng cách camera khoảng 50cm - 1m</li>
92
- <li>Nhìn thẳng vào camera</li>
93
- <li>Giữ khuôn mặt trong khung hình</li>
94
- <li>Hệ thống sẽ tự động nhận diện và ghi nhận</li>
95
- </ol>
96
  </div>
97
-
98
- </div>
99
- <div class="container-attend-right">
100
-
101
- <h3 class="attendance-title">
102
- Thống kê hôm nay
103
- </h3>
104
-
105
- <div class="stat-card success-card">
106
- <p class="attendance-present">
107
- Đã chấm công
108
- </p>
109
- <span class="stat-number">42</span>
110
  </div>
111
-
112
- <div class="stat-card warning-card">
113
- <p class="attendance-late">
114
- Đi muộn
115
- </p>
116
- <span class="stat-number">8</span>
117
  </div>
118
-
119
- <div class="stat-card danger-card">
120
- <p class="attendance-absent">
121
- Chưa chấm công
122
- </p>
123
- <span class="stat-number">12</span>
124
  </div>
 
125
 
126
- <div class="attendance-info">
127
- <p class="attendance-sum">
128
- Tổng nhân viên: 50
129
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
- <p class="attendance-date">
132
- Giờ làm việc: 08:00 - 17:00
133
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  </div>
135
-
136
  </div>
137
- <script>
138
- const btn = document.getElementById("themeToggle");
139
-
140
- btn.addEventListener("click", () => {
141
- document.body.classList.toggle("dark");
142
-
143
- btn.textContent =
144
- document.body.classList.contains("dark")
145
- ? "☀️"
146
- : "🌙";
 
 
 
 
 
 
147
  });
148
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  </body>
150
 
151
-
152
  </html>
 
19
  </div>
20
 
21
  <div class="header-info">
22
+ <h1 class="header-title">Quản điểm danh</h1>
23
+ <div class="header-time" id="currentDate">--</div>
24
  </div>
25
  </div>
26
 
27
  <div class="header-right">
 
28
  <div class="header-clock" id="clock">
29
  00:00:00
30
  </div>
 
31
  <button class="header-btn theme-toggle" id="themeToggle">
32
  🌙
33
  </button>
 
 
 
34
  </div>
35
  </header>
36
+
37
  <!-- =======================================================================
38
  BODY
39
  ==========================================================================-->
40
  <div class="container">
41
+ <!-- Stats -->
42
+ <div class="stats-row">
43
+ <div class="stat-card stat-total">
44
+ <p>Tổng nhân viên</p>
45
+ <span class="stat-number" id="statTotal">0</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  </div>
47
+ <div class="stat-card stat-present">
48
+ <p>Đã điểm danh</p>
49
+ <span class="stat-number" id="statPresent">0</span>
 
 
 
 
 
 
 
 
 
50
  </div>
51
+ <div class="stat-card stat-checked-out">
52
+ <p>Đã checkout</p>
53
+ <span class="stat-number" id="statCheckedOut">0</span>
 
 
 
 
 
 
 
 
 
 
54
  </div>
55
+ <div class="stat-card stat-late">
56
+ <p>Đi muộn</p>
57
+ <span class="stat-number" id="statLate">0</span>
 
 
 
58
  </div>
59
+ <div class="stat-card stat-absent">
60
+ <p>Vắng mặt</p>
61
+ <span class="stat-number" id="statAbsent">0</span>
 
 
 
62
  </div>
63
+ </div>
64
 
65
+ <!-- Filters -->
66
+ <div class="filters-bar">
67
+ <form id="filterForm" class="filters-form">
68
+ <div class="filter-item">
69
+ <label for="dateFilter">Ngày</label>
70
+ <input type="date" id="dateFilter" name="date" />
71
+ </div>
72
+ <div class="filter-item">
73
+ <label for="departmentFilter">Phòng ban</label>
74
+ <select id="departmentFilter" name="departmentId">
75
+ <option value="">Tất cả</option>
76
+ <option value="IT">IT</option>
77
+ <option value="HR">HR</option>
78
+ <option value="MARKETING">MARKETING</option>
79
+ <option value="FINANCE">FINANCE</option>
80
+ </select>
81
+ </div>
82
+ <div class="filter-actions">
83
+ <button type="submit" class="btn btn-primary">Lọc</button>
84
+ <button type="button" class="btn btn-secondary" id="btnRefresh">Làm mới</button>
85
+ </div>
86
+ </form>
87
+ </div>
88
 
89
+ <!-- List -->
90
+ <div class="list-section">
91
+ <h3 class="section-title">Danh sách điểm danh</h3>
92
+ <div class="table-wrapper">
93
+ <table class="attendance-table">
94
+ <thead>
95
+ <tr>
96
+ <th>ID</th>
97
+ <th>Nhân viên</th>
98
+ <th>Phòng ban</th>
99
+ <th>Check-in</th>
100
+ <th>Check-out</th>
101
+ <th>Trạng thái</th>
102
+ </tr>
103
+ </thead>
104
+ <tbody id="attendanceList">
105
+ <tr>
106
+ <td colspan="6" class="text-center">Đang tải dữ liệu...</td>
107
+ </tr>
108
+ </tbody>
109
+ </table>
110
  </div>
 
111
  </div>
112
+ </div>
113
+
114
+ <script src="/js/attendance-manager.js"></script>
115
+ <script>
116
+ const dateInput = document.getElementById("dateFilter");
117
+ const today = new Date().toISOString().slice(0, 10);
118
+ if (dateInput) {
119
+ dateInput.value = today;
120
+ }
121
+ const currentDateEl = document.getElementById("currentDate");
122
+ if (currentDateEl) {
123
+ currentDateEl.textContent = new Date().toLocaleDateString("vi-VN", {
124
+ weekday: "long",
125
+ year: "numeric",
126
+ month: "long",
127
+ day: "numeric"
128
  });
129
+ }
130
+ const btnRefresh = document.getElementById("btnRefresh");
131
+ if (btnRefresh) {
132
+ btnRefresh.addEventListener("click", () => {
133
+ if (dateInput) dateInput.value = today;
134
+ document.getElementById("departmentFilter").value = "";
135
+ loadStats();
136
+ loadList();
137
+ });
138
+ }
139
+ const themeToggle = document.getElementById("themeToggle");
140
+ themeToggle.addEventListener("click", () => {
141
+ document.body.classList.toggle("dark");
142
+ themeToggle.textContent = document.body.classList.contains("dark") ? "☀️" : "🌙";
143
+ });
144
+ function updateClock() {
145
+ const now = new Date();
146
+ const clockEl = document.getElementById("clock");
147
+ if (clockEl) clockEl.textContent = now.toLocaleTimeString("vi-VN");
148
+ }
149
+ updateClock();
150
+ setInterval(updateClock, 1000);
151
+ </script>
152
  </body>
153
 
 
154
  </html>
src/main/resources/templates/cms/attendance/attendance.html CHANGED
@@ -36,7 +36,7 @@
36
  </div>
37
 
38
  <div class="stats-card present">
39
- <span class="stats-value" id="presentCount">0</span>
40
  <span class="stats-label">Đã check out</span>
41
  </div>
42
 
@@ -66,10 +66,9 @@
66
 
67
  <select id="attendanceStatusFilter">
68
  <option value="">Tất cả trạng thái</option>
69
- <option value="present_on_time">Đúng giờ</option>
70
- <option value="late">Đi muộn</option>
71
- <option value="early_leave">Về sớm</option>
72
- <option value="absent">Vắng mặt</option>
73
  </select>
74
 
75
  <input id="attendanceDateFrom" type="date">
@@ -131,6 +130,7 @@
131
 
132
 
133
 
 
134
  </body>
135
 
136
  </html>
 
36
  </div>
37
 
38
  <div class="stats-card present">
39
+ <span class="stats-value" id="checkedOutCount">0</span>
40
  <span class="stats-label">Đã check out</span>
41
  </div>
42
 
 
66
 
67
  <select id="attendanceStatusFilter">
68
  <option value="">Tất cả trạng thái</option>
69
+ <option value="PRESENT">Đã điểm danh</option>
70
+ <option value="LATE">Đi muộn</option>
71
+ <option value="ABSENT">Vắng mặt</option>
 
72
  </select>
73
 
74
  <input id="attendanceDateFrom" type="date">
 
130
 
131
 
132
 
133
+ <script src="/js/attendance-manager.js"></script>
134
  </body>
135
 
136
  </html>
src/main/resources/templates/cms/dashboard/dashboard-manager.html CHANGED
@@ -1,6 +1,6 @@
1
  <!doctype html>
2
 
3
- <html xmlns:th="http://www.thymeleaf.org">
4
 
5
  <head>
6
  <meta charset="UTF-8" />
@@ -27,22 +27,22 @@
27
  <section class="dashboard-overview">
28
 
29
  <div class="stat-card total">
30
- <h3>50</h3>
31
  <p>Tổng nhân viên</p>
32
  </div>
33
 
34
  <div class="stat-card present">
35
- <h3>42</h3>
36
  <p>Có mặt hôm nay</p>
37
  </div>
38
 
39
  <div class="stat-card late">
40
- <h3>8</h3>
41
  <p>Đi muộn hôm nay</p>
42
  </div>
43
 
44
  <div class="stat-card absent">
45
- <h3>10</h3>
46
  <p>Vắng mặt hôm nay</p>
47
  </div>
48
 
@@ -122,6 +122,11 @@
122
  </div>
123
  <script th:src="@{https://cdn.jsdelivr.net/npm/chart.js}"></script>
124
  <script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
 
 
 
 
 
125
  <script th:src="@{/js/charts/Chart.js}"></script>
126
  <script th:src="@{/js/charts/TrendChart.js}"></script>
127
  </body>
 
1
  <!doctype html>
2
 
3
+ <html xmlns:th="http://www.th3.org">
4
 
5
  <head>
6
  <meta charset="UTF-8" />
 
27
  <section class="dashboard-overview">
28
 
29
  <div class="stat-card total">
30
+ <h3 th:text="${dashboard.totalEmployees}">0</h3>
31
  <p>Tổng nhân viên</p>
32
  </div>
33
 
34
  <div class="stat-card present">
35
+ <h3 th:text="${dashboard.attendedEmployees}">0</h3>
36
  <p>Có mặt hôm nay</p>
37
  </div>
38
 
39
  <div class="stat-card late">
40
+ <h3 th:text="${dashboard.lateEmployees}">0</h3>
41
  <p>Đi muộn hôm nay</p>
42
  </div>
43
 
44
  <div class="stat-card absent">
45
+ <h3 th:text="${dashboard.absentEmployees}">0</h3>
46
  <p>Vắng mặt hôm nay</p>
47
  </div>
48
 
 
122
  </div>
123
  <script th:src="@{https://cdn.jsdelivr.net/npm/chart.js}"></script>
124
  <script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
125
+ <script th:inline="javascript">
126
+ var weeklyStatsData = /*[[${dashboard.weeklyStats}]]*/ [];
127
+ console.log('Dashboard data:', /*[[${dashboard}]]*/ 'null');
128
+ console.log('Weekly stats:', weeklyStatsData);
129
+ </script>
130
  <script th:src="@{/js/charts/Chart.js}"></script>
131
  <script th:src="@{/js/charts/TrendChart.js}"></script>
132
  </body>
src/main/resources/templates/cms/document/document-create.html CHANGED
@@ -61,7 +61,7 @@
61
  <select name="departmentId" id="departmentSelect" required>
62
  <option value="" disabled selected>Chọn phòng ban</option>
63
 
64
- <option th:each="d : ${departments}" th:value="${d.value}" th:text="${d.displayName}">
65
  </option>
66
  </select>
67
  </div>
 
61
  <select name="departmentId" id="departmentSelect" required>
62
  <option value="" disabled selected>Chọn phòng ban</option>
63
 
64
+ <option th:each="d : ${departments}" th:value="${d.roomCode}" th:text="${d.displayName}">
65
  </option>
66
  </select>
67
  </div>
src/main/resources/templates/cms/sidebar/sidebar-manage.html CHANGED
@@ -22,7 +22,7 @@
22
  <ul class="sidebar-menu">
23
 
24
  <li class="sidebar-option">
25
- <a href="/dashboardManager" class="sidebar-link">
26
  <span>📊</span>
27
  Dashboard
28
  </a>
 
22
  <ul class="sidebar-menu">
23
 
24
  <li class="sidebar-option">
25
+ <a href="/dashboard/manager" class="sidebar-link">
26
  <span>📊</span>
27
  Dashboard
28
  </a>
src/main/resources/templates/cms/user/user-information.html CHANGED
@@ -125,7 +125,7 @@
125
 
126
  <section class="edit-card" id="editInformation">
127
 
128
- <form th:action="@{/user/update-information}" method="post" th:object="${updateUserInformationRequest}" class="edit-form">
129
 
130
  <div class="edit-body">
131
  <div class="edit-header">
 
125
 
126
  <section class="edit-card" id="editInformation">
127
 
128
+ <form th:action="@{/user/information/update}" method="post" th:object="${updateUserInformationRequest}" class="edit-form">
129
 
130
  <div class="edit-body">
131
  <div class="edit-header">
uploads/face_samples/2/face_2_1.jpg DELETED
Binary file (29.7 kB)
 
uploads/face_samples/2/face_2_2.jpg DELETED
Binary file (29.7 kB)
 
uploads/face_samples/2/face_2_3.jpg DELETED
Binary file (29.9 kB)
 
uploads/face_samples/2/face_2_4.jpg DELETED
Binary file (29.9 kB)
 
uploads/face_samples/2/face_2_5.jpg DELETED
Binary file (29.8 kB)
 
uploads/face_samples/4/face_4_1.jpg ADDED
uploads/face_samples/4/face_4_2.jpg ADDED
uploads/face_samples/4/face_4_3.jpg ADDED
uploads/face_samples/4/face_4_4.jpg ADDED
uploads/face_samples/4/face_4_5.jpg ADDED
uploads/face_samples/5/face_5_1.jpg ADDED
uploads/face_samples/5/face_5_2.jpg ADDED
uploads/face_samples/5/face_5_3.jpg ADDED
uploads/face_samples/5/face_5_4.jpg ADDED
uploads/face_samples/5/face_5_5.jpg ADDED
uploads/face_samples/6/face_6_1.jpg ADDED
uploads/face_samples/6/face_6_2.jpg ADDED
uploads/face_samples/6/face_6_3.jpg ADDED
uploads/face_samples/6/face_6_4.jpg ADDED
uploads/face_samples/6/face_6_5.jpg ADDED