update delete log, add confirm device

This commit is contained in:
dbdbd9 2025-07-28 09:49:24 +07:00
parent 06554328b1
commit e8694be0e2
4 changed files with 343 additions and 203 deletions

View File

@ -422,6 +422,7 @@ mark {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 1rem;
}
@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 {
position: relative;
display: inline-block;
@ -500,6 +481,15 @@ mark {
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 {
display: block;
}
@ -522,3 +512,9 @@ mark {
opacity: 1;
bottom: 40px;
}
@media only screen and (max-width: 768px) {
.modal-content {
max-width: 90% !important;
}
}

View File

@ -38,6 +38,8 @@
></button>
<div class="user-dropdown-content">
<div id="confirm-count-user" class="item"></div>
<button onclick="logout()">Logout</button>
</div>
</div>
@ -60,14 +62,21 @@
</button>
</div>
<button
id="logConfirmDeleteBtn"
type="button"
class="btn-with-badge danger-btn"
>
Delete
<span class="badge" id="logDeleteBadge">0</span>
</button>
<div>
<button
id="deviceConfirmBtn"
type="button"
class="primary-btn"
>
Confirm
</button>
<span
id="confirmUserText"
style="font-size: 12px; display: none"
>
Confirmed by <strong id="confirmedUserName"></strong>
</span>
</div>
</div>
<div id="errorTableContainer" class="command-section">
@ -85,54 +94,76 @@
</div>
<div id="logSections"></div>
<!-- Log Delete Modal -->
<div id="logDeleteModal" class="modal hidden">
<div class="modal-content">
<h2 class="modal-title">Confirm Deletion</h2>
<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 style="max-width: 30%" class="modal-content">
<div id="deleteLogText"></div>
<div class="modal-actions">
<button id="logCloseModalBtn" class="btn secondary-btn">
<button id="logCancelDeleteBtn" class="secondary-btn">
Cancel
</button>
<button id="logSubmitDeleteBtn" class="btn danger-btn">
<button id="logDeleteBtn" class="danger-btn">
Delete
</button>
</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>
<!-- Authentication and get user info -->
<script>
if (!localStorage.getItem("token")) {
logout();
}
document.getElementById(
"welcome-user",
).innerHTML = `Welcome, <strong>${
JSON.parse(localStorage.getItem("user")).name
}</strong> ▼`;
function logout() {
localStorage.removeItem("token");
localStorage.removeItem("user");
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>
<!-- Get PID - Version - Log -->
<script>
// Get param pid, version from URL
const params = new URLSearchParams(window.location.search);
@ -146,7 +177,12 @@
const logSections = document.getElementById("logSections");
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
let versionsUser = [];
async function loadPIDs(pidParam, versionParam) {
const res = await fetch("/api/pids");
@ -181,6 +217,15 @@
);
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>`;
versionList.innerHTML = versions
.map((v) => {
@ -206,7 +251,7 @@
versionList.dispatchEvent(new Event("change"));
currentParams.set("version", versionParam);
} else {
// Default chọn version đầu tiên
// Default chọn version đầu tiên
if (versions.length > 0) {
versionList.value = versions[0].version;
versionList.dispatchEvent(new Event("change"));
@ -266,6 +311,20 @@
`?${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);
}
});
@ -435,36 +494,11 @@ ${license}`.trim();
}
</script>
<!-- Handle delete log -->
<script>
const STORAGE_KEY = "logDeleteList";
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");
let pendingDeleteLog = null;
function getDeleteList() {
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) => {
document.addEventListener("click", async (e) => {
if (e.target.classList.contains("trash-btn")) {
const section = e.target.closest(".command-section");
const pid = pidList.value;
@ -474,103 +508,58 @@ ${license}`.trim();
const log =
logsCache[`${pid}_${version}`][command].logs[index];
const currentList = getDeleteList();
if (!currentList.some((item) => item.id === log.id)) {
currentList.push({
id: log.id,
pid,
version,
command,
});
saveDeleteList(currentList);
pendingDeleteLog = { id: log.id, command, pid, version };
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 deleteList = getDeleteList();
logDeleteListContainer.innerHTML = "";
deleteList.forEach((item, i) => {
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${item.id}</td>
<td>${item.pid}</td>
<td>${item.version}</td>
<td>${item.command}</td>
<td style="text-align: center;"><button type="button" class="danger-btn remove-item-btn" data-index="${i}" title="Remove">🗑</button></td>
`;
logDeleteListContainer.appendChild(tr);
});
const logDeleteModal = document.getElementById("logDeleteModal");
const logCancelDeleteBtn =
document.getElementById("logCancelDeleteBtn");
const logDeleteBtn = document.getElementById("logDeleteBtn");
function showDeleteModal() {
logDeleteModal.classList.remove("hidden");
}
function hideDeleteModal() {
logDeleteModal.classList.add("hidden");
pendingDeleteLog = null;
}
logDeleteListContainer.addEventListener("click", (e) => {
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();
}
});
logCancelDeleteBtn.addEventListener("click", hideDeleteModal);
// Open delete modal
logConfirmDeleteBtn.addEventListener("click", () => {
renderDeleteModal();
logDeleteModal.classList.remove("hidden");
});
logDeleteBtn.addEventListener("click", async () => {
if (!pendingDeleteLog) return;
// Open close modal
logCloseModalBtn.addEventListener("click", () => {
logDeleteModal.classList.add("hidden");
});
// 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", {
const token = localStorage.getItem("token");
try {
const res = await fetch("/api/delete-log", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ items: deletedItems }),
})
.then((res) => res.json())
.then((data) => {
alert("Deleted successfully!");
localStorage.removeItem(STORAGE_KEY);
location.reload();
})
.catch((err) => {
console.error(err);
alert("Delete failed.");
});
body: JSON.stringify(pendingDeleteLog),
});
const data = await res.json();
if (data.success) {
alert("Deleted successfully!");
location.reload();
} else {
alert("Delete failed.");
}
} catch (err) {
console.error(err);
alert("Delete failed.");
} finally {
hideDeleteModal();
pendingDeleteLog = null;
}
});
updateDeleteBtn();
function showToast(message = "Announce something") {
const toast = document.getElementById("logToast");
toast.textContent = message;
@ -583,5 +572,74 @@ ${license}`.trim();
}, 2000);
}
</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>
</html>

