devicetypeAPIGET1

This commit is contained in:
QuangMinh_123
2026-05-21 12:01:10 +07:00
parent 86383e7c03
commit 7aebcf9567
35 changed files with 2784 additions and 145 deletions

View File

@@ -1,13 +1,45 @@
<!doctype html>
<html lang="en">
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NDMS - Quản lý thiết bị mạng</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome -->
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
<body class="bg-gray-100">
<!-- THANH MENU -->
<nav class="bg-white shadow-md px-6 py-4">
<div class="flex gap-3 flex-wrap">
<button data-page="dashboard"
class="menu-btn bg-indigo-600 text-white px-5 py-2 rounded-lg font-medium">
🏠 Trang chủ
</button>
<button data-page="device-type"
class="menu-btn bg-gray-200 text-gray-700 px-5 py-2 rounded-lg font-medium">
📦 Loại thiết bị
</button>
<button data-page="device"
class="menu-btn bg-gray-200 text-gray-700 px-5 py-2 rounded-lg font-medium">
🖥️ Thiết bị
</button>
<button data-page="alert"
class="menu-btn bg-gray-200 text-gray-700 px-5 py-2 rounded-lg font-medium">
🔔 Cảnh báo
</button>
</div>
</nav>
<!-- NỘI DUNG CHÍNH -->
<div id="app" class="p-6">
<div class="text-center text-gray-500">Đang tải...</div>
</div>
<!-- QUAN TRỌNG: Đường dẫn tới main.js phải đúng -->
<script type="module" src="/src/main.js"></script>
</body>
</html>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,8 @@
"axios": "^1.16.0",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-router-dom": "^7.14.2"
"react-router-dom": "^7.14.2",
"tailwindcss": "^4.3.0"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
@@ -25,6 +26,7 @@
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.5.0",
"vite": "^8.0.9"
"vite": "^8.0.12",
"vite-plugin-html": "^3.2.2"
}
}

View File

@@ -0,0 +1,114 @@
// ============================================
// FILE NÀY LÀ TRANG CHỦ (DASHBOARD)
// Nhiệm vụ: Tạo ra giao diện và dữ liệu cho trang chủ
// ============================================
// Hàm này được gọi từ main.js
export async function showDashboard(container) {
// 1. TẠO GIAO DIỆN HTML
container.innerHTML = `
<div class="space-y-6">
<!-- Tiêu đề chào mừng -->
<div class="bg-gradient-to-r from-indigo-600 to-purple-600 rounded-2xl p-6 text-white">
<h1 class="text-2xl font-bold mb-2">Chào mừng đến với NDMS</h1>
<p>Hệ thống quản lý thiết bị mạng tập trung</p>
</div>
<!-- 4 Ô THỐNG KÊ -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
<!-- Ô 1: Tổng thiết bị -->
<div class="bg-white rounded-xl p-5 shadow-md">
<div class="flex justify-between items-center">
<div>
<p class="text-gray-500 text-sm">Tổng thiết bị</p>
<p class="text-3xl font-bold text-indigo-600" id="stat-total">0</p>
</div>
<i class="fas fa-server text-3xl text-indigo-400"></i>
</div>
</div>
<!-- Ô 2: Đang hoạt động -->
<div class="bg-white rounded-xl p-5 shadow-md">
<div class="flex justify-between items-center">
<div>
<p class="text-gray-500 text-sm">Đang hoạt động</p>
<p class="text-3xl font-bold text-green-600" id="stat-up">0</p>
</div>
<i class="fas fa-check-circle text-3xl text-green-400"></i>
</div>
</div>
<!-- Ô 3: Đang tắt -->
<div class="bg-white rounded-xl p-5 shadow-md">
<div class="flex justify-between items-center">
<div>
<p class="text-gray-500 text-sm">Đang tắt</p>
<p class="text-3xl font-bold text-red-600" id="stat-down">0</p>
</div>
<i class="fas fa-power-off text-3xl text-red-400"></i>
</div>
</div>
<!-- Ô 4: Cảnh báo -->
<div class="bg-white rounded-xl p-5 shadow-md">
<div class="flex justify-between items-center">
<div>
<p class="text-gray-500 text-sm">Cảnh báo</p>
<p class="text-3xl font-bold text-yellow-600" id="stat-alert">0</p>
</div>
<i class="fas fa-bell text-3xl text-yellow-400"></i>
</div>
</div>
</div>
<!-- DANH SÁCH HOẠT ĐỘNG GẦN ĐÂY -->
<div class="bg-white rounded-xl shadow-md p-5">
<h2 class="font-bold text-lg mb-4">
<i class="fas fa-clock text-gray-500 mr-2"></i>Hoạt động gần đây
</h2>
<div id="activity-list" class="space-y-2">
<!-- Dữ liệu sẽ được chèn vào đây -->
</div>
</div>
</div>
`;
// 2. ĐỔ DỮ LIỆU VÀO CÁC Ô THỐNG KÊ
// (Đây là dữ liệu mẫu - sau này sẽ gọi API từ backend)
document.getElementById('stat-total').innerText = '24';
document.getElementById('stat-up').innerText = '18';
document.getElementById('stat-down').innerText = '6';
document.getElementById('stat-alert').innerText = '3';
// 3. ĐỔ DỮ LIỆU VÀO DANH SÁCH HOẠT ĐỘNG
const activities = [
{ time: '10:30', action: 'Switch Tầng 1', status: 'down', message: 'Mất kết nối' },
{ time: '09:15', action: 'Router Chính', status: 'up', message: 'Đã kết nối lại' },
{ time: '08:00', action: 'Server DB', status: 'warning', message: 'CPU quá tải' },
{ time: 'Hôm qua', action: 'Cập nhật hệ thống', status: 'info', message: 'Đã cập nhật cấu hình' },
];
// Tạo HTML cho từng hoạt động
let activityHtml = '';
for (let act of activities) {
let iconClass = '';
if (act.status === 'down') iconClass = 'text-red-500 fa-exclamation-circle';
else if (act.status === 'up') iconClass = 'text-green-500 fa-check-circle';
else if (act.status === 'warning') iconClass = 'text-yellow-500 fa-exclamation-triangle';
else iconClass = 'text-blue-500 fa-info-circle';
activityHtml += `
<div class="flex items-center gap-3 p-3 border-b hover:bg-gray-50">
<i class="fas ${iconClass} w-5"></i>
<div class="flex-1">
<div class="flex justify-between">
<span class="font-medium">${act.action}</span>
<span class="text-xs text-gray-400">${act.time}</span>
</div>
<p class="text-sm text-gray-500">${act.message}</p>
</div>
</div>
`;
}
document.getElementById('activity-list').innerHTML = activityHtml;
}

View File

@@ -0,0 +1,52 @@
// ============================================
// API: KẾT NỐI VỚI BACK-END
// ============================================
// Địa chỉ Back-end (chạy cổng 5000)
const API_BASE_URL = 'http://localhost:5000/api';
// Hàm gọi API chung
async function callAPI(endpoint, method = 'GET', body = null) {
const options = {
method: method,
headers: {
'Content-Type': 'application/json',
}
};
if (body && (method === 'POST' || method === 'PUT')) {
options.body = JSON.stringify(body);
}
try {
const response = await fetch(`${API_BASE_URL}${endpoint}`, options);
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Có lỗi xảy ra');
}
return await response.json();
} catch (error) {
console.error('API Error:', error);
throw error;
}
}
// Các hàm API cho Device Type
export const deviceTypeAPI = {
// Lấy danh sách tất cả loại thiết bị
getAll: () => callAPI('/device-types'),
// Lấy chi tiết 1 loại thiết bị
getById: (id) => callAPI(`/device-types/${id}`),
// Thêm mới loại thiết bị
create: (data) => callAPI('/device-types', 'POST', data),
// Cập nhật loại thiết bị
update: (id, data) => callAPI(`/device-types/${id}`, 'PUT', data),
// Xóa loại thiết bị
delete: (id) => callAPI(`/device-types/${id}`, 'DELETE'),
};

View File

@@ -0,0 +1,474 @@
// // ============================================
// // MODULE: QUẢN LÝ LOẠI THIẾT BỊ (DEVICE TYPE)
// // ============================================
// // Nhiệm vụ: Thêm, sửa, xóa, hiển thị danh sách loại thiết bị
// // ============================================
// // DỮ LIỆU MẪU (lưu trong bộ nhớ tạm)
// let deviceTypes = [
// { id: '1', name: 'Switch', description: 'Thiết bị chuyển mạch mạng', color: '#10B981', isActive: true },
// { id: '2', name: 'Router', description: 'Bộ định tuyến mạng', color: '#3B82F6', isActive: true },
// { id: '3', name: 'WorkStation', description: 'Máy trạm làm việc', color: '#8B5CF6', isActive: true },
// { id: '4', name: 'Server', description: 'Máy chủ', color: '#EF4444', isActive: true },
// ];
// // HÀM HIỂN THỊ TRANG DEVICE TYPE
// export async function showDeviceTypePage(container) {
// // Tạo giao diện HTML
// container.innerHTML = `
// <div class="space-y-6">
// <!-- Tiêu đề và nút thêm -->
// <div class="flex justify-between items-center">
// <div>
// <h1 class="text-2xl font-bold text-gray-800">
// <i class="fas fa-tags text-indigo-500 mr-2"></i>Quản lý loại thiết bị
// </h1>
// <p class="text-gray-500 text-sm mt-1">Danh mục các loại thiết bị mạng trong hệ thống</p>
// </div>
// <button id="addDeviceTypeBtn" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg shadow transition flex items-center gap-2">
// <i class="fas fa-plus"></i>
// <span>Thêm loại thiết bị</span>
// </button>
// </div>
// <!-- 2 ô thống kê nhanh -->
// <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
// <div class="bg-blue-50 rounded-xl p-4 border border-blue-100">
// <div class="flex justify-between items-center">
// <div>
// <p class="text-blue-600 text-sm font-medium">Tổng số loại</p>
// <p class="text-2xl font-bold text-blue-700" id="totalCount">0</p>
// </div>
// <i class="fas fa-layer-group text-3xl text-blue-400"></i>
// </div>
// </div>
// <div class="bg-green-50 rounded-xl p-4 border border-green-100">
// <div class="flex justify-between items-center">
// <div>
// <p class="text-green-600 text-sm font-medium">Đang hoạt động</p>
// <p class="text-2xl font-bold text-green-700" id="activeCount">0</p>
// </div>
// <i class="fas fa-check-circle text-3xl text-green-400"></i>
// </div>
// </div>
// </div>
// <!-- Ô tìm kiếm -->
// <div class="relative">
// <i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
// <input type="text" id="searchInput" placeholder="Tìm kiếm theo tên hoặc mô tả..." class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500">
// </div>
// <!-- Danh sách loại thiết bị -->
// <div id="deviceTypeList" class="space-y-3">
// <!-- Nội dung sẽ được render bằng JavaScript -->
// <div class="text-center py-8 text-gray-400">Đang tải...</div>
// </div>
// </div>
// `;
// // HIỂN THỊ DANH SÁCH
// renderList();
// // CẬP NHẬT THỐNG KÊ
// updateStats();
// // GẮN SỰ KIỆN CHO NÚT THÊM
// document.getElementById('addDeviceTypeBtn').onclick = showAddForm;
// // GẮN SỰ KIỆN CHO Ô TÌM KIẾM
// document.getElementById('searchInput').oninput = function(e) {
// const keyword = e.target.value.toLowerCase();
// const filtered = deviceTypes.filter(item =>
// item.name.toLowerCase().includes(keyword) ||
// (item.description && item.description.toLowerCase().includes(keyword))
// );
// renderListWithData(filtered);
// };
// }
// // HÀM HIỂN THỊ DANH SÁCH (lấy từ mảng deviceTypes)
// function renderList() {
// renderListWithData(deviceTypes);
// }
// // HÀM HIỂN THỊ DANH SÁCH VỚI DỮ LIỆU TÙY CHỈNH
// function renderListWithData(data) {
// const container = document.getElementById('deviceTypeList');
// if (!container) return;
// if (data.length === 0) {
// container.innerHTML = `
// <div class="text-center py-12 bg-white rounded-xl border">
// <i class="fas fa-inbox text-gray-300 text-5xl mb-3"></i>
// <p class="text-gray-500">Chưa có loại thiết bị nào</p>
// <button onclick="document.getElementById('addDeviceTypeBtn').click()" class="mt-3 text-indigo-600 hover:text-indigo-700 text-sm">
// <i class="fas fa-plus mr-1"></i>Thêm loại đầu tiên
// </button>
// </div>
// `;
// return;
// }
// let html = '';
// for (let item of data) {
// html += `
// <div class="bg-white border border-gray-200 rounded-xl p-4 hover:shadow-md transition-shadow">
// <div class="flex items-start justify-between">
// <div class="flex items-center gap-4">
// <!-- Icon màu sắc -->
// <div class="w-12 h-12 rounded-xl flex items-center justify-center shadow-sm" style="background-color: ${item.color}20">
// <i class="fas fa-network-wired text-xl" style="color: ${item.color}"></i>
// </div>
// <!-- Thông tin -->
// <div>
// <div class="flex items-center gap-2">
// <h3 class="font-semibold text-gray-800 text-lg">${escapeHtml(item.name)}</h3>
// <span class="text-xs px-2 py-0.5 rounded-full ${item.isActive ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'}">
// ${item.isActive ? '● Hoạt động' : '○ Dừng'}
// </span>
// </div>
// <p class="text-sm text-gray-500 mt-0.5">${escapeHtml(item.description || 'Chưa có mô tả')}</p>
// <div class="flex items-center gap-3 mt-2">
// <span class="text-xs text-gray-400">ID: ${item.id}</span>
// <span class="text-xs text-gray-400">Mã màu: ${item.color}</span>
// </div>
// </div>
// </div>
// <!-- Nút hành động -->
// <div class="flex gap-1">
// <button onclick="window.editDeviceType('${item.id}')" class="p-2 text-indigo-600 hover:bg-indigo-50 rounded-lg transition" title="Sửa">
// <i class="fas fa-edit"></i>
// </button>
// <button onclick="window.deleteDeviceType('${item.id}')" class="p-2 text-red-600 hover:bg-red-50 rounded-lg transition" title="Xóa">
// <i class="fas fa-trash"></i>
// </button>
// </div>
// </div>
// </div>
// `;
// }
// container.innerHTML = html;
// }
// // CẬP NHẬT SỐ LIỆU THỐNG KÊ
// function updateStats() {
// const totalEl = document.getElementById('totalCount');
// const activeEl = document.getElementById('activeCount');
// if (totalEl) totalEl.innerText = deviceTypes.length;
// if (activeEl) activeEl.innerText = deviceTypes.filter(item => item.isActive).length;
// }
// // HIỂN THỊ FORM THÊM MỚI
// function showAddForm() {
// // Tạo modal đơn giản bằng prompt (dễ hiểu cho người mới)
// const name = prompt('📝 Nhập tên loại thiết bị:', '');
// if (!name) return;
// const description = prompt('📄 Nhập mô tả (không bắt buộc):', '');
// const color = prompt('🎨 Nhập mã màu (ví dụ: #3B82F6, #10B981, #EF4444):', '#3B82F6');
// // Tạo mới
// const newType = {
// id: Date.now().toString(),
// name: name,
// description: description || '',
// color: color || '#3B82F6',
// isActive: true
// };
// deviceTypes.push(newType);
// // Cập nhật lại giao diện
// renderList();
// updateStats();
// alert(`✅ Đã thêm loại thiết bị "${name}" thành công!`);
// }
// // HIỂN THỊ FORM SỬA
// window.editDeviceType = function(id) {
// const item = deviceTypes.find(t => t.id === id);
// if (!item) return;
// const newName = prompt('✏️ Sửa tên loại thiết bị:', item.name);
// if (!newName) return;
// const newDesc = prompt('📄 Sửa mô tả:', item.description || '');
// const newColor = prompt('🎨 Sửa mã màu:', item.color);
// const isActive = confirm(`✅ Loại thiết bị này có đang hoạt động không?\nOK = Có, Cancel = Không`);
// // Cập nhật
// item.name = newName;
// item.description = newDesc || '';
// item.color = newColor || '#3B82F6';
// item.isActive = isActive;
// // Cập nhật lại giao diện
// renderList();
// updateStats();
// alert(`✅ Đã cập nhật "${newName}" thành công!`);
// };
// // XÓA LOẠI THIẾT BỊ
// window.deleteDeviceType = function(id) {
// const item = deviceTypes.find(t => t.id === id);
// if (!item) return;
// if (confirm(`⚠️ Bạn có chắc muốn xóa "${item.name}"?\nHành động này không thể hoàn tác!`)) {
// deviceTypes = deviceTypes.filter(t => t.id !== id);
// // Cập nhật lại giao diện
// renderList();
// updateStats();
// alert(`🗑️ Đã xóa "${item.name}" thành công!`);
// }
// };
// // Hàm bảo vệ chống mã độc XSS
// function escapeHtml(str) {
// if (!str) return '';
// return str
// .replace(/&/g, '&amp;')
// .replace(/</g, '&lt;')
// .replace(/>/g, '&gt;')
// .replace(/"/g, '&quot;')
// .replace(/'/g, '&#39;');
// }
// // Export để dùng ở nơi khác (nếu cần)
// export { deviceTypes, renderList, updateStats };
// ============================================
// UI: GIAO DIỆN QUẢN LÝ LOẠI THIẾT BỊ
// ============================================
import { deviceTypeAPI } from './device-type-api.js';
let deviceTypes = [];
// HÀM HIỂN THỊ TRANG DEVICE TYPE
export async function showDeviceTypePage(container) {
// Tạo giao diện HTML
container.innerHTML = `
<div class="space-y-6">
<!-- Tiêu đề và nút thêm -->
<div class="flex justify-between items-center">
<div>
<h1 class="text-2xl font-bold text-gray-800">
<i class="fas fa-tags text-indigo-500 mr-2"></i>Quản lý loại thiết bị
</h1>
<p class="text-gray-500 text-sm mt-1">Danh mục các loại thiết bị mạng</p>
</div>
<button id="addDeviceTypeBtn" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-lg shadow transition flex items-center gap-2">
<i class="fas fa-plus"></i>
<span>Thêm loại thiết bị</span>
</button>
</div>
<!-- 2 ô thống kê -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="bg-blue-50 rounded-xl p-4">
<p class="text-blue-600 text-sm">Tổng số loại</p>
<p class="text-2xl font-bold text-blue-700" id="totalCount">0</p>
</div>
<div class="bg-green-50 rounded-xl p-4">
<p class="text-green-600 text-sm">Đang hoạt động</p>
<p class="text-2xl font-bold text-green-700" id="activeCount">0</p>
</div>
</div>
<!-- Ô tìm kiếm -->
<div class="relative">
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
<input type="text" id="searchInput" placeholder="Tìm kiếm..." class="w-full pl-10 pr-4 py-2 border rounded-lg">
</div>
<!-- Danh sách -->
<div id="deviceTypeList" class="space-y-3">
<div class="text-center py-8 text-gray-400">Đang tải dữ liệu...</div>
</div>
</div>
`;
// Load dữ liệu từ Back-end
await loadDataFromAPI();
// Gắn sự kiện
document.getElementById('addDeviceTypeBtn').onclick = showAddForm;
document.getElementById('searchInput').oninput = handleSearch;
}
// LOAD DỮ LIỆU TỪ BACK-END
async function loadDataFromAPI() {
try {
// Gọi API lấy danh sách
const result = await deviceTypeAPI.getAll();
deviceTypes = Array.isArray(result) ? result : result.data || [];
// Hiển thị lên giao diện
renderList(deviceTypes);
updateStats();
console.log('Đã tải', deviceTypes.length, 'loại thiết bị');
} catch (error) {
console.error('Lỗi tải dữ liệu:', error);
document.getElementById('deviceTypeList').innerHTML = `
<div class="text-center py-8 text-red-500">
<i class="fas fa-exclamation-triangle text-3xl mb-2"></i>
<p>Không thể kết nối đến Back-end!</p>
<p class="text-sm mt-2">Vui lòng đảm bảo Back-end đang chạy tại http://localhost:5000</p>
</div>
`;
}
}
// HIỂN THỊ DANH SÁCH
function renderList(data) {
const container = document.getElementById('deviceTypeList');
if (!container) return;
if (data.length === 0) {
container.innerHTML = `
<div class="text-center py-12 bg-white rounded-xl border">
<i class="fas fa-inbox text-gray-300 text-5xl mb-3"></i>
<p class="text-gray-500">Chưa có loại thiết bị nào</p>
</div>
`;
return;
}
let html = '';
for (let item of data) {
html += `
<div class="bg-white border rounded-xl p-4 hover:shadow-md">
<div class="flex items-start justify-between">
<div class="flex items-center gap-4">
<div class="w-12 h-12 rounded-xl flex items-center justify-center" style="background-color: ${item.color}20">
<i class="fas fa-network-wired text-xl" style="color: ${item.color}"></i>
</div>
<div>
<h3 class="font-semibold text-gray-800">${escapeHtml(item.name)}</h3>
<p class="text-sm text-gray-500">${escapeHtml(item.description || 'Chưa có mô tả')}</p>
<span class="text-xs px-2 py-0.5 rounded-full ${item.isActive ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'}">
${item.isActive ? '● Hoạt động' : '○ Dừng'}
</span>
</div>
</div>
<div class="flex gap-1">
<button onclick="window.editDeviceType('${item.id}')" class="p-2 text-indigo-600 hover:bg-indigo-50 rounded-lg">
<i class="fas fa-edit"></i>
</button>
<button onclick="window.deleteDeviceType('${item.id}')" class="p-2 text-red-600 hover:bg-red-50 rounded-lg">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
`;
}
container.innerHTML = html;
}
// CẬP NHẬT THỐNG KÊ
function updateStats() {
const totalEl = document.getElementById('totalCount');
const activeEl = document.getElementById('activeCount');
if (totalEl) totalEl.innerText = deviceTypes.length;
if (activeEl) activeEl.innerText = deviceTypes.filter(item => item.isActive).length;
}
// TÌM KIẾM
function handleSearch(e) {
const keyword = e.target.value.toLowerCase();
const filtered = deviceTypes.filter(item =>
item.name.toLowerCase().includes(keyword) ||
(item.description && item.description.toLowerCase().includes(keyword))
);
renderList(filtered);
}
// THÊM MỚI (GỌI API)
async function showAddForm() {
const name = prompt('📝 Nhập tên loại thiết bị:');
if (!name) return;
const description = prompt('📄 Nhập mô tả:');
const color = prompt('🎨 Nhập mã màu (vd: #3B82F6):', '#3B82F6');
try {
const newType = await deviceTypeAPI.create({
name: name,
description: description || '',
color: color || '#3B82F6',
isActive: true
});
// Tải lại dữ liệu
await loadDataFromAPI();
alert(`✅ Đã thêm "${name}" thành công!`);
} catch (error) {
alert('❌ Lỗi: ' + error.message);
}
}
// SỬA (GỌI API)
window.editDeviceType = async function(id) {
const item = deviceTypes.find(t => t.id === id);
if (!item) return;
const newName = prompt('✏️ Sửa tên:', item.name);
if (!newName) return;
const newDesc = prompt('📄 Sửa mô tả:', item.description || '');
const newColor = prompt('🎨 Sửa màu:', item.color);
const isActive = confirm('Có đang hoạt động không?');
try {
await deviceTypeAPI.update(id, {
...item,
name: newName,
description: newDesc || '',
color: newColor || '#3B82F6',
isActive: isActive
});
await loadDataFromAPI();
alert('✅ Cập nhật thành công!');
} catch (error) {
alert('❌ Lỗi: ' + error.message);
}
};
// XÓA (GỌI API)
window.deleteDeviceType = async function(id) {
const item = deviceTypes.find(t => t.id === id);
if (!item) return;
if (confirm(`⚠️ Xóa "${item.name}"?`)) {
try {
await deviceTypeAPI.delete(id);
await loadDataFromAPI();
alert('🗑️ Xóa thành công!');
} catch (error) {
alert('❌ Lỗi: ' + error.message);
}
}
};
function escapeHtml(str) {
if (!str) return '';
return str.replace(/[&<>]/g, function(m) {
if (m === '&') return '&amp;';
if (m === '<') return '&lt;';
if (m === '>') return '&gt;';
return m;
});
}

57
frontend/src/main.js Normal file
View File

@@ -0,0 +1,57 @@
// ============================================
// FILE NÀY CÓ NHIỆM VỤ: ĐIỀU KHIỂN CHUYỂN TRANG
// ============================================
// Import các module (tính năng) của ứng dụng
import { showDashboard } from './features/dashboard/dashboard.js';
import { showDeviceTypePage } from './features/device-type/device-type.js';
// Hàm chuyển đổi giữa các trang
async function switchToPage(pageName) {
// Lấy thẻ div chứa nội dung
const appDiv = document.getElementById('app');
// Debug: In ra console để kiểm tra xem hàm có được gọi đúng không
if (!appDiv) {
console.error('Không tìm thấy element #app');
return;
}
console.log('Chuyển đến trang:', pageName); // Debug
// Kiểm tra xem người dùng bấm vào trang nào
if (pageName === 'dashboard') {
// Gọi hàm showDashboard từ file dashboard.js
await showDashboard(appDiv);
}
else if (pageName === 'device-type') {
await showDeviceTypePage(appDiv);
}
else if (pageName === 'device') {
appDiv.innerHTML = '<div class="bg-white p-6 rounded-lg shadow">🖥️ Trang Thiết bị - Đang phát triển</div>';
}
else if (pageName === 'alert') {
appDiv.innerHTML = '<div class="bg-white p-6 rounded-lg shadow">🔔 Trang Cảnh báo - Đang phát triển</div>';
}
// Đổi màu nút menu đang được chọn
document.querySelectorAll('.menu-btn').forEach(btn => {
if (btn.dataset.page === pageName) {
btn.className = 'menu-btn bg-indigo-600 text-white px-5 py-2 rounded-lg font-medium';
} else {
btn.className = 'menu-btn bg-gray-200 text-gray-700 px-5 py-2 rounded-lg font-medium';
}
});
}
// BẮT SỰ KIỆN KHI BẤM VÀO CÁC NÚT MENU
document.querySelectorAll('.menu-btn').forEach(button => {
button.addEventListener('click', () => {
const pageName = button.dataset.page; // lấy tên trang từ thuộc tính data-page
switchToPage(pageName);
});
});
// ⭐ QUAN TRỌNG: Khi load trang, mặc định hiển thị DASHBOARD (trang chủ)
switchToPage('dashboard');

View File

@@ -1,7 +1,22 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite';
import { createHtmlPlugin } from 'vite-plugin-html';
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})
plugins: [
createHtmlPlugin({
minify: true,
entry: '/src/main.js',
template: 'index.html',
}),
],
server: {
port: 5173,
open: true,
proxy: {
'/api': {
target: 'http://localhost:5000', // Backend Flask của bạn
changeOrigin: true,
}
}
}
});