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; 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;
}
}

View File

@ -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>

View File

@ -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
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( 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 = [
{ {