diff --git a/pom.xml b/pom.xml index d51d7feef4d1cadc86cdb901f20fdd2e3dd374e2..8b496d9f6da6ddf456294127838d81fd974f6dca 100644 --- a/pom.xml +++ b/pom.xml @@ -107,6 +107,15 @@ org.springframework.boot spring-boot-starter-thymeleaf + + com.fasterxml.jackson.core + jackson-databind + + + com.github.ben-manes.caffeine + caffeine + 3.1.8 + diff --git a/src/main/java/com/attendenceSystem/module/attendance/api/AttendanceManagerApiController.java b/src/main/java/com/attendenceSystem/module/attendance/api/AttendanceManagerApiController.java index ae7c8bbe6d72e7d3411b48fba6830d20a880bd4e..18a22dfdfd714c2fc27787c1c5e69f5e7e0ab050 100644 --- a/src/main/java/com/attendenceSystem/module/attendance/api/AttendanceManagerApiController.java +++ b/src/main/java/com/attendenceSystem/module/attendance/api/AttendanceManagerApiController.java @@ -2,7 +2,6 @@ package com.attendenceSystem.module.attendance.api; import java.time.LocalDate; import java.util.List; -import java.util.Map; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; diff --git a/src/main/java/com/attendenceSystem/module/attendance/controller/AttendanceController.java b/src/main/java/com/attendenceSystem/module/attendance/controller/AttendanceController.java index e59876bb779eaff7bb555ad6e63b86633acc3cb1..b24c054e878696d949545e2970e2645321dcd36c 100644 --- a/src/main/java/com/attendenceSystem/module/attendance/controller/AttendanceController.java +++ b/src/main/java/com/attendenceSystem/module/attendance/controller/AttendanceController.java @@ -55,6 +55,7 @@ public class AttendanceController { @GetMapping(Routes.Attendance.HISTORY) public String attendanceHistory(@PageableDefault(size = 10) Pageable pageable, Model model) { + model.addAttribute("attendanceHistory", attendanceService.getAttendanceHistory(pageable)); return Views.Attendance.HISTORY; } @@ -83,7 +84,7 @@ public class AttendanceController { } attendanceService.createLeaveRequest(createLeaveRequest); redirectAttributes.addFlashAttribute("successMessage", "Yêu cầu nghỉ phép đã được gửi."); - return Views.Attendance.LEAVE_LIST; + return Routes.REDIRECT + Routes.Attendance.ROOT + Routes.Attendance.LEAVE + Routes.Action.CREATE; } @ExceptionHandler({ diff --git a/src/main/java/com/attendenceSystem/module/attendance/dto/response/AttendanceResponse.java b/src/main/java/com/attendenceSystem/module/attendance/dto/response/AttendanceResponse.java index 6765fbdfddd42b948ea4c914b442b303658a5b91..9c6e4b923dee0b3855d19e138a4f957d4bd0c8c6 100644 --- a/src/main/java/com/attendenceSystem/module/attendance/dto/response/AttendanceResponse.java +++ b/src/main/java/com/attendenceSystem/module/attendance/dto/response/AttendanceResponse.java @@ -1,7 +1,7 @@ package com.attendenceSystem.module.attendance.dto.response; -import java.time.Instant; import java.time.LocalDate; +import java.time.LocalDateTime; import com.attendenceSystem.module.attendance.entity.enums.AttendanceStatus; import com.attendenceSystem.module.user.entity.enums.Department; @@ -15,8 +15,8 @@ public record AttendanceResponse( String fullName, Department department, LocalDate attendanceDate, - Instant checkInTime, - Instant checkOutTime, + LocalDateTime checkInTime, + LocalDateTime checkOutTime, AttendanceStatus status, boolean late, boolean earlyLeave, diff --git a/src/main/java/com/attendenceSystem/module/attendance/dto/response/LeaveDetailResponse.java b/src/main/java/com/attendenceSystem/module/attendance/dto/response/LeaveDetailResponse.java index 2d11b2a4e1de57ff3211d456e79eab610798458e..abecb9d825b635119163c059efbd68f8cc75b7a6 100644 --- a/src/main/java/com/attendenceSystem/module/attendance/dto/response/LeaveDetailResponse.java +++ b/src/main/java/com/attendenceSystem/module/attendance/dto/response/LeaveDetailResponse.java @@ -2,6 +2,7 @@ package com.attendenceSystem.module.attendance.dto.response; import java.time.Instant; import java.time.LocalDate; +import java.time.LocalDateTime; import com.attendenceSystem.module.attendance.entity.enums.LeaveStatus; @@ -13,5 +14,5 @@ public record LeaveDetailResponse( LocalDate endDate, String reason, LeaveStatus status, - Instant createdAt) { + LocalDateTime createdAt) { } diff --git a/src/main/java/com/attendenceSystem/module/attendance/dto/response/LeaveRequestResponse.java b/src/main/java/com/attendenceSystem/module/attendance/dto/response/LeaveRequestResponse.java index a36895f6b4570ebf06dbdfa34166bc9a939c010a..38891034250e202e609631049ce5dfd57fd460aa 100644 --- a/src/main/java/com/attendenceSystem/module/attendance/dto/response/LeaveRequestResponse.java +++ b/src/main/java/com/attendenceSystem/module/attendance/dto/response/LeaveRequestResponse.java @@ -1,7 +1,7 @@ package com.attendenceSystem.module.attendance.dto.response; -import java.time.Instant; import java.time.LocalDate; +import java.time.LocalDateTime; import com.attendenceSystem.module.attendance.entity.enums.LeaveStatus; @@ -13,5 +13,5 @@ public record LeaveRequestResponse( LocalDate endDate, String reason, LeaveStatus status, - Instant createdAt) { + LocalDateTime createdAt) { } diff --git a/src/main/java/com/attendenceSystem/module/attendance/entity/AttendanceRecord.java b/src/main/java/com/attendenceSystem/module/attendance/entity/AttendanceRecord.java index 92be39f56b381217106a851a16e27787378f5966..03103ad48cb3d04fc5fd1eea6f4c7e15b4e88fce 100644 --- a/src/main/java/com/attendenceSystem/module/attendance/entity/AttendanceRecord.java +++ b/src/main/java/com/attendenceSystem/module/attendance/entity/AttendanceRecord.java @@ -1,7 +1,7 @@ package com.attendenceSystem.module.attendance.entity; -import java.time.Instant; import java.time.LocalDate; +import java.time.LocalDateTime; import com.attendenceSystem.module.attendance.entity.enums.AttendanceStatus; import com.attendenceSystem.module.user.entity.User; @@ -44,13 +44,13 @@ public class AttendanceRecord { private User user; @Column(name = "check_in_time", nullable = false) - private Instant checkInTime; + private LocalDateTime checkInTime; @Column(name = "attendance_date", nullable = false) private LocalDate attendanceDate; @Column(name = "check_out_time") - private Instant checkOutTime; + private LocalDateTime checkOutTime; @Enumerated(EnumType.STRING) @Column(name = "status", nullable = false) diff --git a/src/main/java/com/attendenceSystem/module/attendance/entity/LeaveRequest.java b/src/main/java/com/attendenceSystem/module/attendance/entity/LeaveRequest.java index 12e2fbd02614ce0505703a9c2f58b9321f161cb1..eafee80b80b816548bc672e54ad177a3196efc1d 100644 --- a/src/main/java/com/attendenceSystem/module/attendance/entity/LeaveRequest.java +++ b/src/main/java/com/attendenceSystem/module/attendance/entity/LeaveRequest.java @@ -1,7 +1,7 @@ package com.attendenceSystem.module.attendance.entity; -import java.time.Instant; import java.time.LocalDate; +import java.time.LocalDateTime; import org.hibernate.annotations.CreationTimestamp; @@ -53,5 +53,5 @@ public class LeaveRequest { @CreationTimestamp @Column(name = "created_at", nullable = false, updatable = false) - private Instant createdAt; + private LocalDateTime createdAt; } diff --git a/src/main/java/com/attendenceSystem/module/attendance/mapper/response/AttendanceResponseMapper.java b/src/main/java/com/attendenceSystem/module/attendance/mapper/response/AttendanceResponseMapper.java index 8f7bae3f541291476b19993ba73535714694b61e..9a3b1f0865cbaf7f214acb11b09353e8dcdff8a9 100644 --- a/src/main/java/com/attendenceSystem/module/attendance/mapper/response/AttendanceResponseMapper.java +++ b/src/main/java/com/attendenceSystem/module/attendance/mapper/response/AttendanceResponseMapper.java @@ -1,5 +1,8 @@ package com.attendenceSystem.module.attendance.mapper.response; +import java.time.LocalDateTime; +import java.time.ZoneId; + import org.springframework.stereotype.Component; import com.attendenceSystem.module.attendance.dto.response.AttendanceResponse; @@ -14,9 +17,12 @@ public class AttendanceResponseMapper { private final AttendanceCalculator attendanceCalculator; public AttendanceResponse fromEntity(AttendanceRecord attendance) { - boolean late = attendanceCalculator.isLate(attendance.getCheckInTime()); - boolean earlyLeave = attendanceCalculator.isEarlyLeave(attendance.getCheckOutTime()); - long workingMinutes = attendanceCalculator.workingMinutes(attendance.getCheckInTime(), attendance.getCheckOutTime()); + boolean late = attendanceCalculator.isLate(attendance.getCheckInTime()); + boolean earlyLeave = attendanceCalculator.isEarlyLeave(attendance.getCheckOutTime()); + long workingMinutes = attendanceCalculator.workingMinutes( + attendance.getCheckInTime(), + attendance.getCheckOutTime() + ); return AttendanceResponse.builder() .id(attendance.getId()) diff --git a/src/main/java/com/attendenceSystem/module/attendance/mapper/response/LeaveDetailResponseMapper.java b/src/main/java/com/attendenceSystem/module/attendance/mapper/response/LeaveDetailResponseMapper.java index 57a89bb0727c9dc3ee0a7780c0f12900ff3801d5..0d980d10855b9ad4ca5d00296ce885746675d608 100644 --- a/src/main/java/com/attendenceSystem/module/attendance/mapper/response/LeaveDetailResponseMapper.java +++ b/src/main/java/com/attendenceSystem/module/attendance/mapper/response/LeaveDetailResponseMapper.java @@ -4,7 +4,6 @@ import org.springframework.stereotype.Component; import com.attendenceSystem.module.attendance.dto.response.LeaveDetailResponse; import com.attendenceSystem.module.attendance.entity.LeaveRequest; -import com.attendenceSystem.module.attendance.entity.enums.LeaveStatus; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/attendenceSystem/module/attendance/mapper/response/LeaveRequestResponseMapper.java b/src/main/java/com/attendenceSystem/module/attendance/mapper/response/LeaveRequestResponseMapper.java index 346a21e31ea2d70e59282da319c8ea548ed26395..62fb19230357f806b037d33a3dcf911412572c77 100644 --- a/src/main/java/com/attendenceSystem/module/attendance/mapper/response/LeaveRequestResponseMapper.java +++ b/src/main/java/com/attendenceSystem/module/attendance/mapper/response/LeaveRequestResponseMapper.java @@ -4,7 +4,6 @@ import org.springframework.stereotype.Component; import com.attendenceSystem.module.attendance.dto.response.LeaveRequestResponse; import com.attendenceSystem.module.attendance.entity.LeaveRequest; -import com.attendenceSystem.module.attendance.entity.enums.LeaveStatus; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/attendenceSystem/module/attendance/model/DateRange.java b/src/main/java/com/attendenceSystem/module/attendance/model/DateRange.java new file mode 100644 index 0000000000000000000000000000000000000000..f4cc3d6df7389f93d8bb2cf20d6dceefa68656e0 --- /dev/null +++ b/src/main/java/com/attendenceSystem/module/attendance/model/DateRange.java @@ -0,0 +1,9 @@ +package com.attendenceSystem.module.attendance.model; + +import java.time.LocalDate; + +public record DateRange( + LocalDate startDate, + LocalDate endDate) { + +} diff --git a/src/main/java/com/attendenceSystem/module/attendance/service/AttendanceService.java b/src/main/java/com/attendenceSystem/module/attendance/service/AttendanceService.java index 65c8eb54950cc243431340487a8d26671c699ca7..24c056e6768541f8c477185e024c87a66313eb3f 100644 --- a/src/main/java/com/attendenceSystem/module/attendance/service/AttendanceService.java +++ b/src/main/java/com/attendenceSystem/module/attendance/service/AttendanceService.java @@ -5,12 +5,16 @@ import org.springframework.data.domain.Pageable; import java.time.LocalDate; import java.util.List; +import java.util.Optional; import com.attendenceSystem.module.attendance.dto.request.CreateLeaveRequest; import com.attendenceSystem.module.attendance.dto.response.AttendanceResponse; import com.attendenceSystem.module.attendance.dto.response.LeaveDetailResponse; import com.attendenceSystem.module.attendance.dto.response.LeaveRequestResponse; import com.attendenceSystem.module.attendance.dto.response.ManagerStatsResponse; +import com.attendenceSystem.module.attendance.entity.AttendanceRecord; +import com.attendenceSystem.module.attendance.entity.enums.AttendanceStatus; +import com.attendenceSystem.module.user.entity.User; public interface AttendanceService { @@ -18,15 +22,19 @@ public interface AttendanceService { AttendanceResponse checkOut(); + AttendanceResponse checkIn(User user); + + AttendanceResponse checkOut(User user); + Page getAttendanceHistory(Pageable pageable); ManagerStatsResponse getManagerStats(String departmentId, LocalDate startDate, LocalDate endDate); - ManagerStatsResponse getManagerStats(String departmentId, LocalDate startDate, LocalDate endDate, com.attendenceSystem.module.attendance.entity.enums.AttendanceStatus status); + ManagerStatsResponse getManagerStats(String departmentId, LocalDate startDate, LocalDate endDate, AttendanceStatus status); List getManagerAttendanceList(String departmentId, LocalDate startDate, LocalDate endDate); - List getManagerAttendanceList(String departmentId, LocalDate startDate, LocalDate endDate, com.attendenceSystem.module.attendance.entity.enums.AttendanceStatus status); + List getManagerAttendanceList(String departmentId, LocalDate startDate, LocalDate endDate, AttendanceStatus status); LeaveRequestResponse createLeaveRequest(CreateLeaveRequest request); @@ -35,4 +43,6 @@ public interface AttendanceService { Page getAllLeaveRequests(Pageable pageable); LeaveDetailResponse getLeaveDetail(Long id); + + Optional getTodayAttendanceRecord(User user); } diff --git a/src/main/java/com/attendenceSystem/module/attendance/service/impl/AttendanceServiceImpl.java b/src/main/java/com/attendenceSystem/module/attendance/service/impl/AttendanceServiceImpl.java index d820657b8687d5bec8ec3a8341e538ac5022223e..4b9f75bf6a468154efe79452a570084c03ad910b 100644 --- a/src/main/java/com/attendenceSystem/module/attendance/service/impl/AttendanceServiceImpl.java +++ b/src/main/java/com/attendenceSystem/module/attendance/service/impl/AttendanceServiceImpl.java @@ -1,12 +1,14 @@ package com.attendenceSystem.module.attendance.service.impl; -import java.time.Instant; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; import org.springframework.beans.factory.annotation.Value; import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -27,6 +29,7 @@ import com.attendenceSystem.module.attendance.mapper.response.AttendanceResponse import com.attendenceSystem.module.attendance.entity.LeaveRequest; import com.attendenceSystem.module.attendance.mapper.response.LeaveDetailResponseMapper; import com.attendenceSystem.module.attendance.mapper.response.LeaveRequestResponseMapper; +import com.attendenceSystem.module.attendance.model.DateRange; import com.attendenceSystem.module.attendance.repository.LeaveRequestRepository; import com.attendenceSystem.module.attendance.repository.AttendanceRecordRepository; import com.attendenceSystem.module.attendance.service.AttendanceService; @@ -61,16 +64,26 @@ public class AttendanceServiceImpl implements AttendanceService { @Transactional @Override public AttendanceResponse checkIn() { - User user = getCurrentUser(); + return checkIn(getCurrentUser()); + } + + @Transactional + @Override + public AttendanceResponse checkOut() { + return checkOut(getCurrentUser()); + } - LocalDate today = LocalDate.now(timeZoneProvider.getZoneId()); + @Transactional + @Override + public AttendanceResponse checkIn(User user) { + LocalDate today = todayDate(); try { Optional existing = attendanceRecordRepository .findByUserAndAttendanceDateWithLock(user, today); if (existing.isPresent()) { throw new AlreadyCheckedInException("Bạn đã điểm danh hôm nay"); } - Instant checkInTime = Instant.now(); + LocalDateTime checkInTime = LocalDateTime.now(); boolean late = attendanceCalculator.isLate(checkInTime); AttendanceStatus status = late ? AttendanceStatus.LATE : AttendanceStatus.PRESENT; AttendanceRecord attendanceRecord = AttendanceRecord.builder() @@ -82,7 +95,7 @@ public class AttendanceServiceImpl implements AttendanceService { .build(); attendanceRecordRepository.save(attendanceRecord); return attendanceResponseMapper.fromEntity(attendanceRecord); - } catch (org.springframework.dao.DataIntegrityViolationException e) { + } catch (DataIntegrityViolationException e) { throw new AlreadyCheckedInException("Bạn đã điểm danh hôm nay"); } catch (ObjectOptimisticLockingFailureException e) { throw new AlreadyCheckedInException("Dữ liệu đã bị thay đổi, vui lòng thử lại"); @@ -91,10 +104,8 @@ public class AttendanceServiceImpl implements AttendanceService { @Transactional @Override - public AttendanceResponse checkOut() { - User user = getCurrentUser(); - - LocalDate today = LocalDate.now(timeZoneProvider.getZoneId()); + public AttendanceResponse checkOut(User user) { + LocalDate today = todayDate(); AttendanceRecord attendance = attendanceRecordRepository .findByUserAndAttendanceDateWithLock(user, today) .orElseThrow(() -> new NotCheckedInException( @@ -105,7 +116,7 @@ public class AttendanceServiceImpl implements AttendanceService { if (attendance.getStatus() != AttendanceStatus.PRESENT && attendance.getStatus() != AttendanceStatus.LATE) { throw new InvalidAttendanceStateException("Trạng thái điểm danh không hợp lệ"); } - Instant checkOutTime = Instant.now(); + LocalDateTime checkOutTime = LocalDateTime.now(); if (checkOutTime.isBefore(attendance.getCheckInTime())) { throw new InvalidAttendanceStateException("Thời gian checkout phải sau thời gian check-in"); } @@ -117,9 +128,8 @@ public class AttendanceServiceImpl implements AttendanceService { attendanceRecordRepository.save(attendance); return attendanceResponseMapper.fromEntity(attendance); } - @Override - public org.springframework.data.domain.Page getAttendanceHistory(final org.springframework.data.domain.Pageable pageable) { + public Page getAttendanceHistory(final Pageable pageable) { User user = getCurrentUser(); return attendanceRecordRepository @@ -167,19 +177,9 @@ public class AttendanceServiceImpl implements AttendanceService { .absent(0) .build(); } + DateRange dateRange = getDateRange(startDate, endDate); - LocalDate effectiveStartDate = startDate; - LocalDate effectiveEndDate = endDate; - if (effectiveStartDate == null && effectiveEndDate == null) { - effectiveStartDate = LocalDate.now(timeZoneProvider.getZoneId()).minusMonths(6); - effectiveEndDate = LocalDate.now(timeZoneProvider.getZoneId()); - } else if (effectiveStartDate != null && effectiveEndDate == null) { - effectiveEndDate = effectiveStartDate; - } else if (effectiveStartDate == null && effectiveEndDate != null) { - effectiveStartDate = effectiveEndDate; - } - - List records = getFilteredRecords(effectiveStartDate, effectiveEndDate, status); + List records = getFilteredRecords(dateRange.startDate(), dateRange.endDate(), status); long checkedIn = 0; long checkedOut = 0; @@ -227,19 +227,9 @@ public class AttendanceServiceImpl implements AttendanceService { } else { employees = userRepository.findByRoleNot(Role.ADMIN); } + DateRange dateRange = getDateRange(startDate, endDate); - LocalDate effectiveStartDate = startDate; - LocalDate effectiveEndDate = endDate; - if (effectiveStartDate == null && effectiveEndDate == null) { - effectiveStartDate = LocalDate.now(timeZoneProvider.getZoneId()).minusMonths(6); - effectiveEndDate = LocalDate.now(timeZoneProvider.getZoneId()); - } else if (effectiveStartDate != null && effectiveEndDate == null) { - effectiveEndDate = effectiveStartDate; - } else if (effectiveStartDate == null && effectiveEndDate != null) { - effectiveStartDate = effectiveEndDate; - } - - List records = getFilteredRecords(effectiveStartDate, effectiveEndDate, status); + List records = getFilteredRecords(dateRange.startDate(), dateRange.endDate(), status); return records.stream() .map(attendanceResponseMapper::fromEntity) @@ -271,14 +261,14 @@ public class AttendanceServiceImpl implements AttendanceService { } @Override - public org.springframework.data.domain.Page getLeaveRequests(final org.springframework.data.domain.Pageable pageable) { + public Page getLeaveRequests(final Pageable pageable) { User user = getCurrentUser(); return leaveRequestRepository.findByUser(user, pageable) .map(leaveRequestResponseMapper::fromEntity); } @Override - public org.springframework.data.domain.Page getAllLeaveRequests(final org.springframework.data.domain.Pageable pageable) { + public Page getAllLeaveRequests(final Pageable pageable) { return leaveRequestRepository.findAll(pageable).map(leaveRequestResponseMapper::fromEntity); } @@ -292,13 +282,43 @@ public class AttendanceServiceImpl implements AttendanceService { return leaveDetailResponseMapper.fromEntity(leave); } + @Override + public Optional getTodayAttendanceRecord(User user) { + if (user == null) { + return Optional.empty(); + } + LocalDate today = todayDate(); + return attendanceRecordRepository.findByUserAndAttendanceDateWithLock(user, today); + } + private User getCurrentUser() { if (!SecurityUtil.isAuthenticated()) { throw new IllegalStateException("Người dùng chưa đăng nhập"); } String currentUsername = SecurityUtil.getCurrentUserName(); return userRepository.findByUsername(currentUsername) - .orElseThrow(() -> new IllegalArgumentException( + .orElseThrow(() -> new IllegalStateException( "Không tìm thấy người dùng với tên đăng nhập: " + currentUsername)); } + + private LocalDate todayDate() { + return LocalDate.now(timeZoneProvider.getZoneId()); + } + + private DateRange getDateRange(LocalDate startDate, LocalDate endDate) { + LocalDate effectiveStartDate = startDate; + LocalDate effectiveEndDate = endDate; + if (effectiveStartDate == null && effectiveEndDate == null) { + LocalDate today = todayDate(); + effectiveStartDate = today.minusMonths(6); + effectiveEndDate = today; + } else if (effectiveStartDate != null && effectiveEndDate == null) { + effectiveEndDate = effectiveStartDate; + } else if (effectiveStartDate == null && effectiveEndDate != null) { + effectiveStartDate = effectiveEndDate; + } + return new DateRange(effectiveStartDate, effectiveEndDate); + } + + } \ No newline at end of file diff --git a/src/main/java/com/attendenceSystem/module/attendance/util/AttendanceCalculator.java b/src/main/java/com/attendenceSystem/module/attendance/util/AttendanceCalculator.java index 540fc82e97d596fdd12830f4fc62c5788c28db42..8f66b7082a6309351e81d7fba91eb579485bad8c 100644 --- a/src/main/java/com/attendenceSystem/module/attendance/util/AttendanceCalculator.java +++ b/src/main/java/com/attendenceSystem/module/attendance/util/AttendanceCalculator.java @@ -1,14 +1,14 @@ package com.attendenceSystem.module.attendance.util; -import java.time.Instant; +import java.time.LocalDateTime; public interface AttendanceCalculator { - boolean isLate(Instant checkInTime); + boolean isLate(LocalDateTime checkInTime); - boolean isEarlyLeave(Instant checkOutTime); + boolean isEarlyLeave(LocalDateTime checkOutTime); - long workingMinutes(Instant checkInTime, Instant checkOutTime); + long workingMinutes(LocalDateTime checkInTime, LocalDateTime checkOutTime); - double totalWorkingHours(Instant checkInTime, Instant checkOutTime); + double totalWorkingHours(LocalDateTime checkInTime, LocalDateTime checkOutTime); } \ No newline at end of file diff --git a/src/main/java/com/attendenceSystem/module/attendance/util/impl/AttendanceCalculatorImpl.java b/src/main/java/com/attendenceSystem/module/attendance/util/impl/AttendanceCalculatorImpl.java index 9dc53e24c173aa74f07baf67716a49227e935121..89ff50c18dce5cf8ed51d2f6f148c791531b6d26 100644 --- a/src/main/java/com/attendenceSystem/module/attendance/util/impl/AttendanceCalculatorImpl.java +++ b/src/main/java/com/attendenceSystem/module/attendance/util/impl/AttendanceCalculatorImpl.java @@ -1,7 +1,7 @@ package com.attendenceSystem.module.attendance.util.impl; import java.time.Duration; -import java.time.Instant; +import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZoneId; @@ -73,7 +73,7 @@ public class AttendanceCalculatorImpl implements AttendanceCalculator { } @Override - public boolean isLate(final Instant checkInTime) { + public boolean isLate(final LocalDateTime checkInTime) { if (checkInTime == null) { return false; } @@ -82,7 +82,7 @@ public class AttendanceCalculatorImpl implements AttendanceCalculator { } @Override - public boolean isEarlyLeave(final Instant checkOutTime) { + public boolean isEarlyLeave(final LocalDateTime checkOutTime) { if (checkOutTime == null) { return false; } @@ -91,7 +91,7 @@ public class AttendanceCalculatorImpl implements AttendanceCalculator { } @Override - public long workingMinutes(final Instant checkInTime, final Instant checkOutTime) { + public long workingMinutes(final LocalDateTime checkInTime, final LocalDateTime checkOutTime) { if (checkInTime == null || checkOutTime == null) { return 0; } @@ -99,7 +99,7 @@ public class AttendanceCalculatorImpl implements AttendanceCalculator { } @Override - public double totalWorkingHours(final Instant checkInTime, final Instant checkOutTime) { + public double totalWorkingHours(final LocalDateTime checkInTime, final LocalDateTime checkOutTime) { long minutes = workingMinutes(checkInTime, checkOutTime); return minutes / 60.0; } diff --git a/src/main/java/com/attendenceSystem/module/faceid/controller/FaceIdAttendanceController.java b/src/main/java/com/attendenceSystem/module/faceid/controller/FaceIdAttendanceController.java new file mode 100644 index 0000000000000000000000000000000000000000..40355dc3d912e3a4aed0521201eb812576c1ea4e --- /dev/null +++ b/src/main/java/com/attendenceSystem/module/faceid/controller/FaceIdAttendanceController.java @@ -0,0 +1,30 @@ +package com.attendenceSystem.module.faceid.controller; + +import com.attendenceSystem.module.faceid.dto.FaceIdAttendanceRequest; +import com.attendenceSystem.module.faceid.dto.FaceIdAttendanceResponse; +import com.attendenceSystem.module.faceid.service.FaceIdAttendanceService; +import com.attendenceSystem.module.faceid.service.impl.FaceIdAttendanceServiceImpl; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/face-id") +@RequiredArgsConstructor +public class FaceIdAttendanceController { + + private final FaceIdAttendanceServiceImpl faceIdAttendanceService; + + @PostMapping("/attendance") + public ResponseEntity processAttendance( + @Valid @RequestBody FaceIdAttendanceRequest request) { + + FaceIdAttendanceResponse response = faceIdAttendanceService.processAttendance(request); + return ResponseEntity.ok(response); + } +} \ No newline at end of file diff --git a/src/main/java/com/attendenceSystem/module/faceid/dto/FaceIdAction.java b/src/main/java/com/attendenceSystem/module/faceid/dto/FaceIdAction.java new file mode 100644 index 0000000000000000000000000000000000000000..807c88bc49e6a5d0ce61302c313cb33cca8f448b --- /dev/null +++ b/src/main/java/com/attendenceSystem/module/faceid/dto/FaceIdAction.java @@ -0,0 +1,8 @@ +package com.attendenceSystem.module.faceid.dto; + +public enum FaceIdAction { + CHECKIN, + CHECKOUT, + IGNORED, + FAILED +} \ No newline at end of file diff --git a/src/main/java/com/attendenceSystem/module/faceid/dto/FaceIdAttendanceRequest.java b/src/main/java/com/attendenceSystem/module/faceid/dto/FaceIdAttendanceRequest.java new file mode 100644 index 0000000000000000000000000000000000000000..42ae06c07776353769ffd1aaf8cc169247b58122 --- /dev/null +++ b/src/main/java/com/attendenceSystem/module/faceid/dto/FaceIdAttendanceRequest.java @@ -0,0 +1,29 @@ +package com.attendenceSystem.module.faceid.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class FaceIdAttendanceRequest { + @NotBlank(message = "studentCode không được để trống") + private String studentCode; + + @NotNull(message = "confidence không được để trống") + private Double confidence; + + @NotNull(message = "capturedAt không được để trống") + private LocalDateTime capturedAt; + + private String cameraId; + + private String imageId; + + @Size(max = 255, message = "trackingId không được vượt quá 255 ký tự") + private String trackingId; + + private Boolean liveness; +} \ No newline at end of file diff --git a/src/main/java/com/attendenceSystem/module/faceid/dto/FaceIdAttendanceResponse.java b/src/main/java/com/attendenceSystem/module/faceid/dto/FaceIdAttendanceResponse.java new file mode 100644 index 0000000000000000000000000000000000000000..36f4195cfa7afa8f3143faa4baaf640856be958c --- /dev/null +++ b/src/main/java/com/attendenceSystem/module/faceid/dto/FaceIdAttendanceResponse.java @@ -0,0 +1,22 @@ +package com.attendenceSystem.module.faceid.dto; + +import com.attendenceSystem.module.attendance.dto.response.AttendanceResponse; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FaceIdAttendanceResponse { + private boolean success; + private FaceIdAction action; + private String message; + private String trackingId; + private Instant timestamp; + private AttendanceResponse attendance; +} \ No newline at end of file diff --git a/src/main/java/com/attendenceSystem/module/faceid/dto/response/FaceProfileResponse.java b/src/main/java/com/attendenceSystem/module/faceid/dto/response/FaceProfileResponse.java index e676b293474455aa8661650635939739a41d286f..1e4c047e9af9bb0a51577efa4075ac5d163e5ff7 100644 --- a/src/main/java/com/attendenceSystem/module/faceid/dto/response/FaceProfileResponse.java +++ b/src/main/java/com/attendenceSystem/module/faceid/dto/response/FaceProfileResponse.java @@ -1,6 +1,6 @@ package com.attendenceSystem.module.faceid.dto.response; -import java.time.Instant; +import java.time.LocalDateTime; import lombok.Builder; @@ -15,6 +15,6 @@ public record FaceProfileResponse( String userName, String userEmail, String userFullName, - Instant createdAt, - Instant updatedAt) { + LocalDateTime createdAt, + LocalDateTime updatedAt) { } diff --git a/src/main/java/com/attendenceSystem/module/faceid/entity/FaceIdRecognition.java b/src/main/java/com/attendenceSystem/module/faceid/entity/FaceIdRecognition.java new file mode 100644 index 0000000000000000000000000000000000000000..e61d05f53f0ce02298ca35b666764e674dbf281d --- /dev/null +++ b/src/main/java/com/attendenceSystem/module/faceid/entity/FaceIdRecognition.java @@ -0,0 +1,61 @@ +package com.attendenceSystem.module.faceid.entity; + +import com.attendenceSystem.module.faceid.dto.FaceIdAction; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "face_id_recognition_logs") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FaceIdRecognition { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "student_code", nullable = false, length = 50) + private String studentCode; + + @Column(name = "confidence", nullable = false) + private Double confidence; + + @Column(name = "camera_id", length = 50) + private String cameraId; + + @Column(name = "image_id", length = 255) + private String imageId; + + @Column(name = "tracking_id", length = 255, unique = true) + private String trackingId; + + @Column(name = "liveness") + private Boolean liveness; + + @Enumerated(EnumType.STRING) + @Column(name = "action", nullable = false, length = 20) + private FaceIdAction action; + + @Column(name = "message", length = 500) + private String message; + + @Column(name = "request_payload", columnDefinition = "JSON") + private String requestPayload; + + @Column(name = "response_payload", columnDefinition = "JSON") + private String responsePayload; + + @Column(name = "captured_at") + private LocalDateTime capturedAt; + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; +} \ No newline at end of file diff --git a/src/main/java/com/attendenceSystem/module/faceid/entity/FaceProfile.java b/src/main/java/com/attendenceSystem/module/faceid/entity/FaceProfile.java index b47f74430fedf573892b28b26dc65d6a768f5964..3e3c595bcbe8e739a9ff473248df1c070622f124 100644 --- a/src/main/java/com/attendenceSystem/module/faceid/entity/FaceProfile.java +++ b/src/main/java/com/attendenceSystem/module/faceid/entity/FaceProfile.java @@ -1,6 +1,7 @@ package com.attendenceSystem.module.faceid.entity; import java.time.Instant; +import java.time.LocalDateTime; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; @@ -56,9 +57,9 @@ public class FaceProfile { @CreationTimestamp @Column(name = "created_at", nullable = false, updatable = false) - private Instant createdAt; + private LocalDateTime createdAt; @UpdateTimestamp @Column(name = "updated_at", nullable = false) - private Instant updatedAt; + private LocalDateTime updatedAt; } \ No newline at end of file diff --git a/src/main/java/com/attendenceSystem/module/faceid/entity/FaceSample.java b/src/main/java/com/attendenceSystem/module/faceid/entity/FaceSample.java index 28c0d1c5eb50951c9b31b8559ac2438b4ee81cc7..5a00f66a8ae252ab96b773ccb805b41f88572b72 100644 --- a/src/main/java/com/attendenceSystem/module/faceid/entity/FaceSample.java +++ b/src/main/java/com/attendenceSystem/module/faceid/entity/FaceSample.java @@ -1,6 +1,7 @@ package com.attendenceSystem.module.faceid.entity; import java.time.Instant; +import java.time.LocalDateTime; import org.hibernate.annotations.CreationTimestamp; @@ -45,5 +46,5 @@ public class FaceSample { @CreationTimestamp @Column(name = "created_at", nullable = false, updatable = false) - private Instant createdAt; + private LocalDateTime createdAt; } \ No newline at end of file diff --git a/src/main/java/com/attendenceSystem/module/faceid/filter/FaceIdApiKeyFilter.java b/src/main/java/com/attendenceSystem/module/faceid/filter/FaceIdApiKeyFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..24383cfa7d900f16010a2dab2d3bf9cf506d9f1e --- /dev/null +++ b/src/main/java/com/attendenceSystem/module/faceid/filter/FaceIdApiKeyFilter.java @@ -0,0 +1,54 @@ +package com.attendenceSystem.module.faceid.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +public class FaceIdApiKeyFilter extends OncePerRequestFilter { + + @Value("${face-id.api-key}") + private String apiKey; + + private static final String API_KEY_HEADER = "X-API-KEY"; + private static final String FACE_ID_PATH_PREFIX = "/api/face-id"; + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + String path = request.getRequestURI(); + return !path.startsWith(FACE_ID_PATH_PREFIX); + } + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + + // String requestApiKey = extractApiKey(request); + + // if (apiKey == null || apiKey.isBlank() || !apiKey.equals(requestApiKey)) { + // throw new RuntimeException("Invalid or missing API key"); + // } + + filterChain.doFilter(request, response); + } + + private String extractApiKey(HttpServletRequest request) { + String apiKeyHeader = request.getHeader(API_KEY_HEADER); + if (apiKeyHeader != null && apiKeyHeader.startsWith("Bearer ")) { + return apiKeyHeader.substring(7); + } + return apiKeyHeader; + } +} \ No newline at end of file diff --git a/src/main/java/com/attendenceSystem/module/faceid/repository/FaceIdRecognitionRepository.java b/src/main/java/com/attendenceSystem/module/faceid/repository/FaceIdRecognitionRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..da4da6fdb4ef4ded42a50d1797234f88022364c8 --- /dev/null +++ b/src/main/java/com/attendenceSystem/module/faceid/repository/FaceIdRecognitionRepository.java @@ -0,0 +1,10 @@ +package com.attendenceSystem.module.faceid.repository; + +import com.attendenceSystem.module.faceid.entity.FaceIdRecognition; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface FaceIdRecognitionRepository extends JpaRepository { + boolean existsByTrackingId(String trackingId); +} \ No newline at end of file diff --git a/src/main/java/com/attendenceSystem/module/faceid/service/FaceIdAttendanceService.java b/src/main/java/com/attendenceSystem/module/faceid/service/FaceIdAttendanceService.java new file mode 100644 index 0000000000000000000000000000000000000000..18f3fbce5aabfe4af77d8a6670af54cac7e80ebd --- /dev/null +++ b/src/main/java/com/attendenceSystem/module/faceid/service/FaceIdAttendanceService.java @@ -0,0 +1,8 @@ +package com.attendenceSystem.module.faceid.service; + +import com.attendenceSystem.module.faceid.dto.FaceIdAttendanceRequest; +import com.attendenceSystem.module.faceid.dto.FaceIdAttendanceResponse; + +public interface FaceIdAttendanceService { + FaceIdAttendanceResponse processAttendance(FaceIdAttendanceRequest request); +} \ No newline at end of file diff --git a/src/main/java/com/attendenceSystem/module/faceid/service/impl/FaceIdAttendanceServiceImpl.java b/src/main/java/com/attendenceSystem/module/faceid/service/impl/FaceIdAttendanceServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..3b3117ef56438e8b066129a8ac22855ab2b97e0e --- /dev/null +++ b/src/main/java/com/attendenceSystem/module/faceid/service/impl/FaceIdAttendanceServiceImpl.java @@ -0,0 +1,200 @@ +package com.attendenceSystem.module.faceid.service.impl; + +import com.attendenceSystem.module.attendance.dto.response.AttendanceResponse; +import com.attendenceSystem.module.attendance.entity.AttendanceRecord; +import com.attendenceSystem.module.attendance.service.AttendanceService; +import com.attendenceSystem.module.faceid.dto.FaceIdAction; +import com.attendenceSystem.module.faceid.dto.FaceIdAttendanceRequest; +import com.attendenceSystem.module.faceid.dto.FaceIdAttendanceResponse; +import com.attendenceSystem.module.faceid.entity.FaceIdRecognition; +import com.attendenceSystem.module.faceid.repository.FaceIdRecognitionRepository; +import com.attendenceSystem.module.faceid.service.FaceIdAttendanceService; +import com.attendenceSystem.module.user.entity.User; +import com.attendenceSystem.module.user.repository.UserRepository; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.time.Duration; +import java.time.Instant; +import java.util.Optional; + +@Service +@Slf4j +@RequiredArgsConstructor +public class FaceIdAttendanceServiceImpl implements FaceIdAttendanceService { + + private final AttendanceService attendanceService; + private final UserRepository userRepository; + private final FaceIdRecognitionRepository faceIdRecognitionRepository; + + @Value("${face-id.confidence-threshold:0.90}") + private double confidenceThreshold; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + private final Cache processedTrackingIds = Caffeine.newBuilder() + .expireAfterWrite(Duration.ofMinutes(5)) + .maximumSize(100_000) + .build(); + + @Override + @Transactional + public FaceIdAttendanceResponse processAttendance(FaceIdAttendanceRequest request) { + String trackingId = request.getTrackingId(); + Instant timestamp = Instant.now(); + + // 1. Check idempotency (only if trackingId is present) + if (StringUtils.hasText(trackingId)) { + FaceIdAction cachedAction = processedTrackingIds.getIfPresent(trackingId); + if (cachedAction != null) { + log.info("Ignoring duplicate trackingId: {}", trackingId); + return buildResponse(FaceIdAction.IGNORED, "DUPLICATE", trackingId, timestamp, null); + } + } + + // 2. Check confidence threshold + if (request.getConfidence() < confidenceThreshold) { + String message = "Confidence below threshold: " + request.getConfidence(); + log.warn(message); + saveRecognitionLog(request, FaceIdAction.FAILED, message, null, timestamp); + return buildResponse(FaceIdAction.FAILED, message, trackingId, timestamp, null); + } + + // 3. Find user by username (studentCode) + User user = userRepository.findByUsername(request.getStudentCode()) + .orElse(null); + + if (user == null) { + String message = "User not found: " + request.getStudentCode(); + log.warn(message); + saveRecognitionLog(request, FaceIdAction.FAILED, message, null, timestamp); + return buildResponse(FaceIdAction.FAILED, message, trackingId, timestamp, null); + } + + // 4. Determine action using AttendanceService state query + FaceIdAction action; + AttendanceResponse attendanceResponse = null; + String message = "Success"; + + try { + Optional todayRecord = attendanceService.getTodayAttendanceRecord(user); + + if (todayRecord.isEmpty()) { + // Not checked in yet -> CHECKIN + attendanceResponse = attendanceService.checkIn(user); + action = FaceIdAction.CHECKIN; + message = "Checked in successfully"; + } else if (todayRecord.get().getCheckOutTime() == null) { + // Checked in, not checked out -> CHECKOUT + attendanceResponse = attendanceService.checkOut(user); + action = FaceIdAction.CHECKOUT; + message = "Checked out successfully"; + } else { + // Already checked out + action = FaceIdAction.IGNORED; + message = "Already checked out today"; + } + + log.info("Face ID attendance processed: student={}, action={}", request.getStudentCode(), action); + + } catch (Exception e) { + // Catch specific attendance exceptions + String errorMessage = e.getMessage(); + log.error("Attendance processing error for student {}: {}", request.getStudentCode(), errorMessage, e); + saveRecognitionLog(request, FaceIdAction.FAILED, errorMessage, null, timestamp); + return buildResponse(FaceIdAction.FAILED, errorMessage, trackingId, timestamp, null); + } + + // 5. Save recognition log + saveRecognitionLog(request, action, message, attendanceResponse, timestamp); + + // 6. Cache trackingId for idempotency + if (org.springframework.util.StringUtils.hasText(trackingId)) { + processedTrackingIds.put(trackingId, action); + } + + // 7. Build and return response + return buildResponse(true, action, message, trackingId, timestamp, attendanceResponse); + } + + private void saveRecognitionLog( + FaceIdAttendanceRequest request, + FaceIdAction action, + String message, + AttendanceResponse attendance, + Instant timestamp) { + + try { + String requestPayload = objectMapper.writeValueAsString(request); + String responsePayload = objectMapper.writeValueAsString( + FaceIdAttendanceResponse.builder() + .action(action) + .message(message) + .timestamp(timestamp) + .build() + ); + + FaceIdRecognition log = FaceIdRecognition.builder() + .studentCode(request.getStudentCode()) + .confidence(request.getConfidence()) + .cameraId(request.getCameraId()) + .imageId(request.getImageId()) + .trackingId(request.getTrackingId()) + .liveness(request.getLiveness()) + .action(action) + .message(message) + .requestPayload(requestPayload) + .responsePayload(responsePayload) + .capturedAt(request.getCapturedAt()) + .build(); + + faceIdRecognitionRepository.save(log); + + } catch (JsonProcessingException e) { + log.error("Failed to serialize request/response for log", e); + } + } + + private FaceIdAttendanceResponse buildResponse( + FaceIdAction action, + String message, + String trackingId, + Instant timestamp, + AttendanceResponse attendance) { + + return FaceIdAttendanceResponse.builder() + .success(action != FaceIdAction.FAILED) + .action(action) + .message(message) + .trackingId(trackingId) + .timestamp(timestamp) + .attendance(attendance) + .build(); + } + + private FaceIdAttendanceResponse buildResponse( + boolean success, + FaceIdAction action, + String message, + String trackingId, + Instant timestamp, + AttendanceResponse attendance) { + + return FaceIdAttendanceResponse.builder() + .success(success) + .action(action) + .message(message) + .trackingId(trackingId) + .timestamp(timestamp) + .attendance(attendance) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/attendenceSystem/module/log/dto/response/AuditLogResponse.java b/src/main/java/com/attendenceSystem/module/log/dto/response/AuditLogResponse.java index 6697f0bf705a87e21db7607349962c62d88c0568..3201a8007f56ebde24b6d96fdff7cb076737a03d 100644 --- a/src/main/java/com/attendenceSystem/module/log/dto/response/AuditLogResponse.java +++ b/src/main/java/com/attendenceSystem/module/log/dto/response/AuditLogResponse.java @@ -1,5 +1,7 @@ package com.attendenceSystem.module.log.dto.response; +import java.time.LocalDateTime; + import com.attendenceSystem.module.log.entity.enums.LogAction; import com.attendenceSystem.module.log.entity.enums.LogEntityType; @@ -14,5 +16,5 @@ public record AuditLogResponse( Long entityId, LogAction action, String description, - String createdAt) { + LocalDateTime createdAt) { } diff --git a/src/main/java/com/attendenceSystem/module/log/entity/AuditLog.java b/src/main/java/com/attendenceSystem/module/log/entity/AuditLog.java index ebf5e78fa22b768365814afe73119d6db1a30926..fb737864cc7ef10d0ef711d640d220288bbe80b6 100644 --- a/src/main/java/com/attendenceSystem/module/log/entity/AuditLog.java +++ b/src/main/java/com/attendenceSystem/module/log/entity/AuditLog.java @@ -1,6 +1,7 @@ package com.attendenceSystem.module.log.entity; import java.time.Instant; +import java.time.LocalDateTime; import org.hibernate.annotations.CreationTimestamp; @@ -50,7 +51,7 @@ public class AuditLog { @CreationTimestamp @Column(name = "created_at", updatable = false) - private Instant createdAt; + private LocalDateTime createdAt; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", updatable = false) diff --git a/src/main/java/com/attendenceSystem/module/log/mapper/response/AuditLogResponseMapper.java b/src/main/java/com/attendenceSystem/module/log/mapper/response/AuditLogResponseMapper.java index 201c87318773047269decf1b8a317c4c9f391cdb..9841378797bd7be62317093575874ce8cc08473d 100644 --- a/src/main/java/com/attendenceSystem/module/log/mapper/response/AuditLogResponseMapper.java +++ b/src/main/java/com/attendenceSystem/module/log/mapper/response/AuditLogResponseMapper.java @@ -4,7 +4,6 @@ import org.springframework.stereotype.Component; import com.attendenceSystem.module.log.dto.response.AuditLogResponse; import com.attendenceSystem.module.log.entity.AuditLog; -import com.attendenceSystem.util.TimeUtil; @Component public class AuditLogResponseMapper { @@ -17,7 +16,7 @@ public class AuditLogResponseMapper { .entityId(auditLog.getEntityId()) .action(auditLog.getAction()) .description(auditLog.getDescription()) - .createdAt(TimeUtil.formatInstant(auditLog.getCreatedAt(), null)) + .createdAt(auditLog.getCreatedAt()) .build(); } } diff --git a/src/main/java/com/attendenceSystem/module/otp/dto/response/OtpResponse.java b/src/main/java/com/attendenceSystem/module/otp/dto/response/OtpResponse.java index f817a2848adfd1031a3f97b8b3f1985261e7f8bf..bccf7b6c47383de39d1049dc623a1d3f334f2881 100644 --- a/src/main/java/com/attendenceSystem/module/otp/dto/response/OtpResponse.java +++ b/src/main/java/com/attendenceSystem/module/otp/dto/response/OtpResponse.java @@ -1,6 +1,6 @@ package com.attendenceSystem.module.otp.dto.response; -import java.time.Instant; +import java.time.LocalDateTime; import com.attendenceSystem.module.otp.entity.enums.OtpPurpose; @@ -8,8 +8,8 @@ public record OtpResponse( Long id, String destination, OtpPurpose purpose, - Instant expiredAt, - Instant createdAt, + LocalDateTime expiredAt, + LocalDateTime createdAt, boolean used ) { } diff --git a/src/main/java/com/attendenceSystem/module/otp/entity/Otp.java b/src/main/java/com/attendenceSystem/module/otp/entity/Otp.java index 77f5a4f7d740776be94da0b92f85d872a38911cf..9baedb0d75ca64d0a727c017360742be2c92ed5d 100644 --- a/src/main/java/com/attendenceSystem/module/otp/entity/Otp.java +++ b/src/main/java/com/attendenceSystem/module/otp/entity/Otp.java @@ -1,6 +1,7 @@ package com.attendenceSystem.module.otp.entity; import java.time.Instant; +import java.time.LocalDateTime; import com.attendenceSystem.module.otp.entity.enums.OtpPurpose; @@ -35,13 +36,13 @@ public class Otp { @Column(name = "purpose", nullable = false) private OtpPurpose purpose; @Column(name = "expired_at", nullable = false) - private Instant expiredAt; + private LocalDateTime expiredAt; @Column(name = "created_at", nullable = false) - private Instant createdAt; + private LocalDateTime createdAt; @Column(name = "used", nullable = false) private boolean used; public boolean isExpired() { - return Instant.now().isAfter(expiredAt); + return LocalDateTime.now().isAfter(expiredAt); } } diff --git a/src/main/java/com/attendenceSystem/module/otp/mapper/request/SendOtpRequestMapper.java b/src/main/java/com/attendenceSystem/module/otp/mapper/request/SendOtpRequestMapper.java index bbab50366ad6ae3beae0cc29a061c33891c5ef3e..d85ae2bbda310239b55235bd2d1a6d7bee906f18 100644 --- a/src/main/java/com/attendenceSystem/module/otp/mapper/request/SendOtpRequestMapper.java +++ b/src/main/java/com/attendenceSystem/module/otp/mapper/request/SendOtpRequestMapper.java @@ -1,7 +1,7 @@ package com.attendenceSystem.module.otp.mapper.request; import java.time.Instant; -import java.time.temporal.ChronoUnit; +import java.time.LocalDateTime; import com.attendenceSystem.module.otp.dto.request.SendOtpRequest; import com.attendenceSystem.module.otp.entity.Otp; @@ -15,12 +15,12 @@ public class SendOtpRequestMapper { private static final int OTP_EXPIRY_MINUTES = 5; public static Otp toEntity(SendOtpRequest request, String code) { - Instant now = Instant.now(); + LocalDateTime now = LocalDateTime.now(); return Otp.builder() .destination(request.getDestination()) .code(code) .purpose(request.getPurpose()) - .expiredAt(now.plus(OTP_EXPIRY_MINUTES, ChronoUnit.MINUTES)) + .expiredAt(now.plusMinutes(OTP_EXPIRY_MINUTES)) .createdAt(now) .used(false) .build(); diff --git a/src/main/java/com/attendenceSystem/module/report/controller/ReportController.java b/src/main/java/com/attendenceSystem/module/report/controller/ReportController.java index 9a1118b948bd67c86ce156a4ed4700696e0f50bf..4a0b06987b770e1c2796cf4f402c8ff22f274ca1 100644 --- a/src/main/java/com/attendenceSystem/module/report/controller/ReportController.java +++ b/src/main/java/com/attendenceSystem/module/report/controller/ReportController.java @@ -52,6 +52,7 @@ public class ReportController { request.setContent(content); request.setAttachmentUrl(link); request.setSharedUserIds(sharedUserIds); + request.setFiles(files); reportService.createReport(request); diff --git a/src/main/java/com/attendenceSystem/module/report/dto/request/CreateReportRequest.java b/src/main/java/com/attendenceSystem/module/report/dto/request/CreateReportRequest.java index c8c6ead17b61cc5805c0157739bb4b299919d5b1..e57dbcd2ee9a473c3f79ab472a2a13dcca3f3f30 100644 --- a/src/main/java/com/attendenceSystem/module/report/dto/request/CreateReportRequest.java +++ b/src/main/java/com/attendenceSystem/module/report/dto/request/CreateReportRequest.java @@ -6,6 +6,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.springframework.web.multipart.MultipartFile; @Getter @Setter @@ -24,4 +25,6 @@ public class CreateReportRequest { private String attachmentUrl; private Long[] sharedUserIds; + + private MultipartFile[] files; } diff --git a/src/main/java/com/attendenceSystem/module/report/dto/response/ReportDetailResponse.java b/src/main/java/com/attendenceSystem/module/report/dto/response/ReportDetailResponse.java index e601d0a7cce24b8cca0c1ffa5d765f34edbbe466..6e3b55e06559d757f0f3eb147f475e8534937705 100644 --- a/src/main/java/com/attendenceSystem/module/report/dto/response/ReportDetailResponse.java +++ b/src/main/java/com/attendenceSystem/module/report/dto/response/ReportDetailResponse.java @@ -1,6 +1,6 @@ package com.attendenceSystem.module.report.dto.response; -import java.time.Instant; +import java.time.LocalDateTime; import com.attendenceSystem.module.report.entity.enums.ReportStatus; @@ -9,9 +9,10 @@ public record ReportDetailResponse( String title, String content, String attachmentUrl, + String attachmentFiles, String employeeName, ReportStatus status, String rejectReason, - Instant createdAt, - Instant reviewedAt) { -} \ No newline at end of file + LocalDateTime createdAt, + LocalDateTime reviewedAt) { +} diff --git a/src/main/java/com/attendenceSystem/module/report/dto/response/ReportResponse.java b/src/main/java/com/attendenceSystem/module/report/dto/response/ReportResponse.java index e577392fb92fad84f0b0b1be38ccd544e301521a..5335b07388ffb426878b724df35049e668aff28f 100644 --- a/src/main/java/com/attendenceSystem/module/report/dto/response/ReportResponse.java +++ b/src/main/java/com/attendenceSystem/module/report/dto/response/ReportResponse.java @@ -1,6 +1,6 @@ package com.attendenceSystem.module.report.dto.response; -import java.time.Instant; +import java.time.LocalDateTime; import com.attendenceSystem.module.report.entity.enums.ReportStatus; @@ -10,5 +10,7 @@ public record ReportResponse( String content, String employeeName, ReportStatus status, - Instant createdAt) { -} \ No newline at end of file + LocalDateTime createdAt, + String attachmentUrl, + String attachmentFiles) { +} diff --git a/src/main/java/com/attendenceSystem/module/report/entity/Report.java b/src/main/java/com/attendenceSystem/module/report/entity/Report.java index 54cb772ac17a1353eff1e354343d348b8a379415..03bbd1520b74a20c69482f588de249aab190f9a9 100644 --- a/src/main/java/com/attendenceSystem/module/report/entity/Report.java +++ b/src/main/java/com/attendenceSystem/module/report/entity/Report.java @@ -1,6 +1,7 @@ package com.attendenceSystem.module.report.entity; import java.time.Instant; +import java.time.LocalDateTime; import com.attendenceSystem.module.report.entity.enums.ReportStatus; import com.attendenceSystem.module.user.entity.User; @@ -12,6 +13,7 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; +import jakarta.persistence.Lob; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; @@ -47,6 +49,10 @@ public class Report { @Column(name = "attachment_url") private String attachmentUrl; + @Lob + @Column(name = "attachment_files") + private String attachmentFiles; + @Column(name = "status", nullable = false) private ReportStatus status; @@ -54,8 +60,8 @@ public class Report { private String rejectReason; @Column(name = "created_at", nullable = false, updatable = false) - private Instant createdAt; + private LocalDateTime createdAt; @Column(name = "reviewed_at") - private Instant reviewedAt; + private LocalDateTime reviewedAt; } \ No newline at end of file diff --git a/src/main/java/com/attendenceSystem/module/report/mapper/response/ReportDetailResponseMapper.java b/src/main/java/com/attendenceSystem/module/report/mapper/response/ReportDetailResponseMapper.java index 5419b157a853fbc7d2c3a7b1ba763160009ae9c1..2551e664bec5637756f37f7d3255540723543018 100644 --- a/src/main/java/com/attendenceSystem/module/report/mapper/response/ReportDetailResponseMapper.java +++ b/src/main/java/com/attendenceSystem/module/report/mapper/response/ReportDetailResponseMapper.java @@ -15,6 +15,7 @@ public class ReportDetailResponseMapper { report.getTitle(), report.getContent(), report.getAttachmentUrl(), + report.getAttachmentFiles(), report.getEmployee().getFullName(), report.getStatus(), report.getRejectReason(), diff --git a/src/main/java/com/attendenceSystem/module/report/mapper/response/ReportResponseMapper.java b/src/main/java/com/attendenceSystem/module/report/mapper/response/ReportResponseMapper.java index f67688af52c2fd58d0a689b229b9de92531e715e..ca124375a7d9175bfab97b6b269a03d2153b9182 100644 --- a/src/main/java/com/attendenceSystem/module/report/mapper/response/ReportResponseMapper.java +++ b/src/main/java/com/attendenceSystem/module/report/mapper/response/ReportResponseMapper.java @@ -16,7 +16,9 @@ public class ReportResponseMapper { report.getContent(), report.getEmployee().getFullName(), report.getStatus(), - report.getCreatedAt() + report.getCreatedAt(), + report.getAttachmentUrl(), + report.getAttachmentFiles() ); } } \ No newline at end of file diff --git a/src/main/java/com/attendenceSystem/module/report/service/impl/ReportServiceImpl.java b/src/main/java/com/attendenceSystem/module/report/service/impl/ReportServiceImpl.java index 45a24fa6b6c186dc86ef4d8dda991f5140130d2a..639c3da83d53a89dd341c53839e384b641fadd83 100644 --- a/src/main/java/com/attendenceSystem/module/report/service/impl/ReportServiceImpl.java +++ b/src/main/java/com/attendenceSystem/module/report/service/impl/ReportServiceImpl.java @@ -1,8 +1,15 @@ package com.attendenceSystem.module.report.service.impl; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.time.Instant; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; +import java.util.UUID; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -24,24 +31,44 @@ import com.attendenceSystem.module.report.service.ReportService; import com.attendenceSystem.module.user.entity.User; import com.attendenceSystem.module.user.repository.UserRepository; import com.attendenceSystem.util.SecurityUtil; - import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.multipart.MultipartFile; @Service @RequiredArgsConstructor +@Slf4j public class ReportServiceImpl implements ReportService { private final ReportRepository reportRepository; private final ReportShareRepository reportShareRepository; private final UserRepository userRepository; + @Value("${app.storage.upload-dir:uploads}") + private String storageBaseDir; + @Transactional @Override public ReportResponse createReport(final CreateReportRequest request) { User employee = findCurrentUser(); Report report = CreateReportRequestMapper.toEntity(request, employee); - report.setCreatedAt(Instant.now()); + report.setCreatedAt(LocalDateTime.now()); + + // Xử lý upload files + if (request.getFiles() != null && request.getFiles().length > 0) { + List filePaths = new ArrayList<>(); + for (MultipartFile file : request.getFiles()) { + if (!file.isEmpty()) { + String filePath = saveFile(file); + filePaths.add(filePath); + } + } + if (!filePaths.isEmpty()) { + report.setAttachmentFiles(String.join(",", filePaths)); + } + } Report savedReport = reportRepository.save(report); @@ -63,6 +90,32 @@ public class ReportServiceImpl implements ReportService { return ReportResponseMapper.fromEntity(savedReport); } + private String saveFile(MultipartFile file) { + try { + // Tạo thư mục nếu chưa tồn tại + String reportUploadDir = storageBaseDir + "/reports"; + Path uploadPath = Paths.get(reportUploadDir); + if (!Files.exists(uploadPath)) { + Files.createDirectories(uploadPath); + } + + // Tạo tên file unique + String originalFilename = file.getOriginalFilename(); + String extension = originalFilename.substring(originalFilename.lastIndexOf(".")); + String uniqueFilename = UUID.randomUUID().toString() + extension; + + // Lưu file + Path filePath = uploadPath.resolve(uniqueFilename); + file.transferTo(filePath.toFile()); + + // Trả về relative path + return reportUploadDir + "/" + uniqueFilename; + } catch (IOException e) { + log.error("Failed to save file", e); + throw new RuntimeException("Không thể lưu file: " + e.getMessage()); + } + } + @Override public Page getAllReports(final Pageable pageable) { return reportRepository.findAllByOrderByCreatedAtDesc(pageable) diff --git a/src/main/java/com/attendenceSystem/module/user/controller/AccountController.java b/src/main/java/com/attendenceSystem/module/user/controller/AccountController.java index cdf8d902f9b5a2122a90e04058df36289b0bbcc4..19d096c07cf56004419c2677d4ccc77be1dc9573 100644 --- a/src/main/java/com/attendenceSystem/module/user/controller/AccountController.java +++ b/src/main/java/com/attendenceSystem/module/user/controller/AccountController.java @@ -5,8 +5,6 @@ import org.springframework.data.web.PageableDefault; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; - import com.attendenceSystem.constant.Routes; import com.attendenceSystem.constant.Views; import com.attendenceSystem.module.user.dto.response.UserResponse; diff --git a/src/main/java/com/attendenceSystem/module/user/dto/request/UpdateUserInformationRequest.java b/src/main/java/com/attendenceSystem/module/user/dto/request/UpdateUserInformationRequest.java index 9e56a39345bced4d8b25c2c11f7bb9b359cb53d3..7625ae2cb2cca09779f0a6fe276d24bc326510a9 100644 --- a/src/main/java/com/attendenceSystem/module/user/dto/request/UpdateUserInformationRequest.java +++ b/src/main/java/com/attendenceSystem/module/user/dto/request/UpdateUserInformationRequest.java @@ -3,7 +3,6 @@ package com.attendenceSystem.module.user.dto.request; import com.attendenceSystem.module.user.entity.enums.Specialization; import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/src/main/java/com/attendenceSystem/module/user/entity/User.java b/src/main/java/com/attendenceSystem/module/user/entity/User.java index a7d3272f2514cba824c63f8d2c1e5a0625893514..2dbf9f2c2471449f37e083a6130a27b91ebe6671 100644 --- a/src/main/java/com/attendenceSystem/module/user/entity/User.java +++ b/src/main/java/com/attendenceSystem/module/user/entity/User.java @@ -1,6 +1,7 @@ package com.attendenceSystem.module.user.entity; import java.time.Instant; +import java.time.LocalDateTime; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; @@ -56,10 +57,10 @@ public class User { private Department department; @CreationTimestamp @Column(name = "created_at", nullable = false, updatable = false) - private Instant createdAt; + private LocalDateTime createdAt; @UpdateTimestamp @Column(name = "updated_at", nullable = false) - private Instant updatedAt; + private LocalDateTime updatedAt; @Column(name = "must_change_password", nullable = false) @Builder.Default private boolean mustChangePassword = false; diff --git a/src/main/java/com/attendenceSystem/module/user/service/impl/UserServiceImpl.java b/src/main/java/com/attendenceSystem/module/user/service/impl/UserServiceImpl.java index befd27bed2c934e3614b6647fabd447e9d88e30e..8b4aa7558ccf0b56e71aa9dbdff2a74f34b9f2f2 100644 --- a/src/main/java/com/attendenceSystem/module/user/service/impl/UserServiceImpl.java +++ b/src/main/java/com/attendenceSystem/module/user/service/impl/UserServiceImpl.java @@ -1,9 +1,5 @@ package com.attendenceSystem.module.user.service.impl; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 84a5d2459f474988705dc68f31e39a9f492e1fb1..de59eafafecd338927d0740a330a1ce897a91dc1 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -59,4 +59,8 @@ app.storage.upload-dir=uploads # server.error.include-stacktrace=never # server.error.include-exception=false -app.timezone=Asia/Ho_Chi_Minh \ No newline at end of file +app.timezone=Asia/Ho_Chi_Minh + +# Face ID Configuration +face-id.api-key=${FACE_ID_API_KEY:dev-secret-key-change-in-production} +face-id.confidence-threshold=0.90 diff --git a/src/main/resources/templates/cms/dashboard/dashboard-employee.html b/src/main/resources/templates/cms/dashboard/dashboard-employee.html index adca5a20bde6f9dde20fc4f56683c8ffa65d2ca0..6c9ee2f323fc500650d33835ad6b635263264f20 100644 --- a/src/main/resources/templates/cms/dashboard/dashboard-employee.html +++ b/src/main/resources/templates/cms/dashboard/dashboard-employee.html @@ -42,31 +42,6 @@ - -
-
- -
- -
-

10

-

Báo cáo được duyệt

-
- V -
- -
-
- -
- -
-

1

-

Báo cáo bị từ chối

-
- X -
-
@@ -241,17 +216,17 @@
- haivuive266@gmail.com +
- 0987654321 +
- IT Department +
diff --git a/src/main/resources/templates/cms/document/document-detail.html b/src/main/resources/templates/cms/document/document-detail.html index bd6cd7f6cb1f375c08c038f253ed7d9a6216c269..a69e3c89789462eb5c952656f95aa2f82273843e 100644 --- a/src/main/resources/templates/cms/document/document-detail.html +++ b/src/main/resources/templates/cms/document/document-detail.html @@ -21,6 +21,16 @@

+
+ File đính kèm: +
+
+ + +
-
\ No newline at end of file + diff --git a/src/main/resources/templates/cms/document/document-list.html b/src/main/resources/templates/cms/document/document-list.html index cc8144c0084b3fee70b9b0361ee5bddb93264b3b..0ab51b512e05fa7e68e294b8dd6d306f559705cb 100644 --- a/src/main/resources/templates/cms/document/document-list.html +++ b/src/main/resources/templates/cms/document/document-list.html @@ -35,8 +35,6 @@ Ngày nộp File đính kèm Link tài liệu - Trạng thái - Thao tác @@ -108,18 +106,29 @@ const employeeName = report.employeeName || 'N/A'; const attachmentUrl = report.attachmentUrl || ''; + const attachmentFiles = report.attachmentFiles || ''; + + // Hiển thị files đính kèm + let filesHtml = 'Không có'; + if (attachmentFiles) { + const filePaths = attachmentFiles.split(','); + filesHtml = filePaths.map(path => { + const filename = path.split('/').pop(); + return '📎 ' + filename + ''; + }).join('
'); + } - const statusClass = 'status-' + report.status.toLowerCase(); - const statusText = getStatusText(report.status); + // Hiển thị link + const linkHtml = attachmentUrl ? + '🔗 Mở link' : + 'Không có'; row.innerHTML = '' + employeeName + '' + '' + report.title + '' + '' + formattedDate + '' + - '' + (attachmentUrl ? '📎 Tải file' : 'Không có') + '' + - '' + (attachmentUrl ? '🔗 Mở link' : 'Không có') + '' + - '' + statusText + '' + - '
'; + '' + filesHtml + '' + + '' + linkHtml + '' + tableBody.appendChild(row); }); @@ -132,13 +141,6 @@ tableBody.innerHTML = 'Lỗi khi tải danh sách báo cáo'; }); }); - - function getStatusText(status) { - if (status === 'PENDING') return 'Chờ duyệt'; - if (status === 'APPROVED') return 'Đã duyệt'; - if (status === 'REJECTED') return 'Từ chối'; - return status; - } \ No newline at end of file diff --git a/uploads/face_samples/4/face_4_1.jpg b/uploads/face_samples/4/face_4_1.jpg index a5a80f9cab8b92c6ec1ee5034ac40cc05ee2c46b..acb9181e58eee29d35d62f36c9d5ed32ccb838ca 100644 Binary files a/uploads/face_samples/4/face_4_1.jpg and b/uploads/face_samples/4/face_4_1.jpg differ diff --git a/uploads/face_samples/4/face_4_2.jpg b/uploads/face_samples/4/face_4_2.jpg index e7f10bf74b2df4edae6e3d5056b6abd891942690..acb9181e58eee29d35d62f36c9d5ed32ccb838ca 100644 Binary files a/uploads/face_samples/4/face_4_2.jpg and b/uploads/face_samples/4/face_4_2.jpg differ diff --git a/uploads/face_samples/4/face_4_3.jpg b/uploads/face_samples/4/face_4_3.jpg index cca8bc3a3103f6aea37bce1b713538f6d64bbc87..76a8ce6f80fc3c56f67622b2c08b47bebf7dcc76 100644 Binary files a/uploads/face_samples/4/face_4_3.jpg and b/uploads/face_samples/4/face_4_3.jpg differ diff --git a/uploads/face_samples/4/face_4_4.jpg b/uploads/face_samples/4/face_4_4.jpg index a123445725133057f371f3766a13eb21ff277a8c..f2311960e09bd38038c54d10242626881e96c6bc 100644 Binary files a/uploads/face_samples/4/face_4_4.jpg and b/uploads/face_samples/4/face_4_4.jpg differ diff --git a/uploads/face_samples/4/face_4_5.jpg b/uploads/face_samples/4/face_4_5.jpg index b5823040d5c29077374c1369ad83a28249b900d4..8413b3d07433129b60c382509ad8a61441d46fc4 100644 Binary files a/uploads/face_samples/4/face_4_5.jpg and b/uploads/face_samples/4/face_4_5.jpg differ