Spaces:
Running
Running
Upload 37 files
Browse files- index.html +835 -0
- package-lock.json +1718 -0
- package.json +26 -0
- public/js/auth.js +112 -0
- public/js/comments.js +689 -0
- public/js/dashboard.js +145 -0
- public/js/departments.js +177 -0
- public/js/firebase-init.js +54 -0
- public/js/index.js +99 -0
- public/js/kanban.js +389 -0
- public/js/listeners.js +195 -0
- public/js/main-loader.js +53 -0
- public/js/managers/departmentManager.js +166 -0
- public/js/managers/taskManager.js +545 -0
- public/js/managers/userManager.js +276 -0
- public/js/modal.js +58 -0
- public/js/services/commentService.js +71 -0
- public/js/services/departmentService.js +137 -0
- public/js/services/taskService.js +259 -0
- public/js/services/userService.js +125 -0
- public/js/tasks.js +361 -0
- public/js/users.js +282 -0
- public/js/utils/dropdownHelpers.js +110 -0
- public/js/utils/errorHandler.js +14 -0
- public/js/utils/eventBus.js +32 -0
- public/js/utils/utils.js +53 -0
- public/style.css +122 -0
- src/App.jsx +178 -0
- src/components/Sidebar/DepartmentQuickAccess.jsx +35 -0
- src/components/Sidebar/NavLinks.jsx +30 -0
- src/components/Sidebar/Sidebar.jsx +101 -0
- src/components/Sidebar/UserInfo.jsx +41 -0
- src/firebase-config.js +38 -0
- src/main.jsx +24 -0
- src/utils.js +53 -0
- tsconfig.json +31 -0
- vite.config.js +7 -0
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 |
+
'&': '&',
|
| 14 |
+
'<': '<',
|
| 15 |
+
'>': '>',
|
| 16 |
+
'"': '"'
|
| 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 |
+
'&': '&',
|
| 14 |
+
'<': '<',
|
| 15 |
+
'>': '>',
|
| 16 |
+
'"': '"'
|
| 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 |
+
})
|