QPAT commited on
Commit
10d350e
·
1 Parent(s): 4cca9cd

Add my-leave

Browse files
src/main/java/com/attendenceSystem/constant/Routes.java CHANGED
@@ -56,6 +56,7 @@ public final class Routes {
56
  public static final String ROOT = "/attendance";
57
  public static final String HISTORY = "/history";
58
  public static final String LEAVE = "/leave";
 
59
  public static final String LEAVE_DETAIL = "/leave/detail";
60
  public static final String CHECK_IN = "/check-in";
61
  public static final String CHECK_OUT = "/check-out";
 
56
  public static final String ROOT = "/attendance";
57
  public static final String HISTORY = "/history";
58
  public static final String LEAVE = "/leave";
59
+ public static final String MY_LEAVE = "/leave/list";
60
  public static final String LEAVE_DETAIL = "/leave/detail";
61
  public static final String CHECK_IN = "/check-in";
62
  public static final String CHECK_OUT = "/check-out";
src/main/java/com/attendenceSystem/constant/Views.java CHANGED
@@ -37,6 +37,7 @@ public final class Views {
37
  public static final String HISTORY = "cms/attendance/attendance-history";
38
  public static final String LEAVE_CREATE = "cms/absent/absent-create";
39
  public static final String LEAVE_LIST = "cms/absent/absent-list";
 
40
  public static final String LEAVE_HISTORY = "cms/absent/absent-history";
41
  }
42
 
 
37
  public static final String HISTORY = "cms/attendance/attendance-history";
38
  public static final String LEAVE_CREATE = "cms/absent/absent-create";
39
  public static final String LEAVE_LIST = "cms/absent/absent-list";
40
+ public static final String MY_LEAVE_LIST = "cms/absent/my-absent-list";
41
  public static final String LEAVE_HISTORY = "cms/absent/absent-history";
42
  }
43
 
src/main/java/com/attendenceSystem/module/attendance/controller/AttendanceController.java CHANGED
@@ -125,6 +125,30 @@ public class AttendanceController {
125
  return Views.Attendance.LEAVE_LIST;
126
  }
127
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  @GetMapping(Routes.Attendance.LEAVE + Routes.Action.CREATE)
129
  public String toLeaveRequestPage(Model model) {
130
  model.addAttribute("createLeaveRequest", new CreateLeaveRequest());
 
125
  return Views.Attendance.LEAVE_LIST;
126
  }
127
 
128
+ @GetMapping(Routes.Attendance.MY_LEAVE)
129
+ public String myLeaveRequestList(
130
+ @RequestParam(required = false) LeaveStatus status,
131
+ @RequestParam(required = false) String week,
132
+ @PageableDefault(size = 10) Pageable pageable,
133
+ Model model) {
134
+
135
+ boolean hasFilter = status != null || (week != null && !week.isBlank());
136
+
137
+ Page<LeaveRequestResponse> leaveRequests;
138
+ if (hasFilter) {
139
+ leaveRequests = leaveService.getLeaveRequests(status, week, pageable);
140
+ } else {
141
+ leaveRequests = leaveService.getLeaveRequests(pageable);
142
+ }
143
+
144
+ model.addAttribute("leaveRequests", leaveRequests);
145
+ model.addAttribute("selectedStatus", status);
146
+ model.addAttribute("selectedWeek", week);
147
+ model.addAttribute("hasFilter", hasFilter);
148
+
149
+ return Views.Attendance.MY_LEAVE_LIST;
150
+ }
151
+
152
  @GetMapping(Routes.Attendance.LEAVE + Routes.Action.CREATE)
