huylaughmad commited on
Commit
17bca00
·
verified ·
1 Parent(s): e23733e

Upload 37 files

Browse files
index.html ADDED
@@ -0,0 +1,835 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Nha Khoa TTL - Hệ Thống Quản Lý Công Việc</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <script src="https://www.gstatic.com/firebasejs/9.6.0/firebase-app-compat.js"></script>
10
+ <script src="https://www.gstatic.com/firebasejs/9.6.0/firebase-firestore-compat.js"></script>
11
+ <script src="https://www.gstatic.com/firebasejs/9.6.0/firebase-auth-compat.js"></script>
12
+ <script src="https://www.gstatic.com/firebasejs/9.6.0/firebase-database-compat.js"></script>
13
+ <script src="https://www.gstatic.com/firebasejs/9.6.0/firebase-storage-compat.js"></script>
14
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>
15
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/locale/vi.min.js"></script>
16
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
17
+ <style>
18
+ /* Custom styles for better scrollbar and modal backdrop */
19
+ /* Desktop layout */
20
+ @media (min-width: 768px) {
21
+ body {
22
+ display: flex; /* Use flexbox for desktop layout */
23
+ }
24
+ }
25
+ body {
26
+ height: 100vh;
27
+ position: relative;
28
+ background-color: #f3f4f6; /* Light gray background */
29
+ }
30
+ /* Ẩn tất cả section */
31
+ .content-section {
32
+ display: none;
33
+ }
34
+ /* Chỉ show section được gắn .active */
35
+ .content-section.active,
36
+ .content-section:target { /* :target để hỗ trợ deep linking ban đầu nếu có */
37
+ display: block;
38
+ }
39
+ .modal {
40
+ display: none; /* Hidden by default */
41
+ position: fixed; /* Stay in place */
42
+ z-index: 1000; /* Sit on top */
43
+ left: 0;
44
+ top: 0;
45
+ width: 100%; /* Full width */
46
+ height: 100%; /* Full height */
47
+ overflow: auto; /* Enable scroll if needed */
48
+ background-color: rgba(0,0,0,0.5); /* Black w/ opacity */
49
+ justify-content: center; /* Center content horizontally */
50
+ align-items: center; /* Center content vertically */
51
+ padding: 1rem; /* Add some padding for smaller screens */
52
+ }
53
+ #confirmModal {
54
+ z-index: 1050; /* Ensure confirm modal is always on top of other modals */
55
+ }
56
+ .modal-content {
57
+ background-color: #fefefe;
58
+ margin: auto;
59
+ padding: 2rem;
60
+ border-radius: 0.5rem;
61
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
62
+ width: 90%; /* Responsive width */
63
+ max-width: 500px; /* Max width for larger screens */
64
+ }
65
+ @media (min-width: 768px) {
66
+ .modal-content {
67
+ width: 50%;
68
+ }
69
+ }
70
+ /* Custom scrollbar for kanban columns */
71
+ .kanban-column {
72
+ max-height: calc(100vh - 250px); /* Adjust based on header/footer height */
73
+ overflow-y: auto;
74
+ -ms-overflow-style: none; /* IE and Edge */
75
+ scrollbar-width: none; /* Firefox */
76
+ }
77
+ .kanban-column::-webkit-scrollbar {
78
+ display: none; /* Chrome, Safari, Opera*/
79
+ }
80
+ .dropdown-menu {
81
+ display: none;
82
+ }
83
+ .dropdown:hover .dropdown-menu {
84
+ display: block;
85
+ }
86
+ .admin-only {
87
+ display: none;
88
+ }
89
+ </style>
90
+ </head>
91
+ <body class="bg-gray-50">
92
+ <div id="react-sidebar-root">
93
+ </div>
94
+
95
+ <div class="flex h-screen overflow-hidden" id="main-content">
96
+ <div class="flex flex-col flex-1 overflow-hidden">
97
+ <div class="flex items-center justify-between h-16 px-6 bg-white border-b border-gray-200">
98
+ <button id="mobile-menu-button" class="md:hidden text-gray-500 focus:outline-none">
99
+ <i class="fas fa-bars"></i>
100
+ </button>
101
+ <div class="flex-1 max-w-md ml-4 md:ml-6">
102
+ <div class="relative">
103
+ <div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
104
+ <i class="fas fa-search text-gray-400"></i>
105
+ </div>
106
+ <input class="block w-full py-2 pl-10 pr-3 text-sm bg-gray-100 border border-transparent rounded-md focus:bg-white focus:border-gray-300 focus:ring-0" placeholder="Tìm kiếm công việc, nhân viên...">
107
+ </div>
108
+ </div>
109
+ <div class="flex items-center">
110
+ <div class="relative mr-4 dropdown">
111
+ <button class="flex items-center px-3 py-1 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500">
112
+ <i class="mr-2 fas fa-plus"></i> Tạo mới
113
+ </button>
114
+ <div class="absolute right-0 z-50 hidden w-48 py-1 mt-2 bg-white rounded-md shadow-lg dropdown-menu">
115
+ <a href="#" id="create-task-btn-top" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Công việc mới</a>
116
+ <a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Cuộc họp</a>
117
+ <a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Báo cáo</a>
118
+ </div>
119
+ </div>
120
+ <button class="relative p-1 text-gray-400 rounded-full hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500">
121
+ <span class="sr-only">Thông báo</span>
122
+ <i class="fas fa-bell"></i>
123
+ <span class="absolute top-0 right-0 w-2 h-2 bg-red-500 rounded-full notification-badge"></span>
124
+ </button>
125
+ <button class="relative p-1 ml-4 text-gray-400 rounded-full hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500">
126
+ <span class="sr-only">Tin nhắn</span>
127
+ <i class="fas fa-envelope"></i>
128
+ <span class="absolute top-0 right-0 w-2 h-2 bg-blue-500 rounded-full notification-badge"></span>
129
+ </button>
130
+ <div class="relative ml-4 dropdown">
131
+ <button class="flex items-center max-w-xs text-sm rounded-full focus:outline-none focus:ring-2 focus:ring-blue-500">
132
+ <span class="sr-only">Mở menu người dùng</span>
133
+ <img id="user-dropdown-avatar" class="w-8 h-8 rounded-full" src="https://ui-avatars.com/api/?name=User&background=3b82f6&color=fff" alt="User">
134
+ </button>
135
+ <div class="absolute right-0 z-50 hidden w-48 py-1 mt-2 bg-white rounded-md shadow-lg dropdown-menu">
136
+ <a href="#" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Hồ sơ</a>
137
+ <a href="#" id="changePasswordBtn" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Đổi mật khẩu</a>
138
+ <a href="#" id="logoutButton" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">Đăng xuất</a>
139
+ </div>
140
+ </div>
141
+ </div>
142
+ </div>
143
+ <div class="flex-1 overflow-auto p-6 bg-gray-50">
144
+ <div id="dashboard-section" class="content-section active">
145
+ <div class="flex flex-col mb-6 md:flex-row md:items-center md:justify-between">
146
+ <div>
147
+ <h1 class="text-2xl font-bold text-gray-900">Dashboard</h1>
148
+ <p class="text-gray-600">Tổng quan công việc và hiệu suất toàn hệ thống</p>
149
+ </div>
150
+ <div class="mt-4 md:mt-0">
151
+ <select class="text-sm border-gray-300 rounded-md focus:border-blue-500 focus:ring-blue-500">
152
+ <option>Hôm nay</option>
153
+ <option>Tuần này</option>
154
+ <option>Tháng này</option>
155
+ <option>Quý này</option>
156
+ </select>
157
+ </div>
158
+ </div>
159
+ <div class="grid grid-cols-1 gap-6 mb-6 sm:grid-cols-2 lg:grid-cols-4" id="stats-cards-container">
160
+ <div class="p-6 bg-white rounded-lg shadow">
161
+ <div class="flex items-center">
162
+ <div class="p-3 bg-blue-100 rounded-full">
163
+ <i class="text-blue-600 fas fa-tasks"></i>
164
+ </div>
165
+ <div class="ml-4">
166
+ <p class="text-sm font-medium text-gray-500">Tổng công việc</p>
167
+ <p id="totalTasks" class="text-2xl font-semibold text-gray-900">0</p>
168
+ <p class="text-xs text-gray-500 mt-1"><span class="text-green-500">+0%</span> so với tuần trước</p>
169
+ </div>
170
+ </div>
171
+ </div>
172
+ <div class="p-6 bg-white rounded-lg shadow">
173
+ <div class="flex items-center">
174
+ <div class="p-3 bg-green-100 rounded-full">
175
+ <i class="text-green-600 fas fa-check-circle"></i>
176
+ </div>
177
+ <div class="ml-4">
178
+ <p class="text-sm font-medium text-gray-500">Đã hoàn thành</p>
179
+ <p id="completedTasks" class="text-2xl font-semibold text-gray-900">0</p>
180
+ <p class="text-xs text-gray-500 mt-1"><span class="text-green-500">+0%</span> hiệu suất</p>
181
+ </div>
182
+ </div>
183
+ </div>
184
+ <div class="p-6 bg-white rounded-lg shadow">
185
+ <div class="flex items-center">
186
+ <div class="p-3 bg-yellow-100 rounded-full">
187
+ <i class="text-yellow-600 fas fa-exclamation-circle"></i>
188
+ </div>
189
+ <div class="ml-4">
190
+ <p class="text-sm font-medium text-gray-500">Đang chờ</p>
191
+ <p id="pendingTasks" class="text-2xl font-semibold text-gray-900">0</p>
192
+ <p class="text-xs text-gray-500 mt-1"><span class="text-red-500">-0%</span> so với tuần trước</p>
193
+ </div>
194
+ </div>
195
+ </div>
196
+ <div class="p-6 bg-white rounded-lg shadow">
197
+ <div class="flex items-center">
198
+ <div class="p-3 bg-red-100 rounded-full">
199
+ <i class="text-red-600 fas fa-clock"></i>
200
+ </div>
201
+ <div class="ml-4">
202
+ <p class="text-sm font-medium text-gray-500">Quá hạn</p>
203
+ <p id="overdueTasks" class="text-2xl font-semibold text-gray-900">0</p>
204
+ <p class="text-xs text-gray-500 mt-1"><span class="text-green-500">-0%</span> so với tuần trước</p>
205
+ </div>
206
+ </div>
207
+ </div>
208
+ </div>
209
+ <div class="grid grid-cols-1 gap-6 lg:grid-cols-3">
210
+ <div class="lg:col-span-2">
211
+ <div class="p-6 mb-6 bg-white rounded-lg shadow">
212
+ <div class="flex items-center justify-between mb-4">
213
+ <h2 class="text-lg font-medium text-gray-900">Trạng thái công việc theo phòng ban</h2>
214
+ <select class="text-sm border-gray-300 rounded-md focus:border-blue-500 focus:ring-blue-500">
215
+ <option>Tuần này</option>
216
+ <option>Tháng này</option>
217
+ <option>Quý này</option>
218
+ </select>
219
+ </div>
220
+ <div class="chart-container" style="height: 300px;">
221
+ <canvas id="taskStatusByDepartmentChart"></canvas>
222
+ </div>
223
+ </div>
224
+ <div class="p-6 bg-white rounded-lg shadow">
225
+ <div class="flex items-center justify-between mb-4">
226
+ <h2 class="text-lg font-medium text-gray-900">Hiệu suất phòng ban</h2>
227
+ <a href="#" class="text-sm font-medium text-blue-600 hover:text-blue-500">Xem chi tiết</a>
228
+ </div>
229
+ <div id="departmentPerformanceList" class="space-y-4">
230
+ </div>
231
+ </div>
232
+ </div>
233
+ <div>
234
+ <div class="p-6 mb-6 bg-white rounded-lg shadow">
235
+ <h2 class="text-lg font-medium text-gray-900 mb-4">Giao việc nhanh</h2>
236
+ <form id="quick-assign-form">
237
+ <div class="mb-4">
238
+ <label for="assign-department" class="block text-sm font-medium text-gray-700 mb-1">Phòng ban</label>
239
+ <select id="assign-department" class="block w-full text-sm border-gray-300 rounded-md focus:border-blue-500 focus:ring-blue-500">
240
+ <option value="">Chọn phòng ban</option>
241
+ </select>
242
+ </div>
243
+ <div class="mb-4">
244
+ <label for="assign-employee" class="block text-sm font-medium text-gray-700 mb-1">Nhân viên</label>
245
+ <select id="assign-employee" class="block w-full text-sm border-gray-300 rounded-md focus:border-blue-500 focus:ring-blue-500">
246
+ <option value="">Chọn nhân viên</option>
247
+ </select>
248
+ </div>
249
+ <div class="mb-4">
250
+ <label for="assign-task-title" class="block text-sm font-medium text-gray-700 mb-1">Công việc</label>
251
+ <input type="text" id="assign-task-title" class="block w-full text-sm border-gray-300 rounded-md focus:border-blue-500 focus:ring-blue-500" placeholder="Mô tả công việc" required>
252
+ </div>
253
+ <div class="mb-4">
254
+ <label for="assign-due-date" class="block text-sm font-medium text-gray-700 mb-1">Hạn chót</label>
255
+ <input type="date" id="assign-due-date" class="block w-full text-sm border-gray-300 rounded-md focus:border-blue-500 focus:ring-blue-500" required>
256
+ </div>
257
+ <button type="submit" class="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500">
258
+ Giao việc
259
+ </button>
260
+ </form>
261
+ </div>
262
+ <div class="p-6 bg-white rounded-lg shadow">
263
+ <h2 class="text-lg font-medium text-gray-900 mb-4">Hoạt động gần đây</h2>
264
+ <div id="recentActivitiesList" class="space-y-4">
265
+ </div>
266
+ <a href="#" class="block mt-4 text-sm font-medium text-blue-600 hover:text-blue-500 text-center">Xem tất cả</a>
267
+ </div>
268
+ </div>
269
+ </div>
270
+ <div class="mt-8">
271
+ <div class="flex items-center justify-between mb-6">
272
+ <h2 class="text-xl font-bold text-gray-900">Bảng Kanban Công việc (Tổng quan)</h2>
273
+ <div class="flex space-x-2">
274
+ <button class="px-3 py-1 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500">
275
+ <i class="fas fa-plus mr-1"></i> Tạo công việc
276
+ </button>
277
+ </div>
278
+ </div>
279
+ </div>
280
+ <div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4" id="kanban-board-dashboard">
281
+ <div id="todoTasks-dashboard" class="p-4 bg-gray-100 rounded-lg kanban-column">
282
+ <div class="flex items-center justify-between mb-4">
283
+ <h3 class="font-medium text-gray-700">Cần làm</h3>
284
+ <span id="todo-count-dashboard" class="px-2 py-1 text-xs font-medium text-gray-700 bg-gray-200 rounded-full">0</span>
285
+ </div>
286
+ <div id="kanban-todo-dashboard" class="space-y-4">
287
+ </div>
288
+ </div>
289
+ <div id="inProgressTasks-dashboard" class="p-4 bg-gray-100 rounded-lg kanban-column">
290
+ <div class="flex items-center justify-between mb-4">
291
+ <h3 class="font-medium text-gray-700">Đang thực hiện</h3>
292
+ <span id="in-progress-count-dashboard" class="px-2 py-1 text-xs font-medium text-gray-700 bg-gray-200 rounded-full">0</span>
293
+ </div>
294
+ <div id="kanban-in-progress-dashboard" class="space-y-4">
295
+ </div>
296
+ </div>
297
+ <div id="reviewTasks-dashboard" class="p-4 bg-gray-100 rounded-lg kanban-column">
298
+ <div class="flex items-center justify-between mb-4">
299
+ <h3 class="font-medium text-gray-700">Chờ xét duyệt</h3>
300
+ <span id="review-count-dashboard" class="px-2 py-1 text-xs font-medium text-gray-700 bg-gray-200 rounded-full">0</span>
301
+ </div>
302
+ <div id="kanban-review-dashboard" class="space-y-4">
303
+ </div>
304
+ </div>
305
+ <div id="completedTasksBoard-dashboard" class="p-4 bg-gray-100 rounded-lg kanban-column">
306
+ <div class="flex items-center justify-between mb-4">
307
+ <h3 class="font-medium text-gray-700">Hoàn thành</h3>
308
+ <span id="done-count-dashboard" class="px-2 py-1 text-xs font-medium text-gray-700 bg-gray-200 rounded-full">0</span>
309
+ </div>
310
+ <div id="kanban-done-dashboard" class="space-y-4">
311
+ </div>
312
+ </div>
313
+ </div>
314
+
315
+ <div class="mt-8">
316
+ <div class="flex items-center justify-between mb-6">
317
+ <h2 class="text-xl font-bold text-gray-900">Đánh giá hiệu suất nhân viên</h2>
318
+ <select class="text-sm border-gray-300 rounded-md focus:border-blue-500 focus:ring-blue-500">
319
+ <option>Tháng 10/2023</option>
320
+ <option>Tháng 9/2023</option>
321
+ <option>Tháng 8/2023</option>
322
+ </select>
323
+ </div>
324
+ <div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
325
+ <div class="p-6 bg-white rounded-lg shadow department-card">
326
+ <div class="flex flex-col items-center">
327
+ <img class="w-16 h-16 rounded-full mb-3" src="https://ui-avatars.com/api/?name=Nguyễn+Văn+A&background=3b82f6&color=fff" alt="User">
328
+ <h3 class="text-lg font-medium text-gray-900">Nguyễn Văn A</h3>
329
+ <p class="text-sm text-gray-500 mb-4">Quản lý kho</p>
330
+ <div class="relative performance-meter" style="width: 100px; height: 100px;">
331
+ <svg class="w-full h-full" viewBox="0 0 36 36">
332
+ <path d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
333
+ fill="none" stroke="#eee" stroke-width="3" stroke-dasharray="100, 100" />
334
+ <path d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
335
+ fill="none" stroke="#3b82f6" stroke-width="3" stroke-dasharray="85, 100" />
336
+ </svg>
337
+ <div class="absolute inset-0 flex items-center justify-center">
338
+ <span class="text-2xl font-bold text-gray-900">85%</span>
339
+ </div>
340
+ </div>
341
+ <div class="mt-4 text-center">
342
+ <p class="text-sm text-gray-500">Hoàn thành: <span class="font-medium text-gray-900">17/20</span> công việc</p>
343
+ <p class="text-sm text-gray-500">Đúng hạn: <span class="font-medium text-gray-900">15</span> công việc</p>
344
+ </div>
345
+ </div>
346
+ </div>
347
+ <div class="p-6 bg-white rounded-lg shadow department-card">
348
+ <div class="flex flex-col items-center">
349
+ <img class="w-16 h-16 rounded-full mb-3" src="https://ui-avatars.com/api/?name=Trần+Thị+B&background=10b981&color=fff" alt="User">
350
+ <h3 class="text-lg font-medium text-gray-900">Trần Thị B</h3>
351
+ <p class="text-sm text-gray-500 mb-4">Kế toán</p>
352
+ <div class="relative performance-meter" style="width: 100px; height: 100px;">
353
+ <svg class="w-full h-full" viewBox="0 0 36 36">
354
+ <path d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
355
+ fill="none" stroke="#eee" stroke-width="3" stroke-dasharray="100, 100" />
356
+ <path d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
357
+ fill="none" stroke="#10b981" stroke-width="3" stroke-dasharray="92, 100" />
358
+ </svg>
359
+ <div class="absolute inset-0 flex items-center justify-center">
360
+ <span class="text-2xl font-bold text-gray-900">92%</span>
361
+ </div>
362
+ </div>
363
+ <div class="mt-4 text-center">
364
+ <p class="text-sm text-gray-500">Hoàn thành: <span class="font-medium text-gray-900">23/25</span> công việc</p>
365
+ <p class="text-sm text-gray-500">Đúng hạn: <span class="font-medium text-gray-900">22</span> công việc</p>
366
+ </div>
367
+ </div>
368
+ </div>
369
+ </div>
370
+ </div>
371
+ </div>
372
+ <div id="tasks-section" class="content-section">
373
+ <div class="mt-8">
374
+ <div class="flex items-center justify-between mb-6">
375
+ <h2 class="text-xl font-bold text-gray-900">Bảng Kanban Công việc</h2>
376
+ <div class="flex space-x-2">
377
+ <button id="openAddTaskModal" class="px-3 py-1 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500">
378
+ <i class="fas fa-plus mr-1"></i> Tạo công việc
379
+ </button>
380
+ <button id="filterTasksBtn" class="px-3 py-1 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500">
381
+ <i class="fas fa-filter mr-1"></i> Lọc
382
+ </button>
383
+ </div>
384
+ </div>
385
+ </div>
386
+ <div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4" id="kanban-board">
387
+ <div id="todoTasks" class="p-4 bg-gray-100 rounded-lg kanban-column">
388
+ <div class="flex items-center justify-between mb-4" id="kanban-todo-tasks-header">
389
+ <h3 class="font-medium text-gray-700">Cần làm</h3>
390
+ <span id="todo-count" class="px-2 py-1 text-xs font-medium text-gray-700 bg-gray-200 rounded-full">0</span>
391
+ </div>
392
+ <div id="kanban-todo" class="space-y-4">
393
+ </div>
394
+ </div>
395
+ <div id="inProgressTasks" class="p-4 bg-gray-100 rounded-lg kanban-column">
396
+ <div class="flex items-center justify-between mb-4" id="kanban-in-progress-tasks-header">
397
+ <h3 class="font-medium text-gray-700">Đang thực hiện</h3>
398
+ <span id="in-progress-count" class="px-2 py-1 text-xs font-medium text-gray-700 bg-gray-200 rounded-full">0</span>
399
+ </div>
400
+ <div id="kanban-in-progress" class="space-y-4">
401
+ </div>
402
+ </div>
403
+ <div id="reviewTasks" class="p-4 bg-gray-100 rounded-lg kanban-column">
404
+ <div class="flex items-center justify-between mb-4" id="kanban-review-tasks-header">
405
+ <h3 class="font-medium text-gray-700">Chờ xét duyệt</h3>
406
+ <span id="review-count" class="px-2 py-1 text-xs font-medium text-gray-700 bg-gray-200 rounded-full">0</span>
407
+ </div>
408
+ <div id="kanban-review" class="space-y-4">
409
+ </div>
410
+ </div>
411
+ <div id="completedTasksBoard" class="p-4 bg-gray-100 rounded-lg kanban-column">
412
+ <div class="flex items-center justify-between mb-4" id="kanban-done-tasks-header">
413
+ <h3 class="font-medium text-gray-700">Hoàn thành</h3>
414
+ <span id="done-count" class="px-2 py-1 text-xs font-medium text-gray-700 bg-gray-200 rounded-full">0</span>
415
+ </div>
416
+ <div id="kanban-done" class="space-y-4">
417
+ </div>
418
+ </div>
419
+ </div>
420
+ </div>
421
+ <div id="personnel-section" class="content-section">
422
+ <div class="mt-8">
423
+ <div class="flex items-center justify-between mb-6">
424
+ <h2 class="text-xl font-bold text-gray-900">Quản lý Nhân sự</h2>
425
+ <select class="text-sm border-gray-300 rounded-md focus:border-blue-500 focus:ring-blue-500">
426
+ <option>Tháng 10/2023</option>
427
+ <option>Tháng 9/2023</option>
428
+ <option>Tháng 8/2023</option>
429
+ </select>
430
+ </div>
431
+ <div id="employeePerformanceList" class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4">
432
+ </div>
433
+ </div>
434
+ <div id="users-list-container" class="mt-8">
435
+ <h2 class="text-xl font-bold text-gray-900 mb-4">Danh sách Người dùng (<span id="totalUsers">0</span>)</h2>
436
+ <div class="flex justify-end mb-4 admin-only"> <button id="openAddUserModal" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition duration-200">
437
+ <i class="fas fa-user-plus mr-2"></i> Thêm người dùng mới
438
+ </button>
439
+ </div>
440
+ <div class="p-6 bg-white rounded-lg shadow">
441
+ <ul id="usersList" class="divide-y divide-gray-200">
442
+ </ul>
443
+ </div>
444
+ </div>
445
+ </div>
446
+ <div id="departments-section" class="content-section">
447
+ <div class="mt-8">
448
+ <h2 class="text-2xl font-bold text-gray-900 mb-4">Quản lý Phòng ban</h2>
449
+ <div class="flex justify-end mb-4 admin-only"> <button id="openAddDepartmentModal" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 transition duration-200">
450
+ <i class="fas fa-plus mr-2"></i> Thêm phòng ban mới
451
+ </button>
452
+ </div>
453
+ <div class="p-6 bg-white rounded-lg shadow">
454
+ <h3 class="text-lg font-medium text-gray-900 mb-4">Danh sách Phòng ban</h3>
455
+ <ul id="departmentsList" class="divide-y divide-gray-200">
456
+ </ul>
457
+ </div>
458
+ </div>
459
+ </div>
460
+ <div id="reports-section" class="content-section">
461
+ <h1 class="text-2xl font-bold text-gray-900 mb-4">Báo cáo & Thống kê</h1>
462
+ <p class="text-gray-600 mb-6">Tổng hợp các báo cáo hiệu suất và hoạt động.</p>
463
+ <div class="p-6 bg-white rounded-lg shadow">
464
+ <h3 class="text-lg font-medium text-gray-900 mb-4">Báo cáo tổng quan</h3>
465
+ <p class="text-gray-700">Nội dung báo cáo sẽ được hiển thị tại đây.</p>
466
+ </div>
467
+ </div>
468
+ <div id="calendar-section" class="content-section">
469
+ <h1 class="text-2xl font-bold text-gray-900 mb-4">Lịch làm việc</h1>
470
+ <p class="text-gray-600 mb-6">Quản lý lịch làm việc và các sự kiện.</p>
471
+ <div class="p-6 bg-white rounded-lg shadow">
472
+ <h3 class="text-lg font-medium text-gray-900 mb-4">Lịch biểu</h3>
473
+ <p class="text-gray-700">Nội dung lịch làm việc sẽ được hiển thị tại đây.</p>
474
+ </div>
475
+ </div>
476
+ <div id="settings-section" class="content-section">
477
+ <h1 class="text-2xl font-bold text-gray-900 mb-4">Cài đặt hệ thống</h1>
478
+ <p class="text-gray-600 mb-6">Cấu hình các tùy chọn của ứng dụng.</p>
479
+ <div class="p-6 bg-white rounded-lg shadow">
480
+ <h3 class="text-lg font-medium text-gray-900 mb-4">Tùy chọn chung</h3>
481
+ <p class="text-gray-700">Nội dung cài đặt sẽ được hiển thị tại đây.</p>
482
+ </div>
483
+ </div>
484
+ </div>
485
+ </div>
486
+ </div>
487
+
488
+ <div id="loginModal" class="modal">
489
+ <div class="modal-content">
490
+ <h3 class="text-2xl font-bold text-center text-gray-900 mb-6">Đăng nhập</h3>
491
+ <form id="loginForm" class="space-y-4">
492
+ <div>
493
+ <label for="login-email" class="sr-only">Email</label>
494
+ <input type="email" id="login-email" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" placeholder="Email" required>
495
+ </div>
496
+ <div>
497
+ <label for="login-password" class="sr-only">Mật khẩu</label>
498
+ <input type="password" id="login-password" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" placeholder="Mật khẩu" required>
499
+ </div>
500
+ <p id="login-error-message" class="text-red-500 text-sm text-center hidden"></p>
501
+ <button type="submit" id="vanillaLoginButton" class="w-full bg-blue-600 text-white py-2 rounded-md hover:bg-blue-700 transition duration-200">Đăng nhập</button>
502
+ </form>
503
+ <div class="mt-4 text-center">
504
+ <a href="#" id="showResetPassword" class="text-blue-600 hover:underline text-sm">Quên mật khẩu?</a>
505
+ <p class="text-sm text-gray-600 mt-2">Chưa có tài khoản? <a href="#" id="showRegister" class="text-blue-600 hover:underline">Đăng ký ngay</a></p>
506
+ </div>
507
+ </div>
508
+ </div>
509
+ <div id="registerModal" class="modal">
510
+ <div class="modal-content">
511
+ <h3 class="text-2xl font-bold text-center text-gray-900 mb-6">Đăng ký</h3>
512
+ <form id="registerForm" class="space-y-4">
513
+ <div>
514
+ <label for="register-name" class="sr-only">Tên của bạn</label>
515
+ <input type="text" id="register-name" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" placeholder="Tên của bạn" required>
516
+ </div>
517
+ <div>
518
+ <label for="register-email" class="sr-only">Email</label>
519
+ <input type="email" id="register-email" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" placeholder="Email" required>
520
+ </div>
521
+ <div>
522
+ <label for="register-password" class="sr-only">Mật khẩu</label>
523
+ <input type="password" id="register-password" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" placeholder="Mật khẩu (ít nhất 6 ký tự)" required>
524
+ </div>
525
+ <button type="submit" id="registerButton" class="w-full bg-green-600 text-white py-2 rounded-md hover:bg-green-700 transition duration-200">Đăng ký</button>
526
+ </form>
527
+ <div class="mt-4 text-center">
528
+ <p class="text-sm text-gray-600">Đã có tài khoản? <a href="#" id="showLoginFromRegister" class="text-blue-600 hover:underline close-modal">Đăng nhập</a></p>
529
+ </div>
530
+ </div>
531
+ </div>
532
+ <div id="resetPasswordModal" class="modal">
533
+ <div class="modal-content">
534
+ <h3 class="text-2xl font-bold text-center text-gray-900 mb-6">Đặt lại mật khẩu</h3>
535
+ <form id="resetPasswordForm" class="space-y-4">
536
+ <div>
537
+ <label for="reset-email" class="sr-only">Email</label>
538
+ <input type="email" id="reset-email" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" placeholder="Nhập email của bạn" required>
539
+ </div>
540
+ <button type="submit" id="resetPasswordSubmit" class="w-full bg-blue-600 text-white py-2 rounded-md hover:bg-blue-700 transition duration-200">Gửi liên kết đặt lại</button>
541
+ </form>
542
+ <div class="mt-4 text-center">
543
+ <p class="text-sm text-gray-600">Quay lại <a href="#" id="showLoginFromReset" class="text-blue-600 hover:underline close-modal">Đăng nhập</a></p>
544
+ </div>
545
+ </div>
546
+ </div>
547
+ <div id="changePasswordModal" class="modal">
548
+ <div class="modal-content">
549
+ <h3 class="text-2xl font-bold text-center text-gray-900 mb-6">Đổi mật khẩu</h3>
550
+ <form id="changePasswordForm" class="space-y-4">
551
+ <div>
552
+ <label for="new-password" class="sr-only">Mật khẩu mới</label>
553
+ <input type="password" id="new-password" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" placeholder="Mật khẩu mới (ít nhất 6 ký tự)" required>
554
+ </div>
555
+ <div>
556
+ <label for="confirm-new-password" class="sr-only">Xác nhận mật khẩu mới</label>
557
+ <input type="password" id="confirm-new-password" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" placeholder="Xác nhận mật khẩu mới" required>
558
+ </div>
559
+ <button type="submit" id="updatePasswordSubmit" class="w-full bg-blue-600 text-white py-2 rounded-md hover:bg-blue-700 transition duration-200">Cập nhật mật khẩu</button>
560
+ </form>
561
+ <div class="mt-4 text-center">
562
+ <button type="button" class="text-gray-600 hover:underline text-sm close-modal">Hủy</button>
563
+ </div>
564
+ </div>
565
+ </div>
566
+ <div id="messageModal" class="modal">
567
+ <div class="modal-content">
568
+ <h3 id="messageModalTitle" class="text-xl font-semibold text-gray-900 mb-4"></h3>
569
+ <p id="messageModalContent" class="text-gray-700 mb-6"></p>
570
+ <div class="flex justify-end">
571
+ <button type="button" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700 close-modal">Đóng</button>
572
+ </div>
573
+ </div>
574
+ </div>
575
+ <div id="confirmModal" class="modal">
576
+ <div class="modal-content">
577
+ <h3 id="confirmModalTitle" class="text-xl font-semibold text-gray-900 mb-4">Xác nhận</h3>
578
+ <p id="confirmModalContent" class="text-gray-700 mb-6"></p>
579
+ <div class="flex justify-end space-x-3">
580
+ <button type="button" id="confirmCancelBtn" class="px-4 py-2 text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300 close-modal">Hủy</button>
581
+ <button type="button" id="confirmProceedBtn" class="px-4 py-2 text-white bg-red-600 rounded-md hover:bg-red-700">Xác nhận</button>
582
+ </div>
583
+ </div>
584
+ </div>
585
+ <div id="addDepartmentModal" class="modal">
586
+ <div class="modal-content">
587
+ <h3 class="text-2xl font-bold text-center text-gray-900 mb-6">Thêm phòng ban</h3>
588
+ <form id="addDepartmentForm" class="space-y-4">
589
+ <div>
590
+ <label for="departmentName" class="block text-sm font-medium text-gray-700">Tên phòng ban</label>
591
+ <input type="text" id="departmentName" class="mt-1 w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" required>
592
+ </div>
593
+ <div class="flex justify-end space-x-3">
594
+ <button type="button" class="px-4 py-2 text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300 close-modal">Hủy</button>
595
+ <button type="submit" id="saveDepartmentBtn" class="px-4 py-2 text-white bg-blue-600 rounded-md hover:bg-blue-700">Lưu</button>
596
+ </div>
597
+ </form>
598
+ </div>
599
+ </div>
600
+ <div id="editDepartmentModal" class="modal">
601
+ <div class="modal-content">
602
+ <h3 class="text-2xl font-bold text-center text-gray-900 mb-6">Sửa phòng ban</h3>
603
+ <form id="editDepartmentForm" class="space-y-4">
604
+ <input type="hidden" id="editDepartmentId">
605
+ <div>
606
+ <label for="editDepartmentName" class="block text-sm font-medium text-gray-700">Tên phòng ban</label>
607
+ <input type="text" id="editDepartmentName" class="mt-1 w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" required>
608
+ </div>
609
+ <div class="flex justify-end space-x-3">
610
+ <button type="button" class="px-4 py-2 text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300 close-modal">Hủy</button>
611
+ <button type="submit" id="updateDepartmentBtn" class="px-4 py-2 text-white bg-blue-600 rounded-md hover:bg-blue-700">Cập nhật</button>
612
+ </div>
613
+ </form>
614
+ </div>
615
+ </div>
616
+ <div id="addUserModal" class="modal">
617
+ <div class="modal-content">
618
+ <h3 id="userModalTitle" class="text-2xl font-bold text-center text-gray-900 mb-6">Thêm người dùng</h3>
619
+ <form id="addUserForm" class="space-y-4">
620
+ <div>
621
+ <label for="addUserName" class="block text-sm font-medium text-gray-700">Tên người dùng</label>
622
+ <input type="text" id="addUserName" name="userName" class="mt-1 w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" required>
623
+ </div>
624
+ <div>
625
+ <label for="addUserEmail" class="block text-sm font-medium text-gray-700">Email</label>
626
+ <input type="email" id="addUserEmail" class="mt-1 w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" required>
627
+ </div>
628
+ <div>
629
+ <label for="addUserPassword" class="block text-sm font-medium text-gray-700">Mật khẩu</label>
630
+ <input type="password" id="addUserPassword" name="addUserPassword" class="mt-1 w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" placeholder="Ít nhất 6 ký tự" required>
631
+ </div>
632
+ <div>
633
+ <label for="addUserRole" class="block text-sm font-medium text-gray-700">Vai trò</label>
634
+ <select id="addUserRole" name="addUserRole" class="mt-1 w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500">
635
+ <option value="employee">Nhân viên</option>
636
+ <option value="admin" class="admin-only">Quản trị viên</option>
637
+ </select>
638
+ </div>
639
+ <div>
640
+ <label for="addUserDepartment" class="block text-sm font-medium text-gray-700">Phòng ban</label>
641
+ <select id="addUserDepartment" name="department" class="mt-1 w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500">
642
+ </select>
643
+ </div>
644
+ <div class="flex justify-end space-x-3">
645
+ <button type="button" class="px-4 py-2 text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300 close-modal">Hủy</button>
646
+ <button type="submit" id="saveUserBtn" class="px-4 py-2 text-white bg-blue-600 rounded-md hover:bg-blue-700">Lưu</button>
647
+ </div>
648
+ </form>
649
+ </div>
650
+ </div>
651
+ <div id="editUserModal" class="modal">
652
+ <div class="modal-content">
653
+ <h3 id="editUserModalLabel" class="text-2xl font-bold text-center text-gray-900 mb-6">Sửa người dùng</h3>
654
+ <form id="editUserForm" class="space-y-4">
655
+ <input type="hidden" id="editUserId">
656
+ <div>
657
+ <label for="editUserName" class="block text-sm font-medium text-gray-700">Tên người dùng</label>
658
+ <input type="text" id="editUserName" class="mt-1 w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" required>
659
+ </div>
660
+ <div>
661
+ <label for="editUserEmail" class="block text-sm font-medium text-gray-700">Email</label>
662
+ <input type="email" id="editUserEmail" class="mt-1 w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" required>
663
+ </div>
664
+ <div>
665
+ <label for="editUserRole" class="block text-sm font-medium text-gray-700">Vai trò</label>
666
+ <select id="editUserRole" class="mt-1 w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500">
667
+ <option value="employee">Nhân viên</option>
668
+ <option value="admin">Quản trị viên</option>
669
+ </select>
670
+ </div>
671
+ <div>
672
+ <label for="editUserDepartment" class="block text-sm font-medium text-gray-700">Phòng ban</label>
673
+ <select id="editUserDepartment" class="mt-1 w-full px-4 py-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500">
674
+ </select>
675
+ </div>
676
+ <div class="flex justify-end space-x-3">
677
+ <button type="button" class="px-4 py-2 text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300 close-modal">Hủy</button>
678
+ <button type="submit" id="updateUserBtn" class="px-4 py-2 text-white bg-blue-600 rounded-md hover:bg-blue-700">Cập nhật</button>
679
+ </div>
680
+ </form>
681
+ </div>
682
+ </div>
683
+ <div id="addTaskModal" class="modal">
684
+ <div class="modal-content">
685
+ <h3 class="text-2xl font-bold text-center text-gray-900 mb-6" id="taskModalTitle">Thêm công việc</h3>
686
+ <form id="taskForm" class="space-y-4">
687
+ <input type="hidden" id="taskId">
688
+ <div>
689
+ <label for="taskTitle" class="block text-sm font-medium text-gray-700">Tiêu đề công việc</label>
690
+ <input id="taskTitle" name="taskTitle" type="text" class="w-full p-2 border rounded-md mt-1 focus:ring-blue-500 focus:border-blue-500" required>
691
+ </div>
692
+ <div>
693
+ <label for="taskDescription" class="block text-sm font-medium text-gray-700">Mô tả</label>
694
+ <textarea id="taskDescription" name="taskDescription" rows="3" class="w-full p-2 border rounded-md mt-1 focus:ring-blue-500 focus:border-blue-500"></textarea>
695
+ </div>
696
+ <div>
697
+ <label for="taskDepartment" class="block text-sm font-medium text-gray-700">Phòng ban</label>
698
+ <select id="taskDepartment" class="w-full p-2 border rounded-md mt-1 focus:ring-blue-500 focus:border-blue-500" required>
699
+ </select>
700
+ </div>
701
+ <div>
702
+ <label for="taskAssignedTo" class="block text-sm font-medium text-gray-700">Người thực hiện</label>
703
+ <select id="taskAssignedTo" class="w-full p-2 border rounded-md mt-1 focus:ring-blue-500 focus:border-blue-500" required>
704
+ </select>
705
+ </div>
706
+ <div>
707
+ <label for="taskPriority" class="block text-sm font-medium text-gray-700">Mức độ ưu tiên</label>
708
+ <select id="taskPriority" class="w-full p-2 border rounded-md mt-1 focus:ring-blue-500 focus:border-blue-500">
709
+ <option value="Cao">Cao</option>
710
+ <option value="Trung bình" selected>Trung bình</option>
711
+ <option value="Thấp">Thấp</option>
712
+ </select>
713
+ </div>
714
+ <div>
715
+ <label for="taskProgress" class="block text-sm font-medium text-gray-700">Tiến độ (%)</label>
716
+ <input id="taskProgress" type="number" min="0" max="100" value="0" class="w-full p-2 border rounded-md mt-1 focus:ring-blue-500 focus:border-blue-500">
717
+ </div>
718
+ <div>
719
+ <label for="taskDueDate" class="block text-sm font-medium text-gray-700">Ngày hết hạn</label>
720
+ <input id="taskDueDate" type="date" class="w-full p-2 border rounded-md mt-1 focus:ring-blue-500 focus:border-blue-500">
721
+ </div>
722
+ <div>
723
+ <label for="taskStatus" class="block text-sm font-medium text-gray-700">Trạng thái</label>
724
+ <select id="taskStatus" name="taskStatus" class="w-full p-2 border rounded-md mt-1 focus:ring-blue-500 focus:border-blue-500">
725
+ <option value="Cần làm">Cần làm</option>
726
+ <option value="Đang tiến hành">Đang tiến hành</option>
727
+ <option value="Đã duyệt">Đã duyệt</option>
728
+ <option value="Từ chối">Từ chối</option>
729
+ <option value="Hoàn thành">Hoàn thành</option>
730
+ </select>
731
+ </div>
732
+ <div class="flex justify-end space-x-3 mt-6">
733
+ <button type="button" id="deleteTaskBtn" class="hidden px-4 py-2 text-white bg-red-600 rounded-md hover:bg-red-700">Xóa</button>
734
+ <button type="button" class="px-4 py-2 text-gray-700 bg-gray-200 rounded-md hover:bg-gray-300 close-modal">Hủy</button>
735
+ <button type="submit" id="saveTaskBtn" class="px-4 py-2 text-white bg-blue-600 rounded-md hover:bg-blue-700">Lưu</button>
736
+ </div>
737
+ </form>
738
+ </div>
739
+ </div>
740
+ <div id="taskDetailModal" class="modal">
741
+ <div class="modal-content w-full max-w-7xl max-h-[90vh] overflow-y-auto p-6 relative bg-white rounded-xl shadow-lg border border-gray-200">
742
+ <button class="absolute top-4 right-4 text-gray-400 hover:text-gray-800 transition close-modal">
743
+ <i class="fas fa-times text-xl"></i>
744
+ </button>
745
+ <span id="detailTaskId" class="hidden"></span>
746
+ <h3 id="detailTaskTitle" class="text-2xl font-bold text-gray-800 mb-4">Chi tiết công việc</h3>
747
+ <div id="detailTaskBody" class="mb-6 text-sm text-gray-700 leading-relaxed space-y-2 bg-gray-50 p-4 rounded border border-gray-200 shadow-sm">
748
+ </div>
749
+ <div>
750
+ <h4 class="text-lg font-semibold text-gray-800 mb-2">Bình luận</h4>
751
+ <div id="commentList" class="space-y-3 max-h-72 overflow-y-auto border rounded-md bg-white p-3 shadow-inner">
752
+ </div>
753
+ <form id="commentForm" class="mt-4 space-y-2">
754
+ <textarea id="commentText" class="w-full border border-gray-300 rounded-md p-3 text-sm shadow-sm focus:outline-none focus:ring focus:border-blue-500" rows="3" placeholder="Nhập bình luận..."></textarea>
755
+ <button type="submit" class="bg-blue-600 text-white px-5 py-2 rounded-md hover:bg-blue-700 transition">Gửi bình luận</button>
756
+ </form>
757
+ </div>
758
+ </div>
759
+ </div>
760
+
761
+ <script>
762
+ document.addEventListener('DOMContentLoaded', () => {
763
+ const contentSections = document.querySelectorAll('.content-section');
764
+
765
+ window.showVanillaSection = function(targetId) {
766
+ console.log("VanillaJS: showVanillaSection called for", targetId);
767
+ contentSections.forEach(section => {
768
+ section.classList.remove('active');
769
+ });
770
+ const targetSection = document.getElementById(targetId);
771
+ if (targetSection) {
772
+ targetSection.classList.add('active');
773
+ }
774
+ }
775
+
776
+ function handleVanillaHashChangeInitial() {
777
+ const hash = window.location.hash.substring(1) || 'dashboard-section';
778
+ window.showVanillaSection(hash);
779
+ if (window.reactBridge && window.reactBridge.updateActiveSection) {
780
+ console.log("VanillaJS: Initial hash check, updating React bridge with active section:", hash);
781
+ window.reactBridge.updateActiveSection(hash);
782
+ }
783
+ }
784
+ // window.addEventListener('hashchange', handleVanillaHashChangeInitial); // Removed, only listeners.js should handle
785
+
786
+ setTimeout(() => {
787
+ console.log("VanillaJS: Initial section display. Relying on listeners.js now.");
788
+ // handleVanillaHashChangeInitial(); // Removed, handled by listeners.js
789
+ }, 200);
790
+
791
+ const mobileMenuButton = document.getElementById('mobile-menu-button');
792
+ if (mobileMenuButton) {
793
+ mobileMenuButton.addEventListener('click', () => {
794
+ if (window.reactBridge && window.reactBridge.setMobileMenuOpen) {
795
+ console.log("VanillaJS: mobile-menu-button clicked, calling reactBridge.setMobileMenuOpen(true)");
796
+ window.reactBridge.setMobileMenuOpen(true);
797
+ } else {
798
+ console.warn("VanillaJS: mobile-menu-button clicked, but reactBridge or setMobileMenuOpen is not available.");
799
+ }
800
+ });
801
+ }
802
+ // Đóng modal khi click nút .close-modal (cho các modal thuần)
803
+ document.querySelectorAll('.modal .close-modal').forEach(button => {
804
+ button.addEventListener('click', () => {
805
+ const modal = button.closest('.modal');
806
+ if (modal) {
807
+ modal.style.display = 'none';
808
+ }
809
+ });
810
+ });
811
+ });
812
+ </script>
813
+ <script type="module" src="public/js/utils/eventBus.js"></script>
814
+ <script type="module" src="public/js/firebase-init.js"></script>
815
+ <script type="module" src="public/js/utils/utils.js"></script>
816
+ <script type="module" src="public/js/utils/errorHandler.js"></script>
817
+ <script type="module" src="public/js/utils/dropdownHelpers.js"></script>
818
+ <script type="module" src="public/js/services/userService.js"></script>
819
+ <script type="module" src="public/js/services/departmentService.js"></script>
820
+ <script type="module" src="public/js/services/taskService.js"></script>
821
+ <script type="module" src="public/js/services/commentService.js"></script> <script type="module" src="public/js/managers/userManager.js"></script>
822
+ <script type="module" src="public/js/managers/departmentManager.js"></script>
823
+ <script type="module" src="public/js/managers/taskManager.js"></script>
824
+ <script type="module" src="public/js/auth.js"></script>
825
+ <script type="module" src="public/js/modal.js"></script>
826
+ <script type="module" src="public/js/departments.js"></script>
827
+ <script type="module" src="public/js/users.js"></script>
828
+ <script type="module" src="public/js/tasks.js"></script>
829
+ <script type="module" src="public/js/comments.js"></script>
830
+ <script type="module" src="public/js/kanban.js"></script>
831
+ <script type="module" src="public/js/dashboard.js"></script>
832
+ <script type="module" src="public/js/listeners.js"></script>
833
+ <script type="module" src="public/js/index.js"></script> <script type="module" src="/src/main.jsx"></script>
834
+ </body>
835
+ </html>
package-lock.json ADDED
@@ -0,0 +1,1718 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "nha-khoa-ttl---react-initial-setup",
3
+ "version": "0.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "nha-khoa-ttl---react-initial-setup",
9
+ "version": "0.0.0",
10
+ "dependencies": {
11
+ "@vitejs/plugin-react": "^4.5.1",
12
+ "lucide-react": "^0.511.0",
13
+ "react": "^19.1.0",
14
+ "react-dom": "^19.1.0"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "^22.14.0",
18
+ "@types/react": "^19.1.6",
19
+ "autoprefixer": "^10.4.21",
20
+ "postcss": "^8.5.4",
21
+ "tailwindcss": "^4.1.8",
22
+ "typescript": "~5.7.2",
23
+ "vite": "^6.2.0"
24
+ }
25
+ },
26
+ "node_modules/@ampproject/remapping": {
27
+ "version": "2.3.0",
28
+ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
29
+ "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
30
+ "license": "Apache-2.0",
31
+ "dependencies": {
32
+ "@jridgewell/gen-mapping": "^0.3.5",
33
+ "@jridgewell/trace-mapping": "^0.3.24"
34
+ },
35
+ "engines": {
36
+ "node": ">=6.0.0"
37
+ }
38
+ },
39
+ "node_modules/@babel/code-frame": {
40
+ "version": "7.27.1",
41
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
42
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
43
+ "license": "MIT",
44
+ "dependencies": {
45
+ "@babel/helper-validator-identifier": "^7.27.1",
46
+ "js-tokens": "^4.0.0",
47
+ "picocolors": "^1.1.1"
48
+ },
49
+ "engines": {
50
+ "node": ">=6.9.0"
51
+ }
52
+ },
53
+ "node_modules/@babel/compat-data": {
54
+ "version": "7.27.3",
55
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.3.tgz",
56
+ "integrity": "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw==",
57
+ "license": "MIT",
58
+ "engines": {
59
+ "node": ">=6.9.0"
60
+ }
61
+ },
62
+ "node_modules/@babel/core": {
63
+ "version": "7.27.4",
64
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz",
65
+ "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==",
66
+ "license": "MIT",
67
+ "dependencies": {
68
+ "@ampproject/remapping": "^2.2.0",
69
+ "@babel/code-frame": "^7.27.1",
70
+ "@babel/generator": "^7.27.3",
71
+ "@babel/helper-compilation-targets": "^7.27.2",
72
+ "@babel/helper-module-transforms": "^7.27.3",
73
+ "@babel/helpers": "^7.27.4",
74
+ "@babel/parser": "^7.27.4",
75
+ "@babel/template": "^7.27.2",
76
+ "@babel/traverse": "^7.27.4",
77
+ "@babel/types": "^7.27.3",
78
+ "convert-source-map": "^2.0.0",
79
+ "debug": "^4.1.0",
80
+ "gensync": "^1.0.0-beta.2",
81
+ "json5": "^2.2.3",
82
+ "semver": "^6.3.1"
83
+ },
84
+ "engines": {
85
+ "node": ">=6.9.0"
86
+ },
87
+ "funding": {
88
+ "type": "opencollective",
89
+ "url": "https://opencollective.com/babel"
90
+ }
91
+ },
92
+ "node_modules/@babel/generator": {
93
+ "version": "7.27.3",
94
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.3.tgz",
95
+ "integrity": "sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q==",
96
+ "license": "MIT",
97
+ "dependencies": {
98
+ "@babel/parser": "^7.27.3",
99
+ "@babel/types": "^7.27.3",
100
+ "@jridgewell/gen-mapping": "^0.3.5",
101
+ "@jridgewell/trace-mapping": "^0.3.25",
102
+ "jsesc": "^3.0.2"
103
+ },
104
+ "engines": {
105
+ "node": ">=6.9.0"
106
+ }
107
+ },
108
+ "node_modules/@babel/helper-compilation-targets": {
109
+ "version": "7.27.2",
110
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
111
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
112
+ "license": "MIT",
113
+ "dependencies": {
114
+ "@babel/compat-data": "^7.27.2",
115
+ "@babel/helper-validator-option": "^7.27.1",
116
+ "browserslist": "^4.24.0",
117
+ "lru-cache": "^5.1.1",
118
+ "semver": "^6.3.1"
119
+ },
120
+ "engines": {
121
+ "node": ">=6.9.0"
122
+ }
123
+ },
124
+ "node_modules/@babel/helper-module-imports": {
125
+ "version": "7.27.1",
126
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
127
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
128
+ "license": "MIT",
129
+ "dependencies": {
130
+ "@babel/traverse": "^7.27.1",
131
+ "@babel/types": "^7.27.1"
132
+ },
133
+ "engines": {
134
+ "node": ">=6.9.0"
135
+ }
136
+ },
137
+ "node_modules/@babel/helper-module-transforms": {
138
+ "version": "7.27.3",
139
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz",
140
+ "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==",
141
+ "license": "MIT",
142
+ "dependencies": {
143
+ "@babel/helper-module-imports": "^7.27.1",
144
+ "@babel/helper-validator-identifier": "^7.27.1",
145
+ "@babel/traverse": "^7.27.3"
146
+ },
147
+ "engines": {
148
+ "node": ">=6.9.0"
149
+ },
150
+ "peerDependencies": {
151
+ "@babel/core": "^7.0.0"
152
+ }
153
+ },
154
+ "node_modules/@babel/helper-plugin-utils": {
155
+ "version": "7.27.1",
156
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
157
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
158
+ "license": "MIT",
159
+ "engines": {
160
+ "node": ">=6.9.0"
161
+ }
162
+ },
163
+ "node_modules/@babel/helper-string-parser": {
164
+ "version": "7.27.1",
165
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
166
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
167
+ "license": "MIT",
168
+ "engines": {
169
+ "node": ">=6.9.0"
170
+ }
171
+ },
172
+ "node_modules/@babel/helper-validator-identifier": {
173
+ "version": "7.27.1",
174
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
175
+ "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
176
+ "license": "MIT",
177
+ "engines": {
178
+ "node": ">=6.9.0"
179
+ }
180
+ },
181
+ "node_modules/@babel/helper-validator-option": {
182
+ "version": "7.27.1",
183
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
184
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
185
+ "license": "MIT",
186
+ "engines": {
187
+ "node": ">=6.9.0"
188
+ }
189
+ },
190
+ "node_modules/@babel/helpers": {
191
+ "version": "7.27.4",
192
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.4.tgz",
193
+ "integrity": "sha512-Y+bO6U+I7ZKaM5G5rDUZiYfUvQPUibYmAFe7EnKdnKBbVXDZxvp+MWOH5gYciY0EPk4EScsuFMQBbEfpdRKSCQ==",
194
+ "license": "MIT",
195
+ "dependencies": {
196
+ "@babel/template": "^7.27.2",
197
+ "@babel/types": "^7.27.3"
198
+ },
199
+ "engines": {
200
+ "node": ">=6.9.0"
201
+ }
202
+ },
203
+ "node_modules/@babel/parser": {
204
+ "version": "7.27.4",
205
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.4.tgz",
206
+ "integrity": "sha512-BRmLHGwpUqLFR2jzx9orBuX/ABDkj2jLKOXrHDTN2aOKL+jFDDKaRNo9nyYsIl9h/UE/7lMKdDjKQQyxKKDZ7g==",
207
+ "license": "MIT",
208
+ "dependencies": {
209
+ "@babel/types": "^7.27.3"
210
+ },
211
+ "bin": {
212
+ "parser": "bin/babel-parser.js"
213
+ },
214
+ "engines": {
215
+ "node": ">=6.0.0"
216
+ }
217
+ },
218
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
219
+ "version": "7.27.1",
220
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
221
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
222
+ "license": "MIT",
223
+ "dependencies": {
224
+ "@babel/helper-plugin-utils": "^7.27.1"
225
+ },
226
+ "engines": {
227
+ "node": ">=6.9.0"
228
+ },
229
+ "peerDependencies": {
230
+ "@babel/core": "^7.0.0-0"
231
+ }
232
+ },
233
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
234
+ "version": "7.27.1",
235
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
236
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
237
+ "license": "MIT",
238
+ "dependencies": {
239
+ "@babel/helper-plugin-utils": "^7.27.1"
240
+ },
241
+ "engines": {
242
+ "node": ">=6.9.0"
243
+ },
244
+ "peerDependencies": {
245
+ "@babel/core": "^7.0.0-0"
246
+ }
247
+ },
248
+ "node_modules/@babel/template": {
249
+ "version": "7.27.2",
250
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
251
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
252
+ "license": "MIT",
253
+ "dependencies": {
254
+ "@babel/code-frame": "^7.27.1",
255
+ "@babel/parser": "^7.27.2",
256
+ "@babel/types": "^7.27.1"
257
+ },
258
+ "engines": {
259
+ "node": ">=6.9.0"
260
+ }
261
+ },
262
+ "node_modules/@babel/traverse": {
263
+ "version": "7.27.4",
264
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz",
265
+ "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==",
266
+ "license": "MIT",
267
+ "dependencies": {
268
+ "@babel/code-frame": "^7.27.1",
269
+ "@babel/generator": "^7.27.3",
270
+ "@babel/parser": "^7.27.4",
271
+ "@babel/template": "^7.27.2",
272
+ "@babel/types": "^7.27.3",
273
+ "debug": "^4.3.1",
274
+ "globals": "^11.1.0"
275
+ },
276
+ "engines": {
277
+ "node": ">=6.9.0"
278
+ }
279
+ },
280
+ "node_modules/@babel/types": {
281
+ "version": "7.27.3",
282
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.3.tgz",
283
+ "integrity": "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==",
284
+ "license": "MIT",
285
+ "dependencies": {
286
+ "@babel/helper-string-parser": "^7.27.1",
287
+ "@babel/helper-validator-identifier": "^7.27.1"
288
+ },
289
+ "engines": {
290
+ "node": ">=6.9.0"
291
+ }
292
+ },
293
+ "node_modules/@esbuild/aix-ppc64": {
294
+ "version": "0.25.5",
295
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
296
+ "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
297
+ "cpu": [
298
+ "ppc64"
299
+ ],
300
+ "license": "MIT",
301
+ "optional": true,
302
+ "os": [
303
+ "aix"
304
+ ],
305
+ "engines": {
306
+ "node": ">=18"
307
+ }
308
+ },
309
+ "node_modules/@esbuild/android-arm": {
310
+ "version": "0.25.5",
311
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
312
+ "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
313
+ "cpu": [
314
+ "arm"
315
+ ],
316
+ "license": "MIT",
317
+ "optional": true,
318
+ "os": [
319
+ "android"
320
+ ],
321
+ "engines": {
322
+ "node": ">=18"
323
+ }
324
+ },
325
+ "node_modules/@esbuild/android-arm64": {
326
+ "version": "0.25.5",
327
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
328
+ "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
329
+ "cpu": [
330
+ "arm64"
331
+ ],
332
+ "license": "MIT",
333
+ "optional": true,
334
+ "os": [
335
+ "android"
336
+ ],
337
+ "engines": {
338
+ "node": ">=18"
339
+ }
340
+ },
341
+ "node_modules/@esbuild/android-x64": {
342
+ "version": "0.25.5",
343
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
344
+ "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
345
+ "cpu": [
346
+ "x64"
347
+ ],
348
+ "license": "MIT",
349
+ "optional": true,
350
+ "os": [
351
+ "android"
352
+ ],
353
+ "engines": {
354
+ "node": ">=18"
355
+ }
356
+ },
357
+ "node_modules/@esbuild/darwin-arm64": {
358
+ "version": "0.25.5",
359
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
360
+ "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
361
+ "cpu": [
362
+ "arm64"
363
+ ],
364
+ "license": "MIT",
365
+ "optional": true,
366
+ "os": [
367
+ "darwin"
368
+ ],
369
+ "engines": {
370
+ "node": ">=18"
371
+ }
372
+ },
373
+ "node_modules/@esbuild/darwin-x64": {
374
+ "version": "0.25.5",
375
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
376
+ "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
377
+ "cpu": [
378
+ "x64"
379
+ ],
380
+ "license": "MIT",
381
+ "optional": true,
382
+ "os": [
383
+ "darwin"
384
+ ],
385
+ "engines": {
386
+ "node": ">=18"
387
+ }
388
+ },
389
+ "node_modules/@esbuild/freebsd-arm64": {
390
+ "version": "0.25.5",
391
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
392
+ "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
393
+ "cpu": [
394
+ "arm64"
395
+ ],
396
+ "license": "MIT",
397
+ "optional": true,
398
+ "os": [
399
+ "freebsd"
400
+ ],
401
+ "engines": {
402
+ "node": ">=18"
403
+ }
404
+ },
405
+ "node_modules/@esbuild/freebsd-x64": {
406
+ "version": "0.25.5",
407
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
408
+ "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
409
+ "cpu": [
410
+ "x64"
411
+ ],
412
+ "license": "MIT",
413
+ "optional": true,
414
+ "os": [
415
+ "freebsd"
416
+ ],
417
+ "engines": {
418
+ "node": ">=18"
419
+ }
420
+ },
421
+ "node_modules/@esbuild/linux-arm": {
422
+ "version": "0.25.5",
423
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
424
+ "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
425
+ "cpu": [
426
+ "arm"
427
+ ],
428
+ "license": "MIT",
429
+ "optional": true,
430
+ "os": [
431
+ "linux"
432
+ ],
433
+ "engines": {
434
+ "node": ">=18"
435
+ }
436
+ },
437
+ "node_modules/@esbuild/linux-arm64": {
438
+ "version": "0.25.5",
439
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
440
+ "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
441
+ "cpu": [
442
+ "arm64"
443
+ ],
444
+ "license": "MIT",
445
+ "optional": true,
446
+ "os": [
447
+ "linux"
448
+ ],
449
+ "engines": {
450
+ "node": ">=18"
451
+ }
452
+ },
453
+ "node_modules/@esbuild/linux-ia32": {
454
+ "version": "0.25.5",
455
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
456
+ "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
457
+ "cpu": [
458
+ "ia32"
459
+ ],
460
+ "license": "MIT",
461
+ "optional": true,
462
+ "os": [
463
+ "linux"
464
+ ],
465
+ "engines": {
466
+ "node": ">=18"
467
+ }
468
+ },
469
+ "node_modules/@esbuild/linux-loong64": {
470
+ "version": "0.25.5",
471
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
472
+ "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
473
+ "cpu": [
474
+ "loong64"
475
+ ],
476
+ "license": "MIT",
477
+ "optional": true,
478
+ "os": [
479
+ "linux"
480
+ ],
481
+ "engines": {
482
+ "node": ">=18"
483
+ }
484
+ },
485
+ "node_modules/@esbuild/linux-mips64el": {
486
+ "version": "0.25.5",
487
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
488
+ "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
489
+ "cpu": [
490
+ "mips64el"
491
+ ],
492
+ "license": "MIT",
493
+ "optional": true,
494
+ "os": [
495
+ "linux"
496
+ ],
497
+ "engines": {
498
+ "node": ">=18"
499
+ }
500
+ },
501
+ "node_modules/@esbuild/linux-ppc64": {
502
+ "version": "0.25.5",
503
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
504
+ "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
505
+ "cpu": [
506
+ "ppc64"
507
+ ],
508
+ "license": "MIT",
509
+ "optional": true,
510
+ "os": [
511
+ "linux"
512
+ ],
513
+ "engines": {
514
+ "node": ">=18"
515
+ }
516
+ },
517
+ "node_modules/@esbuild/linux-riscv64": {
518
+ "version": "0.25.5",
519
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
520
+ "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
521
+ "cpu": [
522
+ "riscv64"
523
+ ],
524
+ "license": "MIT",
525
+ "optional": true,
526
+ "os": [
527
+ "linux"
528
+ ],
529
+ "engines": {
530
+ "node": ">=18"
531
+ }
532
+ },
533
+ "node_modules/@esbuild/linux-s390x": {
534
+ "version": "0.25.5",
535
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
536
+ "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
537
+ "cpu": [
538
+ "s390x"
539
+ ],
540
+ "license": "MIT",
541
+ "optional": true,
542
+ "os": [
543
+ "linux"
544
+ ],
545
+ "engines": {
546
+ "node": ">=18"
547
+ }
548
+ },
549
+ "node_modules/@esbuild/linux-x64": {
550
+ "version": "0.25.5",
551
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
552
+ "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==",
553
+ "cpu": [
554
+ "x64"
555
+ ],
556
+ "license": "MIT",
557
+ "optional": true,
558
+ "os": [
559
+ "linux"
560
+ ],
561
+ "engines": {
562
+ "node": ">=18"
563
+ }
564
+ },
565
+ "node_modules/@esbuild/netbsd-arm64": {
566
+ "version": "0.25.5",
567
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
568
+ "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
569
+ "cpu": [
570
+ "arm64"
571
+ ],
572
+ "license": "MIT",
573
+ "optional": true,
574
+ "os": [
575
+ "netbsd"
576
+ ],
577
+ "engines": {
578
+ "node": ">=18"
579
+ }
580
+ },
581
+ "node_modules/@esbuild/netbsd-x64": {
582
+ "version": "0.25.5",
583
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
584
+ "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
585
+ "cpu": [
586
+ "x64"
587
+ ],
588
+ "license": "MIT",
589
+ "optional": true,
590
+ "os": [
591
+ "netbsd"
592
+ ],
593
+ "engines": {
594
+ "node": ">=18"
595
+ }
596
+ },
597
+ "node_modules/@esbuild/openbsd-arm64": {
598
+ "version": "0.25.5",
599
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
600
+ "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
601
+ "cpu": [
602
+ "arm64"
603
+ ],
604
+ "license": "MIT",
605
+ "optional": true,
606
+ "os": [
607
+ "openbsd"
608
+ ],
609
+ "engines": {
610
+ "node": ">=18"
611
+ }
612
+ },
613
+ "node_modules/@esbuild/openbsd-x64": {
614
+ "version": "0.25.5",
615
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
616
+ "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
617
+ "cpu": [
618
+ "x64"
619
+ ],
620
+ "license": "MIT",
621
+ "optional": true,
622
+ "os": [
623
+ "openbsd"
624
+ ],
625
+ "engines": {
626
+ "node": ">=18"
627
+ }
628
+ },
629
+ "node_modules/@esbuild/sunos-x64": {
630
+ "version": "0.25.5",
631
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
632
+ "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
633
+ "cpu": [
634
+ "x64"
635
+ ],
636
+ "license": "MIT",
637
+ "optional": true,
638
+ "os": [
639
+ "sunos"
640
+ ],
641
+ "engines": {
642
+ "node": ">=18"
643
+ }
644
+ },
645
+ "node_modules/@esbuild/win32-arm64": {
646
+ "version": "0.25.5",
647
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
648
+ "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
649
+ "cpu": [
650
+ "arm64"
651
+ ],
652
+ "license": "MIT",
653
+ "optional": true,
654
+ "os": [
655
+ "win32"
656
+ ],
657
+ "engines": {
658
+ "node": ">=18"
659
+ }
660
+ },
661
+ "node_modules/@esbuild/win32-ia32": {
662
+ "version": "0.25.5",
663
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
664
+ "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
665
+ "cpu": [
666
+ "ia32"
667
+ ],
668
+ "license": "MIT",
669
+ "optional": true,
670
+ "os": [
671
+ "win32"
672
+ ],
673
+ "engines": {
674
+ "node": ">=18"
675
+ }
676
+ },
677
+ "node_modules/@esbuild/win32-x64": {
678
+ "version": "0.25.5",
679
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
680
+ "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
681
+ "cpu": [
682
+ "x64"
683
+ ],
684
+ "license": "MIT",
685
+ "optional": true,
686
+ "os": [
687
+ "win32"
688
+ ],
689
+ "engines": {
690
+ "node": ">=18"
691
+ }
692
+ },
693
+ "node_modules/@jridgewell/gen-mapping": {
694
+ "version": "0.3.8",
695
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
696
+ "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
697
+ "license": "MIT",
698
+ "dependencies": {
699
+ "@jridgewell/set-array": "^1.2.1",
700
+ "@jridgewell/sourcemap-codec": "^1.4.10",
701
+ "@jridgewell/trace-mapping": "^0.3.24"
702
+ },
703
+ "engines": {
704
+ "node": ">=6.0.0"
705
+ }
706
+ },
707
+ "node_modules/@jridgewell/resolve-uri": {
708
+ "version": "3.1.2",
709
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
710
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
711
+ "license": "MIT",
712
+ "engines": {
713
+ "node": ">=6.0.0"
714
+ }
715
+ },
716
+ "node_modules/@jridgewell/set-array": {
717
+ "version": "1.2.1",
718
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
719
+ "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
720
+ "license": "MIT",
721
+ "engines": {
722
+ "node": ">=6.0.0"
723
+ }
724
+ },
725
+ "node_modules/@jridgewell/sourcemap-codec": {
726
+ "version": "1.5.0",
727
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
728
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
729
+ "license": "MIT"
730
+ },
731
+ "node_modules/@jridgewell/trace-mapping": {
732
+ "version": "0.3.25",
733
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
734
+ "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
735
+ "license": "MIT",
736
+ "dependencies": {
737
+ "@jridgewell/resolve-uri": "^3.1.0",
738
+ "@jridgewell/sourcemap-codec": "^1.4.14"
739
+ }
740
+ },
741
+ "node_modules/@rolldown/pluginutils": {
742
+ "version": "1.0.0-beta.9",
743
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz",
744
+ "integrity": "sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==",
745
+ "license": "MIT"
746
+ },
747
+ "node_modules/@rollup/rollup-android-arm-eabi": {
748
+ "version": "4.41.1",
749
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz",
750
+ "integrity": "sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==",
751
+ "cpu": [
752
+ "arm"
753
+ ],
754
+ "license": "MIT",
755
+ "optional": true,
756
+ "os": [
757
+ "android"
758
+ ]
759
+ },
760
+ "node_modules/@rollup/rollup-android-arm64": {
761
+ "version": "4.41.1",
762
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.1.tgz",
763
+ "integrity": "sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==",
764
+ "cpu": [
765
+ "arm64"
766
+ ],
767
+ "license": "MIT",
768
+ "optional": true,
769
+ "os": [
770
+ "android"
771
+ ]
772
+ },
773
+ "node_modules/@rollup/rollup-darwin-arm64": {
774
+ "version": "4.41.1",
775
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz",
776
+ "integrity": "sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==",
777
+ "cpu": [
778
+ "arm64"
779
+ ],
780
+ "license": "MIT",
781
+ "optional": true,
782
+ "os": [
783
+ "darwin"
784
+ ]
785
+ },
786
+ "node_modules/@rollup/rollup-darwin-x64": {
787
+ "version": "4.41.1",
788
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.1.tgz",
789
+ "integrity": "sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==",
790
+ "cpu": [
791
+ "x64"
792
+ ],
793
+ "license": "MIT",
794
+ "optional": true,
795
+ "os": [
796
+ "darwin"
797
+ ]
798
+ },
799
+ "node_modules/@rollup/rollup-freebsd-arm64": {
800
+ "version": "4.41.1",
801
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.1.tgz",
802
+ "integrity": "sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==",
803
+ "cpu": [
804
+ "arm64"
805
+ ],
806
+ "license": "MIT",
807
+ "optional": true,
808
+ "os": [
809
+ "freebsd"
810
+ ]
811
+ },
812
+ "node_modules/@rollup/rollup-freebsd-x64": {
813
+ "version": "4.41.1",
814
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.1.tgz",
815
+ "integrity": "sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==",
816
+ "cpu": [
817
+ "x64"
818
+ ],
819
+ "license": "MIT",
820
+ "optional": true,
821
+ "os": [
822
+ "freebsd"
823
+ ]
824
+ },
825
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
826
+ "version": "4.41.1",
827
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.1.tgz",
828
+ "integrity": "sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==",
829
+ "cpu": [
830
+ "arm"
831
+ ],
832
+ "license": "MIT",
833
+ "optional": true,
834
+ "os": [
835
+ "linux"
836
+ ]
837
+ },
838
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
839
+ "version": "4.41.1",
840
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.1.tgz",
841
+ "integrity": "sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==",
842
+ "cpu": [
843
+ "arm"
844
+ ],
845
+ "license": "MIT",
846
+ "optional": true,
847
+ "os": [
848
+ "linux"
849
+ ]
850
+ },
851
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
852
+ "version": "4.41.1",
853
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.1.tgz",
854
+ "integrity": "sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==",
855
+ "cpu": [
856
+ "arm64"
857
+ ],
858
+ "license": "MIT",
859
+ "optional": true,
860
+ "os": [
861
+ "linux"
862
+ ]
863
+ },
864
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
865
+ "version": "4.41.1",
866
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.1.tgz",
867
+ "integrity": "sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==",
868
+ "cpu": [
869
+ "arm64"
870
+ ],
871
+ "license": "MIT",
872
+ "optional": true,
873
+ "os": [
874
+ "linux"
875
+ ]
876
+ },
877
+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
878
+ "version": "4.41.1",
879
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.1.tgz",
880
+ "integrity": "sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==",
881
+ "cpu": [
882
+ "loong64"
883
+ ],
884
+ "license": "MIT",
885
+ "optional": true,
886
+ "os": [
887
+ "linux"
888
+ ]
889
+ },
890
+ "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
891
+ "version": "4.41.1",
892
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.1.tgz",
893
+ "integrity": "sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==",
894
+ "cpu": [
895
+ "ppc64"
896
+ ],
897
+ "license": "MIT",
898
+ "optional": true,
899
+ "os": [
900
+ "linux"
901
+ ]
902
+ },
903
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
904
+ "version": "4.41.1",
905
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.1.tgz",
906
+ "integrity": "sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==",
907
+ "cpu": [
908
+ "riscv64"
909
+ ],
910
+ "license": "MIT",
911
+ "optional": true,
912
+ "os": [
913
+ "linux"
914
+ ]
915
+ },
916
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
917
+ "version": "4.41.1",
918
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.1.tgz",
919
+ "integrity": "sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==",
920
+ "cpu": [
921
+ "riscv64"
922
+ ],
923
+ "license": "MIT",
924
+ "optional": true,
925
+ "os": [
926
+ "linux"
927
+ ]
928
+ },
929
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
930
+ "version": "4.41.1",
931
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.1.tgz",
932
+ "integrity": "sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==",
933
+ "cpu": [
934
+ "s390x"
935
+ ],
936
+ "license": "MIT",
937
+ "optional": true,
938
+ "os": [
939
+ "linux"
940
+ ]
941
+ },
942
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
943
+ "version": "4.41.1",
944
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.1.tgz",
945
+ "integrity": "sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==",
946
+ "cpu": [
947
+ "x64"
948
+ ],
949
+ "license": "MIT",
950
+ "optional": true,
951
+ "os": [
952
+ "linux"
953
+ ]
954
+ },
955
+ "node_modules/@rollup/rollup-linux-x64-musl": {
956
+ "version": "4.41.1",
957
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.1.tgz",
958
+ "integrity": "sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==",
959
+ "cpu": [
960
+ "x64"
961
+ ],
962
+ "license": "MIT",
963
+ "optional": true,
964
+ "os": [
965
+ "linux"
966
+ ]
967
+ },
968
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
969
+ "version": "4.41.1",
970
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.1.tgz",
971
+ "integrity": "sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==",
972
+ "cpu": [
973
+ "arm64"
974
+ ],
975
+ "license": "MIT",
976
+ "optional": true,
977
+ "os": [
978
+ "win32"
979
+ ]
980
+ },
981
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
982
+ "version": "4.41.1",
983
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.1.tgz",
984
+ "integrity": "sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==",
985
+ "cpu": [
986
+ "ia32"
987
+ ],
988
+ "license": "MIT",
989
+ "optional": true,
990
+ "os": [
991
+ "win32"
992
+ ]
993
+ },
994
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
995
+ "version": "4.41.1",
996
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz",
997
+ "integrity": "sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==",
998
+ "cpu": [
999
+ "x64"
1000
+ ],
1001
+ "license": "MIT",
1002
+ "optional": true,
1003
+ "os": [
1004
+ "win32"
1005
+ ]
1006
+ },
1007
+ "node_modules/@types/babel__core": {
1008
+ "version": "7.20.5",
1009
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
1010
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
1011
+ "license": "MIT",
1012
+ "dependencies": {
1013
+ "@babel/parser": "^7.20.7",
1014
+ "@babel/types": "^7.20.7",
1015
+ "@types/babel__generator": "*",
1016
+ "@types/babel__template": "*",
1017
+ "@types/babel__traverse": "*"
1018
+ }
1019
+ },
1020
+ "node_modules/@types/babel__generator": {
1021
+ "version": "7.27.0",
1022
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
1023
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
1024
+ "license": "MIT",
1025
+ "dependencies": {
1026
+ "@babel/types": "^7.0.0"
1027
+ }
1028
+ },
1029
+ "node_modules/@types/babel__template": {
1030
+ "version": "7.4.4",
1031
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
1032
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
1033
+ "license": "MIT",
1034
+ "dependencies": {
1035
+ "@babel/parser": "^7.1.0",
1036
+ "@babel/types": "^7.0.0"
1037
+ }
1038
+ },
1039
+ "node_modules/@types/babel__traverse": {
1040
+ "version": "7.20.7",
1041
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz",
1042
+ "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==",
1043
+ "license": "MIT",
1044
+ "dependencies": {
1045
+ "@babel/types": "^7.20.7"
1046
+ }
1047
+ },
1048
+ "node_modules/@types/estree": {
1049
+ "version": "1.0.7",
1050
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
1051
+ "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
1052
+ "license": "MIT"
1053
+ },
1054
+ "node_modules/@types/node": {
1055
+ "version": "22.15.29",
1056
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz",
1057
+ "integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==",
1058
+ "devOptional": true,
1059
+ "license": "MIT",
1060
+ "dependencies": {
1061
+ "undici-types": "~6.21.0"
1062
+ }
1063
+ },
1064
+ "node_modules/@types/react": {
1065
+ "version": "19.1.6",
1066
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.6.tgz",
1067
+ "integrity": "sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q==",
1068
+ "dev": true,
1069
+ "license": "MIT",
1070
+ "dependencies": {
1071
+ "csstype": "^3.0.2"
1072
+ }
1073
+ },
1074
+ "node_modules/@vitejs/plugin-react": {
1075
+ "version": "4.5.1",
1076
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.5.1.tgz",
1077
+ "integrity": "sha512-uPZBqSI0YD4lpkIru6M35sIfylLGTyhGHvDZbNLuMA73lMlwJKz5xweH7FajfcCAc2HnINciejA9qTz0dr0M7A==",
1078
+ "license": "MIT",
1079
+ "dependencies": {
1080
+ "@babel/core": "^7.26.10",
1081
+ "@babel/plugin-transform-react-jsx-self": "^7.25.9",
1082
+ "@babel/plugin-transform-react-jsx-source": "^7.25.9",
1083
+ "@rolldown/pluginutils": "1.0.0-beta.9",
1084
+ "@types/babel__core": "^7.20.5",
1085
+ "react-refresh": "^0.17.0"
1086
+ },
1087
+ "engines": {
1088
+ "node": "^14.18.0 || >=16.0.0"
1089
+ },
1090
+ "peerDependencies": {
1091
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0"
1092
+ }
1093
+ },
1094
+ "node_modules/autoprefixer": {
1095
+ "version": "10.4.21",
1096
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
1097
+ "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==",
1098
+ "dev": true,
1099
+ "funding": [
1100
+ {
1101
+ "type": "opencollective",
1102
+ "url": "https://opencollective.com/postcss/"
1103
+ },
1104
+ {
1105
+ "type": "tidelift",
1106
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
1107
+ },
1108
+ {
1109
+ "type": "github",
1110
+ "url": "https://github.com/sponsors/ai"
1111
+ }
1112
+ ],
1113
+ "license": "MIT",
1114
+ "dependencies": {
1115
+ "browserslist": "^4.24.4",
1116
+ "caniuse-lite": "^1.0.30001702",
1117
+ "fraction.js": "^4.3.7",
1118
+ "normalize-range": "^0.1.2",
1119
+ "picocolors": "^1.1.1",
1120
+ "postcss-value-parser": "^4.2.0"
1121
+ },
1122
+ "bin": {
1123
+ "autoprefixer": "bin/autoprefixer"
1124
+ },
1125
+ "engines": {
1126
+ "node": "^10 || ^12 || >=14"
1127
+ },
1128
+ "peerDependencies": {
1129
+ "postcss": "^8.1.0"
1130
+ }
1131
+ },
1132
+ "node_modules/browserslist": {
1133
+ "version": "4.25.0",
1134
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz",
1135
+ "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==",
1136
+ "funding": [
1137
+ {
1138
+ "type": "opencollective",
1139
+ "url": "https://opencollective.com/browserslist"
1140
+ },
1141
+ {
1142
+ "type": "tidelift",
1143
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1144
+ },
1145
+ {
1146
+ "type": "github",
1147
+ "url": "https://github.com/sponsors/ai"
1148
+ }
1149
+ ],
1150
+ "license": "MIT",
1151
+ "dependencies": {
1152
+ "caniuse-lite": "^1.0.30001718",
1153
+ "electron-to-chromium": "^1.5.160",
1154
+ "node-releases": "^2.0.19",
1155
+ "update-browserslist-db": "^1.1.3"
1156
+ },
1157
+ "bin": {
1158
+ "browserslist": "cli.js"
1159
+ },
1160
+ "engines": {
1161
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
1162
+ }
1163
+ },
1164
+ "node_modules/caniuse-lite": {
1165
+ "version": "1.0.30001720",
1166
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001720.tgz",
1167
+ "integrity": "sha512-Ec/2yV2nNPwb4DnTANEV99ZWwm3ZWfdlfkQbWSDDt+PsXEVYwlhPH8tdMaPunYTKKmz7AnHi2oNEi1GcmKCD8g==",
1168
+ "funding": [
1169
+ {
1170
+ "type": "opencollective",
1171
+ "url": "https://opencollective.com/browserslist"
1172
+ },
1173
+ {
1174
+ "type": "tidelift",
1175
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
1176
+ },
1177
+ {
1178
+ "type": "github",
1179
+ "url": "https://github.com/sponsors/ai"
1180
+ }
1181
+ ],
1182
+ "license": "CC-BY-4.0"
1183
+ },
1184
+ "node_modules/convert-source-map": {
1185
+ "version": "2.0.0",
1186
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
1187
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
1188
+ "license": "MIT"
1189
+ },
1190
+ "node_modules/csstype": {
1191
+ "version": "3.1.3",
1192
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
1193
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
1194
+ "dev": true,
1195
+ "license": "MIT"
1196
+ },
1197
+ "node_modules/debug": {
1198
+ "version": "4.4.1",
1199
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
1200
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
1201
+ "license": "MIT",
1202
+ "dependencies": {
1203
+ "ms": "^2.1.3"
1204
+ },
1205
+ "engines": {
1206
+ "node": ">=6.0"
1207
+ },
1208
+ "peerDependenciesMeta": {
1209
+ "supports-color": {
1210
+ "optional": true
1211
+ }
1212
+ }
1213
+ },
1214
+ "node_modules/electron-to-chromium": {
1215
+ "version": "1.5.162",
1216
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.162.tgz",
1217
+ "integrity": "sha512-hQA+Zb5QQwoSaXJWEAGEw1zhk//O7qDzib05Z4qTqZfNju/FAkrm5ZInp0JbTp4Z18A6bilopdZWEYrFSsfllA==",
1218
+ "license": "ISC"
1219
+ },
1220
+ "node_modules/esbuild": {
1221
+ "version": "0.25.5",
1222
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
1223
+ "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
1224
+ "hasInstallScript": true,
1225
+ "license": "MIT",
1226
+ "bin": {
1227
+ "esbuild": "bin/esbuild"
1228
+ },
1229
+ "engines": {
1230
+ "node": ">=18"
1231
+ },
1232
+ "optionalDependencies": {
1233
+ "@esbuild/aix-ppc64": "0.25.5",
1234
+ "@esbuild/android-arm": "0.25.5",
1235
+ "@esbuild/android-arm64": "0.25.5",
1236
+ "@esbuild/android-x64": "0.25.5",
1237
+ "@esbuild/darwin-arm64": "0.25.5",
1238
+ "@esbuild/darwin-x64": "0.25.5",
1239
+ "@esbuild/freebsd-arm64": "0.25.5",
1240
+ "@esbuild/freebsd-x64": "0.25.5",
1241
+ "@esbuild/linux-arm": "0.25.5",
1242
+ "@esbuild/linux-arm64": "0.25.5",
1243
+ "@esbuild/linux-ia32": "0.25.5",
1244
+ "@esbuild/linux-loong64": "0.25.5",
1245
+ "@esbuild/linux-mips64el": "0.25.5",
1246
+ "@esbuild/linux-ppc64": "0.25.5",
1247
+ "@esbuild/linux-riscv64": "0.25.5",
1248
+ "@esbuild/linux-s390x": "0.25.5",
1249
+ "@esbuild/linux-x64": "0.25.5",
1250
+ "@esbuild/netbsd-arm64": "0.25.5",
1251
+ "@esbuild/netbsd-x64": "0.25.5",
1252
+ "@esbuild/openbsd-arm64": "0.25.5",
1253
+ "@esbuild/openbsd-x64": "0.25.5",
1254
+ "@esbuild/sunos-x64": "0.25.5",
1255
+ "@esbuild/win32-arm64": "0.25.5",
1256
+ "@esbuild/win32-ia32": "0.25.5",
1257
+ "@esbuild/win32-x64": "0.25.5"
1258
+ }
1259
+ },
1260
+ "node_modules/escalade": {
1261
+ "version": "3.2.0",
1262
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
1263
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
1264
+ "license": "MIT",
1265
+ "engines": {
1266
+ "node": ">=6"
1267
+ }
1268
+ },
1269
+ "node_modules/fdir": {
1270
+ "version": "6.4.5",
1271
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz",
1272
+ "integrity": "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==",
1273
+ "license": "MIT",
1274
+ "peerDependencies": {
1275
+ "picomatch": "^3 || ^4"
1276
+ },
1277
+ "peerDependenciesMeta": {
1278
+ "picomatch": {
1279
+ "optional": true
1280
+ }
1281
+ }
1282
+ },
1283
+ "node_modules/fraction.js": {
1284
+ "version": "4.3.7",
1285
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
1286
+ "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
1287
+ "dev": true,
1288
+ "license": "MIT",
1289
+ "engines": {
1290
+ "node": "*"
1291
+ },
1292
+ "funding": {
1293
+ "type": "patreon",
1294
+ "url": "https://github.com/sponsors/rawify"
1295
+ }
1296
+ },
1297
+ "node_modules/fsevents": {
1298
+ "version": "2.3.3",
1299
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1300
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1301
+ "hasInstallScript": true,
1302
+ "license": "MIT",
1303
+ "optional": true,
1304
+ "os": [
1305
+ "darwin"
1306
+ ],
1307
+ "engines": {
1308
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1309
+ }
1310
+ },
1311
+ "node_modules/gensync": {
1312
+ "version": "1.0.0-beta.2",
1313
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
1314
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
1315
+ "license": "MIT",
1316
+ "engines": {
1317
+ "node": ">=6.9.0"
1318
+ }
1319
+ },
1320
+ "node_modules/globals": {
1321
+ "version": "11.12.0",
1322
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
1323
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
1324
+ "license": "MIT",
1325
+ "engines": {
1326
+ "node": ">=4"
1327
+ }
1328
+ },
1329
+ "node_modules/js-tokens": {
1330
+ "version": "4.0.0",
1331
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
1332
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
1333
+ "license": "MIT"
1334
+ },
1335
+ "node_modules/jsesc": {
1336
+ "version": "3.1.0",
1337
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
1338
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
1339
+ "license": "MIT",
1340
+ "bin": {
1341
+ "jsesc": "bin/jsesc"
1342
+ },
1343
+ "engines": {
1344
+ "node": ">=6"
1345
+ }
1346
+ },
1347
+ "node_modules/json5": {
1348
+ "version": "2.2.3",
1349
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
1350
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
1351
+ "license": "MIT",
1352
+ "bin": {
1353
+ "json5": "lib/cli.js"
1354
+ },
1355
+ "engines": {
1356
+ "node": ">=6"
1357
+ }
1358
+ },
1359
+ "node_modules/lru-cache": {
1360
+ "version": "5.1.1",
1361
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
1362
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
1363
+ "license": "ISC",
1364
+ "dependencies": {
1365
+ "yallist": "^3.0.2"
1366
+ }
1367
+ },
1368
+ "node_modules/lucide-react": {
1369
+ "version": "0.511.0",
1370
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.511.0.tgz",
1371
+ "integrity": "sha512-VK5a2ydJ7xm8GvBeKLS9mu1pVK6ucef9780JVUjw6bAjJL/QXnd4Y0p7SPeOUMC27YhzNCZvm5d/QX0Tp3rc0w==",
1372
+ "license": "ISC",
1373
+ "peerDependencies": {
1374
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
1375
+ }
1376
+ },
1377
+ "node_modules/ms": {
1378
+ "version": "2.1.3",
1379
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1380
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1381
+ "license": "MIT"
1382
+ },
1383
+ "node_modules/nanoid": {
1384
+ "version": "3.3.11",
1385
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
1386
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
1387
+ "funding": [
1388
+ {
1389
+ "type": "github",
1390
+ "url": "https://github.com/sponsors/ai"
1391
+ }
1392
+ ],
1393
+ "license": "MIT",
1394
+ "bin": {
1395
+ "nanoid": "bin/nanoid.cjs"
1396
+ },
1397
+ "engines": {
1398
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1399
+ }
1400
+ },
1401
+ "node_modules/node-releases": {
1402
+ "version": "2.0.19",
1403
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
1404
+ "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
1405
+ "license": "MIT"
1406
+ },
1407
+ "node_modules/normalize-range": {
1408
+ "version": "0.1.2",
1409
+ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
1410
+ "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
1411
+ "dev": true,
1412
+ "license": "MIT",
1413
+ "engines": {
1414
+ "node": ">=0.10.0"
1415
+ }
1416
+ },
1417
+ "node_modules/picocolors": {
1418
+ "version": "1.1.1",
1419
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1420
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1421
+ "license": "ISC"
1422
+ },
1423
+ "node_modules/picomatch": {
1424
+ "version": "4.0.2",
1425
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
1426
+ "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
1427
+ "license": "MIT",
1428
+ "engines": {
1429
+ "node": ">=12"
1430
+ },
1431
+ "funding": {
1432
+ "url": "https://github.com/sponsors/jonschlinkert"
1433
+ }
1434
+ },
1435
+ "node_modules/postcss": {
1436
+ "version": "8.5.4",
1437
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz",
1438
+ "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==",
1439
+ "funding": [
1440
+ {
1441
+ "type": "opencollective",
1442
+ "url": "https://opencollective.com/postcss/"
1443
+ },
1444
+ {
1445
+ "type": "tidelift",
1446
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1447
+ },
1448
+ {
1449
+ "type": "github",
1450
+ "url": "https://github.com/sponsors/ai"
1451
+ }
1452
+ ],
1453
+ "license": "MIT",
1454
+ "dependencies": {
1455
+ "nanoid": "^3.3.11",
1456
+ "picocolors": "^1.1.1",
1457
+ "source-map-js": "^1.2.1"
1458
+ },
1459
+ "engines": {
1460
+ "node": "^10 || ^12 || >=14"
1461
+ }
1462
+ },
1463
+ "node_modules/postcss-value-parser": {
1464
+ "version": "4.2.0",
1465
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
1466
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
1467
+ "dev": true,
1468
+ "license": "MIT"
1469
+ },
1470
+ "node_modules/react": {
1471
+ "version": "19.1.0",
1472
+ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
1473
+ "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
1474
+ "license": "MIT",
1475
+ "engines": {
1476
+ "node": ">=0.10.0"
1477
+ }
1478
+ },
1479
+ "node_modules/react-dom": {
1480
+ "version": "19.1.0",
1481
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
1482
+ "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
1483
+ "license": "MIT",
1484
+ "dependencies": {
1485
+ "scheduler": "^0.26.0"
1486
+ },
1487
+ "peerDependencies": {
1488
+ "react": "^19.1.0"
1489
+ }
1490
+ },
1491
+ "node_modules/react-refresh": {
1492
+ "version": "0.17.0",
1493
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
1494
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
1495
+ "license": "MIT",
1496
+ "engines": {
1497
+ "node": ">=0.10.0"
1498
+ }
1499
+ },
1500
+ "node_modules/rollup": {
1501
+ "version": "4.41.1",
1502
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.1.tgz",
1503
+ "integrity": "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==",
1504
+ "license": "MIT",
1505
+ "dependencies": {
1506
+ "@types/estree": "1.0.7"
1507
+ },
1508
+ "bin": {
1509
+ "rollup": "dist/bin/rollup"
1510
+ },
1511
+ "engines": {
1512
+ "node": ">=18.0.0",
1513
+ "npm": ">=8.0.0"
1514
+ },
1515
+ "optionalDependencies": {
1516
+ "@rollup/rollup-android-arm-eabi": "4.41.1",
1517
+ "@rollup/rollup-android-arm64": "4.41.1",
1518
+ "@rollup/rollup-darwin-arm64": "4.41.1",
1519
+ "@rollup/rollup-darwin-x64": "4.41.1",
1520
+ "@rollup/rollup-freebsd-arm64": "4.41.1",
1521
+ "@rollup/rollup-freebsd-x64": "4.41.1",
1522
+ "@rollup/rollup-linux-arm-gnueabihf": "4.41.1",
1523
+ "@rollup/rollup-linux-arm-musleabihf": "4.41.1",
1524
+ "@rollup/rollup-linux-arm64-gnu": "4.41.1",
1525
+ "@rollup/rollup-linux-arm64-musl": "4.41.1",
1526
+ "@rollup/rollup-linux-loongarch64-gnu": "4.41.1",
1527
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1",
1528
+ "@rollup/rollup-linux-riscv64-gnu": "4.41.1",
1529
+ "@rollup/rollup-linux-riscv64-musl": "4.41.1",
1530
+ "@rollup/rollup-linux-s390x-gnu": "4.41.1",
1531
+ "@rollup/rollup-linux-x64-gnu": "4.41.1",
1532
+ "@rollup/rollup-linux-x64-musl": "4.41.1",
1533
+ "@rollup/rollup-win32-arm64-msvc": "4.41.1",
1534
+ "@rollup/rollup-win32-ia32-msvc": "4.41.1",
1535
+ "@rollup/rollup-win32-x64-msvc": "4.41.1",
1536
+ "fsevents": "~2.3.2"
1537
+ }
1538
+ },
1539
+ "node_modules/scheduler": {
1540
+ "version": "0.26.0",
1541
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
1542
+ "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
1543
+ "license": "MIT"
1544
+ },
1545
+ "node_modules/semver": {
1546
+ "version": "6.3.1",
1547
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
1548
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
1549
+ "license": "ISC",
1550
+ "bin": {
1551
+ "semver": "bin/semver.js"
1552
+ }
1553
+ },
1554
+ "node_modules/source-map-js": {
1555
+ "version": "1.2.1",
1556
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1557
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1558
+ "license": "BSD-3-Clause",
1559
+ "engines": {
1560
+ "node": ">=0.10.0"
1561
+ }
1562
+ },
1563
+ "node_modules/tailwindcss": {
1564
+ "version": "4.1.8",
1565
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.8.tgz",
1566
+ "integrity": "sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og==",
1567
+ "dev": true,
1568
+ "license": "MIT"
1569
+ },
1570
+ "node_modules/tinyglobby": {
1571
+ "version": "0.2.14",
1572
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
1573
+ "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
1574
+ "license": "MIT",
1575
+ "dependencies": {
1576
+ "fdir": "^6.4.4",
1577
+ "picomatch": "^4.0.2"
1578
+ },
1579
+ "engines": {
1580
+ "node": ">=12.0.0"
1581
+ },
1582
+ "funding": {
1583
+ "url": "https://github.com/sponsors/SuperchupuDev"
1584
+ }
1585
+ },
1586
+ "node_modules/typescript": {
1587
+ "version": "5.7.3",
1588
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
1589
+ "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
1590
+ "dev": true,
1591
+ "license": "Apache-2.0",
1592
+ "bin": {
1593
+ "tsc": "bin/tsc",
1594
+ "tsserver": "bin/tsserver"
1595
+ },
1596
+ "engines": {
1597
+ "node": ">=14.17"
1598
+ }
1599
+ },
1600
+ "node_modules/undici-types": {
1601
+ "version": "6.21.0",
1602
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
1603
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
1604
+ "devOptional": true,
1605
+ "license": "MIT"
1606
+ },
1607
+ "node_modules/update-browserslist-db": {
1608
+ "version": "1.1.3",
1609
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
1610
+ "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
1611
+ "funding": [
1612
+ {
1613
+ "type": "opencollective",
1614
+ "url": "https://opencollective.com/browserslist"
1615
+ },
1616
+ {
1617
+ "type": "tidelift",
1618
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
1619
+ },
1620
+ {
1621
+ "type": "github",
1622
+ "url": "https://github.com/sponsors/ai"
1623
+ }
1624
+ ],
1625
+ "license": "MIT",
1626
+ "dependencies": {
1627
+ "escalade": "^3.2.0",
1628
+ "picocolors": "^1.1.1"
1629
+ },
1630
+ "bin": {
1631
+ "update-browserslist-db": "cli.js"
1632
+ },
1633
+ "peerDependencies": {
1634
+ "browserslist": ">= 4.21.0"
1635
+ }
1636
+ },
1637
+ "node_modules/vite": {
1638
+ "version": "6.3.5",
1639
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
1640
+ "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
1641
+ "license": "MIT",
1642
+ "dependencies": {
1643
+ "esbuild": "^0.25.0",
1644
+ "fdir": "^6.4.4",
1645
+ "picomatch": "^4.0.2",
1646
+ "postcss": "^8.5.3",
1647
+ "rollup": "^4.34.9",
1648
+ "tinyglobby": "^0.2.13"
1649
+ },
1650
+ "bin": {
1651
+ "vite": "bin/vite.js"
1652
+ },
1653
+ "engines": {
1654
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
1655
+ },
1656
+ "funding": {
1657
+ "url": "https://github.com/vitejs/vite?sponsor=1"
1658
+ },
1659
+ "optionalDependencies": {
1660
+ "fsevents": "~2.3.3"
1661
+ },
1662
+ "peerDependencies": {
1663
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
1664
+ "jiti": ">=1.21.0",
1665
+ "less": "*",
1666
+ "lightningcss": "^1.21.0",
1667
+ "sass": "*",
1668
+ "sass-embedded": "*",
1669
+ "stylus": "*",
1670
+ "sugarss": "*",
1671
+ "terser": "^5.16.0",
1672
+ "tsx": "^4.8.1",
1673
+ "yaml": "^2.4.2"
1674
+ },
1675
+ "peerDependenciesMeta": {
1676
+ "@types/node": {
1677
+ "optional": true
1678
+ },
1679
+ "jiti": {
1680
+ "optional": true
1681
+ },
1682
+ "less": {
1683
+ "optional": true
1684
+ },
1685
+ "lightningcss": {
1686
+ "optional": true
1687
+ },
1688
+ "sass": {
1689
+ "optional": true
1690
+ },
1691
+ "sass-embedded": {
1692
+ "optional": true
1693
+ },
1694
+ "stylus": {
1695
+ "optional": true
1696
+ },
1697
+ "sugarss": {
1698
+ "optional": true
1699
+ },
1700
+ "terser": {
1701
+ "optional": true
1702
+ },
1703
+ "tsx": {
1704
+ "optional": true
1705
+ },
1706
+ "yaml": {
1707
+ "optional": true
1708
+ }
1709
+ }
1710
+ },
1711
+ "node_modules/yallist": {
1712
+ "version": "3.1.1",
1713
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
1714
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
1715
+ "license": "ISC"
1716
+ }
1717
+ }
1718
+ }
package.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "nha-khoa-ttl---react-initial-setup",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@vitejs/plugin-react": "^4.5.1",
13
+ "lucide-react": "^0.511.0",
14
+ "react": "^19.1.0",
15
+ "react-dom": "^19.1.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^22.14.0",
19
+ "@types/react": "^19.1.6",
20
+ "autoprefixer": "^10.4.21",
21
+ "postcss": "^8.5.4",
22
+ "tailwindcss": "^4.1.8",
23
+ "typescript": "~5.7.2",
24
+ "vite": "^6.2.0"
25
+ }
26
+ }
public/js/auth.js ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { showMessage, hideModal } from './modal.js';
2
+ import { generateAvatarUrl, toggleButtonLoading } from './utils/utils.js';
3
+ import { auth, db, app } from './firebase-init.js'; // Import auth, db, app
4
+
5
+ export async function handleLogin(event) {
6
+ event.preventDefault();
7
+ toggleButtonLoading('loginButton', true);
8
+ const email = document.getElementById('login-email').value;
9
+ const password = document.getElementById('login-password').value;
10
+
11
+ try {
12
+ await auth.signInWithEmailAndPassword(email, password); // Sử dụng auth đã import
13
+ } catch (error) {
14
+ console.error('Login error:', error);
15
+ showMessage('Lỗi', 'Đăng nhập thất bại. Vui lòng kiểm tra lại thông tin.');
16
+ } finally {
17
+ toggleButtonLoading('loginButton', false);
18
+ }
19
+ }
20
+
21
+ export async function handleRegister(event) {
22
+ event.preventDefault();
23
+ toggleButtonLoading('registerButton', true);
24
+ const email = document.getElementById('register-email').value;
25
+ const password = document.getElementById('register-password').value;
26
+ const name = document.getElementById('register-name').value;
27
+
28
+ try {
29
+ const userCredential = await auth.createUserWithEmailAndPassword(email, password); // Sử dụng auth đã import
30
+ const user = userCredential.user;
31
+ // Sử dụng app.options.appId thay vì firebase.app().options.appId
32
+ const isFirstUser = (await db.collection('artifacts').doc(app.options.appId).collection('users').limit(1).get()).empty; //
33
+
34
+ await db.collection('artifacts').doc(app.options.appId).collection('users').doc(user.uid).set({ // Sử dụng db đã import
35
+ email,
36
+ name: name,
37
+ role: isFirstUser ? 'admin' : 'employee',
38
+ createdAt: firebase.firestore.FieldValue.serverTimestamp(), // Giữ lại FieldValue nếu Firebase compat được load toàn cục
39
+ avatar: generateAvatarUrl(name),
40
+ department: ''
41
+ });
42
+
43
+ showMessage('Đăng ký thành công', 'Vui lòng đăng nhập.');
44
+ hideModal('registerModal');
45
+ showModal('loginModal');
46
+ } catch (error) {
47
+ console.error('Register error:', error);
48
+ showMessage('Lỗi', 'Không thể đăng ký. Vui lòng thử lại.');
49
+ } finally {
50
+ toggleButtonLoading('registerButton', false);
51
+ }
52
+ }
53
+
54
+ export async function handleLogout() {
55
+ toggleButtonLoading('logoutButton', true);
56
+ try {
57
+ await auth.signOut(); // Sử dụng auth đã import
58
+ } catch (error) {
59
+ console.error('Logout error:', error);
60
+ showMessage('Lỗi', 'Không thể đăng xuất.');
61
+ } finally {
62
+ toggleButtonLoading('logoutButton', false);
63
+ }
64
+ }
65
+
66
+ export async function resetPassword(event) {
67
+ event.preventDefault();
68
+ toggleButtonLoading('resetPasswordSubmit', true);
69
+
70
+ const email = document.getElementById('reset-email').value;
71
+ try {
72
+ await auth.sendPasswordResetEmail(email); // Sử dụng auth đã import
73
+ showMessage('Thành công', 'Email đặt lại mật khẩu đã được gửi.');
74
+ hideModal('resetPasswordModal');
75
+ } catch (error) {
76
+ console.error('Reset password error:', error);
77
+ showMessage('Lỗi', 'Không thể gửi email đặt lại mật khẩu.');
78
+ } finally {
79
+ toggleButtonLoading('resetPasswordSubmit', false);
80
+ }
81
+ }
82
+
83
+ export async function updatePassword(event) {
84
+ event.preventDefault();
85
+ toggleButtonLoading('updatePasswordSubmit', true);
86
+
87
+ const newPassword = document.getElementById('new-password').value;
88
+ const confirmNewPassword = document.getElementById('confirm-new-password').value;
89
+
90
+ if (newPassword !== confirmNewPassword) {
91
+ showMessage('Lỗi', 'Mật khẩu mới không khớp.');
92
+ toggleButtonLoading('updatePasswordSubmit', false);
93
+ return;
94
+ }
95
+
96
+ if (newPassword.length < 6) {
97
+ showMessage('Lỗi', 'Mật khẩu phải có ít nhất 6 ký tự.');
98
+ toggleButtonLoading('updatePasswordSubmit', false);
99
+ return;
100
+ }
101
+
102
+ try {
103
+ await auth.currentUser.updatePassword(newPassword); // Sử dụng auth đã import
104
+ showMessage('Thành công', 'Mật khẩu đã được cập nhật.');
105
+ hideModal('changePasswordModal');
106
+ } catch (error) {
107
+ console.error('Update password error:', error);
108
+ showMessage('Lỗi', 'Không thể cập nhật mật khẩu.');
109
+ } finally {
110
+ toggleButtonLoading('updatePasswordSubmit', false);
111
+ }
112
+ }
public/js/comments.js ADDED
@@ -0,0 +1,689 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // script-modularized/comments.js
2
+
3
+ import { isModalOpen, showModal, hideModal, showConfirm, showMessage } from './modal.js'; // Import showMessage and showConfirm
4
+ import { escapeHtml, escapeJs, formatDate } from './utils/utils.js';
5
+ import eventBus from './utils/eventBus.js';
6
+ import * as departmentManager from './managers/departmentManager.js';
7
+ import * as userManager from './managers/userManager.js';
8
+ import * as taskManager from './managers/taskManager.js'; // Now comments are handled by taskManager
9
+ import { handleError } from './utils/errorHandler.js'; // Import error handler
10
+
11
+
12
+ // --- DOM Elements (Cache) ---
13
+ const commentListElement = document.getElementById('commentList');
14
+ const commentFormElement = document.getElementById('commentForm');
15
+ const commentTextareaElement = document.getElementById('commentText');
16
+ const detailTaskIdElement = document.getElementById('detailTaskId'); // Hidden input storing current task ID
17
+ const commentSubmitButtonElement = document.querySelector('#commentForm button[type="submit"]'); // Cache submit button
18
+
19
+
20
+ document.addEventListener('DOMContentLoaded', () => {
21
+ console.log('comments.js: DOMContentLoaded. Setting up comment UI listeners.');
22
+ setupCommentEventListeners(); // Setup listeners for UI actions and Manager events
23
+
24
+ // Add submit listener for the comment form
25
+ if (commentFormElement) {
26
+ commentFormElement.addEventListener('submit', handleCommentFormSubmit);
27
+ console.log('comments.js: Submit listener added for #commentForm.');
28
+ } else {
29
+ console.warn('comments.js: #commentForm element not found, skipping submit listener setup.');
30
+ }
31
+
32
+ // Add event delegation for comment actions (edit/delete buttons on individual comments)
33
+ if (commentListElement) {
34
+ commentListElement.addEventListener('click', handleCommentListClick);
35
+ console.log('comments.js: Click listener added for #commentList.');
36
+ } else {
37
+ console.warn('comments.js: #commentList element not found, skipping click listener setup.');
38
+ }
39
+ });
40
+
41
+
42
+ // --- Render Functions (Handle DOM Manipulation) ---
43
+
44
+ /**
45
+ * Generates the HTML string for a single comment item.
46
+ * @param {object} comment - The comment object.
47
+ * @param {string} taskId - The ID of the task the comment belongs to.
48
+ * @returns {string} The HTML string for the comment item.
49
+ */
50
+ function renderCommentItem(comment, taskId) {
51
+ const currentUser = userManager.getCurrentUserData();
52
+ const isAuthor = currentUser && comment.userId === currentUser.id;
53
+ const formattedCreatedAt = formatDate(comment.createdAt);
54
+ const formattedUpdatedAt = formatDate(comment.updatedAt);
55
+
56
+ return `
57
+ <div class="p-2 bg-white rounded shadow group relative" id="comment-${comment.id}">
58
+ <div class="flex justify-between items-start">
59
+ <div class="text-sm flex-1">
60
+ <strong>${escapeHtml(comment.authorName || 'Người dùng không xác định')}:</strong>
61
+ <span id="comment-text-${comment.id}">
62
+ ${comment.deleted
63
+ ? '<i class="text-gray-400 italic">[Bình luận đã xoá]</i>'
64
+ : escapeHtml(comment.text || 'Không có nội dung')}
65
+ </span>
66
+ ${comment.edited && !comment.deleted
67
+ ? `<span class="text-xs text-gray-400 ml-2">(đã chỉnh sửa ${formattedUpdatedAt})</span>`
68
+ : ''}
69
+ </div>
70
+ ${!comment.deleted && isAuthor ? `
71
+ <div class="comment-actions flex gap-2 text-xs text-blue-600 opacity-0 group-hover:opacity-100 transition">
72
+ <button class="edit-comment-button comment-action text-xs text-blue-600"
73
+ data-task-id="${taskId}"
74
+ data-comment-id="${comment.id}"
75
+ data-current-text="${escapeJs(comment.text || '')}"
76
+ >Sửa</button>
77
+
78
+ <button class="delete-comment-button comment-action text-xs text-red-600"
79
+ data-task-id="${taskId}"
80
+ data-comment-id="${comment.id}"
81
+ >Xoá</button>
82
+ </div>` : ''}
83
+ </div>
84
+ <p class="text-xs text-gray-400 mt-1">
85
+ ${formattedCreatedAt}
86
+ </p>
87
+ </div>
88
+ `;
89
+ }
90
+
91
+ /**
92
+ * Renders the list of comments for a specific task.
93
+ * @param {string} taskId - The ID of the task.
94
+ * @param {Array<object>} comments - Array of comment objects.
95
+ */
96
+ function renderCommentsList(taskId, comments) {
97
+ console.log('comments.js: renderCommentsList called for Task ID', taskId, 'with', comments.length, 'comments.');
98
+ console.log('comments.js: renderCommentsList received comments data:', comments);
99
+ if (!commentListElement) {
100
+ console.warn('renderCommentsList: commentList element not found.');
101
+ return;
102
+ }
103
+
104
+ commentListElement.innerHTML = ''; // Clear existing list
105
+
106
+ if (comments.length === 0) {
107
+ commentListElement.innerHTML = '<p class="text-gray-500">Chưa có bình luận nào.</p>';
108
+ console.log('renderCommentsList: No comments to display.');
109
+ return;
110
+ }
111
+
112
+ comments.forEach(comment => {
113
+ // Append the HTML string for each comment
114
+ commentListElement.innerHTML += renderCommentItem(comment, taskId);
115
+ });
116
+ console.log('comments.js: Finished rendering comments list for Task ID', taskId);
117
+
118
+ }
119
+
120
+ /**
121
+ * Adds a single comment item to the UI list.
122
+ * This is used when a new comment is added.
123
+ * @param {object} comment - The new comment object.
124
+ */
125
+ function addCommentToUI(comment) {
126
+ console.log('comments.js: addCommentToUI called with comment object:', comment); // <- Log đầu vào
127
+ console.log('comments.js: Type of comment.createdAt received:', typeof comment.createdAt); // <- Log kiểu dữ liệu
128
+ console.log('comments.js: Value of comment.createdAt received:', comment.createdAt); // <- Log giá trị
129
+ console.log('comments.js: addCommentToUI called for comment:', comment.id);
130
+ if (!commentListElement || !detailTaskIdElement) {
131
+ console.warn('addCommentToUI: commentList or detailTaskId element not found.');
132
+ return;
133
+ }
134
+ const taskId = detailTaskIdElement.value;
135
+ if (!taskId || comment.taskId !== taskId) {
136
+ console.log('addCommentToUI: Task ID mismatch or missing. Not adding comment to list.');
137
+ return; // Only add if it belongs to the currently viewed task
138
+ }
139
+
140
+ // Remove the "No comments" message if it exists
141
+ const noCommentsMessage = commentListElement.querySelector('p.text-gray-500');
142
+ if (noCommentsMessage) {
143
+ noCommentsMessage.remove();
144
+ }
145
+
146
+ // Create a temporary div to hold the rendered item
147
+ const tempDiv = document.createElement('div');
148
+ tempDiv.innerHTML = renderCommentItem(comment, taskId);
149
+ const newCommentElement = tempDiv.firstChild; // Get the first child (the comment div)
150
+
151
+ // Find the correct position to insert the new comment to maintain chronological order (oldest first)
152
+ const newCommentTime = comment.createdAt?.getTime() || 0;
153
+ let inserted = false;
154
+ const existingComments = commentListElement.children;
155
+
156
+ for (let i = 0; i < existingComments.length; i++) {
157
+ // Ensure we are comparing with actual comment elements, not the "No comments" message
158
+ if (existingComments[i].id && existingComments[i].id.startsWith('comment-')) {
159
+ // Find the corresponding comment object in the cache to get its timestamp for sorting
160
+ const existingCommentId = existingComments[i].id.replace('comment-', '');
161
+ const cachedComments = taskManager.getTaskCommentsData(taskId);
162
+ console.log('comments.js: addCommentToUI - Cached comments data:', cachedComments); // Log dữ liệu từ cache
163
+
164
+ const existingComment = cachedComments.find(c => c.id === existingCommentId);
165
+ console.log('comments.js: addCommentToUI - Found existing comment in cache for sorting:', existingComment); // Log bình luận tìm thấy trong cache
166
+
167
+ const existingCommentTime = existingComment?.createdAt?.getTime() || 0;
168
+ console.log('comments.js: Type of existingComment.createdAt from cache:', typeof existingComment?.createdAt); // Log kiểu dữ liệu
169
+ console.log('comments.js: Value of existingComment.createdAt from cache:', existingComment?.createdAt); // Log giá trị
170
+
171
+
172
+ if (newCommentTime < existingCommentTime) {
173
+ commentListElement.insertBefore(newCommentElement, existingComments[i]);
174
+ inserted = true;
175
+ break;
176
+ }
177
+ }
178
+ }
179
+
180
+ // If not inserted (either list was empty or comment is newest), append it
181
+ if (!inserted) {
182
+ commentListElement.appendChild(newCommentElement);
183
+ }
184
+
185
+ console.log('comments.js: Added comment to UI:', comment.id);
186
+ }
187
+
188
+ /**
189
+ * Updates a single comment item in the UI list.
190
+ * @param {object} comment - The updated comment object.
191
+ */
192
+ function updateCommentInUI(comment) {
193
+ console.log('comments.js: updateCommentInUI called for comment:', comment.id);
194
+ if (!commentListElement || !detailTaskIdElement) {
195
+ console.warn('updateCommentInUI: commentList or detailTaskId element not found.');
196
+ return;
197
+ }
198
+ const taskId = detailTaskIdElement.value;
199
+ if (!taskId || comment.taskId !== taskId) {
200
+ console.log('updateCommentInUI: Task ID mismatch or missing. Not updating comment in list.');
201
+ return; // Only update if it belongs to the currently viewed task
202
+ }
203
+
204
+ const existingCommentElement = commentListElement.querySelector(`#comment-${comment.id}`);
205
+
206
+ if (existingCommentElement) {
207
+ console.log('comments.js: Found existing comment element in UI:', comment.id);
208
+ // Create the updated HTML for the comment
209
+ const tempDiv = document.createElement('div');
210
+ tempDiv.innerHTML = renderCommentItem(comment, taskId);
211
+ const updatedCommentElement = tempDiv.firstChild;
212
+
213
+ // Replace the old element with the new one
214
+ commentListElement.replaceChild(updatedCommentElement, existingCommentElement);
215
+ console.log('comments.js: Updated comment in UI:', comment.id);
216
+
217
+ // Re-sort the list if necessary (e.g., if update affects sort order, though for comments by time, less likely)
218
+ // For simplicity and guaranteed order, you might re-render the list or remove+add back.
219
+ // Let's use remove+add back to maintain order if update logic ever changes sort criteria.
220
+ // existingCommentElement.remove();
221
+ // addCommentToUI(comment); // Add back the updated comment, function handles sorting
222
+
223
+ // Note: If the update involves changing the text and status (e.g., soft delete),
224
+ // the renderCommentItem function handles displaying "[Bình luận đã xoá]".
225
+ // The sorting logic in addCommentToUI assumes creation time.
226
+ // If updates could change sort order, a full re-render might be simpler but less performant.
227
+ // Given comment sorting is typically by creation time, replaceChild is often sufficient.
228
+
229
+ } else {
230
+ console.warn('comments.js: Comment element not found for update in UI:', comment.id);
231
+ // Fallback: If the specific element isn't found, re-render the whole list
232
+ const cachedComments = taskManager.getTaskCommentsData(taskId);
233
+ renderCommentsList(taskId, cachedComments);
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Removes a single comment item from the UI list.
239
+ * @param {string} commentId - The ID of the comment to remove.
240
+ */
241
+ function removeCommentFromUI(commentId) {
242
+ console.log('comments.js: removeCommentFromUI called for ID:', commentId);
243
+ if (!commentListElement) {
244
+ console.warn('removeCommentFromUI: commentList element not found.');
245
+ return;
246
+ }
247
+ const commentElement = commentListElement.querySelector(`#comment-${commentId}`);
248
+
249
+ if (commentElement) {
250
+ commentElement.remove();
251
+ console.log('comments.js: Removed comment from UI:', commentId);
252
+
253
+ // If the list becomes empty after removal, show the "No comments" message
254
+ if (commentListElement.children.length === 0) {
255
+ commentListElement.innerHTML = '<p class="text-gray-500">Chưa có bình luận nào.</p>';
256
+ console.log('comments.js: Comment list is empty, showing no comments message.');
257
+ }
258
+
259
+ } else {
260
+ console.warn('comments.js: Comment element not found for removal in UI:', commentId);
261
+ // Fallback: If element not found, re-render the whole list
262
+ const taskId = detailTaskIdElement?.value;
263
+ if(taskId) {
264
+ const cachedComments = taskManager.getTaskCommentsData(taskId);
265
+ renderCommentsList(taskId, cachedComments);
266
+ }
267
+ }
268
+ }
269
+
270
+
271
+ /**
272
+ * Enables inline editing for a specific comment.
273
+ * Replaces the comment text span with a textarea and shows save/cancel buttons.
274
+ * @param {string} taskId - The ID of the task the comment belongs to.
275
+ * @param {string} commentId - The ID of the comment to edit.
276
+ * @param {string} currentText - The current text of the comment.
277
+ */
278
+ function enableInlineEdit(taskId, commentId, currentText) {
279
+ console.log('comments.js: enableInlineEdit called for comment:', commentId);
280
+ const commentElement = document.getElementById(`comment-${commentId}`);
281
+ if (!commentElement) {
282
+ console.warn('enableInlineEdit: Comment element not found for ID:', commentId);
283
+ return;
284
+ }
285
+
286
+ const textSpan = commentElement.querySelector(`#comment-text-${commentId}`);
287
+ const actionsDiv = commentElement.querySelector('.comment-actions');
288
+
289
+ if (!textSpan || !actionsDiv) {
290
+ console.warn('enableInlineEdit: Required elements (text span or actions div) not found for comment:', commentId);
291
+ return;
292
+ }
293
+
294
+ // Replace text span with textarea
295
+ const textarea = document.createElement('textarea');
296
+ textarea.id = `edit-input-${commentId}`;
297
+ textarea.className = 'flex-1 p-1 border rounded text-sm'; // Add some basic styling
298
+ textarea.value = currentText;
299
+ textSpan.replaceWith(textarea);
300
+ textarea.focus(); // Focus on the textarea
301
+
302
+ // Replace actions div with save/cancel buttons
303
+ actionsDiv.innerHTML = `
304
+ <button class="save-edited-comment-button comment-action text-xs text-green-600" data-task-id="${taskId}" data-comment-id="${commentId}" data-original-text="${escapeJs(currentText)}">Lưu</button>
305
+ <button class="cancel-edit-comment-button comment-action text-xs text-gray-600" data-comment-id="${commentId}" data-original-text="${escapeJs(currentText)}">Huỷ</button>`;
306
+ }
307
+
308
+ /**
309
+ * Cancels inline editing for a specific comment.
310
+ * Reverts the UI back to the original comment text and buttons.
311
+ * @param {string} commentId - The ID of the comment being edited.
312
+ * @param {string} originalText - The original text of the comment before editing.
313
+ */
314
+ function cancelEditComment(commentId, originalText) {
315
+ console.log('comments.js: cancelEditComment called for comment:', commentId);
316
+ const commentElement = document.getElementById(`comment-${commentId}`);
317
+ if (!commentElement) {
318
+ console.warn('cancelEditComment: Comment element not found for ID:', commentId);
319
+ return;
320
+ }
321
+
322
+ const textarea = commentElement.querySelector(`#edit-input-${commentId}`);
323
+ const actionsDiv = commentElement.querySelector('.comment-actions');
324
+
325
+ if (!textarea || !actionsDiv) {
326
+ console.warn('cancelEditComment: Required elements (textarea or actions div) not found for comment:', commentId);
327
+ return;
328
+ }
329
+
330
+ // Replace textarea with the original text span
331
+ const textSpan = document.createElement('span');
332
+ textSpan.id = `comment-text-${commentId}`;
333
+ textSpan.innerHTML = escapeHtml(originalText || 'Không có nội dung'); // Use innerHTML for escaped HTML
334
+ textarea.replaceWith(textSpan);
335
+
336
+ // Replace save/cancel buttons with original edit/delete buttons
337
+ // Note: Re-rendering the whole item is cleaner if the full comment object is available.
338
+ // For simplicity here, we'll attempt to recreate the original buttons.
339
+ // A more robust approach would be to store the original actions HTML or re-render.
340
+
341
+ // To get the original buttons, we need the taskId and know if the current user is the author.
342
+ // Re-rendering the item is the most reliable way. We'll fetch the comment data again.
343
+
344
+ const taskId = detailTaskIdElement?.value; // Get current task ID from the hidden input
345
+ if (!taskId) {
346
+ console.warn('cancelEditComment: Task ID not found for re-rendering comment:', commentId);
347
+ // Fallback: just remove the actions div if task ID isn't available
348
+ actionsDiv.innerHTML = ''; // Clear actions
349
+ return;
350
+ }
351
+
352
+ // Re-render the specific comment item to restore original state
353
+ // Find the comment object in the cached comments
354
+ const cachedComments = taskManager.getTaskCommentsData(taskId);
355
+ const comment = cachedComments.find(c => c.id === commentId);
356
+
357
+ if (comment) {
358
+ const originalItemHTML = renderCommentItem(comment, taskId);
359
+ const tempDiv = document.createElement('div');
360
+ tempDiv.innerHTML = originalItemHTML;
361
+ const originalCommentElement = tempDiv.firstChild;
362
+ commentElement.replaceWith(originalCommentElement); // Replace the entire comment element
363
+ console.log('comments.js: Cancelled edit and re-rendered comment:', commentId);
364
+ } else {
365
+ console.warn('cancelEditComment: Comment data not found in cache for re-rendering:', commentId);
366
+ // Fallback if comment data isn't in cache: clear actions div
367
+ actionsDiv.innerHTML = ''; // Clear actions
368
+ }
369
+ }
370
+
371
+ // --- UI Action Handlers (Triggered by UI Events / Event Delegation) ---
372
+ // These functions handle UI interactions and publish events for the Manager.
373
+
374
+ /**
375
+ * Handles the submit event of the comment form.
376
+ * @param {Event} event - The form submit event.
377
+ */
378
+ function handleCommentFormSubmit(event) {
379
+ console.log('comments.js: handleCommentFormSubmit called.');
380
+ event.preventDefault(); // Prevent default form submission
381
+
382
+ if (!commentTextareaElement || !detailTaskIdElement) {
383
+ console.error('comments.js: handleCommentFormSubmit - Required elements not found.');
384
+ return;
385
+ }
386
+
387
+ const taskId = detailTaskIdElement.value; // Get taskId from the hidden input in the modal
388
+ const text = commentTextareaElement.value.trim();
389
+ console.log('comments.js: Captured text value from textarea:', text); // Thêm log này
390
+
391
+ if (!text || !taskId) {
392
+ console.warn('comments.js: handleCommentFormSubmit - Missing text or taskId.');
393
+ // Optionally show a message to the user
394
+ showMessage('Thông báo', 'Nội dung bình luận không được để trống.');
395
+ return;
396
+ }
397
+
398
+ // **Thêm kiểm tra người dùng đăng nhập và dữ liệu người dùng có sẵn**
399
+ const currentUser = userManager.getCurrentUserData();
400
+ if (!currentUser || !currentUser.id) {
401
+ console.error('comments.js: handleCommentFormSubmit - User data not available. Aborting comment submission.');
402
+ showMessage('Lỗi', 'Bạn cần đăng nhập để thêm bình luận.'); // Use showMessage
403
+ return; // Ngừng xử lý nếu người dùng chưa đăng nhập
404
+ }
405
+
406
+ console.log('comments.js: Publishing addCommentSubmit event for Task ID:', taskId, 'with text:', text); // Log giá trị text trước khi publish
407
+ eventBus.publish('addCommentSubmit', { taskId: taskId, text: text }); // Đảm bảo truyền cả taskId và text trong một đối tượng
408
+
409
+ // UI clears the textarea immediately for better UX, but state update is from Manager event
410
+ commentTextareaElement.value = '';
411
+
412
+ // UI does NOT reload comments or show success message here.
413
+ // This is handled by the listener for commentAdded/commentSaveFailed events.
414
+ }
415
+
416
+ /**
417
+ * Handles click events on the comment list (event delegation for edit/delete buttons).
418
+ * @param {Event} event - The click event.
419
+ */
420
+ async function handleCommentListClick(event) {
421
+ console.log('comments.js: Click event on commentList detected.');
422
+ const target = event.target;
423
+
424
+ // Use closest to find the button ancestor with the specific class
425
+ const editButton = target.closest('.edit-comment-button');
426
+ const deleteButton = target.closest('.delete-comment-button');
427
+ const saveEditButton = target.closest('.save-edited-comment-button'); // Handle save button click from inline edit
428
+ const cancelEditButton = target.closest('.cancel-edit-comment-button'); // Handle cancel button click from inline edit
429
+
430
+
431
+ if (editButton) {
432
+ event.stopPropagation(); // Prevent other listeners on the list from firing
433
+ const taskId = editButton.dataset.taskId;
434
+ const commentId = editButton.dataset.commentId;
435
+ const currentText = editButton.dataset.currentText; // Text is escaped by render function
436
+
437
+ console.log('comments.js: Edit comment button clicked, Task ID:', taskId, 'Comment ID:', commentId);
438
+ enableInlineEdit(taskId, commentId, currentText); // Call the UI function to enable inline edit
439
+
440
+ } else if (deleteButton) {
441
+ event.stopPropagation(); // Prevent other listeners on the list from firing
442
+ const taskId = deleteButton.dataset.taskId;
443
+ const commentId = deleteButton.dataset.commentId;
444
+ // Find the comment text element to get text for confirmation
445
+ const commentTextSpan = deleteButton.closest(`.p-2.bg-white`).querySelector('span[id^="comment-text-"]');
446
+ const commentText = commentTextSpan ? commentTextSpan.textContent.trim() : 'bình luận này';
447
+
448
+ console.log('comments.js: Delete comment button clicked, Task ID:', taskId, 'Comment ID:', commentId);
449
+
450
+ // Show confirmation dialog
451
+ const confirmed = await showConfirm('Xác nhận xóa bình luận', `Bạn có chắc chắn muốn xóa ${commentText}?`);
452
+ if (confirmed) {
453
+ console.log('comments.js: User confirmed deletion. Publishing deleteCommentClicked event for ID:', commentId);
454
+ // Publish event for Manager to handle deletion (soft delete)
455
+ eventBus.publish('deleteCommentClicked', { taskId: taskId, commentId: commentId });
456
+ // UI will wait for commentDeleted or commentDeleteFailed event from Manager
457
+ } else {
458
+ console.log('comments.js: User cancelled deletion.');
459
+ }
460
+
461
+ } else if (saveEditButton) {
462
+ event.stopPropagation();
463
+ const taskId = saveEditButton.dataset.taskId;
464
+ const commentId = saveEditButton.dataset.commentId;
465
+ const inputElement = document.getElementById(`edit-input-${commentId}`);
466
+ const newText = inputElement ? inputElement.value.trim() : '';
467
+ const originalText = saveEditButton.dataset.originalText; // Get original text from data attribute
468
+
469
+ console.log('comments.js: Save edited comment button clicked, Task ID:', taskId, 'Comment ID:', commentId, 'New Text:', newText);
470
+
471
+ if (!newText || newText === originalText) { // Use originalText from data attribute
472
+ console.log('comments.js: New text is empty or unchanged. Cancelling edit.');
473
+ cancelEditComment(commentId, originalText); // Cancel edit if no change or empty
474
+ return;
475
+ }
476
+
477
+ // Publish event for Manager to handle saving edited comment
478
+ eventBus.publish('saveEditedCommentSubmit', { taskId: taskId, commentId: commentId, newText: newText });
479
+ // UI waits for commentUpdated/commentSaveFailed events
480
+
481
+ } else if (cancelEditButton) {
482
+ event.stopPropagation();
483
+ const commentId = cancelEditButton.dataset.commentId;
484
+ const originalText = cancelEditButton.dataset.originalText; // Get original text from data attribute
485
+ console.log('comments.js: Cancel edit comment button clicked, Comment ID:', commentId);
486
+ cancelEditComment(commentId, originalText); // Call the UI function to cancel edit
487
+ }
488
+ }
489
+
490
+
491
+ // --- EventBus Listeners (Listen for events from Manager) ---
492
+
493
+ function setupCommentEventListeners() {
494
+ console.log('comments.js: Setting up EventBus listeners.');
495
+
496
+ // Listen for the main event when comments for a task are loaded/ready from the Manager
497
+ eventBus.subscribe('taskCommentsLoaded', ({ taskId, comments }) => {
498
+ console.log('comments.js: EventBus received taskCommentsLoaded for Task ID', taskId);
499
+ console.log('comments.js: taskCommentsLoaded handler received comments data:', comments);
500
+ const currentOpenTaskId = detailTaskIdElement?.value;
501
+ if (currentOpenTaskId === taskId) {
502
+ // Render the entire list of comments
503
+ renderCommentsList(taskId, comments);
504
+ console.log('comments.js: taskCommentsLoaded handler finished rendering.');
505
+ } else {
506
+ console.log('comments.js: taskCommentsLoaded received, but modal is for a different task. Ignoring.');
507
+ }
508
+ });
509
+
510
+ // Listen for individual comment added event from Manager
511
+ eventBus.subscribe('commentAdded', ({ taskId, comment }) => {
512
+ console.log('comments.js: EventBus received commentAdded.', comment.id, 'for Task ID', taskId);
513
+ const currentOpenTaskId = detailTaskIdElement?.value;
514
+ if (currentOpenTaskId === taskId) {
515
+ // Add the new comment to the UI list
516
+ addCommentToUI(comment); // Use the granular add function
517
+ console.log('comments.js: commentAdded handler finished.');
518
+ } else {
519
+ console.log('comments.js: commentAdded received, but modal is for a different task. Ignoring.');
520
+ }
521
+ });
522
+
523
+ // Listen for individual comment updated event from Manager
524
+ eventBus.subscribe('commentUpdated', ({ taskId, comment }) => {
525
+ console.log('comments.js: EventBus received commentUpdated.', comment.id, 'for Task ID', taskId);
526
+ const currentOpenTaskId = detailTaskIdElement?.value;
527
+ if (currentOpenTaskId === taskId) {
528
+ // Update the comment in the UI list
529
+ updateCommentInUI(comment); // Use the granular update function
530
+ console.log('comments.js: commentUpdated handler finished.');
531
+ } else {
532
+ console.log('comments.js: commentUpdated received, but modal is for a different task. Ignoring.');
533
+ }
534
+ });
535
+
536
+ // Listen for individual comment deleted event from Manager (soft or hard delete)
537
+ eventBus.subscribe('commentDeleted', ({ taskId, commentId }) => {
538
+ console.log('comments.js: EventBus received commentDeleted.', commentId, 'for Task ID', taskId);
539
+ const currentOpenTaskId = detailTaskIdElement?.value;
540
+ if (currentOpenTaskId === taskId) {
541
+ // Remove the comment from the UI list
542
+ removeCommentFromUI(commentId); // Use the granular remove function
543
+ console.log('comments.js: commentDeleted handler finished.');
544
+ } else {
545
+ console.log('comments.js: commentDeleted received, but modal is for a different task. Ignoring.');
546
+ }
547
+ });
548
+
549
+ // Listen for comment action failed events from Manager
550
+ eventBus.subscribe('commentSaveFailed', ({ taskId, commentId, error, message }) => {
551
+ console.error(`comments.js: EventBus received commentSaveFailed for Task ID ${taskId}, Comment ID ${commentId}. Message:`, message, 'Error:', error);
552
+ // Display error message to the user
553
+ handleError(error, message); // Use the imported error handler
554
+ // Keep inline edit or form open as appropriate
555
+ });
556
+
557
+ eventBus.subscribe('commentDeleteFailed', ({ taskId, commentId, error, message }) => {
558
+ console.error(`comments.js: EventBus received commentDeleteFailed for Task ID ${taskId}, Comment ID ${commentId}. Message:`, message, 'Error:', error);
559
+ // Display error message to the user
560
+ handleError(error, message); // Use the imported error handler
561
+ // Keep delete button/UI state as is
562
+ });
563
+
564
+
565
+ }
566
+
567
+ // --- Setup Functions ---
568
+
569
+ /**
570
+ * Sets up the comment form for a specific task.
571
+ * This is called when the task detail modal is opened.
572
+ * @param {string} taskId - The ID of the task.
573
+ */
574
+ function setupCommentForm(taskId) {
575
+ console.log('comments.js: setupCommentForm called for Task ID:', taskId);
576
+ if (!commentSubmitButtonElement || !detailTaskIdElement) {
577
+ console.warn('comments.js: setupCommentForm - Required elements for form setup not found.');
578
+ return;
579
+ }
580
+
581
+ detailTaskIdElement.value = taskId; // Set the current task ID in the hidden input
582
+
583
+ // Check if user data is available and enable/disable the comment submit button
584
+ const currentUser = userManager.getCurrentUserData();
585
+ console.log('comments.js: setupCommentForm - Checking currentUserData for button state:', currentUser);
586
+
587
+ if (currentUser && currentUser.id) {
588
+ console.log('comments.js: setupCommentForm - User logged in. Enabling comment submit button.');
589
+ commentSubmitButtonElement.disabled = false; // Enable the button
590
+ } else {
591
+ console.log('comments.js: setupCommentForm - User not logged in. Disabling comment submit button.');
592
+ commentSubmitButtonElement.disabled = true; // Disable the button
593
+ // Optionally set placeholder text or show message indicating login is required
594
+ commentTextareaElement.placeholder = 'Đăng nhập để thêm bình luận...';
595
+ }
596
+
597
+ // Trigger loading comments for this task. The rendering will happen
598
+ // when the 'taskCommentsLoaded' event is received from the Manager.
599
+ taskManager.handleLoadTaskComments(taskId); // Call manager to load comments
600
+
601
+
602
+ }
603
+
604
+
605
+ /**
606
+ * Handles opening the task detail modal and populating task data.
607
+ * This function remains in comments.js as it sets up the modal that includes comments.
608
+ * However, it gets task data from taskManager.
609
+ * @param {object} task - The task object.
610
+ */
611
+ async function openTaskDetailModal(task) { // Removed taskId parameter as it's in task object
612
+ console.log('comments.js: openTaskDetailModal called with task:', task);
613
+ if (!task || !task.id) {
614
+ console.error('comments.js: openTaskDetailModal called with missing or invalid task object.');
615
+ return;
616
+ }
617
+
618
+ const taskId = task.id; // Get task ID from the task object
619
+
620
+ // --- Populate Task Details (UI Manipulation) ---
621
+ const detailTaskTitleElement = document.getElementById('detailTaskTitle');
622
+ const detailTaskBodyElement = document.getElementById('detailTaskBody');
623
+
624
+ if (!detailTaskTitleElement || !detailTaskBodyElement || !detailTaskIdElement) {
625
+ console.error('comments.js: openTaskDetailModal - Required task detail elements not found.');
626
+ return;
627
+ }
628
+
629
+ detailTaskTitleElement.textContent = task.title || 'Chi tiết công việc';
630
+
631
+ // Fetch departments to display names instead of IDs - Get from manager cache
632
+ const departmentsData = departmentManager.getDepartmentsData();
633
+ const departmentsMap = new Map(departmentsData.map(dept => [dept.id, dept.name]));
634
+
635
+ // Get user data from manager cache
636
+ const usersData = userManager.getAllUsersFromCache();
637
+ const assignedUser = usersData.find(user => user.id === task.assignedToUid)?.name || task.assignedToUid || 'Không xác định'; // Use assignedToUid
638
+ const assignedAvatar = `https://ui-avatars.com/api/?name=${encodeURIComponent(assignedUser)}&background=3b82f6&color=fff`;
639
+
640
+ let createdAtFormatted = 'N/A';
641
+ if (task.createdAt) {
642
+ if (typeof task.createdAt.toDate === 'function') {
643
+ // It's likely a Firestore Timestamp
644
+ createdAtFormatted = moment(task.createdAt.toDate()).format('DD/MM/YYYY HH:mm');
645
+ } else {
646
+ // Try parsing directly (might be a string or other date object)
647
+ createdAtFormatted = moment(task.createdAt).format('DD/MM/YYYY HH:mm');
648
+ }
649
+ }
650
+ const dueDateFormatted = task.dueDate ? moment(task.dueDate).format('DD/MM/YYYY HH:mm') : 'N/A'; // Ensure moment is available
651
+ const progress = task.progress || 0;
652
+
653
+
654
+ detailTaskBodyElement.innerHTML = `
655
+ <input type="hidden" id="detailTaskId" value="${taskId}">
656
+ <div class="space-y-3 text-gray-700 text-sm">
657
+ <p><strong>Mô tả:</strong> ${escapeHtml(task.description || 'Không có')}</p>
658
+ <p><strong>Phòng ban:</strong> ${escapeHtml(departmentsMap.get(task.departmentId) || 'Không xác định')}</p> <!-- Use departmentId -->
659
+ <div class="flex items-center gap-3">
660
+ <img src="${assignedAvatar}" class="w-10 h-10 rounded-full border" alt="Avatar">
661
+ <span><strong>Người thực hiện:</strong> ${escapeHtml(assignedUser)}</span>
662
+ </div>
663
+ <div class="space-y-1">
664
+ <p><strong>Tiến độ:</strong></p>
665
+ <div class="w-full bg-gray-200 rounded-full h-4 overflow-hidden">\n <div class=\"bg-green-500 h-4 text-xs text-white text-center transition-all duration-300\" style=\"width: ${progress}%\">${progress}%</div>\n </div>\n </div>\n <p><strong>Ngày giao:</strong> ${createdAtFormatted}</p>\n <p><strong>Hạn chót:</strong> ${dueDateFormatted}</p>\n <p><strong>Trạng thái:</strong> ${escapeHtml(task.status)}</p>\n </div>\n `;
666
+
667
+ // Use showModal from modal.js
668
+ showModal('taskDetailModal'); // Assuming the task detail modal has ID 'taskDetailModal'
669
+
670
+ // Setup the comment form and load comments after the modal is shown and basic task data is rendered
671
+ // A small delay helps ensure the DOM elements for comments are ready
672
+ setTimeout(() => setupCommentForm(taskId), 50);
673
+
674
+ // Publish an event indicating the modal is opened for this task.
675
+ // Other modules (like task list rendering) might listen to this.
676
+ eventBus.publish('taskDetailModalOpened', taskId);
677
+
678
+ }
679
+
680
+ // Export functions that might be needed externally
681
+ export {
682
+ setupCommentForm,
683
+ openTaskDetailModal,
684
+ // loadTaskComments, // No longer needed for direct external calls from UI
685
+ // enableInlineEdit, // Called via event delegation, no need to export
686
+ // saveEditedComment, // Called via event delegation, no need to export
687
+ // cancelEditComment, // Called via event delegation, no need to export
688
+ // deleteComment, // Called via event delegation, no need to export
689
+ };
public/js/dashboard.js ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // public/js/dashboard.js
2
+
3
+ import { db, appId, userId } from './firebase-init.js'; // userId không được export, chỉ cần db và appId
4
+
5
+ let taskStatusByDepartmentChart;
6
+
7
+ export async function loadDashboardStats() {
8
+ // if (!db || !userId) return; // userId cũng không được export từ firebase-init.js, nếu cần userId hãy lấy từ currentUserData trong userManager
9
+ if (!db || !appId) return; // Kiểm tra db và appId thay vì userId
10
+
11
+ const snapshot = await db.collection('artifacts').doc(appId).collection('tasks').get(); //
12
+ let total = 0, done = 0, pending = 0, overdue = 0;
13
+ const now = new Date();
14
+
15
+ snapshot.forEach(doc => {
16
+ const task = doc.data();
17
+ total++;
18
+ if (task.status === 'Hoàn thành') done++;
19
+ else if (['Đang tiến hành', 'Đã duyệt', 'Từ chối', 'Cần làm'].includes(task.status)) pending++;
20
+ if (task.dueDate && typeof task.dueDate.toDate === 'function' && task.status !== 'Hoàn thành' && task.dueDate.toDate() < now) overdue++;
21
+ });
22
+
23
+ document.getElementById('totalTasks').textContent = total;
24
+ document.getElementById('completedTasks').textContent = done;
25
+ document.getElementById('pendingTasks').textContent = pending;
26
+ document.getElementById('overdueTasks').textContent = overdue;
27
+ }
28
+
29
+ export async function loadDepartmentPerformance() {
30
+ const list = document.getElementById('departmentPerformanceList');
31
+ if (!list) return;
32
+
33
+ const deptsSnapshot = await db.collection('artifacts').doc(appId).collection('departments').get(); //
34
+ const tasksSnapshot = await db.collection('artifacts').doc(appId).collection('tasks').get(); //
35
+ const departments = deptsSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
36
+ const tasks = tasksSnapshot.docs.map(doc => doc.data());
37
+
38
+ list.innerHTML = '';
39
+
40
+ departments.forEach(dept => {
41
+ let total = 0, done = 0;
42
+ tasks.forEach(task => {
43
+ if (task.department === dept.name) {
44
+ total++;
45
+ if (task.status === 'Hoàn thành') done++;
46
+ }
47
+ });
48
+ const percent = total > 0 ? Math.round((done / total) * 100) : 0;
49
+ const color = percent >= 75 ? 'bg-green-500' : percent >= 50 ? 'bg-yellow-500' : 'bg-red-500';
50
+
51
+ const li = document.createElement('li');
52
+ li.className = 'p-3 border-b last:border-b-0 flex items-center';
53
+ li.innerHTML = `
54
+ <div class="flex-1">
55
+ <p class="font-semibold">${dept.name}</p>
56
+ <div class="w-full bg-gray-200 rounded-full h-2.5 mt-1">
57
+ <div class="h-2.5 rounded-full ${color}" style="width: ${percent}%"></div>
58
+ </div>
59
+ </div>
60
+ <span class="ml-4 text-sm font-medium text-gray-700">${percent}%</span>`;
61
+ list.appendChild(li);
62
+ });
63
+ }
64
+
65
+ export async function loadRecentActivities() {
66
+ const list = document.getElementById('recentActivitiesList');
67
+ if (!list) return;
68
+
69
+ const doc = await db.collection('artifacts').doc(appId).collection('activities').doc('recent_activities').get(); //
70
+ list.innerHTML = '';
71
+
72
+ if (doc.exists) {
73
+ const activities = (doc.data().list || []).sort((a, b) => b.timestamp.toDate() - a.timestamp.toDate());
74
+ activities.slice(0, 5).forEach(activity => {
75
+ const li = document.createElement('li');
76
+ li.className = 'p-3 border-b last:border-b-0 text-sm text-gray-700';
77
+ li.innerHTML = `
78
+ <p>${activity.message}</p>
79
+ <p class="text-xs text-gray-500 mt-1">${moment(activity.timestamp.toDate()).fromNow()}</p>`;
80
+ list.appendChild(li);
81
+ });
82
+ } else {
83
+ list.innerHTML = '<p class="p-3 text-gray-500">Chưa có hoạt động gần đây.</p>';
84
+ }
85
+ }
86
+
87
+ export async function loadTaskStatusByDepartmentChart() {
88
+ const ctx = document.getElementById('taskStatusByDepartmentChart')?.getContext('2d');
89
+ if (!ctx) return;
90
+
91
+ const deptSnapshot = await db.collection('artifacts').doc(appId).collection('departments').get(); //
92
+ const taskSnapshot = await db.collection('artifacts').doc(appId).collection('tasks').get(); //
93
+ const departments = deptSnapshot.docs.map(doc => doc.data().name);
94
+ const tasks = taskSnapshot.docs.map(doc => doc.data());
95
+
96
+ const statusLabels = ['Cần làm', 'Đang tiến hành', 'Từ chối', 'Hoàn thành'];
97
+ const dataMap = {};
98
+ departments.forEach(name => {
99
+ dataMap[name] = { 'Cần làm': 0, 'Đang tiến hành': 0, 'Từ chối': 0, 'Hoàn thành': 0 };
100
+ });
101
+
102
+ tasks.forEach(task => {
103
+ const dept = task.department;
104
+ if (dept && dataMap[dept]) {
105
+ const status = task.status === 'Đã duyệt' ? 'Hoàn thành' : task.status;
106
+ if (dataMap[dept][status] !== undefined) {
107
+ dataMap[dept][status]++;
108
+ }
109
+ }
110
+ });
111
+
112
+ const datasets = statusLabels.map(status => {
113
+ const data = departments.map(dept => dataMap[dept][status]);
114
+ let backgroundColor = '#d1d5db';
115
+ if (status === 'Đang tiến hành') backgroundColor = '#3b82f6';
116
+ else if (status === 'Từ chối') backgroundColor = '#ef4444';
117
+ else if (status === 'Hoàn thành') backgroundColor = '#6b7280';
118
+ else if (status === 'Cần làm') backgroundColor = '#f59e0b';
119
+
120
+ return { label: status, data, backgroundColor };
121
+ });
122
+
123
+ if (taskStatusByDepartmentChart) {
124
+ taskStatusByDepartmentChart.data.labels = departments;
125
+ taskStatusByDepartmentChart.data.datasets = datasets;
126
+ taskStatusByDepartmentChart.update();
127
+ } else {
128
+ taskStatusByDepartmentChart = new Chart(ctx, {
129
+ type: 'bar',
130
+ data: { labels: departments, datasets },
131
+ options: {
132
+ responsive: true,
133
+ maintainAspectRatio: false,
134
+ plugins: {
135
+ title: { display: true, text: 'Trạng thái công việc theo phòng ban' },
136
+ tooltip: { mode: 'index', intersect: false }
137
+ },
138
+ scales: {
139
+ x: { stacked: true },
140
+ y: { stacked: true, beginAtZero: true }
141
+ }
142
+ }
143
+ });
144
+ }
145
+ }
public/js/departments.js ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // public/js/departments.js
2
+ import { showModal, hideModal, showConfirm } from './modal.js';
3
+ import eventBus from './utils/eventBus.js';
4
+ import * as departmentManager from './managers/departmentManager.js'; // Import the manager
5
+ import { populateAllDepartmentDropdowns } from './utils/dropdownHelpers.js'; // Import from helper file
6
+ // Import hàm từ departmentService.js
7
+ import { handleError } from './utils/errorHandler.js'; // Import the error handler
8
+ /*
9
+ * Tải danh sách phòng ban và hiển thị
10
+ * This function is now primarily triggered by EventBus events or initial page load.
11
+ */export async function loadDepartments(deptManager) { // <--- Đã thêm đối số deptManager
12
+ if (!deptManager) {
13
+ console.error('loadDepartments called without departmentManager');
14
+ return;
15
+ }
16
+ console.log('loadDepartments function called');
17
+ const departmentsList = document.getElementById('departmentsList');
18
+ if (!departmentsList) {
19
+ console.warn('loadDepartments: departmentsList element not found.');
20
+ return;
21
+ }
22
+ departmentsList.innerHTML = ''; // Xóa danh sách cũ
23
+
24
+ try {
25
+ console.log('loadDepartments: Getting departments data from manager');
26
+ // Sử dụng đối số deptManager thay vì biến toàn cục departmentManager
27
+ const departments = deptManager.getDepartmentsData(); // <--- Đã thay đổi
28
+
29
+ if (departments.length === 0) {
30
+ departmentsList.innerHTML = '<li class="p-4 text-center text-gray-500">Chưa có phòng ban nào.</li>';
31
+ console.log('loadDepartments: No departments found.');
32
+ return;
33
+ }
34
+
35
+ console.log(`loadDepartments: Found ${departments.length} departments, starting render.`);
36
+
37
+ departments.forEach(department => {
38
+ const id = department.id;
39
+ const data = department;
40
+ const li = document.createElement('li');
41
+ li.className = 'flex items-center justify-between p-4 hover:bg-gray-50';
42
+ li.innerHTML = `
43
+ <div class="flex-1 flex items-center">
44
+ <i class="fas fa-building text-blue-500 mr-3"></i>
45
+ <p class="text-gray-800 font-medium">${data.name}</p>
46
+ </div>
47
+ <div class="flex space-x-2 admin-only">
48
+ <button data-id="${id}" data-name="${data.name}" class="edit-department-btn text-blue-600 hover:text-blue-800 transition">
49
+ <i class="fas fa-edit"></i> Sửa
50
+ </button>
51
+ <button data-id="${id}" data-name="${data.name}" class="delete-department-btn text-red-600 hover:text-red-800 transition">
52
+ <i class="fas fa-trash-alt"></i> Xóa</i>
53
+ </button>
54
+ </div>
55
+ `;
56
+ departmentsList.appendChild(li);
57
+ });
58
+
59
+ // **Removed direct calls to populateDepartmentDropdown here.**
60
+ // Dropdown updates will now be handled by the EventBus listeners.
61
+
62
+
63
+ } catch (err) {
64
+ console.error('loadDepartments error:', err);
65
+ handleError(err, 'Không thể tải danh sách phòng ban.');
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Mở modal sửa phòng ban và điền dữ liệu (remains the same)
71
+ */
72
+ export function editDepartment(id, name) {
73
+ console.log('Entering editDepartment function with ID:', id, 'Name:', name);
74
+ const editDepartmentModal = document.getElementById('editDepartmentModal');
75
+ const editDepartmentId = document.getElementById('editDepartmentId');
76
+ const editDepartmentName = document.getElementById('editDepartmentName');
77
+ const editDepartmentForm = document.getElementById('editDepartmentForm');
78
+
79
+ if (editDepartmentId && editDepartmentName && editDepartmentModal && editDepartmentForm) {
80
+ editDepartmentId.value = id;
81
+ editDepartmentName.value = name;
82
+
83
+ showModal('editDepartmentModal');
84
+ } else {
85
+ console.error('editDepartment: Required modal elements not found.');
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Mở modal thêm phòng ban (remains the same)
91
+ */
92
+ export function addDepartment() {
93
+ console.log('departments.js: addDepartment function called');
94
+ console.log('addDepartment function called.');
95
+ const form = document.getElementById('addDepartmentForm');
96
+ if (form) {
97
+ form.reset();
98
+ } else {
99
+ console.error('addDepartment: addDepartmentForm element not found.');
100
+ }
101
+ showModal('addDepartmentModal');
102
+ }
103
+
104
+ /**
105
+ * Setup event listeners for department-related events from Event Bus
106
+ */
107
+ function setupDepartmentEventListeners() {
108
+ // Listen for successful department update from the manager
109
+ eventBus.subscribe('departmentUpdated', (updatedDepartment) => {
110
+ console.log('departments.js: EventBus received departmentUpdated. Hiding modal and showing success message.', updatedDepartment);
111
+ hideModal('editDepartmentModal');
112
+ });
113
+ // Listen for the event published when the "Add new department" button is clicked.
114
+ eventBus.subscribe('addDepartmentClicked', () => {
115
+ addDepartment(); // Call the function to open the add department modal
116
+ });
117
+
118
+ console.log('Setting up department event listeners via EventBus');
119
+ // Listen for the event from the manager indicating department data is ready
120
+ eventBus.subscribe('departmentsDataReady', () => {
121
+ console.log('departments.js: EventBus received departmentsDataReady. Updating UI.');
122
+ console.log('EventBus: departmentsDataReady event received. Updating UI.');
123
+ loadDepartments(departmentManager); // Reload department list
124
+
125
+ // Logic to reset state and hide modal after successful add
126
+ // is now handled in listeners.js where the submit event is processed.
127
+
128
+ console.log('departments.js: Reset isAddingDepartment flag and button state.');
129
+ });
130
+
131
+ // Add event delegation for edit and delete buttons on the departments list
132
+ const departmentsList = document.getElementById('departmentsList');
133
+ if (departmentsList) {
134
+ departmentsList.addEventListener('click', async (event) => {
135
+ try {
136
+ console.log('Click event on departmentsList detected.');
137
+ const target = event.target;
138
+ const editButton = target.closest('.edit-department-btn');
139
+ const deleteButton = target.closest('.delete-department-btn');
140
+
141
+ if (editButton) {
142
+ const id = editButton.dataset.id;
143
+ const name = editButton.dataset.name;
144
+ console.log('Edit button found:', editButton);
145
+ editDepartment(id, name); // Call the function defined in this file
146
+ } else if (deleteButton) {
147
+ const id = deleteButton.dataset.id;
148
+ console.log('Delete button found:', deleteButton);
149
+ const confirmed = await showConfirm('Xác nhận xóa', 'Bạn có chắc chắn muốn xóa phòng ban này không?');
150
+ if (confirmed) {
151
+ eventBus.publish('deleteDepartmentClicked', id); // Publish event for manager to handle
152
+ }
153
+ }
154
+ } catch (err) {
155
+ console.error('Error in departmentsList click listener:', err);
156
+ }
157
+ });
158
+ console.log('Event delegation listener added for #departmentsList in setupDepartmentEventListeners.');
159
+ } else {
160
+ console.warn('setupDepartmentEventListeners: #departmentsList element not found, skipping click listener setup.');
161
+ }
162
+ // Note: Success/Error messages and modal hiding are now handled by the Manager
163
+ // or implicitly by the UI forms after calling the Manager.
164
+ }
165
+
166
+
167
+ // Khối DOMContentLoaded chỉ nên chứa mã thực thi khi DOM đã sẵn sàng
168
+ document.addEventListener('DOMContentLoaded', () => {
169
+ // Initial load of departments when the page loads (optional, depending on app flow)
170
+ // If departments list is shown by default, call loadDepartments() here.
171
+ // If it's loaded on a specific tab/page, call it in the handler for that tab/page.
172
+ // Assuming it loads with the page for now:
173
+ // if (addDepartmentBtn) {
174
+ setupDepartmentEventListeners();
175
+ // addDepartmentBtn.addEventListener('click', addDepartment);
176
+ // }
177
+ });
public/js/firebase-init.js ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // public/js/firebase-init.js
2
+
3
+ export let app, db, auth, rtdb;
4
+ // Thêm export appId ở đây
5
+ export let appId, userId; //
6
+
7
+ export async function initializeFirebase() {
8
+ console.log('Initializing Firebase...');
9
+ const firebaseConfig = {
10
+ apiKey: "AIzaSyAh3e5wBNfeQX5EO9DALjEQGXH9OrH3bUA",
11
+ authDomain: "qlnb-web-app.firebaseapp.com",
12
+ databaseURL: "https://qlnb-web-app-default-rtdb.asia-southeast1.firebasedatabase.app",
13
+ projectId: "qlnb-web-app",
14
+ storageBucket: "qlnb-web-app.firebasestorage.app",
15
+ messagingSenderId: "871217970406",
16
+ appId: "1:871217970406:web:6b3482a4efeaa869bccf27",
17
+ measurementId: "G-FSGHYLQNSJ"
18
+ };
19
+
20
+ try {
21
+ // Initialize Firebase app using compat namespace
22
+ app = firebase.initializeApp(firebaseConfig);
23
+ appId = firebaseConfig.appId; // Gán appId từ firebaseConfig
24
+
25
+ // Get service instances using compat namespace
26
+ db = firebase.firestore();
27
+ auth = firebase.auth();
28
+ rtdb = firebase.database();
29
+ // Check if analytics is available before getting it
30
+ // Use firebase.analytics() if available, otherwise skip
31
+ if (firebase.analytics) {
32
+ try {
33
+ const analytics = firebase.analytics();
34
+ console.log('Firebase Analytics initialized.');
35
+ } catch (e) {
36
+ console.warn('Firebase Analytics failed to initialize:', e);
37
+ }
38
+ } else {
39
+ console.warn('Firebase Analytics SDK not available.');
40
+ }
41
+
42
+ console.log('Firebase initialized successfully.');
43
+ // Remove the Auth state observer - This logic belongs in userManager.js
44
+ console.log('Firebase initialization promise resolved.');
45
+ return Promise.resolve();
46
+
47
+ } catch (error) {
48
+ console.error('Firebase initialization error:', error);
49
+ return Promise.reject(error);
50
+ }
51
+ }
52
+
53
+ // Call initializeFirebase() early in your main script (e.g., index.js)
54
+ // and await it before doing anything that depends on Firebase.
public/js/index.js ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // js/index.js
2
+ import {
3
+ initializeFirebase
4
+ } from './firebase-init.js'; // Keep this import for initial Firebase setup
5
+ import * as taskManager from './managers/taskManager.js'; // Import taskManager
6
+ import * as departmentManager from './managers/departmentManager.js'; // Import departmentManager
7
+ import {
8
+ setupEventListeners
9
+ } from './listeners.js';
10
+ import {
11
+ loadDepartments
12
+ } from './departments.js'; // Keep for window assignments for now
13
+ import * as userManager from './managers/userManager.js'; // Import userManager
14
+ import {
15
+ setupTaskModalForAdd,
16
+ editTask
17
+ } from './tasks.js';
18
+ import {
19
+ setupKanbanEventListeners
20
+ } from './kanban.js';
21
+ import eventBus from './utils/eventBus.js'; // Still needed for other potential subscriptions here
22
+
23
+ import { showModal, hideModal } from './modal.js'; // Import modal functions
24
+ // Gán các hàm lên window để gọi từ HTML
25
+ window.deleteTask = function(taskId) {
26
+ taskManager.handleDeleteTask(taskId);
27
+ }; // Define wrapper function
28
+ window.setupTaskModalForAdd = setupTaskModalForAdd;
29
+
30
+ // Handle mobile menu toggle
31
+ document.addEventListener('DOMContentLoaded', () => {
32
+ const mobileMenuButton = document.getElementById('mobile-menu-button');
33
+ const overlay = document.getElementById('sidebar-overlay');
34
+ const closeSidebarButton = document.getElementById('close-sidebar-button'); // Get the close button
35
+
36
+ // Function to close the sidebar
37
+ const closeSidebar = () => {
38
+ document.body.classList.add('sidebar-closing'); // Add temporary class
39
+ document.body.classList.remove('sidebar-open');
40
+ // Remove the temporary class after a short delay
41
+ setTimeout(() => document.body.classList.remove('sidebar-closing'), 300);
42
+ };
43
+
44
+ if (mobileMenuButton && overlay) {
45
+ mobileMenuButton.addEventListener('click', () => {
46
+ document.body.classList.toggle('sidebar-open');
47
+ });
48
+ overlay.addEventListener('click', closeSidebar); // Call closeSidebar function
49
+ }
50
+
51
+ // Add event listener for the close button if it exists
52
+ if (closeSidebarButton) {
53
+ closeSidebarButton.addEventListener('click', closeSidebar); // Call closeSidebar function
54
+ }
55
+
56
+ // Initialize managers (which may set up auth state observers and load initial data)
57
+ // The order of initialization might matter if managers depend on each other for initial data load.
58
+ // Let's initialize userManager first as many other managers might depend on user authentication state.
59
+ userManager.initUserManager();
60
+ departmentManager.initDepartmentManager();
61
+ taskManager.initTaskManager(); // Initialize task manager to set up its listeners
62
+
63
+ // Setup general UI listeners early that don't strictly depend on initial data load
64
+ setupEventListeners();
65
+ setupKanbanEventListeners();
66
+
67
+
68
+ // REMOVED: The listener for currentUserDataLoaded should be in taskManager.js, not here.
69
+ // subscribe('currentUserDataLoaded', () => {
70
+ // console.log('index.js: currentUserDataLoaded event received. Loading tasks...');
71
+ // // The loading of tasks based on currentUserDataLoaded is handled internally by taskManager
72
+ // });
73
+
74
+
75
+ });
76
+ // Khởi tạo Firebase và load app (Đảm bảo Firebase được initialized trước khi manager cần)
77
+ initializeFirebase().then(() => {
78
+ // Listen for auth state changes
79
+ firebase.auth().onAuthStateChanged(async (user) => {
80
+ const loginModal = document.getElementById('loginModal');
81
+ const sidebar = document.getElementById('sidebar');
82
+ const mainContent = document.getElementById('main-content');
83
+
84
+ if (user) {
85
+ // User is signed in.
86
+ console.log('User is logged in:', user.uid);
87
+ hideModal('loginModal');
88
+ if (sidebar) sidebar.style.display = 'flex'; // Show sidebar
89
+ if (mainContent) mainContent.style.display = 'flex'; // Show main content
90
+ // Load all necessary data after successful login
91
+ } else {
92
+ // User is signed out.
93
+ console.log('User is logged out');
94
+ if (sidebar) sidebar.style.display = 'none'; // Hide sidebar
95
+ if (mainContent) mainContent.style.display = 'none'; // Hide main content
96
+ showModal('loginModal');
97
+ }
98
+ });
99
+ }).catch(error => console.error("Failed to initialize Firebase:", error));
public/js/kanban.js ADDED
@@ -0,0 +1,389 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getAllTasksFromCache } from './managers/taskManager.js'; // Import taskManager functions
2
+ import { getCurrentUserId, getCurrentUserData, getUserData } from './managers/userManager.js';
3
+ import { showModal, hideModal, showMessage, showConfirm } from './modal.js';
4
+ import * as userManager from './managers/userManager.js'; // Import userManager
5
+ import eventBus from './utils/eventBus.js';
6
+ import { generateAvatarUrl } from './utils/utils.js'; // Ensure usersMap is imported
7
+ import { openTaskDetailModal } from './comments.js'; // Import the modal function
8
+ import { editTask } from './tasks.js';
9
+
10
+
11
+ function getStatusColor(status) {
12
+ switch (status) {
13
+ case 'Cần làm': return 'text-gray-400';
14
+ case 'Đang tiến hành': return 'text-blue-500';
15
+ case 'Đã duyệt': return 'text-purple-500';
16
+ case 'Từ chối': return 'text-red-500';
17
+ case 'Hoàn thành': return 'text-green-500';
18
+ default: return 'text-gray-300';
19
+ }
20
+ }
21
+
22
+ function getStatusTextColor(status) {
23
+ switch (status) {
24
+ case 'Cần làm': return 'text-gray-600';
25
+ case 'Đang tiến hành': return 'text-blue-600';
26
+ case 'Đã duyệt': return 'text-purple-600';
27
+ case 'Từ chối': return 'text-red-600';
28
+ case 'Hoàn thành': return 'text-green-600';
29
+ default: return 'text-gray-500';
30
+ }
31
+ }
32
+
33
+ function getPriorityClass(priority) {
34
+ switch (priority) {
35
+ case 'Cao': return 'text-red-800 bg-red-100';
36
+ case 'Trung bình': return 'text-yellow-800 bg-yellow-100';
37
+ case 'Thấp': return 'text-green-800 bg-green-100';
38
+ default: return 'text-gray-800 bg-gray-100';
39
+ }
40
+ }
41
+
42
+ function createTaskCardElement(task, taskId, isDraggable = true) {
43
+ console.log('createTaskCardElement: Current user data:', userManager.getCurrentUserData());
44
+ console.log('createTaskCardElement: Current user role for admin check:', userManager.getCurrentUserData()?.role);
45
+ console.log('createTaskCardElement: Current user role:', getCurrentUserData()?.role);
46
+ const taskCard = document.createElement('div');
47
+ taskCard.className = 'p-4 bg-white rounded-lg shadow task-card mb-4 cursor-pointer hover:shadow-md transition-shadow duration-200';
48
+ taskCard.draggable = isDraggable;
49
+ taskCard.id = `task-${taskId}-${isDraggable ? 'main' : 'db'}`; // Unique ID based on task ID and board type
50
+
51
+ if (isDraggable) {
52
+ taskCard.addEventListener('dragstart', handleDragStart);
53
+ }
54
+
55
+ // Click listener for opening task detail modal
56
+ taskCard.addEventListener('click', (e) => {
57
+ console.log('createTaskCardElement: Task card clicked.');
58
+ // Prevent opening modal when clicking on action buttons or forms within the card
59
+ if (
60
+ e.target.closest('.comment-action') ||
61
+ e.target.closest('form') ||
62
+ e.target.closest('.task-action') || // Ensure task action buttons have this class or similar
63
+ e.target.closest('.admin-actions') // Admin action buttons
64
+ ) return;
65
+ openTaskDetailModal(task, taskId);
66
+ console.log('Task object data:', task);
67
+ });
68
+
69
+ const assignedUser = userManager.getUserData(task.assignedToUid)?.name || 'Chưa gán'; // Access userManager here
70
+ const assignedAvatar = generateAvatarUrl(assignedUser);
71
+ let createdDate = 'N/A';
72
+ if (task.createdAt) {
73
+ if (typeof task.createdAt.toDate === 'function') {
74
+ // It's likely a Firestore Timestamp
75
+ createdDate = moment(task.createdAt.toDate()).format('DD/MM/YYYY');
76
+ } else {
77
+ // Try parsing directly (e.g., if it's a string)
78
+ createdDate = moment(task.createdAt).format('DD/MM/YYYY');
79
+ }
80
+ }
81
+ const dueDateForDisplay = task.dueDate ? moment(task.dueDate).format('DD/MM/YYYY') : 'Không có';
82
+ const dueDateForInput = task.dueDate ? moment(task.dueDate).format('YYYY-MM-DD') : ''; // Format for input
83
+
84
+ const progress = task.progress || 0;
85
+
86
+ taskCard.innerHTML = `
87
+ <div class="flex items-center mb-1">
88
+
89
+
90
+ <i class="fas fa-circle text-xs mr-1 ${getStatusColor(task.status)}"></i>
91
+ <span class="text-xs font-medium ${getStatusTextColor(task.status)}">${task.status}</span>
92
+ </div>
93
+ <div class="flex items-center justify-between mb-2">
94
+ <span class="text-sm font-semibold text-gray-900 truncate" title="${task.title}">${task.title}</span>
95
+ <span class="inline-flex px-2 text-xs font-semibold leading-5 ${getPriorityClass(task.priority)} rounded-full">${task.priority}</span>
96
+ </div>
97
+ <p class="text-xs text-gray-600 mb-2 truncate" title="${task.description || ''}">${task.description || 'Không có mô tả'}</p>
98
+ <div class="text-xs text-gray-500 mb-1">
99
+ <p><strong>Ngày giao:</strong> ${createdDate}</p>
100
+
101
+
102
+ <p><strong>Hạn chót:</strong> ${dueDateForDisplay}</p>
103
+ </div>
104
+ <div class="flex items-center justify-between mt-2">
105
+ <div class="flex items-center">
106
+ <img class="w-6 h-6 rounded-full" src="${assignedAvatar}" alt="${assignedUser}">
107
+ <span class="ml-1 text-xs text-gray-600 truncate" title="${assignedUser}">${assignedUser}</span>
108
+ </div>
109
+ <span class="text-xs text-gray-500">${progress}%</span>
110
+ </div>
111
+ <div class="mt-2">
112
+ <div class="w-full bg-gray-200 rounded-full h-1.5">
113
+ <div class="bg-blue-600 h-1.5 rounded-full" style="width: ${progress}%"></div>
114
+ </div>
115
+ </div>`;
116
+ console.log('Created task card for ID:', taskId, 'HTML:', taskCard.outerHTML);
117
+
118
+ // Admin action buttons (Sửa, Xoá)
119
+ if (getCurrentUserData()?.role === 'admin') {
120
+ // Restore onclick handlers for edit and delete buttons
121
+ taskCard.innerHTML += `
122
+ <div class="mt-2 flex gap-2 text-xs text-blue-600 admin-actions">
123
+ <button
124
+ class="text-blue-600 hover:text-blue-800 edit-task-button task-action"
125
+ data-id="${taskId}"
126
+ data-title="${task.title}"
127
+ data-description="${task.description || ''}"
128
+ data-department="${task.department || ''}"
129
+ data-assigned-to="${task.assignedTo || ''}"
130
+ data-status="${task.status || ''}"
131
+ data-due-date="${task.dueDate && typeof task.dueDate.toDate === 'function' ? task.dueDate.toDate().toISOString().split('T')[0] : ''}"
132
+ data-priority="${task.priority || 'Trung bình'}"
133
+ data-progress="${task.progress || 0}"
134
+ >Sửa</button>
135
+ <button class="text-red-600 hover:text-red-800 delete-task-button task-action" data-id="${taskId}"
136
+ >Xoá</button>
137
+ </div>
138
+ `;
139
+ }
140
+
141
+ return taskCard;
142
+ }
143
+
144
+ // Helper function to map task status to container elements
145
+ function getContainerByStatus(containers, status, boardType) {
146
+ switch (status) {
147
+ case 'Cần làm':
148
+ return containers[boardType].todo;
149
+ case 'Đang tiến hành':
150
+ return containers[boardType].inProgress;
151
+ case 'Đã duyệt':
152
+ case 'Từ chối': // Assuming rejected also goes to review
153
+ return containers[boardType].review;
154
+ case 'Hoàn thành':
155
+ return containers[boardType].done;
156
+ default:
157
+ return null; // Or a default container
158
+ }
159
+ }
160
+
161
+ // Helper function to update column counts
162
+ function updateColumnCounts(containers, boardType) {
163
+ const counts = containers[boardType].counts;
164
+ const cols = containers[boardType];
165
+
166
+ if (counts.todo) {
167
+ counts.todo.textContent = cols.todo.childElementCount;
168
+ }
169
+
170
+ if (counts.inProgress) {
171
+ counts.inProgress.textContent = cols.inProgress.childElementCount;
172
+ }
173
+
174
+ if (counts.review) {
175
+ counts.review.textContent = cols.review.childElementCount;
176
+ }
177
+
178
+ if (counts.done) {
179
+ counts.done.textContent = cols.done.childElementCount;
180
+ }
181
+
182
+
183
+ }
184
+
185
+ // Function to render tasks on the Kanban board
186
+ function renderKanbanBoard(tasks) {
187
+ console.log('renderKanbanBoard executed. Received tasks:', tasks);
188
+
189
+ console.log('kanban.js: renderKanbanBoard called with', tasks.length, 'tasks');
190
+
191
+ console.log('Starting task rendering loop...');
192
+
193
+ const containers = {
194
+ main: {
195
+ todo: document.getElementById('todoTasks'),
196
+ inProgress: document.getElementById('inProgressTasks'),
197
+ review: document.getElementById('reviewTasks'),
198
+ done: document.getElementById('completedTasksBoard'),
199
+ counts: {
200
+ todo: document.getElementById('todo-count'),
201
+ inProgress: document.getElementById('in-progress-count'),
202
+ review: document.getElementById('review-count'),
203
+ done: document.getElementById('done-count')
204
+ }
205
+ },
206
+ dashboard: {
207
+ todo: document.getElementById('todoTasks-dashboard'),
208
+ inProgress: document.getElementById('inProgressTasks-dashboard'),
209
+ review: document.getElementById('reviewTasks-dashboard'),
210
+ done: document.getElementById('completedTasksBoard-dashboard'),
211
+ counts: {
212
+ todo: document.getElementById('todo-count-dashboard'),
213
+ inProgress: document.getElementById('in-progress-count-dashboard'),
214
+ review: document.getElementById('review-count-dashboard'),
215
+ done: document.getElementById('done-count-dashboard')
216
+ }
217
+ }
218
+ };
219
+
220
+ // Clear existing tasks from all containers
221
+ for (const boardType in containers) {
222
+ for (const colKey in containers[boardType]) {
223
+ // Only clear if it's a DOM element container, not the 'counts' object
224
+ if (containers[boardType][colKey] instanceof Element) {
225
+ containers[boardType][colKey].innerHTML = '';
226
+ console.log(`Cleared container: ${boardType} - ${colKey}`);
227
+ }
228
+ }
229
+ }
230
+
231
+
232
+ // Append tasks to the correct containers
233
+ if (tasks && tasks.length > 0) {
234
+ tasks.forEach((task) => {
235
+ console.log('Processing task:', task.id, 'with status:', task.status);
236
+ const mainContainer = getContainerByStatus(containers, task.status, 'main');
237
+ console.log('Main container for task ID:', task.id, 'is:', mainContainer ? mainContainer.id : 'none');
238
+ const dashboardContainer = getContainerByStatus(containers, task.status, 'dashboard');
239
+
240
+ if (mainContainer) {
241
+ mainContainer.appendChild(createTaskCardElement(task, task.id, true));
242
+ console.log('Attempted to append task ID:', task.id, 'to main container. Resulting child count:', mainContainer ? mainContainer.childElementCount : 'N/A');
243
+ }
244
+ if (dashboardContainer) {
245
+ dashboardContainer.appendChild(createTaskCardElement(task, task.id, false));
246
+ }
247
+ });
248
+ }
249
+
250
+ console.log('Finished task rendering loop.');
251
+
252
+ // Update counts for both boards after rendering
253
+ updateColumnCounts(containers, 'main');
254
+ updateColumnCounts(containers, 'dashboard');
255
+ }
256
+
257
+ // Function to load tasks for the Kanban board by calling the task manager
258
+ async function loadKanbanBoard() {
259
+ console.log('kanban.js: loadKanbanBoard called, calling taskManager.handleLoadTasks...');
260
+ // The tasksLoaded event listener will handle rendering
261
+ }
262
+
263
+ // Function to set up EventBus listeners for Kanban-related events
264
+ export function setupKanbanEventListeners() {
265
+ console.log('kanban.js: Setting up EventBus listeners');
266
+ // Subscribe to task loaded event
267
+ eventBus.subscribe('tasksDataReady', (tasks) => { // Changed from tasksLoaded to tasksDataReady
268
+ console.log('EventBus: tasksDataReady event received in kanban.js. Calling render...');
269
+ console.log('EventBus received tasks data:', tasks); // Log tasks data
270
+ console.log('kanban.js: EventBus - tasksLoaded event received.');
271
+ try {
272
+ renderKanbanBoard(tasks); // Call the render function with the loaded tasks
273
+ } catch (error) {
274
+ console.error('Error rendering Kanban board:', error);
275
+ }
276
+ });
277
+ }
278
+
279
+ // Subscribe to task deletion completion event
280
+ eventBus.subscribe('taskDeleteCompleted', (data) => {
281
+ console.log('taskDeleteCompleted listener in kanban.js triggered');
282
+ showMessage('Thành công', 'Công việc đã được xoá.');
283
+ hideModal('taskDetailModal'); // Hide the task detail modal
284
+ loadKanbanBoard(); // Reload the kanban board to update the view - This will trigger tasksLoaded event
285
+ });
286
+ function handleDragStart(event) {
287
+ // Only allow dragging task cards in the main kanban board
288
+ if (event.target.classList.contains('task-card') && event.target.id.endsWith('-main')) {
289
+ event.dataTransfer.setData("text/plain", event.target.id); // Use text/plain for data type
290
+ console.log('Dragging task card with ID:', event.target.id);
291
+ } else {
292
+ event.preventDefault(); // Prevent dragging on other elements
293
+ console.log('Preventing drag on non-draggable element.');
294
+ }
295
+ }
296
+
297
+ // Define the status mapping based on column IDs
298
+ const statusMap = {
299
+ 'todoTasks': 'Cần làm',
300
+ 'inProgressTasks': 'Đang tiến hành',
301
+ 'reviewTasks': 'Đã duyệt',
302
+ 'completedTasksBoard': 'Hoàn thành',
303
+ 'completedTasksBoard-dashboard': 'Hoàn thành'
304
+ };
305
+
306
+ eventBus.subscribe('taskAdded', (newTask) => {
307
+ // When a task is added, we should reload the board to update the view
308
+ // Or, we could implement logic to add the new task card directly to the UI
309
+ // For simplicity now, let's reload. The tasksLoaded listener will handle rendering.
310
+ // loadKanbanBoard(); // This might cause issues if called too quickly. Let's rely on tasksLoaded from Manager reload.
311
+ });
312
+ import * as taskManager from './managers/taskManager.js'; // Import taskManager
313
+ document.addEventListener('DOMContentLoaded', () => {
314
+ console.log('Calling loadKanbanBoard from kanban.js - DOMContentLoaded');
315
+ // Add drag and drop listeners after the board is loaded
316
+ const kanbanColumns = document.querySelectorAll('.kanban-column');
317
+ kanbanColumns.forEach(column => {
318
+
319
+ // Add a click listener to the column for event delegation
320
+ column.addEventListener('click', async (event) => {
321
+ const targetButton = event.target.closest('.edit-task-button');
322
+ if (targetButton) {
323
+ event.stopPropagation(); // Prevent task card click
324
+ const taskId = targetButton.dataset.id;
325
+ console.log('Edit button clicked for task ID:', taskId);
326
+ // Call the editTask function from tasks.js with data from the button
327
+ // Publish event for tasks.js to handle opening the edit modal
328
+ eventBus.publish('editTaskClicked', { taskId: taskId });
329
+ } else if (event.target.classList.contains('delete-task-button')) {
330
+ event.stopPropagation(); // Prevent task card click
331
+ const taskId = event.target.dataset.id;
332
+ console.log('Delete button clicked for task ID:', taskId);
333
+ const confirmed = await showConfirm('Xác nhận xoá', 'Bạn có chắc chắn muốn xoá công việc này không?');
334
+ if (confirmed) {
335
+ taskManager.handleDeleteTask(taskId);
336
+ eventBus.publish('deleteTaskClicked', taskId);
337
+ }
338
+ }
339
+ });
340
+
341
+ // Allow dropping
342
+ column.addEventListener('dragover', (event) => {
343
+ event.preventDefault();
344
+ });
345
+
346
+ // Handle drop
347
+ column.addEventListener('drop', async (event) => {
348
+ event.preventDefault();
349
+ const taskIdWithPrefix = event.dataTransfer.getData("text/plain");
350
+ const parts = taskIdWithPrefix.split('-');
351
+ const taskId = parts[1];
352
+
353
+ const targetColumn = event.target.closest('.kanban-column');
354
+ if (!targetColumn) {
355
+ console.warn('Drop target is not a kanban column.');
356
+ return;
357
+ }
358
+ const targetColumnId = targetColumn.id;
359
+ const newStatus = statusMap[targetColumnId];
360
+ console.log('Drop Event Debug: taskIdWithPrefix:', taskIdWithPrefix, 'parts:', parts, 'taskId:', taskId); // <-- Moved log here
361
+ console.log('Drop Event Debug: targetColumnId:', targetColumnId, 'statusMap:', statusMap, 'newStatus:', newStatus); // <-- Moved log here
362
+
363
+ // --- Moved console.logs outside the if/else block ---
364
+ /*
365
+ console.log('Drop Event Debug: taskIdWithPrefix:', taskIdWithPrefix, 'parts:', parts, 'taskId:', taskId);
366
+ console.log('Drop Event Debug: targetColumnId:', targetColumnId, 'statusMap:', statusMap, 'newStatus:', newStatus);
367
+ */
368
+ // ----------------------------------------------------
369
+
370
+
371
+
372
+
373
+ if (taskId && newStatus !== undefined) {
374
+ console.log(`Task ID ${taskId} dropped on column ${targetColumnId}, new status: ${newStatus}`);
375
+ try {
376
+ await taskManager.handleUpdateTaskStatus(taskId, newStatus);
377
+ console.log('Calling loadKanbanBoard from kanban.js - drop event listener');
378
+ loadKanbanBoard();
379
+ } catch (err) {
380
+ console.error('Drop update error:', err);
381
+ showMessage('Lỗi', 'Không thể cập nhật trạng thái công việc.');
382
+ }
383
+ } else {
384
+ console.warn(`Drop event: Invalid Task ID (${taskId}) or New Status (${newStatus})`);
385
+ }
386
+ });
387
+ });
388
+ console.log('Drag and drop listeners added to kanban columns.');
389
+ });
public/js/listeners.js ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // public/js/listeners.js
2
+ import { handleLogin, handleRegister, handleLogout, resetPassword, updatePassword } from './auth.js';
3
+ import { showModal, hideModal, showMessage, showConfirm } from './modal.js';
4
+ import * as departmentManager from './managers/departmentManager.js';
5
+ import * as userManager from './managers/userManager.js';
6
+ import * as taskManager from './managers/taskManager.js';
7
+ import { setupTaskModalForAdd } from './tasks.js';
8
+ import { openTaskDetailModal } from './comments.js';
9
+ import eventBus from './utils/eventBus.js';
10
+ // let isAddingDepartment = false; // Quản lý state này có thể cần xem xét lại nếu có xung đột
11
+ import { generateAvatarUrl } from './utils/utils.js'; // Đảm bảo import đúng đường dẫn
12
+ import { editUser } from './users.js';
13
+ import { addDepartment } from './departments.js';
14
+
15
+ export function setupEventListeners() {
16
+ // const sidebarItems = document.querySelectorAll('.sidebar-item'); // Sẽ không dùng cho React Sidebar
17
+ const sections = document.querySelectorAll('.content-section');
18
+
19
+ // Hàm này sẽ được React Sidebar gọi để hiển thị section tương ứng
20
+ window.vanillaNavigate = function(sectionId) {
21
+ console.log("Vanilla JS: window.vanillaNavigate called by React with sectionId:", sectionId);
22
+ sections.forEach(s => s.classList.remove('active'));
23
+ const targetSection = document.getElementById(sectionId);
24
+ if (targetSection) {
25
+ targetSection.classList.add('active');
26
+ }
27
+ // Không cần setActiveItem cho sidebar cũ nữa nếu nó đã bị ẩn/xóa
28
+ window.location.hash = sectionId; // Cập nhật hash
29
+ };
30
+
31
+ // Xử lý hashchange từ trình duyệt (back/forward) hoặc từ JS thuần
32
+ function handleVanillaHashChange() {
33
+ const hash = window.location.hash.substring(1) || 'dashboard-section';
34
+ console.log("Vanilla JS: Hash changed to:", hash);
35
+
36
+ // Hiển thị section tương ứng (React Sidebar sẽ gọi window.vanillaNavigate)
37
+ // Nếu hash thay đổi do trình duyệt, chúng ta vẫn cần hiển thị section
38
+ sections.forEach(s => s.classList.remove('active'));
39
+ const targetSection = document.getElementById(hash);
40
+ if (targetSection) {
41
+ targetSection.classList.add('active');
42
+ }
43
+
44
+ // Thông báo cho React Sidebar biết active section đã thay đổi
45
+ if (window.reactBridge && window.reactBridge.updateActiveSection) {
46
+ console.log("Vanilla JS: Updating React Sidebar active section to:", hash);
47
+ window.reactBridge.updateActiveSection(hash);
48
+ }
49
+ }
50
+ window.addEventListener('hashchange', handleVanillaHashChange);
51
+
52
+ // Gọi lần đầu để đồng bộ (có thể cần trì hoãn một chút để React bridge sẵn sàng)
53
+ setTimeout(() => {
54
+ const initialHash = window.location.hash.substring(1) || 'dashboard-section';
55
+ console.log("Vanilla JS: Initial hash processing for:", initialHash);
56
+ // Hiển thị section ban đầu
57
+ const initialTargetSection = document.getElementById(initialHash);
58
+ if (initialTargetSection) {
59
+ initialTargetSection.classList.add('active');
60
+ }
61
+ // Cập nhật React Sidebar về section ban đầu
62
+ if (window.reactBridge && window.reactBridge.updateActiveSection) {
63
+ window.reactBridge.updateActiveSection(initialHash);
64
+ }
65
+ }, 300); // Tăng thời gian chờ nếu cần
66
+
67
+ // --- Các event listeners khác của bạn giữ nguyên ---
68
+ document.getElementById('loginForm')?.addEventListener('submit', handleLogin);
69
+ document.getElementById('registerForm')?.addEventListener('submit', handleRegister);
70
+ document.getElementById('resetPasswordForm')?.addEventListener('submit', resetPassword);
71
+ document.getElementById('changePasswordForm')?.addEventListener('submit', updatePassword);
72
+
73
+ // Nút logout trong header (nếu vẫn do HTML thuần quản lý)
74
+ const vanillaLogoutButton = document.getElementById('logoutButton');
75
+ if (vanillaLogoutButton) {
76
+ vanillaLogoutButton.addEventListener('click', (e) => {
77
+ e.preventDefault();
78
+ handleLogout(); // Gọi hàm handleLogout đã có của bạn
79
+ });
80
+ }
81
+
82
+
83
+ // User Management (giữ nguyên, nhưng userManager sẽ gọi reactBridge)
84
+ document.getElementById('editUserForm')?.addEventListener('submit', (event) => {
85
+ event.preventDefault();
86
+ const userId = document.getElementById('editUserId').value;
87
+ const name = document.getElementById('editUserName').value.trim();
88
+ const email = document.getElementById('editUserEmail').value.trim();
89
+ const role = document.getElementById('editUserRole').value;
90
+ const department = document.getElementById('editUserDepartment').value;
91
+ const userData = { name, email, role, department };
92
+ eventBus.publish('editUserSubmit', { userId, userData });
93
+ });
94
+
95
+ const usersList = document.getElementById('usersList');
96
+ if (usersList) {
97
+ usersList.addEventListener('click', async (event) => {
98
+ // ... (logic xử lý edit/delete user của b��n giữ nguyên)
99
+ // Ví dụ:
100
+ const target = event.target.closest('button');
101
+ if (!target) return;
102
+ const userId = target.dataset.id;
103
+ // ...
104
+ if (target.classList.contains('edit-user-btn')) {
105
+ eventBus.publish('editUserClicked', { /* ...data... */ });
106
+ } else if (target.classList.contains('delete-user-btn')) {
107
+ // ...
108
+ eventBus.publish('deleteUserClicked', { /* ...data... */ });
109
+ }
110
+ });
111
+ }
112
+
113
+ // Department Management (giữ nguyên, departmentManager sẽ gọi reactBridge)
114
+ document.getElementById('openAddDepartmentModal')?.addEventListener('click', () => {
115
+ eventBus.publish('addDepartmentClicked');
116
+ });
117
+
118
+ const departmentsList = document.getElementById('departmentsList');
119
+ if (departmentsList) {
120
+ departmentsList.addEventListener('click', async (event) => {
121
+ // ... (logic xử lý edit/delete department của bạn giữ nguyên)
122
+ });
123
+ }
124
+
125
+ const addDepartmentForm = document.getElementById('addDepartmentForm');
126
+ if (addDepartmentForm) {
127
+ addDepartmentForm.addEventListener('submit', (event) => {
128
+ event.preventDefault();
129
+ // ... (logic của bạn)
130
+ const departmentNameInput = document.getElementById('departmentName');
131
+ const departmentName = departmentNameInput ? departmentNameInput.value.trim() : '';
132
+ if (departmentName) {
133
+ eventBus.publish('addDepartmentSubmit', departmentName);
134
+ }
135
+ });
136
+ }
137
+
138
+ // Task & Comment listeners (giữ nguyên)
139
+ document.getElementById('openAddTaskModal')?.addEventListener('click', setupTaskModalForAdd);
140
+
141
+ const commentList = document.getElementById('commentList');
142
+ if (commentList) {
143
+ commentList.addEventListener('click', async (event) => {
144
+ // ... (logic xử lý comment của bạn giữ nguyên)
145
+ });
146
+ }
147
+
148
+ // Modal close listeners (giữ nguyên)
149
+ document.querySelectorAll('.modal .close-modal').forEach(button => {
150
+ button.addEventListener('click', () => {
151
+ const modal = button.closest('.modal');
152
+ if (modal) {
153
+ // hideModal(modal.id); // Sử dụng hàm hideModal nếu có
154
+ modal.style.display = 'none';
155
+ }
156
+ });
157
+ });
158
+ // ... các listeners khác cho modal ...
159
+
160
+
161
+ // Lắng nghe sự kiện userDataUpdated từ userManager (hoặc nơi khác) để cập nhật React Sidebar
162
+ eventBus.subscribe('userDataUpdated', (currentUserData) => {
163
+ console.log('Vanilla JS (listeners.js): userDataUpdated event received.', currentUserData);
164
+ if (window.reactBridge && window.reactBridge.updateUserData) {
165
+ if (currentUserData) {
166
+ // Đảm bảo cấu trúc dữ liệu khớp với những gì UserInfo.jsx trong React mong đợi
167
+ const reactUserData = {
168
+ id: currentUserData.id || currentUserData.uid, // Lấy id hoặc uid
169
+ name: currentUserData.name,
170
+ role: currentUserData.role,
171
+ avatar: currentUserData.avatar || generateAvatarUrl(currentUserData.name || 'User')
172
+ };
173
+ console.log("Vanilla JS (listeners.js): Calling reactBridge.updateUserData with:", reactUserData);
174
+ window.reactBridge.updateUserData(reactUserData);
175
+ } else {
176
+ // Người dùng đã đăng xuất
177
+ console.log("Vanilla JS (listeners.js): Calling reactBridge.updateUserData with null (logout)");
178
+ window.reactBridge.updateUserData(null);
179
+ }
180
+ } else {
181
+ console.warn("Vanilla JS (listeners.js): reactBridge or updateUserData not available for user data.");
182
+ }
183
+ });
184
+
185
+ // Lắng nghe sự kiện departmentsDataReady từ departmentManager để cập nhật React Sidebar
186
+ eventBus.subscribe('departmentsDataReady', (departmentsData) => {
187
+ console.log('Vanilla JS (listeners.js): departmentsDataReady event received.', departmentsData);
188
+ if (window.reactBridge && window.reactBridge.updateDepartmentsData) {
189
+ console.log("Vanilla JS (listeners.js): Calling reactBridge.updateDepartmentsData with:", departmentsData);
190
+ window.reactBridge.updateDepartmentsData(departmentsData || []);
191
+ } else {
192
+ console.warn("Vanilla JS (listeners.js): reactBridge or updateDepartmentsData not available for departments.");
193
+ }
194
+ });
195
+ }
public/js/main-loader.js ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { loadDepartments } from './departments.js';
2
+ import { loadKanbanBoard } from './kanban.js';
3
+ import { loadDashboardStats, loadDepartmentPerformance, loadRecentActivities, loadTaskStatusByDepartmentChart } from './dashboard.js';
4
+ import { appId, db, currentUserData } from './firebase-init.js';
5
+
6
+ function waitForFirebaseReady() {
7
+ return new Promise((resolve) => {
8
+ const interval = setInterval(() => {
9
+ if (appId && db && currentUserData?.role) {
10
+ clearInterval(interval);
11
+ resolve();
12
+ }
13
+ }, 100);
14
+ });
15
+ }
16
+
17
+ export async function loadAllData() {
18
+ await waitForFirebaseReady();
19
+ console.log('Calling loadKanbanBoard from main-loader.js');
20
+ loadKanbanBoard();
21
+ loadDashboardStats();
22
+ loadDepartmentPerformance();
23
+ loadRecentActivities();
24
+ loadTaskStatusByDepartmentChart();
25
+ loadDepartments();
26
+ }
27
+
28
+ // Đăng ký event listener 1 lần duy nhất
29
+ export function registerTabListeners() {
30
+ const sidebarItems = document.querySelectorAll('.sidebar-item');
31
+ const sections = document.querySelectorAll('.content-section');
32
+
33
+ function showSection(id) {
34
+ sections.forEach(s => s.classList.remove('active'));
35
+ document.getElementById(id)?.classList.add('active');
36
+ }
37
+
38
+ function setActiveItem(id) {
39
+ sidebarItems.forEach(item => {
40
+ item.classList.toggle('active', item.dataset.target === id);
41
+ });
42
+ }
43
+
44
+ sidebarItems.forEach(item => {
45
+ item.addEventListener('click', e => {
46
+ e.preventDefault();
47
+ const target = item.dataset.target;
48
+ showSection(target);
49
+ setActiveItem(target);
50
+ window.location.hash = target;
51
+ });
52
+ });
53
+ }
public/js/managers/departmentManager.js ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // public/js/managers/departmentManager.js
2
+
3
+ import eventBus from '../utils/eventBus.js';
4
+ import { db } from '../firebase-init.js'; // Import db
5
+ import {
6
+ getDepartments as getServiceDepartments,
7
+ createDepartment as createServiceDepartment,
8
+ updateDepartment as updateServiceDepartment,
9
+ deleteDepartment as deleteServiceDepartment
10
+ } from '../services/departmentService.js';
11
+
12
+ // Import UI functions needed by the manager
13
+ import { handleError } from '../utils/errorHandler.js';
14
+ // import { loadDepartments, populateDepartmentDropdown, populateAllDepartmentDropdowns } from '../departments.js'; // Import UI rendering functions (removed direct calls)
15
+
16
+ // --- Internal State (for caching data) ---
17
+ let departmentsData = [];
18
+
19
+
20
+ /* --- Event Handlers triggered by UI Events (published by departments.js) ---
21
+ * These functions are exported so departments.js can call them directly if needed,
22
+ * although the primary interaction flow is via EventBus. */
23
+
24
+ async function handleAddDepartmentSubmit(departmentName) {
25
+ console.log('departmentManager: handleAddDepartmentSubmit called with name', departmentName);
26
+ if (!departmentName) {
27
+ handleError(new Error('Tên phòng ban không được để trống.'), 'Tên phòng ban không được để trống.');
28
+ return;
29
+ }
30
+
31
+ try {
32
+ const departmentData = { name: departmentName };
33
+ console.log('departmentManager: Calling createServiceDepartment');
34
+ await createServiceDepartment(departmentData);
35
+
36
+ // Data changed on backend, reload and publish
37
+ console.log('departmentManager: Department creation initiated. Awaiting EventBus event.');
38
+
39
+ } catch (error) {
40
+ console.error('departmentManager: Error adding department:', error);
41
+ if (error.message === 'Duplicate department name') {
42
+ handleError(error, `Phòng ban "${departmentName}" đã tồn tại.`);
43
+ } else {
44
+ handleError(error, 'Không thể thêm phòng ban.');
45
+ }
46
+ }
47
+ }
48
+
49
+ export { handleAddDepartmentSubmit };
50
+
51
+ async function handleEditDepartmentSubmit(departmentId, newName) {
52
+ console.log('departmentManager: handleEditDepartmentSubmit called for ID', departmentId, 'with new name', newName);
53
+ if (!newName) {
54
+ handleError(new Error('Tên phòng ban không được để trống.'), 'Tên phòng ban không được để trống.');
55
+ return;
56
+ }
57
+
58
+ try {
59
+ const updateData = { name: newName };
60
+ console.log('departmentManager: Calling updateServiceDepartment');
61
+ await updateServiceDepartment(departmentId, updateData);
62
+
63
+ // Data changed on backend, reload and publish
64
+
65
+ console.log('departmentManager: Department update initiated. Awaiting EventBus event.');
66
+
67
+ } catch (error) {
68
+ console.error(`departmentManager: Error updating department ${departmentId}:`, error);
69
+ handleError(error, 'Không thể cập nhật phòng ban.');
70
+ }
71
+ }
72
+
73
+ export { handleEditDepartmentSubmit };
74
+
75
+ async function handleDeleteDepartmentClick(departmentId) {
76
+ console.log('departmentManager: handleDeleteDepartmentClick called for ID', departmentId);
77
+ console.log(`[DEBUG] handleDeleteDepartmentClick: Starting for ID ${departmentId}`);
78
+ // Confirmation is handled by the UI component (departments.js) using showConfirm
79
+ // The UI component publishes the event *after* confirmation.
80
+
81
+ try {
82
+ console.log('departmentManager: Calling deleteServiceDepartment');
83
+ await deleteServiceDepartment(departmentId);
84
+
85
+ // Data changed on backend, reload and publish
86
+
87
+ console.log('departmentManager: Department deletion initiated. Awaiting EventBus event.');
88
+
89
+ console.log(`[DEBUG] handleDeleteDepartmentClick: Finished for ID ${departmentId}`);
90
+ } catch (error) {
91
+ console.error(`departmentManager: Error deleting department ${departmentId}:`, error);
92
+ handleError(error, `Không thể xóa phòng ban.`);
93
+ }
94
+ }
95
+
96
+ // Function to load departments and publish event
97
+ async function handleLoadDepartments() {
98
+ console.log('departmentManager: handleLoadDepartments called');
99
+ try {
100
+ const departments = await getServiceDepartments();
101
+ departmentsData = departments;
102
+ console.log('departmentManager: Fetched departments. Publishing departmentsDataReady');
103
+ eventBus.publish('departmentsDataReady', departmentsData); // Publish the data
104
+ } catch (error) {
105
+ console.error('departmentManager: Error loading departments:', error);
106
+ // Optionally publish an error event: eventBus.publish('departmentsLoadFailed', error);
107
+ handleError(error, 'Không thể tải danh sách phòng ban.');
108
+ }
109
+ }
110
+
111
+ // Helper to get cached data (for UI to populate dropdowns, etc.)
112
+ export function getDepartmentsData() {
113
+ return departmentsData;
114
+ }
115
+
116
+ export { handleDeleteDepartmentClick };
117
+ // --- EventBus Listeners for Service Events (published by departmentService.js) ---
118
+
119
+ function setupEventBusListeners() {
120
+ console.log('departmentManager: Setting up EventBus listeners for department service events');
121
+ eventBus.subscribe('departmentCreated', (newDepartment) => {
122
+ console.log('departmentManager: EventBus received departmentCreated.', newDepartment);
123
+ console.log('departmentManager: Received departmentCreated event in subscriber', newDepartment);
124
+ // The UI (departments.js) should handle messages and modal hiding, and react to departmentsDataReady
125
+ // showMessage('Thành công', `Đã thêm phòng ban "${newDepartment.name}".`); // Moved to UI listener
126
+ console.log('departmentManager: EventBus subscriber for departmentCreated is running.');
127
+ handleLoadDepartments();
128
+ });
129
+
130
+ eventBus.subscribe('departmentUpdated', (updatedDepartment) => {
131
+ console.log('departmentManager: EventBus received departmentUpdated.', updatedDepartment);
132
+ console.log('departmentManager: EventBus subscriber for departmentUpdated is running.');
133
+ handleLoadDepartments();
134
+ });
135
+
136
+ eventBus.subscribe('departmentDeleted', (deletedDepartmentId) => {
137
+ console.log('departmentManager: EventBus received departmentDeleted.', deletedDepartmentId);
138
+ handleLoadDepartments();
139
+ console.log('departmentManager: EventBus subscriber for departmentDeleted is running.');
140
+ });
141
+ }
142
+
143
+
144
+ // --- Setup UI Event Listeners (listen to events published by departments.js) ---
145
+ function setupUIEventListeners() {
146
+ console.log('departmentManager: Setting up UI Event listeners');
147
+ // Listen for events published by departments.js on UI interactions
148
+ eventBus.subscribe('addDepartmentSubmit', handleAddDepartmentSubmit);
149
+ eventBus.subscribe('editDepartmentSubmit', handleEditDepartmentSubmit);
150
+ eventBus.subscribe('deleteDepartmentClick', handleDeleteDepartmentClick);
151
+ eventBus.subscribe('deleteDepartmentClicked', handleDeleteDepartmentClick);
152
+ }
153
+
154
+ // --- Initialization Function ---
155
+ export function initDepartmentManager() {
156
+ console.log('Initializing Department Manager');
157
+ setupUIEventListeners();
158
+ setupEventBusListeners();
159
+
160
+ // Subscribe to currentUserDataLoaded event from userManager
161
+ // This ensures departments are loaded only after the user is logged in and their data is loaded
162
+ eventBus.subscribe('currentUserDataLoaded', () => {
163
+ console.log('departmentManager: currentUserDataLoaded event received. Loading departments...');
164
+ handleLoadDepartments();
165
+ });
166
+ }
public/js/managers/taskManager.js ADDED
@@ -0,0 +1,545 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // public/js/managers/taskManager.js
2
+
3
+ import eventBus from '../utils/eventBus.js';
4
+ // Import service functions
5
+ import {
6
+ getTasks as getServiceTasks,
7
+ createTask as createServiceTask,
8
+ updateTask as updateServiceTask,
9
+ deleteTask as deleteServiceTask,
10
+ // Import comment service functions from taskService
11
+ getTaskComments as getServiceTaskComments,
12
+ addCommentToTask as addServiceCommentToTask,
13
+ updateTaskComment as updateServiceTaskComment,
14
+ deleteTaskComment as deleteServiceTaskComment
15
+ } from '../services/taskService.js';
16
+
17
+ import * as userManager from './userManager.js'; // Import userManager
18
+ import { generateAvatarUrl } from '../utils/utils.js'; // Assuming generateAvatarUrl is kept in utils
19
+ import { handleError } from '../utils/errorHandler.js'; // Keep handleError for now, might refactor error handling later
20
+ import { db } from '../firebase-init.js'; // Import db
21
+
22
+
23
+ // --- Internal State (Caches) ---
24
+ let tasksCache = {}; // Use object for faster lookup by ID
25
+ let commentsCache = {}; // Cache comments by task ID: { taskId: [comment1, comment2, ...] }
26
+
27
+
28
+ // --- Internal Helper: Load all tasks into cache ---
29
+ async function loadAllTasksIntoCache() {
30
+ console.log('TaskManager: Loading all tasks into cache');
31
+ try {
32
+ const tasksArray = await getServiceTasks();
33
+ tasksCache = tasksArray.reduce((cache, task) => {
34
+ cache[task.id] = task;
35
+ return cache;
36
+ }, {});
37
+ console.log('TaskManager: All tasks loaded into cache:', Object.keys(tasksCache).length);
38
+ // Publish event after cache is loaded
39
+ eventBus.publish('tasksCacheLoaded', tasksCache);
40
+ eventBus.publish('tasksDataReady', Object.values(tasksCache)); // Publish the array for UI rendering
41
+ } catch (error) {
42
+ console.error('TaskManager: Error loading all tasks into cache:', error);
43
+ eventBus.publish('tasksLoadFailed', { message: 'Không thể tải danh sách công việc.', error: error });
44
+ }
45
+ }
46
+
47
+ // --- Internal Helper: Process raw comment data (add author info, format dates) ---
48
+ function processCommentForCache(comment) {
49
+ const author = userManager.getUserData(comment.userId);
50
+ // console.log('TaskManager: processCommentForCache - Input comment:', comment); // Log input
51
+
52
+ if (!author) console.warn(`TaskManager: User data not found in userManager cache for userId ${comment.userId} for comment ${comment.id}`);
53
+
54
+ const processed = {
55
+ ...comment,
56
+ authorName: author?.name || 'Người dùng không rõ',
57
+ authorAvatar: author?.avatar || generateAvatarUrl(author?.name || 'Người dùng'),
58
+ // Explicitly handle Firebase Timestamps and existing Date objects
59
+ createdAt: comment.createdAt?.toDate ? comment.createdAt.toDate() : null,
60
+ updatedAt: comment.updatedAt?.toDate ? comment.updatedAt.toDate() : (comment.updatedAt instanceof Date ? comment.updatedAt : null), // Handle already a Date
61
+ };
62
+ console.log('TaskManager: processCommentForCache - Processed comment:', processed); // Log output
63
+ return processed;
64
+ }
65
+
66
+ // --- Internal Helper: Load comments for a specific task into cache ---
67
+ // Renamed and moved from commentManager.js
68
+ async function handleLoadTaskComments(taskId, forceReload = false) {
69
+ console.log('TaskManager: handleLoadTaskComments called for taskId', taskId, 'forceReload:', forceReload);
70
+ if (!taskId) {
71
+ console.warn('TaskManager: handleLoadTaskComments called without taskId.');
72
+ eventBus.publish('commentLoadFailed', { taskId, message: 'Thiếu ID công việc để tải bình luận.' });
73
+ return []; // Return empty array as a safe default
74
+ }
75
+
76
+ // Use cache if available and not forced to reload
77
+ if (commentsCache[taskId] && !forceReload) {
78
+ console.log('TaskManager: Returning comments from cache for taskId', taskId);
79
+ // Publish the cached data
80
+ eventBus.publish('taskCommentsLoaded', { taskId, comments: commentsCache[taskId] });
81
+ return commentsCache[taskId]; // Return cached data
82
+ }
83
+
84
+ // If cache is not available or forced reload
85
+ try {
86
+ console.log('TaskManager: Fetching comments from service for taskId', taskId);
87
+ const comments = await getServiceTaskComments(taskId);
88
+ console.log('TaskManager: Fetched', comments.length, 'comments from service.');
89
+
90
+ // Process and filter comments before caching
91
+ const processedComments = comments
92
+ .map(processCommentForCache) // Add author info, format dates
93
+ .filter(comment => !comment.deleted); // Filter out soft-deleted comments for display
94
+
95
+ // Sort comments (e.g., by creation date)
96
+ processedComments.sort((a, b) => (a.createdAt?.getTime() || 0) - (b.createdAt?.getTime() || 0));
97
+
98
+ // Update cache
99
+ commentsCache[taskId] = processedComments;
100
+ console.log('TaskManager: Updated cache for taskId', taskId);
101
+
102
+ // Publish success event with the processed and cached comments
103
+ eventBus.publish('taskCommentsLoaded', { taskId, comments: processedComments });
104
+
105
+ return processedComments; // Return the fetched and processed data
106
+
107
+ } catch (error) {
108
+ console.error('TaskManager: Error loading comments for taskId', taskId, ':', error);
109
+ // Publish error event
110
+ eventBus.publish('commentLoadFailed', { taskId, error: error.message || 'Không thể tải bình luận.' });
111
+ throw error; // Re-throw for higher-level error handling
112
+ }
113
+ }
114
+
115
+ // Function for UI to get cached comment data immediately - Moved from commentManager.js
116
+ function getTaskCommentsData(taskId) {
117
+ console.log('TaskManager: getTaskCommentsData (from cache) called for taskId', taskId);
118
+ return commentsCache[taskId] || []; // Return empty array if no comments cached
119
+ }
120
+
121
+
122
+ // --- Task Action Handlers (Called by UI via EventBus or direct call) ---
123
+
124
+ async function handleSaveTask(taskData) { // Handles both add and initial save for edit
125
+ console.log('TaskManager: handleSaveTask called with data', taskData);
126
+ // Basic validation (can be more extensive in UI)
127
+ if (!taskData.title || !taskData.status) {
128
+ eventBus.publish('taskActionFailed', { action: 'save', message: 'Tiêu đề và trạng thái công việc không được để trống.' });
129
+ return;
130
+ }
131
+ try {
132
+ console.log('TaskManager: Calling createServiceTask');
133
+ const newTask = await createServiceTask(taskData);
134
+
135
+ // Service call successful. Update cache and publish event.
136
+ console.log('TaskManager: Task created successfully. Updating cache and publishing event.');
137
+ tasksCache[newTask.id] = newTask; // Add to cache
138
+
139
+ // Publish a specific event for UI to update
140
+ eventBus.publish('taskAdded', newTask);
141
+ eventBus.publish('tasksDataReady', Object.values(tasksCache)); // Publish updated full list
142
+
143
+
144
+ } catch (error) {
145
+ console.error('TaskManager: Error saving task:', error);
146
+ // Publish an error event for UI
147
+ eventBus.publish('taskActionFailed', { action: 'save', message: 'Không thể lưu công việc.', error: error });
148
+ }
149
+ }
150
+
151
+ async function handleUpdateTask(data) {
152
+ const { taskId, taskData } = data;
153
+ console.log('TaskManager: handleUpdateTask called for ID', taskId, 'with data', taskData); // Keep this log to verify data
154
+ if (!taskId || !taskData.title || !taskData.status) {
155
+ eventBus.publish('taskActionFailed', { action: 'update', message: 'Thiếu thông tin công việc để cập nhật.' });
156
+ return;
157
+ }
158
+ try {
159
+ console.log('TaskManager: Calling updateServiceTask');
160
+ const updatedTask = await updateServiceTask(taskId, taskData);
161
+
162
+ // Service call successful. Update cache and publish event.
163
+ console.log('TaskManager: Task updated successfully. Updating cache and publishing event.');
164
+ // Update in cache
165
+ if (tasksCache[taskId]) {
166
+ tasksCache[taskId] = { ...tasksCache[taskId], ...updatedTask }; // Merge updated data
167
+ } else {
168
+ console.warn(`TaskManager: Updated task with ID ${taskId} not found in cache. Reloading all tasks.`);
169
+ loadAllTasksIntoCache(); // Fallback to full reload
170
+ }
171
+
172
+
173
+ // Publish a specific event for UI to update
174
+ eventBus.publish('taskUpdated', tasksCache[taskId]); // Publish the updated object from cache
175
+ eventBus.publish('tasksDataReady', Object.values(tasksCache)); // Publish updated full list
176
+
177
+
178
+ } catch (error) {
179
+ console.error(`TaskManager: Error updating task ${taskId}:`, error);
180
+ // Publish an error event for UI
181
+ eventBus.publish('taskActionFailed', { action: 'update', message: 'Không thể cập nhật công việc.', error: error });
182
+ }
183
+ }
184
+
185
+
186
+ async function handleDeleteTask(taskId) { // Renamed from handleDeleteTaskClick for consistency
187
+ console.log('TaskManager: handleDeleteTask called for ID', taskId);
188
+ if (!taskId) {
189
+ eventBus.publish('taskActionFailed', { action: 'delete', message: 'Thiếu ID công việc để xoá.' });
190
+ return;
191
+ }
192
+ try {
193
+ console.log('TaskManager: Calling deleteTaskService');
194
+ const deletedTaskId = await deleteServiceTask(taskId);
195
+
196
+ // Service call successful. Update cache and publish event.
197
+ console.log('TaskManager: Task deleted successfully. Updating cache and publishing event.');
198
+ // Remove from cache
199
+ delete tasksCache[deletedTaskId];
200
+ // Also remove comments cache for this task
201
+ delete commentsCache[deletedTaskId];
202
+
203
+
204
+ // Publish a specific event for UI to update
205
+ eventBus.publish('taskDeleted', deletedTaskId);
206
+ eventBus.publish('tasksDataReady', Object.values(tasksCache)); // Publish updated full list
207
+
208
+
209
+ } catch (error) {
210
+ console.error(`TaskManager: Error deleting task ${taskId}:`, error);
211
+ // Publish an error event for UI
212
+ eventBus.publish('taskActionFailed', { action: 'delete', message: 'Không thể xoá công việc.', error: error });
213
+ }
214
+ }
215
+
216
+
217
+ // --- Comment Action Handlers (Called by UI via EventBus or direct call) ---
218
+ // Moved and updated from commentManager.js
219
+
220
+ // Điều chỉnh thứ tự thực hiện và sửa lỗi ReferenceError
221
+ async function handleAddComment(eventData) {
222
+ console.log('TaskManager: handleAddComment called for Task ID', eventData.taskId, 'with text:', eventData.text);
223
+ const { taskId, text } = eventData;
224
+
225
+ // Validate input
226
+ if (!taskId || typeof taskId !== 'string' || taskId.trim() === '') {
227
+ console.error('TaskManager: handleAddComment - Invalid Task ID:', taskId);
228
+ eventBus.publish('commentSaveFailed', { error: 'Task ID không hợp lệ.', taskId });
229
+ return;
230
+ }
231
+ if (!text || text.trim() === '') {
232
+ eventBus.publish('commentSaveFailed', { error: 'Nội dung bình luận không được để trống.', taskId });
233
+ return;
234
+ }
235
+
236
+ const currentUser = userManager.getCurrentUserData();
237
+ if (!currentUser || !currentUser.id) {
238
+ console.error('TaskManager: handleAddComment - User data not available. Aborting comment submission.');
239
+ eventBus.publish('commentSaveFailed', { error: 'Người dùng chưa đăng nhập.', taskId });
240
+ return;
241
+ }
242
+
243
+ const commentData = {
244
+ taskId: taskId,
245
+ userId: currentUser.id,
246
+ text: text,
247
+ // createdAt và updatedAt sẽ được set bởi server Firestore
248
+ deleted: false,
249
+ edited: false
250
+ };
251
+
252
+ let fetchedNewComment = null; // Khai báo và khởi tạo biến ở ngoài khối try
253
+
254
+ try {
255
+ console.log('TaskManager: Calling addServiceCommentToTask');
256
+ // Service now returns the full comment object with server timestamps
257
+ fetchedNewComment = await addServiceCommentToTask(taskId, commentData);
258
+ console.log('TaskManager: Received comment from service (fetched by service):', fetchedNewComment);
259
+
260
+ // Process the fetched comment (which now should have server timestamps)
261
+ const processedNewComment = processCommentForCache(fetchedNewComment);
262
+
263
+ console.log('TaskManager: Raw fetchedNewComment object BEFORE processing:', fetchedNewComment);
264
+ console.log('TaskManager: processedNewComment object AFTER processing:', processedNewComment);
265
+ console.log('TaskManager: Type of processedNewComment.createdAt:', typeof processedNewComment.createdAt);
266
+ console.log('TaskManager: Value of processedNewComment.createdAt:', processedNewComment.createdAt);
267
+
268
+
269
+ console.log('TaskManager: Comment added successfully. Updating cache and publishing event.');
270
+
271
+ // Update cache
272
+ if (!commentsCache[taskId]) {
273
+ commentsCache[taskId] = [];
274
+ }
275
+ // Find and replace the comment in cache if it exists (e.g., from a previous incomplete add)
276
+ const existingIndex = commentsCache[taskId].findIndex(c => c.id === processedNewComment.id);
277
+ if (existingIndex > -1) {
278
+ commentsCache[taskId][existingIndex] = processedNewComment;
279
+ console.log('TaskManager: Replaced existing comment in cache:', processedNewComment.id);
280
+ } else {
281
+ commentsCache[taskId].push(processedNewComment);
282
+ console.log('TaskManager: Added new comment to cache:', processedNewComment.id);
283
+ }
284
+
285
+ // Keep comments sorted by createdAt (now should be Date objects)
286
+ commentsCache[taskId].sort((a, b) => (a.createdAt?.getTime() || 0) - (b.createdAt?.getTime() || 0));
287
+ console.log('TaskManager: Cache sorted.');
288
+
289
+
290
+ // Publish specific event for UI to add comment
291
+ // PUBLISH ONLY AFTER FETCHING AND PROCESSING SUCCEED
292
+ eventBus.publish('commentAdded', { taskId: taskId, comment: processedNewComment });
293
+ console.log('TaskManager: Publishing commentAdded event with object:', processedNewComment);
294
+ console.log('TaskManager: Type of object.createdAt being published:', typeof processedNewComment.createdAt);
295
+ console.log('TaskManager: Value of object.createdAt being published:', processedNewComment.createdAt);
296
+
297
+ eventBus.publish('taskCommentsLoaded', { taskId: taskId, comments: commentsCache[taskId] }); // Publish updated full list after adding
298
+
299
+
300
+ } catch (error) {
301
+ console.error('TaskManager: handleAddComment error during add or fetch:', error);
302
+ // Sử dụng biến taskId được truyền vào hàm
303
+ eventBus.publish('commentSaveFailed', { error: error.message || 'Không thể thêm hoặc tải bình luận.', taskId: taskId });
304
+ // Không throw error ở đây để không chặn các xử lý khác nếu cần
305
+ }
306
+ }
307
+
308
+
309
+
310
+ async function handleSaveEditedComment(taskId, commentId, newText) {
311
+ console.log('TaskManager: handleSaveEditedComment called for task', taskId, 'comment', commentId, 'with text', newText);
312
+ if (!taskId || !commentId || !newText || newText.trim() === '') {
313
+ eventBus.publish('commentSaveFailed', { error: 'Nội dung bình luận không được để trống.', taskId, commentId });
314
+ return;
315
+ }
316
+
317
+ const updateData = {
318
+ text: newText.trim(),
319
+ edited: true,
320
+ updatedAt: db.firestore.FieldValue.serverTimestamp(), // Sử dụng db.firestore.FieldValue
321
+ };
322
+
323
+ try {
324
+ console.log('TaskManager: Calling updateServiceTaskComment');
325
+ await updateServiceTaskComment(taskId, commentId, updateData);
326
+ console.log('TaskManager: Comment updated via service:', commentId);
327
+
328
+ // After successfully updating, update cache and publish event
329
+ console.log('TaskManager: Comment updated successfully. Updating cache and publishing event.');
330
+ if (commentsCache[taskId]) {
331
+ const index = commentsCache[taskId].findIndex(comment => comment.id === commentId);
332
+ if (index !== -1) {
333
+ // Update text, edited status, and updatedAt in cache
334
+ commentsCache[taskId][index] = { ...commentsCache[taskId][index], ...updateData };
335
+ // Ensure createdAt remains (might be lost in simple merge)
336
+ if (!commentsCache[taskId][index].createdAt && commentsCache[taskId][index].createdAt_raw) {
337
+ commentsCache[taskId][index].createdAt = commentsCache[taskId][index].createdAt_raw; // Assuming raw timestamp was stored
338
+ }
339
+
340
+ // Re-process the updated comment to ensure author info/date format is correct
341
+ const updatedComment = processCommentForCache(commentsCache[taskId][index]);
342
+ commentsCache[taskId][index] = updatedComment;
343
+
344
+ // Keep comments sorted by createdAt
345
+ commentsCache[taskId].sort((a, b) => (a.createdAt?.getTime() || 0) - (b.createdAt?.getTime() || 0));
346
+
347
+ // Publish the updated comment object
348
+ eventBus.publish('commentUpdated', { taskId: taskId, comment: updatedComment });
349
+ eventBus.publish('taskCommentsLoaded', { taskId: taskId, comments: commentsCache[taskId] }); // Publish updated full list
350
+
351
+ } else {
352
+ console.warn(`TaskManager: Updated comment with ID ${commentId} not found in cache for task ${taskId}. Reloading comments for task.`);
353
+ handleLoadTaskComments(taskId, true); // Fallback to full reload
354
+ }
355
+ } else {
356
+ console.warn(`TaskManager: Comment cache for task ${taskId} not found during update. Reloading comments for task.`);
357
+ handleLoadTaskComments(taskId, true); // Fallback to full reload
358
+ }
359
+
360
+ } catch (error) {
361
+ console.error(`TaskManager: Error updating comment ${commentId} for task ${taskId}:`, error);
362
+ eventBus.publish('commentSaveFailed', { error: error.message || 'Không thể cập nhật bình luận.', taskId, commentId });
363
+ }
364
+ }
365
+
366
+
367
+ async function handleDeleteComment(taskId, commentId) { // Handles soft delete
368
+ console.log('TaskManager: handleDeleteComment (soft delete) called for Task ID', taskId, 'Comment ID', commentId);
369
+ if (!taskId || !commentId) {
370
+ eventBus.publish('commentDeleteFailed', { error: 'Thiếu thông tin bình luận để xoá.', taskId, commentId });
371
+ return;
372
+ }
373
+
374
+ const updateData = {
375
+ deleted: true,
376
+ edited: true, // Mark as edited as content changes
377
+ updatedAt: db.firestore.FieldValue.serverTimestamp(), // Sử dụng db.firestore.FieldValue
378
+ };
379
+
380
+ try {
381
+ console.log('TaskManager: Calling updateServiceTaskComment for soft delete commentId', commentId);
382
+ await updateServiceTaskComment(taskId, commentId, updateData);
383
+ console.log('TaskManager: Comment soft deleted via service:', commentId);
384
+
385
+ // After successfully soft deleting, update cache and publish event
386
+ console.log('TaskManager: Comment soft deleted successfully. Updating cache and publishing event.');
387
+ if (commentsCache[taskId]) {
388
+ const index = commentsCache[taskId].findIndex(comment => comment.id === commentId);
389
+ if (index !== -1) {
390
+ // Update deleted status, edited status, and updatedAt in cache
391
+ commentsCache[taskId][index] = { ...commentsCache[taskId][index], ...updateData };
392
+
393
+ // Re-process the soft-deleted comment to ensure author info/date format is correct and filter it out
394
+ const updatedComment = processCommentForCache(commentsCache[taskId][index]);
395
+
396
+ // Since it's soft deleted, remove it from the displayed cache array
397
+ commentsCache[taskId] = commentsCache[taskId].filter(comment => comment.id !== commentId);
398
+
399
+
400
+ // Publish a specific event for UI to remove comment from display
401
+ eventBus.publish('commentDeleted', { taskId: taskId, commentId: commentId });
402
+ eventBus.publish('taskCommentsLoaded', { taskId: taskId, comments: commentsCache[taskId] }); // Publish updated full list
403
+
404
+ } else {
405
+ console.warn(`TaskManager: Soft deleted comment with ID ${commentId} not found in cache for task ${taskId}. Reloading comments for task.`);
406
+ handleLoadTaskComments(taskId, true); // Fallback to full reload for comments
407
+ }
408
+ } else {
409
+ console.warn(`TaskManager: Comment cache for task ${taskId} not found during soft delete. Cache might be out of sync.`);
410
+ handleLoadTaskComments(taskId, true); // Fallback to full reload for comments
411
+ }
412
+
413
+ } catch (error) {
414
+ console.error('TaskManager: handleDeleteComment error:', error);
415
+ eventBus.publish('commentDeleteFailed', { error: error.message || 'Không thể xoá bình luận.', taskId, commentId });
416
+ }
417
+ }
418
+
419
+ async function handleHardDeleteComment(taskId, commentId) {
420
+ console.log('TaskManager: handleHardDeleteComment called for Task ID', taskId, 'Comment ID', commentId);
421
+ if (!taskId || !commentId) {
422
+ eventBus.publish('commentDeleteFailed', { error: 'Thiếu thông tin bình luận để xoá vĩnh viễn.', taskId, commentId });
423
+ return;
424
+ }
425
+
426
+ console.log('TaskManager: Calling deleteTaskComment service for hard delete commentId', commentId);
427
+ try {
428
+ await deleteServiceTaskComment(taskId, commentId);
429
+ console.log('TaskManager: Comment hard deleted via service:', commentId);
430
+
431
+ // After successfully hard deleting, update cache and publish event
432
+ console.log('TaskManager: Comment hard deleted successfully. Updating cache and publishing event.');
433
+ // Remove from comments cache for this task
434
+ if (commentsCache[taskId]) {
435
+ commentsCache[taskId] = commentsCache[taskId].filter(comment => comment.id !== commentId);
436
+ } else {
437
+ console.warn(`TaskManager: Comment cache for task ${taskId} not found during hard delete. Cache might be out of sync.`);
438
+ // Optionally trigger a full reload of comments for this task
439
+ handleLoadTaskComments(taskId, true); // Fallback to full reload
440
+ }
441
+
442
+ // Publish a specific event for UI to remove comment
443
+ eventBus.publish('commentDeleted', { taskId: taskId, commentId: commentId });
444
+ eventBus.publish('taskCommentsLoaded', { taskId: taskId, comments: commentsCache[taskId] }); // Publish updated full list
445
+
446
+
447
+ } catch (error) {
448
+ console.error('TaskManager: handleHardDeleteComment error:', error);
449
+ eventBus.publish('commentDeleteFailed', { error: error.message || 'Không thể xoá vĩnh viễn bình luận.', taskId, commentId });
450
+ }
451
+ }
452
+
453
+
454
+ // --- Helper to get cached data ---
455
+ // Kept from previous version and commentManager.js
456
+
457
+ function getAllTasksFromCache() {
458
+ console.log('TaskManager: getAllTasksFromCache called. Cache size:', Object.keys(tasksCache).length);
459
+ return Object.values(tasksCache);
460
+ }
461
+
462
+ function getTaskByIdFromCache(taskId) {
463
+ console.log('TaskManager: getTaskByIdFromCache called for ID:', taskId);
464
+ return tasksCache[taskId] || null;
465
+ }
466
+
467
+
468
+ // --- EventBus Listeners ---
469
+
470
+ function setupEventBusListeners() {
471
+ console.log('TaskManager: Setting up EventBus listeners');
472
+
473
+ // Listen for currentUserDataLoaded from userManager to trigger initial task load
474
+ eventBus.subscribe('currentUserDataLoaded', () => {
475
+ console.log('TaskManager: currentUserDataLoaded event received. Loading tasks...');
476
+ loadAllTasksIntoCache(); // Initial load of tasks
477
+ });
478
+
479
+ // Listen for task detail modal opening to load comments for that task
480
+ // This assumes a UI component publishes this event when the modal is opened
481
+ eventBus.subscribe('taskDetailModalOpened', (taskId) => {
482
+ console.log(`TaskManager: taskDetailModalOpened event received for Task ID: ${taskId}. Loading comments.`);
483
+ handleLoadTaskComments(taskId); // Load comments when modal opens
484
+ });
485
+
486
+ // Listen for task deletion to clear comment cache for that task
487
+ eventBus.subscribe('taskDeleted', (deletedTaskId) => { // Assuming taskManager publishes taskDeleted with taskId
488
+ if (commentsCache[deletedTaskId]) {
489
+ delete commentsCache[deletedTaskId];
490
+ console.log('TaskManager: Cleared comment cache for deleted task', deletedTaskId);
491
+ }
492
+ });
493
+
494
+ // No listeners for Service events anymore.
495
+ }
496
+
497
+ // --- Setup UI Event Listeners ---
498
+ function setupUIEventListeners() {
499
+ console.log('TaskManager: Setting up UI Event listeners');
500
+ // Listen for UI events published by listeners.js or tasks.js
501
+ eventBus.subscribe('saveTaskSubmit', handleSaveTask); // Listen for form submit to save/add task
502
+ eventBus.subscribe('updateTaskSubmit', handleUpdateTask); // Listen for form submit to update task
503
+ eventBus.subscribe('deleteTaskClicked', handleDeleteTask); // Listen for delete button click (after confirmation in UI)
504
+
505
+ // Comment UI Listeners (Moved from where they were in commentManager.js or UI file)
506
+ eventBus.subscribe('addCommentSubmit', handleAddComment); // Listen for add comment form submit
507
+ eventBus.subscribe('saveEditedCommentSubmit', handleSaveEditedComment); // Listen for save edited comment button click
508
+ // Note: Assuming UI publishes these events after confirmation for hard delete
509
+ eventBus.subscribe('deleteCommentClicked', handleDeleteComment); // Listen for soft delete (if used)
510
+ eventBus.subscribe('hardDeleteCommentClicked', handleHardDeleteComment); // Listen for hard delete
511
+
512
+
513
+ }
514
+
515
+
516
+ // --- Initialization Function ---
517
+ function initTaskManager() {
518
+ console.log('Initializing Task Manager');
519
+ setupEventBusListeners(); // Set up listeners for other managers/UI
520
+ setupUIEventListeners(); // Set up listeners for UI actions
521
+
522
+ // Initial load of tasks is now triggered by currentUserDataLoaded event
523
+ // Initial load of comments is triggered by taskDetailModalOpened event
524
+ }
525
+
526
+ // Export public functions
527
+ export {
528
+ // Export task handlers
529
+ handleSaveTask,
530
+ handleUpdateTask,
531
+ handleDeleteTask,
532
+ // Export comment handlers (now in TaskManager)
533
+ handleLoadTaskComments, // Export the function to load comments
534
+ handleAddComment,
535
+ handleSaveEditedComment,
536
+ handleDeleteComment, // Soft delete
537
+ handleHardDeleteComment, // Hard delete
538
+
539
+ // Export cache getters
540
+ getAllTasksFromCache,
541
+ getTaskByIdFromCache,
542
+ getTaskCommentsData, // Export the function to get cached comments
543
+
544
+ initTaskManager
545
+ };
public/js/managers/userManager.js ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // public/js/managers/userManager.js
2
+
3
+ import eventBus from '../utils/eventBus.js';
4
+ import { generateAvatarUrl } from '../utils/utils.js'; // Import generateAvatarUrl
5
+ import {
6
+ getAllUsers as getServiceUsers, // Changed from getUsers to getAllUsers
7
+ createUser as createServiceUser, // Tên đã được sửa
8
+ updateUser as updateServiceUser,
9
+ deleteUser as deleteServiceUser,
10
+ getUserById as getServiceUserById // Import getServiceUserById if needed elsewhere, though cache should be primary
11
+ } from '../services/userService.js';
12
+ import { auth, db, app } from '../firebase-init.js'; // Import auth, db, app
13
+
14
+ // --- Internal State ---
15
+ let currentUserId = null;
16
+ let currentUserData = null;
17
+ // Use an object for usersCache for faster lookups by ID
18
+ let usersCache = {};
19
+
20
+ // --- Internal Helper to load all users into cache ---
21
+ async function loadAllUsersIntoCache() {
22
+ console.log('UserManager: Loading all users into cache');
23
+ try {
24
+ const usersArray = await getServiceUsers();
25
+ usersCache = usersArray.reduce((cache, user) => {
26
+ cache[user.id] = user;
27
+ return cache;
28
+ }, {});
29
+ console.log('UserManager: All users loaded into cache:', Object.keys(usersCache).length);
30
+ // Publish event after cache is loaded
31
+ eventBus.publish('usersCacheLoaded', usersCache);
32
+ } catch (error) {
33
+ console.error('UserManager: Error loading all users into cache:', error);
34
+ // Optionally publish an error event
35
+ eventBus.publish('usersCacheLoadFailed', error);
36
+ }
37
+ }
38
+
39
+
40
+ // --- Initialization Function ---
41
+ export function initUserManager() {
42
+ console.log('Initializing User Manager');
43
+
44
+ // Setup Firebase Auth state observer
45
+ // Ensure it's set up only once
46
+ if (!auth._userManagerAuthStateListener) { // Sử dụng auth đã import
47
+ auth._userManagerAuthStateListener = auth.onAuthStateChanged(async (user) => { // Sử dụng auth đã import
48
+ console.log('UserManager: Auth state changed:', user ? user.uid : 'Logged out');
49
+ if (user) {
50
+ currentUserId = user.uid;
51
+ try {
52
+ // Access Firestore and App ID via global firebase object
53
+ // const db = firebase.firestore(); // Removed
54
+ // const appId = firebase.app().options.appId; // Removed
55
+ const FieldValue = firebase.firestore.FieldValue; // Giữ lại FieldValue nếu Firebase compat được load toàn cục
56
+
57
+ const userDocRef = db.collection('artifacts').doc(app.options.appId).collection('users').doc(currentUserId); // Sử dụng db, app đã import
58
+ const userDoc = await userDocRef.get();
59
+ currentUserData = userDoc.data();
60
+ // Thêm ID của document vào currentUserData
61
+ if (currentUserData) { // Check if data exists before adding id
62
+ currentUserData.id = userDoc.id;
63
+ }
64
+
65
+ if (!currentUserData) {
66
+ console.log('UserManager: User data not found in Firestore for', currentUserId, ', creating default.');
67
+ // Check if this is the very first user in the system
68
+ const usersCollectionSnapshot = await db.collection('artifacts').doc(app.options.appId).collection('users').limit(1).get(); // Sử dụng db, app đã import
69
+ const isFirstUser = usersCollectionSnapshot.empty; // If empty, this is the first user
70
+ console.log('UserManager: Is first user?', isFirstUser);
71
+
72
+ const defaultUserData = {
73
+ email: user.email,
74
+ name: user.displayName || user.email.split('@')[0] || 'Người dùng mới', // Sử dụng email nếu displayName trống
75
+ role: isFirstUser ? 'admin' : 'employee', // First user is admin
76
+ department: '', // Default department empty
77
+ avatar: generateAvatarUrl(user.displayName || user.email || 'Người dùng mới'), // Generate default avatar
78
+ createdAt: FieldValue.serverTimestamp(),
79
+ updatedAt: FieldValue.serverTimestamp()
80
+ };
81
+
82
+ await userDocRef.set(defaultUserData, { merge: true });
83
+ console.log('UserManager: Default user data created for', currentUserId);
84
+ // Fetch data again after creation to get the correct data including server timestamps
85
+ currentUserData = (await userDocRef.get()).data();
86
+ // Thêm ID vào đây nữa nếu logic tạo user mới cũng không bao gồm ID
87
+
88
+ console.log('UserManager: Fetched new currentUserData (with ID):', currentUserData); // Cập nhật log để xác nhận ID
89
+ } else {
90
+ console.log('UserManager: Existing currentUserData loaded (with ID):', currentUserData); // Cập nhật log để xác nhận ID
91
+ }
92
+
93
+ // Load all users into cache after successful login and current user data is ready
94
+ await loadAllUsersIntoCache();
95
+
96
+ // Publish events for UI
97
+ eventBus.publish('userLoggedIn', currentUserData); // Publish current user specific data
98
+ eventBus.publish('userDataUpdated', currentUserData); // Publish current user specific data
99
+ // 'usersCacheLoaded' is published by loadAllUsersIntoCache
100
+ console.log('UserManager: Publishing currentUserDataLoaded event with data:', currentUserData); // Log before publishing
101
+ eventBus.publish('currentUserDataLoaded', currentUserData);
102
+
103
+
104
+ } catch (error) {
105
+ console.error('UserManager: Error during user data fetching or creation on login:', error);
106
+ // Handle errors, maybe sign out user or show a critical error message
107
+ eventBus.publish('authError', { action: 'loginUserData', error: error.message || 'Không thể tải hoặc tạo dữ liệu người dùng.' });
108
+ // It might be necessary to sign the user out if their data cannot be loaded/created
109
+ auth.signOut(); // Sử dụng auth đã import
110
+ }
111
+
112
+ } else {
113
+ // User is signed out
114
+ console.log('UserManager: User logged out.');
115
+ currentUserId = null;
116
+ currentUserData = null;
117
+ usersCache = {}; // Clear user cache on logout
118
+
119
+ // Publish events for UI
120
+ eventBus.publish('userLoggedOut');
121
+ eventBus.publish('userDataUpdated', currentUserData); // Publish null for current user
122
+ eventBus.publish('usersCacheLoaded', usersCache); // Publish empty cache
123
+ }
124
+ });
125
+ } else {
126
+ console.log('UserManager: Auth state listener already initialized.');
127
+ }
128
+
129
+
130
+ // Manager subscribes to UI submit/click events from listeners.js
131
+ // The manager handles the business logic and calls the service
132
+ // UI listeners will call these exported handler functions directly.
133
+
134
+ // The manager *publishes* these events for UI (or other managers) to listen to:
135
+ // 'userLoggedIn' (contains currentUserData)
136
+ // 'userLoggedOut'
137
+ // 'userDataUpdated' (contains currentUserData - helpful if user data changes while logged in)
138
+ // 'usersCacheLoaded' (contains the usersCache object)
139
+ // 'usersCacheLoadFailed'
140
+ // 'userSaveCompleted'
141
+ // 'userSaveFailed' (contains error info)
142
+ // 'userDeleteCompleted' (contains deleted userId)
143
+ // 'userDeleteFailed' (contains error info)
144
+ }
145
+
146
+
147
+ async function handleAddUserSubmit(userData) {
148
+ console.log('userManager: handleAddUserSubmit called with data', userData);
149
+ // Basic validation (more robust validation can be in UI or here)
150
+ if (!userData.name || !userData.email || !userData.role || !userData.department) {
151
+ console.warn('userManager: handleAddUserSubmit - Missing user data.');
152
+ // Publish error event for UI
153
+ eventBus.publish('userAddFailed', 'Vui lòng điền đầy đủ thông tin người dùng.');
154
+ return;
155
+ }
156
+
157
+ try {
158
+ console.log('userManager: Calling createServiceUser', userData);
159
+ const newUser = await createServiceUser(userData); // Service interacts with backend and returns new user data
160
+
161
+ // After successfully adding, update cache and notify UI
162
+ // Fetch all users again to ensure cache is consistent with backend
163
+ await loadAllUsersIntoCache();
164
+
165
+ // Publish a completed event with new user data if needed by UI, otherwise general completed is enough
166
+ eventBus.publish('userAdded', newUser);
167
+
168
+
169
+ } catch (error) {
170
+ console.error('userManager: Error adding user:', error);
171
+ // Publish an error event
172
+ eventBus.publish('userAddFailed', error.message || 'Không thể thêm người dùng.');
173
+ }
174
+ }
175
+
176
+ async function handleEditUserSubmit(userId, userData) {
177
+ console.log('userManager: handleEditUserSubmit called for ID', userId, 'with data', userData);
178
+ if (!userId || !userData.name || !userData.email || !userData.role || !userData.department) {
179
+ console.warn('userManager: handleEditUserSubmit - Missing info or user data.');
180
+ eventBus.publish('userUpdateFailed', 'Thiếu thông tin người dùng để cập nhật.');
181
+ return;
182
+ }
183
+
184
+ try {
185
+ console.log('userManager: Calling updateServiceUser', userId, userData);
186
+ const updatedUser = await updateServiceUser(userId, userData); // Service interacts with backend and returns updated user data
187
+
188
+ // After successfully updating, update cache and notify UI
189
+ // Fetch all users again to ensure cache is consistent with backend
190
+ await loadAllUsersIntoCache();
191
+
192
+ // Publish a completed event with updated user data if needed by UI
193
+ eventBus.publish('userUpdated', updatedUser);
194
+
195
+
196
+ } catch (error) {
197
+ console.error(`userManager: Error updating user ${userId}:`, error);
198
+ // Publish an error event
199
+ eventBus.publish('userUpdateFailed', error.message || 'Không thể cập nhật người dùng.');
200
+ }
201
+ }
202
+
203
+ async function handleDeleteUserClick(userId) { // Remove userName parameter, Manager shouldn't need UI display info
204
+ console.log('userManager: handleDeleteUserClick called for ID', userId);
205
+ // Confirmation is handled by the UI component (listeners.js) using showConfirm
206
+ // This function is called *after* confirmation.
207
+ if (!userId) {
208
+ console.warn('userManager: handleDeleteUserClick - Missing userId.');
209
+ eventBus.publish('userDeleteFailed', 'Thiếu ID người dùng để xoá.');
210
+ return;
211
+ }
212
+
213
+ try {
214
+ console.log('userManager: Calling deleteServiceUser', userId);
215
+ const deletedUserId = await deleteServiceUser(userId); // Service interacts with backend and returns deleted ID
216
+
217
+ // After successfully deleting, update cache and notify UI
218
+ // Fetch all users again to ensure cache is consistent with backend
219
+ await loadAllUsersIntoCache();
220
+
221
+ console.log('userManager: Finished calling deleteServiceUser and loadAllUsersIntoCache.');
222
+ // Publish a completed event with deleted user ID
223
+ eventBus.publish('userDeleted', deletedUserId);
224
+
225
+
226
+ } catch (error) {
227
+ console.error(`userManager: Error deleting user ${userId}:`, error);
228
+ // Publish an error event
229
+ eventBus.publish('userDeleteFailed', error.message || 'Không thể xoá người dùng.');
230
+ }
231
+ }
232
+
233
+ // --- Helper to get cached data (for UI to populate dropdowns, etc.) ---
234
+ // This allows UI components to access the latest user data without calling the service directly
235
+
236
+ // Get all users from cache
237
+ export function getAllUsersFromCache() {
238
+ console.log('UserManager: getAllUsersFromCache called. Cache size:', Object.keys(usersCache).length);
239
+ // Return an array of users from the cache object
240
+ return Object.values(usersCache);
241
+ }
242
+
243
+ // Get a specific user from cache by ID
244
+ export function getUserData(userId) {
245
+ console.log('UserManager: getUserData called for ID:', userId);
246
+ return usersCache[userId] || null;
247
+ }
248
+
249
+ // Get the currently logged-in user's data from state
250
+ export function getCurrentUserData() {
251
+ console.log('UserManager: getCurrentUserData called.');
252
+ return currentUserData;
253
+ }
254
+
255
+ // Get the currently logged-in user's ID
256
+ export function getCurrentUserId() {
257
+ console.log('UserManager: getCurrentUserId called.');
258
+ return currentUserId;
259
+ }
260
+
261
+
262
+ // Export the handlers and getters that will be called by listeners.js and other parts
263
+ export {
264
+ // handleLoadUsers, // Initial load is now triggered by auth state change
265
+ handleAddUserSubmit,
266
+ handleEditUserSubmit,
267
+ handleDeleteUserClick,
268
+ // Export getters below instead of the old getUsersData alias
269
+ // getAllUsersFromCache, // Already exported above
270
+ // getUserData, // Already exported above
271
+ // getCurrentUserData, // Already exported above
272
+ // getCurrentUserId // Already exported above
273
+ };
274
+
275
+ // Make sure initUserManager is exported
276
+ // export { initUserManager }; // Also exported above
public/js/modal.js ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // script-modularized/modal.js
2
+
3
+ export function showModal(id) {
4
+ if (id === 'loginModal') {
5
+ console.trace('showModal(\'loginModal\') called');
6
+ }
7
+ const modal = document.getElementById(id);
8
+ if (modal) modal.style.display = 'flex';
9
+ else console.error(`Modal with ID ${id} not found.`);
10
+ }
11
+
12
+ export function hideModal(id) {
13
+ const modal = document.getElementById(id);
14
+ if (modal) modal.style.display = 'none';
15
+ else console.error(`Modal with ID ${id} not found.`);
16
+ }
17
+
18
+ // Function to check if a modal is currently open/visible
19
+ export function isModalOpen(modalId) {
20
+ const modal = document.getElementById(modalId);
21
+ // Check if the modal exists and its display style is not 'none'
22
+ return modal && modal.style.display !== 'none';
23
+ }
24
+
25
+ export function showMessage(title, content) {
26
+ document.getElementById('messageModalTitle').textContent = title;
27
+ document.getElementById('messageModalContent').textContent = content;
28
+ }
29
+
30
+ const confirmModalEl = document.getElementById('confirmModal');
31
+ const confirmProceedBtn = document.getElementById('confirmProceedBtn');
32
+ const confirmCancelBtn = document.getElementById('confirmCancelBtn');
33
+
34
+ export function showConfirm(title, message) {
35
+ console.log('showConfirm function called with title:', title);
36
+ console.log('showConfirm called with message:', message);
37
+ return new Promise((resolve) => {
38
+ document.getElementById('confirmModalTitle').textContent = title;
39
+ document.getElementById('confirmModalContent').textContent = message;
40
+
41
+ const handleProceed = () => {
42
+ console.log('Confirm button clicked for:', message);
43
+ hideModal('confirmModal');
44
+ resolve(true);
45
+ };
46
+
47
+ const handleCancel = () => {
48
+ console.log('Cancel button clicked for:', message);
49
+ hideModal('confirmModal');
50
+ resolve(false); // Resolve with false on cancel
51
+ };
52
+
53
+ confirmProceedBtn.addEventListener('click', handleProceed, { once: true });
54
+ confirmCancelBtn.addEventListener('click', handleCancel, { once: true });
55
+
56
+ showModal('confirmModal');
57
+ });
58
+ }
public/js/services/commentService.js ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // public/js/services/commentService.js
2
+
3
+ import eventBus from '../utils/eventBus.js'; // Thay đổi import để lấy default export là eventBus
4
+
5
+ export async function getCommentsForTask(taskId) {
6
+ console.log('commentService: getCommentsForTask called for taskId', taskId);
7
+ try {
8
+ const db = firebase.firestore();
9
+ const appId = firebase.app().options.appId;
10
+ const snapshot = await db.collection('artifacts').doc(appId).collection('tasks').doc(taskId).collection('comments').orderBy('createdAt').get();
11
+ const comments = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
12
+ console.log('commentService: getCommentsForTask - Fetched', comments.length, 'comments');
13
+ return comments;
14
+ } catch (error) {
15
+ console.error('commentService: Error getting comments for task', taskId, ':', error);
16
+ throw error;
17
+ }
18
+ }
19
+
20
+ export async function addCommentToTask(taskId, commentData) {
21
+ console.log('commentService: addCommentToTask called for taskId', taskId, 'with data', commentData);
22
+ try {
23
+ const db = firebase.firestore();
24
+ const appId = firebase.app().options.appId;
25
+ const docRef = await db.collection('artifacts').doc(appId).collection('tasks').doc(taskId).collection('comments').add({
26
+ ...commentData,
27
+ createdAt: firebase.firestore.FieldValue.serverTimestamp() // Ensure timestamp is set by server
28
+ });
29
+ const newComment = { id: docRef.id, ...commentData, createdAt: new Date() }; // Return temporary date, actual timestamp handled by listener
30
+ console.log('commentService: addCommentToTask - Comment added with ID:', docRef.id);
31
+ // Publish event? Or should this be handled by the listener that fetches comments?
32
+ // eventBus.publish('commentAdded', { taskId, comment: newComment }); // Sử dụng eventBus.publish
33
+ return newComment;
34
+ } catch (error) {
35
+ console.error('commentService: Error adding comment to task', taskId, ':', error);
36
+ throw error;
37
+ }
38
+ }
39
+
40
+ export async function updateComment(taskId, commentId, updatedData) {
41
+ console.log('commentService: updateComment called for taskId', taskId, 'commentId', commentId, 'with data', updatedData);
42
+ try {
43
+ const db = firebase.firestore();
44
+ const appId = firebase.app().options.appId;
45
+ await db.collection('artifacts').doc(appId).collection('tasks').doc(taskId).collection('comments').doc(commentId).update({
46
+ ...updatedData,
47
+ updatedAt: firebase.firestore.FieldValue.serverTimestamp() // Add updated timestamp
48
+ });
49
+ console.log('commentService: updateComment - Comment updated successfully');
50
+ // eventBus.publish('commentUpdated', { taskId, commentId, updatedData }); // Sử dụng eventBus.publish
51
+ return { taskId, commentId, updatedData };
52
+ } catch (error) {
53
+ console.error('commentService: Error updating comment', commentId, 'for task', taskId, ':', error);
54
+ throw error;
55
+ }
56
+ }
57
+
58
+ export async function deleteComment(taskId, commentId) {
59
+ console.log('commentService: deleteComment called for taskId', taskId, 'commentId', commentId);
60
+ try {
61
+ const db = firebase.firestore();
62
+ const appId = firebase.app().options.appId;
63
+ await db.collection('artifacts').doc(appId).collection('tasks').doc(taskId).collection('comments').doc(commentId).delete();
64
+ console.log('commentService: deleteComment - Comment deleted successfully');
65
+ // eventBus.publish('commentDeleted', { taskId, commentId }); // Sử dụng eventBus.publish
66
+ return { taskId, commentId };
67
+ } catch (error) {
68
+ console.error('commentService: Error deleting comment', commentId, 'for task', taskId, ':', error);
69
+ throw error;
70
+ }
71
+ }
public/js/services/departmentService.js ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // public/js/services/departmentService.js
2
+
3
+ import eventBus from '../utils/eventBus.js';
4
+ import { db, appId } from '../firebase-init.js'; // Import db, appId
5
+
6
+ export async function getDepartments() {
7
+ console.log('departmentService: getDepartments called');
8
+ // Get Firebase instances inside the function
9
+ // const db = firebase.firestore(); // Removed
10
+ // const appId = firebase.app().options.appId; // Removed
11
+
12
+ try {
13
+ const snapshot = await db.collection('artifacts').doc(appId).collection('departments').orderBy('name').get(); // Sử dụng db, appId đã import
14
+ const departments = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
15
+ console.log('departmentService: getDepartments - Fetched', departments.length, 'departments');
16
+ return departments;
17
+ } catch (error) {
18
+ console.error('departmentService: Error getting departments:', error);
19
+ throw error;
20
+ }
21
+ }
22
+
23
+ async function checkDuplicateName(name, excludeId = null) {
24
+ console.log(`departmentService: checkDuplicateName called for name "${name}", excluding ID "${excludeId}"`);
25
+ // Get Firebase instances inside the function
26
+ // const db = firebase.firestore(); // Removed
27
+ // const appId = firebase.app().options.appId; // Removed
28
+ try {
29
+ let query = db.collection('artifacts').doc(appId).collection('departments').where('name', '==', name); // Sử dụng db, appId đã import
30
+
31
+ // When updating, exclude the current department
32
+ if (excludeId) {
33
+ // Note: Firestore does not support != queries directly
34
+ // A common workaround is to fetch all potentially matching docs and filter client-side
35
+ // However, for uniqueness checks, fetching by name and checking if any resulting doc has a different ID is sufficient and more efficient.
36
+ }
37
+
38
+ const snapshot = await query.get();
39
+
40
+ // Check if any documents were returned. If excludeId is provided, ensure the found document is not the one being updated.
41
+ // Return true if a duplicate *exists* (a doc with the same name but different ID if excludeId is provided)
42
+ return snapshot.docs.some(doc => (excludeId ? doc.id !== excludeId : true));
43
+ } catch (error) {
44
+ console.error('departmentService: Error checking duplicate name:', error);
45
+ throw error;
46
+ }
47
+ }
48
+
49
+
50
+ export async function createDepartment(departmentData) {
51
+ console.log('departmentService: createDepartment called with data', departmentData);
52
+ // Get Firebase instances inside the function
53
+ // const db = firebase.firestore(); // Removed
54
+ // const appId = firebase.app().options.appId; // Removed
55
+ try {
56
+ // Check for duplicate name before creating
57
+ if (await checkDuplicateName(departmentData.name)) {
58
+ const duplicateError = new Error('Duplicate department name');
59
+ duplicateError.message = `Phòng ban "${departmentData.name}" đã tồn tại.`; // Set user-friendly message
60
+ throw duplicateError;
61
+ }
62
+
63
+ const docRef = await db.collection('artifacts').doc(appId).collection('departments').add(departmentData); // Sử dụng db, appId đã import
64
+ const newDepartment = { id: docRef.id, ...departmentData };
65
+ console.log('departmentService: createDepartment - Department created with ID:', docRef.id);
66
+ // REMOVED: eventBus.publish('departmentCreated', newDepartment); // Publish event
67
+ console.log('departmentService: departmentCreated event published', newDepartment);eventBus.publish('departmentCreated', newDepartment);
68
+ return newDepartment; // Return the created department
69
+ } catch (error) {
70
+ console.error('departmentService: Error creating department:', error);
71
+ throw error; // Re-throw the error
72
+ }
73
+ }
74
+
75
+
76
+ export async function updateDepartment(departmentId, departmentData) {
77
+ console.log('departmentService: updateDepartment called for ID', departmentId, 'with data', departmentData);
78
+ // Get Firebase instances inside the function
79
+ // const db = firebase.firestore(); // Removed
80
+ // const appId = firebase.app().options.appId; // Removed
81
+ try {
82
+ // Check for duplicate name, excluding the current department being updated
83
+ if (await checkDuplicateName(departmentData.name, departmentId)) {
84
+ const duplicateError = new Error('Duplicate department name');
85
+ duplicateError.message = `Phòng ban "${departmentData.name}" đã tồn tại.`; // Set user-friendly message
86
+ throw duplicateError;
87
+ }
88
+
89
+ await db.collection('artifacts').doc(appId).collection('departments').doc(departmentId).update(departmentData); // Sử dụng db, appId đã import
90
+ const updatedDepartment = { id: departmentId, ...departmentData }; // Assuming departmentData contains all fields
91
+ console('departmentService: updateDepartment - Department updated successfully');
92
+ // REMOVED: eventBus.publish('departmentUpdated', updatedDepartment); // Publish event
93
+ return updatedDepartment; // Return the updated department data
94
+ } catch (error) {
95
+ console.error('departmentService: Error updating department:', error);
96
+ throw error; // Re-throw the error
97
+ }
98
+ }
99
+
100
+
101
+ export async function deleteDepartment(departmentId) {
102
+ console.log('departmentService: deleteDepartment START for ID', departmentId);
103
+ console.log('departmentService: deleteDepartment called for ID', departmentId);
104
+ // Get Firebase instances inside the function
105
+ // const db = firebase.firestore(); // Removed
106
+ // const appId = firebase.app().options.appId; // Removed
107
+ try {
108
+ await db.collection('artifacts').doc(appId).collection('departments').doc(departmentId).delete(); // Sử dụng db, appId đã import
109
+ console.log('departmentService: deleteDepartment - Department deleted successfully');
110
+ console.log('departmentService: departmentDeleted event published for ID:', departmentId); eventBus.publish('departmentDeleted', departmentId); // Publish event
111
+ console.log('departmentService: deleteDepartment END (Success) for ID', departmentId);
112
+ return departmentId; // Return the ID of the deleted department
113
+ } catch (error) {
114
+ console.error('departmentService: Error deleting department:', error);
115
+ console.log('departmentService: deleteDepartment END (Error) for ID', departmentId, 'Error:', error);
116
+ throw error; // Re-throw the error
117
+ }
118
+ }
119
+
120
+
121
+ // Function to get a single department by ID if needed
122
+ export async function getDepartmentById(departmentId) {
123
+ console.log('departmentService: getDepartmentById called for ID', departmentId);
124
+ // Get Firebase instances inside the function
125
+ // const db = firebase.firestore(); // Removed
126
+ // const appId = firebase.app().options.appId; // Removed
127
+ try {
128
+ const doc = await db.collection('artifacts').doc(appId).collection('departments').doc(departmentId).get(); // Sử dụng db, appId đã import
129
+ if (doc.exists) {
130
+ console.log('departmentService: getDepartmentById - Document found');
131
+ return { id: doc.id, ...doc.data() };
132
+ } else {
133
+ console.log('departmentService: getDepartmentById - Document not found');
134
+ return null; // Department not found
135
+ }
136
+ } catch (error) {console.error('departmentService: Error getting department by ID:', error);throw error;}
137
+ }
public/js/services/taskService.js ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // public/js/services/taskService.js
2
+
3
+ import eventBus from '../utils/eventBus.js';
4
+ import { db, appId } from '../firebase-init.js'; // Import db, appId
5
+
6
+ // --- Hàm cho collection Tasks ---
7
+
8
+ export async function getTasks() {
9
+ console.log('taskService: getTasks called');
10
+ // const db = firebase.firestore(); // Removed
11
+ // const appId = firebase.app().options.appId; // Removed
12
+ if (!db || !appId) { // Sử dụng db, appId đã import
13
+ console.error('taskService: getTasks - Firebase or appId not initialized.');
14
+ throw new Error('Firebase or App ID not available.');
15
+ }
16
+ try {
17
+ // Example: Get all tasks, you might want to add filtering/ordering later
18
+ const snapshot = await db.collection('artifacts').doc(appId).collection('tasks').get(); // Sử dụng db, appId đã import
19
+ const tasks = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
20
+ console.log('taskService: getTasks - Fetched', tasks.length, 'tasks');
21
+ return tasks;
22
+ } catch (error) {
23
+ console.error('taskService: Error getting tasks:', error);
24
+ throw error;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Fetches all comments for a given task from Firestore.
30
+ * @param {string} taskId - The ID of the task to get comments for.
31
+ * @returns {Promise<Array<object>>} A promise that resolves with an array of comment data objects.\n */
32
+ export async function getTaskCommentsFromService(taskId) { // Renamed to avoid conflict
33
+ console.log('taskService: getTaskCommentsFromService called for taskId', taskId);
34
+ // const db = firebase.firestore(); // Removed
35
+ // const appId = firebase.app().options.appId; // Removed
36
+ if (!db || !appId) { // Sử dụng db, appId đã import
37
+ console.error('taskService: Firebase or appId not initialized.');
38
+ throw new Error('Firebase not initialized.');
39
+ }
40
+ try {
41
+ const snapshot = await db.collection('artifacts').doc(appId).collection('tasks').doc(taskId).collection('comments').orderBy('createdAt').get(); // Sử dụng db, appId đã import
42
+
43
+ // --- Thêm log chi tiết tại đây ---
44
+ console.log('taskService: getTaskCommentsFromService - RAW Snapshot:', snapshot); // Log toàn bộ snapshot object
45
+ console.log('taskService: getTaskCommentsFromService - Snapshot empty:', snapshot.empty); // Kiểm tra snapshot rỗng
46
+ console.log('taskService: getTaskCommentsFromService - Snapshot size:', snapshot.size); // Kiểm tra kích thước snapshot
47
+
48
+ if (!snapshot.empty) {
49
+ console.log('taskService: getTaskCommentsFromService - Snapshot docs:', snapshot.docs); // Log mảng các DocumentSnapshot
50
+ snapshot.docs.forEach((doc, index) => {
51
+ console.log(`taskService: getTaskCommentsFromService - Doc ${index} ID:`, doc.id);
52
+ console.log(`taskService: getTaskCommentsFromService - Doc ${index} Data:`, doc.data());
53
+ console.log(`taskService: getTaskCommentsFromService - Doc ${index} Exists:`, doc.exists); // Kiểm tra tài liệu có tồn tại không
54
+ });
55
+ }
56
+ // --- Kết thúc log chi tiết ---
57
+
58
+
59
+ const comments = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
60
+ console.log('taskService: getTaskCommentsFromService successful. Found', comments.length, 'comments.');
61
+ return comments;
62
+ } catch (error) {
63
+ console.error('taskService: getTaskCommentsFromService error:', error);
64
+ throw error;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Creates a new comment for a given task in Firestore.
70
+ * @param {string} taskId - The ID of the task the comment belongs to.\n * @param {object} commentData - The data for the new comment (e.g., text, userId, timestamp).\n * @returns {Promise<object>} A promise that resolves with the new comment data object including ID.\n */
71
+ export async function createCommentForTask(taskId, commentData) { // Renamed
72
+ console.log('taskService: createCommentForTask called for taskId', taskId, 'with data', commentData);
73
+ // const db = firebase.firestore(); // Removed
74
+ // const appId = firebase.app().options.appId; // Removed
75
+ if (!db || !appId) { // Sử dụng db, appId đã import
76
+ console.error('taskService: Firebase or appId not initialized.');
77
+ throw new Error('Firebase not initialized.');
78
+ }
79
+ try {
80
+ const commentsCollectionRef = db.collection('artifacts').doc(appId).collection('tasks').doc(taskId).collection('comments'); // Sử dụng db, appId đã import
81
+ const docRef = await commentsCollectionRef.add(commentData);
82
+ const newCommentSnapshot = await docRef.get();
83
+ const newCommentData = { id: newCommentSnapshot.id, ...newCommentSnapshot.data() };
84
+ console.log('taskService: createCommentForTask successful. New comment:', newCommentData);
85
+ return newCommentData;
86
+ } catch (error) {
87
+ console.error('taskService: createCommentForTask error:', error);
88
+ throw error;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Updates an existing comment in Firestore.
94
+ * @param {string} taskId - The ID of the task the comment belongs to.
95
+ * @param {string} commentId - The ID of the comment to update.
96
+ * @param {object} updateData - The data to update (e.g., { text: 'new text' }).\n * @returns {Promise<void>} A promise that resolves when the update is complete.\n */
97
+ export async function updateTaskComment(taskId, commentId, updateData) {
98
+ console.log('taskService: updateTaskComment called for taskId', taskId, 'commentId', commentId, 'with data', updateData);
99
+ // const db = firebase.firestore(); // Removed
100
+ // const appId = firebase.app().options.appId; // Removed
101
+ if (!db || !appId) { // Sử dụng db, appId đã import
102
+ console.error('taskService: Firebase or appId not initialized.');
103
+ throw new Error('Firebase not initialized.');
104
+ }
105
+ try {
106
+ const commentDocRef = db.collection('artifacts').doc(appId).collection('tasks').doc(taskId).collection('comments').doc(commentId); // Sử dụng db, appId đã import
107
+ await commentDocRef.update(updateData);
108
+ console.log('taskService: updateTaskComment successful for commentId', commentId);
109
+ // Return void or success status
110
+ } catch (error) {
111
+ console.error('taskService: updateTaskComment error:', error);
112
+ throw error;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Deletes a comment from Firestore.
118
+ * @param {string} taskId - The ID of the task the comment belongs to.
119
+ * @param {string} commentId - The ID of the comment to delete.
120
+ * @returns {Promise<void>} A promise that resolves when the deletion is complete.\n */
121
+ export async function deleteTaskComment(taskId, commentId) { // Renamed
122
+ console.log('taskService: deleteTaskComment called for taskId', taskId, 'commentId', commentId);
123
+ // const db = firebase.firestore(); // Removed
124
+ // const appId = firebase.app().options.appId; // Removed
125
+ if (!db || !appId) { // Sử dụng db, appId đã import
126
+ console.error('taskService: Firebase or appId not initialized.');
127
+ throw new Error('Firebase not initialized.');
128
+ }
129
+ try {
130
+ const commentDocRef = db.collection('artifacts').doc(appId).collection('tasks').doc(taskId).collection('comments').doc(commentId); // Sử dụng db, appId đã import
131
+ await commentDocRef.delete();
132
+ console.log('taskService: deleteTaskComment successful for taskId', taskId, 'commentId', commentId);
133
+ } catch (error) {
134
+ console.error('taskService: deleteTaskComment error:', error);
135
+ throw error;
136
+ }
137
+ }
138
+ export async function createTask(taskData) {
139
+ console.log('taskService: createTask called with data', taskData);
140
+ // const db = firebase.firestore(); // Removed
141
+ // const appId = firebase.app().options.appId; // Removed
142
+ if (!db || !appId) { // Sử dụng db, appId đã import
143
+ console.error('taskService: createTask - Firebase or appId not initialized.');
144
+ throw new Error('Firebase or App ID not available.');
145
+ }
146
+ try {
147
+ taskData.createdAt = firebase.firestore.FieldValue.serverTimestamp(); // Giữ lại FieldValue nếu Firebase compat được load toàn cục
148
+ const docRef = await db.collection('artifacts').doc(appId).collection('tasks').add(taskData); // Sử dụng db, appId đã import
149
+ const newTask = { id: docRef.id, ...taskData };
150
+ console.log('taskService: createTask - Task created with ID:', docRef.id);eventBus.publish('taskCreated', newTask); // Publish event
151
+ return newTask;
152
+ } catch (error) {
153
+ console.error('taskService: Error creating task:', error);
154
+ throw error;
155
+ }
156
+ }
157
+
158
+ export async function updateTask(taskId, taskData) {
159
+ console.log('taskService: updateTask called for ID', taskId, 'with data', taskData);
160
+ // const db = firebase.firestore(); // Removed
161
+ // const appId = firebase.app().options.appId; // Removed
162
+ if (!db || !appId) { // Sử dụng db, appId đã import
163
+ console.error('taskService: updateTask - Firebase or appId not initialized.');
164
+ throw new Error('Firebase or App ID not available.');
165
+ }
166
+ try {
167
+ await db.collection('artifacts').doc(appId).collection('tasks').doc(taskId).update(taskData); // Sử dụng db, appId đã import
168
+ const updatedTask = { id: taskId, ...taskData };console.log('taskService: updateTask - Task updated successfully');
169
+ eventBus.publish('taskUpdated', updatedTask); // Publish event
170
+ return updatedTask;
171
+ } catch (error) {
172
+ console.error('taskService: Error updating task:', error);
173
+ throw error;
174
+ }
175
+ }
176
+
177
+ export async function deleteTask(taskId) {
178
+ console.log('taskService: deleteTask called for ID', taskId);
179
+ // const db = firebase.firestore(); // Removed
180
+ // const appId = firebase.app().options.appId; // Removed
181
+ if (!db || !appId) { // Sử dụng db, appId đã import
182
+ console.error('taskService: deleteTask - Firebase or appId not initialized.');
183
+ throw new Error('Firebase or App ID not available.');
184
+ }
185
+ try {
186
+ await db.collection('artifacts').doc(appId).collection('tasks').doc(taskId).delete(); // Sử dụng db, appId đã import
187
+ eventBus.publish('taskDeleteCompleted', taskId); // Publish event
188
+ console.log('taskService: deleteTask - Task deleted successfully');
189
+ return taskId;
190
+ } catch (error) {
191
+ console.error('taskService: Error deleting task:', error);
192
+ throw error;
193
+ }
194
+ }
195
+
196
+ // --- Hàm cho subcollection Comments ---
197
+
198
+ export async function getTaskComments(taskId) {
199
+ console.log('taskService: getTaskComments called for Task ID', taskId);
200
+ // const db = firebase.firestore(); // Removed
201
+ // const appId = firebase.app().options.appId; // Removed
202
+ if (!db || !appId) { // Sử dụng db, appId đã import
203
+ console.error('taskService: getTaskComments - Firebase or appId not initialized.');
204
+ throw new Error('Firebase or App ID not available.');
205
+ }
206
+ try {
207
+ console.log('taskService: getTaskComments - Using appId:', appId, 'and taskId:', taskId);
208
+ console.log('taskService: getTaskComments - Firestore path:', `artifacts/${appId}/tasks/${taskId}/comments`);
209
+ const snapshot = await db.collection('artifacts').doc(appId).collection('tasks').doc(taskId).collection('comments').orderBy('createdAt').get(); // Sử dụng db, appId đã import
210
+
211
+ // --- Thêm log chi tiết tại đây ---
212
+ console.log('taskService: getTaskComments - RAW Snapshot:', snapshot); // Log toàn bộ snapshot object
213
+ console.log('taskService: getTaskComments - Snapshot empty:', snapshot.empty); // Kiểm tra snapshot rỗng
214
+ console.log('taskService: getTaskComments - Snapshot size:', snapshot.size); // Kiểm tra kích thước snapshot
215
+
216
+ if (!snapshot.empty) {
217
+ console.log('taskService: getTaskComments - Snapshot docs:', snapshot.docs); // Log mảng các DocumentSnapshot
218
+ snapshot.docs.forEach((doc, index) => {
219
+ console.log(`taskService: getTaskComments - Doc ${index} ID:`, doc.id);
220
+ console.log(`taskService: getTaskComments - Doc ${index} Data:`, doc.data());
221
+ console.log(`taskService: getTaskComments - Doc ${index} Exists:`, doc.exists); // Kiểm tra tài liệu có tồn tại không
222
+ });
223
+ }
224
+ // --- Kết thúc log chi tiết ---
225
+
226
+ const comments = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
227
+ console.log('taskService: getTaskComments - Fetched', comments.length, 'comments for Task ID', taskId);
228
+ console.log('taskService: getTaskComments - Fetched comments data:', comments);
229
+ return comments;
230
+ } catch (error) {
231
+ console.error('taskService: Error getting comments for Task ID', taskId, ':', error);
232
+ throw error;
233
+ }
234
+ }
235
+
236
+ export async function addCommentToTask(taskId, commentData) {
237
+ console.log('taskService: addCommentToTask called for Task ID', taskId, 'with data', commentData);
238
+ // const db = firebase.firestore(); // Removed
239
+ // const appId = firebase.app().options.appId; // Removed
240
+ if (!db || !appId) { // Sử dụng db, appId đã import
241
+ console.error('taskService: addCommentToTask - Firebase or appId not initialized.');
242
+ throw new Error('Firebase or App ID not available.');
243
+ }
244
+ try {
245
+ const docRef = await db.collection('artifacts').doc(appId).collection('tasks').doc(taskId).collection('comments').add(commentData); // Sử dụng db, appId đã import
246
+ console.log('taskService: addCommentToTask - Comment added with ID:', docRef.id);
247
+ const newCommentSnapshot = await docRef.get();
248
+ const newCommentData = { id: newCommentSnapshot.id, ...newCommentSnapshot.data() };
249
+ console.log('taskService: addCommentToTask - Fetched new comment data:', newCommentData);
250
+ return newCommentData; // Change return value
251
+ } catch (error) {
252
+ console.error('taskService: Error adding comment to Task ID', taskId, ':', error);
253
+ throw error;
254
+ }
255
+ }
256
+
257
+ // Note: Soft delete is handled in the comments.js file by calling this updateTaskComment
258
+ // function with { deleted: true, ... }. You could also create a dedicated softDeleteTaskComment
259
+ // function here if preferred.
public/js/services/userService.js ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { db, auth, appId } from '../firebase-init.js'; // Import db, auth, appId
2
+
3
+ export async function getUserById(userId) {
4
+ console.log('userService: getUserById called for ID', userId);
5
+ // Get Firebase services and appId inside the function
6
+ // const db = firebase.firestore(); // Removed
7
+ // const appId = firebase.app().options.appId; // Removed
8
+ try {
9
+ // Removed initialization check
10
+ await db.collection('artifacts').doc(appId).collection('users').doc(userId).get(); // Sử dụng db, appId đã import
11
+ // ... rest of the code ...
12
+ } catch (error) {
13
+ console.error('userService: Error getting user by ID:', error);
14
+ throw error;
15
+ }
16
+ }
17
+
18
+ export async function getAllUsers() {
19
+ console.log('userService: getAllUsers called');
20
+ // Get Firebase services and appId inside the function
21
+ // const db = firebase.firestore(); // Removed
22
+ // const appId = firebase.app().options.appId; // Removed
23
+ try {
24
+ // Removed initialization check
25
+ const snapshot = await db.collection('artifacts').doc(appId).collection('users').orderBy('name').get(); // Sử dụng db, appId đã import
26
+ const users = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
27
+ console.log('userService: getAllUsers - Fetched', users.length, 'users');
28
+ return users;
29
+ } catch (error) {
30
+ console.error('userService: Error getting all users:', error);
31
+ throw error;
32
+ }
33
+ }
34
+
35
+ // This function should handle user creation including Auth
36
+ export async function createUser(userData) {
37
+ console.log('userService: createUser called with data (excluding password)', { ...userData, password: '***' });
38
+ // Get Firebase services and appId inside the function
39
+ // const auth = firebase.auth(); // Removed
40
+ // const db = firebase.firestore(); // Removed
41
+ // const appId = firebase.app().options.appId; // Removed
42
+ try {
43
+ // Removed initialization check
44
+
45
+ // 1. Create user in Firebase Authentication
46
+ const userCredential = await auth.createUserWithEmailAndPassword(userData.email, userData.password); // Sử dụng auth đã import
47
+ const firebaseUser = userCredential.user;
48
+
49
+ // 2. Add user data to Firestore under the specific artifact
50
+ const userDocRef = db.collection('artifacts').doc(appId).collection('users').doc(firebaseUser.uid); // Sử dụng db, appId đã import
51
+ await userDocRef.set({
52
+ name: userData.name,
53
+ email: userData.email, // Store email for reference
54
+ role: userData.role || 'user', // Default role
55
+ department: userData.department || null, // Optional department
56
+ createdAt: firebase.firestore.FieldValue.serverTimestamp(), // Giữ lại FieldValue nếu Firebase compat được load toàn cục
57
+ // avatarUrl: userData.avatarUrl // Add if generating/uploading avatars
58
+ });
59
+
60
+ console.log('userService: createUser - User created in Auth and Firestore for UID:', firebaseUser.uid);
61
+ // Fetch the created user data from Firestore to ensure it's complete
62
+ const newUserDoc = await userDocRef.get();
63
+ const newUser = { id: newUserDoc.id, ...newUserDoc.data() };
64
+
65
+ return newUser;
66
+ } catch (error) {
67
+ console.error('userService: Error creating user:', error);
68
+ // Clean up Auth user if Firestore write fails? Depends on error handling strategy.
69
+ throw error; // Re-throw the error for the caller to handle
70
+ }
71
+ }
72
+
73
+
74
+ export async function updateUser(userId, userData) {
75
+ console.log('userService: updateUser called for ID', userId, 'with data', userData);
76
+ // Get Firebase services and appId inside the function
77
+ // const db = firebase.firestore(); // Removed
78
+ // const appId = firebase.app().options.appId; // Removed
79
+ try {
80
+ // Removed initialization check
81
+
82
+ // Update user data in Firestore
83
+ const userDocRef = db.collection('artifacts').doc(appId).collection('users').doc(userId); // Sử dụng db, appId đã import
84
+ await userDocRef.update(userData);
85
+
86
+ console.log('userService: updateUser - User data updated in Firestore for UID:', userId);
87
+
88
+ // Optionally fetch and return the updated user data
89
+ const updatedUserDoc = await userDocRef.get();
90
+ const updatedUser = { id: updatedUserDoc.id, ...updatedUserDoc.data() };
91
+
92
+ return updatedUser;
93
+
94
+ } catch (error) {
95
+ console.error('userService: Error updating user:', error);
96
+ throw error;
97
+ }
98
+ }
99
+
100
+ export async function deleteUser(userId) {
101
+ console.log('userService: deleteUser called for ID', userId);
102
+ // Get Firebase services and appId inside the function
103
+ // const auth = firebase.auth(); // Removed
104
+ // const db = firebase.firestore(); // Removed
105
+ // const appId = firebase.app().options.appId; // Removed
106
+ try {
107
+ // Removed initialization check
108
+
109
+ // 1. Delete user data from Firestore
110
+ await db.collection('artifacts').doc(appId).collection('users').doc(userId).delete(); // Sử dụng db, appId đã import
111
+ console.log('userService: deleteUser - User data deleted from Firestore for UID:', userId);
112
+
113
+ // 2. Delete user from Firebase Authentication
114
+ // NOTE: Deleting a user requires elevated privileges (Admin SDK) or being the user themselves.
115
+ // This client-side delete function is likely to fail for other users unless you use Cloud Functions.
116
+ // Assuming for now you have a mechanism (e.g., Admin SDK via Cloud Functions) to handle Auth deletion safely.
117
+ console.warn('userService: deleteUser - Client-side Auth user deletion for other users is not possible without Admin SDK. Implement via Cloud Functions.');
118
+
119
+ return userId;
120
+
121
+ } catch (error) {
122
+ console.error('userService: Error deleting user:', error);
123
+ throw error;
124
+ }
125
+ }
public/js/tasks.js ADDED
@@ -0,0 +1,361 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // script-modularized/tasks.js
2
+
3
+ import { showModal, hideModal, showMessage, showConfirm } from './modal.js'; // Import showConfirm
4
+ import { toggleButtonLoading } from './utils/utils.js';
5
+ import { populateUserDropdown, populateDepartmentDropdown } from './utils/dropdownHelpers.js';
6
+ import eventBus from './utils/eventBus.js';
7
+ import * as taskManager from './managers/taskManager.js'; // Import taskManager
8
+ import { handleError } from './utils/errorHandler.js';
9
+
10
+ // --- DOM Elements (Cache) ---
11
+ const taskFormElement = document.getElementById('taskForm');
12
+ const taskIdElement = document.getElementById('taskId');
13
+ const taskModalTitleElement = document.getElementById('taskModalTitle');
14
+ const saveTaskBtnElement = document.getElementById('saveTaskBtn');
15
+ const deleteTaskBtnElement = document.getElementById('deleteTaskBtn');
16
+ const taskAssignedToElement = document.getElementById('taskAssignedTo'); // Cache dropdown elements
17
+ const taskDepartmentElement = document.getElementById('taskDepartment');
18
+ const taskTitleElement = document.getElementById('taskTitle');
19
+ const taskDescriptionElement = document.getElementById('taskDescription');
20
+ const taskDueDateElement = document.getElementById('taskDueDate');
21
+ const taskStatusElement = document.getElementById('taskStatus');
22
+ const taskPriorityElement = document.getElementById('taskPriority');
23
+ const taskProgressElement = document.getElementById('taskProgress');
24
+ const addTaskModalElement = document.getElementById('addTaskModal'); // Cache the modal element
25
+
26
+ document.addEventListener('DOMContentLoaded', () => {
27
+ console.log('tasks.js: DOMContentLoaded. Setting up task UI listeners.');
28
+ setupTaskEventListeners(); // Setup listeners for UI actions and Manager events
29
+
30
+ // Add listener for the "Add Task" button (if it exists)
31
+ const addTaskButton = document.getElementById('openAddTaskModal'); // Target the correct button
32
+ if (addTaskButton) {
33
+ addTaskButton.addEventListener('click', setupTaskModalForAdd);
34
+ console.log('tasks.js: Click listener added for #openAddTaskModal.');
35
+ }
36
+ });
37
+
38
+
39
+ /**
40
+ * Sets up the task modal for adding a new task.
41
+ */
42
+ function setupTaskModalForAdd() {
43
+ console.log('tasks.js: setupTaskModalForAdd called.');
44
+ if (taskFormElement && taskIdElement && taskModalTitleElement && saveTaskBtnElement && deleteTaskBtnElement) {
45
+ taskFormElement.reset();
46
+ taskIdElement.value = '';
47
+ taskModalTitleElement.textContent = 'Thêm công việc';
48
+ saveTaskBtnElement.textContent = 'Lưu';
49
+ deleteTaskBtnElement.style.display = 'none'; // Hide delete button for add mode
50
+
51
+ // Populate dropdowns
52
+ populateUserDropdown('taskAssignedTo', null);
53
+ populateDepartmentDropdown('taskDepartment');
54
+
55
+ showModal('addTaskModal');
56
+ } else {
57
+ console.error('tasks.js: setupTaskModalForAdd - Required modal elements not found.');
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Sets up the task modal for editing an existing task.
63
+ * @param {string} id - The ID of the task.
64
+ * @param {string} title - The title of the task.
65
+ * @param {string} description - The description of the task.
66
+ * @param {string} departmentId - The ID of the department.
67
+ * @param {string} assignedToUid - The UID of the assigned user.
68
+ * @param {string} status - The status of the task.
69
+ * @param {string} dueDateStr - The due date string (YYYY-MM-DD).
70
+ * @param {string} priority - The priority of the task.
71
+ * @param {number} progress - The progress percentage.
72
+ */
73
+ async function editTask(id, title, description, departmentId, assignedToUid, status, dueDateStr, priority, progress) {
74
+ console.log('tasks.js: editTask called with ID:', id);
75
+ if (!id || !taskFormElement || !taskIdElement || !taskModalTitleElement || !saveTaskBtnElement || !deleteTaskBtnElement || !taskTitleElement || !taskDescriptionElement || !taskDueDateElement || !taskStatusElement || !taskPriorityElement || !taskProgressElement) {
76
+ console.error('tasks.js: editTask - Required modal elements or task ID missing.');
77
+ // Optionally publish an error event or show message
78
+ return;
79
+ }
80
+
81
+ taskFormElement.reset(); // Reset form first
82
+ taskIdElement.value = id;
83
+ taskModalTitleElement.textContent = 'Sửa công việc';
84
+ taskTitleElement.value = title;
85
+ taskDescriptionElement.value = description;
86
+ taskDueDateElement.value = dueDateStr;
87
+ taskStatusElement.value = status;
88
+ taskPriorityElement.value = priority || 'Trung bình';
89
+ taskProgressElement.value = progress || 0;
90
+
91
+
92
+ // Populate dropdowns *before* attempting to set selected values
93
+ await populateDepartmentDropdown('taskDepartment', departmentId); // Populate department dropdown and select
94
+ await populateUserDropdown('taskAssignedTo', assignedToUid); // Populate user dropdown and select
95
+
96
+
97
+ saveTaskBtnElement.textContent = 'Lưu'; // Changed to 'Lưu'
98
+ deleteTaskBtnElement.style.display = 'block'; // Show delete button for edit mode
99
+
100
+ // REMOVED: Direct onclick assignment. Delete handling is now via EventBus.
101
+ // document.getElementById('deleteTaskBtn').onclick = async () => await taskManager.handleDeleteTask(id);
102
+
103
+ showModal('addTaskModal'); // Show the modal
104
+ }
105
+
106
+
107
+ // --- UI Action Handlers (Triggered by UI Events) ---
108
+ // These functions handle UI interactions and publish events for the Manager.
109
+
110
+ /**
111
+ * Handles the submit event of the task form (for both add and edit).
112
+ * @param {Event} event - The form submit event.
113
+ */
114
+ function handleTaskFormSubmit(event) {
115
+ console.log('tasks.js: handleTaskFormSubmit called.');
116
+ event.preventDefault(); // Prevent default form submission
117
+
118
+ if (!taskFormElement) {
119
+ console.error('tasks.js: handleTaskFormSubmit - taskFormElement not found.');
120
+ return;
121
+ }
122
+
123
+ const taskId = taskIdElement?.value.trim(); // Get task ID (will be empty for new task)
124
+ const title = taskTitleElement?.value.trim();
125
+ const description = taskDescriptionElement?.value.trim();
126
+ const departmentId = taskDepartmentElement.value; // Get value directly from cached element
127
+ const assignedToUid = taskAssignedToElement.value; // Get value directly from cached element
128
+ const status = taskStatusElement.value;
129
+ const dueDate = taskDueDateElement.value; // YYYY-MM-DD format
130
+ const priority = taskPriorityElement.value;
131
+ const progress = parseInt(taskProgressElement.value, 10) || 0; // Parse as integer
132
+
133
+ // Basic Validation (can be more extensive)
134
+ if (!title || !status) {
135
+ console.log('tasks.js: Validation check - Title:', title, 'Status:', status);
136
+ handleError(new Error('Validation Error'), 'Tiêu đề và trạng thái công việc không được để trống.');
137
+ return;
138
+ }
139
+
140
+ const taskData = {
141
+ title: title,
142
+ description: description,
143
+ departmentId: departmentId,
144
+ assignedToUid: assignedToUid,
145
+ status: status,
146
+ dueDate: dueDate,
147
+ priority: priority,
148
+ progress: progress,
149
+ // createdAt and updatedAt timestamps will be handled by the Service/Manager
150
+ };
151
+
152
+ console.log('tasks.js: Task data being published (direct element value):', taskData); // Add this console log
153
+ console.log('tasks.js: Task form data captured:', taskData, 'Task ID:', taskId);
154
+
155
+ if (taskId) {
156
+ // Editing existing task
157
+ console.log('tasks.js: Publishing updateTaskSubmit event for ID:', taskId);
158
+ eventBus.publish('updateTaskSubmit', { taskId: taskId, taskData: taskData });
159
+ } else {
160
+ // Adding new task
161
+ console.log('tasks.js: Publishing saveTaskSubmit event (add new task).');
162
+ eventBus.publish('saveTaskSubmit', taskData);
163
+ }
164
+
165
+ // UI does NOT hide modal or show success/error messages here.
166
+ // This is handled by the listeners for Manager events.
167
+ }
168
+
169
+ /**
170
+ * Handles the click event on the Delete Task button inside the modal.
171
+ */
172
+ function handleDeleteTaskClick() {
173
+ console.log('tasks.js: handleDeleteTaskClick called.');
174
+ if (!taskIdElement || !taskTitleElement) {
175
+ console.error('tasks.js: handleDeleteTaskClick - taskIdElement or taskTitleElement not found.');
176
+ return;
177
+ }
178
+ const taskId = taskIdElement.value;
179
+ const taskTitle = taskTitleElement.value; // Get title for confirmation message
180
+
181
+ if (!taskId) {
182
+ console.warn('tasks.js: handleDeleteTaskClick - No task ID found for deletion.');
183
+ handleError(null, 'Không tìm thấy ID công việc để xoá.');
184
+ return;
185
+ }
186
+
187
+ // Show confirmation dialog
188
+ showConfirm('Xác nhận xóa công việc', `Bạn có chắc chắn muốn xóa công việc "${taskTitle}" không?`)
189
+ .then(confirmed => {
190
+ if (confirmed) {
191
+ console.log('tasks.js: User confirmed deletion. Publishing deleteTaskClicked event for ID:', taskId);
192
+ // Publish event for Manager to handle deletion
193
+ eventBus.publish('deleteTaskClicked', taskId);
194
+ // UI will wait for taskDeleted or taskDeleteFailed event from Manager
195
+ } else {
196
+ console.log('tasks.js: User cancelled deletion.');
197
+ }
198
+ })
199
+ .catch(error => {
200
+ console.error('tasks.js: Error showing confirmation dialog:', error);
201
+ handleError(error, 'Lỗi khi hiển thị xác nhận xoá.');
202
+ });
203
+ }
204
+
205
+ // --- EventBus Listeners (Listen for events from Manager) ---
206
+
207
+ function setupTaskEventListeners() {
208
+ console.log('tasks.js: Setting up EventBus listeners.');
209
+
210
+ // Listener for editTaskClicked event from Kanban board or other modules
211
+ eventBus.subscribe('editTaskClicked', ({ taskId }) => {
212
+ console.log('tasks.js: EventBus received editTaskClicked for Task ID:', taskId);
213
+ const task = taskManager.getTaskByIdFromCache(taskId);
214
+
215
+ if (task) {
216
+ // Format the dueDate for the input field (YYYY-MM-DD)
217
+ const dueDateFormatted = task.dueDate
218
+ ? (task.dueDate.toDate ? moment(task.dueDate.toDate()).format('YYYY-MM-DD') : moment(task.dueDate).format('YYYY-MM-DD'))
219
+ : '';
220
+
221
+ // Call the editTask function to populate the modal
222
+ editTask(task.id, task.title, task.description, task.departmentId, task.assignedToUid, task.status, dueDateFormatted, task.priority, task.progress);
223
+ } else {
224
+ console.error('tasks.js: Could not find task in cache for editing with ID:', taskId);
225
+ // Optionally show an error message to the user
226
+ handleError(null, 'Không tìm thấy thông tin công việc để chỉnh sửa.');
227
+ }
228
+ });
229
+
230
+
231
+ // Listeners for Task Save/Update events from Task Manager
232
+ eventBus.subscribe('taskSaveStarted', () => {
233
+ console.log('tasks.js: EventBus received taskSaveStarted. Toggling loading.');
234
+ toggleButtonLoading('saveTaskBtn', true);
235
+ });
236
+
237
+ eventBus.subscribe('taskSaveStarted', () => {
238
+ console.log('tasks.js: EventBus received taskSaveStarted. Toggling loading.');
239
+ toggleButtonLoading('saveTaskBtn', true);
240
+ });
241
+
242
+ eventBus.subscribe('taskAdded', (newTask) => { // Listen for taskAdded event from Manager
243
+ console.log('tasks.js: EventBus received taskAdded. Hiding modal and showing success message.', newTask);
244
+ toggleButtonLoading('saveTaskBtn', false);
245
+ showMessage('Thành công', 'Công việc đã được tạo.');
246
+ hideModal('addTaskModal'); // Hide the modal after successful add
247
+ // Note: UI list update is handled by the listener for taskAdded in the task list rendering module
248
+ });
249
+
250
+ eventBus.subscribe('taskUpdated', (updatedTask) => { // Listen for taskUpdated event from Manager
251
+ console.log('tasks.js: EventBus received taskUpdated. Hiding modal and showing success message.', updatedTask);
252
+ toggleButtonLoading('saveTaskBtn', false);
253
+ showMessage('Thành công', 'Đã cập nhật công việc.');
254
+ hideModal('addTaskModal'); // Hide the modal after successful update
255
+ // Note: UI list update is handled by the listener for taskUpdated in the task list rendering module
256
+ });
257
+
258
+ eventBus.subscribe('taskActionFailed', ({ action, message, error }) => { // Listen for failed task actions
259
+ console.error(`tasks.js: EventBus received taskActionFailed for action "${action}". Message:`, message, 'Error:', error);
260
+ toggleButtonLoading('saveTaskBtn', false); // Ensure button is enabled
261
+ handleError(error, message); // Show error message
262
+ // Modals remain open to allow correction
263
+ });
264
+
265
+
266
+ // Listeners for Task Delete events from Task Manager
267
+ eventBus.subscribe('taskDeleteStarted', (taskId) => {
268
+ console.log(`tasks.js: EventBus received taskDeleteStarted for ID: ${taskId}.`);
269
+ // Optional: Add a visual cue in the UI list (e.g., disable or fade the task card)
270
+ // This would be handled in the module that renders the task list.
271
+ });
272
+
273
+ eventBus.subscribe('taskDeleted', (deletedTaskId) => { // Listen for taskDeleted event from Manager
274
+ console.log('tasks.js: EventBus received taskDeleted for ID:', deletedTaskId);
275
+ showMessage('Thành công', 'Đã xoá công việc.'); // Show success message
276
+ hideModal('addTaskModal'); // Hide the modal after successful deletion
277
+ // Note: UI list update is handled by the listener for taskDeleted in the task list rendering module
278
+ });
279
+
280
+ eventBus.subscribe('taskDeleteFailed', ({ taskId, message, error }) => { // Listen for failed delete action
281
+ console.error(`tasks.js: EventBus received taskDeleteFailed for ID ${taskId}. Message:`, message, 'Error:', error);
282
+ handleError(error, message); // Show error message
283
+ // Modal might remain open or close depending on UX decision on delete failure
284
+ });
285
+
286
+
287
+ // Listener for User data updates - populate user dropdown
288
+ eventBus.subscribe('usersDataReady', (usersData) => { // Receive data with event
289
+ console.log('tasks.js: EventBus received usersDataReady. Populating task assignedTo dropdown.');
290
+ populateUserDropdown('taskAssignedTo', null, usersData); // Pass data to helper
291
+
292
+ // Check if the edit task modal is open and set the selected user
293
+ // This requires accessing the task data being edited, which should ideally be managed
294
+ // by a state mechanism or passed with the editTask call.
295
+ // For now, let's assume editTask populates the value directly.
296
+ // A better approach: When 'taskDetailModalOpened' (or similar) is handled
297
+ // and editTask is called, the userManager's cache is already ready,
298
+ // and populateUserDropdown should set the value correctly using the initial data.
299
+ const taskModal = document.getElementById('addTaskModal'); // Assuming addTaskModal is used for editing too
300
+ if (taskModal && taskModal.classList.contains('show') && taskIdElement && taskIdElement.value) {
301
+ // Attempt to set value if already in edit mode
302
+ const currentAssignedToUid = taskManager.getTaskByIdFromCache(taskIdElement.value)?.assignedToUid;
303
+ if (currentAssignedToUid) {
304
+ taskAssignedToElement.value = currentAssignedToUid;
305
+ console.log('tasks.js: Set assignedTo dropdown value in open edit modal.');
306
+ }
307
+ }
308
+ });
309
+
310
+ // Listener for Department data updates - populate department dropdown
311
+ eventBus.subscribe('departmentsDataReady', (departmentsData) => { // Receive data with event
312
+ try {
313
+ console.log('tasks.js: EventBus received departmentsDataReady. Populating task department dropdown.');
314
+ populateDepartmentDropdown('taskDepartment', null, departmentsData); // Pass data to helper
315
+
316
+ // Check if the edit task modal is open and set the selected department
317
+ const taskModal = document.getElementById('addTaskModal');
318
+ if (taskModal && taskModal.classList.contains('show') && taskIdElement && taskIdElement.value) {
319
+ const currentDepartmentId = taskManager.getTaskByIdFromCache(taskIdElement.value)?.departmentId;
320
+ if (currentDepartmentId) {
321
+ taskDepartmentElement.value = currentDepartmentId;
322
+ console.log('tasks.js: Set department dropdown value in open edit modal.');
323
+ }
324
+ }
325
+
326
+ } catch (error) {
327
+ console.error('tasks.js: Error in departmentsDataReady listener:', error);
328
+ handleError(error, 'Lỗi khi tải danh sách phòng ban.'); // Use error handler
329
+ }
330
+ });
331
+
332
+
333
+ // Add submit listener for the task form
334
+ if (taskFormElement) {
335
+ taskFormElement.addEventListener('submit', handleTaskFormSubmit);
336
+ console.log('tasks.js: Submit listener added for #taskForm.');
337
+ } else {
338
+ console.warn('tasks.js: #taskForm element not found, skipping submit listener setup.');
339
+ }
340
+
341
+ // Add click listener for the Delete Task button inside the modal
342
+ if (deleteTaskBtnElement) {
343
+ deleteTaskBtnElement.addEventListener('click', handleDeleteTaskClick);
344
+ console.log('tasks.js: Click listener added for #deleteTaskBtn.');
345
+ } else {
346
+ console.warn('tasks.js: #deleteTaskBtn element not found, skipping click listener setup.');
347
+ }
348
+
349
+ // Note: Click listener for Edit/Delete buttons on individual task items in the list
350
+ // should be handled in the module that renders the task list, publishing
351
+ // events like 'editTaskClicked' or 'deleteTaskClicked' (before confirmation).
352
+ // The 'deleteTaskClicked' event is then handled here to show confirmation.
353
+ }
354
+
355
+ // Export functions that might be needed externally
356
+ export {
357
+ setupTaskModalForAdd,
358
+ editTask,
359
+ // Export handlers if needed directly (EventBus preferred)
360
+ // handleTaskFormSubmit,
361
+ };
public/js/users.js ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // users.js
2
+ import * as userManager from './managers/userManager.js';
3
+ import * as departmentManager from './managers/departmentManager.js'; // Use * as
4
+ import { showMessage, showModal, hideModal, showConfirm } from './modal.js';
5
+ import { handleError } from './utils/errorHandler.js';
6
+ import { toggleButtonLoading, generateAvatarUrl } from './utils/utils.js';
7
+ import { populateUserDropdown, populateDepartmentDropdown } from './utils/dropdownHelpers.js'; // Ensure this is imported
8
+ import eventBus from './utils/eventBus.js';
9
+
10
+ // Helper function to display users (triggered by usersDataReady event)
11
+ async function displayUsers(users) { // Accepts users data as argument
12
+ console.trace('displayUsers called');
13
+ if (!usersList) {
14
+ console.error('displayUsers: usersList element not found');
15
+ return;
16
+ }
17
+
18
+
19
+ try {
20
+ // departmentsMap is now passed as an argument
21
+ if (users.length === 0 || !departmentManager.getDepartmentsData()) {
22
+ console.log('displayUsers: No users found');
23
+ usersList.innerHTML = '<li class="py-4 text-center text-gray-500">Chưa có người dùng nào.</li>';
24
+ const totalUsersEl = document.getElementById('totalUsers');
25
+ if (totalUsersEl) totalUsersEl.textContent = 0;
26
+ return;
27
+ }
28
+
29
+ // Clear the existing list before appending
30
+ usersList.innerHTML = ''; // Clear after check
31
+ users.forEach(user => {
32
+ const li = document.createElement('li');
33
+ li.className = 'py-4 flex items-center justify-between user-item'; // Added user-item class for potential future use
34
+ const department = departmentManager.getDepartmentsData().find(d => d.id === user.department);
35
+
36
+ li.innerHTML = `
37
+ <div><p class="text-sm font-medium text-gray-900">${user.name}</p><p class="text-sm text-gray-500">${user.email}</p></div>
38
+ <div class="text-sm text-gray-900">${department ? department.name : ''}</div>
39
+ <div class="text-sm text-gray-900">${user.role || ''}</div>
40
+ <div class="flex gap-2 admin-only"><button class="edit-user-btn text-blue-600 hover:underline" data-id="${user.id}" data-name="${user.name}" data-email="${user.email}" data-role="${user.role}" data-department="${user.department || ''}">Sửa</button><button class="delete-user-btn text-red-600 hover:underline" data-id="${user.id}" data-name="${user.name}">Xoá</button></div>
41
+ `;
42
+ usersList.appendChild(li);
43
+ });
44
+ console.log('displayUsers: Cleared usersList'); // Add log here
45
+ console.log('displayUsers: Rendered', users.length, 'users'); // Add log here
46
+
47
+ } catch (error) {
48
+ console.error('displayUsers error:', error);
49
+ // Error message for display logic itself is shown here
50
+ }
51
+ }
52
+
53
+ // The loadUsers function is now primarily triggered by EventBus events
54
+ // It should load data via manager and then call displayUsers
55
+ async function loadUsers() {
56
+ console.trace('loadUsers called');
57
+ console.log('users.js: loadUsers called. Getting data from cache.');
58
+ // It should fetch the latest data (already cached in manager) and display it.
59
+ const users = userManager.getAllUsersFromCache(); // Call the getter via the userManager object
60
+ console.log('users.js: loadUsers got data from cache. Calling displayUsers with', users.length, 'users.');
61
+ displayUsers(users); // Call displayUsers with the fetched users data
62
+ }
63
+ /**
64
+ * Setup event listeners for user-related events from Event Bus
65
+ */
66
+ function setupUserEventListeners() {
67
+ console.log('Setting up user event listeners via EventBus');
68
+
69
+ // --- Listeners for User Data Loading/Updates ---
70
+ eventBus.subscribe('usersCacheLoaded', async () => { // This listener should trigger loadUsers which calls displayUsers
71
+ console.log('users.js: Received usersCacheLoaded event.')
72
+ console.log('EventBus: usersCacheLoaded event received in users.js. Loading users.');
73
+ await loadUsers(); // Call loadUsers to fetch cached data and display
74
+ });
75
+
76
+ // --- Listeners for User Create/Update/Delete Actions ---
77
+ eventBus.subscribe('userAdded', (newUser) => {
78
+ console.log('EventBus: userAdded event received in users.js', newUser);
79
+ showMessage('Thành công', `Đã thêm người dùng "${newUser.name}".`);
80
+ hideModal('addUserModal'); // Hide the add modal on success
81
+ // The 'usersDataReady' event (triggered by manager after add) will cause the list to reload
82
+ });
83
+
84
+ const addUserForm = document.getElementById('addUserForm');
85
+ const saveUserBtn = document.querySelector('#addUserForm button[type="submit"]'); // Assuming the save button is a submit button within the form
86
+ if (saveUserBtn) toggleButtonLoading(saveUserBtn, false);
87
+ if (addUserForm) addUserForm.reset();
88
+
89
+ eventBus.subscribe('userAddFailed', (error) => {
90
+ console.error('EventBus: userAddFailed event received in users.js', error);
91
+ handleError(error, error.message || 'Đã xảy ra lỗi khi thêm người dùng.');
92
+ });
93
+
94
+
95
+ eventBus.subscribe('userUpdated', (updatedUser) => {
96
+ console.log('EventBus: userUpdated event received in users.js', updatedUser);
97
+ showMessage('Thành công', `Đã cập nhật người dùng "${updatedUser.name}".`);
98
+ hideModal('editUserModal'); // Hide the edit modal on success
99
+ // The 'usersDataReady' event (triggered by manager after update) will cause the list to reload
100
+ });
101
+
102
+ eventBus.subscribe('userUpdateFailed', (error) => {
103
+ console.error('EventBus: userUpdateFailed event received in users.js', error);
104
+ handleError(error, error.message || 'Đã xảy ra lỗi khi cập nhật người dùng.');
105
+ });
106
+ eventBus.subscribe('userDeleted', (deletedUserId) => {
107
+ console.log('EventBus: userDeleted event received in users.js', deletedUserId);
108
+ showMessage('Thành công', 'Đã xoá người dùng.');
109
+ // showMessage is handled by the confirm dialog's callback in the click listener below
110
+ // The 'usersDataReady' event (triggered by manager after delete) will cause the list to reload
111
+ });
112
+
113
+ eventBus.subscribe('userDeleteFailed', (error) => {
114
+ console.error('EventBus: userDeleteFailed event received in users.js', error);
115
+ handleError(error, error.message || 'Đã xảy ra lỗi khi xóa người dùng.');
116
+ });
117
+
118
+ // Remove userSaveCompleted listener as it's redundant with userAdded/userUpdated
119
+ }
120
+
121
+ export function editUser(id, name, email, role, department) {
122
+ console.log('editUser function called with ID:', id, 'Name:', name, 'Email:', email, 'Role:', role, 'Department:', department); // Log all received parameters
123
+ const editUserIdEl = document.getElementById('editUserId');
124
+ if (editUserIdEl) editUserIdEl.value = id;
125
+
126
+ const editUserNameEl = document.getElementById('editUserName');
127
+ if (editUserNameEl) editUserNameEl.value = name;
128
+
129
+ const editUserEmailEl = document.getElementById('editUserEmail');
130
+ if (editUserEmailEl) {
131
+ editUserEmailEl.value = email;
132
+ editUserEmailEl.disabled = true; // Keep email disabled
133
+ }
134
+
135
+ const editUserRoleEl = document.getElementById('editUserRole');
136
+ if (editUserRoleEl) editUserRoleEl.value = role;
137
+
138
+ // populateDepartmentDropdown vẫn giữ nguyên
139
+ // Populate the user dropdown for the assignedTo field in edit task modal
140
+ populateDepartmentDropdown('editUserDepartment', department);
141
+ // Set the modal to 'edit' mode
142
+ document.getElementById('saveUserBtn').style.display = 'none';
143
+ document.getElementById('updateUserBtn').style.display = 'block';
144
+ document.getElementById('editUserModalLabel').textContent = 'Cập Nhật Người Dùng';
145
+ document.getElementById('editUserEmail').disabled = true; // Ensure email is disabled for edit
146
+ showModal('editUserModal');
147
+ }
148
+
149
+ /**
150
+ * Mở modal thêm người dùng (Simplified UI function)
151
+ */
152
+ export function addUser() {
153
+ const form = document.getElementById('addUserForm');
154
+ if (form) form.reset();
155
+ // populateDepartmentDropdown vẫn giữ nguyên vì thuộc về departments.js
156
+ populateDepartmentDropdown('addUserDepartment');
157
+ // Set the modal to 'add' mode
158
+ document.getElementById('saveUserBtn').style.display = 'block';
159
+ document.getElementById('updateUserBtn').style.display = 'none';
160
+ document.getElementById('addUserEmail').disabled = false; // Enable email for add
161
+ }
162
+
163
+
164
+ /**
165
+ * Handles the submission of the Add New User form.
166
+ */
167
+ async function handleAddNewUser(event) {
168
+ console.log('Handling new user form submission');
169
+ event.preventDefault();
170
+ console.log('addUserForm submitted');
171
+
172
+ const form = event.target;
173
+ const saveButton = event.submitter;
174
+ toggleButtonLoading(saveButton, true);
175
+
176
+ const name = form.userName.value.trim(); // Đảm bảo tên trường là userName
177
+ const email = form.addUserEmail.value.trim(); // Đảm bảo tên trường là addUserEmail
178
+ const password = form.addUserPassword.value; // Đảm bảo tên trường là addUserPassword
179
+ const department = form.addUserDepartment.value; // Đảm bảo tên trường là addUserDepartment
180
+
181
+ // Kiểm tra sự tồn tại của trường role trước khi truy cập value
182
+ const role = form.addUserRole ? form.addUserRole.value : '';
183
+
184
+ const userData = { email, password, name, role, department };
185
+ console.log('Form data:', userData);
186
+
187
+ await userManager.handleAddUserSubmit(userData); // Call manager function
188
+ }
189
+
190
+ // Call setupUserEventListeners to start listening for events when users.js is imported
191
+ setupUserEventListeners();
192
+
193
+ // Add event listener for the "Thêm người dùng mới" button
194
+ document.addEventListener('DOMContentLoaded', () => {
195
+ const openAddUserModalBtn = document.getElementById('openAddUserModal');
196
+ if (openAddUserModalBtn) {
197
+ openAddUserModalBtn.addEventListener('click', (e) => {
198
+ e.preventDefault();
199
+ addUser(); // Call the addUser function to open the modal
200
+ showModal('addUserModal'); // Show the modal
201
+ });
202
+ }
203
+ });
204
+
205
+ // Add event listener for the Add User form submission
206
+ document.addEventListener('DOMContentLoaded', () => {
207
+ const addUserForm = document.getElementById('addUserForm');
208
+ if (addUserForm) {
209
+ addUserForm.addEventListener('submit', handleAddNewUser);
210
+ }
211
+ });
212
+
213
+ // Add event listener for the Edit User form submission - Reconstructed to fix syntax
214
+ document.addEventListener('DOMContentLoaded', () => {
215
+ const editUserForm = document.getElementById('editUserForm');
216
+ if (editUserForm) {
217
+ editUserForm.addEventListener('submit', async (event) => {
218
+ event.preventDefault();
219
+ console.log('editUserForm submitted');
220
+ const form = event.target;
221
+ const userId = form.editUserId.value;
222
+ const name = form.editUserName.value;
223
+ const email = form.editUserEmail.value;
224
+ const role = form.editUserRole.value;
225
+ const department = form.editUserDepartment.value;
226
+ const updatedUserData = { name, email, role, department };
227
+ console.log(`Attempting to update user ${userId} with data:`, updatedUserData);
228
+ try {
229
+ await userManager.handleEditUserSubmit(userId, updatedUserData);
230
+ // Optional: show success message or hide modal here if needed
231
+ // Example: showMessage('Thành công', 'Đã cập nhật người dùng.');
232
+ // Example: hideModal('editUserModal');
233
+ } catch (error) {
234
+ console.error('Error submitting edit user form:', error);
235
+ handleError(error, 'Đã xảy ra lỗi khi cập nhật người dùng.');
236
+ }
237
+ });
238
+ }
239
+
240
+ // Add event listener for the delete buttons using event delegation
241
+ const usersListEl = document.getElementById('usersList');
242
+ if (usersListEl) {
243
+ usersListEl.addEventListener('click', async (event) => {
244
+ const deleteButton = event.target.closest('.delete-user-btn');
245
+ const editButton = event.target.closest('.edit-user-btn');
246
+
247
+ if (editButton) {
248
+ event.preventDefault();
249
+ const userId = editButton.dataset.id;
250
+ const name = editButton.dataset.name;
251
+ const email = editButton.dataset.email;
252
+ const role = editButton.dataset.role;
253
+ const department = editButton.dataset.department;
254
+ console.log(`Edit button clicked for user ID: ${userId}`);
255
+ editUser(userId, name, email, role, department); // Call the editUser function
256
+ }
257
+
258
+ if (deleteButton) {
259
+ const userId = deleteButton.dataset.id;
260
+ const userName = deleteButton.dataset.name;
261
+ console.log(`Delete button clicked for user ID: ${userId}, Name: ${userName}`);
262
+
263
+ // Show confirmation modal (this is UI logic)
264
+ showConfirm('Xác nhận xoá', `Bạn có chắc chắn muốn xoá người dùng "${userName}" này không?`)
265
+ .then(async (confirmed) => { // Use .then() to handle the promise
266
+ console.log('users.js: showConfirm resolved with:', confirmed); // Added log here
267
+ if (confirmed) {
268
+ console.log('users.js: Inside confirmed block.'); // Added log here
269
+ console.log(`User confirmed deletion for ID: ${userId}`);
270
+ console.log('users.js: Calling userManager.handleDeleteUserClick for ID:', userId); // Added log
271
+ await userManager.handleDeleteUserClick(userId); // Call the manager function after confirmation
272
+ // Success message is now handled by the userDeleted event listener
273
+ } else {
274
+ console.log(`User cancelled deletion for ID: ${userId}`);
275
+ }
276
+ }).catch(error => { // Add .catch() to log errors
277
+ console.error('Error during confirmation:', error);
278
+ });
279
+ }
280
+ });
281
+ }
282
+ });
public/js/utils/dropdownHelpers.js ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // public/js/utils/dropdownHelpers.js
2
+
3
+ import * as departmentManager from '../managers/departmentManager.js';
4
+ import * as userManager from '../managers/userManager.js';
5
+ import { getDepartmentsData } from '../managers/departmentManager.js';
6
+ import { getAllUsersFromCache } from '../managers/userManager.js';
7
+
8
+ /**
9
+ * Hàm để đổ dữ liệu phòng ban vào các dropdown
10
+ */
11
+ export async function populateDepartmentDropdown(dropdownId, selectedUid = '') {
12
+ console.log('populateDepartmentDropdown called for ID:', dropdownId);
13
+ const dropdown = document.getElementById(dropdownId);
14
+ if (!dropdown) {
15
+ console.error(`Department dropdown element with ID "${dropdownId}" not found.`);
16
+ return;
17
+ }
18
+ const currentValue = dropdown.value; // Store current value to try and re-select after repopulating
19
+ dropdown.innerHTML = '<option value="">-- Chọn Phòng ban --</option>'; // Default option
20
+
21
+ try {
22
+ // Lấy dữ liệu phòng ban từ manager (đã được cache)
23
+ console.log('populateDepartmentDropdown: Calling getDepartmentsData from departmentManager');
24
+ const departments = departmentManager.getDepartmentsData(); // Use manager to get cached data
25
+
26
+ console.log('populateDepartmentDropdown: Fetched', departments.length, 'departments for dropdown.');
27
+
28
+ departments.forEach(department => {
29
+ console.log('populateDepartmentDropdown: department option value:', department.id, 'name:', department.name);
30
+ const option = document.createElement('option');
31
+ option.value = department.id;
32
+ option.textContent = department.name;
33
+ // Prioritize the originally selectedUid passed to the function
34
+ // If not provided, try to select the previously selected value from the dropdown
35
+ if (selectedUid && department.id === selectedUid) {
36
+ option.selected = true;
37
+ } else if (!selectedUid && department.id === currentValue) {
38
+ option.selected = true;
39
+ }
40
+ dropdown.appendChild(option);
41
+ });
42
+ console.log('populateDepartmentDropdown: Dropdown populated.');
43
+ } catch (err) {
44
+ console.error('populateDepartmentDropdown error:', err);
45
+ }
46
+ }
47
+
48
+
49
+ /**
50
+ * Hàm để đổ dữ liệu người dùng vào các dropdown
51
+ */
52
+ export async function populateUserDropdown(dropdownId, selectedUid = '') {
53
+ const dropdown = document.getElementById(dropdownId);
54
+ if (!dropdown) {
55
+ console.error(`User dropdown element with ID "${dropdownId}" not found.`);
56
+ return;
57
+ }
58
+ const currentValue = dropdown.value; // Store current value
59
+ dropdown.innerHTML = '<option value="">-- Chọn Người dùng --</option>'; // Default option
60
+
61
+ try {
62
+ console.log('populateUserDropdown: Calling getUsersData from userManager');
63
+ // Lấy dữ liệu người dùng từ manager (đã được cache)
64
+ const users = userManager.getAllUsersFromCache(); // Use manager to get cached data
65
+
66
+ console.log('populateUserDropdown: Fetched', users.length, 'users for dropdown.');
67
+
68
+
69
+ users.forEach(user => {
70
+ const option = document.createElement('option');
71
+ option.value = user.id;
72
+ option.textContent = user.name;
73
+ // Prioritize the originally selectedUid passed to the function
74
+ // If not provided, try to select the previously selected value from the dropdown
75
+ if (selectedUid && user.id === selectedUid) {
76
+ option.selected = true;
77
+ } else if (!selectedUid && user.id === currentValue) {
78
+ option.selected = true;
79
+ }
80
+ dropdown.appendChild(option);
81
+ });
82
+ console.log('populateUserDropdown: Dropdown populated');
83
+ } catch (err) {
84
+ console.error('populateUserDropdown error:', err);
85
+ // Note: This error will likely be caught by the caller, but logging here is also fine.
86
+ }
87
+ }
88
+
89
+
90
+ // Optional: Helper to populate multiple known department dropdowns
91
+ export function populateAllDepartmentDropdowns() {
92
+ console.log('Populating all department dropdowns.');
93
+ // List all IDs of dropdowns that need department data
94
+ const dropdownIds = ['addUserDepartment', 'editUserDepartment', 'taskDepartment']; // Add other IDs if necessary
95
+ dropdownIds.forEach(dropdownId => {
96
+ // We don't have the selected value easily here, so it will default to the first option.
97
+ // If preserving the selected value is critical, the component managing that dropdown
98
+ // would need to re-select based on its own state after this repopulation happens.
99
+ populateDepartmentDropdown(dropdownId);
100
+ });
101
+ }
102
+ // Optional: Helper to populate multiple known user dropdowns
103
+ export function populateAllUserDropdowns() {
104
+ console.log('Populating all user dropdowns.');
105
+ // List all IDs of dropdowns that need user data
106
+ const dropdownIds = ['taskAssignedTo', 'commentUser', 'otherUserDropdownId']; // Add other IDs if necessary
107
+ dropdownIds.forEach(dropdownId => {
108
+ populateUserDropdown(dropdownId);
109
+ });
110
+ }
public/js/utils/errorHandler.js ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // public/js/utils/errorHandler.js
2
+
3
+ import { showMessage } from '../modal.js';
4
+
5
+ /**
6
+ * Handles errors by logging to the console and showing a user-friendly message.
7
+ * @param {Error} error - The error object.
8
+ * @param {string} defaultMessage - The default message to display if the error object has no message.
9
+ */
10
+ export function handleError(error, defaultMessage = 'Đã xảy ra lỗi.') {
11
+ console.error('An error occurred:', error);
12
+ const userMessage = error && error.message ? error.message : defaultMessage;
13
+ showMessage('Lỗi', userMessage);
14
+ }
public/js/utils/eventBus.js ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // public/js/eventBus.js
2
+
3
+ const subscribers = {};
4
+
5
+ function subscribe(eventName, callback) {
6
+ if (!subscribers[eventName]) {
7
+ subscribers[eventName] = [];
8
+ }
9
+ subscribers[eventName].push(callback);
10
+ return () => { // Keep unsubscribe function
11
+ subscribers[eventName] = subscribers[eventName].filter(cb => cb !== callback);
12
+ if (subscribers[eventName].length === 0) {
13
+ delete subscribers[eventName];
14
+ }
15
+ };
16
+ }
17
+
18
+ function publish(eventName, data) {
19
+ if (subscribers[eventName]) {
20
+ subscribers[eventName].forEach(callback => {
21
+ try {
22
+ callback(data);
23
+ } catch (error) {
24
+
25
+ console.error(`Error handling event "${eventName}":`, error);
26
+ }
27
+ });
28
+ }
29
+ }
30
+
31
+ const eventBus = { publish, subscribe };
32
+ export default eventBus;
public/js/utils/utils.js ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // script-modularized/utils.js
2
+
3
+
4
+
5
+ export function generateAvatarUrl(name, background = '3b82f6', color = 'fff') {
6
+ return `https://ui-avatars.com/api/?name=${encodeURIComponent(name || '')}&background=${background}&color=${color}`;
7
+ }
8
+
9
+ export function escapeHtml(text) {
10
+ if (!text) return '';
11
+ return text.replace(/[&<>"]/g, c => {
12
+ return {
13
+ '&': '&amp;',
14
+ '<': '&lt;',
15
+ '>': '&gt;',
16
+ '"': '&quot;'
17
+ }[c];
18
+ });
19
+ }
20
+
21
+ export function escapeJs(text) {
22
+ if (!text) return '';
23
+ return text
24
+ .replace(/\\/g, '\\\\')
25
+ .replace(/`/g, '\\`')
26
+ .replace(/\$/g, '\\$')
27
+ .replace(/'/g, "\\'")
28
+ .replace(/\"/g, '\\"');
29
+ }
30
+
31
+ export function formatDate(timestamp) {
32
+ return timestamp?.toDate ? moment(timestamp.toDate()).format('HH:mm DD/MM/YYYY') : '';
33
+ }
34
+
35
+ export function toggleButtonLoading(buttonId, isLoading) {
36
+ const button = document.getElementById(buttonId);
37
+ if (!button) return;
38
+
39
+ const defaultText = button.getAttribute('data-original-text') || button.innerText;
40
+ if (!button.hasAttribute('data-original-text')) {
41
+ button.setAttribute('data-original-text', defaultText);
42
+ }
43
+
44
+ if (isLoading) {
45
+ button.disabled = true;
46
+ button.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Đang xử lý...';
47
+ button.classList.add('opacity-50', 'cursor-not-allowed');
48
+ } else {
49
+ button.disabled = false;
50
+ button.innerHTML = defaultText;
51
+ button.classList.remove('opacity-50', 'cursor-not-allowed');
52
+ }
53
+ }
public/style.css ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Custom styles for better scrollbar and modal backdrop */
2
+ /* Desktop layout */
3
+ @media (min-width: 768px) {
4
+ body {
5
+ display: flex; /* Use flexbox for desktop layout */
6
+ }
7
+ }
8
+ body {
9
+ height: 100vh;
10
+ position: relative;
11
+ background-color: #f3f4f6; /* Light gray background */
12
+ }
13
+ /* Ẩn tất cả section */
14
+ .content-section {
15
+ display: none;
16
+ }
17
+ /* Chỉ show section được gắn .active */
18
+ .content-section.active,
19
+ .content-section:target {
20
+ display: block;
21
+ }
22
+ .modal {
23
+ display: none; /* Hidden by default */
24
+ position: fixed; /* Stay in place */
25
+ z-index: 1000; /* Sit on top */
26
+ left: 0;
27
+ top: 0;
28
+ width: 100%; /* Full width */
29
+ height: 100%; /* Full height */
30
+ overflow: auto; /* Enable scroll if needed */
31
+ background-color: rgba(0,0,0,0.5); /* Black w/ opacity */
32
+ justify-content: center; /* Center content horizontally */
33
+ align-items: center; /* Center content vertically */
34
+ padding: 1rem; /* Add some padding for smaller screens */
35
+ }
36
+ #confirmModal {
37
+ z-index: 1050; /* Ensure confirm modal is always on top of other modals */
38
+ }
39
+ .modal-content {
40
+ background-color: #fefefe;
41
+ margin: auto;
42
+ padding: 2rem;
43
+ border-radius: 0.5rem;
44
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
45
+ width: 90%; /* Responsive width */
46
+ max-width: 500px; /* Max width for larger screens */
47
+ }
48
+ @media (min-width: 768px) {
49
+ .modal-content {
50
+ width: 50%;
51
+ }
52
+ }
53
+ /* Custom scrollbar for kanban columns */
54
+ .kanban-column {
55
+ max-height: calc(100vh - 250px); /* Adjust based on header/footer height */
56
+ overflow-y: auto;
57
+ -ms-overflow-style: none; /* IE and Edge */
58
+ scrollbar-width: none; /* Firefox */
59
+ }
60
+ .kanban-column::-webkit-scrollbar {
61
+ display: none; /* Chrome, Safari, Opera*/
62
+ }
63
+ /* Active sidebar item styling (from original HTML) */
64
+ .sidebar-item.active {
65
+ background-color: rgba(255, 255, 255, 0.2); /* Light background for active item */
66
+ color: white; /* White text for active item */
67
+ font-weight: 600; /* Semi-bold for active item */
68
+ }
69
+ .sidebar-item.active i {
70
+ color: white; /* White icon for active item */
71
+ }
72
+ /* Dropdown styling for top right menu (from original HTML) */
73
+ .dropdown-menu {
74
+ display: none;
75
+ }
76
+ .dropdown:hover .dropdown-menu {
77
+ display: block;
78
+ }
79
+ .admin-only {
80
+ display: none; /* Đảm bảo các phần tử admin-only bị ẩn ban đầu */
81
+ }
82
+
83
+ /* Custom styles for responsive sidebar */
84
+ /* Styles for screens smaller than md (max-width: 767px) */
85
+ @media (max-width: 767px) {
86
+ #sidebar {
87
+ position: fixed;
88
+ left: 0;
89
+ display: block !important; /* Ensure the sidebar is displayed on small screens */
90
+ top: 0;
91
+ z-index: 50; /* Ensure sidebar is above the overlay */
92
+ height: 100vh;
93
+ flex-shrink: 0; /* Prevent the sidebar from shrinking in a flex container */
94
+ transform: translateX(-100%);
95
+ transition: transform 0.3s ease-in-out;
96
+ }
97
+ /* Show sidebar when sidebar-open class is on the body */
98
+ body.sidebar-open #sidebar {
99
+ transform: translateX(0);
100
+ }
101
+ /* Show and dim overlay when sidebar-open class is on the body */
102
+ body.sidebar-open #sidebar-overlay {
103
+ opacity: 0.5;
104
+ /* display: block; */ /* Tailwind's `hidden` utility handles this based on breakpoint */
105
+ display: block; /* Show the overlay */
106
+ pointer-events: auto; /* Enable pointer events on overlay when sidebar is open */
107
+ }
108
+ #main-content {
109
+ overflow: visible; /* Allow content to overflow when sidebar is fixed */
110
+ }
111
+
112
+ }
113
+ /* Styles for screens md and larger (min-width: 768px) */
114
+ @media (min-width: 768px) {
115
+ #sidebar {
116
+ width: 256px; /* Fixed width for sidebar on desktop */
117
+ }
118
+ #main-content {
119
+ flex-grow: 1; /* Allow main content to take the remaining space */
120
+ overflow-y: auto; /* Enable scrolling for main content */
121
+ }
122
+ }
src/App.jsx ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // src/App.jsx
2
+ import React, { useState, useEffect } from 'react';
3
+ import Sidebar from './components/Sidebar/Sidebar';
4
+ // import { auth, db, FieldValue } from './firebase-config.js'; // Không import trực tiếp auth, db nữa
5
+ import { generateAvatarUrl } from './utils';
6
+ import eventBus from '../public/js/utils/eventBus.js'; // Import eventBus
7
+ import { showModal, hideModal } from '../public/js/modal.js'; // Import modal functions
8
+
9
+ function App() {
10
+ const [currentUser, setCurrentUser] = useState(null);
11
+ const [loadingAuth, setLoadingAuth] = useState(true);
12
+ const [departments, setDepartments] = useState([]);
13
+ const [loadingDepartments, setLoadingDepartments] = useState(true);
14
+
15
+ const getInitialSection = () => {
16
+ const hash = window.location.hash.substring(1);
17
+ const validSections = ['dashboard-section', 'tasks-section', 'personnel-section', 'departments-section', 'reports-section', 'calendar-section', 'settings-section'];
18
+ return validSections.includes(hash) ? hash : 'dashboard-section';
19
+ };
20
+
21
+ const [activeSection, setActiveSection] = useState(getInitialSection());
22
+ const [isMobileMenuOpen, setMobileMenuOpen] = useState(false);
23
+
24
+ // Listen for auth state changes from vanilla JS (via userManager and listeners.js)
25
+ useEffect(() => {
26
+ const unsubscribeUser = eventBus.subscribe('userDataUpdated', (userData) => {
27
+ console.log('App.jsx: Received userDataUpdated event from EventBus:', userData);
28
+ setCurrentUser(userData);
29
+ setLoadingAuth(false);
30
+ // If user is null, show login modal
31
+ if (!userData) {
32
+ showModal('loginModal');
33
+ // Ensure vanilla JS also handles hiding main content etc.
34
+ // Or: React handles it completely by not rendering main UI
35
+ } else {
36
+ hideModal('loginModal');
37
+ }
38
+ });
39
+
40
+ const unsubscribeDepartments = eventBus.subscribe('departmentsDataReady', (deptsData) => {
41
+ console.log('App.jsx: Received departmentsDataReady event from EventBus:', deptsData);
42
+ setDepartments(deptsData);
43
+ setLoadingDepartments(false);
44
+ });
45
+
46
+ // Set up reactBridge functions for vanilla JS to call
47
+ window.reactBridge = {
48
+ updateActiveSection: (sectionId) => {
49
+ console.log("App.jsx: reactBridge.updateActiveSection called with", sectionId);
50
+ setActiveSection(sectionId);
51
+ },
52
+ setMobileMenuOpen: (isOpen) => {
53
+ console.log("App.jsx: reactBridge.setMobileMenuOpen called with", isOpen);
54
+ setMobileMenuOpen(isOpen);
55
+ }
56
+ // userData and departmentData are now passed via EventBus, not directly via reactBridge
57
+ };
58
+ console.log("App.jsx: Bridge functions attached to window.reactBridge");
59
+
60
+
61
+ return () => {
62
+ unsubscribeUser();
63
+ unsubscribeDepartments();
64
+ delete window.reactBridge; // Clean up bridge on unmount
65
+ };
66
+ }, []);
67
+
68
+ // Sync active section with URL hash
69
+ useEffect(() => {
70
+ if (currentUser) {
71
+ window.location.hash = activeSection;
72
+ }
73
+ }, [activeSection, currentUser]);
74
+
75
+ // Handle hashchange from browser (back/forward)
76
+ useEffect(() => {
77
+ const handleHashChange = () => {
78
+ const hash = window.location.hash.substring(1);
79
+ if (hash && hash !== activeSection) {
80
+ setActiveSection(hash);
81
+ }
82
+ };
83
+ window.addEventListener('hashchange', handleHashChange);
84
+ return () => {
85
+ window.removeEventListener('hashchange', handleHashChange);
86
+ };
87
+ }, [activeSection]);
88
+
89
+
90
+ const handleNavigate = (sectionId) => {
91
+ setActiveSection(sectionId);
92
+ // This will trigger the useEffect above to update window.location.hash
93
+ // And vanillaNavigate (in listeners.js) will listen to hashchange
94
+ if (window.innerWidth < 768) { // Close sidebar on mobile after navigation
95
+ setMobileMenuOpen(false);
96
+ }
97
+ };
98
+
99
+ const handleLogout = () => {
100
+ // This will be handled by the vanilla JS auth.js via the logout button in the header
101
+ // userManager.js will then publish 'userLoggedOut' which App.jsx will listen to.
102
+ // You can optionally add a direct call to auth.signOut() here if you want React to initiate logout.
103
+ // For now, let's keep the vanilla JS button as the initiator.
104
+ console.log("App.jsx: Logout button clicked, vanilla JS should handle this.");
105
+ };
106
+
107
+
108
+ // Render logic
109
+ if (loadingAuth || (currentUser && loadingDepartments)) {
110
+ return (
111
+ <div className="flex-1 flex items-center justify-center h-screen">
112
+ <p>Đang tải ứng dụng...</p>
113
+ </div>
114
+ );
115
+ }
116
+
117
+ if (!currentUser) {
118
+ // When not logged in, only render the login modal (which is part of vanilla HTML)
119
+ // The main content area (main-content) will be display:none by vanilla JS.
120
+ return (
121
+ <div className="flex-1 flex flex-col items-center justify-center p-6">
122
+ {/* Content hidden or login form shown by vanilla JS */}
123
+ {/* <h1 className="text-2xl font-bold mb-4">Vui lòng đăng nhập</h1> */}
124
+ {/* The login modal is rendered by vanilla JS, so no React component for it here */}
125
+ </div>
126
+ );
127
+ }
128
+
129
+ return (
130
+ <div className="flex h-screen">
131
+ <Sidebar
132
+ initialUser={currentUser}
133
+ initialDepartments={departments}
134
+ initialActiveSection={activeSection}
135
+ onNavigate={handleNavigate}
136
+ isMobileMenuOpen={isMobileMenuOpen}
137
+ setMobileMenuOpen={setMobileMenuOpen}
138
+ />
139
+
140
+ {/* Main content area, controlled by vanilla JS for now, but React header can be here */}
141
+ <div className="flex-1 flex flex-col overflow-hidden">
142
+ {/* Header is part of HTML, but React can manage its logout button etc. */}
143
+ {/* For now, the logout button in HTML is handled by vanilla JS */}
144
+ <header className="bg-white shadow h-16 flex items-center justify-between px-6 md:hidden">
145
+ {/* The mobile menu button for small screens */}
146
+ <button
147
+ className="text-gray-500 focus:outline-none"
148
+ onClick={() => setMobileMenuOpen(true)}
149
+ >
150
+ <i className="fas fa-bars text-xl"></i>
151
+ </button>
152
+ <div className="flex-1"></div>
153
+ <div>
154
+ {/* This logout button is part of HTML, handled by vanilla JS */}
155
+ {/* <button
156
+ onClick={handleLogout}
157
+ className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded"
158
+ >
159
+ Đăng xuất
160
+ </button> */}
161
+ </div>
162
+ </header>
163
+
164
+ <main className="flex-1 overflow-x-hidden overflow-y-auto bg-gray-100">
165
+ {/* Nội dung chính sẽ được quản lý bởi JavaScript thuần thông qua class 'active' */}
166
+ {/* Chúng ta chỉ cần đảm bảo các phần tử content-section tồn tại trong index.html */}
167
+ {/* Và JavaScript thuần (listeners.js) sẽ thêm/bỏ class 'active' */}
168
+ {/* Không cần render lại nội dung ở đây bằng React trừ khi chuyển hoàn toàn section đó sang React */}
169
+ {/* Ví dụ, bạn có thể render:
170
+ {activeSection === 'dashboard-section' && <DashboardComponent />}
171
+ nhưng điều đó sẽ là bước tiếp theo của việc chuyển đổi từng section */}
172
+ </main>
173
+ </div>
174
+ </div>
175
+ );
176
+ }
177
+
178
+ export default App;
src/components/Sidebar/DepartmentQuickAccess.jsx ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // src/components/Sidebar/DepartmentQuickAccess.jsx
2
+ import React from 'react';
3
+
4
+ const DepartmentQuickAccess = ({ departments, onNavigateToDepartment }) => {
5
+ return (
6
+ <div className="mt-auto mb-4 flex-shrink-0">
7
+ <p className="px-4 text-xs font-semibold text-blue-200 uppercase tracking-wider mb-2">
8
+ Phòng ban
9
+ </p>
10
+ <div id="department-quick-access" className="space-y-1 break-words">
11
+ {departments && departments.length > 0 ? ( // Kiểm tra departments có tồn tại và có phần tử không
12
+ departments.map((dept) => (
13
+ <a
14
+ key={dept.id}
15
+ href={`#departments-section?deptId=${dept.id}`}
16
+ className="block px-4 py-1 text-xs text-blue-100 hover:text-white hover:bg-blue-700 hover:bg-opacity-20 rounded"
17
+ onClick={(e) => {
18
+ e.preventDefault();
19
+ if (onNavigateToDepartment) {
20
+ onNavigateToDepartment(dept.id);
21
+ }
22
+ }}
23
+ >
24
+ {dept.name}
25
+ </a>
26
+ ))
27
+ ) : (
28
+ <p className="px-4 text-xs text-blue-200">Đang tải...</p> // Hoặc "Không có phòng ban."
29
+ )}
30
+ </div>
31
+ </div>
32
+ );
33
+ };
34
+
35
+ export default DepartmentQuickAccess;
src/components/Sidebar/NavLinks.jsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // src/components/Sidebar/NavLinks.jsx
2
+ import React from 'react';
3
+
4
+ const NavLinks = ({ navItems, activeSection, onNavigate }) => {
5
+ return (
6
+ <nav className="flex-1 space-y-2">
7
+ {navItems.map((item) => (
8
+ <a
9
+ key={item.id}
10
+ href={`#${item.id}`} // Sẽ thay đổi khi dùng React Router
11
+ className={`sidebar-item flex items-center px-4 py-3 text-sm font-medium rounded-lg ${
12
+ activeSection === item.id
13
+ ? 'bg-blue-700 bg-opacity-50 text-white font-semibold' // Lớp active mạnh hơn chút
14
+ : 'text-blue-100 hover:bg-blue-700 hover:bg-opacity-30'
15
+ }`}
16
+ onClick={(e) => {
17
+ e.preventDefault(); // Ngăn hành vi mặc định của thẻ a
18
+ onNavigate(item.id);
19
+ }}
20
+ data-target={item.id} // Giữ lại data-target nếu cần cho các logic khác tạm thời
21
+ >
22
+ <i className={`fas ${item.icon} mr-3 ${activeSection === item.id ? 'text-white' : 'text-blue-200'}`}></i>
23
+ {item.label}
24
+ </a>
25
+ ))}
26
+ </nav>
27
+ );
28
+ };
29
+
30
+ export default NavLinks;
src/components/Sidebar/Sidebar.jsx ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // src/components/Sidebar/Sidebar.jsx
2
+ import React, { useState, useEffect } from 'react';
3
+ import UserInfo from './UserInfo';
4
+ import NavLinks from './NavLinks';
5
+ import DepartmentQuickAccess from './DepartmentQuickAccess';
6
+
7
+ const Sidebar = ({
8
+ initialUser,
9
+ initialDepartments, // Prop này sẽ nhận danh sách phòng ban từ App.jsx
10
+ initialActiveSection,
11
+ onNavigate,
12
+ isMobileMenuOpen,
13
+ setMobileMenuOpen,
14
+ }) => {
15
+ // ... (navItems và các state khác giữ nguyên) ...
16
+ const navItems = [
17
+ { id: 'dashboard-section', label: 'Dashboard', icon: 'fa-tachometer-alt' },
18
+ { id: 'tasks-section', label: 'Công việc', icon: 'fa-tasks' },
19
+ { id: 'personnel-section', label: 'Nhân sự', icon: 'fa-users' },
20
+ { id: 'departments-section', label: 'Phòng ban', icon: 'fa-building' },
21
+ { id: 'reports-section', label: 'Báo cáo', icon: 'fa-chart-line' },
22
+ { id: 'calendar-section', label: 'Lịch làm việc', icon: 'fa-calendar-alt' },
23
+ { id: 'settings-section', label: 'Cài đặt', icon: 'fa-cog' },
24
+ ];
25
+
26
+ const [currentUser, setCurrentUser] = useState(initialUser);
27
+ const [departments, setDepartments] = useState(initialDepartments); // State này sẽ được cập nhật
28
+ const [activeSection, setActiveSection] = useState(initialActiveSection);
29
+
30
+ useEffect(() => {
31
+ setCurrentUser(initialUser);
32
+ }, [initialUser]);
33
+
34
+ useEffect(() => {
35
+ setDepartments(initialDepartments); // Cập nhật state departments khi prop thay đổi
36
+ }, [initialDepartments]);
37
+
38
+ useEffect(() => {
39
+ setActiveSection(initialActiveSection);
40
+ }, [initialActiveSection]);
41
+
42
+ const handleNavigation = (sectionId) => {
43
+ // ... (giữ nguyên) ...
44
+ setActiveSection(sectionId);
45
+ if (onNavigate) {
46
+ onNavigate(sectionId);
47
+ }
48
+ if (window.innerWidth < 768) { // md breakpoint
49
+ setMobileMenuOpen(false);
50
+ }
51
+ };
52
+
53
+ const sidebarClasses = `
54
+ md:flex md:flex-shrink-0 fixed md:relative inset-y-0 left-0 z-50
55
+ transform ${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full'} md:translate-x-0
56
+ transition-transform duration-300 ease-in-out
57
+ `;
58
+
59
+ return (
60
+ <>
61
+ {isMobileMenuOpen && (
62
+ <div
63
+ className="fixed inset-0 bg-black opacity-50 z-40 md:hidden"
64
+ onClick={() => setMobileMenuOpen(false)}
65
+ ></div>
66
+ )}
67
+ <div id="sidebar" className={sidebarClasses}>
68
+ <div className="flex flex-col w-64 bg-gradient-to-b from-blue-600 to-blue-800 h-full">
69
+ <div className="flex items-center justify-between h-16 px-4 flex-shrink-0">
70
+ <div className="flex items-center">
71
+ <i className="fas fa-tooth text-white text-2xl mr-2"></i>
72
+ <span className="text-white font-bold text-xl">Nha Khoa TTL</span>
73
+ </div>
74
+ <button
75
+ id="close-sidebar-button"
76
+ className="md:hidden text-white ml-auto focus:outline-none"
77
+ onClick={() => setMobileMenuOpen(false)}
78
+ >
79
+ <i className="fas fa-times text-xl"></i>
80
+ </button>
81
+ </div>
82
+ <div className="flex flex-col flex-grow px-4 py-4 overflow-y-auto">
83
+ <UserInfo currentUser={currentUser} />
84
+ <NavLinks
85
+ navItems={navItems}
86
+ activeSection={activeSection}
87
+ onNavigate={handleNavigation}
88
+ />
89
+ {/* DepartmentQuickAccess sẽ nhận `departments` từ state của Sidebar */}
90
+ <DepartmentQuickAccess
91
+ departments={departments}
92
+ // onNavigateToDepartment={ (deptId) => handleNavigation(`departments-section?deptId=${deptId}`)}
93
+ />
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </>
98
+ );
99
+ };
100
+
101
+ export default Sidebar;
src/components/Sidebar/UserInfo.jsx ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // src/components/Sidebar/UserInfo.jsx
2
+ import React from 'react';
3
+
4
+ const UserInfo = ({ currentUser }) => {
5
+ // Sử dụng ảnh mặc định nếu currentUser không có hoặc không có avatar
6
+ const avatarUrl = currentUser?.avatar || `https://ui-avatars.com/api/?name=${currentUser?.name || 'User'}&background=ffffff&color=3b82f6`;
7
+ const userName = currentUser?.name || 'Đang tải...';
8
+ const userRole = currentUser?.role || 'Đang tải...';
9
+ const userId = currentUser?.id || ''; // Giả sử currentUser có id
10
+
11
+ return (
12
+ <div className="flex items-center px-4 py-3 mb-6 bg-blue-700 bg-opacity-30 rounded-lg">
13
+ <img
14
+ id="user-avatar"
15
+ className="w-10 h-10 rounded-full"
16
+ src={avatarUrl}
17
+ alt="User Avatar"
18
+ />
19
+ <div className="ml-3 flex-shrink-0">
20
+ <p id="user-name" className="text-sm font-medium text-white bold">
21
+ {userName}
22
+ </p>
23
+ <p id="user-role" className="text-xs text-blue-100 break-words">
24
+ {userRole}
25
+ </p>
26
+ {userId && (
27
+ <p
28
+ id="user-id"
29
+ className="text-xs text-blue-100 break-words"
30
+ style={{ wordBreak: 'break-all' }}
31
+ >
32
+ {/* Có thể bạn không muốn hiển thị User ID ở đây, tùy theo thiết kế */}
33
+ {/* ID: {userId} */}
34
+ </p>
35
+ )}
36
+ </div>
37
+ </div>
38
+ );
39
+ };
40
+
41
+ export default UserInfo;
src/firebase-config.js ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // src/firebase-config.js
2
+ import firebase from 'firebase/compat/app'; // Sử dụng compat nếu bạn vẫn đang dùng API cũ
3
+ import 'firebase/compat/auth';
4
+ import 'firebase/compat/firestore';
5
+ // Nếu dùng Firebase v9+ modular SDK, cách import sẽ khác:
6
+ // import { initializeApp } from "firebase/app";
7
+ // import { getAuth } from "firebase/auth";
8
+ // import { getFirestore } from "firebase/firestore";
9
+
10
+ const firebaseConfig = {
11
+ apiKey: "AIzaSyAh3e5wBNfeQX5EO9DALjEQGXH9OrH3bUA",
12
+ authDomain: "qlnb-web-app.firebaseapp.com",
13
+ databaseURL: "https://qlnb-web-app-default-rtdb.asia-southeast1.firebasedatabase.app", // Có thể không cần nếu chỉ dùng Firestore
14
+ projectId: "qlnb-web-app",
15
+ storageBucket: "qlnb-web-app.firebasestorage.app",
16
+ messagingSenderId: "871217970406",
17
+ appId: "1:871217970406:web:6b3482a4efeaa869bccf27",
18
+ measurementId: "G-FSGHYLQNSJ"
19
+ };
20
+
21
+ // Khởi tạo Firebase
22
+ if (!firebase.apps.length) {
23
+ firebase.initializeApp(firebaseConfig);
24
+ } else {
25
+ firebase.app(); // if already initialized, use that one
26
+ }
27
+
28
+ const auth = firebase.auth();
29
+ const db = firebase.firestore();
30
+ const FieldValue = firebase.firestore.FieldValue; // Export FieldValue
31
+
32
+ export { auth, db, FieldValue, firebase }; // Export firebase nếu cần truy cập các dịch vụ khác
33
+
34
+ // Đối với Firebase v9+ modular SDK:
35
+ // const app = initializeApp(firebaseConfig);
36
+ // const auth = getAuth(app);
37
+ // const db = getFirestore(app);
38
+ // export { auth, db };
src/main.jsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // src/main.jsx
2
+ import React from 'react';
3
+ import ReactDOM from 'react-dom/client';
4
+ // Giả sử bạn muốn SidebarWrapper là điểm bắt đầu cho phần React này
5
+ // import SidebarWrapper from './SidebarWrapper'; // Hoặc import App from './App.jsx' nếu App là wrapper
6
+ import App from './App.jsx'; // <--- Thay đổi ở đây
7
+
8
+ // Import CSS toàn cục nếu có, ví dụ nếu bạn dùng Tailwind theo cách import vào JS
9
+ // import './index.css';
10
+
11
+ const reactRootElement = document.getElementById('react-sidebar-root'); // Hoặc 'root' nếu bạn đổi tên
12
+
13
+ if (reactRootElement) {
14
+ ReactDOM.createRoot(reactRootElement).render(
15
+ <React.StrictMode>
16
+ {/* Nếu App.jsx là wrapper quản lý Sidebar và các phần khác */}
17
+ <App /> {/* <--- Thay đổi ở đây */}
18
+ {/* Nếu SidebarWrapper là component bạn muốn render trực tiếp vào div này */}
19
+ {/* <SidebarWrapper /> */}
20
+ </React.StrictMode>
21
+ );
22
+ } else {
23
+ console.error('React root element (#react-sidebar-root or #root) not found in HTML.');
24
+ }
src/utils.js ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // script-modularized/utils.js
2
+
3
+
4
+
5
+ export function generateAvatarUrl(name, background = '3b82f6', color = 'fff') {
6
+ return `https://ui-avatars.com/api/?name=${encodeURIComponent(name || '')}&background=${background}&color=${color}`;
7
+ }
8
+
9
+ export function escapeHtml(text) {
10
+ if (!text) return '';
11
+ return text.replace(/[&<>"]/g, c => {
12
+ return {
13
+ '&': '&amp;',
14
+ '<': '&lt;',
15
+ '>': '&gt;',
16
+ '"': '&quot;'
17
+ }[c];
18
+ });
19
+ }
20
+
21
+ export function escapeJs(text) {
22
+ if (!text) return '';
23
+ return text
24
+ .replace(/\\/g, '\\\\')
25
+ .replace(/`/g, '\\`')
26
+ .replace(/\$/g, '\\$')
27
+ .replace(/'/g, "\\'")
28
+ .replace(/\"/g, '\\"');
29
+ }
30
+
31
+ export function formatDate(timestamp) {
32
+ return timestamp?.toDate ? moment(timestamp.toDate()).format('HH:mm DD/MM/YYYY') : '';
33
+ }
34
+
35
+ export function toggleButtonLoading(buttonId, isLoading) {
36
+ const button = document.getElementById(buttonId);
37
+ if (!button) return;
38
+
39
+ const defaultText = button.getAttribute('data-original-text') || button.innerText;
40
+ if (!button.hasAttribute('data-original-text')) {
41
+ button.setAttribute('data-original-text', defaultText);
42
+ }
43
+
44
+ if (isLoading) {
45
+ button.disabled = true;
46
+ button.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Đang xử lý...';
47
+ button.classList.add('opacity-50', 'cursor-not-allowed');
48
+ } else {
49
+ button.disabled = false;
50
+ button.innerHTML = defaultText;
51
+ button.classList.remove('opacity-50', 'cursor-not-allowed');
52
+ }
53
+ }
tsconfig.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "experimentalDecorators": true,
5
+ "useDefineForClassFields": false,
6
+ "module": "ESNext",
7
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "isolatedModules": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+ "allowJs": true,
17
+ "jsx": "react-jsx",
18
+
19
+ /* Linting */
20
+ "strict": true,
21
+ "noUnusedLocals": true,
22
+ "noUnusedParameters": true,
23
+ "noFallthroughCasesInSwitch": true,
24
+ "noUncheckedSideEffectImports": true,
25
+
26
+ "paths": {
27
+ "@/*" : ["./*"]
28
+ }
29
+ }
30
+ }
31
+
vite.config.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ // https://vite.dev/config/
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ })