This commit is contained in:
2026-04-22 17:03:29 +07:00
5 changed files with 63 additions and 1425 deletions

BIN
.gitignore vendored

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -145,6 +145,7 @@ END
-- =========================================== -- ===========================================
-- 6. CREATE INDEXES -- 6. CREATE INDEXES
-- 6. CREATE INDEXES
-- =========================================== -- ===========================================
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Users_Username') IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Users_Username')
BEGIN BEGIN
@@ -171,10 +172,21 @@ BEGIN
CREATE INDEX IX_AssetInventory_Status ON AssetInventory(Status); CREATE INDEX IX_AssetInventory_Status ON AssetInventory(Status);
END END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetInventory_AssetCode')
BEGIN
CREATE INDEX IX_AssetInventory_AssetCode ON AssetInventory(AssetCode);
END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetInventory_Status')
BEGIN
CREATE INDEX IX_AssetInventory_Status ON AssetInventory(Status);
END
PRINT 'Indexes created successfully.'; PRINT 'Indexes created successfully.';
-- =========================================== -- ===========================================
-- 7. INSERT INITIAL DATA -- 7. INSERT INITIAL DATA
-- 7. INSERT INITIAL DATA
-- =========================================== -- ===========================================
-- Check if admin user exists -- Check if admin user exists
@@ -199,6 +211,7 @@ END
-- =========================================== -- ===========================================
-- 8. DISPLAY DATABASE INFORMATION -- 8. DISPLAY DATABASE INFORMATION
-- 8. DISPLAY DATABASE INFORMATION
-- =========================================== -- ===========================================
PRINT ''; PRINT '';
PRINT '========================================'; PRINT '========================================';

View File

