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

Add api face-id, fix bug and refactor code

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. pom.xml +9 -0
  2. src/main/java/com/attendenceSystem/module/attendance/api/AttendanceManagerApiController.java +0 -1
  3. src/main/java/com/attendenceSystem/module/attendance/controller/AttendanceController.java +2 -1
  4. src/main/java/com/attendenceSystem/module/attendance/dto/response/AttendanceResponse.java +3 -3
  5. src/main/java/com/attendenceSystem/module/attendance/dto/response/LeaveDetailResponse.java +2 -1
  6. src/main/java/com/attendenceSystem/module/attendance/dto/response/LeaveRequestResponse.java +2 -2
  7. src/main/java/com/attendenceSystem/module/attendance/entity/AttendanceRecord.java +3 -3
  8. src/main/java/com/attendenceSystem/module/attendance/entity/LeaveRequest.java +2 -2
  9. src/main/java/com/attendenceSystem/module/attendance/mapper/response/AttendanceResponseMapper.java +9 -3
  10. src/main/java/com/attendenceSystem/module/attendance/mapper/response/LeaveDetailResponseMapper.java +0 -1
  11. src/main/java/com/attendenceSystem/module/attendance/mapper/response/LeaveRequestResponseMapper.java +0 -1
  12. src/main/java/com/attendenceSystem/module/attendance/model/DateRange.java +9 -0
  13. src/main/java/com/attendenceSystem/module/attendance/service/AttendanceService.java +12 -2
  14. src/main/java/com/attendenceSystem/module/attendance/service/impl/AttendanceServiceImpl.java +59 -39
  15. src/main/java/com/attendenceSystem/module/attendance/util/AttendanceCalculator.java +5 -5
  16. src/main/java/com/attendenceSystem/module/attendance/util/impl/AttendanceCalculatorImpl.java +5 -5
  17. src/main/java/com/attendenceSystem/module/faceid/controller/FaceIdAttendanceController.java +30 -0
  18. src/main/java/com/attendenceSystem/module/faceid/dto/FaceIdAction.java +8 -0
  19. src/main/java/com/attendenceSystem/module/faceid/dto/FaceIdAttendanceRequest.java +29 -0
  20. src/main/java/com/attendenceSystem/module/faceid/dto/FaceIdAttendanceResponse.java +22 -0
  21. src/main/java/com/attendenceSystem/module/faceid/dto/response/FaceProfileResponse.java +3 -3
  22. src/main/java/com/attendenceSystem/module/faceid/entity/FaceIdRecognition.java +61 -0
  23. src/main/java/com/attendenceSystem/module/faceid/entity/FaceProfile.java +3 -2
  24. src/main/java/com/attendenceSystem/module/faceid/entity/FaceSample.java +2 -1
  25. src/main/java/com/attendenceSystem/module/faceid/filter/FaceIdApiKeyFilter.java +54 -0
  26. src/main/java/com/attendenceSystem/module/faceid/repository/FaceIdRecognitionRepository.java +10 -0
  27. src/main/java/com/attendenceSystem/module/faceid/service/FaceIdAttendanceService.java +8 -0
  28. src/main/java/com/attendenceSystem/module/faceid/service/impl/FaceIdAttendanceServiceImpl.java +200 -0
  29. src/main/java/com/attendenceSystem/module/log/dto/response/AuditLogResponse.java +3 -1
  30. src/main/java/com/attendenceSystem/module/log/entity/AuditLog.java +2 -1
  31. src/main/java/com/attendenceSystem/module/log/mapper/response/AuditLogResponseMapper.java +1 -2
  32. src/main/java/com/attendenceSystem/module/otp/dto/response/OtpResponse.java +3 -3
  33. src/main/java/com/attendenceSystem/module/otp/entity/Otp.java +4 -3
  34. src/main/java/com/attendenceSystem/module/otp/mapper/request/SendOtpRequestMapper.java +3 -3
  35. src/main/java/com/attendenceSystem/module/report/controller/ReportController.java +1 -0
  36. src/main/java/com/attendenceSystem/module/report/dto/request/CreateReportRequest.java +3 -0
  37. src/main/java/com/attendenceSystem/module/report/dto/response/ReportDetailResponse.java +5 -4
  38. src/main/java/com/attendenceSystem/module/report/dto/response/ReportResponse.java +5 -3
  39. src/main/java/com/attendenceSystem/module/report/entity/Report.java +8 -2
  40. src/main/java/com/attendenceSystem/module/report/mapper/response/ReportDetailResponseMapper.java +1 -0
  41. src/main/java/com/attendenceSystem/module/report/mapper/response/ReportResponseMapper.java +3 -1
  42. src/main/java/com/attendenceSystem/module/report/service/impl/ReportServiceImpl.java +55 -2
  43. src/main/java/com/attendenceSystem/module/user/controller/AccountController.java +0 -2
  44. src/main/java/com/attendenceSystem/module/user/dto/request/UpdateUserInformationRequest.java +0 -1
  45. src/main/java/com/attendenceSystem/module/user/entity/User.java +3 -2
  46. src/main/java/com/attendenceSystem/module/user/service/impl/UserServiceImpl.java +0 -4
  47. src/main/resources/application.properties +5 -1
  48. src/main/resources/templates/cms/dashboard/dashboard-employee.html +3 -28
  49. src/main/resources/templates/cms/document/document-detail.html +11 -1
  50. src/main/resources/templates/cms/document/document-list.html +17 -15
pom.xml CHANGED
@@ -107,6 +107,15 @@
107
  <groupId>org.springframework.boot</groupId>
108
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
109
  </dependency>
 
 
 
 
 
 
 
 
 
110
  </dependencies>
111
 
112
  <build>
 
107
  <groupId>org.springframework.boot</groupId>
108
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
109
  </dependency>
110
+ <dependency>
111
+ <groupId>com.fasterxml.jackson.core</groupId>
112
+ <artifactId>jackson-databind</artifactId>
113
+ </dependency>
114
+ <dependency>
115
+ <groupId>com.github.ben-manes.caffeine</groupId>
116
+ <artifactId>caffeine</artifactId>
117
+ <version>3.1.8</version>
118
+ </dependency>
119
  </dependencies>
120
 
121
  <build>
src/main/java/com/attendenceSystem/module/attendance/api/AttendanceManagerApiController.java CHANGED
@@ -2,7 +2,6 @@ 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;
 
2
 
3
  import java.time.LocalDate;
4
  import java.util.List;
 
5
 
6
  import org.springframework.http.ResponseEntity;
7
  import org.springframework.web.bind.annotation.GetMapping;
src/main/java/com/attendenceSystem/module/attendance/controller/AttendanceController.java CHANGED
@@ -55,6 +55,7 @@ public class AttendanceController {
55
 
56
  @GetMapping(Routes.Attendance.HISTORY)
57
  public String attendanceHistory(@PageableDefault(size = 10) Pageable pageable, Model model) {
 
58
  model.addAttribute("attendanceHistory", attendanceService.getAttendanceHistory(pageable));
59
  return Views.Attendance.HISTORY;
60
  }
@@ -83,7 +84,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 Views.Attendance.LEAVE_LIST;
87
  }
88
 
