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

Continue report and user changes

Browse files
Files changed (44) hide show
  1. cookie.txt +5 -0
  2. response.txt +202 -0
  3. spring-boot.log +0 -0
  4. src/main/java/com/attendenceSystem/constant/Routes.java +8 -9
  5. src/main/java/com/attendenceSystem/constant/Views.java +5 -0
  6. src/main/java/com/attendenceSystem/module/attendance/dto/response/LeaveRequestResponse.java +1 -0
  7. src/main/java/com/attendenceSystem/module/attendance/mapper/response/LeaveRequestResponseMapper.java +3 -0
  8. src/main/java/com/attendenceSystem/module/report/api/ReportApiController.java +63 -0
  9. src/main/java/com/attendenceSystem/module/report/controller/ReportController.java +65 -0
  10. src/main/java/com/attendenceSystem/module/report/dto/request/CreateReportRequest.java +3 -1
  11. src/main/java/com/attendenceSystem/module/report/entity/ReportShare.java +39 -0
  12. src/main/java/com/attendenceSystem/module/report/repository/ReportShareRepository.java +18 -0
  13. src/main/java/com/attendenceSystem/module/report/service/ReportService.java +3 -1
  14. src/main/java/com/attendenceSystem/module/report/service/impl/ReportServiceImpl.java +29 -1
  15. src/main/java/com/attendenceSystem/module/user/controller/AccountController.java +24 -8
  16. src/main/java/com/attendenceSystem/module/user/controller/UserController.java +1 -1
  17. src/main/java/com/attendenceSystem/module/user/dto/request/UpdateUserInformationRequest.java +2 -2
  18. src/main/java/com/attendenceSystem/module/user/dto/response/UserInformationResponse.java +2 -0
  19. src/main/java/com/attendenceSystem/module/user/dto/response/UserResponse.java +12 -9
  20. src/main/java/com/attendenceSystem/module/user/entity/User.java +6 -2
  21. src/main/java/com/attendenceSystem/module/user/entity/converter/DepartmentConverter.java +13 -6
  22. src/main/java/com/attendenceSystem/module/user/entity/converter/SpecializationConverter.java +21 -0
  23. src/main/java/com/attendenceSystem/module/user/entity/enums/Department.java +7 -13
  24. src/main/java/com/attendenceSystem/module/user/entity/enums/Specialization.java +25 -0
  25. src/main/java/com/attendenceSystem/module/user/mapper/request/RegisterRequestMapper.java +2 -2
  26. src/main/java/com/attendenceSystem/module/user/mapper/response/UserInformationResponseMapper.java +1 -0
  27. src/main/java/com/attendenceSystem/module/user/mapper/response/UserResponseMapper.java +3 -0
  28. src/main/java/com/attendenceSystem/module/user/repository/UserRepository.java +4 -0
  29. src/main/java/com/attendenceSystem/module/user/service/AccountService.java +4 -1
  30. src/main/java/com/attendenceSystem/module/user/service/impl/AccountServiceImpl.java +39 -0
  31. src/main/java/com/attendenceSystem/module/user/service/impl/UserServiceImpl.java +2 -2
  32. src/main/java/com/attendenceSystem/security/CustomUserDetails.java +2 -2
  33. src/main/resources/static/css/account/employee-detail.css +195 -0
  34. src/main/resources/static/css/document/document-create.css +69 -1
  35. src/main/resources/templates/cms/account/employee-detail.html +127 -0
  36. src/main/resources/templates/cms/account/employee-list.html +16 -72
  37. src/main/resources/templates/cms/dashboard/dashboard-employee.html +1 -1
  38. src/main/resources/templates/cms/document/document-create.html +37 -16
  39. src/main/resources/templates/cms/document/document-list.html +74 -75
  40. src/main/resources/templates/cms/report/report-day.html +0 -144
  41. src/main/resources/templates/cms/report/report-month.html +0 -184
  42. src/main/resources/templates/cms/sidebar/sidebar-employee.html +3 -3
  43. src/main/resources/templates/cms/sidebar/sidebar-manage.html +1 -1
  44. src/main/resources/templates/cms/user/user-information.html +7 -7
cookie.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # Netscape HTTP Cookie File
2
+ # https://curl.se/docs/http-cookies.html
3
+ # This file was generated by libcurl! Edit at your own risk.
4
+
5
+ #HttpOnly_localhost FALSE / FALSE 0 JSESSIONID 304BEE582413C60E27EA083347D13FC8
response.txt ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html>
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Nộp báo cáo công việc</title>
8
+
9
+ <link rel="stylesheet" href="/css/document/document-create.css">
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
11
+ </head>
12
+
13
+ <body>
14
+
15
+ <div class="bg-image"></div>
16
+
17
+ <div class="layout">
18
+
19
+ <!-- SIDEBAR -->
20
+
21
+
22
+
23
+
24
+
25
+
26
+ <!-- CONTENT -->
27
+ <div class="container-layout">
28
+
29
+ <!-- HEADER -->
30
+ <div class="container-header">
31
+ <h1>Nộp báo cáo công việc</h1>
32
+ <p>Gửi báo cáo và chia sẻ cho phòng ban</p>
33
+ </div>
34
+
35
+ <main class="main-container">
36
+
37
+ <!-- FORM CARD -->
38
+ <section class="dashboard-card">
39
+
40
+ <div class="card-header">
41
+ <h2>Gửi báo cáo</h2>
42
+ </div>
43
+
44
+ <div class="card-body">
45
+
46
+ <form method="post" action="/report/create" enctype="multipart/form-data">
47
+
48
+ <!-- Tiêu đề -->
49
+ <div class="form-group">
50
+ <label>Tiêu đề báo cáo</label>
51
+ <input type="text" name="title" placeholder="Nhập tiêu đề báo cáo..." required>
52
+ </div>
53
+
54
+ <!-- Nội dung -->
55
+ <div class="form-group">
56
+ <label>Nội dung báo cáo</label>
57
+ <textarea name="content" rows="5" placeholder="Nhập nội dung báo cáo..."
58
+ required></textarea>
59
+ </div>
60
+
61
+ <!-- Phòng ban -->
62
+ <div class="form-group">
63
+ <label>Phòng ban</label>
64
+
65
+ <select name="departmentId" id="departmentSelect" required>
66
+ <option value="" disabled selected>Chọn phòng ban</option>
67
+
68
+ <option value="0">UNASSIGNED</option>
69
+
70
+ <option value="1">SE</option>
71
+ </select>
72
+ </div>
73
+
74
+ <!-- Người được xem -->
75
+ <div class="form-group">
76
+ <label>Người được chia sẻ</label>
77
+
78
+ <div id="userCheckboxes" class="checkbox-container">
79
+ <p class="placeholder-text">Chọn phòng ban trước</p>
80
+ </div>
81
+ </div>
82
+
83
+ <!-- Upload file -->
84
+ <div class="form-group">
85
+ <label>Tệp đính kèm</label>
86
+
87
+ <input type="file" name="files" multiple>
88
+ </div>
89
+
90
+ <!-- Link -->
91
+ <div class="form-group">
92
+ <label>Hoặc liên kết</label>
93
+
94
+ <input type="url" name="link" placeholder="https://drive.google.com/...">
95
+ </div>
96
+
97
+ <!-- BUTTON -->
98
+ <div class="form-actions">
99
+
100
+ <button type="submit" class="btn-primary">
101
+ <i class="fa-regular fa-paper-plane"></i>
102
+ Gửi báo cáo
103
+ </button>
104
+
105
+ <button type="reset" class="btn-secondary">
106
+ Reset
107
+ </button>
108
+
109
+ </div>
110
+
111
+ </form>
112
+
113
+ </div>
114
+
115
+ </section>
116
+
117
+ </main>
118
+ <footer>
119
+ <div class="footer-layout">
120
+
121
+ <div class="footer-left">
122
+ <h2 class="left-title">DXLab Attendance</h2>
123
+
124
+ <div class="left-description">
125
+ Hệ thống điểm danh thông minh bằng nhận diện khuôn mặt,
126
+ hỗ trợ quản lý sinh viên và giảng viên hiệu quả.
127
+ </div>
128
+ </div>
129
+
130
+ <div class="footer-right">
131
+ <div class="right-title">
132
+ Thông tin liên hệ
133
+ </div>
134
+
135
+ <a href="#" class="right-description">
136
+ Liên hệ với chúng tôi
137
+ </a>
138
+
139
+ <a href="#" class="right-description">
140
+ Báo cáo lỗi
141
+ </a>
142
+
143
+ <a href="#" class="right-description">
144
+ Điều khoản sử dụng
145
+ </a>
146
+ </div>
147
+
148
+ </div>
149
+
150
+ <div class="footer-bottom">
151
+ © 2026 DXLab Attendance. All rights reserved.
152
+ </div>
153
+ </footer>
154
+
155
+ </div>
156
+ </div>
157
+
158
+ <!-- FOOTER -->
159
+
160
+ <!-- JS load user theo phòng -->
161
+ <script>
162
+ document.getElementById("departmentSelect").addEventListener("change", function () {
163
+
164
+ const departmentId = this.value;
165
+
166
+ fetch(`/api/report/users/by-department/${departmentId}`)
167
+ .then(res => res.json())
168
+ .then(data => {
169
+
170
+ const container = document.getElementById("userCheckboxes");
171
+ container.innerHTML = "";
172
+
173
+ if (data.length === 0) {
174
+ container.innerHTML = "<p class='placeholder-text'>Không có người dùng trong phòng ban này</p>";
175
+ return;
176
+ }
177
+
178
+ data.forEach(user => {
179
+ const label = document.createElement("label");
180
+ label.className = "checkbox-label";
181
+
182
+ const checkbox = document.createElement("input");
183
+ checkbox.type = "checkbox";
184
+ checkbox.name = "sharedUserIds";
185
+ checkbox.value = user.id;
186
+
187
+ const span = document.createElement("span");
188
+ span.textContent = user.fullName;
189
+
190
+ label.appendChild(checkbox);
191
+ label.appendChild(span);
192
+ container.appendChild(label);
193
+ });
194
+
195
+ });
196
+
197
+ });
198
+ </script>
199
+
200
+ </body>
201
+
202
+ </html>
spring-boot.log ADDED
The diff for this file is too large to render. See raw diff
 
src/main/java/com/attendenceSystem/constant/Routes.java CHANGED
@@ -32,18 +32,13 @@ public final class Routes {
32
  public static final String PROFILE = "/profile";
33
  public static final String CHANGE_PASSWORD = "/change-password";
34
  public static final String UPDATE_PASSWORD = "/update-password";
35
- public static final String UPDATE_INFORMATION = "/update-information";
36
  }
37
 
38
  public static final class Account {
39
- public static final String ROOT = "/admin/accounts";
 
40
  public static final String MANAGER = "/manager/accounts";
41
- public static final String CREATE = "/create";
42
- public static final String UPDATE = "/update";
43
- public static final String DELETE = "/delete";
44
- public static final String DEACTIVATE = "/deactivate";
45
- public static final String ACTIVATE = "/activate";
46
- public static final String UPDATE_INFORMATION = "/update-information";
47
  }
48
 
49
  public static final class Dashboard {
@@ -68,4 +63,8 @@ public final class Routes {
68
  public static final class Schedule {
69
  public static final String ROOT = "/schedule";
70
  }
71
- }
 
 
 
 
 
32
  public static final String PROFILE = "/profile";
33
  public static final String CHANGE_PASSWORD = "/change-password";
34
  public static final String UPDATE_PASSWORD = "/update-password";
35
+ public static final String INFORMATION = "/information";
36
  }
37
 
38
  public static final class Account {
39
+ public static final String ROOT = "/accounts";
40
+ public static final String ADMIN = "/admin/accounts";
41
  public static final String MANAGER = "/manager/accounts";
 
 
 
 
 
 
42
  }
43
 
44
  public static final class Dashboard {
 
63
  public static final class Schedule {
64
  public static final String ROOT = "/schedule";
65
  }
66
+
67
+ public static final class Report {
68
+ public static final String ROOT = "/report";
69
+ }
70
+ }
src/main/java/com/attendenceSystem/constant/Views.java CHANGED
@@ -22,6 +22,7 @@ public final class Views {
22
  public static final class Account {
23
  public static final String LIST = "cms/account/account-list";
24
  public static final String EMPLOYEE_LIST = "cms/account/employee-list";
 
25
  public static final String DETAIL = "cms/account/account-detail";
26
  }
27
 
@@ -43,4 +44,8 @@ public final class Views {
43
  public static final String CREATE = "cms/face-id/faceID-create";
44
  public static final String LIST = "cms/face-id/faceID-list";
45
  }
 
 
 
 
46
  }
 
22
  public static final class Account {
23
  public static final String LIST = "cms/account/account-list";
24
  public static final String EMPLOYEE_LIST = "cms/account/employee-list";
25
+ public static final String EMPLOYEE_DETAIL = "cms/account/employee-detail";
26
  public static final String DETAIL = "cms/account/account-detail";
27
  }
28
 
 
44
  public static final String CREATE = "cms/face-id/faceID-create";
45
  public static final String LIST = "cms/face-id/faceID-list";
46
  }
47
+ public static final class Document {
48
+ public static final String CREATE = "cms/document/document-create";
49
+ public static final String LIST = "cms/document/document-list";
50
+ }
51
  }
src/main/java/com/attendenceSystem/module/attendance/dto/response/LeaveRequestResponse.java CHANGED
@@ -8,6 +8,7 @@ import com.attendenceSystem.module.attendance.entity.enums.LeaveStatus;
8
  public record LeaveRequestResponse(
9
  Long id,
10
  String fullName,
 
11
  LocalDate startDate,
12
  LocalDate endDate,
13
  String reason,
 
8
  public record LeaveRequestResponse(
9
  Long id,
10
  String fullName,
11
+ String departmentDisplay,
12
  LocalDate startDate,
13
  LocalDate endDate,
14
  String reason,
src/main/java/com/attendenceSystem/module/attendance/mapper/response/LeaveRequestResponseMapper.java CHANGED
@@ -15,6 +15,9 @@ public class LeaveRequestResponseMapper {
15
  return new LeaveRequestResponse(
16
  leave.getId(),
17
  leave.getUser() == null ? null : leave.getUser().getFullName(),
 
 
 
18
  leave.getStartDate(),
19
  leave.getEndDate(),
20
  leave.getReason(),
 
15
  return new LeaveRequestResponse(
16
  leave.getId(),
17
  leave.getUser() == null ? null : leave.getUser().getFullName(),
18
+ leave.getUser() == null || leave.getUser().getDepartment() == null
19
+ ? "N/A"
20
+ : leave.getUser().getDepartment().getDisplayName(),
21
  leave.getStartDate(),
22
  leave.getEndDate(),
23
  leave.getReason(),
src/main/java/com/attendenceSystem/module/report/api/ReportApiController.java ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.report.api;
2
+
3
+ import org.springframework.data.domain.Page;
4
+ import org.springframework.data.domain.Pageable;
5
+ import org.springframework.http.ResponseEntity;
6
+ import org.springframework.web.bind.annotation.GetMapping;
7
+ import org.springframework.web.bind.annotation.PathVariable;
8
+ import org.springframework.web.bind.annotation.RequestMapping;
9
+ import org.springframework.web.bind.annotation.RestController;
10
+
11
+ import com.attendenceSystem.constant.Routes;
12
+ import com.attendenceSystem.module.report.dto.response.ReportDetailResponse;
13
+ import com.attendenceSystem.module.report.dto.response.ReportResponse;
14
+ import com.attendenceSystem.module.report.service.ReportService;
15
+ import com.attendenceSystem.module.user.entity.User;
16
+ import com.attendenceSystem.module.user.entity.enums.Specialization;
17
+ import com.attendenceSystem.module.user.entity.enums.Role;
18
+ import com.attendenceSystem.module.user.repository.UserRepository;
19
+
20
+ import lombok.RequiredArgsConstructor;
21
+
22
+ import java.util.List;
23
+ import java.util.stream.Collectors;
24
+
25
+ @RestController
26
+ @RequestMapping(Routes.API + Routes.Report.ROOT)
27
+ @RequiredArgsConstructor
28
+ public class ReportApiController {
29
+
30
+ private final ReportService reportService;
31
+ private final UserRepository userRepository;
32
+
33
+ @GetMapping
34
+ public ResponseEntity<Page<ReportResponse>> getAllReports(Pageable pageable) {
35
+ Page<ReportResponse> reports = reportService.getAllReports(pageable);
36
+ return ResponseEntity.ok(reports);
37
+ }
38
+
39
+ @GetMapping("/my-reports")
40
+ public ResponseEntity<Page<ReportResponse>> getMyReports(Pageable pageable) {
41
+ Page<ReportResponse> reports = reportService.getMyReports(pageable);
42
+ return ResponseEntity.ok(reports);
43
+ }
44
+
45
+ @GetMapping("/shared-with-me")
46
+ public ResponseEntity<Page<ReportResponse>> getSharedWithMe(Pageable pageable) {
47
+ Page<ReportResponse> reports = reportService.getSharedWithMe(pageable);
48
+ return ResponseEntity.ok(reports);
49
+ }
50
+
51
+ @GetMapping("/{id}")
52
+ public ResponseEntity<ReportDetailResponse> getReportById(@PathVariable Long id) {
53
+ ReportDetailResponse report = reportService.getReportById(id);
54
+ return ResponseEntity.ok(report);
55
+ }
56
+
57
+ @GetMapping("/users/by-department/{departmentId}")
58
+ public ResponseEntity<List<User>> getUsersByDepartment(@PathVariable Integer departmentId) {
59
+ Specialization specialization = Specialization.fromValue(departmentId);
60
+ List<User> users = userRepository.findBySpecializationAndRoleNot(specialization, Role.ADMIN);
61
+ return ResponseEntity.ok(users);
62
+ }
63
+ }
src/main/java/com/attendenceSystem/module/report/controller/ReportController.java ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.report.controller;
2
+
3
+ import jakarta.servlet.http.HttpSession;
4
+ import org.springframework.stereotype.Controller;
5
+ import org.springframework.web.bind.annotation.GetMapping;
6
+ import org.springframework.web.bind.annotation.PostMapping;
7
+ import org.springframework.web.bind.annotation.RequestMapping;
8
+ import org.springframework.web.bind.annotation.RequestParam;
9
+ import org.springframework.web.multipart.MultipartFile;
10
+
11
+ import com.attendenceSystem.constant.Routes;
12
+ import com.attendenceSystem.constant.Views;
13
+ import com.attendenceSystem.module.report.dto.request.CreateReportRequest;
14
+ import com.attendenceSystem.module.report.service.ReportService;
15
+ import com.attendenceSystem.module.user.entity.enums.Specialization;
16
+
17
+ import lombok.RequiredArgsConstructor;
18
+ import org.springframework.ui.Model;
19
+
20
+ @Controller
21
+ @RequestMapping(Routes.Report.ROOT)
22
+ @RequiredArgsConstructor
23
+ public class ReportController {
24
+
25
+ private final ReportService reportService;
26
+
27
+ @GetMapping
28
+ public String reportList() {
29
+ return Views.Document.LIST;
30
+ }
31
+
32
+ @GetMapping(Routes.Action.CREATE)
33
+ public String reportCreate(Model model) {
34
+ model.addAttribute("departments", Specialization.values());
35
+ return Views.Document.CREATE;
36
+ }
37
+
38
+ @PostMapping(Routes.Action.CREATE)
39
+ public String submitReport(
40
+ @RequestParam("title") String title,
41
+ @RequestParam("content") String content,
42
+ @RequestParam("departmentId") Integer departmentId,
43
+ @RequestParam(value = "sharedUserIds", required = false) Long[] sharedUserIds,
44
+ @RequestParam(value = "files", required = false) MultipartFile[] files,
45
+ @RequestParam(value = "link", required = false) String link,
46
+ HttpSession session,
47
+ Model model) {
48
+
49
+ try {
50
+ CreateReportRequest request = new CreateReportRequest();
51
+ request.setTitle(title);
52
+ request.setContent(content);
53
+ request.setAttachmentUrl(link);
54
+ request.setSharedUserIds(sharedUserIds);
55
+
56
+ reportService.createReport(request);
57
+
58
+ return Routes.REDIRECT + Routes.Report.ROOT + "?success=true";
59
+ } catch (Exception e) {
60
+ model.addAttribute("errorMessage", e.getMessage());
61
+ model.addAttribute("departments", Specialization.values());
62
+ return Views.Document.CREATE;
63
+ }
64
+ }
65
+ }
src/main/java/com/attendenceSystem/module/report/dto/request/CreateReportRequest.java CHANGED
@@ -22,4 +22,6 @@ public class CreateReportRequest {
22
  private String content;
23
 
24
  private String attachmentUrl;
25
- }
 
 
 
22
  private String content;
23
 
24
  private String attachmentUrl;
25
+
26
+ private Long[] sharedUserIds;
27
+ }
src/main/java/com/attendenceSystem/module/report/entity/ReportShare.java ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.report.entity;
2
+
3
+ import com.attendenceSystem.module.user.entity.User;
4
+
5
+ import jakarta.persistence.Entity;
6
+ import jakarta.persistence.FetchType;
7
+ import jakarta.persistence.GeneratedValue;
8
+ import jakarta.persistence.GenerationType;
9
+ import jakarta.persistence.Id;
10
+ import jakarta.persistence.JoinColumn;
11
+ import jakarta.persistence.ManyToOne;
12
+ import jakarta.persistence.Table;
13
+ import lombok.AllArgsConstructor;
14
+ import lombok.Builder;
15
+ import lombok.Getter;
16
+ import lombok.NoArgsConstructor;
17
+ import lombok.Setter;
18
+
19
+ @Getter
20
+ @Setter
21
+ @NoArgsConstructor
22
+ @AllArgsConstructor
23
+ @Builder
24
+ @Entity
25
+ @Table(name = "report_shares")
26
+ public class ReportShare {
27
+
28
+ @Id
29
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
30
+ private Long id;
31
+
32
+ @ManyToOne(fetch = FetchType.LAZY)
33
+ @JoinColumn(name = "report_id", nullable = false)
34
+ private Report report;
35
+
36
+ @ManyToOne(fetch = FetchType.LAZY)
37
+ @JoinColumn(name = "user_id", nullable = false)
38
+ private User user;
39
+ }
src/main/java/com/attendenceSystem/module/report/repository/ReportShareRepository.java ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.report.repository;
2
+
3
+ import org.springframework.data.domain.Page;
4
+ import org.springframework.data.domain.Pageable;
5
+ import org.springframework.data.jpa.repository.JpaRepository;
6
+ import org.springframework.data.jpa.repository.Query;
7
+ import org.springframework.stereotype.Repository;
8
+
9
+ import com.attendenceSystem.module.report.entity.Report;
10
+ import com.attendenceSystem.module.report.entity.ReportShare;
11
+ import com.attendenceSystem.module.user.entity.User;
12
+
13
+ @Repository
14
+ public interface ReportShareRepository extends JpaRepository<ReportShare, Long> {
15
+
16
+ @Query("SELECT rs.report FROM ReportShare rs WHERE rs.user = :user ORDER BY rs.report.createdAt DESC")
17
+ Page<Report> findReportsSharedWithUser(User user, Pageable pageable);
18
+ }
src/main/java/com/attendenceSystem/module/report/service/ReportService.java CHANGED
@@ -15,5 +15,7 @@ public interface ReportService {
15
 
16
  Page<ReportResponse> getMyReports(Pageable pageable);
17
 
 
 
18
  ReportDetailResponse getReportById(Long id);
19
- }
 
15
 
16
  Page<ReportResponse> getMyReports(Pageable pageable);
17
 
18
+ Page<ReportResponse> getSharedWithMe(Pageable pageable);
19
+
20
  ReportDetailResponse getReportById(Long id);
21
+ }
src/main/java/com/attendenceSystem/module/report/service/impl/ReportServiceImpl.java CHANGED
@@ -1,6 +1,8 @@
1
  package com.attendenceSystem.module.report.service.impl;
2
 
3
  import java.time.Instant;
 
 
4
 
5
  import org.springframework.data.domain.Page;
6
  import org.springframework.data.domain.Pageable;
@@ -12,10 +14,12 @@ import com.attendenceSystem.module.report.dto.request.CreateReportRequest;
12
  import com.attendenceSystem.module.report.dto.response.ReportDetailResponse;
13
  import com.attendenceSystem.module.report.dto.response.ReportResponse;
14
  import com.attendenceSystem.module.report.entity.Report;
 
15
  import com.attendenceSystem.module.report.mapper.request.CreateReportRequestMapper;
16
  import com.attendenceSystem.module.report.mapper.response.ReportDetailResponseMapper;
17
  import com.attendenceSystem.module.report.mapper.response.ReportResponseMapper;
18
  import com.attendenceSystem.module.report.repository.ReportRepository;
 
19
  import com.attendenceSystem.module.report.service.ReportService;
20
  import com.attendenceSystem.module.user.entity.User;
21
  import com.attendenceSystem.module.user.repository.UserRepository;
@@ -28,6 +32,7 @@ import lombok.RequiredArgsConstructor;
28
  public class ReportServiceImpl implements ReportService {
29
 
30
  private final ReportRepository reportRepository;
 
31
  private final UserRepository userRepository;
32
 
33
  @Transactional
@@ -39,6 +44,22 @@ public class ReportServiceImpl implements ReportService {
39
  report.setCreatedAt(Instant.now());
40
 
41
  Report savedReport = reportRepository.save(report);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  return ReportResponseMapper.fromEntity(savedReport);
43
  }
44
 
@@ -55,6 +76,13 @@ public class ReportServiceImpl implements ReportService {
55
  .map(ReportResponseMapper::fromEntity);
56
  }
57
 
 
 
 
 
 
 
 
58
  @Override
59
  public ReportDetailResponse getReportById(final Long id) {
60
  Report report = reportRepository.findById(id)
@@ -71,4 +99,4 @@ public class ReportServiceImpl implements ReportService {
71
  .orElseThrow(() -> new IllegalArgumentException(
72
  "Không tìm thấy người dùng với tên đăng nhập: " + currentUsername));
73
  }
74
- }
 
1
  package com.attendenceSystem.module.report.service.impl;
2
 
3
  import java.time.Instant;
4
+ import java.util.ArrayList;
5
+ import java.util.List;
6
 
7
  import org.springframework.data.domain.Page;
8
  import org.springframework.data.domain.Pageable;
 
14
  import com.attendenceSystem.module.report.dto.response.ReportDetailResponse;
15
  import com.attendenceSystem.module.report.dto.response.ReportResponse;
16
  import com.attendenceSystem.module.report.entity.Report;
17
+ import com.attendenceSystem.module.report.entity.ReportShare;
18
  import com.attendenceSystem.module.report.mapper.request.CreateReportRequestMapper;
19
  import com.attendenceSystem.module.report.mapper.response.ReportDetailResponseMapper;
20
  import com.attendenceSystem.module.report.mapper.response.ReportResponseMapper;
21
  import com.attendenceSystem.module.report.repository.ReportRepository;
22
+ import com.attendenceSystem.module.report.repository.ReportShareRepository;
23
  import com.attendenceSystem.module.report.service.ReportService;
24
  import com.attendenceSystem.module.user.entity.User;
25
  import com.attendenceSystem.module.user.repository.UserRepository;
 
32
  public class ReportServiceImpl implements ReportService {
33
 
34
  private final ReportRepository reportRepository;
35
+ private final ReportShareRepository reportShareRepository;
36
  private final UserRepository userRepository;
37
 
38
  @Transactional
 
44
  report.setCreatedAt(Instant.now());
45
 
46
  Report savedReport = reportRepository.save(report);
47
+
48
+ // Save shared users
49
+ if (request.getSharedUserIds() != null && request.getSharedUserIds().length > 0) {
50
+ List<ReportShare> shares = new ArrayList<>();
51
+ for (Long userId : request.getSharedUserIds()) {
52
+ User sharedUser = userRepository.findById(userId)
53
+ .orElseThrow(() -> new IllegalArgumentException(
54
+ "Không tìm thấy người dùng với id: " + userId));
55
+ shares.add(ReportShare.builder()
56
+ .report(savedReport)
57
+ .user(sharedUser)
58
+ .build());
59
+ }
60
+ reportShareRepository.saveAll(shares);
61
+ }
62
+
63
  return ReportResponseMapper.fromEntity(savedReport);
64
  }
65
 
 
76
  .map(ReportResponseMapper::fromEntity);
77
  }
78
 
79
+ @Override
80
+ public Page<ReportResponse> getSharedWithMe(final Pageable pageable) {
81
+ User currentUser = findCurrentUser();
82
+ return reportShareRepository.findReportsSharedWithUser(currentUser, pageable)
83
+ .map(ReportResponseMapper::fromEntity);
84
+ }
85
+
86
  @Override
87
  public ReportDetailResponse getReportById(final Long id) {
88
  Report report = reportRepository.findById(id)
 
99
  .orElseThrow(() -> new IllegalArgumentException(
100
  "Không tìm thấy người dùng với tên đăng nhập: " + currentUsername));
101
  }
102
+ }
src/main/java/com/attendenceSystem/module/user/controller/AccountController.java CHANGED
@@ -22,7 +22,7 @@ import org.springframework.web.bind.annotation.PostMapping;
22
  public class AccountController {
23
  private final AccountService accountService;
24
 
25
- @GetMapping(Routes.Account.ROOT)
26
  public String toListPage(@PageableDefault(size = 10) Pageable pageable, Model model) {
27
  model.addAttribute("users", accountService.getUsers(pageable));
28
  model.addAttribute("currentUsername", SecurityUtil.getCurrentUserName());
@@ -32,28 +32,44 @@ public class AccountController {
32
 
33
  @GetMapping(Routes.Account.MANAGER)
34
  public String toEmployeeListPage(@PageableDefault(size = 10) Pageable pageable, Model model) {
35
- model.addAttribute("users", accountService.getUsers(pageable));
36
  model.addAttribute("currentUsername", SecurityUtil.getCurrentUserName());
37
  model.addAttribute("currentRole", SecurityUtil.getCurrentUserRole());
 
38
  return Views.Account.EMPLOYEE_LIST;
39
  }
40
 
41
- @GetMapping(Routes.Account.ROOT + "/{id}")
42
  public String toDetailPage(@PathVariable("id") Long id, Model model) {
43
  UserResponse user = accountService.getUserById(id);
44
  model.addAttribute("user", user);
45
  return Views.User.DETAIL;
46
  }
47
 
48
- @PostMapping(Routes.Account.ROOT + Routes.Action.DEACTIVATE + "/{id}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  public String deactiveUser(@PathVariable("id") Long id) {
50
  accountService.deactivateUser(id);
51
- return Routes.REDIRECT + Routes.Account.ROOT;
52
  }
53
 
54
- @PostMapping(Routes.Account.ROOT + Routes.Action.ACTIVATE + "/{id}")
55
  public String activateUser(@PathVariable("id") Long id) {
56
  accountService.activateUser(id);
57
- return Routes.REDIRECT + Routes.Account.ROOT;
58
  }
59
- }
 
22
  public class AccountController {
23
  private final AccountService accountService;
24
 
25
+ @GetMapping(Routes.Account.ADMIN)
26
  public String toListPage(@PageableDefault(size = 10) Pageable pageable, Model model) {
27
  model.addAttribute("users", accountService.getUsers(pageable));
28
  model.addAttribute("currentUsername", SecurityUtil.getCurrentUserName());
 
32
 
33
  @GetMapping(Routes.Account.MANAGER)
34
  public String toEmployeeListPage(@PageableDefault(size = 10) Pageable pageable, Model model) {
35
+ model.addAttribute("users", accountService.getEmployees(pageable));
36
  model.addAttribute("currentUsername", SecurityUtil.getCurrentUserName());
37
  model.addAttribute("currentRole", SecurityUtil.getCurrentUserRole());
38
+ model.addAttribute("departments", com.attendenceSystem.module.user.entity.enums.Department.values());
39
  return Views.Account.EMPLOYEE_LIST;
40
  }
41
 
42
+ @GetMapping(Routes.Account.ADMIN + "/{id}")
43
  public String toDetailPage(@PathVariable("id") Long id, Model model) {
44
  UserResponse user = accountService.getUserById(id);
45
  model.addAttribute("user", user);
46
  return Views.User.DETAIL;
47
  }
48
 
49
+ @GetMapping(Routes.Account.MANAGER + "/{id}")
50
+ public String toEmployeeDetailPage(@PathVariable("id") Long id, Model model) {
51
+ UserResponse user = accountService.getUserById(id);
52
+ model.addAttribute("user", user);
53
+ model.addAttribute("departments", com.attendenceSystem.module.user.entity.enums.Department.values());
54
+ return Views.Account.EMPLOYEE_DETAIL;
55
+ }
56
+
57
+ @PostMapping(Routes.Account.MANAGER + Routes.Action.UPDATE + "/{id}")
58
+ public String changeEmployeeDepartment(@PathVariable("id") Long id,
59
+ @org.springframework.web.bind.annotation.RequestParam("department") String department) {
60
+ accountService.changeDepartment(id, department);
61
+ return Routes.REDIRECT + Routes.Account.MANAGER;
62
+ }
63
+
64
+ @PostMapping(Routes.Account.ADMIN + Routes.Action.DEACTIVATE + "/{id}")
65
  public String deactiveUser(@PathVariable("id") Long id) {
66
  accountService.deactivateUser(id);
67
+ return Routes.REDIRECT + Routes.Account.ADMIN;
68
  }
69
 
70
+ @PostMapping(Routes.Account.ADMIN + Routes.Action.ACTIVATE + "/{id}")
71
  public String activateUser(@PathVariable("id") Long id) {
72
  accountService.activateUser(id);
73
+ return Routes.REDIRECT + Routes.Account.ADMIN;
74
  }
75
+ }
src/main/java/com/attendenceSystem/module/user/controller/UserController.java CHANGED
@@ -100,7 +100,7 @@ public class UserController {
100
  }
101
  }
102
 
103
- @PostMapping(Routes.User.UPDATE_INFORMATION)
104
  public String updateUserInformation(@Valid @ModelAttribute UpdateUserInformationRequest request,
105
  BindingResult result,
106
  Model model) {
 
100
  }
101
  }
102
 
103
+ @PostMapping(Routes.User.INFORMATION + Routes.Action.UPDATE)
104
  public String updateUserInformation(@Valid @ModelAttribute UpdateUserInformationRequest request,
105
  BindingResult result,
106
  Model model) {
src/main/java/com/attendenceSystem/module/user/dto/request/UpdateUserInformationRequest.java CHANGED
@@ -1,6 +1,6 @@
1
  package com.attendenceSystem.module.user.dto.request;
2
 
3
- import com.attendenceSystem.module.user.entity.enums.Department;
4
 
5
  import jakarta.validation.constraints.Email;
6
  import jakarta.validation.constraints.NotBlank;
@@ -23,5 +23,5 @@ public class UpdateUserInformationRequest {
23
  @Size(max = 255, message = "Email không được vượt quá 255 ký tự")
24
  private String email;
25
 
26
- private Department department;
27
  }
 
1
  package com.attendenceSystem.module.user.dto.request;
2
 
3
+ import com.attendenceSystem.module.user.entity.enums.Specialization;
4
 
5
  import jakarta.validation.constraints.Email;
6
  import jakarta.validation.constraints.NotBlank;
 
23
  @Size(max = 255, message = "Email không được vượt quá 255 ký tự")
24
  private String email;
25
 
26
+ private Specialization specialization;
27
  }
src/main/java/com/attendenceSystem/module/user/dto/response/UserInformationResponse.java CHANGED
@@ -2,6 +2,7 @@ package com.attendenceSystem.module.user.dto.response;
2
 
3
  import com.attendenceSystem.module.user.entity.enums.Department;
4
  import com.attendenceSystem.module.user.entity.enums.Role;
 
5
  import com.attendenceSystem.module.user.entity.enums.Status;
6
 
7
  import lombok.Builder;
@@ -15,6 +16,7 @@ public record UserInformationResponse(
15
  String fullName,
16
  Role role,
17
  Status status,
 
18
  Department department) {
19
 
20
  }
 
2
 
3
  import com.attendenceSystem.module.user.entity.enums.Department;
4
  import com.attendenceSystem.module.user.entity.enums.Role;
5
+ import com.attendenceSystem.module.user.entity.enums.Specialization;
6
  import com.attendenceSystem.module.user.entity.enums.Status;
7
 
8
  import lombok.Builder;
 
16
  String fullName,
17
  Role role,
18
  Status status,
19
+ Specialization specialization,
20
  Department department) {
21
 
22
  }
src/main/java/com/attendenceSystem/module/user/dto/response/UserResponse.java CHANGED
@@ -2,13 +2,16 @@ package com.attendenceSystem.module.user.dto.response;
2
 
3
  import lombok.Builder;
4
 
5
- @Builder
6
- public record UserResponse(
7
- Long id,
8
- String username,
9
- String email,
10
- String fullName,
11
- String status,
12
- String role) {
 
 
 
13
 
14
- }
 
2
 
3
  import lombok.Builder;
4
 
5
+ @Builder
6
+ public record UserResponse(
7
+ Long id,
8
+ String username,
9
+ String email,
10
+ String phone,
11
+ String fullName,
12
+ String status,
13
+ String role,
14
+ String department,
15
+ String departmentDisplay) {
16
 
17
+ }
src/main/java/com/attendenceSystem/module/user/entity/User.java CHANGED
@@ -7,6 +7,7 @@ import org.hibernate.annotations.UpdateTimestamp;
7
 
8
  import com.attendenceSystem.module.user.entity.enums.Department;
9
  import com.attendenceSystem.module.user.entity.enums.Role;
 
10
  import com.attendenceSystem.module.user.entity.enums.Status;
11
 
12
  import jakarta.persistence.Column;
@@ -48,7 +49,10 @@ public class User {
48
  private Status status = Status.PENDING;
49
  @Column(name = "full_name", nullable = false, length = 255)
50
  private String fullName;
51
- @Column(name = "department", nullable = false)
 
 
 
52
  private Department department;
53
  @CreationTimestamp
54
  @Column(name = "created_at", nullable = false, updatable = false)
@@ -59,4 +63,4 @@ public class User {
59
  @Column(name = "must_change_password", nullable = false)
60
  @Builder.Default
61
  private boolean mustChangePassword = false;
62
- }
 
7
 
8
  import com.attendenceSystem.module.user.entity.enums.Department;
9
  import com.attendenceSystem.module.user.entity.enums.Role;
10
+ import com.attendenceSystem.module.user.entity.enums.Specialization;
11
  import com.attendenceSystem.module.user.entity.enums.Status;
12
 
13
  import jakarta.persistence.Column;
 
49
  private Status status = Status.PENDING;
50
  @Column(name = "full_name", nullable = false, length = 255)
51
  private String fullName;
52
+ @Column(name = "specialization", nullable = false)
53
+ @Builder.Default
54
+ private Specialization specialization = Specialization.UNASSIGNED;
55
+ @Column(name = "department")
56
  private Department department;
57
  @CreationTimestamp
58
  @Column(name = "created_at", nullable = false, updatable = false)
 
63
  @Column(name = "must_change_password", nullable = false)
64
  @Builder.Default
65
  private boolean mustChangePassword = false;
66
+ }
src/main/java/com/attendenceSystem/module/user/entity/converter/DepartmentConverter.java CHANGED
@@ -6,16 +6,23 @@ import jakarta.persistence.AttributeConverter;
6
  import jakarta.persistence.Converter;
7
 
8
  @Converter(autoApply = true)
9
- public class DepartmentConverter implements AttributeConverter<Department, Integer> {
10
 
11
  @Override
12
- public Integer convertToDatabaseColumn(Department attribute) {
13
- return attribute == null ? null : attribute.value;
14
  }
15
 
16
  @Override
17
- public Department convertToEntityAttribute(Integer dbData) {
18
- return dbData == null ? null : Department.fromValue(dbData);
 
 
 
 
 
 
 
 
19
  }
20
-
21
  }
 
6
  import jakarta.persistence.Converter;
7
 
8
  @Converter(autoApply = true)
9
+ public class DepartmentConverter implements AttributeConverter<Department, String> {
10
 
11
  @Override
12
+ public String convertToDatabaseColumn(Department attribute) {
13
+ return attribute == null ? null : attribute.getRoomCode();
14
  }
15
 
16
  @Override
17
+ public Department convertToEntityAttribute(String dbData) {
18
+ if (dbData == null) {
19
+ return null;
20
+ }
21
+ for (Department dept : Department.values()) {
22
+ if (dept.getRoomCode().equals(dbData)) {
23
+ return dept;
24
+ }
25
+ }
26
+ return null;
27
  }
 
28
  }
src/main/java/com/attendenceSystem/module/user/entity/converter/SpecializationConverter.java ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.user.entity.converter;
2
+
3
+ import com.attendenceSystem.module.user.entity.enums.Specialization;
4
+
5
+ import jakarta.persistence.AttributeConverter;
6
+ import jakarta.persistence.Converter;
7
+
8
+ @Converter(autoApply = true)
9
+ public class SpecializationConverter implements AttributeConverter<Specialization, Integer> {
10
+
11
+ @Override
12
+ public Integer convertToDatabaseColumn(Specialization attribute) {
13
+ return attribute == null ? null : attribute.value;
14
+ }
15
+
16
+ @Override
17
+ public Specialization convertToEntityAttribute(Integer dbData) {
18
+ return dbData == null ? null : Specialization.fromValue(dbData);
19
+ }
20
+
21
+ }
src/main/java/com/attendenceSystem/module/user/entity/enums/Department.java CHANGED
@@ -6,17 +6,11 @@ import lombok.RequiredArgsConstructor;
6
  @Getter
7
  @RequiredArgsConstructor
8
  public enum Department {
9
- UNASSIGNED(0),
10
- SE(1);
 
11
 
12
- public final int value;
13
-
14
- public static Department fromValue(int value) {
15
- for (Department d : Department.values()) {
16
- if (d.value == value) {
17
- return d;
18
- }
19
- }
20
- return null;
21
- }
22
- }
 
6
  @Getter
7
  @RequiredArgsConstructor
8
  public enum Department {
9
+ DE213("DE213", "DE213-X7A9", "Phòng DE213"),
10
+ DE212("DE212", "DE212-X7A9", "Phòng DE212"),
11
+ DE211("DE211", "DE211-X7A9", "Phòng DE211");
12
 
13
+ private final String roomCode;
14
+ private final String joinCode;
15
+ private final String displayName;
16
+ }
 
 
 
 
 
 
 
src/main/java/com/attendenceSystem/module/user/entity/enums/Specialization.java ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.attendenceSystem.module.user.entity.enums;
2
+
3
+ import lombok.Getter;
4
+ import lombok.RequiredArgsConstructor;
5
+
6
+ @Getter
7
+ @RequiredArgsConstructor
8
+ public enum Specialization {
9
+ UNASSIGNED(0, "Chưa phân công"),
10
+ SOFTWARE_ENGINEERING(1, "Kỹ thuật phần mềm"),
11
+ INFORMATION_TECHNOLOGY(2, "Công nghệ thông tin"),
12
+ DATA_SCIENCE(3, "Khoa học dữ liệu");
13
+
14
+ public final int value;
15
+ private final String displayName;
16
+
17
+ public static Specialization fromValue(int value) {
18
+ for (Specialization s : Specialization.values()) {
19
+ if (s.value == value) {
20
+ return s;
21
+ }
22
+ }
23
+ return null;
24
+ }
25
+ }
src/main/java/com/attendenceSystem/module/user/mapper/request/RegisterRequestMapper.java CHANGED
@@ -2,7 +2,7 @@ package com.attendenceSystem.module.user.mapper.request;
2
 
3
  import com.attendenceSystem.module.user.dto.request.RegisterRequest;
4
  import com.attendenceSystem.module.user.entity.User;
5
- import com.attendenceSystem.module.user.entity.enums.Department;
6
  import com.attendenceSystem.module.user.entity.enums.Role;
7
  import com.attendenceSystem.module.user.entity.enums.Status;
8
 
@@ -17,7 +17,7 @@ public final class RegisterRequestMapper {
17
  .email(request.getEmail())
18
  .password(hashPassword)
19
  .fullName(request.getFullName())
20
- .department(Department.UNASSIGNED)
21
  .role(Role.EMPLOYEE)
22
  .status(Status.PENDING)
23
  .mustChangePassword(false)
 
2
 
3
  import com.attendenceSystem.module.user.dto.request.RegisterRequest;
4
  import com.attendenceSystem.module.user.entity.User;
5
+ import com.attendenceSystem.module.user.entity.enums.Specialization;
6
  import com.attendenceSystem.module.user.entity.enums.Role;
7
  import com.attendenceSystem.module.user.entity.enums.Status;
8
 
 
17
  .email(request.getEmail())
18
  .password(hashPassword)
19
  .fullName(request.getFullName())
20
+ .specialization(Specialization.UNASSIGNED)
21
  .role(Role.EMPLOYEE)
22
  .status(Status.PENDING)
23
  .mustChangePassword(false)
src/main/java/com/attendenceSystem/module/user/mapper/response/UserInformationResponseMapper.java CHANGED
@@ -20,6 +20,7 @@ public class UserInformationResponseMapper {
20
  .fullName(user.getFullName())
21
  .role(user.getRole())
22
  .status(user.getStatus())
 
23
  .department(user.getDepartment())
24
  .build();
25
  }
 
20
  .fullName(user.getFullName())
21
  .role(user.getRole())
22
  .status(user.getStatus())
23
+ .specialization(user.getSpecialization())
24
  .department(user.getDepartment())
25
  .build();
26
  }
src/main/java/com/attendenceSystem/module/user/mapper/response/UserResponseMapper.java CHANGED
@@ -20,9 +20,12 @@ public class UserResponseMapper {
20
  .id(user.getId())
21
  .username(user.getUsername())
22
  .email(user.getEmail())
 
23
  .fullName(user.getFullName())
24
  .status(user.getStatus() == null ? null : user.getStatus().name())
25
  .role(user.getRole().name())
 
 
26
  .build();
27
  }
28
  }
 
20
  .id(user.getId())
21
  .username(user.getUsername())
22
  .email(user.getEmail())
23
+ .phone(user.getPhone())
24
  .fullName(user.getFullName())
25
  .status(user.getStatus() == null ? null : user.getStatus().name())
26
  .role(user.getRole().name())
27
+ .department(user.getDepartment() == null ? null : user.getDepartment().getRoomCode())
28
+ .departmentDisplay(user.getDepartment() == null ? "N/A" : user.getDepartment().getDisplayName())
29
  .build();
30
  }
31
  }
src/main/java/com/attendenceSystem/module/user/repository/UserRepository.java CHANGED
@@ -1,5 +1,6 @@
1
  package com.attendenceSystem.module.user.repository;
2
 
 
3
  import java.util.Optional;
4
 
5
  import org.springframework.data.domain.Page;
@@ -8,6 +9,7 @@ import org.springframework.data.jpa.repository.JpaRepository;
8
  import org.springframework.stereotype.Repository;
9
 
10
  import com.attendenceSystem.module.user.entity.User;
 
11
  import com.attendenceSystem.module.user.entity.enums.Role;
12
  import com.attendenceSystem.module.user.entity.enums.Status;
13
 
@@ -26,4 +28,6 @@ public interface UserRepository extends JpaRepository<User, Long> {
26
  long countByRoleNot(Role role);
27
  Optional<User> findByUsername(String username);
28
  boolean existsByPhone(String phone);
 
 
29
  }
 
1
  package com.attendenceSystem.module.user.repository;
2
 
3
+ import java.util.List;
4
  import java.util.Optional;
5
 
6
  import org.springframework.data.domain.Page;
 
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;
15
 
 
28
  long countByRoleNot(Role role);
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
  }
src/main/java/com/attendenceSystem/module/user/service/AccountService.java CHANGED
@@ -7,6 +7,9 @@ import com.attendenceSystem.module.user.dto.response.UserResponse;
7
 
8
  public interface AccountService {
9
  Page<UserResponse> getUsers(Pageable pageable);
 
10
  UserResponse getUserById(Long id);
11
  void deactivateUser(Long id);
12
- void activateUser(Long id);}
 
 
 
7
 
8
  public interface AccountService {
9
  Page<UserResponse> getUsers(Pageable pageable);
10
+ Page<UserResponse> getEmployees(Pageable pageable);
11
  UserResponse getUserById(Long id);
12
  void deactivateUser(Long id);
13
+ void activateUser(Long id);
14
+ void changeDepartment(Long id, String departmentCode);
15
+ }
src/main/java/com/attendenceSystem/module/user/service/impl/AccountServiceImpl.java CHANGED
@@ -7,6 +7,7 @@ import org.springframework.transaction.annotation.Transactional;
7
 
8
  import com.attendenceSystem.module.user.dto.response.UserResponse;
9
  import com.attendenceSystem.module.user.entity.User;
 
10
  import com.attendenceSystem.module.user.entity.enums.Role;
11
  import com.attendenceSystem.module.user.entity.enums.Status;
12
  import com.attendenceSystem.module.user.mapper.response.UserResponseMapper;
@@ -27,6 +28,12 @@ public class AccountServiceImpl implements AccountService {
27
  return userRepository.findAll(pageable).map(userResponseMapper::fromEntity);
28
  }
29
 
 
 
 
 
 
 
30
  @Override
31
  public UserResponse getUserById(final Long id) {
32
  User user = findById(id);
@@ -57,6 +64,38 @@ public class AccountServiceImpl implements AccountService {
57
  userRepository.save(user);
58
  }
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  private User findById(final Long id) {
61
  return userRepository.findById(id)
62
  .orElseThrow(() -> new IllegalArgumentException("Không tìm thấy người dùng với id: " + id));
 
7
 
8
  import com.attendenceSystem.module.user.dto.response.UserResponse;
9
  import com.attendenceSystem.module.user.entity.User;
10
+ import com.attendenceSystem.module.user.entity.enums.Department;
11
  import com.attendenceSystem.module.user.entity.enums.Role;
12
  import com.attendenceSystem.module.user.entity.enums.Status;
13
  import com.attendenceSystem.module.user.mapper.response.UserResponseMapper;
 
28
  return userRepository.findAll(pageable).map(userResponseMapper::fromEntity);
29
  }
30
 
31
+ @Override
32
+ public Page<UserResponse> getEmployees(final Pageable pageable) {
33
+ return userRepository.findByRole(Role.EMPLOYEE, pageable)
34
+ .map(userResponseMapper::fromEntity);
35
+ }
36
+
37
  @Override
38
  public UserResponse getUserById(final Long id) {
39
  User user = findById(id);
 
64
  userRepository.save(user);
65
  }
66
 
67
+ @Transactional
68
+ @Override
69
+ public void changeDepartment(final Long id, final String departmentCode) {
70
+ User user = findById(id);
71
+
72
+ // Chỉ manager mới có quyền chuyển phòng
73
+ if (!SecurityUtil.isAuthenticated()) {
74
+ throw new IllegalStateException("Không xác định được tài khoản hiện tại");
75
+ }
76
+ Role currentUserRole = SecurityUtil.getCurrentUserRole();
77
+ if (currentUserRole != Role.MANAGER && currentUserRole != Role.ADMIN) {
78
+ throw new IllegalStateException("Bạn không có quyền thực hiện hành động này");
79
+ }
80
+
81
+ // Chỉ thao tác trên employee
82
+ if (user.getRole() != Role.EMPLOYEE) {
83
+ throw new IllegalStateException("Chỉ có thể chuyển phòng cho nhân viên");
84
+ }
85
+
86
+ Department department = null;
87
+ if (departmentCode != null && !departmentCode.isBlank()) {
88
+ try {
89
+ department = Department.valueOf(departmentCode);
90
+ } catch (IllegalArgumentException e) {
91
+ throw new IllegalArgumentException("Mã phòng không hợp lệ: " + departmentCode);
92
+ }
93
+ }
94
+
95
+ user.setDepartment(department);
96
+ userRepository.save(user);
97
+ }
98
+
99
  private User findById(final Long id) {
100
  return userRepository.findById(id)
101
  .orElseThrow(() -> new IllegalArgumentException("Không tìm thấy người dùng với id: " + id));
src/main/java/com/attendenceSystem/module/user/service/impl/UserServiceImpl.java CHANGED
@@ -79,8 +79,8 @@ public class UserServiceImpl implements UserService {
79
  }
80
  currentUser.setEmail(request.getEmail());
81
  }
82
- if (request.getDepartment() != null) {
83
- currentUser.setDepartment(request.getDepartment());
84
  }
85
  userRepository.save(currentUser);
86
  return userInformationResponseMapper.fromEntity(currentUser);
 
79
  }
80
  currentUser.setEmail(request.getEmail());
81
  }
82
+ if (request.getSpecialization() != null) {
83
+ currentUser.setSpecialization(request.getSpecialization());
84
  }
85
  userRepository.save(currentUser);
86
  return userInformationResponseMapper.fromEntity(currentUser);
src/main/java/com/attendenceSystem/security/CustomUserDetails.java CHANGED
@@ -35,7 +35,7 @@ public class CustomUserDetails implements UserDetails {
35
  user.getFullName(),
36
  user.getEmail(),
37
  user.getPhone(),
38
- user.getDepartment() == null ? null : user.getDepartment().name(),
39
  user.getRole().name(),
40
  user.getStatus());
41
  }
@@ -48,7 +48,7 @@ public class CustomUserDetails implements UserDetails {
48
  response.fullName(),
49
  response.email(),
50
  response.phone(),
51
- response.department() == null ? null : response.department().name(),
52
  response.role().name(),
53
  response.status());
54
  }
 
35
  user.getFullName(),
36
  user.getEmail(),
37
  user.getPhone(),
38
+ user.getSpecialization() == null ? null : user.getSpecialization().name(),
39
  user.getRole().name(),
40
  user.getStatus());
41
  }
 
48
  response.fullName(),
49
  response.email(),
50
  response.phone(),
51
+ response.specialization() == null ? null : response.specialization().name(),
52
  response.role().name(),
53
  response.status());
54
  }
src/main/resources/static/css/account/employee-detail.css ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url("../base.css");
2
+
3
+ /* ===================================
4
+ HEADER
5
+ =================================== */
6
+
7
+ .container-header {
8
+ margin-bottom: 24px;
9
+ }
10
+
11
+ .container-header h1 {
12
+ font-size: 1.8rem;
13
+ font-weight: 700;
14
+ color: var(--text-white);
15
+ }
16
+
17
+ .container-header p {
18
+ color: var(--text-light);
19
+ font-size: 0.9rem;
20
+ }
21
+
22
+ /* ===================================
23
+ BACK BUTTON
24
+ =================================== */
25
+
26
+ .back-btn {
27
+ display: inline-flex;
28
+ align-items: center;
29
+ gap: 8px;
30
+ padding: 10px 20px;
31
+ border-radius: 12px;
32
+ background: rgba(255, 255, 255, 0.06);
33
+ color: var(--text-light);
34
+ font-size: 0.85rem;
35
+ font-weight: 600;
36
+ text-decoration: none;
37
+ transition: 0.25s;
38
+ }
39
+
40
+ .back-btn:hover {
41
+ background: rgba(255, 255, 255, 0.12);
42
+ color: var(--text-white);
43
+ }
44
+
45
+ /* ===================================
46
+ PROFILE CARD
47
+ =================================== */
48
+
49
+ .container-card {
50
+ background: var(--bg-card);
51
+ backdrop-filter: blur(20px);
52
+ border: 1px solid var(--border);
53
+ border-radius: 24px;
54
+ overflow: hidden;
55
+ margin-bottom: 24px;
56
+ }
57
+
58
+ .card-header {
59
+ padding: 24px;
60
+ display: flex;
61
+ justify-content: space-between;
62
+ align-items: center;
63
+ border-bottom: 1px solid var(--border);
64
+ }
65
+
66
+ .card-header h2 {
67
+ font-size: 1.3rem;
68
+ font-weight: 700;
69
+ color: var(--text-white);
70
+ }
71
+
72
+ .card-body {
73
+ padding: 28px;
74
+ }
75
+
76
+ /* ===================================
77
+ INFO GRID
78
+ =================================== */
79
+
80
+ .info-grid {
81
+ display: grid;
82
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
83
+ gap: 24px;
84
+ }
85
+
86
+ .info-item {
87
+ display: flex;
88
+ flex-direction: column;
89
+ gap: 8px;
90
+ }
91
+
92
+ .info-item label {
93
+ color: var(--text-light);
94
+ font-size: 0.85rem;
95
+ font-weight: 600;
96
+ }
97
+
98
+ .info-item p {
99
+ color: var(--text-white);
100
+ font-size: 1rem;
101
+ font-weight: 500;
102
+ }
103
+
104
+ /* ===================================
105
+ STATUS
106
+ =================================== */
107
+
108
+ .status {
109
+ display: inline-flex;
110
+ align-items: center;
111
+ padding: 6px 14px;
112
+ border-radius: 999px;
113
+ font-size: 0.85rem;
114
+ font-weight: 600;
115
+ }
116
+
117
+ .active {
118
+ background: rgba(34, 197, 94, 0.15);
119
+ color: var(--success);
120
+ }
121
+
122
+ .inactive {
123
+ background: rgba(245, 158, 11, 0.15);
124
+ color: var(--warning);
125
+ }
126
+
127
+ .locked {
128
+ background: rgba(239, 68, 68, 0.15);
129
+ color: var(--danger);
130
+ }
131
+
132
+ /* ===================================
133
+ CHANGE DEPARTMENT CARD
134
+ =================================== */
135
+
136
+ .dashboard-card {
137
+ background: var(--bg-card);
138
+ backdrop-filter: blur(20px);
139
+ border: 1px solid var(--border);
140
+ border-radius: 24px;
141
+ overflow: hidden;
142
+ }
143
+
144
+ .change-dept-form .form-group {
145
+ padding: 24px;
146
+ display: flex;
147
+ flex-direction: column;
148
+ gap: 12px;
149
+ }
150
+
151
+ .change-dept-form label {
152
+ color: var(--text-light);
153
+ font-size: 0.9rem;
154
+ font-weight: 600;
155
+ }
156
+
157
+ .change-dept-form select {
158
+ width: 100%;
159
+ height: 52px;
160
+ padding: 0 18px;
161
+ border-radius: 14px;
162
+ border: 1px solid rgba(255, 255, 255, 0.08);
163
+ background: rgba(255, 255, 255, 0.05);
164
+ color: var(--text-white);
165
+ font-size: 0.95rem;
166
+ cursor: pointer;
167
+ transition: 0.25s;
168
+ }
169
+
170
+ .change-dept-form select:focus {
171
+ outline: none;
172
+ border-color: var(--indigo-06);
173
+ box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.15);
174
+ }
175
+
176
+ .change-dept-form option {
177
+ background: #0f172a;
178
+ color: white;
179
+ }
180
+
181
+ /* ===================================
182
+ RESPONSIVE
183
+ =================================== */
184
+
185
+ @media (max-width: 768px) {
186
+ .info-grid {
187
+ grid-template-columns: 1fr;
188
+ }
189
+
190
+ .card-header {
191
+ flex-direction: column;
192
+ align-items: flex-start;
193
+ gap: 15px;
194
+ }
195
+ }
src/main/resources/static/css/document/document-create.css CHANGED
@@ -188,4 +188,72 @@ input[type="file"]{
188
  width:100%;
189
  justify-content:center;
190
  }
191
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
  width:100%;
189
  justify-content:center;
190
  }
191
+ }
192
+
193
+ /* ===================================
194
+ CHECKBOX SYSTEM
195
+ =================================== */
196
+
197
+ .checkbox-container{
198
+ display:flex;
199
+
200
+ flex-direction:column;
201
+
202
+ gap:10px;
203
+
204
+ padding:12px 14px;
205
+
206
+ border-radius:14px;
207
+
208
+ border:1px solid var(--border);
209
+
210
+ background:var(--bg-table-06);
211
+
212
+ min-height:120px;
213
+
214
+ max-height:300px;
215
+
216
+ overflow-y:auto;
217
+ }
218
+
219
+ .checkbox-label{
220
+ display:flex;
221
+
222
+ align-items:center;
223
+
224
+ gap:10px;
225
+
226
+ padding:8px 10px;
227
+
228
+ border-radius:10px;
229
+
230
+ cursor:pointer;
231
+
232
+ transition:.25s;
233
+
234
+ color:var(--text-white);
235
+ }
236
+
237
+ .checkbox-label:hover{
238
+ background:var(--bg-table);
239
+ }
240
+
241
+ .checkbox-label input[type="checkbox"]{
242
+ width:18px;
243
+
244
+ height:18px;
245
+
246
+ cursor:pointer;
247
+
248
+ accent-color:var(--indigo-06);
249
+ }
250
+
251
+ .placeholder-text{
252
+ color:var(--text-gray);
253
+
254
+ font-style:italic;
255
+
256
+ padding:20px;
257
+
258
+ text-align:center;
259
+ }
src/main/resources/templates/cms/account/employee-detail.html ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+
3
+ <html xmlns:th="http://www.thymeleaf.org">
4
+
5
+ <head>
6
+ <meta charset="UTF-8">
7
+ <title>Chi tiết nhân viên</title>
8
+
9
+ <link rel="stylesheet" th:href="@{/css/account/employee-detail.css}">
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
11
+ </head>
12
+
13
+ <body>
14
+
15
+ <div class="bg-image"></div>
16
+
17
+ <div class="layout">
18
+
19
+ <div th:replace="~{cms/sidebar/sidebar-role :: sidebar}"></div>
20
+
21
+ <div class="container-layout">
22
+
23
+ <!-- HEADER -->
24
+ <div class="container-header">
25
+ <h1>Chi tiết nhân viên</h1>
26
+ <p>Thông tin chi tiết của nhân viên</p>
27
+ </div>
28
+
29
+ <main class="main-container">
30
+
31
+ <section class="container-card">
32
+
33
+ <!-- PROFILE CARD -->
34
+ <div class="card-header">
35
+ <h2>Thông tin cá nhân</h2>
36
+ <a th:href="@{/manager/accounts}" class="back-btn">
37
+ <i class="fas fa-arrow-left"></i>
38
+ Quay lại
39
+ </a>
40
+ </div>
41
+
42
+ <div class="card-body">
43
+ <div class="info-grid">
44
+
45
+ <div class="info-item">
46
+ <label>Họ và tên</label>
47
+ <p th:text="${user.fullName()}">Nguyễn Văn A</p>
48
+ </div>
49
+
50
+ <div class="info-item">
51
+ <label>Email</label>
52
+ <p th:text="${user.email()}">nguyenvana@company.com</p>
53
+ </div>
54
+
55
+ <div class="info-item">
56
+ <label>Số điện thoại</label>
57
+ <p th:text="${user.phone() != null && !user.phone().isEmpty() ? user.phone() : 'Chưa có'}">Chưa có</p>
58
+ </div>
59
+
60
+ <div class="info-item">
61
+ <label>Mã nhân viên</label>
62
+ <p th:text="${user.username()}">EMP001</p>
63
+ </div>
64
+
65
+ <div class="info-item">
66
+ <label>Phòng ban</label>
67
+ <p th:text="${user.departmentDisplay()}">N/A</p>
68
+ </div>
69
+
70
+ <div class="info-item">
71
+ <label>Vai trò</label>
72
+ <p th:text="${user.role() == 'EMPLOYEE' ? 'Nhân viên' : (user.role() == 'MANAGER' ? 'Quản lý' : (user.role() == 'ADMIN' ? 'Quản trị viên' : user.role()))}">Nhân viên</p>
73
+ </div>
74
+
75
+ <div class="info-item">
76
+ <label>Trạng thái</label>
77
+ <span th:text="${user.status() == 'ACTIVE' ? 'Đang làm việc' : (user.status() == 'INACTIVE' ? 'Tạm khóa' : (user.status() == 'PENDING' ? 'Chờ duyệt' : 'Không xác định'))}"
78
+ th:class="${'status ' + (user.status() == 'ACTIVE' ? 'active' : (user.status() == 'INACTIVE' ? 'inactive' : 'locked'))}">
79
+ Đang làm việc
80
+ </span>
81
+ </div>
82
+
83
+ </div>
84
+ </div>
85
+
86
+ </section>
87
+
88
+ <!-- CHANGE DEPARTMENT CARD -->
89
+ <section class="dashboard-card">
90
+
91
+ <div class="card-header">
92
+ <h2>Chuyển phòng</h2>
93
+ </div>
94
+
95
+ <div class="card-body">
96
+
97
+ <form th:action="@{/manager/accounts/update/{id}(id=${user.id()})}" method="post" class="change-dept-form">
98
+
99
+ <div class="form-group">
100
+ <label for="department">Chọn phòng mới</label>
101
+ <select id="department" name="department" onchange="this.form.submit()">
102
+ <option value="">-- Chọn phòng ban --</option>
103
+ <option th:each="dept : ${departments}"
104
+ th:value="${dept.name()}"
105
+ th:text="${dept.displayName}"
106
+ th:selected="${user.department() == dept.roomCode}">
107
+ </option>
108
+ </select>
109
+ </div>
110
+
111
+ </form>
112
+
113
+ </div>
114
+
115
+ </section>
116
+
117
+ </main>
118
+
119
+ <div th:replace="~{cms/layout/footer :: footer}"></div>
120
+
121
+ </div>
122
+
123
+ </div>
124
+
125
+ </body>
126
+
127
+ </html>
src/main/resources/templates/cms/account/employee-list.html CHANGED
@@ -51,10 +51,7 @@
51
 
52
  <select>
53
  <option>Tất cả phòng ban</option>
54
- <option>Phòng IT</option>
55
- <option>Phòng Nhân sự</option>
56
- <option>Phòng Kế toán</option>
57
- <option>Phòng Marketing</option>
58
  </select>
59
 
60
  <select>
@@ -95,25 +92,25 @@
95
 
96
  <tbody>
97
 
98
- <tr>
99
 
100
  <td>
101
 
102
  <div class="user">
103
 
104
- <div class="avatar">H</div>
105
 
106
- <strong>Nguyễn Minh Hải</strong>
107
 
108
  </div>
109
 
110
  </td>
111
 
112
- <td>EMP001</td>
113
 
114
- <td>hai@gmail.com</td>
115
 
116
- <td>IT</td>
117
 
118
  <td>Nhân viên</td>
119
 
@@ -129,69 +126,16 @@
129
 
130
  <div class="actions">
131
 
132
- <button class="icon-btn">
133
  Chi tiết
134
- </button>
135
-
136
- <button class="icon-btn btn-role">
137
- Chuyển phòng
138
- </button>
139
-
140
- <button class="icon-btn lock">
141
- Khóa
142
- </button>
143
-
144
- </div>
145
-
146
- </td>
147
-
148
- </tr>
149
-
150
- <tr>
151
-
152
- <td>
153
-
154
- <div class="user">
155
-
156
- <div class="avatar">A</div>
157
-
158
- <strong>Nguyễn Văn An</strong>
159
-
160
- </div>
161
-
162
- </td>
163
-
164
- <td>EMP002</td>
165
-
166
- <td>an@gmail.com</td>
167
-
168
- <td>Nhân sự</td>
169
-
170
- <td>Trưởng phòng</td>
171
-
172
- <td>
173
-
174
- <span class="status inactive">
175
- Nghỉ việc
176
- </span>
177
-
178
- </td>
179
-
180
- <td>
181
-
182
- <div class="actions">
183
-
184
- <button class="icon-btn">
185
- Chi tiết
186
- </button>
187
-
188
- <button class="icon-btn btn-role">
189
- Chuyển phòng
190
- </button>
191
-
192
- <button class="icon-btn lock">
193
- Mở khóa
194
- </button>
195
 
196
  </div>
197
 
 
51
 
52
  <select>
53
  <option>Tất cả phòng ban</option>