@@ -15,6 +15,7 @@ class AccountManager {
this.applications = []; this.applications = [];
this.users = []; this.users = [];
this.assets = []; this.assets = [];
this.assets = [];
this.roles = []; this.roles = [];
this.accountPage = 1; this.accountPage = 1;
this.accountPageSize = 9; this.accountPageSize = 9;
@@ -24,6 +25,8 @@ class AccountManager {
this.userPageSize = 9; this.userPageSize = 9;
this.assetPage = 1; this.assetPage = 1;
this.assetPageSize = 10; this.assetPageSize = 10;
this.assetPage = 1;
this.assetPageSize = 10;
this.apiBase = '/api'; this.apiBase = '/api';
this.currentPage = 'dashboard'; this.currentPage = 'dashboard';
this.accountSearchTerm = ''; this.accountSearchTerm = '';
@@ -34,6 +37,9 @@ class AccountManager {
this.assetSearchTerm = ''; this.assetSearchTerm = '';
this.assetStatusFilter = ''; this.assetStatusFilter = '';
this.selectedAssetIds = new Set(); this.selectedAssetIds = new Set();
this.assetSearchTerm = '';
this.assetStatusFilter = '';
this.selectedAssetIds = new Set();
this.mobileBreakpoint = 900; this.mobileBreakpoint = 900;
this.boundResizeHandler = null; this.boundResizeHandler = null;
this.configureNotifications(); this.configureNotifications();
@@ -166,6 +172,8 @@ class AccountManager {
if (usersNav) usersNav.style.display = ''; if (usersNav) usersNav.style.display = '';
const usersSection = document.getElementById('usersSection'); const usersSection = document.getElementById('usersSection');
if (usersSection) usersSection.style.display = ''; if (usersSection) usersSection.style.display = '';
const usersSection = document.getElementById('usersSection');
if (usersSection) usersSection.style.display = '';
} }
this.setupEventListeners(); this.setupEventListeners();
@@ -201,6 +209,12 @@ class AccountManager {
this.setupAddButtonListeners(); this.setupAddButtonListeners();
this.setupFilters(); this.setupFilters();
this.setupAssetPagerListeners(); this.setupAssetPagerListeners();
} else if (page === 'assets') {
mainContent.innerHTML = this.getAssetsContent();
this.setupAssetRowListeners();
this.setupAddButtonListeners();
this.setupFilters();
this.setupAssetPagerListeners();
} else if (page === 'accounts') { } else if (page === 'accounts') {
mainContent.innerHTML = this.getAccountsContent(); mainContent.innerHTML = this.getAccountsContent();
this.setupAccountRowListeners(); this.setupAccountRowListeners();
@@ -481,6 +495,7 @@ class AccountManager {
const accountSearch = document.getElementById('accountSearch'); const accountSearch = document.getElementById('accountSearch');
const appSearch = document.getElementById('appSearch'); const appSearch = document.getElementById('appSearch');
const assetSearch = document.getElementById('assetSearch'); const assetSearch = document.getElementById('assetSearch');
const assetSearch = document.getElementById('assetSearch');
if (accountSearch && accountSearch.dataset.focused === 'true') { if (accountSearch && accountSearch.dataset.focused === 'true') {
const pos = accountSearch.selectionStart || accountSearch.value.length; const pos = accountSearch.selectionStart || accountSearch.value.length;
@@ -499,6 +514,12 @@ class AccountManager {
assetSearch.focus(); assetSearch.focus();
assetSearch.setSelectionRange(pos, pos); assetSearch.setSelectionRange(pos, pos);
} }
if (assetSearch && assetSearch.dataset.focused === 'true') {
const pos = assetSearch.selectionStart || assetSearch.value.length;
assetSearch.focus();
assetSearch.setSelectionRange(pos, pos);
}
} }
setupEventListeners() { setupEventListeners() {
@@ -3448,6 +3469,35 @@ class AccountManager {
}); });
} }
const assetSearch = document.getElementById('assetSearch');
if (assetSearch) {
assetSearch.value = this.assetSearchTerm;
const handleAssetSearch = event => {
this.assetSearchTerm = event.target.value.toLowerCase();
this.assetPage = 1;
this.renderAssetsTableBody();
};
assetSearch.addEventListener('input', handleAssetSearch);
assetSearch.addEventListener('focus', () => {
assetSearch.dataset.focused = 'true';
});
assetSearch.addEventListener('blur', () => {
assetSearch.dataset.focused = 'false';
});
});
}
const assetStatusFilter = document.getElementById('assetStatusFilter');
if (assetStatusFilter) {
assetStatusFilter.value = this.assetStatusFilter || '';
assetStatusFilter.addEventListener('change', (e) => {
this.assetStatusFilter = String(e.target.value || '').toLowerCase();
this.assetPage = 1;
this.renderAssetsTableBody();
});
}
const assetSearch = document.getElementById('assetSearch'); const assetSearch = document.getElementById('assetSearch');
if (assetSearch) { if (assetSearch) {
assetSearch.value = this.assetSearchTerm; assetSearch.value = this.assetSearchTerm;

View File

@@ -197,183 +197,3 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Add/Edit Asset Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetModal">
<div class="modal-content w-full max-w-2xl bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4 flex flex-col" style="max-height: calc(100vh - 2rem);">
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between bg-slate-50">
<h3 class="text-base font-extrabold text-slate-900">Biểu mẫu tài sản</h3>
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeAssetModal()">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<form id="assetForm" class="p-6 space-y-4 overflow-y-auto">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Mã tài sản</label>
<input type="text" id="assetCodeInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required placeholder="TS-001">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Tên tài sản</label>
<input type="text" id="assetNameInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required placeholder="Laptop Dell Latitude 5440">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Trạng thái</label>
<select id="assetStatusInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3">
<option value="in_use">Đang sử dụng</option>
<option value="in_stock">Trong kho</option>
<option value="maintenance">Bảo trì</option>
<option value="disposed">Thanh lý</option>
</select>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Model</label>
<input type="text" id="assetModelInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" placeholder="Latitude 5440">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Số serial</label>
<input type="text" id="assetSerialInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" placeholder="SN123...">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Số lượng (Tồn đầu kỳ)</label>
<input type="number" id="assetQuantityInput" min="0" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" value="0">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Nhập trong kỳ</label>
<input type="number" id="assetImportInPeriodInput" min="0" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" value="0">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Xuất trong kỳ</label>
<input type="number" id="assetExportInPeriodInput" min="0" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" value="0" readonly>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Tồn cuối kỳ</label>
<input type="number" id="assetEndingBalanceInput" min="0" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" value="0" readonly>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Đơn vị</label>
<input type="text" id="assetUnitInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" placeholder="cái">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Phòng ban</label>
<input type="text" id="assetDepartmentInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" placeholder="Kỹ thuật">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Dự án</label>
<input type="text" id="assetProjectInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" placeholder="AGV / SS demo">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Vị trí</label>
<input type="text" id="assetLocationInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" placeholder="Kho A">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Người phụ trách</label>
<select id="assetCustodianInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3">
<option value="">-- Chon nguoi phu trach --</option>
</select>
</div>
<div class="md:col-span-2">
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Người mượn hiện tại</label>
<textarea id="assetBorrowerSummaryInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 h-24 resize-none bg-slate-50" readonly></textarea>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Ngày mua</label>
<input type="date" id="assetPurchaseDateInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Giá mua</label>
<input type="number" id="assetPriceInput" min="0" step="0.01" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" placeholder="0">
</div>
<div class="md:col-span-2">
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Ghi chú</label>
<textarea id="assetNotesInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 h-20 resize-none" placeholder="Thông tin bổ sung"></textarea>
</div>
</div>
<div class="flex gap-3 pt-2 sticky bottom-0 bg-white pb-1">
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeAssetModal()">Hủy</button>
<button type="submit" class="flex-1 px-4 py-2 bg-primary hover:bg-primary-dim text-on-primary rounded-lg text-xs font-bold">Lưu tài sản</button>
</div>
</form>
</div>
</div>
<!-- Borrow Asset Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="borrowAssetModal">
<div class="modal-content w-full max-w-lg bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4">
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between bg-slate-50">
<h3 class="text-base font-extrabold text-slate-900">Mượn tài sản</h3>
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeBorrowAssetModal()">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<form id="borrowAssetForm" class="p-6 space-y-4">
<input type="hidden" id="borrowAssetIdInput">
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Tài sản</label>
<input type="text" id="borrowAssetNameInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" readonly>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Tồn cuối kỳ hiện tại</label>
<input type="number" id="borrowCurrentEndingInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" readonly>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Số lượng mượn</label>
<input type="number" id="borrowQuantityInput" min="1" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" value="1" required>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Người mượn</label>
<select id="borrowAssetUserInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required>
<option value="">-- Chon nguoi muon --</option>
</select>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Người xuất</label>
<input type="text" id="borrowByInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" readonly>
</div>
<div class="md:col-span-2">
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Role người thao tác</label>
<input type="text" id="borrowRoleInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" readonly>
</div>
</div>
<div class="flex gap-3 pt-2">
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeBorrowAssetModal()">Hủy</button>
<button type="submit" class="flex-1 px-4 py-2 bg-primary hover:bg-primary-dim text-on-primary rounded-lg text-xs font-bold">Xác nhận mượn</button>
</div>
</form>
</div>
</div>
<!-- View Asset Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="viewAssetModal">
<div class="modal-content w-full max-w-2xl bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4 flex flex-col" style="max-height: calc(100vh - 2rem);">
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between bg-slate-50">
<h3 class="text-base font-extrabold text-slate-900">Chi tiết tài sản</h3>
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeViewAssetModal()">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<div id="assetDetailsContent" class="p-6 grid grid-cols-1 md:grid-cols-2 gap-3 text-sm text-slate-700 overflow-y-auto"></div>
<div class="p-6 pt-3 flex gap-3 border-t border-slate-100 bg-white">
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeViewAssetModal()">Đóng</button>
<button type="button" class="flex-1 px-4 py-2 bg-primary hover:bg-primary-dim text-on-primary rounded-lg text-xs font-bold edit-asset-from-view">Sửa</button>
</div>
</div>
</div>
<!-- Delete Asset Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="deleteAssetModal">
<div class="modal-content w-full max-w-md bg-white rounded-xl shadow-2xl border border-slate-200 overflow-hidden m-4">
<div class="px-6 py-4 border-b border-slate-100 bg-red-50 flex items-center gap-3">
<span class="material-symbols-outlined text-red-600 text-2xl">warning</span>
<h3 class="text-base font-extrabold text-red-700">Xóa tài sản</h3>
</div>
<div class="p-6">
<p class="text-sm text-slate-600 mb-6">Bạn có chắc muốn xóa tài sản <strong id="deleteAssetName">-</strong>? Hành động này không thể hoàn tác.</p>
<div class="flex gap-3">
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeDeleteAssetModal()">Hủy</button>
<button type="button" class="flex-1 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-xs font-bold confirm-delete-asset">Xóa</button>
</div>
</div>
</div>
</div>