89
  @ExceptionHandler({
 
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
  }
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({
src/main/java/com/attendenceSystem/module/attendance/dto/response/AttendanceResponse.java CHANGED
@@ -1,7 +1,7 @@
1
  package com.attendenceSystem.module.attendance.dto.response;
2
 
3
- import java.time.Instant;
4
  import java.time.LocalDate;
 
5
 
6
  import com.attendenceSystem.module.attendance.entity.enums.AttendanceStatus;
7
  import com.attendenceSystem.module.user.entity.enums.Department;
@@ -15,8 +15,8 @@ public record AttendanceResponse(
15
  String fullName,
16
  Department department,
17
  LocalDate attendanceDate,
18
- Instant checkInTime,
19
- Instant checkOutTime,
20
  AttendanceStatus status,
21
  boolean late,
22
  boolean earlyLeave,
 
1
  package com.attendenceSystem.module.attendance.dto.response;
2
 
 
3
  import java.time.LocalDate;
4
+ import java.time.LocalDateTime;
5
 
6
  import com.attendenceSystem.module.attendance.entity.enums.AttendanceStatus;
7
  import com.attendenceSystem.module.user.entity.enums.Department;
 
15
  String fullName,
16
  Department department,
17
  LocalDate attendanceDate,
18
+ LocalDateTime checkInTime,
19
+ LocalDateTime checkOutTime,
20
  AttendanceStatus status,
21
  boolean late,
22
  boolean earlyLeave,
src/main/java/com/attendenceSystem/module/attendance/dto/response/LeaveDetailResponse.java CHANGED
@@ -2,6 +2,7 @@ package com.attendenceSystem.module.attendance.dto.response;
2
 
3
  import java.time.Instant;
4
  import java.time.LocalDate;
 
5
 
6
  import com.attendenceSystem.module.attendance.entity.enums.LeaveStatus;
7
 
@@ -13,5 +14,5 @@ public record LeaveDetailResponse(
13
  LocalDate endDate,
14
  String reason,
15
  LeaveStatus status,
16
- Instant createdAt) {
17
  }
 
2
 
3
  import java.time.Instant;
4
  import java.time.LocalDate;
5
+ import java.time.LocalDateTime;
6
 
7
  import com.attendenceSystem.module.attendance.entity.enums.LeaveStatus;
8
 
 
14
  LocalDate endDate,
15
  String reason,
16
  LeaveStatus status,
17
+ LocalDateTime createdAt) {
18
  }
src/main/java/com/attendenceSystem/module/attendance/dto/response/LeaveRequestResponse.java CHANGED
@@ -1,7 +1,7 @@
1
  package com.attendenceSystem.module.attendance.dto.response;
2
 
3
- import java.time.Instant;
4
  import java.time.LocalDate;
 
5
 
6
  import com.attendenceSystem.module.attendance.entity.enums.LeaveStatus;
7
 
@@ -13,5 +13,5 @@ public record LeaveRequestResponse(
13
  LocalDate endDate,
14
  String reason,
15
  LeaveStatus status,
16
- Instant createdAt) {
17
  }
 
1
  package com.attendenceSystem.module.attendance.dto.response;
2
 
 
3
  import java.time.LocalDate;
4
+ import java.time.LocalDateTime;
5
 
6
  import com.attendenceSystem.module.attendance.entity.enums.LeaveStatus;
7
 
 
13
  LocalDate endDate,
14
  String reason,
15
  LeaveStatus status,
16
+ LocalDateTime createdAt) {
17
  }
src/main/java/com/attendenceSystem/module/attendance/entity/AttendanceRecord.java CHANGED
@@ -1,7 +1,7 @@
1
  package com.attendenceSystem.module.attendance.entity;
2
 
3
- import java.time.Instant;
4
  import java.time.LocalDate;
 
5
 
6
  import com.attendenceSystem.module.attendance.entity.enums.AttendanceStatus;
7
  import com.attendenceSystem.module.user.entity.User;
@@ -44,13 +44,13 @@ public class AttendanceRecord {
44
  private User user;
45
 
46
  @Column(name = "check_in_time", nullable = false)
47
- private Instant checkInTime;
48
 
49
  @Column(name = "attendance_date", nullable = false)
50
  private LocalDate attendanceDate;
51
 
52
  @Column(name = "check_out_time")
53
- private Instant checkOutTime;
54
 
55
  @Enumerated(EnumType.STRING)
56
  @Column(name = "status", nullable = false)
 
1
  package com.attendenceSystem.module.attendance.entity;
2
 
 
3
  import java.time.LocalDate;
4
+ import java.time.LocalDateTime;
5
 
6
  import com.attendenceSystem.module.attendance.entity.enums.AttendanceStatus;
7
  import com.attendenceSystem.module.user.entity.User;
 
44
  private User user;
45
 
46
  @Column(name = "check_in_time", nullable = false)
47
+ private LocalDateTime checkInTime;
48
 
49
  @Column(name = "attendance_date", nullable = false)
50
  private LocalDate attendanceDate;
51
 
52
  @Column(name = "check_out_time")
53
+ private LocalDateTime checkOutTime;
54
 
55
  @Enumerated(EnumType.STRING)
56
  @Column(name = "status", nullable = false)
src/main/java/com/attendenceSystem/module/attendance/entity/LeaveRequest.java CHANGED
@@ -1,7 +1,7 @@
1
  package com.attendenceSystem.module.attendance.entity;
2
 
3
- import java.time.Instant;
4
  import java.time.LocalDate;
 
5
 
6
  import org.hibernate.annotations.CreationTimestamp;
7
 
@@ -53,5 +53,5 @@ public class LeaveRequest {
53
 
54
  @CreationTimestamp
55
  @Column(name = "created_at", nullable = false, updatable = false)
56
- private Instant createdAt;
57
  }
 
1
  package com.attendenceSystem.module.attendance.entity;
2
 
 
3
  import java.time.LocalDate;
4
+ import java.time.LocalDateTime;
5
 
6
  import org.hibernate.annotations.CreationTimestamp;
7
 
 
53
 
54
  @CreationTimestamp
55
  @Column(name = "created_at", nullable = false, updatable = false)
56
+ private LocalDateTime createdAt;
57
  }
src/main/java/com/attendenceSystem/module/attendance/mapper/response/AttendanceResponseMapper.java CHANGED
@@ -1,5 +1,8 @@
1
  package com.attendenceSystem.module.attendance.mapper.response;
2
 
 
 
 
3
  import org.springframework.stereotype.Component;
4
 
5
  import com.attendenceSystem.module.attendance.dto.response.AttendanceResponse;
@@ -14,9 +17,12 @@ public class AttendanceResponseMapper {
14
  private final AttendanceCalculator attendanceCalculator;
15
 
16
  public AttendanceResponse fromEntity(AttendanceRecord attendance) {
17
- boolean late = attendanceCalculator.isLate(attendance.getCheckInTime());
18
- boolean earlyLeave = attendanceCalculator.isEarlyLeave(attendance.getCheckOutTime());
19
- long workingMinutes = attendanceCalculator.workingMinutes(attendance.getCheckInTime(), attendance.getCheckOutTime());
 
 
 
20
 
21
  return AttendanceResponse.builder()
22
  .id(attendance.getId())
 
1
  package com.attendenceSystem.module.attendance.mapper.response;
2
 
3
+ import java.time.LocalDateTime;
4
+ import java.time.ZoneId;
5
+
6
  import org.springframework.stereotype.Component;
7
 
8
  import com.attendenceSystem.module.attendance.dto.response.AttendanceResponse;
 
17
  private final AttendanceCalculator attendanceCalculator;
18
 
19
  public AttendanceResponse fromEntity(AttendanceRecord attendance) {
20
+ boolean late = attendanceCalculator.isLate(attendance.getCheckInTime());
21
+ boolean earlyLeave = attendanceCalculator.isEarlyLeave(attendance.getCheckOutTime());
22
+ long workingMinutes = attendanceCalculator.workingMinutes(
23
+ attendance.getCheckInTime(),
24
+ attendance.getCheckOutTime()
25
+ );
26
 
27
  return AttendanceResponse.builder()
28
  .id(attendance.getId())
src/main/java/com/attendenceSystem/module/attendance/mapper/response/LeaveDetailResponseMapper.java CHANGED
@@ -4,7 +4,6 @@ import org.springframework.stereotype.Component;
4
 
5
  import com.attendenceSystem.module.attendance.dto.response.LeaveDetailResponse;
6
  import com.attendenceSystem.module.attendance.entity.LeaveRequest;
7
- import com.attendenceSystem.module.attendance.entity.enums.LeaveStatus;
8
 
9
  import lombok.RequiredArgsConstructor;
10
 
 
4
 
5
  import com.attendenceSystem.module.attendance.dto.response.LeaveDetailResponse;
6
  import com.attendenceSystem.module.attendance.entity.LeaveRequest;
 
7
 
8
  import lombok.RequiredArgsConstructor;
9
 
src/main/java/com/attendenceSystem/module/attendance/mapper/response/LeaveRequestResponseMapper.java CHANGED
@@ -4,7 +4,6 @@ import org.springframework.stereotype.Component;
4
 
5
  import com.attendenceSystem.module.attendance.dto.response.LeaveRequestResponse;
6
  import com.attendenceSystem.module.attendance.entity.LeaveRequest;
7
- import com.attendenceSystem.module.attendance.entity.enums.LeaveStatus;
8
 
9
  import lombok.RequiredArgsConstructor;
10
 
 
4
 
5
  import com.attendenceSystem.module.attendance.dto.response.LeaveRequestResponse;
6
  import com.attendenceSystem.module.attendance.entity.LeaveRequest;
 
7
 
8
  import lombok.RequiredArgsConstructor;
9
 
src/main/java/com/attendenceSystem/module/attendance/model/DateRange.java ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.attendance.model;
2
+
3
+ import java.time.LocalDate;
4
+
5
+ public record DateRange(
6
+ LocalDate startDate,
7
+ LocalDate endDate) {
8
+
9
+ }
src/main/java/com/attendenceSystem/module/attendance/service/AttendanceService.java CHANGED
@@ -5,12 +5,16 @@ 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
 
@@ -18,15 +22,19 @@ public interface AttendanceService {
18
 
19
  AttendanceResponse checkOut();
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
 
@@ -35,4 +43,6 @@ public interface AttendanceService {
35
  Page<LeaveRequestResponse> getAllLeaveRequests(Pageable pageable);
36
 
37
  LeaveDetailResponse getLeaveDetail(Long id);
 
 
38
  }
 
5
 
6
  import java.time.LocalDate;
7
  import java.util.List;
8
+ import java.util.Optional;
9
 
10
  import com.attendenceSystem.module.attendance.dto.request.CreateLeaveRequest;
11
  import com.attendenceSystem.module.attendance.dto.response.AttendanceResponse;
12
  import com.attendenceSystem.module.attendance.dto.response.LeaveDetailResponse;
13
  import com.attendenceSystem.module.attendance.dto.response.LeaveRequestResponse;
14
  import com.attendenceSystem.module.attendance.dto.response.ManagerStatsResponse;
15
+ import com.attendenceSystem.module.attendance.entity.AttendanceRecord;
16
+ import com.attendenceSystem.module.attendance.entity.enums.AttendanceStatus;
17
+ import com.attendenceSystem.module.user.entity.User;
18
 
19
  public interface AttendanceService {
20
 
 
22
 
23
  AttendanceResponse checkOut();
24
 
25
+ AttendanceResponse checkIn(User user);
26
+
27
+ AttendanceResponse checkOut(User user);
28
+
29
  Page<AttendanceResponse> getAttendanceHistory(Pageable pageable);
30
 
31
  ManagerStatsResponse getManagerStats(String departmentId, LocalDate startDate, LocalDate endDate);
32
 
33
+ ManagerStatsResponse getManagerStats(String departmentId, LocalDate startDate, LocalDate endDate, AttendanceStatus status);
34
 
35
  List<AttendanceResponse> getManagerAttendanceList(String departmentId, LocalDate startDate, LocalDate endDate);
36
 
37
+ List<AttendanceResponse> getManagerAttendanceList(String departmentId, LocalDate startDate, LocalDate endDate, AttendanceStatus status);
38
 
39
  LeaveRequestResponse createLeaveRequest(CreateLeaveRequest request);
40
 
 
43
  Page<LeaveRequestResponse> getAllLeaveRequests(Pageable pageable);
44
 
45
  LeaveDetailResponse getLeaveDetail(Long id);
46
+
47
+ Optional<AttendanceRecord> getTodayAttendanceRecord(User user);
48
  }
src/main/java/com/attendenceSystem/module/attendance/service/impl/AttendanceServiceImpl.java CHANGED
@@ -1,12 +1,14 @@
1
  package com.attendenceSystem.module.attendance.service.impl;
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;
@@ -27,6 +29,7 @@ import com.attendenceSystem.module.attendance.mapper.response.AttendanceResponse
27
  import com.attendenceSystem.module.attendance.entity.LeaveRequest;
28
  import com.attendenceSystem.module.attendance.mapper.response.LeaveDetailResponseMapper;
29
  import com.attendenceSystem.module.attendance.mapper.response.LeaveRequestResponseMapper;
 
30
  import com.attendenceSystem.module.attendance.repository.LeaveRequestRepository;
31
  import com.attendenceSystem.module.attendance.repository.AttendanceRecordRepository;
32
  import com.attendenceSystem.module.attendance.service.AttendanceService;
@@ -61,16 +64,26 @@ public class AttendanceServiceImpl implements AttendanceService {
61
  @Transactional
62
  @Override
63
  public AttendanceResponse checkIn() {
64
- User user = getCurrentUser();
 
 
 
 
 
 
 
65
 
66
- LocalDate today = LocalDate.now(timeZoneProvider.getZoneId());
 
 
 
67
  try {
68
  Optional<AttendanceRecord> existing = attendanceRecordRepository
69
  .findByUserAndAttendanceDateWithLock(user, today);
70
  if (existing.isPresent()) {
71
  throw new AlreadyCheckedInException("Bạn đã điểm danh hôm nay");
72
  }
73
- Instant checkInTime = Instant.now();
74
  boolean late = attendanceCalculator.isLate(checkInTime);
75
  AttendanceStatus status = late ? AttendanceStatus.LATE : AttendanceStatus.PRESENT;
76
  AttendanceRecord attendanceRecord = AttendanceRecord.builder()
@@ -82,7 +95,7 @@ public class AttendanceServiceImpl implements AttendanceService {
82
  .build();
83
  attendanceRecordRepository.save(attendanceRecord);
84
  return attendanceResponseMapper.fromEntity(attendanceRecord);
85
- } catch (org.springframework.dao.DataIntegrityViolationException e) {
86
  throw new AlreadyCheckedInException("Bạn đã điểm danh hôm nay");
87
  } catch (ObjectOptimisticLockingFailureException e) {
88
  throw new AlreadyCheckedInException("Dữ liệu đã bị thay đổi, vui lòng thử lại");
@@ -91,10 +104,8 @@ public class AttendanceServiceImpl implements AttendanceService {
91
 
92
  @Transactional
93
  @Override
94
- public AttendanceResponse checkOut() {
95
- User user = getCurrentUser();
96
-
97
- LocalDate today = LocalDate.now(timeZoneProvider.getZoneId());
98
  AttendanceRecord attendance = attendanceRecordRepository
99
  .findByUserAndAttendanceDateWithLock(user, today)
100
  .orElseThrow(() -> new NotCheckedInException(
@@ -105,7 +116,7 @@ public class AttendanceServiceImpl implements AttendanceService {
105
  if (attendance.getStatus() != AttendanceStatus.PRESENT && attendance.getStatus() != AttendanceStatus.LATE) {
106
  throw new InvalidAttendanceStateException("Trạng thái điểm danh không hợp lệ");
107
  }
108
- Instant checkOutTime = Instant.now();
109
  if (checkOutTime.isBefore(attendance.getCheckInTime())) {
110
  throw new InvalidAttendanceStateException("Thời gian checkout phải sau thời gian check-in");
111
  }
@@ -117,9 +128,8 @@ public class AttendanceServiceImpl implements AttendanceService {
117
  attendanceRecordRepository.save(attendance);
118
  return attendanceResponseMapper.fromEntity(attendance);
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
@@ -167,19 +177,9 @@ public class AttendanceServiceImpl implements AttendanceService {
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;
@@ -227,19 +227,9 @@ public class AttendanceServiceImpl implements AttendanceService {
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)
@@ -271,14 +261,14 @@ public class AttendanceServiceImpl implements AttendanceService {
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
 
@@ -292,13 +282,43 @@ public class AttendanceServiceImpl implements AttendanceService {
292
  return leaveDetailResponseMapper.fromEntity(leave);
293
  }
294
 
 
 
 
 
 
 
 
 
 
295
  private User getCurrentUser() {
296
  if (!SecurityUtil.isAuthenticated()) {
297
  throw new IllegalStateException("Người dùng chưa đăng nhập");
298
  }
299
  String currentUsername = SecurityUtil.getCurrentUserName();
300
  return userRepository.findByUsername(currentUsername)
301
- .orElseThrow(() -> new IllegalArgumentException(
302
  "Không tìm thấy người dùng với tên đăng nhập: " + currentUsername));
303
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  }
 
1
  package com.attendenceSystem.module.attendance.service.impl;
2
 
 
3
  import java.time.LocalDate;
4
+ import java.time.LocalDateTime;
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.data.domain.Page;
11
+ import org.springframework.data.domain.Pageable;
12
  import org.springframework.orm.ObjectOptimisticLockingFailureException;
13
  import org.springframework.stereotype.Service;
14
  import org.springframework.transaction.annotation.Transactional;
 
29
  import com.attendenceSystem.module.attendance.entity.LeaveRequest;
30
  import com.attendenceSystem.module.attendance.mapper.response.LeaveDetailResponseMapper;
31
  import com.attendenceSystem.module.attendance.mapper.response.LeaveRequestResponseMapper;
32
+ import com.attendenceSystem.module.attendance.model.DateRange;
33
  import com.attendenceSystem.module.attendance.repository.LeaveRequestRepository;
34
  import com.attendenceSystem.module.attendance.repository.AttendanceRecordRepository;
35
  import com.attendenceSystem.module.attendance.service.AttendanceService;
 
64
  @Transactional
65
  @Override
66
  public AttendanceResponse checkIn() {
67
+ return checkIn(getCurrentUser());
68
+ }
69
+
70
+ @Transactional
71
+ @Override
72
+ public AttendanceResponse checkOut() {
73
+ return checkOut(getCurrentUser());
74
+ }
75
 
76
+ @Transactional
77
+ @Override
78
+ public AttendanceResponse checkIn(User user) {
79
+ LocalDate today = todayDate();
80
  try {
81
  Optional<AttendanceRecord> existing = attendanceRecordRepository
82
  .findByUserAndAttendanceDateWithLock(user, today);
83
  if (existing.isPresent()) {
84
  throw new AlreadyCheckedInException("Bạn đã điểm danh hôm nay");
85
  }
86
+ LocalDateTime checkInTime = LocalDateTime.now();
87
  boolean late = attendanceCalculator.isLate(checkInTime);
88
  AttendanceStatus status = late ? AttendanceStatus.LATE : AttendanceStatus.PRESENT;
89
  AttendanceRecord attendanceRecord = AttendanceRecord.builder()
 
95
  .build();
96
  attendanceRecordRepository.save(attendanceRecord);
97
  return attendanceResponseMapper.fromEntity(attendanceRecord);
98
+ } catch (DataIntegrityViolationException e) {
99
  throw new AlreadyCheckedInException("Bạn đã điểm danh hôm nay");
100
  } catch (ObjectOptimisticLockingFailureException e) {
101
  throw new AlreadyCheckedInException("Dữ liệu đã bị thay đổi, vui lòng thử lại");
 
104
 
105
  @Transactional
106
  @Override
107
+ public AttendanceResponse checkOut(User user) {
108
+ LocalDate today = todayDate();
 
 
109
  AttendanceRecord attendance = attendanceRecordRepository
110
  .findByUserAndAttendanceDateWithLock(user, today)
111
  .orElseThrow(() -> new NotCheckedInException(
 
116
  if (attendance.getStatus() != AttendanceStatus.PRESENT && attendance.getStatus() != AttendanceStatus.LATE) {
117
  throw new InvalidAttendanceStateException("Trạng thái điểm danh không hợp lệ");
118
  }
119
+ LocalDateTime checkOutTime = LocalDateTime.now();
120
  if (checkOutTime.isBefore(attendance.getCheckInTime())) {
121
  throw new InvalidAttendanceStateException("Thời gian checkout phải sau thời gian check-in");
122
  }
 
128
  attendanceRecordRepository.save(attendance);
129
  return attendanceResponseMapper.fromEntity(attendance);
130
  }
 
131
  @Override
132
+ public Page<AttendanceResponse> getAttendanceHistory(final Pageable pageable) {
133
  User user = getCurrentUser();
134
 
135
  return attendanceRecordRepository
 
177
  .absent(0)
178
  .build();
179
  }
180
+ DateRange dateRange = getDateRange(startDate, endDate);
181
 
182
+ List<AttendanceRecord> records = getFilteredRecords(dateRange.startDate(), dateRange.endDate(), status);
 
 
 
 
 
 
 
 
 
 
 
183
 
184
  long checkedIn = 0;
185
  long checkedOut = 0;
 
227
  } else {
228
  employees = userRepository.findByRoleNot(Role.ADMIN);
229
  }
230
+ DateRange dateRange = getDateRange(startDate, endDate);
231
 
232
+ List<AttendanceRecord> records = getFilteredRecords(dateRange.startDate(), dateRange.endDate(), status);
 
 
 
 
 
 
 
 
 
 
 
233
 
234
  return records.stream()
235
  .map(attendanceResponseMapper::fromEntity)
 
261
  }
262
 
263
  @Override
264
+ public Page<LeaveRequestResponse> getLeaveRequests(final Pageable pageable) {
265
  User user = getCurrentUser();
266
  return leaveRequestRepository.findByUser(user, pageable)
267
  .map(leaveRequestResponseMapper::fromEntity);
268
  }
269
 
270
  @Override
271
+ public Page<LeaveRequestResponse> getAllLeaveRequests(final Pageable pageable) {
272
  return leaveRequestRepository.findAll(pageable).map(leaveRequestResponseMapper::fromEntity);
273
  }
274
 
 
282
  return leaveDetailResponseMapper.fromEntity(leave);
283
  }
284
 
285
+ @Override
286
+ public Optional<AttendanceRecord> getTodayAttendanceRecord(User user) {
287
+ if (user == null) {
288
+ return Optional.empty();
289
+ }
290
+ LocalDate today = todayDate();
291
+ return attendanceRecordRepository.findByUserAndAttendanceDateWithLock(user, today);
292
+ }
293
+
294
  private User getCurrentUser() {
295
  if (!SecurityUtil.isAuthenticated()) {
296
  throw new IllegalStateException("Người dùng chưa đăng nhập");
297
  }
298
  String currentUsername = SecurityUtil.getCurrentUserName();
299
  return userRepository.findByUsername(currentUsername)
300
+ .orElseThrow(() -> new IllegalStateException(
301
  "Không tìm thấy người dùng với tên đăng nhập: " + currentUsername));
302
  }
303
+
304
+ private LocalDate todayDate() {
305
+ return LocalDate.now(timeZoneProvider.getZoneId());
306
+ }
307
+
308
+ private DateRange getDateRange(LocalDate startDate, LocalDate endDate) {
309
+ LocalDate effectiveStartDate = startDate;
310
+ LocalDate effectiveEndDate = endDate;
311
+ if (effectiveStartDate == null && effectiveEndDate == null) {
312
+ LocalDate today = todayDate();
313
+ effectiveStartDate = today.minusMonths(6);
314
+ effectiveEndDate = today;
315
+ } else if (effectiveStartDate != null && effectiveEndDate == null) {
316
+ effectiveEndDate = effectiveStartDate;
317
+ } else if (effectiveStartDate == null && effectiveEndDate != null) {
318
+ effectiveStartDate = effectiveEndDate;
319
+ }
320
+ return new DateRange(effectiveStartDate, effectiveEndDate);
321
+ }
322
+
323
+
324
  }
src/main/java/com/attendenceSystem/module/attendance/util/AttendanceCalculator.java CHANGED
@@ -1,14 +1,14 @@
1
  package com.attendenceSystem.module.attendance.util;
2
 
3
- import java.time.Instant;
4
 
5
  public interface AttendanceCalculator {
6
 
7
- boolean isLate(Instant checkInTime);
8
 
9
- boolean isEarlyLeave(Instant checkOutTime);
10
 
11
- long workingMinutes(Instant checkInTime, Instant checkOutTime);
12
 
13
- double totalWorkingHours(Instant checkInTime, Instant checkOutTime);
14
  }
 
1
  package com.attendenceSystem.module.attendance.util;
2
 
3
+ import java.time.LocalDateTime;
4
 
5
  public interface AttendanceCalculator {
6
 
7
+ boolean isLate(LocalDateTime checkInTime);
8
 
9
+ boolean isEarlyLeave(LocalDateTime checkOutTime);
10
 
11
+ long workingMinutes(LocalDateTime checkInTime, LocalDateTime checkOutTime);
12
 
13
+ double totalWorkingHours(LocalDateTime checkInTime, LocalDateTime checkOutTime);
14
  }
src/main/java/com/attendenceSystem/module/attendance/util/impl/AttendanceCalculatorImpl.java CHANGED
@@ -1,7 +1,7 @@
1
  package com.attendenceSystem.module.attendance.util.impl;
2
 
3
  import java.time.Duration;
4
- import java.time.Instant;
5
  import java.time.LocalTime;
6
  import java.time.ZoneId;
7
 
@@ -73,7 +73,7 @@ public class AttendanceCalculatorImpl implements AttendanceCalculator {
73
  }
74
 
75
  @Override
76
- public boolean isLate(final Instant checkInTime) {
77
  if (checkInTime == null) {
78
  return false;
79
  }
@@ -82,7 +82,7 @@ public class AttendanceCalculatorImpl implements AttendanceCalculator {
82
  }
83
 
84
  @Override
85
- public boolean isEarlyLeave(final Instant checkOutTime) {
86
  if (checkOutTime == null) {
87
  return false;
88
  }
@@ -91,7 +91,7 @@ public class AttendanceCalculatorImpl implements AttendanceCalculator {
91
  }
92
 
93
  @Override
94
- public long workingMinutes(final Instant checkInTime, final Instant checkOutTime) {
95
  if (checkInTime == null || checkOutTime == null) {
96
  return 0;
97
  }
@@ -99,7 +99,7 @@ public class AttendanceCalculatorImpl implements AttendanceCalculator {
99
  }
100
 
101
  @Override
102
- public double totalWorkingHours(final Instant checkInTime, final Instant checkOutTime) {
103
  long minutes = workingMinutes(checkInTime, checkOutTime);
104
  return minutes / 60.0;
105
  }
 
1
  package com.attendenceSystem.module.attendance.util.impl;
2
 
3
  import java.time.Duration;
4
+ import java.time.LocalDateTime;
5
  import java.time.LocalTime;
6
  import java.time.ZoneId;
7
 
 
73
  }
74
 
75
  @Override
76
+ public boolean isLate(final LocalDateTime checkInTime) {
77
  if (checkInTime == null) {
78
  return false;
79
  }
 
82
  }
83
 
84
  @Override
85
+ public boolean isEarlyLeave(final LocalDateTime checkOutTime) {
86
  if (checkOutTime == null) {
87
  return false;
88
  }
 
91
  }
92
 
93
  @Override
94
+ public long workingMinutes(final LocalDateTime checkInTime, final LocalDateTime checkOutTime) {
95
  if (checkInTime == null || checkOutTime == null) {
96
  return 0;
97
  }
 
99
  }
100
 
101
  @Override
102
+ public double totalWorkingHours(final LocalDateTime checkInTime, final LocalDateTime checkOutTime) {
103
  long minutes = workingMinutes(checkInTime, checkOutTime);
104
  return minutes / 60.0;
105
  }
src/main/java/com/attendenceSystem/module/faceid/controller/FaceIdAttendanceController.java ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.faceid.controller;
2
+
3
+ import com.attendenceSystem.module.faceid.dto.FaceIdAttendanceRequest;
4
+ import com.attendenceSystem.module.faceid.dto.FaceIdAttendanceResponse;
5
+ import com.attendenceSystem.module.faceid.service.FaceIdAttendanceService;
6
+ import com.attendenceSystem.module.faceid.service.impl.FaceIdAttendanceServiceImpl;
7
+
8
+ import jakarta.validation.Valid;
9
+ import lombok.RequiredArgsConstructor;
10
+ import org.springframework.http.ResponseEntity;
11
+ import org.springframework.web.bind.annotation.PostMapping;
12
+ import org.springframework.web.bind.annotation.RequestBody;
13
+ import org.springframework.web.bind.annotation.RequestMapping;
14
+ import org.springframework.web.bind.annotation.RestController;
15
+
16
+ @RestController
17
+ @RequestMapping("/api/face-id")
18
+ @RequiredArgsConstructor
19
+ public class FaceIdAttendanceController {
20
+
21
+ private final FaceIdAttendanceServiceImpl faceIdAttendanceService;
22
+
23
+ @PostMapping("/attendance")
24
+ public ResponseEntity<FaceIdAttendanceResponse> processAttendance(
25
+ @Valid @RequestBody FaceIdAttendanceRequest request) {
26
+
27
+ FaceIdAttendanceResponse response = faceIdAttendanceService.processAttendance(request);
28
+ return ResponseEntity.ok(response);
29
+ }
30
+ }
src/main/java/com/attendenceSystem/module/faceid/dto/FaceIdAction.java ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.faceid.dto;
2
+
3
+ public enum FaceIdAction {
4
+ CHECKIN,
5
+ CHECKOUT,
6
+ IGNORED,
7
+ FAILED
8
+ }
src/main/java/com/attendenceSystem/module/faceid/dto/FaceIdAttendanceRequest.java ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.faceid.dto;
2
+
3
+ import jakarta.validation.constraints.NotBlank;
4
+ import jakarta.validation.constraints.NotNull;
5
+ import jakarta.validation.constraints.Size;
6
+ import lombok.Data;
7
+
8
+ import java.time.LocalDateTime;
9
+
10
+ @Data
11
+ public class FaceIdAttendanceRequest {
12
+ @NotBlank(message = "studentCode không được để trống")
13
+ private String studentCode;
14
+
15
+ @NotNull(message = "confidence không được để trống")
16
+ private Double confidence;
17
+
18
+ @NotNull(message = "capturedAt không được để trống")
19
+ private LocalDateTime capturedAt;
20
+
21
+ private String cameraId;
22
+
23
+ private String imageId;
24
+
25
+ @Size(max = 255, message = "trackingId không được vượt quá 255 ký tự")
26
+ private String trackingId;
27
+
28
+ private Boolean liveness;
29
+ }
src/main/java/com/attendenceSystem/module/faceid/dto/FaceIdAttendanceResponse.java ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.faceid.dto;
2
+
3
+ import com.attendenceSystem.module.attendance.dto.response.AttendanceResponse;
4
+ import lombok.AllArgsConstructor;
5
+ import lombok.Builder;
6
+ import lombok.Data;
7
+ import lombok.NoArgsConstructor;
8
+
9
+ import java.time.Instant;
10
+
11
+ @Data
12
+ @Builder
13
+ @NoArgsConstructor
14
+ @AllArgsConstructor
15
+ public class FaceIdAttendanceResponse {
16
+ private boolean success;
17
+ private FaceIdAction action;
18
+ private String message;
19
+ private String trackingId;
20
+ private Instant timestamp;
21
+ private AttendanceResponse attendance;
22
+ }
src/main/java/com/attendenceSystem/module/faceid/dto/response/FaceProfileResponse.java CHANGED
@@ -1,6 +1,6 @@
1
  package com.attendenceSystem.module.faceid.dto.response;
2
 
3
- import java.time.Instant;
4
 
5
  import lombok.Builder;
6
 
@@ -15,6 +15,6 @@ public record FaceProfileResponse(
15
  String userName,
16
  String userEmail,
17
  String userFullName,
18
- Instant createdAt,
19
- Instant updatedAt) {
20
  }
 
1
  package com.attendenceSystem.module.faceid.dto.response;
2
 
3
+ import java.time.LocalDateTime;
4
 
5
  import lombok.Builder;
6
 
 
15
  String userName,
16
  String userEmail,
17
  String userFullName,
18
+ LocalDateTime createdAt,
19
+ LocalDateTime updatedAt) {
20
  }
src/main/java/com/attendenceSystem/module/faceid/entity/FaceIdRecognition.java ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.faceid.entity;
2
+
3
+ import com.attendenceSystem.module.faceid.dto.FaceIdAction;
4
+ import jakarta.persistence.*;
5
+ import lombok.AllArgsConstructor;
6
+ import lombok.Builder;
7
+ import lombok.Data;
8
+ import lombok.NoArgsConstructor;
9
+ import org.hibernate.annotations.CreationTimestamp;
10
+
11
+ import java.time.LocalDateTime;
12
+
13
+ @Entity
14
+ @Table(name = "face_id_recognition_logs")
15
+ @Data
16
+ @Builder
17
+ @NoArgsConstructor
18
+ @AllArgsConstructor
19
+ public class FaceIdRecognition {
20
+ @Id
21
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
22
+ private Long id;
23
+
24
+ @Column(name = "student_code", nullable = false, length = 50)
25
+ private String studentCode;
26
+
27
+ @Column(name = "confidence", nullable = false)
28
+ private Double confidence;
29
+
30
+ @Column(name = "camera_id", length = 50)
31
+ private String cameraId;
32
+
33
+ @Column(name = "image_id", length = 255)
34
+ private String imageId;
35
+
36
+ @Column(name = "tracking_id", length = 255, unique = true)
37
+ private String trackingId;
38
+
39
+ @Column(name = "liveness")
40
+ private Boolean liveness;
41
+
42
+ @Enumerated(EnumType.STRING)
43
+ @Column(name = "action", nullable = false, length = 20)
44
+ private FaceIdAction action;
45
+
46
+ @Column(name = "message", length = 500)
47
+ private String message;
48
+
49
+ @Column(name = "request_payload", columnDefinition = "JSON")
50
+ private String requestPayload;
51
+
52
+ @Column(name = "response_payload", columnDefinition = "JSON")
53
+ private String responsePayload;
54
+
55
+ @Column(name = "captured_at")
56
+ private LocalDateTime capturedAt;
57
+
58
+ @CreationTimestamp
59
+ @Column(name = "created_at", updatable = false)
60
+ private LocalDateTime createdAt;
61
+ }
src/main/java/com/attendenceSystem/module/faceid/entity/FaceProfile.java CHANGED
@@ -1,6 +1,7 @@
1
  package com.attendenceSystem.module.faceid.entity;
2
 
3
  import java.time.Instant;
 
4
 
5
  import org.hibernate.annotations.CreationTimestamp;
6
  import org.hibernate.annotations.UpdateTimestamp;
@@ -56,9 +57,9 @@ public class FaceProfile {
56
 
57
  @CreationTimestamp
58
  @Column(name = "created_at", nullable = false, updatable = false)
59
- private Instant createdAt;
60
 
61
  @UpdateTimestamp
62
  @Column(name = "updated_at", nullable = false)
63
- private Instant updatedAt;
64
  }
 
1
  package com.attendenceSystem.module.faceid.entity;
2
 
3
  import java.time.Instant;
4
+ import java.time.LocalDateTime;
5
 
6
  import org.hibernate.annotations.CreationTimestamp;
7
  import org.hibernate.annotations.UpdateTimestamp;
 
57
 
58
  @CreationTimestamp
59
  @Column(name = "created_at", nullable = false, updatable = false)
60
+ private LocalDateTime createdAt;
61
 
62
  @UpdateTimestamp
63
  @Column(name = "updated_at", nullable = false)
64
+ private LocalDateTime updatedAt;
65
  }
src/main/java/com/attendenceSystem/module/faceid/entity/FaceSample.java CHANGED
@@ -1,6 +1,7 @@
1
  package com.attendenceSystem.module.faceid.entity;
2
 
3
  import java.time.Instant;
 
4
 
5
  import org.hibernate.annotations.CreationTimestamp;
6
 
@@ -45,5 +46,5 @@ public class FaceSample {
45
 
46
  @CreationTimestamp
47
  @Column(name = "created_at", nullable = false, updatable = false)
48
- private Instant createdAt;
49
  }
 
1
  package com.attendenceSystem.module.faceid.entity;
2
 
3
  import java.time.Instant;
4
+ import java.time.LocalDateTime;
5
 
6
  import org.hibernate.annotations.CreationTimestamp;
7
 
 
46
 
47
  @CreationTimestamp
48
  @Column(name = "created_at", nullable = false, updatable = false)
49
+ private LocalDateTime createdAt;
50
  }
src/main/java/com/attendenceSystem/module/faceid/filter/FaceIdApiKeyFilter.java ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.faceid.filter;
2
+
3
+ import jakarta.servlet.FilterChain;
4
+ import jakarta.servlet.ServletException;
5
+ import jakarta.servlet.http.HttpServletRequest;
6
+ import jakarta.servlet.http.HttpServletResponse;
7
+ import org.springframework.beans.factory.annotation.Value;
8
+ import org.springframework.core.Ordered;
9
+ import org.springframework.core.annotation.Order;
10
+ import org.springframework.stereotype.Component;
11
+ import org.springframework.web.filter.OncePerRequestFilter;
12
+
13
+ import java.io.IOException;
14
+
15
+ @Component
16
+ @Order(Ordered.HIGHEST_PRECEDENCE)
17
+ public class FaceIdApiKeyFilter extends OncePerRequestFilter {
18
+
19
+ @Value("${face-id.api-key}")
20
+ private String apiKey;
21
+
22
+ private static final String API_KEY_HEADER = "X-API-KEY";
23
+ private static final String FACE_ID_PATH_PREFIX = "/api/face-id";
24
+
25
+ @Override
26
+ protected boolean shouldNotFilter(HttpServletRequest request) {
27
+ String path = request.getRequestURI();
28
+ return !path.startsWith(FACE_ID_PATH_PREFIX);
29
+ }
30
+
31
+ @Override
32
+ protected void doFilterInternal(
33
+ HttpServletRequest request,
34
+ HttpServletResponse response,
35
+ FilterChain filterChain
36
+ ) throws ServletException, IOException {
37
+
38
+ // String requestApiKey = extractApiKey(request);
39
+
40
+ // if (apiKey == null || apiKey.isBlank() || !apiKey.equals(requestApiKey)) {
41
+ // throw new RuntimeException("Invalid or missing API key");
42
+ // }
43
+
44
+ filterChain.doFilter(request, response);
45
+ }
46
+
47
+ private String extractApiKey(HttpServletRequest request) {
48
+ String apiKeyHeader = request.getHeader(API_KEY_HEADER);
49
+ if (apiKeyHeader != null && apiKeyHeader.startsWith("Bearer ")) {
50
+ return apiKeyHeader.substring(7);
51
+ }
52
+ return apiKeyHeader;
53
+ }
54
+ }
src/main/java/com/attendenceSystem/module/faceid/repository/FaceIdRecognitionRepository.java ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.faceid.repository;
2
+
3
+ import com.attendenceSystem.module.faceid.entity.FaceIdRecognition;
4
+ import org.springframework.data.jpa.repository.JpaRepository;
5
+ import org.springframework.stereotype.Repository;
6
+
7
+ @Repository
8
+ public interface FaceIdRecognitionRepository extends JpaRepository<FaceIdRecognition, Long> {
9
+ boolean existsByTrackingId(String trackingId);
10
+ }
src/main/java/com/attendenceSystem/module/faceid/service/FaceIdAttendanceService.java ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.faceid.service;
2
+
3
+ import com.attendenceSystem.module.faceid.dto.FaceIdAttendanceRequest;
4
+ import com.attendenceSystem.module.faceid.dto.FaceIdAttendanceResponse;
5
+
6
+ public interface FaceIdAttendanceService {
7
+ FaceIdAttendanceResponse processAttendance(FaceIdAttendanceRequest request);
8
+ }
src/main/java/com/attendenceSystem/module/faceid/service/impl/FaceIdAttendanceServiceImpl.java ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.faceid.service.impl;
2
+
3
+ import com.attendenceSystem.module.attendance.dto.response.AttendanceResponse;
4
+ import com.attendenceSystem.module.attendance.entity.AttendanceRecord;
5
+ import com.attendenceSystem.module.attendance.service.AttendanceService;
6
+ import com.attendenceSystem.module.faceid.dto.FaceIdAction;
7
+ import com.attendenceSystem.module.faceid.dto.FaceIdAttendanceRequest;
8
+ import com.attendenceSystem.module.faceid.dto.FaceIdAttendanceResponse;
9
+ import com.attendenceSystem.module.faceid.entity.FaceIdRecognition;
10
+ import com.attendenceSystem.module.faceid.repository.FaceIdRecognitionRepository;
11
+ import com.attendenceSystem.module.faceid.service.FaceIdAttendanceService;
12
+ import com.attendenceSystem.module.user.entity.User;
13
+ import com.attendenceSystem.module.user.repository.UserRepository;
14
+ import com.fasterxml.jackson.core.JsonProcessingException;
15
+ import com.fasterxml.jackson.databind.ObjectMapper;
16
+ import com.github.benmanes.caffeine.cache.Cache;
17
+ import com.github.benmanes.caffeine.cache.Caffeine;
18
+ import lombok.RequiredArgsConstructor;
19
+ import lombok.extern.slf4j.Slf4j;
20
+ import org.springframework.beans.factory.annotation.Value;
21
+ import org.springframework.stereotype.Service;
22
+ import org.springframework.transaction.annotation.Transactional;
23
+ import org.springframework.util.StringUtils;
24
+
25
+ import java.time.Duration;
26
+ import java.time.Instant;
27
+ import java.util.Optional;
28
+
29
+ @Service
30
+ @Slf4j
31
+ @RequiredArgsConstructor
32
+ public class FaceIdAttendanceServiceImpl implements FaceIdAttendanceService {
33
+
34
+ private final AttendanceService attendanceService;
35
+ private final UserRepository userRepository;
36
+ private final FaceIdRecognitionRepository faceIdRecognitionRepository;
37
+
38
+ @Value("${face-id.confidence-threshold:0.90}")
39
+ private double confidenceThreshold;
40
+
41
+ private final ObjectMapper objectMapper = new ObjectMapper();
42
+
43
+ private final Cache<String, FaceIdAction> processedTrackingIds = Caffeine.newBuilder()
44
+ .expireAfterWrite(Duration.ofMinutes(5))
45
+ .maximumSize(100_000)
46
+ .build();
47
+
48
+ @Override
49
+ @Transactional
50
+ public FaceIdAttendanceResponse processAttendance(FaceIdAttendanceRequest request) {
51
+ String trackingId = request.getTrackingId();
52
+ Instant timestamp = Instant.now();
53
+
54
+ // 1. Check idempotency (only if trackingId is present)
55
+ if (StringUtils.hasText(trackingId)) {
56
+ FaceIdAction cachedAction = processedTrackingIds.getIfPresent(trackingId);
57
+ if (cachedAction != null) {
58
+ log.info("Ignoring duplicate trackingId: {}", trackingId);
59
+ return buildResponse(FaceIdAction.IGNORED, "DUPLICATE", trackingId, timestamp, null);
60
+ }
61
+ }
62
+
63
+ // 2. Check confidence threshold
64
+ if (request.getConfidence() < confidenceThreshold) {
65
+ String message = "Confidence below threshold: " + request.getConfidence();
66
+ log.warn(message);
67
+ saveRecognitionLog(request, FaceIdAction.FAILED, message, null, timestamp);
68
+ return buildResponse(FaceIdAction.FAILED, message, trackingId, timestamp, null);
69
+ }
70
+
71
+ // 3. Find user by username (studentCode)
72
+ User user = userRepository.findByUsername(request.getStudentCode())
73
+ .orElse(null);
74
+
75
+ if (user == null) {
76
+ String message = "User not found: " + request.getStudentCode();
77
+ log.warn(message);
78
+ saveRecognitionLog(request, FaceIdAction.FAILED, message, null, timestamp);
79
+ return buildResponse(FaceIdAction.FAILED, message, trackingId, timestamp, null);
80
+ }
81
+
82
+ // 4. Determine action using AttendanceService state query
83
+ FaceIdAction action;
84
+ AttendanceResponse attendanceResponse = null;
85
+ String message = "Success";
86
+
87
+ try {
88
+ Optional<AttendanceRecord> todayRecord = attendanceService.getTodayAttendanceRecord(user);
89
+
90
+ if (todayRecord.isEmpty()) {
91
+ // Not checked in yet -> CHECKIN
92
+ attendanceResponse = attendanceService.checkIn(user);
93
+ action = FaceIdAction.CHECKIN;
94
+ message = "Checked in successfully";
95
+ } else if (todayRecord.get().getCheckOutTime() == null) {
96
+ // Checked in, not checked out -> CHECKOUT
97
+ attendanceResponse = attendanceService.checkOut(user);
98
+ action = FaceIdAction.CHECKOUT;
99
+ message = "Checked out successfully";
100
+ } else {
101
+ // Already checked out
102
+ action = FaceIdAction.IGNORED;
103
+ message = "Already checked out today";
104
+ }
105
+
106
+ log.info("Face ID attendance processed: student={}, action={}", request.getStudentCode(), action);
107
+
108
+ } catch (Exception e) {
109
+ // Catch specific attendance exceptions
110
+ String errorMessage = e.getMessage();
111
+ log.error("Attendance processing error for student {}: {}", request.getStudentCode(), errorMessage, e);
112
+ saveRecognitionLog(request, FaceIdAction.FAILED, errorMessage, null, timestamp);
113
+ return buildResponse(FaceIdAction.FAILED, errorMessage, trackingId, timestamp, null);
114
+ }
115
+
116
+ // 5. Save recognition log
117
+ saveRecognitionLog(request, action, message, attendanceResponse, timestamp);
118
+
119
+ // 6. Cache trackingId for idempotency
120
+ if (org.springframework.util.StringUtils.hasText(trackingId)) {
121
+ processedTrackingIds.put(trackingId, action);
122
+ }
123
+
124
+ // 7. Build and return response
125
+ return buildResponse(true, action, message, trackingId, timestamp, attendanceResponse);
126
+ }
127
+
128
+ private void saveRecognitionLog(
129
+ FaceIdAttendanceRequest request,
130
+ FaceIdAction action,
131
+ String message,
132
+ AttendanceResponse attendance,
133
+ Instant timestamp) {
134
+
135
+ try {
136
+ String requestPayload = objectMapper.writeValueAsString(request);
137
+ String responsePayload = objectMapper.writeValueAsString(
138
+ FaceIdAttendanceResponse.builder()
139
+ .action(action)
140
+ .message(message)
141
+ .timestamp(timestamp)
142
+ .build()
143
+ );
144
+
145
+ FaceIdRecognition log = FaceIdRecognition.builder()
146
+ .studentCode(request.getStudentCode())
147
+ .confidence(request.getConfidence())
148
+ .cameraId(request.getCameraId())
149
+ .imageId(request.getImageId())
150
+ .trackingId(request.getTrackingId())
151
+ .liveness(request.getLiveness())
152
+ .action(action)
153
+ .message(message)
154
+ .requestPayload(requestPayload)
155
+ .responsePayload(responsePayload)
156
+ .capturedAt(request.getCapturedAt())
157
+ .build();
158
+
159
+ faceIdRecognitionRepository.save(log);
160
+
161
+ } catch (JsonProcessingException e) {
162
+ log.error("Failed to serialize request/response for log", e);
163
+ }
164
+ }
165
+
166
+ private FaceIdAttendanceResponse buildResponse(
167
+ FaceIdAction action,
168
+ String message,
169
+ String trackingId,
170
+ Instant timestamp,
171
+ AttendanceResponse attendance) {
172
+
173
+ return FaceIdAttendanceResponse.builder()
174
+ .success(action != FaceIdAction.FAILED)
175
+ .action(action)
176
+ .message(message)
177
+ .trackingId(trackingId)
178
+ .timestamp(timestamp)
179
+ .attendance(attendance)
180
+ .build();
181
+ }
182
+
183
+ private FaceIdAttendanceResponse buildResponse(
184
+ boolean success,
185
+ FaceIdAction action,
186
+ String message,
187
+ String trackingId,
188
+ Instant timestamp,
189
+ AttendanceResponse attendance) {
190
+
191
+ return FaceIdAttendanceResponse.builder()
192
+ .success(success)
193
+ .action(action)
194
+ .message(message)
195
+ .trackingId(trackingId)
196
+ .timestamp(timestamp)
197
+ .attendance(attendance)
198
+ .build();
199
+ }
200
+ }
src/main/java/com/attendenceSystem/module/log/dto/response/AuditLogResponse.java CHANGED
@@ -1,5 +1,7 @@
1
  package com.attendenceSystem.module.log.dto.response;
2
 
 
 
3
  import com.attendenceSystem.module.log.entity.enums.LogAction;
4
  import com.attendenceSystem.module.log.entity.enums.LogEntityType;
5
 
@@ -14,5 +16,5 @@ public record AuditLogResponse(
14
  Long entityId,
15
  LogAction action,
16
  String description,
17
- String createdAt) {
18
  }
 
1
  package com.attendenceSystem.module.log.dto.response;
2
 
3
+ import java.time.LocalDateTime;
4
+
5
  import com.attendenceSystem.module.log.entity.enums.LogAction;
6
  import com.attendenceSystem.module.log.entity.enums.LogEntityType;
7
 
 
16
  Long entityId,
17
  LogAction action,
18
  String description,
19
+ LocalDateTime createdAt) {
20
  }
src/main/java/com/attendenceSystem/module/log/entity/AuditLog.java CHANGED
@@ -1,6 +1,7 @@
1
  package com.attendenceSystem.module.log.entity;
2
 
3
  import java.time.Instant;
 
4
 
5
  import org.hibernate.annotations.CreationTimestamp;
6
 
@@ -50,7 +51,7 @@ public class AuditLog {
50
 
51
  @CreationTimestamp
52
  @Column(name = "created_at", updatable = false)
53
- private Instant createdAt;
54
 
55
  @ManyToOne(fetch = FetchType.LAZY)
56
  @JoinColumn(name = "user_id", updatable = false)
 
1
  package com.attendenceSystem.module.log.entity;
2
 
3
  import java.time.Instant;
4
+ import java.time.LocalDateTime;
5
 
6
  import org.hibernate.annotations.CreationTimestamp;
7
 
 
51
 
52
  @CreationTimestamp
53
  @Column(name = "created_at", updatable = false)
54
+ private LocalDateTime createdAt;
55
 
56
  @ManyToOne(fetch = FetchType.LAZY)
57
  @JoinColumn(name = "user_id", updatable = false)
src/main/java/com/attendenceSystem/module/log/mapper/response/AuditLogResponseMapper.java CHANGED
@@ -4,7 +4,6 @@ import org.springframework.stereotype.Component;
4
 
5
  import com.attendenceSystem.module.log.dto.response.AuditLogResponse;
6
  import com.attendenceSystem.module.log.entity.AuditLog;
7
- import com.attendenceSystem.util.TimeUtil;
8
 
9
  @Component
10
  public class AuditLogResponseMapper {
@@ -17,7 +16,7 @@ public class AuditLogResponseMapper {
17
  .entityId(auditLog.getEntityId())
18
  .action(auditLog.getAction())
19
  .description(auditLog.getDescription())
20
- .createdAt(TimeUtil.formatInstant(auditLog.getCreatedAt(), null))
21
  .build();
22
  }
23
  }
 
4
 
5
  import com.attendenceSystem.module.log.dto.response.AuditLogResponse;
6
  import com.attendenceSystem.module.log.entity.AuditLog;
 
7
 
8
  @Component
9
  public class AuditLogResponseMapper {
 
16
  .entityId(auditLog.getEntityId())
17
  .action(auditLog.getAction())
18
  .description(auditLog.getDescription())
19
+ .createdAt(auditLog.getCreatedAt())
20
  .build();
21
  }
22
  }
src/main/java/com/attendenceSystem/module/otp/dto/response/OtpResponse.java CHANGED
@@ -1,6 +1,6 @@
1
  package com.attendenceSystem.module.otp.dto.response;
2
 
3
- import java.time.Instant;
4
 
5
  import com.attendenceSystem.module.otp.entity.enums.OtpPurpose;
6
 
@@ -8,8 +8,8 @@ public record OtpResponse(
8
  Long id,
9
  String destination,
10
  OtpPurpose purpose,
11
- Instant expiredAt,
12
- Instant createdAt,
13
  boolean used
14
  ) {
15
  }
 
1
  package com.attendenceSystem.module.otp.dto.response;
2
 
3
+ import java.time.LocalDateTime;
4
 
5
  import com.attendenceSystem.module.otp.entity.enums.OtpPurpose;
6
 
 
8
  Long id,
9
  String destination,
10
  OtpPurpose purpose,
11
+ LocalDateTime expiredAt,
12
+ LocalDateTime createdAt,
13
  boolean used
14
  ) {
15
  }
src/main/java/com/attendenceSystem/module/otp/entity/Otp.java CHANGED
@@ -1,6 +1,7 @@
1
  package com.attendenceSystem.module.otp.entity;
2
 
3
  import java.time.Instant;
 
4
 
5
  import com.attendenceSystem.module.otp.entity.enums.OtpPurpose;
6
 
@@ -35,13 +36,13 @@ public class Otp {
35
  @Column(name = "purpose", nullable = false)
36
  private OtpPurpose purpose;
37
  @Column(name = "expired_at", nullable = false)
38
- private Instant expiredAt;
39
  @Column(name = "created_at", nullable = false)
40
- private Instant createdAt;
41
  @Column(name = "used", nullable = false)
42
  private boolean used;
43
 
44
  public boolean isExpired() {
45
- return Instant.now().isAfter(expiredAt);
46
  }
47
  }
 
1
  package com.attendenceSystem.module.otp.entity;
2
 
3
  import java.time.Instant;
4
+ import java.time.LocalDateTime;
5
 
6
  import com.attendenceSystem.module.otp.entity.enums.OtpPurpose;
7
 
 
36
  @Column(name = "purpose", nullable = false)
37
  private OtpPurpose purpose;
38
  @Column(name = "expired_at", nullable = false)
39
+ private LocalDateTime expiredAt;
40
  @Column(name = "created_at", nullable = false)
41
+ private LocalDateTime createdAt;
42
  @Column(name = "used", nullable = false)
43
  private boolean used;
44
 
45
  public boolean isExpired() {
46
+ return LocalDateTime.now().isAfter(expiredAt);
47
  }
48
  }
src/main/java/com/attendenceSystem/module/otp/mapper/request/SendOtpRequestMapper.java CHANGED
@@ -1,7 +1,7 @@
1
  package com.attendenceSystem.module.otp.mapper.request;
2
 
3
  import java.time.Instant;
4
- import java.time.temporal.ChronoUnit;
5
 
6
  import com.attendenceSystem.module.otp.dto.request.SendOtpRequest;
7
  import com.attendenceSystem.module.otp.entity.Otp;
@@ -15,12 +15,12 @@ public class SendOtpRequestMapper {
15
  private static final int OTP_EXPIRY_MINUTES = 5;
16
 
17
  public static Otp toEntity(SendOtpRequest request, String code) {
18
- Instant now = Instant.now();
19
  return Otp.builder()
20
  .destination(request.getDestination())
21
  .code(code)
22
  .purpose(request.getPurpose())
23
- .expiredAt(now.plus(OTP_EXPIRY_MINUTES, ChronoUnit.MINUTES))
24
  .createdAt(now)
25
  .used(false)
26
  .build();
 
1
  package com.attendenceSystem.module.otp.mapper.request;
2
 
3
  import java.time.Instant;
4
+ import java.time.LocalDateTime;
5
 
6
  import com.attendenceSystem.module.otp.dto.request.SendOtpRequest;
7
  import com.attendenceSystem.module.otp.entity.Otp;
 
15
  private static final int OTP_EXPIRY_MINUTES = 5;
16
 
17
  public static Otp toEntity(SendOtpRequest request, String code) {
18
+ LocalDateTime now = LocalDateTime.now();
19
  return Otp.builder()
20
  .destination(request.getDestination())
21
  .code(code)
22
  .purpose(request.getPurpose())
23
+ .expiredAt(now.plusMinutes(OTP_EXPIRY_MINUTES))
24
  .createdAt(now)
25
  .used(false)
26
  .build();
src/main/java/com/attendenceSystem/module/report/controller/ReportController.java CHANGED
@@ -52,6 +52,7 @@ public class ReportController {
52
  request.setContent(content);
53
  request.setAttachmentUrl(link);
54
  request.setSharedUserIds(sharedUserIds);
 
55
 
56
  reportService.createReport(request);
57
 
 
52
  request.setContent(content);
53
  request.setAttachmentUrl(link);
54
  request.setSharedUserIds(sharedUserIds);
55
+ request.setFiles(files);
56
 
57
  reportService.createReport(request);
58
 
src/main/java/com/attendenceSystem/module/report/dto/request/CreateReportRequest.java CHANGED
@@ -6,6 +6,7 @@ import lombok.AllArgsConstructor;
6
  import lombok.Getter;
7
  import lombok.NoArgsConstructor;
8
  import lombok.Setter;
 
9
 
10
  @Getter
11
  @Setter
@@ -24,4 +25,6 @@ public class CreateReportRequest {
24
  private String attachmentUrl;
25
 
26
  private Long[] sharedUserIds;
 
 
27
  }
 
6
  import lombok.Getter;
7
  import lombok.NoArgsConstructor;
8
  import lombok.Setter;
9
+ import org.springframework.web.multipart.MultipartFile;
10
 
11
  @Getter
12
  @Setter
 
25
  private String attachmentUrl;
26
 
27
  private Long[] sharedUserIds;
28
+
29
+ private MultipartFile[] files;
30
  }
src/main/java/com/attendenceSystem/module/report/dto/response/ReportDetailResponse.java CHANGED
@@ -1,6 +1,6 @@
1
  package com.attendenceSystem.module.report.dto.response;
2
 
3
- import java.time.Instant;
4
 
5
  import com.attendenceSystem.module.report.entity.enums.ReportStatus;
6
 
@@ -9,9 +9,10 @@ public record ReportDetailResponse(
9
  String title,
10
  String content,
11
  String attachmentUrl,
 
12
  String employeeName,
13
  ReportStatus status,
14
  String rejectReason,
15
- Instant createdAt,
16
- Instant reviewedAt) {
17
- }
 
1
  package com.attendenceSystem.module.report.dto.response;
2
 
3
+ import java.time.LocalDateTime;
4
 
5
  import com.attendenceSystem.module.report.entity.enums.ReportStatus;
6
 
 
9
  String title,
10
  String content,
11
  String attachmentUrl,
12
+ String attachmentFiles,
13
  String employeeName,
14
  ReportStatus status,
15
  String rejectReason,
16
+ LocalDateTime createdAt,
17
+ LocalDateTime reviewedAt) {
18
+ }
src/main/java/com/attendenceSystem/module/report/dto/response/ReportResponse.java CHANGED
@@ -1,6 +1,6 @@
1
  package com.attendenceSystem.module.report.dto.response;
2
 
3
- import java.time.Instant;
4
 
5
  import com.attendenceSystem.module.report.entity.enums.ReportStatus;
6
 
@@ -10,5 +10,7 @@ public record ReportResponse(
10
  String content,
11
  String employeeName,
12
  ReportStatus status,
13
- Instant createdAt) {
14
- }
 
 
 
1
  package com.attendenceSystem.module.report.dto.response;
2
 
3
+ import java.time.LocalDateTime;
4
 
5
  import com.attendenceSystem.module.report.entity.enums.ReportStatus;
6
 
 
10
  String content,
11
  String employeeName,
12
  ReportStatus status,
13
+ LocalDateTime createdAt,
14
+ String attachmentUrl,
15
+ String attachmentFiles) {
16
+ }
src/main/java/com/attendenceSystem/module/report/entity/Report.java CHANGED
@@ -1,6 +1,7 @@
1
  package com.attendenceSystem.module.report.entity;
2
 
3
  import java.time.Instant;
 
4
 
5
  import com.attendenceSystem.module.report.entity.enums.ReportStatus;
6
  import com.attendenceSystem.module.user.entity.User;
@@ -12,6 +13,7 @@ import jakarta.persistence.GeneratedValue;
12
  import jakarta.persistence.GenerationType;
13
  import jakarta.persistence.Id;
14
  import jakarta.persistence.JoinColumn;
 
15
  import jakarta.persistence.ManyToOne;
16
  import jakarta.persistence.Table;
17
 
@@ -47,6 +49,10 @@ public class Report {
47
  @Column(name = "attachment_url")
48
  private String attachmentUrl;
49
 
 
 
 
 
50
  @Column(name = "status", nullable = false)
51
  private ReportStatus status;
52
 
@@ -54,8 +60,8 @@ public class Report {
54
  private String rejectReason;
55
 
56
  @Column(name = "created_at", nullable = false, updatable = false)
57
- private Instant createdAt;
58
 
59
  @Column(name = "reviewed_at")
60
- private Instant reviewedAt;
61
  }
 
1
  package com.attendenceSystem.module.report.entity;
2
 
3
  import java.time.Instant;
4
+ import java.time.LocalDateTime;
5
 
6
  import com.attendenceSystem.module.report.entity.enums.ReportStatus;
7
  import com.attendenceSystem.module.user.entity.User;
 
13
  import jakarta.persistence.GenerationType;
14
  import jakarta.persistence.Id;
15
  import jakarta.persistence.JoinColumn;
16
+ import jakarta.persistence.Lob;
17
  import jakarta.persistence.ManyToOne;
18
  import jakarta.persistence.Table;
19
 
 
49
  @Column(name = "attachment_url")
50
  private String attachmentUrl;
51
 
52
+ @Lob
53
+ @Column(name = "attachment_files")
54
+ private String attachmentFiles;
55
+
56
  @Column(name = "status", nullable = false)
57
  private ReportStatus status;
58
 
 
60
  private String rejectReason;
61
 
62
  @Column(name = "created_at", nullable = false, updatable = false)
63
+ private LocalDateTime createdAt;
64
 
65
  @Column(name = "reviewed_at")
66
+ private LocalDateTime reviewedAt;
67
  }
src/main/java/com/attendenceSystem/module/report/mapper/response/ReportDetailResponseMapper.java CHANGED
@@ -15,6 +15,7 @@ public class ReportDetailResponseMapper {
15
  report.getTitle(),
16
  report.getContent(),
17
  report.getAttachmentUrl(),
 
18
  report.getEmployee().getFullName(),
19
  report.getStatus(),
20
  report.getRejectReason(),
 
15
  report.getTitle(),
16
  report.getContent(),
17
  report.getAttachmentUrl(),
18
+ report.getAttachmentFiles(),
19
  report.getEmployee().getFullName(),
20
  report.getStatus(),
21
  report.getRejectReason(),
src/main/java/com/attendenceSystem/module/report/mapper/response/ReportResponseMapper.java CHANGED
@@ -16,7 +16,9 @@ public class ReportResponseMapper {
16
  report.getContent(),
17
  report.getEmployee().getFullName(),
18
  report.getStatus(),
19
- report.getCreatedAt()
 
 
20
  );
21
  }
22
  }
 
16
  report.getContent(),
17
  report.getEmployee().getFullName(),
18
  report.getStatus(),
19
+ report.getCreatedAt(),
20
+ report.getAttachmentUrl(),
21
+ report.getAttachmentFiles()
22
  );
23
  }
24
  }
src/main/java/com/attendenceSystem/module/report/service/impl/ReportServiceImpl.java CHANGED
@@ -1,8 +1,15 @@
1
  package com.attendenceSystem.module.report.service.impl;
2
 
 
 
 
 
 
3
  import java.time.Instant;
 
4
  import java.util.ArrayList;
5
  import java.util.List;
 
6
 
7
  import org.springframework.data.domain.Page;
8
  import org.springframework.data.domain.Pageable;
@@ -24,24 +31,44 @@ import com.attendenceSystem.module.report.service.ReportService;
24
  import com.attendenceSystem.module.user.entity.User;
25
  import com.attendenceSystem.module.user.repository.UserRepository;
26
  import com.attendenceSystem.util.SecurityUtil;
27
-
28
  import lombok.RequiredArgsConstructor;
 
 
 
29
 
30
  @Service
31
  @RequiredArgsConstructor
 
32
  public class ReportServiceImpl implements ReportService {
33
 
34
  private final ReportRepository reportRepository;
35
  private final ReportShareRepository reportShareRepository;
36
  private final UserRepository userRepository;
37
 
 
 
 
38
  @Transactional
39
  @Override
40
  public ReportResponse createReport(final CreateReportRequest request) {
41
  User employee = findCurrentUser();
42
 
43
  Report report = CreateReportRequestMapper.toEntity(request, employee);
44
- report.setCreatedAt(Instant.now());
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
  Report savedReport = reportRepository.save(report);
47
 
@@ -63,6 +90,32 @@ public class ReportServiceImpl implements ReportService {
63
  return ReportResponseMapper.fromEntity(savedReport);
64
  }
65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  @Override
67
  public Page<ReportResponse> getAllReports(final Pageable pageable) {
68
  return reportRepository.findAllByOrderByCreatedAtDesc(pageable)
 
1
  package com.attendenceSystem.module.report.service.impl;
2
 
3
+ import java.io.File;
4
+ import java.io.IOException;
5
+ import java.nio.file.Files;
6
+ import java.nio.file.Path;
7
+ import java.nio.file.Paths;
8
  import java.time.Instant;
9
+ import java.time.LocalDateTime;
10
  import java.util.ArrayList;
11
  import java.util.List;
12
+ import java.util.UUID;
13
 
14
  import org.springframework.data.domain.Page;
15
  import org.springframework.data.domain.Pageable;
 
31
  import com.attendenceSystem.module.user.entity.User;
32
  import com.attendenceSystem.module.user.repository.UserRepository;
33
  import com.attendenceSystem.util.SecurityUtil;
 
34
  import lombok.RequiredArgsConstructor;
35
+ import lombok.extern.slf4j.Slf4j;
36
+ import org.springframework.beans.factory.annotation.Value;
37
+ import org.springframework.web.multipart.MultipartFile;
38
 
39
  @Service
40
  @RequiredArgsConstructor
41
+ @Slf4j
42
  public class ReportServiceImpl implements ReportService {
43
 
44
  private final ReportRepository reportRepository;
45
  private final ReportShareRepository reportShareRepository;
46
  private final UserRepository userRepository;
47
 
48
+ @Value("${app.storage.upload-dir:uploads}")
49
+ private String storageBaseDir;
50
+
51
  @Transactional
52
  @Override
53
  public ReportResponse createReport(final CreateReportRequest request) {
54
  User employee = findCurrentUser();
55
 
56
  Report report = CreateReportRequestMapper.toEntity(request, employee);
57
+ report.setCreatedAt(LocalDateTime.now());
58
+
59
+ // Xử lý upload files
60
+ if (request.getFiles() != null && request.getFiles().length > 0) {
61
+ List<String> filePaths = new ArrayList<>();
62
+ for (MultipartFile file : request.getFiles()) {
63
+ if (!file.isEmpty()) {
64
+ String filePath = saveFile(file);
65
+ filePaths.add(filePath);
66
+ }
67
+ }
68
+ if (!filePaths.isEmpty()) {
69
+ report.setAttachmentFiles(String.join(",", filePaths));
70
+ }
71
+ }
72
 
73
  Report savedReport = reportRepository.save(report);
74
 
 
90
  return ReportResponseMapper.fromEntity(savedReport);
91
  }
92
 
93
+ private String saveFile(MultipartFile file) {
94
+ try {
95
+ // Tạo thư mục nếu chưa tồn tại
96
+ String reportUploadDir = storageBaseDir + "/reports";
97
+ Path uploadPath = Paths.get(reportUploadDir);
98
+ if (!Files.exists(uploadPath)) {
99
+ Files.createDirectories(uploadPath);
100
+ }
101
+
102
+ // Tạo tên file unique
103
+ String originalFilename = file.getOriginalFilename();
104
+ String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
105
+ String uniqueFilename = UUID.randomUUID().toString() + extension;
106
+
107
+ // Lưu file
108
+ Path filePath = uploadPath.resolve(uniqueFilename);
109
+ file.transferTo(filePath.toFile());
110
+
111
+ // Trả về relative path
112
+ return reportUploadDir + "/" + uniqueFilename;
113
+ } catch (IOException e) {
114
+ log.error("Failed to save file", e);
115
+ throw new RuntimeException("Không thể lưu file: " + e.getMessage());
116
+ }
117
+ }
118
+
119
  @Override
120
  public Page<ReportResponse> getAllReports(final Pageable pageable) {
121
  return reportRepository.findAllByOrderByCreatedAtDesc(pageable)
src/main/java/com/attendenceSystem/module/user/controller/AccountController.java CHANGED
@@ -5,8 +5,6 @@ import org.springframework.data.web.PageableDefault;
5
  import org.springframework.stereotype.Controller;
6
  import org.springframework.ui.Model;
7
  import org.springframework.web.bind.annotation.PathVariable;
8
- import org.springframework.web.bind.annotation.RequestMapping;
9
-
10
  import com.attendenceSystem.constant.Routes;
11
  import com.attendenceSystem.constant.Views;
12
  import com.attendenceSystem.module.user.dto.response.UserResponse;
 
5
  import org.springframework.stereotype.Controller;
6
  import org.springframework.ui.Model;
7
  import org.springframework.web.bind.annotation.PathVariable;
 
 
8
  import com.attendenceSystem.constant.Routes;
9
  import com.attendenceSystem.constant.Views;
10
  import com.attendenceSystem.module.user.dto.response.UserResponse;
src/main/java/com/attendenceSystem/module/user/dto/request/UpdateUserInformationRequest.java CHANGED
@@ -3,7 +3,6 @@ package com.attendenceSystem.module.user.dto.request;
3
  import com.attendenceSystem.module.user.entity.enums.Specialization;
4
 
5
  import jakarta.validation.constraints.Email;
6
- import jakarta.validation.constraints.NotBlank;
7
  import jakarta.validation.constraints.Size;
8
  import lombok.AllArgsConstructor;
9
  import lombok.Data;
 
3
  import com.attendenceSystem.module.user.entity.enums.Specialization;
4
 
5
  import jakarta.validation.constraints.Email;
 
6
  import jakarta.validation.constraints.Size;
7
  import lombok.AllArgsConstructor;
8
  import lombok.Data;
src/main/java/com/attendenceSystem/module/user/entity/User.java CHANGED
@@ -1,6 +1,7 @@
1
  package com.attendenceSystem.module.user.entity;
2
 
3
  import java.time.Instant;
 
4
 
5
  import org.hibernate.annotations.CreationTimestamp;
6
  import org.hibernate.annotations.UpdateTimestamp;
@@ -56,10 +57,10 @@ public class User {
56
  private Department department;
57
  @CreationTimestamp
58
  @Column(name = "created_at", nullable = false, updatable = false)
59
- private Instant createdAt;
60
  @UpdateTimestamp
61
  @Column(name = "updated_at", nullable = false)
62
- private Instant updatedAt;
63
  @Column(name = "must_change_password", nullable = false)
64
  @Builder.Default
65
  private boolean mustChangePassword = false;
 
1
  package com.attendenceSystem.module.user.entity;
2
 
3
  import java.time.Instant;
4
+ import java.time.LocalDateTime;
5
 
6
  import org.hibernate.annotations.CreationTimestamp;
7
  import org.hibernate.annotations.UpdateTimestamp;
 
57
  private Department department;
58
  @CreationTimestamp
59
  @Column(name = "created_at", nullable = false, updatable = false)
60
+ private LocalDateTime createdAt;
61
  @UpdateTimestamp
62
  @Column(name = "updated_at", nullable = false)
63
+ private LocalDateTime updatedAt;
64
  @Column(name = "must_change_password", nullable = false)
65
  @Builder.Default
66
  private boolean mustChangePassword = false;
src/main/java/com/attendenceSystem/module/user/service/impl/UserServiceImpl.java CHANGED
@@ -1,9 +1,5 @@
1
  package com.attendenceSystem.module.user.service.impl;
2
 
3
- import org.springframework.data.domain.Page;
4
- import org.springframework.data.domain.Pageable;
5
- import org.springframework.security.core.Authentication;
6
- import org.springframework.security.core.context.SecurityContextHolder;
7
  import org.springframework.security.crypto.password.PasswordEncoder;
8
  import org.springframework.stereotype.Service;
9
  import org.springframework.transaction.annotation.Transactional;
 
1
  package com.attendenceSystem.module.user.service.impl;
2
 
 
 
 
 
3
  import org.springframework.security.crypto.password.PasswordEncoder;
4
  import org.springframework.stereotype.Service;
5
  import org.springframework.transaction.annotation.Transactional;
src/main/resources/application.properties CHANGED
@@ -59,4 +59,8 @@ app.storage.upload-dir=uploads
59
  # server.error.include-stacktrace=never
60
  # server.error.include-exception=false
61
 
62
- app.timezone=Asia/Ho_Chi_Minh
 
 
 
 
 
59
  # server.error.include-stacktrace=never
60
  # server.error.include-exception=false
61
 
62
+ app.timezone=Asia/Ho_Chi_Minh
63
+
64
+ # Face ID Configuration
65
+ face-id.api-key=${FACE_ID_API_KEY:dev-secret-key-change-in-production}
66
+ face-id.confidence-threshold=0.90
src/main/resources/templates/cms/dashboard/dashboard-employee.html CHANGED
@@ -42,31 +42,6 @@
42
 
43
  <span class="employee-stat-card__trend">↗</span>
44
  </div>
45
-
46
- <div class="employee-stat-card report-approved">
47
- <div class="employee-stat-card__icon">
48
- <i class="bi bi-file-earmark-check"></i>
49
- </div>
50
-
51
- <div class="employee-stat-card__content">
52
- <h3 class="employee-stat-card__number">10</h3>
53
- <p class="employee-stat-card__label">Báo cáo được duyệt</p>
54
- </div>
55
- <span class="employee-stat-card__trend">V</span>
56
- </div>
57
-
58
- <div class="employee-stat-card report-rejected">
59
- <div class="employee-stat-card__icon">
60
- <i class="bi bi-file-earmark-excel-fill"></i>
61
- </div>
62
-
63
- <div class="employee-stat-card__content">
64
- <h3 class="employee-stat-card__number">1</h3>
65
- <p class="employee-stat-card__label">Báo cáo bị từ chối</p>
66
- </div>
67
- <span class="employee-stat-card__trend">X</span>
68
- </div>
69
-
70
  <div class="employee-stat-card attendance-rate">
71
  <div class="employee-stat-card__icon">
72
  <i class="bi bi-file-earmark-bar-graph-fill"></i>
@@ -241,17 +216,17 @@
241
 
242
  <div class="profile-item">
243
  <label>Email</label>
244
- <span >haivuive266@gmail.com</span>
245
  </div>
246
 
247
  <div class="profile-item">
248
  <label>Số điện thoại</label>
249
- <span >0987654321</span>
250
  </div>
251
 
252
  <div class="profile-item">
253
  <label>Phòng ban</label>
254
- <span >IT Department</span>
255
  </div>
256
 
257
  <div class="profile-item">
 
42
 
43
  <span class="employee-stat-card__trend">↗</span>
44
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  <div class="employee-stat-card attendance-rate">
46
  <div class="employee-stat-card__icon">
47
  <i class="bi bi-file-earmark-bar-graph-fill"></i>
 
216
 
217
  <div class="profile-item">
218
  <label>Email</label>
219
+ <span th:text="${#authentication.principal.email}"></span>
220
  </div>
221
 
222
  <div class="profile-item">
223
  <label>Số điện thoại</label>
224
+ <span th:text="${#authentication.principal.phone}"></span>
225
  </div>
226
 
227
  <div class="profile-item">
228
  <label>Phòng ban</label>
229
+ <span th:text="${#authentication.principal.department}"></span>
230
  </div>
231
 
232
  <div class="profile-item">
src/main/resources/templates/cms/document/document-detail.html CHANGED
@@ -21,6 +21,16 @@
21
  <p id="modalContent"></p>
22
  </div>
23
 
 
 
 
 
 
 
 
 
 
 
24
  </div>
25
 
26
- </div>
 
21
  <p id="modalContent"></p>
22
  </div>
23
 
24
+ <div class="report-attachments">
25
+ <strong>File đính kèm:</strong>
26
+ <div id="modalFiles"></div>
27
+ </div>
28
+
29
+ <div class="report-link">
30
+ <strong>Link tài liệu:</strong>
31
+ <div id="modalLink"></div>
32
+ </div>
33
+
34
  </div>
35
 
36
+ </div>
src/main/resources/templates/cms/document/document-list.html CHANGED
@@ -35,8 +35,6 @@
35
  <th>Ngày nộp</th>
36
  <th>File đính kèm</th>
37
  <th>Link tài liệu</th>
38
- <th>Trạng thái</th>
39
- <th>Thao tác</th>
40
  </tr>
41
  </thead>
42
 
@@ -108,18 +106,29 @@
108
 
109
  const employeeName = report.employeeName || 'N/A';
110
  const attachmentUrl = report.attachmentUrl || '';
 
 
 
 
 
 
 
 
 
 
 
111
 
112
- const statusClass = 'status-' + report.status.toLowerCase();
113
- const statusText = getStatusText(report.status);
 
 
114
 
115
  row.innerHTML =
116
  '<td>' + employeeName + '</td>' +
117
  '<td>' + report.title + '</td>' +
118
  '<td>' + formattedDate + '</td>' +
119
- '<td>' + (attachmentUrl ? '<a href="' + attachmentUrl + '" class="file-link" download>📎 Tải file</a>' : '<span>Không có</span>') + '</td>' +
120
- '<td>' + (attachmentUrl ? '<a href="' + attachmentUrl + '" target="_blank" class="file-link">🔗 Mở link</a>' : '<span>Không có</span>') + '</td>' +
121
- '<td><span class="' + statusClass + '">' + statusText + '</span></td>' +
122
- '<td><div class="action-group"><form method="post"><button class="btn btn-success" type="submit">Duyệt</button></form><form method="post"><button class="btn btn-danger" type="submit">Từ chối</button></form></div></td>';
123
 
124
  tableBody.appendChild(row);
125
  });
@@ -132,13 +141,6 @@
132
  tableBody.innerHTML = '<tr><td colspan="7" style="text-align:center">Lỗi khi tải danh sách báo cáo</td></tr>';
133
  });
134
  });
135
-
136
- function getStatusText(status) {
137
- if (status === 'PENDING') return 'Chờ duyệt';
138
- if (status === 'APPROVED') return 'Đã duyệt';
139
- if (status === 'REJECTED') return 'Từ chối';
140
- return status;
141
- }
142
  </script>
143
  </body>
144
  </html>
 
35
  <th>Ngày nộp</th>
36
  <th>File đính kèm</th>
37
  <th>Link tài liệu</th>
 
 
38
  </tr>
39
  </thead>
40
 
 
106
 
107
  const employeeName = report.employeeName || 'N/A';
108
  const attachmentUrl = report.attachmentUrl || '';
109
+ const attachmentFiles = report.attachmentFiles || '';
110
+
111
+ // Hiển thị files đính kèm
112
+ let filesHtml = '<span>Không có</span>';
113
+ if (attachmentFiles) {
114
+ const filePaths = attachmentFiles.split(',');
115
+ filesHtml = filePaths.map(path => {
116
+ const filename = path.split('/').pop();
117
+ return '<a href="' + path + '" class="file-link" download>📎 ' + filename + '</a>';
118
+ }).join('<br>');
119
+ }
120
 
121
+ // Hiển thị link
122
+ const linkHtml = attachmentUrl ?
123
+ '<a href="' + attachmentUrl + '" target="_blank" class="file-link">🔗 Mở link</a>' :
124
+ '<span>Không có</span>';
125
 
126
  row.innerHTML =
127
  '<td>' + employeeName + '</td>' +
128
  '<td>' + report.title + '</td>' +
129
  '<td>' + formattedDate + '</td>' +
130
+ '<td>' + filesHtml + '</td>' +
131
+ '<td>' + linkHtml + '</td>' +
 
 
132
 
133
  tableBody.appendChild(row);
134
  });
 
141
  tableBody.innerHTML = '<tr><td colspan="7" style="text-align:center">Lỗi khi tải danh sách báo cáo</td></tr>';
142
  });
143
  });
 
 
 
 
 
 
 
144
  </script>
145
  </body>
146
  </html>