54
+ <option th:each="dept : ${departments}" th:value="${dept.name()}" th:text="${dept.displayName}"></option>
 
 
 
55
  </select>
56
 
57
  <select>
 
92
 
93
  <tbody>
94
 
95
+ <tr th:each="user : ${users.content}">
96
 
97
  <td>
98
 
99
  <div class="user">
100
 
101
+ <div class="avatar" th:text="${#strings.substring(user.fullName(), 0, 1)}">H</div>
102
 
103
+ <strong th:text="${user.fullName()}">Nguyễn Minh Hải</strong>
104
 
105
  </div>
106
 
107
  </td>
108
 
109
+ <td th:text="${user.username()}">EMP001</td>
110
 
111
+ <td th:text="${user.email()}">hai@gmail.com</td>
112
 
113
+ <td th:text="${user.departmentDisplay()}">IT</td>
114
 
115
  <td>Nhân viên</td>
116
 
 
126
 
127
  <div class="actions">
128
 
129
+ <a th:href="@{/manager/accounts/{id}(id=${user.id()})}" class="icon-btn">
130
  Chi tiết
131
+ </a>
132
+
133
+ <form th:action="@{/manager/accounts/update/{id}(id=${user.id()})}" method="post" style="display:inline;">
134
+ <select name="department" onchange="this.form.submit()">
135
+ <option value="">Chuyển phòng</option>
136
+ <option th:each="dept : ${departments}" th:value="${dept.name()}" th:text="${dept.displayName}"></option>
137
+ </select>
138
+ </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
 
140
  </div>
141
 
src/main/resources/templates/cms/dashboard/dashboard-employee.html CHANGED
@@ -115,7 +115,7 @@
115
  </div>
116
  </a>
117
 
118
- <a href="/documentCreate" class="quick-action-card">
119
  <div class="quick-action-card__icon">
120
  <i class="bi bi-file-earmark-arrow-up-fill"></i>
121
  </div>
 
115
  </div>
116
  </a>
117
 
118
+ <a href="/report/create" class="quick-action-card">
119
  <div class="quick-action-card__icon">
120
  <i class="bi bi-file-earmark-arrow-up-fill"></i>
121
  </div>
src/main/resources/templates/cms/document/document-create.html CHANGED
@@ -41,6 +41,12 @@
41
 
42
  <form method="post" th:action="@{/report/create}" enctype="multipart/form-data">
43
 
 
 
 
 
 
 
44
  <!-- Nội dung -->
45
  <div class="form-group">
46
  <label>Nội dung báo cáo</label>
@@ -55,19 +61,19 @@
55
  <select name="departmentId" id="departmentSelect" required>
56
  <option value="" disabled selected>Chọn phòng ban</option>
57
 
58
- <option th:each="d : ${departments}" th:value="${d.id}" th:text="${d.name}">
59
  </option>
60
  </select>
61
  </div>
62
 
63
- <!-- Người được xem -->
64
- <div class="form-group">
65
- <label>Người được chia sẻ</label>
66
 
67
- <select name="sharedUserIds" id="userSelect" multiple class="multi-select">
68
- <option value="">Chọn phòng ban trước</option>
69
- </select>
70
- </div>
71
 
72
  <!-- Upload file -->
73
  <div class="form-group">
@@ -117,18 +123,33 @@
117
 
118
  const departmentId = this.value;
119
 
120
- fetch(`/api/users/by-department/${departmentId}`)
121
  .then(res => res.json())
122
  .then(data => {
123
 
124
- const userSelect = document.getElementById("userSelect");
125
- userSelect.innerHTML = "";
 
 
 
 
 
126
 
127
  data.forEach(user => {
128
- const option = document.createElement("option");
129
- option.value = user.id;
130
- option.text = user.fullName;
131
- userSelect.appendChild(option);
 
 
 
 
 
 
 
 
 
 
132
  });
133
 
134
  });
@@ -138,4 +159,4 @@
138
 
139
  </body>
140
 
141
- </html>
 
41
 
42
  <form method="post" th:action="@{/report/create}" enctype="multipart/form-data">
43
 
44
+ <!-- Tiêu đề -->
45
+ <div class="form-group">
46
+ <label>Tiêu đề báo cáo</label>
47
+ <input type="text" name="title" placeholder="Nhập tiêu đề báo cáo..." required>
48
+ </div>
49
+
50
  <!-- Nội dung -->
51
  <div class="form-group">
52
  <label>Nội dung báo cáo</label>
 
61
  <select name="departmentId" id="departmentSelect" required>
62
  <option value="" disabled selected>Chọn phòng ban</option>
63
 
64
+ <option th:each="d : ${departments}" th:value="${d.value}" th:text="${d.displayName}">
65
  </option>
66
  </select>
67
  </div>
68
 
69
+ <!-- Người được xem -->
70
+ <div class="form-group">
71
+ <label>Người được chia sẻ</label>
72
 
73
+ <div id="userCheckboxes" class="checkbox-container">
74
+ <p class="placeholder-text">Chọn phòng ban trước</p>
75
+ </div>
76
+ </div>
77
 
78
  <!-- Upload file -->
79
  <div class="form-group">
 
123
 
124
  const departmentId = this.value;
125
 
126
+ fetch(`/api/report/users/by-department/${departmentId}`)
127
  .then(res => res.json())
128
  .then(data => {
129
 
130
+ const container = document.getElementById("userCheckboxes");
131
+ container.innerHTML = "";
132
+
133
+ if (data.length === 0) {
134
+ container.innerHTML = "<p class='placeholder-text'>Không có người dùng trong phòng ban này</p>";
135
+ return;
136
+ }
137
 
138
  data.forEach(user => {
139
+ const label = document.createElement("label");
140
+ label.className = "checkbox-label";
141
+
142
+ const checkbox = document.createElement("input");
143
+ checkbox.type = "checkbox";
144
+ checkbox.name = "sharedUserIds";
145
+ checkbox.value = user.id;
146
+
147
+ const span = document.createElement("span");
148
+ span.textContent = user.fullName;
149
+
150
+ label.appendChild(checkbox);
151
+ label.appendChild(span);
152
+ container.appendChild(label);
153
  });
154
 
155
  });
 
159
 
160
  </body>
161
 
162
+ </html>
src/main/resources/templates/cms/document/document-list.html CHANGED
@@ -40,85 +40,14 @@
40
  </tr>
41
  </thead>
42
 
43
- <tbody>
44
-
45
  <tr>
46
-
47
- <td>
48
- Nguyễn Văn A
49
- </td>
50
-
51
- <td>
52
- Báo cáo tuần 01
53
- </td>
54
-
55
- <td>
56
- 15/02/2025
57
- </td>
58
-
59
- <td>
60
- <a class=" file-link">
61
- 📎 Tải file
62
- </a>
63
-
64
- <span>
65
- Không có
66
- </span>
67
- </td>
68
-
69
- <td>
70
- <a target="_blank" class="file-link">
71
- 🔗 Mở link
72
- </a>
73
-
74
- <span>
75
- Không có
76
- </span>
77
- </td>
78
-
79
- <td>
80
-
81
- <span class="status-pending">
82
- Chờ duyệt
83
- </span>
84
-
85
-
86
-
87
- </td>
88
-
89
-
90
-
91
- <td>
92
-
93
- <div class="action-group" >
94
-
95
- <form method="post">
96
-
97
- <button class="btn btn-success">
98
- Duyệt
99
- </button>
100
-
101
- </form>
102
-
103
- <form method="post">
104
-
105
- <button class="btn btn-danger">
106
- Từ chối
107
- </button>
108
-
109
- </form>
110
-
111
- </div>
112
-
113
- </td>
114
-
115
  </tr>
116
-
117
  </tbody>
118
  </table>
119
  </div>
120
 
121
-
122
  <div class="pagination">
123
  <a class="btn btn-pagination">
124
  ← Trước
@@ -140,6 +69,76 @@
140
 
141
  </div>
142
  </div>
143
- </body>
144
 
145
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  </tr>
41
  </thead>
42
 
43
+ <tbody id="reportTableBody">
 
44
  <tr>
45
+ <td colspan="7" style="text-align:center">Đang tải...</td>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  </tr>
 
47
  </tbody>
48
  </table>
49
  </div>
50
 
 
51
  <div class="pagination">
52
  <a class="btn btn-pagination">
53
  ← Trước
 
69
 
70
  </div>
71
  </div>
 
72
 
