Merge pull request 'update delete log, add confirm device' (#2) from vi into main
Reviewed-on: #2
This commit is contained in:
		
						commit
						306d878c9e
					
				| 
						 | 
					@ -422,6 +422,7 @@ mark {
 | 
				
			||||||
	display: flex;
 | 
						display: flex;
 | 
				
			||||||
	justify-content: flex-end;
 | 
						justify-content: flex-end;
 | 
				
			||||||
	gap: 10px;
 | 
						gap: 10px;
 | 
				
			||||||
 | 
						margin-top: 1rem;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@keyframes fadeIn {
 | 
					@keyframes fadeIn {
 | 
				
			||||||
| 
						 | 
					@ -435,26 +436,6 @@ mark {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.btn-with-badge {
 | 
					 | 
				
			||||||
	position: relative;
 | 
					 | 
				
			||||||
	padding: 8px 14px !important;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.btn-with-badge .badge {
 | 
					 | 
				
			||||||
	position: absolute;
 | 
					 | 
				
			||||||
	top: -6px;
 | 
					 | 
				
			||||||
	right: -10px;
 | 
					 | 
				
			||||||
	background-color: #fff;
 | 
					 | 
				
			||||||
	color: #dc3545;
 | 
					 | 
				
			||||||
	font-size: 12px;
 | 
					 | 
				
			||||||
	font-weight: bold;
 | 
					 | 
				
			||||||
	padding: 3px 6px;
 | 
					 | 
				
			||||||
	border-radius: 999px;
 | 
					 | 
				
			||||||
	border: 1px solid #dc3545;
 | 
					 | 
				
			||||||
	min-width: 20px;
 | 
					 | 
				
			||||||
	text-align: center;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.user-dropdown {
 | 
					.user-dropdown {
 | 
				
			||||||
	position: relative;
 | 
						position: relative;
 | 
				
			||||||
	display: inline-block;
 | 
						display: inline-block;
 | 
				
			||||||
| 
						 | 
					@ -500,6 +481,15 @@ mark {
 | 
				
			||||||
	background-color: #e9ecef;
 | 
						background-color: #e9ecef;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.user-dropdown-content .item {
 | 
				
			||||||
 | 
						width: 100%;
 | 
				
			||||||
 | 
						padding: 10px 16px;
 | 
				
			||||||
 | 
						border: none;
 | 
				
			||||||
 | 
						background-color: #f8f9fa;
 | 
				
			||||||
 | 
						text-align: left;
 | 
				
			||||||
 | 
						font-size: 14px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.user-dropdown:hover .user-dropdown-content {
 | 
					.user-dropdown:hover .user-dropdown-content {
 | 
				
			||||||
	display: block;
 | 
						display: block;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -522,3 +512,9 @@ mark {
 | 
				
			||||||
	opacity: 1;
 | 
						opacity: 1;
 | 
				
			||||||
	bottom: 40px;
 | 
						bottom: 40px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media only screen and (max-width: 768px) {
 | 
				
			||||||
 | 
						.modal-content {
 | 
				
			||||||
 | 
							max-width: 90% !important;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -38,6 +38,8 @@
 | 
				
			||||||
					></button>
 | 
										></button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					<div class="user-dropdown-content">
 | 
										<div class="user-dropdown-content">
 | 
				
			||||||
 | 
											<div id="confirm-count-user" class="item"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
						<button onclick="logout()">Logout</button>
 | 
											<button onclick="logout()">Logout</button>
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
| 
						 | 
					@ -60,14 +62,21 @@
 | 
				
			||||||
					</button>
 | 
										</button>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				<button
 | 
									<div>
 | 
				
			||||||
					id="logConfirmDeleteBtn"
 | 
										<button
 | 
				
			||||||
					type="button"
 | 
											id="deviceConfirmBtn"
 | 
				
			||||||
					class="btn-with-badge danger-btn"
 | 
											type="button"
 | 
				
			||||||
				>
 | 
											class="primary-btn"
 | 
				
			||||||
					Delete
 | 
										>
 | 
				
			||||||
					<span class="badge" id="logDeleteBadge">0</span>
 | 
											Confirm
 | 
				
			||||||
				</button>
 | 
										</button>
 | 
				
			||||||
 | 
										<span
 | 
				
			||||||
 | 
											id="confirmUserText"
 | 
				
			||||||
 | 
											style="font-size: 12px; display: none"
 | 
				
			||||||
 | 
										>
 | 
				
			||||||
 | 
											Confirmed by <strong id="confirmedUserName"></strong>
 | 
				
			||||||
 | 
										</span>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			<div id="errorTableContainer" class="command-section">
 | 
								<div id="errorTableContainer" class="command-section">
 | 
				
			||||||
| 
						 | 
					@ -85,54 +94,76 @@
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<div id="logSections"></div>
 | 
								<div id="logSections"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			<!-- Log Delete Modal -->
 | 
					 | 
				
			||||||
			<div id="logDeleteModal" class="modal hidden">
 | 
								<div id="logDeleteModal" class="modal hidden">
 | 
				
			||||||
				<div class="modal-content">
 | 
									<div style="max-width: 30%" class="modal-content">
 | 
				
			||||||
					<h2 class="modal-title">Confirm Deletion</h2>
 | 
										<div id="deleteLogText"></div>
 | 
				
			||||||
 | 
					 | 
				
			||||||
					<table class="delete-table">
 | 
					 | 
				
			||||||
						<thead>
 | 
					 | 
				
			||||||
							<tr>
 | 
					 | 
				
			||||||
								<th>ID</th>
 | 
					 | 
				
			||||||
								<th>PID</th>
 | 
					 | 
				
			||||||
								<th>Version</th>
 | 
					 | 
				
			||||||
								<th>Command</th>
 | 
					 | 
				
			||||||
								<th style="text-align: center">Action</th>
 | 
					 | 
				
			||||||
							</tr>
 | 
					 | 
				
			||||||
						</thead>
 | 
					 | 
				
			||||||
						<tbody id="logDeleteListContainer"></tbody>
 | 
					 | 
				
			||||||
					</table>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					<div class="modal-actions">
 | 
										<div class="modal-actions">
 | 
				
			||||||
						<button id="logCloseModalBtn" class="btn secondary-btn">
 | 
											<button id="logCancelDeleteBtn" class="secondary-btn">
 | 
				
			||||||
							Cancel
 | 
												Cancel
 | 
				
			||||||
						</button>
 | 
											</button>
 | 
				
			||||||
						<button id="logSubmitDeleteBtn" class="btn danger-btn">
 | 
											<button id="logDeleteBtn" class="danger-btn">
 | 
				
			||||||
							Delete
 | 
												Delete
 | 
				
			||||||
						</button>
 | 
											</button>
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								<div id="deviceConfirmModal" class="modal hidden">
 | 
				
			||||||
 | 
									<div style="max-width: 30%" class="modal-content">
 | 
				
			||||||
 | 
										<div id="deviceConfirmText"></div>
 | 
				
			||||||
 | 
										<div class="modal-actions">
 | 
				
			||||||
 | 
											<button id="deviceCancelBtn" class="secondary-btn">
 | 
				
			||||||
 | 
												Cancel
 | 
				
			||||||
 | 
											</button>
 | 
				
			||||||
 | 
											<button id="submitDeviceConfirmBtn" class="danger-btn">
 | 
				
			||||||
 | 
												Confirm
 | 
				
			||||||
 | 
											</button>
 | 
				
			||||||
 | 
										</div>
 | 
				
			||||||
 | 
									</div>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<!-- Authentication and get user info -->
 | 
				
			||||||
		<script>
 | 
							<script>
 | 
				
			||||||
			if (!localStorage.getItem("token")) {
 | 
								if (!localStorage.getItem("token")) {
 | 
				
			||||||
				logout();
 | 
									logout();
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			document.getElementById(
 | 
					 | 
				
			||||||
				"welcome-user",
 | 
					 | 
				
			||||||
			).innerHTML = `Welcome, <strong>${
 | 
					 | 
				
			||||||
				JSON.parse(localStorage.getItem("user")).name
 | 
					 | 
				
			||||||
			}</strong> ▼`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			function logout() {
 | 
								function logout() {
 | 
				
			||||||
				localStorage.removeItem("token");
 | 
									localStorage.removeItem("token");
 | 
				
			||||||
				localStorage.removeItem("user");
 | 
									localStorage.removeItem("user");
 | 
				
			||||||
				window.location.href = "/login";
 | 
									window.location.href = "/login";
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								async function getProfile() {
 | 
				
			||||||
 | 
									const token = localStorage.getItem("token");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									await fetch("/api/profile", {
 | 
				
			||||||
 | 
										method: "GET",
 | 
				
			||||||
 | 
										headers: {
 | 
				
			||||||
 | 
											"Content-Type": "application/json",
 | 
				
			||||||
 | 
											Authorization: `Bearer ${token}`,
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
										.then((res) => res.json())
 | 
				
			||||||
 | 
										.then((data) => {
 | 
				
			||||||
 | 
											document.getElementById(
 | 
				
			||||||
 | 
												"welcome-user",
 | 
				
			||||||
 | 
											).innerHTML = `Welcome, <strong>${data?.user?.name}</strong> ▼`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											document.getElementById(
 | 
				
			||||||
 | 
												"confirm-count-user",
 | 
				
			||||||
 | 
											).innerHTML = `Confirmed: <strong>${data?.user?.confirm_count}</strong>`;
 | 
				
			||||||
 | 
										})
 | 
				
			||||||
 | 
										.catch((err) => {
 | 
				
			||||||
 | 
											console.error(err);
 | 
				
			||||||
 | 
										});
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								getProfile();
 | 
				
			||||||
		</script>
 | 
							</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<!-- Get PID - Version - Log -->
 | 
				
			||||||
		<script>
 | 
							<script>
 | 
				
			||||||
			// Get param pid, version from URL
 | 
								// Get param pid, version from URL
 | 
				
			||||||
			const params = new URLSearchParams(window.location.search);
 | 
								const params = new URLSearchParams(window.location.search);
 | 
				
			||||||
| 
						 | 
					@ -146,7 +177,12 @@
 | 
				
			||||||
			const logSections = document.getElementById("logSections");
 | 
								const logSections = document.getElementById("logSections");
 | 
				
			||||||
			const commands = ["inventory", "version", "license", "logging"];
 | 
								const commands = ["inventory", "version", "license", "logging"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const confirmUserText = document.getElementById("confirmUserText");
 | 
				
			||||||
 | 
								const confirmedUserName =
 | 
				
			||||||
 | 
									document.getElementById("confirmedUserName");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const logsCache = {}; // Store logs and current index for each command
 | 
								const logsCache = {}; // Store logs and current index for each command
 | 
				
			||||||
 | 
								let versionsUser = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			async function loadPIDs(pidParam, versionParam) {
 | 
								async function loadPIDs(pidParam, versionParam) {
 | 
				
			||||||
				const res = await fetch("/api/pids");
 | 
									const res = await fetch("/api/pids");
 | 
				
			||||||
| 
						 | 
					@ -181,6 +217,15 @@
 | 
				
			||||||
				);
 | 
									);
 | 
				
			||||||
				const versions = await res.json();
 | 
									const versions = await res.json();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									versionsUser = versions
 | 
				
			||||||
 | 
										.filter((verItem) => verItem.user)
 | 
				
			||||||
 | 
										.map((verItem) => {
 | 
				
			||||||
 | 
											return {
 | 
				
			||||||
 | 
												version: verItem.version,
 | 
				
			||||||
 | 
												user: verItem.user,
 | 
				
			||||||
 | 
											};
 | 
				
			||||||
 | 
										});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				pidList.innerHTML = `<option value="${pid}">${pid}</option>`;
 | 
									pidList.innerHTML = `<option value="${pid}">${pid}</option>`;
 | 
				
			||||||
				versionList.innerHTML = versions
 | 
									versionList.innerHTML = versions
 | 
				
			||||||
					.map((v) => {
 | 
										.map((v) => {
 | 
				
			||||||
| 
						 | 
					@ -206,7 +251,7 @@
 | 
				
			||||||
					versionList.dispatchEvent(new Event("change"));
 | 
										versionList.dispatchEvent(new Event("change"));
 | 
				
			||||||
					currentParams.set("version", versionParam);
 | 
										currentParams.set("version", versionParam);
 | 
				
			||||||
				} else {
 | 
									} else {
 | 
				
			||||||
					// ✅ Default chọn version đầu tiên
 | 
										// Default chọn version đầu tiên
 | 
				
			||||||
					if (versions.length > 0) {
 | 
										if (versions.length > 0) {
 | 
				
			||||||
						versionList.value = versions[0].version;
 | 
											versionList.value = versions[0].version;
 | 
				
			||||||
						versionList.dispatchEvent(new Event("change"));
 | 
											versionList.dispatchEvent(new Event("change"));
 | 
				
			||||||
| 
						 | 
					@ -266,6 +311,20 @@
 | 
				
			||||||
						`?${currentParams.toString()}`,
 | 
											`?${currentParams.toString()}`,
 | 
				
			||||||
					);
 | 
										);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										// Render Confirm Section
 | 
				
			||||||
 | 
										const currentVerUser = versionsUser.find(
 | 
				
			||||||
 | 
											(verItem) => verItem.version === version,
 | 
				
			||||||
 | 
										);
 | 
				
			||||||
 | 
										if (currentVerUser) {
 | 
				
			||||||
 | 
											deviceConfirmBtn.style.display = "none";
 | 
				
			||||||
 | 
											confirmedUserName.textContent =
 | 
				
			||||||
 | 
												currentVerUser.user.name;
 | 
				
			||||||
 | 
											confirmUserText.style.display = "inline";
 | 
				
			||||||
 | 
										} else {
 | 
				
			||||||
 | 
											deviceConfirmBtn.style.display = "inline-block";
 | 
				
			||||||
 | 
											confirmUserText.style.display = "none";
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					loadLogs(pid, version);
 | 
										loadLogs(pid, version);
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
| 
						 | 
					@ -435,36 +494,11 @@ ${license}`.trim();
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		</script>
 | 
							</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<!-- Handle delete log -->
 | 
				
			||||||
		<script>
 | 
							<script>
 | 
				
			||||||
			const STORAGE_KEY = "logDeleteList";
 | 
								let pendingDeleteLog = null;
 | 
				
			||||||
			const logConfirmDeleteBtn = document.getElementById(
 | 
					 | 
				
			||||||
				"logConfirmDeleteBtn",
 | 
					 | 
				
			||||||
			);
 | 
					 | 
				
			||||||
			const logDeleteModal = document.getElementById("logDeleteModal");
 | 
					 | 
				
			||||||
			const logDeleteListContainer = document.getElementById(
 | 
					 | 
				
			||||||
				"logDeleteListContainer",
 | 
					 | 
				
			||||||
			);
 | 
					 | 
				
			||||||
			const logCloseModalBtn =
 | 
					 | 
				
			||||||
				document.getElementById("logCloseModalBtn");
 | 
					 | 
				
			||||||
			const logSubmitDeleteBtn =
 | 
					 | 
				
			||||||
				document.getElementById("logSubmitDeleteBtn");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			function getDeleteList() {
 | 
								document.addEventListener("click", async (e) => {
 | 
				
			||||||
				return JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			function saveDeleteList(list) {
 | 
					 | 
				
			||||||
				localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
 | 
					 | 
				
			||||||
				updateDeleteBtn();
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			function updateDeleteBtn() {
 | 
					 | 
				
			||||||
				const deleteList = getDeleteList();
 | 
					 | 
				
			||||||
				const badge = document.getElementById("logDeleteBadge");
 | 
					 | 
				
			||||||
				badge.textContent = deleteList.length;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			document.addEventListener("click", (e) => {
 | 
					 | 
				
			||||||
				if (e.target.classList.contains("trash-btn")) {
 | 
									if (e.target.classList.contains("trash-btn")) {
 | 
				
			||||||
					const section = e.target.closest(".command-section");
 | 
										const section = e.target.closest(".command-section");
 | 
				
			||||||
					const pid = pidList.value;
 | 
										const pid = pidList.value;
 | 
				
			||||||
| 
						 | 
					@ -474,103 +508,58 @@ ${license}`.trim();
 | 
				
			||||||
					const log =
 | 
										const log =
 | 
				
			||||||
						logsCache[`${pid}_${version}`][command].logs[index];
 | 
											logsCache[`${pid}_${version}`][command].logs[index];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					const currentList = getDeleteList();
 | 
										pendingDeleteLog = { id: log.id, command, pid, version };
 | 
				
			||||||
					if (!currentList.some((item) => item.id === log.id)) {
 | 
					 | 
				
			||||||
						currentList.push({
 | 
					 | 
				
			||||||
							id: log.id,
 | 
					 | 
				
			||||||
							pid,
 | 
					 | 
				
			||||||
							version,
 | 
					 | 
				
			||||||
							command,
 | 
					 | 
				
			||||||
						});
 | 
					 | 
				
			||||||
						saveDeleteList(currentList);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
						showToast("Added to delete list");
 | 
										const text = `Are you sure you want to delete this log? <br /><code>${pid} - ${version} - ${command}</code> <br /> <div style="font-size: 12px; font-weight: 700" >${log.filename}</div>`;
 | 
				
			||||||
					}
 | 
										document.getElementById("deleteLogText").innerHTML = text;
 | 
				
			||||||
 | 
										showDeleteModal();
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			function renderDeleteModal() {
 | 
								const logDeleteModal = document.getElementById("logDeleteModal");
 | 
				
			||||||
				const deleteList = getDeleteList();
 | 
								const logCancelDeleteBtn =
 | 
				
			||||||
				logDeleteListContainer.innerHTML = "";
 | 
									document.getElementById("logCancelDeleteBtn");
 | 
				
			||||||
				deleteList.forEach((item, i) => {
 | 
								const logDeleteBtn = document.getElementById("logDeleteBtn");
 | 
				
			||||||
					const tr = document.createElement("tr");
 | 
					
 | 
				
			||||||
					tr.innerHTML = `
 | 
								function showDeleteModal() {
 | 
				
			||||||
				<td>${item.id}</td>
 | 
									logDeleteModal.classList.remove("hidden");
 | 
				
			||||||
				<td>${item.pid}</td>
 | 
								}
 | 
				
			||||||
				<td>${item.version}</td>
 | 
								function hideDeleteModal() {
 | 
				
			||||||
				<td>${item.command}</td>
 | 
									logDeleteModal.classList.add("hidden");
 | 
				
			||||||
				<td style="text-align: center;"><button type="button" class="danger-btn remove-item-btn" data-index="${i}" title="Remove">🗑</button></td>
 | 
									pendingDeleteLog = null;
 | 
				
			||||||
			`;
 | 
					 | 
				
			||||||
					logDeleteListContainer.appendChild(tr);
 | 
					 | 
				
			||||||
				});
 | 
					 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			logDeleteListContainer.addEventListener("click", (e) => {
 | 
								logCancelDeleteBtn.addEventListener("click", hideDeleteModal);
 | 
				
			||||||
				if (e.target.classList.contains("remove-item-btn")) {
 | 
					 | 
				
			||||||
					const index = parseInt(e.target.getAttribute("data-index"));
 | 
					 | 
				
			||||||
					const list = getDeleteList();
 | 
					 | 
				
			||||||
					list.splice(index, 1);
 | 
					 | 
				
			||||||
					saveDeleteList(list);
 | 
					 | 
				
			||||||
					renderDeleteModal();
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// Open delete modal
 | 
								logDeleteBtn.addEventListener("click", async () => {
 | 
				
			||||||
			logConfirmDeleteBtn.addEventListener("click", () => {
 | 
									if (!pendingDeleteLog) return;
 | 
				
			||||||
				renderDeleteModal();
 | 
					 | 
				
			||||||
				logDeleteModal.classList.remove("hidden");
 | 
					 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// Open close modal
 | 
									const token = localStorage.getItem("token");
 | 
				
			||||||
			logCloseModalBtn.addEventListener("click", () => {
 | 
									try {
 | 
				
			||||||
				logDeleteModal.classList.add("hidden");
 | 
										const res = await fetch("/api/delete-log", {
 | 
				
			||||||
			});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Submit delete
 | 
					 | 
				
			||||||
			logSubmitDeleteBtn.addEventListener("click", async () => {
 | 
					 | 
				
			||||||
				const deleteList = getDeleteList();
 | 
					 | 
				
			||||||
				if (deleteList.length === 0) {
 | 
					 | 
				
			||||||
					alert("Please select at least one item to delete.");
 | 
					 | 
				
			||||||
					return;
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				if (
 | 
					 | 
				
			||||||
					confirm(
 | 
					 | 
				
			||||||
						`Are you sure you want to delete ${deleteList.length} item(s)?`,
 | 
					 | 
				
			||||||
					)
 | 
					 | 
				
			||||||
				) {
 | 
					 | 
				
			||||||
					const deletedItems = deleteList.map((item) => {
 | 
					 | 
				
			||||||
						return {
 | 
					 | 
				
			||||||
							id: item.id,
 | 
					 | 
				
			||||||
							command: item.command,
 | 
					 | 
				
			||||||
						};
 | 
					 | 
				
			||||||
					});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					const token = localStorage.getItem("token");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
					await fetch("/api/delete-logs", {
 | 
					 | 
				
			||||||
						method: "POST",
 | 
											method: "POST",
 | 
				
			||||||
						headers: {
 | 
											headers: {
 | 
				
			||||||
							"Content-Type": "application/json",
 | 
												"Content-Type": "application/json",
 | 
				
			||||||
							Authorization: `Bearer ${token}`,
 | 
												Authorization: `Bearer ${token}`,
 | 
				
			||||||
						},
 | 
											},
 | 
				
			||||||
						body: JSON.stringify({ items: deletedItems }),
 | 
											body: JSON.stringify(pendingDeleteLog),
 | 
				
			||||||
					})
 | 
										});
 | 
				
			||||||
						.then((res) => res.json())
 | 
										const data = await res.json();
 | 
				
			||||||
						.then((data) => {
 | 
										if (data.success) {
 | 
				
			||||||
							alert("Deleted successfully!");
 | 
											alert("Deleted successfully!");
 | 
				
			||||||
							localStorage.removeItem(STORAGE_KEY);
 | 
											location.reload();
 | 
				
			||||||
							location.reload();
 | 
										} else {
 | 
				
			||||||
						})
 | 
											alert("Delete failed.");
 | 
				
			||||||
						.catch((err) => {
 | 
										}
 | 
				
			||||||
							console.error(err);
 | 
									} catch (err) {
 | 
				
			||||||
							alert("Delete failed.");
 | 
										console.error(err);
 | 
				
			||||||
						});
 | 
										alert("Delete failed.");
 | 
				
			||||||
 | 
									} finally {
 | 
				
			||||||
 | 
										hideDeleteModal();
 | 
				
			||||||
 | 
										pendingDeleteLog = null;
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			updateDeleteBtn();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			function showToast(message = "Announce something") {
 | 
								function showToast(message = "Announce something") {
 | 
				
			||||||
				const toast = document.getElementById("logToast");
 | 
									const toast = document.getElementById("logToast");
 | 
				
			||||||
				toast.textContent = message;
 | 
									toast.textContent = message;
 | 
				
			||||||
| 
						 | 
					@ -583,5 +572,74 @@ ${license}`.trim();
 | 
				
			||||||
				}, 2000);
 | 
									}, 2000);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		</script>
 | 
							</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<!-- Handle confirm device -->
 | 
				
			||||||
 | 
							<script>
 | 
				
			||||||
 | 
								const deviceConfirmBtn =
 | 
				
			||||||
 | 
									document.getElementById("deviceConfirmBtn");
 | 
				
			||||||
 | 
								const deviceConfirmModal =
 | 
				
			||||||
 | 
									document.getElementById("deviceConfirmModal");
 | 
				
			||||||
 | 
								const deviceCancelBtn = document.getElementById("deviceCancelBtn");
 | 
				
			||||||
 | 
								const submitDeviceConfirmBtn = document.getElementById(
 | 
				
			||||||
 | 
									"submitDeviceConfirmBtn",
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
								const deviceConfirmText =
 | 
				
			||||||
 | 
									document.getElementById("deviceConfirmText");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								let pendingDevice = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								deviceConfirmBtn.addEventListener("click", () => {
 | 
				
			||||||
 | 
									const pid = document.getElementById("pidList").value;
 | 
				
			||||||
 | 
									const version = document.getElementById("versionList").value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									if (!pid || !version) {
 | 
				
			||||||
 | 
										alert("PID or version is missing.");
 | 
				
			||||||
 | 
										return;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									pendingDevice = { pid, version };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									deviceConfirmText.innerHTML = `
 | 
				
			||||||
 | 
								Are you sure you want to confirm this device?<br />
 | 
				
			||||||
 | 
								<strong>${pid} - ${version}</strong>
 | 
				
			||||||
 | 
							`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									deviceConfirmModal.classList.remove("hidden");
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								deviceCancelBtn.addEventListener("click", () => {
 | 
				
			||||||
 | 
									deviceConfirmModal.classList.add("hidden");
 | 
				
			||||||
 | 
									pendingDevice = null;
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								submitDeviceConfirmBtn.addEventListener("click", async () => {
 | 
				
			||||||
 | 
									if (!pendingDevice) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									const token = localStorage.getItem("token");
 | 
				
			||||||
 | 
									try {
 | 
				
			||||||
 | 
										const res = await fetch("/api/confirm-device", {
 | 
				
			||||||
 | 
											method: "POST",
 | 
				
			||||||
 | 
											headers: {
 | 
				
			||||||
 | 
												"Content-Type": "application/json",
 | 
				
			||||||
 | 
												Authorization: `Bearer ${token}`,
 | 
				
			||||||
 | 
											},
 | 
				
			||||||
 | 
											body: JSON.stringify(pendingDevice),
 | 
				
			||||||
 | 
										});
 | 
				
			||||||
 | 
										const data = await res.json();
 | 
				
			||||||
 | 
										if (data.success) {
 | 
				
			||||||
 | 
											alert(data.message || "Device confirmed successfully!");
 | 
				
			||||||
 | 
											location.reload();
 | 
				
			||||||
 | 
										} else {
 | 
				
			||||||
 | 
											alert(data.message || "Confirmation failed.");
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									} catch (err) {
 | 
				
			||||||
 | 
										console.error(err);
 | 
				
			||||||
 | 
										alert("Confirmation failed.");
 | 
				
			||||||
 | 
									} finally {
 | 
				
			||||||
 | 
										deviceConfirmModal.classList.add("hidden");
 | 
				
			||||||
 | 
										pendingDevice = null;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							</script>
 | 
				
			||||||
	</body>
 | 
						</body>
 | 
				
			||||||
</html>
 | 
					</html>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										22
									
								
								route/web.js
								
								
								
								
							
							
						
						
									
										22
									
								
								route/web.js
								
								
								
								
							| 
						 | 
					@ -2,6 +2,7 @@ const express = require("express");
 | 
				
			||||||
const path = require("path");
 | 
					const path = require("path");
 | 
				
			||||||
const bcrypt = require("bcrypt");
 | 
					const bcrypt = require("bcrypt");
 | 
				
			||||||
const { createToken } = require("../utils/jwt");
 | 
					const { createToken } = require("../utils/jwt");
 | 
				
			||||||
 | 
					const { authenticateToken } = require("../middleware/auth");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let router = express.Router();
 | 
					let router = express.Router();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -48,5 +49,26 @@ module.exports = (app, db) => {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						router.get("/api/profile", authenticateToken, async (req, res) => {
 | 
				
			||||||
 | 
							const userId = req.user?.id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								const [rows] = await db.query("SELECT * FROM users WHERE id = ?", [
 | 
				
			||||||
 | 
									userId,
 | 
				
			||||||
 | 
								]);
 | 
				
			||||||
 | 
								const user = rows[0];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return res.status(200).json({
 | 
				
			||||||
 | 
									user: {
 | 
				
			||||||
 | 
										name: user.name,
 | 
				
			||||||
 | 
										confirm_count: user.confirm_count,
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							} catch (err) {
 | 
				
			||||||
 | 
								console.error(err);
 | 
				
			||||||
 | 
								return res.status(500).json({ message: "Internal Server Error" });
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	app.use("/", router);
 | 
						app.use("/", router);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										144
									
								
								server.js
								
								
								
								
							
							
						
						
									
										144
									
								
								server.js
								
								
								
								
							| 
						 | 
					@ -90,9 +90,21 @@ const checkAndInsertAdminUser = async () => {
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (!(await columnExists("devices", "updated_by"))) {
 | 
						if (await columnExists("devices", "updated_by")) {
 | 
				
			||||||
 | 
							await db.query(`ALTER TABLE devices DROP COLUMN updated_by`);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!(await columnExists("devices", "updated_user"))) {
 | 
				
			||||||
 | 
							await db.query(`
 | 
				
			||||||
 | 
								ALTER TABLE devices 
 | 
				
			||||||
 | 
								ADD COLUMN updated_user INT DEFAULT NULL,
 | 
				
			||||||
 | 
								ADD CONSTRAINT fk_updated_by_user FOREIGN KEY (updated_user) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE
 | 
				
			||||||
 | 
							`);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!(await columnExists("devices", "is_confirmed"))) {
 | 
				
			||||||
		await db.query(
 | 
							await db.query(
 | 
				
			||||||
			`ALTER TABLE devices ADD COLUMN updated_by VARCHAR(100) DEFAULT NULL`,
 | 
								`ALTER TABLE devices ADD COLUMN is_confirmed BOOLEAN DEFAULT FALSE`,
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -144,7 +156,7 @@ app.get("/api/pid/:pid/versions", async (req, res) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Lấy danh sách các version tương ứng với PID
 | 
						// Lấy danh sách các version tương ứng với PID
 | 
				
			||||||
	const [devices] = await db.query(
 | 
						const [devices] = await db.query(
 | 
				
			||||||
		"SELECT id, version FROM devices WHERE pid = ?",
 | 
							"SELECT id, version, updated_user FROM devices WHERE pid = ?",
 | 
				
			||||||
		[pid],
 | 
							[pid],
 | 
				
			||||||
	);
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -153,7 +165,12 @@ app.get("/api/pid/:pid/versions", async (req, res) => {
 | 
				
			||||||
	for (const device of devices) {
 | 
						for (const device of devices) {
 | 
				
			||||||
		const deviceId = device.id;
 | 
							const deviceId = device.id;
 | 
				
			||||||
		const version = device.version;
 | 
							const version = device.version;
 | 
				
			||||||
 | 
							const userId = device.updated_user;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const [user] = await db.query(
 | 
				
			||||||
 | 
								"SELECT id, name FROM users WHERE id = ?",
 | 
				
			||||||
 | 
								[userId],
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
		const [[inv]] = await db.query(
 | 
							const [[inv]] = await db.query(
 | 
				
			||||||
			"SELECT COUNT(*) AS c FROM inventory_outputs WHERE device_id = ?",
 | 
								"SELECT COUNT(*) AS c FROM inventory_outputs WHERE device_id = ?",
 | 
				
			||||||
			[deviceId],
 | 
								[deviceId],
 | 
				
			||||||
| 
						 | 
					@ -172,6 +189,7 @@ app.get("/api/pid/:pid/versions", async (req, res) => {
 | 
				
			||||||
		);
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		results.push({
 | 
							results.push({
 | 
				
			||||||
 | 
								user: user[0],
 | 
				
			||||||
			version,
 | 
								version,
 | 
				
			||||||
			commands: {
 | 
								commands: {
 | 
				
			||||||
				inventory: inv.c,
 | 
									inventory: inv.c,
 | 
				
			||||||
| 
						 | 
					@ -202,47 +220,33 @@ app.get("/api/device/:pid/:version/:command", async (req, res) => {
 | 
				
			||||||
	res.json(logs);
 | 
						res.json(logs);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
app.post("/api/delete-logs", authenticateToken, async (req, res) => {
 | 
					app.post("/api/delete-log", authenticateToken, async (req, res) => {
 | 
				
			||||||
	const items = req.body.items;
 | 
						const logId = req.body.id;
 | 
				
			||||||
	const userId = req.user?.id;
 | 
						const logCommand = req.body.command;
 | 
				
			||||||
 | 
					 | 
				
			||||||
	if (!Array.isArray(items)) {
 | 
					 | 
				
			||||||
		return res.status(400).json({ message: "Invalid payload" });
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	try {
 | 
						try {
 | 
				
			||||||
		for (const item of items) {
 | 
							if (!logCommand || typeof logId !== "number") {
 | 
				
			||||||
			const { command, id } = item;
 | 
								return res
 | 
				
			||||||
 | 
									.status(400)
 | 
				
			||||||
			if (!command || typeof id !== "number") continue;
 | 
									.json({ success: false, message: "Invalid input" });
 | 
				
			||||||
 | 
					 | 
				
			||||||
			const allowedCommands = [
 | 
					 | 
				
			||||||
				"inventory",
 | 
					 | 
				
			||||||
				"version",
 | 
					 | 
				
			||||||
				"license",
 | 
					 | 
				
			||||||
				"logging",
 | 
					 | 
				
			||||||
			];
 | 
					 | 
				
			||||||
			if (!allowedCommands.includes(command)) continue;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			const [rows] = await db.query(
 | 
					 | 
				
			||||||
				`SELECT id FROM \`${command}_outputs\` WHERE id = ?`,
 | 
					 | 
				
			||||||
				[id],
 | 
					 | 
				
			||||||
			);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			if (rows.length === 0) {
 | 
					 | 
				
			||||||
				continue;
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// Delete log
 | 
					 | 
				
			||||||
			await db.query(`DELETE FROM \`${command}_outputs\` WHERE id = ?`, [
 | 
					 | 
				
			||||||
				id,
 | 
					 | 
				
			||||||
			]);
 | 
					 | 
				
			||||||
			await db.query(
 | 
					 | 
				
			||||||
				"UPDATE users SET confirm_count = confirm_count + 1 WHERE id = ?",
 | 
					 | 
				
			||||||
				[userId],
 | 
					 | 
				
			||||||
			);
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const [rows] = await db.query(
 | 
				
			||||||
 | 
								`SELECT id FROM \`${logCommand}_outputs\` WHERE id = ?`,
 | 
				
			||||||
 | 
								[logId],
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (rows.length === 0) {
 | 
				
			||||||
 | 
								return res.status(404).json({
 | 
				
			||||||
 | 
									success: false,
 | 
				
			||||||
 | 
									message: "Log not found",
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							await db.query(`DELETE FROM \`${logCommand}_outputs\` WHERE id = ?`, [
 | 
				
			||||||
 | 
								logId,
 | 
				
			||||||
 | 
							]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		res.json({ success: true });
 | 
							res.json({ success: true });
 | 
				
			||||||
	} catch (error) {
 | 
						} catch (error) {
 | 
				
			||||||
		console.error("Delete error:", error);
 | 
							console.error("Delete error:", error);
 | 
				
			||||||
| 
						 | 
					@ -250,6 +254,66 @@ app.post("/api/delete-logs", authenticateToken, async (req, res) => {
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					app.post("/api/confirm-device", authenticateToken, async (req, res) => {
 | 
				
			||||||
 | 
						const { pid, version } = req.body;
 | 
				
			||||||
 | 
						const updatedBy = req.user?.id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!pid || !version) {
 | 
				
			||||||
 | 
							return res.status(400).json({
 | 
				
			||||||
 | 
								success: false,
 | 
				
			||||||
 | 
								message: "Missing pid or version",
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						try {
 | 
				
			||||||
 | 
							// Check if device exists
 | 
				
			||||||
 | 
							const [rows] = await db.query(
 | 
				
			||||||
 | 
								`SELECT * FROM devices WHERE pid = ? AND version = ?`,
 | 
				
			||||||
 | 
								[pid, version],
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (rows.length === 0) {
 | 
				
			||||||
 | 
								return res.status(404).json({
 | 
				
			||||||
 | 
									success: false,
 | 
				
			||||||
 | 
									message: "Device not found.",
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (rows[0].is_confirmed) {
 | 
				
			||||||
 | 
								return res.status(400).json({
 | 
				
			||||||
 | 
									success: false,
 | 
				
			||||||
 | 
									message: "Device is already confirmed.",
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Update device confirmation
 | 
				
			||||||
 | 
							await db.query(
 | 
				
			||||||
 | 
								`UPDATE devices
 | 
				
			||||||
 | 
									SET is_confirmed = true,
 | 
				
			||||||
 | 
										updated_at = CURRENT_TIMESTAMP,
 | 
				
			||||||
 | 
										updated_user = ?
 | 
				
			||||||
 | 
									WHERE pid = ? AND version = ?`,
 | 
				
			||||||
 | 
								[updatedBy, pid, version],
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							await db.query(
 | 
				
			||||||
 | 
								"UPDATE users SET confirm_count = confirm_count + 1 WHERE id = ?",
 | 
				
			||||||
 | 
								[updatedBy],
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							res.json({
 | 
				
			||||||
 | 
								success: true,
 | 
				
			||||||
 | 
								message: "Device confirmed successfully!",
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						} catch (error) {
 | 
				
			||||||
 | 
							console.error("Confirm device error:", error);
 | 
				
			||||||
 | 
							res.status(500).json({
 | 
				
			||||||
 | 
								success: false,
 | 
				
			||||||
 | 
								message: "Server error, please try again!",
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Danh sách regex lọc lỗi
 | 
					// Danh sách regex lọc lỗi
 | 
				
			||||||
const errorPatterns = [
 | 
					const errorPatterns = [
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue