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