73
+ <script>
74
+ document.addEventListener('DOMContentLoaded', function() {
75
+ const tableBody = document.getElementById('reportTableBody');
76
+
77
+ Promise.all([
78
+ fetch('/api/report/my-reports').then(res => res.json()),
79
+ fetch('/api/report/shared-with-me').then(res => res.json())
80
+ ])
81
+ .then(([myReports, sharedReports]) => {
82
+ tableBody.innerHTML = '';
83
+
84
+ const allReports = [];
85
+
86
+ if (myReports.content && myReports.content.length > 0) {
87
+ allReports.push(...myReports.content);
88
+ }
89
+
90
+ if (sharedReports.content && sharedReports.content.length > 0) {
91
+ // Add reports shared with me that are not already in my reports
92
+ sharedReports.content.forEach(shared => {
93
+ if (!allReports.some(r => r.id === shared.id)) {
94
+ allReports.push(shared);
95
+ }
96
+ });
97
+ }
98
+
99
+ // Sort by createdAt descending
100
+ allReports.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
101
+
102
+ if (allReports.length > 0) {
103
+ allReports.forEach(report => {
104
+ const row = document.createElement('tr');
105
+
106
+ const date = new Date(report.createdAt);
107
+ const formattedDate = date.toLocaleDateString('vi-VN');
108
+
109
+ const employeeName = report.employeeName || 'N/A';
110
+ const attachmentUrl = report.attachmentUrl || '';
111
+
112
+ const statusClass = 'status-' + report.status.toLowerCase();
113
+ const statusText = getStatusText(report.status);
114
+
115
+ row.innerHTML =
116
+ '<td>' + employeeName + '</td>' +
117
+ '<td>' + report.title + '</td>' +
118
+ '<td>' + formattedDate + '</td>' +
119
+ '<td>' + (attachmentUrl ? '<a href="' + attachmentUrl + '" class="file-link" download>📎 Tải file</a>' : '<span>Không có</span>') + '</td>' +
120
+ '<td>' + (attachmentUrl ? '<a href="' + attachmentUrl + '" target="_blank" class="file-link">🔗 Mở link</a>' : '<span>Không có</span>') + '</td>' +
121
+ '<td><span class="' + statusClass + '">' + statusText + '</span></td>' +
122
+ '<td><div class="action-group"><form method="post"><button class="btn btn-success" type="submit">Duyệt</button></form><form method="post"><button class="btn btn-danger" type="submit">Từ chối</button></form></div></td>';
123
+
124
+ tableBody.appendChild(row);
125
+ });
126
+ } else {
127
+ tableBody.innerHTML = '<tr><td colspan="7" style="text-align:center">Không có báo cáo nào</td></tr>';
128
+ }
129
+ })
130
+ .catch(error => {
131
+ console.error('Error loading reports:', error);
132
+ tableBody.innerHTML = '<tr><td colspan="7" style="text-align:center">Lỗi khi tải danh sách báo cáo</td></tr>';
133
+ });
134
+ });
135
+
136
+ function getStatusText(status) {
137
+ if (status === 'PENDING') return 'Chờ duyệt';
138
+ if (status === 'APPROVED') return 'Đã duyệt';
139
+ if (status === 'REJECTED') return 'Từ chối';
140
+ return status;
141
+ }
142
+ </script>
143
+ </body>
144
+ </html>
src/main/resources/templates/cms/report/report-day.html DELETED
@@ -1,144 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="vi">
3
-
4
- <head>
5
- <meta charset="UTF-8">
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>Điểm danh hằng ngày</title>
8
- <link rel="stylesheet" href="/css/report/report-day.css">
9
- </head>
10
-
11
- <body>
12
-
13
- <div class="bg-image"></div>
14
-
15
- <div class="layout">
16
-
17
- <!-- Sidebar -->
18
- <div th:replace="~{cms/sidebar/sidebar-role :: sidebar}"></div>
19
-
20
- <!-- Content -->
21
- <div class="container-layout">
22
-
23
- <!-- Header -->
24
- <div class="container-header">
25
- <h1>Báo cáo hằng ngày</h1>
26
- <p>Báo cáo điểm danh hằng ngày</p>
27
- </div>
28
-
29
- <!-- Card -->
30
- <main class="main-container">
31
-
32
- <div class="card-header">
33
- <h2>Danh sách điểm danh</h2>
34
- </div>
35
-
36
- <!-- Bộ lọc -->
37
- <div class="filter-section">
38
-
39
- <div class="filter-group">
40
- <label>Tên nhân viên</label>
41
- <input type="text" placeholder="Nhập tên nhân viên...">
42
- </div>
43
-
44
- <div class="filter-group">
45
- <label>Phòng ban</label>
46
- <select>
47
- <option>Tất cả</option>
48
- <option>IT</option>
49
- <option>Marketing</option>
50
- <option>Nhân sự</option>
51
- <option>Kế toán</option>
52
- </select>
53
- </div>
54
-
55
- <div class="filter-group">
56
- <label>Trạng thái</label>
57
- <select>
58
- <option>Tất cả</option>
59
- <option>Có mặt</option>
60
- <option>Đi muộn</option>
61
- <option>Nghỉ</option>
62
- </select>
63
- </div>
64
-
65
- <button class="filter-btn">
66
- Tìm kiếm
67
- </button>
68
-
69
- </div>
70
-
71
- <!-- Table -->
72
- <div class="table-wrapper">
73
-
74
- <table class="attendance-table">
75
-
76
- <thead>
77
- <tr>
78
- <th>STT</th>
79
- <th>Họ và tên</th>
80
- <th>Phòng ban</th>
81
- <th>Check In</th>
82
- <th>Check Out</th>
83
- <th>Trạng thái</th>
84
- </tr>
85
- </thead>
86
-
87
- <tbody>
88
-
89
- <tr>
90
- <td>1</td>
91
- <td>Nguyễn Văn A</td>
92
- <td>IT</td>
93
- <td>08:00</td>
94
- <td>17:30</td>
95
- <td>
96
- <span class="status present">
97
- Có mặt
98
- </span>
99
- </td>
100
- </tr>
101
-
102
- <tr>
103
- <td>2</td>
104
- <td>Trần Thị B</td>
105
- <td>Marketing</td>
106
- <td>08:25</td>
107
- <td>17:30</td>
108
- <td>
109
- <span class="status late">
110
- Đi muộn
111
- </span>
112
- </td>
113
- </tr>
114
-
115
- <tr>
116
- <td>3</td>
117
- <td>Lê Văn C</td>
118
- <td>Nhân sự</td>
119
- <td>--</td>
120
- <td>--</td>
121
- <td>
122
- <span class="status absent">
123
- Nghỉ
124
- </span>
125
- </td>
126
- </tr>
127
-
128
- </tbody>
129
-
130
- </table>
131
-
132
- </div>
133
-
134
- </main>
135
- <div th:replace="~{cms/layout/footer :: footer}"></div>
136
-
137
- </div>
138
-
139
- </div>
140
-
141
-
142
- </body>
143
-
144
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/main/resources/templates/cms/report/report-month.html DELETED
@@ -1,184 +0,0 @@
1
- ```html
2
- <!DOCTYPE html>
3
- <html lang="vi">
4
-
5
- <head>
6
- <meta charset="UTF-8">
7
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
- <title>Báo cáo tháng</title>
9
- <link rel="stylesheet" href="/css/report/report-month.css">
10
- </head>
11
-
12
- <body>
13
-
14
- <div class="bg-image"></div>
15
-
16
- <div class="layout">
17
-
18
- <div th:replace="~{cms/sidebar/sidebar-role :: sidebar}"></div>
19
-
20
- <div class="container-layout">
21
-
22
- <div class="container-header">
23
- <h1>Báo cáo hằng tháng</h1>
24
- <p>Thống kê chuyên cần hằng tháng </p>
25
- </div>
26
-
27
- <main class="main-container">
28
-
29
- <!-- Filter -->
30
- <div class="filter-bar">
31
-
32
- <div class="filter-group">
33
- <label>Phòng ban</label>
34
- <select>
35
- <option>IT</option>
36
- <option>Marketing</option>
37
- <option>Nhân sự</option>
38
- <option>Kế toán</option>
39
- </select>
40
- </div>
41
-
42
- <div class="filter-group">
43
- <label>Tháng</label>
44
- <input type="month">
45
- </div>
46
-
47
- <button class="export-btn">
48
- Xuất Excel
49
- </button>
50
-
51
- </div>
52
-
53
- <!-- Overview -->
54
- <div class="overview-grid">
55
-
56
- <div class="stat-card">
57
- <h3>Đi muộn</h3>
58
- <span>23</span>
59
- </div>
60
-
61
- <div class="stat-card">
62
- <h3>Đúng giờ</h3>
63
- <span>154</span>
64
- </div>
65
-
66
- <div class="stat-card">
67
- <h3>Về sớm</h3>
68
- <span>12</span>
69
- </div>
70
-
71
- <div class="stat-card">
72
- <h3>Nghỉ phép</h3>
73
- <span>8</span>
74
- </div>
75
-
76
- <div class="stat-card">
77
- <h3>Báo cáo đã nộp</h3>
78
- <span>46</span>
79
- </div>
80
-
81
- </div>
82
-
83
- <!-- Chart -->
84
- <div class="chart-card">
85
-
86
- <h2>Thống kê phòng ban</h2>
87
-
88
- <div id="monthlyChart"></div>
89
-
90
- </div>
91
-
92
- <!-- Table -->
93
- <div class="table-card">
94
-
95
- <h2>Chi tiết thành viên</h2>
96
-
97
- <table class="report-table">
98
-
99
- <thead>
100
- <tr>
101
- <th>Tên</th>
102
- <th>Phòng</th>
103
- <th>Đi muộn</th>
104
- <th>Đúng giờ</th>
105
- <th>Về sớm</th>
106
- <th>Nghỉ phép</th>
107
- <th>Báo cáo nộp</th>
108
- </tr>
109
- </thead>
110
-
111
- <tbody>
112
-
113
- <tr>
114
- <td>Nguyễn Văn A</td>
115
- <td>IT</td>
116
- <td>3</td>
117
- <td>22</td>
118
- <td>1</td>
119
- <td>0</td>
120
- <td>5</td>
121
- </tr>
122
-
123
- <tr>
124
- <td>Trần Thị B</td>
125
- <td>IT</td>
126
- <td>1</td>
127
- <td>24</td>
128
- <td>0</td>
129
- <td>1</td>
130
- <td>4</td>
131
- </tr>
132
-
133
- </tbody>
134
-
135
- </table>
136
-
137
- </div>
138
-
139
- </main>
140
- <div th:replace="~{cms/layout/footer :: footer}"></div>
141
-
142
- </div>
143
-
144
- </div>
145
-
146
-
147
- <script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
148
-
149
- <script>
150
-
151
- var options = {
152
- chart: {
153
- type: 'bar',
154
- height: 350
155
- },
156
-
157
- series: [{
158
- name: 'Số lần',
159
- data: [23, 154, 12, 8, 46]
160
- }],
161
-
162
- xaxis: {
163
- categories: [
164
- 'Đi muộn',
165
- 'Đúng giờ',
166
- 'Về sớm',
167
- 'Nghỉ phép',
168
- 'Báo cáo'
169
- ]
170
- }
171
- };
172
-
173
- var chart = new ApexCharts(
174
- document.querySelector("#monthlyChart"),
175
- options
176
- );
177
-
178
- chart.render();
179
-
180
- </script>
181
-
182
- </body>
183
-
184
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/main/resources/templates/cms/sidebar/sidebar-employee.html CHANGED
@@ -43,13 +43,13 @@
43
  </a>
44
  </li>
45
  <li class="sidebar-option">
46
- <a href="/documentList" class="sidebar-link">
47
  <span>📝</span>
48
- Báo cáo được chia sẻ
49
  </a>
50
  </li>
51
  <li class="sidebar-option">
52
- <a href="/documentCreate" class="sidebar-link">
53
  <span>📄</span>
54
  Nộp báo cáo
55
  </a>
 
43
  </a>
44
  </li>
45
  <li class="sidebar-option">
46
+ <a href="/report" class="sidebar-link">
47
  <span>📝</span>
48
+ Danh sách báo cáo
49
  </a>
50
  </li>
51
  <li class="sidebar-option">
52
+ <a href="/report/create" class="sidebar-link">
53
  <span>📄</span>
54
  Nộp báo cáo
55
  </a>
src/main/resources/templates/cms/sidebar/sidebar-manage.html CHANGED
@@ -54,7 +54,7 @@
54
  </a>
55
  </li>
56
  <li class="sidebar-option">
57
- <a href="/documentList" class="sidebar-link">
58
  <span>📝</span>
59
  Danh sách báo cáo
60
  </a>
 
54
  </a>
55
  </li>
56
  <li class="sidebar-option">
57
+ <a href="/report" class="sidebar-link">
58
  <span>📝</span>
59
  Danh sách báo cáo
60
  </a>
src/main/resources/templates/cms/user/user-information.html CHANGED
@@ -61,7 +61,7 @@
61
  </div>
62
 
63
  <div class="info-item">
64
- <label>Phòng ban</label>
65
  <p th:text="${#authentication.principal.department}">Không có </p>
66
  </div>
67
 
@@ -148,12 +148,12 @@
148
  <div class="error-message" th:if="${#fields.hasErrors('phone')}" th:errors="*{phone}"></div>
149
  </div>
150
  <div class="info-item">
151
- <label >Phòng ban</label>
152
- <select name="department">
153
- <option th:each="dept : ${T(com.attendenceSystem.module.user.entity.enums.Department).values()}"
154
- th:value="${dept.name()}"
155
- th:text="${dept.name()}"
156
- th:selected="${#authentication.principal.department == dept.name()}">
157
  </option>
158
  </select>
159
  </div>
 
61
  </div>
62
 
63
  <div class="info-item">
64
+ <label>Chuyên ngành</label>
65
  <p th:text="${#authentication.principal.department}">Không có </p>
66
  </div>
67
 
 
148
  <div class="error-message" th:if="${#fields.hasErrors('phone')}" th:errors="*{phone}"></div>
149
  </div>
150
  <div class="info-item">
151
+ <label >Chuyên ngành</label>
152
+ <select name="specialization">
153
+ <option th:each="spec : ${T(com.attendenceSystem.module.user.entity.enums.Specialization).values()}"
154
+ th:value="${spec.name()}"
155
+ th:text="${spec.displayName}"
156
+ th:selected="${#authentication.principal.department == spec.name()}">
157
  </option>
158
  </select>
159
  </div>