View File

@ -2,6 +2,7 @@ const express = require("express");
const path = require("path");
const bcrypt = require("bcrypt");
const { createToken } = require("../utils/jwt");
const { authenticateToken } = require("../middleware/auth");
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);
};

144
server.js
View File

@ -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(
`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
const [devices] = await db.query(
"SELECT id, version FROM devices WHERE pid = ?",
"SELECT id, version, updated_user FROM devices WHERE pid = ?",
[pid],
);
@ -153,7 +165,12 @@ app.get("/api/pid/:pid/versions", async (req, res) => {
for (const device of devices) {
const deviceId = device.id;
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(
"SELECT COUNT(*) AS c FROM inventory_outputs WHERE device_id = ?",
[deviceId],
@ -172,6 +189,7 @@ app.get("/api/pid/:pid/versions", async (req, res) => {
);
results.push({
user: user[0],
version,
commands: {
inventory: inv.c,
@ -202,47 +220,33 @@ app.get("/api/device/:pid/:version/:command", async (req, res) => {
res.json(logs);
});
app.post("/api/delete-logs", authenticateToken, async (req, res) => {
const items = req.body.items;
const userId = req.user?.id;
if (!Array.isArray(items)) {
return res.status(400).json({ message: "Invalid payload" });
}
app.post("/api/delete-log", authenticateToken, async (req, res) => {
const logId = req.body.id;
const logCommand = req.body.command;
try {
for (const item of items) {
const { command, id } = item;
if (!command || typeof id !== "number") continue;
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],
);
if (!logCommand || typeof logId !== "number") {
return res
.status(400)
.json({ success: false, message: "Invalid input" });
}
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 });
} catch (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
const errorPatterns = [
{