153
  public String toLeaveRequestPage(Model model) {
154
  model.addAttribute("createLeaveRequest", new CreateLeaveRequest());
src/main/java/com/attendenceSystem/module/attendance/service/LeaveService.java CHANGED
@@ -16,6 +16,8 @@ public interface LeaveService {
16
 
17
  Page<LeaveRequestResponse> getLeaveRequests(Pageable pageable);
18
 
 
 
19
  Page<LeaveRequestResponse> getAllLeaveRequests(Pageable pageable);
20
 
21
  Page<LeaveRequestResponse> getAllLeaveRequests(String keyword, LeaveStatus status, String week, Pageable pageable);
 
16
 
17
  Page<LeaveRequestResponse> getLeaveRequests(Pageable pageable);
18
 
19
+ Page<LeaveRequestResponse> getLeaveRequests(LeaveStatus status, String week, Pageable pageable);
20
+
21
  Page<LeaveRequestResponse> getAllLeaveRequests(Pageable pageable);
22
 
23
  Page<LeaveRequestResponse> getAllLeaveRequests(String keyword, LeaveStatus status, String week, Pageable pageable);
src/main/java/com/attendenceSystem/module/attendance/service/impl/LeaveServiceImpl.java CHANGED
@@ -73,6 +73,20 @@ public class LeaveServiceImpl implements LeaveService {
73
  .map(leaveRequestResponseMapper::fromEntity);
74
  }
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  @Override
77
  public Page<LeaveRequestResponse> getAllLeaveRequests(final Pageable pageable) {
78
  return leaveRequestRepository.findAll(pageable).map(leaveRequestResponseMapper::fromEntity);
 
73
  .map(leaveRequestResponseMapper::fromEntity);
74
  }
75
 
76
+ @Override
77
+ public Page<LeaveRequestResponse> getLeaveRequests(final LeaveStatus status, final String week, final Pageable pageable) {
78
+ User user = userContextProvider.getCurrentUserEntity();
79
+ Specification<LeaveRequest> spec = Specification
80
+ .where(hasUser(user))
81
+ .and(LeaveRequestSpecification.hasStatus(status))
82
+ .and(LeaveRequestSpecification.hasWeek(week));
83
+ return leaveRequestRepository.findAll(spec, pageable).map(leaveRequestResponseMapper::fromEntity);
84
+ }
85
+
86
+ private Specification<LeaveRequest> hasUser(User user) {
87
+ return (root, query, cb) -> cb.equal(root.get("user"), user);
88
+ }
89
+
90
  @Override
91
  public Page<LeaveRequestResponse> getAllLeaveRequests(final Pageable pageable) {
92
  return leaveRequestRepository.findAll(pageable).map(leaveRequestResponseMapper::fromEntity);
src/main/resources/static/css/absent/my-absent-list.css ADDED
@@ -0,0 +1,430 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url("../base.css");
2
+ /* ===================================
3
+ CARD
4
+ =================================== */
5
+
6
+ .dashboard-card {
7
+ background: var(--bg-card);
8
+
9
+ backdrop-filter: blur(20px);
10
+
11
+ border: 1px solid var(--border);
12
+ border-radius: 24px;
13
+
14
+ overflow: hidden;
15
+
16
+ margin-bottom: 24px;
17
+ }
18
+
19
+ .card-header {
20
+ padding: 24px;
21
+
22
+ border-bottom: 1px solid var(--border);
23
+ }
24
+
25
+ .card-header h2 {
26
+ font-size: 1.25rem;
27
+ font-weight: 700;
28
+ }
29
+
30
+ .card-body {
31
+ padding: 24px;
32
+ }
33
+
34
+ /* ===================================
35
+ FILTER
36
+ =================================== */
37
+
38
+ .filter-form {
39
+ display: flex;
40
+ flex-direction: column;
41
+ gap: 20px;
42
+ }
43
+
44
+ .filter-grid {
45
+ display: grid;
46
+ grid-template-columns:
47
+ 1fr 1fr;
48
+
49
+ gap: 18px;
50
+ }
51
+
52
+ .form-group {
53
+ display: flex;
54
+ flex-direction: column;
55
+ gap: 8px;
56
+ }
57
+
58
+ .form-group label {
59
+ color: var(--text-light);
60
+ font-size: .9rem;
61
+ font-weight: 600;
62
+ }
63
+
64
+ .form-group input,
65
+ .form-group select {
66
+ height: 50px;
67
+
68
+ padding: 0 16px;
69
+
70
+ border-radius: 14px;
71
+
72
+ background:
73
+ rgba(255, 255, 255, .05);
74
+
75
+ border: 1px solid rgba(255, 255, 255, .08);
76
+
77
+ color: white;
78
+
79
+ transition: .3s;
80
+ }
81
+
82
+ .form-group input:focus,
83
+ .form-group select:focus {
84
+ outline: none;
85
+
86
+ border-color: var(--indigo-06);
87
+
88
+ box-shadow:
89
+ 0 0 0 4px rgba(99, 102, 241, .15);
90
+ }
91
+
92
+ .form-group select option {
93
+ background: #0f172a;
94
+ }
95
+
96
+ /* ===================================
97
+ BUTTON
98
+ =================================== */
99
+
100
+ .form-actions {
101
+ display: flex;
102
+ gap: 12px;
103
+ }
104
+
105
+ .btn-primary,
106
+ .btn-secondary {
107
+ height: 50px;
108
+
109
+ padding: 0 24px;
110
+
111
+ border: none;
112
+ border-radius: 14px;
113
+
114
+ cursor: pointer;
115
+
116
+ font-weight: 600;
117
+
118
+ display: flex;
119
+ align-items: center;
120
+ justify-content: center;
121
+ gap: 8px;
122
+
123
+ transition: .25s;
124
+ }
125
+
126
+ .btn-primary {
127
+ color: white;
128
+
129
+ background:
130
+ linear-gradient(to right,
131
+ var(--indigo-06),
132
+ var(--purple));
133
+ }
134
+
135
+ .btn-primary:hover {
136
+ transform: translateY(-2px);
137
+ }
138
+
139
+ .btn-secondary {
140
+ background: #334155;
141
+ color: white;
142
+ }
143
+
144
+ .btn-secondary:hover {
145
+ background: #475569;
146
+ }
147
+
148
+ /* ===================================
149
+ TABLE
150
+ =================================== */
151
+
152
+ .table-wrapper {
153
+ overflow-x: auto;
154
+ }
155
+
156
+ .table {
157
+ width: 100%;
158
+ border-collapse: collapse;
159
+ }
160
+
161
+ .table thead {
162
+ background:
163
+ rgba(255, 255, 255, .03);
164
+ }
165
+
166
+ .table th {
167
+ padding: 18px;
168
+
169
+ text-align: left;
170
+
171
+ color: var(--text-light);
172
+
173
+ border-bottom: 1px solid var(--border);
174
+ }
175
+
176
+ .table td {
177
+ padding: 18px;
178
+
179
+ color: #e2e8f0;
180
+
181
+ border-bottom:
182
+ 1px solid rgba(255, 255, 255, .04);
183
+ }
184
+
185
+ .table tbody tr {
186
+ transition: .25s;
187
+ }
188
+
189
+ .table tbody tr:hover {
190
+ background:
191
+ rgba(255, 255, 255, .03);
192
+ }
193
+
194
+ /* ===================================
195
+ STATUS
196
+ =================================== */
197
+
198
+ .status {
199
+ display: inline-flex;
200
+
201
+ padding: 6px 12px;
202
+
203
+ border-radius: 999px;
204
+
205
+ font-size: .85rem;
206
+ font-weight: 600;
207
+ }
208
+
209
+ .pending {
210
+ background:
211
+ rgba(245, 158, 11, .15);
212
+
213
+ color: var(--warning);
214
+ }
215
+
216
+ .approved {
217
+ background:
218
+ rgba(34, 197, 94, .15);
219
+
220
+ color: var(--success);
221
+ }
222
+
223
+ .rejected {
224
+ background:
225
+ rgba(239, 68, 68, .15);
226
+
227
+ color: var(--danger);
228
+ }
229
+
230
+ /* ===================================
231
+ PAGINATION
232
+ =================================== */
233
+
234
+ .pagination {
235
+ margin-top: 30px;
236
+
237
+ display: flex;
238
+ justify-content: center;
239
+ align-items: center;
240
+
241
+ gap: 18px;
242
+ }
243
+
244
+ .btn-pagination {
245
+ padding: 12px 20px;
246
+
247
+ border-radius: 14px;
248
+
249
+ cursor: pointer;
250
+
251
+ color: white;
252
+
253
+ text-decoration: none;
254
+
255
+ background:
256
+ rgba(255, 255, 255, .05);
257
+
258
+ border: 1px solid rgba(255, 255, 255, .08);
259
+
260
+ transition: .25s;
261
+ }
262
+
263
+ .btn-pagination:hover {
264
+ background:
265
+ rgba(255, 255, 255, .08);
266
+ }
267
+
268
+ .page-info {
269
+ color: var(--text-light);
270
+ font-weight: 600;
271
+ }
272
+
273
+ /* ===================================
274
+ SCROLLBAR
275
+ =================================== */
276
+
277
+ ::-webkit-scrollbar {
278
+ width: 8px;
279
+ height: 8px;
280
+ }
281
+
282
+ ::-webkit-scrollbar-track {
283
+ background: #1e293b;
284
+ }
285
+
286
+ ::-webkit-scrollbar-thumb {
287
+ background: #475569;
288
+ border-radius: 8px;
289
+ }
290
+ /* ==========================
291
+ ABSENT MODAL
292
+ ========================== */
293
+
294
+ .absent-modal {
295
+ display: none;
296
+
297
+ position: fixed;
298
+
299
+ top: 0;
300
+ left: 0;
301
+
302
+ width: 100%;
303
+ height: 100%;
304
+
305
+ background: rgba(0, 0, 0, .75);
306
+
307
+ justify-content: center;
308
+ align-items: center;
309
+
310
+ z-index: 9999;
311
+ }
312
+
313
+ .absent-modal.show {
314
+ display: flex;
315
+ }
316
+
317
+ .absent-modal-content {
318
+ width: 850px;
319
+ max-width: 95%;
320
+
321
+ background: var(--bg-card);
322
+
323
+ border: 1px solid var(--border);
324
+
325
+ border-radius: 20px;
326
+
327
+ overflow: hidden;
328
+
329
+ box-shadow: 0 0 30px rgba(0,0,0,.4);
330
+ }
331
+
332
+ .absent-modal-header {
333
+ display: flex;
334
+
335
+ justify-content: space-between;
336
+ align-items: center;
337
+
338
+ padding: 20px;
339
+
340
+ border-bottom: 1px solid var(--border);
341
+
342
+ color: var(--text-white);
343
+ }
344
+
345
+ .absent-modal-body {
346
+ padding: 24px;
347
+ }
348
+
349
+ .detail-grid {
350
+ display: grid;
351
+
352
+ grid-template-columns: repeat(2, 1fr);
353
+
354
+ gap: 20px;
355
+
356
+ margin-bottom: 24px;
357
+ }
358
+
359
+ .detail-item {
360
+ display: flex;
361
+ flex-direction: column;
362
+ gap: 8px;
363
+ }
364
+
365
+ .detail-item label {
366
+ color: var(--text-light);
367
+ font-size: .85rem;
368
+ font-weight: 600;
369
+ }
370
+
371
+ .detail-item span {
372
+ color: var(--text-white);
373
+ }
374
+
375
+ .detail-reason {
376
+ display: flex;
377
+ flex-direction: column;
378
+ gap: 10px;
379
+ }
380
+
381
+ .reason-box {
382
+ padding: 16px;
383
+
384
+ border-radius: 12px;
385
+
386
+ border: 1px solid var(--border);
387
+
388
+ background: var(--bg-table);
389
+
390
+ color: var(--text-white);
391
+
392
+ line-height: 1.7;
393
+ }
394
+
395
+ @media(max-width:768px) {
396
+
397
+ .detail-grid {
398
+ grid-template-columns: 1fr;
399
+ }
400
+ }
401
+ /* ===================================
402
+ RESPONSIVE
403
+ =================================== */
404
+
405
+ @media(max-width:992px) {
406
+
407
+ .filter-grid {
408
+ grid-template-columns: 1fr;
409
+ }
410
+ }
411
+
412
+ @media(max-width:768px) {
413
+
414
+ .card-body {
415
+ padding: 18px;
416
+ }
417
+
418
+ .form-actions {
419
+ flex-direction: column;
420
+ }
421
+
422
+ .btn-primary,
423
+ .btn-secondary {
424
+ width: 100%;
425
+ }
426
+
427
+ .pagination {
428
+ flex-direction: column;
429
+ }
430
+ }
src/main/resources/templates/cms/absent/my-absent-list.html ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html xmlns:th="http://www.thymeleaf.org">
3
+
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <title>Xem nghỉ phép</title>
7
+ <link rel="stylesheet" href="/css/absent/my-absent-list.css">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.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>Xem nghỉ phép</h1>
26
+ <p>Danh sách đơn xin nghỉ phép của bạn</p>
27
+ </div>
28
+
29
+ <main class="main-container">
30
+
31
+ <!-- FILTER CARD -->
32
+ <section class="dashboard-card">
33
+
34
+ <div class="card-header">
35
+ <h2>Bộ lọc tìm kiếm</h2>
36
+ </div>
37
+
38
+ <div class="card-body">
39
+
40
+ <form class="filter-form" th:action="@{/attendance/leave/list}" method="get">
41
+
42
+ <div class="filter-grid">
43
+
44
+ <!-- Trạng thái -->
45
+ <div class="form-group">
46
+ <label>Trạng thái</label>
47
+ <select name="status">
48
+ <option value="" th:selected="${selectedStatus == null}">Tất cả</option>
49
+ <option value="PENDING" th:selected="${selectedStatus?.name() == 'PENDING'}">Chờ duyệt</option>
50
+ <option value="APPROVED" th:selected="${selectedStatus?.name() == 'APPROVED'}">Đã duyệt</option>
51
+ <option value="REJECTED" th:selected="${selectedStatus?.name() == 'REJECTED'}">Từ chối</option>
52
+ </select>
53
+ </div>
54
+
55
+ <!-- Tuần -->
56
+ <div class="form-group">
57
+ <label>Tuần</label>
58
+ <input type="week" name="week" th:value="${selectedWeek}">
59
+ </div>
60
+
61
+ </div>
62
+
63
+ <div class="form-actions">
64
+ <button type="submit" class="btn-primary">
65
+ <i class="fa-solid fa-magnifying-glass"></i>
66
+ Tìm kiếm
67
+ </button>
68
+
69
+ <button type="reset" class="btn-secondary">
70
+ Reset
71
+ </button>
72
+ </div>
73
+
74
+ </form>
75
+
76
+ </div>
77
+
78
+ </section>
79
+
80
+ <!-- TABLE CARD -->
81
+ <section class="dashboard-card">
82
+
83
+ <div class="card-header">
84
+ <h2>Danh sách đơn nghỉ phép</h2>
85
+ </div>
86
+
87
+ <div class="card-body">
88
+
89
+ <div class="table-wrapper">
90
+
91
+ <table class="table">
92
+ <thead>
93
+ <tr>
94
+ <th>Từ ngày</th>
95
+ <th>Đến ngày</th>
96
+ <th>Lý do</th>
97
+ <th>Trạng thái</th>
98
+ <th>Chi tiết</th>
99
+ </tr>
100
+ </thead>
101
+
102
+ <tbody>
103
+ <!-- Không có dữ liệu -->
104
+ <tr th:if="${leaveRequests == null or leaveRequests.content.isEmpty()}">
105
+ <td colspan="5" class="text-center">
106
+ Chưa có đơn nghỉ phép nào.
107
+ </td>
108
+ </tr>
109
+ <tr th:each="leave : ${leaveRequests.content}">
110
+ <td th:text="${#temporals.format(leave.startDate(), 'dd/MM/yyyy')}">Từ ngày</td>
111
+ <td th:text="${#temporals.format(leave.endDate(), 'dd/MM/yyyy')}">Đến ngày</td>
112
+ <td th:text="${leave.reason()}">Lý do</td>
113
+ <td>
114
+ <span th:classappend="${leave.status()} ? 'status-' + ${leave.status().name().toLowerCase()}"
115
+ class="status"
116
+ th:text="${leave.status()}">
117
+ </span>
118
+ </td>
119
+ <td>
120
+ <button th:data-id="${leave.id}"
121
+ class="btn btn-small btn-primary btn-view-detail" type="button">
122
+ <i class="fa-solid fa-eye"></i>
123
+ Xem chi tiết
124
+ </button>
125
+ </td>
126
+ </tr>
127
+
128
+ </tbody>
129
+
130
+ </table>
131
+
132
+ </div>
133
+
134
+ </div>
135
+
136
+ </section>
137
+
138
+ <!-- PAGINATION -->
139
+ <div class="pagination" th:if="${leaveRequests.totalPages > 1}">
140
+ <a th:if="${leaveRequests.hasPrevious()}"
141
+ th:href="@{/attendance/leave/list(page=${leaveRequests.number - 1}, size=${leaveRequests.size}, status=${selectedStatus}, week=${selectedWeek})}"
142
+ class="btn btn-pagination">← Trước</a>
143
+
144
+ <span class="page-info">
145
+ <a th:each="i : ${#numbers.sequence(0, leaveRequests.totalPages - 1)}"
146
+ th:if="${i == leaveRequests.number}"
147
+ th:href="@{/attendance/leave/list(page=${i}, size=${leaveRequests.size}, status=${selectedStatus}, week=${selectedWeek})}"
148
+ class="page-link active" th:text="${i + 1}">1</a>
149
+ <a th:each="i : ${#numbers.sequence(0, leaveRequests.totalPages - 1)}"
150
+ th:unless="${i == leaveRequests.number}"
151
+ th:href="@{/attendance/leave/list(page=${i}, size=${leaveRequests.size}, status=${selectedStatus}, week=${selectedWeek})}"
152
+ class="page-link" th:text="${i + 1}">1</a>
153
+ </span>
154
+
155
+ <a th:if="${leaveRequests.hasNext()}"
156
+ th:href="@{/attendance/leave/list(page=${leaveRequests.number + 1}, size=${leaveRequests.size}, status=${selectedStatus}, week=${selectedWeek})}"
157
+ class="btn btn-pagination">Sau →</a>
158
+ </div>
159
+
160
+ <div id="absentDetailModal" class="absent-modal">
161
+
162
+ <div class="absent-modal-content">
163
+
164
+ <div class="absent-modal-header">
165
+ <h2>Chi tiết đơn nghỉ phép</h2>
166
+
167
+ <button class="close-btn" onclick="closeAbsentDetail()">
168
+
169
+ </button>
170
+ </div>
171
+
172
+ <div class="absent-modal-body">
173
+
174
+ <div class="detail-grid">
175
+
176
+ <div class="detail-item">
177
+ <label>Mã đơn</label>
178
+ <span id="detailId">#LV001</span>
179
+ </div>
180
+
181
+ <div class="detail-item">
182
+ <label>Tên đăng nhập</label>
183
+ <span id="detailUsername">--</span>
184
+ </div>
185
+
186
+ <div class="detail-item">
187
+ <label>Người gửi</label>
188
+ <span id="detailFullName">--</span>
189
+ </div>
190
+
191
+ <div class="detail-item">
192
+ <label>Từ ngày</label>
193
+ <span id="detailStartDate">--</span>
194
+ </div>
195
+
196
+ <div class="detail-item">
197
+ <label>Đến ngày</label>
198
+ <span id="detailEndDate">--</span>
199
+ </div>
200
+
201
+ <div class="detail-item">
202
+ <label>Số ngày nghỉ</label>
203
+ <span id="detailDays">--</span>
204
+ </div>
205
+
206
+ <div class="detail-item">
207
+ <label>Trạng thái</label>
208
+ <span id="detailStatus" class="status pending">
209
+ --
210
+ </span>
211
+ </div>
212
+
213
+ </div>
214
+
215
+ <div class="detail-reason">
216
+ <label>Lý do nghỉ phép</label>
217
+
218
+ <div id="detailReason" class="reason-box">
219
+ --
220
+ </div>
221
+ </div>
222
+
223
+ </div>
224
+
225
+ </div>
226
+
227
+ </div>
228
+ </main>
229
+ <!-- FOOTER -->
230
+ <div th:replace="~{cms/layout/footer :: footer}"></div>
231
+ </div>
232
+ </div>
233
+
234
+ <!-- DETAIL CARD -->
235
+
236
+ <script>
237
+ function openAbsentDetail() {
238
+ document
239
+ .getElementById("absentDetailModal")
240
+ .classList.add("show");
241
+ }
242
+
243
+ function closeAbsentDetail() {
244
+ document
245
+ .getElementById("absentDetailModal")
246
+ .classList.remove("show");
247
+ }
248
+
249
+ window.onclick = function (event) {
250
+
251
+ const modal =
252
+ document.getElementById("absentDetailModal");
253
+
254
+ if (event.target === modal) {
255
+ closeAbsentDetail();
256
+ }
257
+ };
258
+
259
+ document.querySelectorAll('.btn-view-detail').forEach(button => {
260
+ button.addEventListener('click', async function () {
261
+ const leaveId = this.getAttribute('data-id');
262
+ try {
263
+ const response = await fetch('/api/attendance/leave/detail/' + leaveId);
264
+ if (!response.ok) {
265
+ throw new Error('Không thể tải chi tiết đơn nghỉ phép');
266
+ }
267
+ const data = await response.json();
268
+
269
+ document.getElementById('detailId').textContent = '#' + data.id;
270
+ document.getElementById('detailUsername').textContent = data.username || '--';
271
+ document.getElementById('detailFullName').textContent = data.fullName || '--';
272
+ document.getElementById('detailStartDate').textContent = data.startDate || '--';
273
+ document.getElementById('detailEndDate').textContent = data.endDate || '--';
274
+
275
+ if (data.startDate && data.endDate) {
276
+ const start = new Date(data.startDate);
277
+ const end = new Date(data.endDate);
278
+ const days = Math.max(1, Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1);
279
+ document.getElementById('detailDays').textContent = days + ' ngày';
280
+ } else {
281
+ document.getElementById('detailDays').textContent = '--';
282
+ }
283
+
284
+ const statusEl = document.getElementById('detailStatus');
285
+ statusEl.textContent = data.status || '--';
286
+ statusEl.className = 'status ' + (data.status ? data.status.toLowerCase() : '');
287
+
288
+ document.getElementById('detailReason').textContent = data.reason || '--';
289
+
290
+ openAbsentDetail();
291
+ } catch (error) {
292
+ console.error(error);
293
+ alert(error.message || 'Có lỗi xảy ra, vui lòng thử lại');
294
+ }
295
+ });
296
+ });
297
+ </script>
298
+
299
+ </body>
300
+
301
+ </html>
src/main/resources/templates/cms/sidebar/sidebar-employee.html CHANGED
@@ -28,7 +28,12 @@
28
  Lịch sử điểm danh
29
  </a>
30
  </li>
31
-
 
 
 
 
 
32
  <li class="sidebar-option">
33
  <a href="/attendance/leave/create" class="sidebar-link">
34
  <span>📅</span>
 
28
  Lịch sử điểm danh
29
  </a>
30
  </li>
31
+ <li class="sidebar-option">
32
+ <a href="/attendance/leave/list" class="sidebar-link">
33
+ <span>📋</span>
34
+ Đơn nghỉ phép của tôi
35
+ </a>
36
+ </li>
37
  <li class="sidebar-option">
38
  <a href="/attendance/leave/create" class="sidebar-link">
39
  <span>📅</span>