Compare commits

...

13 Commits

Author SHA1 Message Date
395b1f6e85 fixx 01 2026-05-06 16:56:58 +07:00
9f14491562 trạng thái 2026-05-06 16:36:12 +07:00
d88aa39bd6 xuất tài sản 2026-05-06 16:04:56 +07:00
8b2a9d7afe import fix 2026-05-06 11:14:10 +07:00
197186eac8 add dự án 2026-04-25 21:34:17 +07:00
8bd67200ce forgot pass 2026-04-25 11:58:09 +07:00
4fb7f412bf fix mượn trả 2026-04-25 09:24:35 +07:00
bc7a484a01 color
Co-authored-by: Copilot <copilot@github.com>
2026-04-24 16:34:14 +07:00
d4800beb67 request 2026-04-24 16:11:00 +07:00
3961514f6c 24/04/2026 - mã tài sản 2026-04-24 13:56:12 +07:00
9526628334 test 2026-04-22 17:18:31 +07:00
6dc2391858 ok 2026-04-22 17:11:01 +07:00
bcc22b1971 ignore .env 2026-04-22 17:10:23 +07:00
11 changed files with 5939 additions and 236 deletions

2
.env
View File

@@ -7,7 +7,7 @@ NODE_ENV=production
APP_PORT=3000
# Image used for server pull deployment
DOCKER_IMAGE=toiiiiday/accmanager:1.0.3
DOCKER_IMAGE=toiiiiday/accmanager:1.2.1
# Container app port
PORT=3000

View File

@@ -58,5 +58,5 @@ He thong da ho tro xac thuc email cho:
Phiên bản tài liệu: 3.0.0
Phiên bản tài liệu: 4.0.0
Cập nhật: Tháng 4 năm 2026

File diff suppressed because it is too large Load Diff

View File

@@ -95,6 +95,8 @@ BEGIN
ImportInPeriod INT NOT NULL DEFAULT 0,
ExportInPeriod INT NOT NULL DEFAULT 0,
EndingBalance INT NOT NULL DEFAULT 0,
NewQuantity INT NOT NULL DEFAULT 0,
UsedQuantity INT NOT NULL DEFAULT 0,
Unit NVARCHAR(50),
Department NVARCHAR(100),
Project NVARCHAR(150),
@@ -124,8 +126,282 @@ BEGIN
ALTER TABLE AssetInventory ADD ExportedBy NVARCHAR(100) NULL;
END
IF COL_LENGTH('dbo.AssetInventory', 'NewQuantity') IS NULL
BEGIN
ALTER TABLE AssetInventory ADD NewQuantity INT NOT NULL CONSTRAINT DF_AssetInventory_NewQuantity DEFAULT(0);
END
IF COL_LENGTH('dbo.AssetInventory', 'UsedQuantity') IS NULL
BEGIN
ALTER TABLE AssetInventory ADD UsedQuantity INT NOT NULL CONSTRAINT DF_AssetInventory_UsedQuantity DEFAULT(0);
END
UPDATE AssetInventory
SET EndingBalance = ISNULL(EndingBalance, ISNULL(Quantity, 0));
UPDATE AssetInventory
SET UsedQuantity = CASE WHEN ISNULL(UsedQuantity, 0) < 0 THEN 0 ELSE ISNULL(UsedQuantity, 0) END;
UPDATE AssetInventory
SET NewQuantity = CASE
WHEN ISNULL(NewQuantity, 0) < 0 THEN 0
ELSE ISNULL(NewQuantity, 0)
END;
UPDATE AssetInventory
SET NewQuantity = CASE
WHEN (ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) < ISNULL(EndingBalance, 0)
THEN ISNULL(NewQuantity, 0) + (ISNULL(EndingBalance, 0) - (ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)))
WHEN (ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) > ISNULL(EndingBalance, 0)
THEN CASE
WHEN ISNULL(NewQuantity, 0) >= ((ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) - ISNULL(EndingBalance, 0))
THEN ISNULL(NewQuantity, 0) - ((ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) - ISNULL(EndingBalance, 0))
ELSE 0
END
ELSE ISNULL(NewQuantity, 0)
END,
UsedQuantity = CASE
WHEN (ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) > ISNULL(EndingBalance, 0)
AND ISNULL(NewQuantity, 0) < ((ISNULL(NewQuantity, 0) + ISNULL(UsedQuantity, 0)) - ISNULL(EndingBalance, 0))
THEN ISNULL(EndingBalance, 0)
ELSE ISNULL(UsedQuantity, 0)
END;
-- ===========================================
-- 5. CREATE AUDIT LOG TABLE
-- 5. CREATE ASSET DEPARTMENTS TABLE
-- ===========================================
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetDepartments')
BEGIN
CREATE TABLE AssetDepartments (
DepartmentId INT PRIMARY KEY IDENTITY(1,1),
DepartmentName NVARCHAR(100) NOT NULL,
CreatedDate DATETIME DEFAULT GETDATE(),
UpdatedDate DATETIME DEFAULT GETDATE()
);
PRINT 'Table AssetDepartments created successfully.';
END
;WITH SourceDepartments AS (
SELECT DISTINCT LTRIM(RTRIM(Department)) AS DepartmentName
FROM AssetInventory
WHERE Department IS NOT NULL
AND LTRIM(RTRIM(Department)) <> ''
)
INSERT INTO AssetDepartments (DepartmentName)
SELECT source.DepartmentName
FROM SourceDepartments source
WHERE NOT EXISTS (
SELECT 1
FROM AssetDepartments target
WHERE LOWER(LTRIM(RTRIM(target.DepartmentName))) = LOWER(source.DepartmentName)
);
-- ===========================================
-- 6. CREATE ASSET PROJECTS TABLE
-- ===========================================
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetProjects')
BEGIN
CREATE TABLE AssetProjects (
ProjectId INT PRIMARY KEY IDENTITY(1,1),
ProjectName NVARCHAR(150) NOT NULL,
CreatedDate DATETIME DEFAULT GETDATE(),
UpdatedDate DATETIME DEFAULT GETDATE()
);
PRINT 'Table AssetProjects created successfully.';
END
-- ===========================================
-- 7. CREATE ASSET BORROW REQUESTS TABLE
-- ===========================================
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetBorrowRequests')
BEGIN
CREATE TABLE AssetBorrowRequests (
BorrowId INT PRIMARY KEY IDENTITY(1,1),
AssetId INT NOT NULL,
RequestType NVARCHAR(20) NOT NULL DEFAULT 'borrow',
RequestStatus NVARCHAR(20) NOT NULL DEFAULT 'pending',
BorrowerName NVARCHAR(100) NOT NULL,
BorrowQuantity INT NOT NULL DEFAULT 1,
Unit NVARCHAR(50),
BorrowDate DATE NOT NULL DEFAULT CAST(GETDATE() AS DATE),
RequestNote NVARCHAR(500) NULL,
RejectReason NVARCHAR(1000) NULL,
CreatedBy INT NULL,
ProcessedBy INT NULL,
ProcessedByName NVARCHAR(100) NULL,
ProcessedDate DATETIME NULL,
CreatedDate DATETIME DEFAULT GETDATE(),
UpdatedDate DATETIME DEFAULT GETDATE(),
FOREIGN KEY (AssetId) REFERENCES AssetInventory(AssetId) ON DELETE CASCADE,
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL,
FOREIGN KEY (ProcessedBy) REFERENCES Users(UserId) ON DELETE SET NULL
);
PRINT 'Table AssetBorrowRequests created successfully.';
END
IF COL_LENGTH('dbo.AssetBorrowRequests', 'Unit') IS NULL
BEGIN
ALTER TABLE AssetBorrowRequests ADD Unit NVARCHAR(50) NULL;
END
IF COL_LENGTH('dbo.AssetBorrowRequests', 'BorrowDate') IS NULL
BEGIN
ALTER TABLE AssetBorrowRequests ADD BorrowDate DATE NOT NULL CONSTRAINT DF_AssetBorrowRequests_BorrowDate DEFAULT(CAST(GETDATE() AS DATE));
END
IF COL_LENGTH('dbo.AssetBorrowRequests', 'UpdatedDate') IS NULL
BEGIN
ALTER TABLE AssetBorrowRequests ADD UpdatedDate DATETIME NOT NULL CONSTRAINT DF_AssetBorrowRequests_UpdatedDate DEFAULT(GETDATE());
END
IF COL_LENGTH('dbo.AssetBorrowRequests', 'RequestType') IS NULL
BEGIN
ALTER TABLE AssetBorrowRequests ADD RequestType NVARCHAR(20) NOT NULL CONSTRAINT DF_AssetBorrowRequests_RequestType DEFAULT('borrow');
END
IF COL_LENGTH('dbo.AssetBorrowRequests', 'RequestStatus') IS NULL
BEGIN
ALTER TABLE AssetBorrowRequests ADD RequestStatus NVARCHAR(20) NOT NULL CONSTRAINT DF_AssetBorrowRequests_RequestStatus DEFAULT('approved');
END
IF COL_LENGTH('dbo.AssetBorrowRequests', 'RequestNote') IS NULL
BEGIN
ALTER TABLE AssetBorrowRequests ADD RequestNote NVARCHAR(500) NULL;
END
IF COL_LENGTH('dbo.AssetBorrowRequests', 'RejectReason') IS NULL
BEGIN
ALTER TABLE AssetBorrowRequests ADD RejectReason NVARCHAR(1000) NULL;
END
IF COL_LENGTH('dbo.AssetBorrowRequests', 'ProcessedBy') IS NULL
BEGIN
ALTER TABLE AssetBorrowRequests ADD ProcessedBy INT NULL;
END
IF COL_LENGTH('dbo.AssetBorrowRequests', 'ProcessedByName') IS NULL
BEGIN
ALTER TABLE AssetBorrowRequests ADD ProcessedByName NVARCHAR(100) NULL;
END
IF COL_LENGTH('dbo.AssetBorrowRequests', 'ProcessedDate') IS NULL
BEGIN
ALTER TABLE AssetBorrowRequests ADD ProcessedDate DATETIME NULL;
END
IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = 'FK_AssetBorrowRequests_ProcessedBy')
BEGIN
ALTER TABLE AssetBorrowRequests
ADD CONSTRAINT FK_AssetBorrowRequests_ProcessedBy
FOREIGN KEY (ProcessedBy) REFERENCES Users(UserId) ON DELETE SET NULL;
END
UPDATE AssetBorrowRequests
SET RequestType = ISNULL(NULLIF(LTRIM(RTRIM(RequestType)), ''), 'borrow');
UPDATE AssetBorrowRequests
SET RequestStatus = ISNULL(NULLIF(LTRIM(RTRIM(RequestStatus)), ''), 'approved');
-- ===========================================
-- 8. CREATE ASSET EXPORT HISTORY TABLE
-- ===========================================
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetExportHistory')
BEGIN
CREATE TABLE AssetExportHistory (
ExportHistoryId INT PRIMARY KEY IDENTITY(1,1),
AssetId INT NOT NULL,
AssetCode NVARCHAR(100) NOT NULL,
AssetName NVARCHAR(255) NOT NULL,
ExportQuantity INT NOT NULL DEFAULT 1,
ProjectName NVARCHAR(150) NULL,
CustodianName NVARCHAR(100) NOT NULL,
ExportedByName NVARCHAR(100) NOT NULL,
ExportNote NVARCHAR(1000) NULL,
CreatedBy INT NULL,
ExportedDate DATETIME NOT NULL DEFAULT GETDATE(),
CreatedDate DATETIME NOT NULL DEFAULT GETDATE(),
UpdatedDate DATETIME NOT NULL DEFAULT GETDATE(),
FOREIGN KEY (AssetId) REFERENCES AssetInventory(AssetId) ON DELETE CASCADE
);
PRINT 'Table AssetExportHistory created successfully.';
END
IF COL_LENGTH('dbo.AssetExportHistory', 'AssetCode') IS NULL
BEGIN
ALTER TABLE AssetExportHistory ADD AssetCode NVARCHAR(100) NULL;
END
IF COL_LENGTH('dbo.AssetExportHistory', 'AssetName') IS NULL
BEGIN
ALTER TABLE AssetExportHistory ADD AssetName NVARCHAR(255) NULL;
END
IF COL_LENGTH('dbo.AssetExportHistory', 'ExportQuantity') IS NULL
BEGIN
ALTER TABLE AssetExportHistory ADD ExportQuantity INT NOT NULL CONSTRAINT DF_AssetExportHistory_ExportQuantity DEFAULT(1);
END
IF COL_LENGTH('dbo.AssetExportHistory', 'ProjectName') IS NULL
BEGIN
ALTER TABLE AssetExportHistory ADD ProjectName NVARCHAR(150) NULL;
END
IF COL_LENGTH('dbo.AssetExportHistory', 'CustodianName') IS NULL
BEGIN
ALTER TABLE AssetExportHistory ADD CustodianName NVARCHAR(100) NULL;
END
IF COL_LENGTH('dbo.AssetExportHistory', 'ExportedByName') IS NULL
BEGIN
ALTER TABLE AssetExportHistory ADD ExportedByName NVARCHAR(100) NULL;
END
IF COL_LENGTH('dbo.AssetExportHistory', 'ExportNote') IS NULL
BEGIN
ALTER TABLE AssetExportHistory ADD ExportNote NVARCHAR(1000) NULL;
END
IF COL_LENGTH('dbo.AssetExportHistory', 'CreatedBy') IS NULL
BEGIN
ALTER TABLE AssetExportHistory ADD CreatedBy INT NULL;
END
IF COL_LENGTH('dbo.AssetExportHistory', 'ExportedDate') IS NULL
BEGIN
ALTER TABLE AssetExportHistory ADD ExportedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_ExportedDate DEFAULT(GETDATE());
END
IF COL_LENGTH('dbo.AssetExportHistory', 'CreatedDate') IS NULL
BEGIN
ALTER TABLE AssetExportHistory ADD CreatedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_CreatedDate DEFAULT(GETDATE());
END
IF COL_LENGTH('dbo.AssetExportHistory', 'UpdatedDate') IS NULL
BEGIN
ALTER TABLE AssetExportHistory ADD UpdatedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_UpdatedDate DEFAULT(GETDATE());
END
IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = 'FK_AssetExportHistory_CreatedBy')
AND COL_LENGTH('dbo.AssetExportHistory', 'CreatedBy') IS NOT NULL
BEGIN
IF NOT EXISTS (
SELECT 1
FROM sys.foreign_key_columns fkc
INNER JOIN sys.columns c
ON c.object_id = fkc.parent_object_id
AND c.column_id = fkc.parent_column_id
WHERE fkc.parent_object_id = OBJECT_ID('dbo.AssetExportHistory')
AND c.name = 'CreatedBy'
)
BEGIN
ALTER TABLE AssetExportHistory
ADD CONSTRAINT FK_AssetExportHistory_CreatedBy
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL;
END
END
-- ===========================================
-- 9. CREATE AUDIT LOG TABLE
-- ===========================================
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AuditLog')
BEGIN
@@ -144,7 +420,7 @@ BEGIN
END
-- ===========================================
-- 6. CREATE INDEXES
-- 10. CREATE INDEXES
-- ===========================================
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Users_Username')
BEGIN
@@ -171,10 +447,55 @@ BEGIN
CREATE INDEX IX_AssetInventory_Status ON AssetInventory(Status);
END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetInventory_Department')
BEGIN
CREATE INDEX IX_AssetInventory_Department ON AssetInventory(Department);
END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'UX_AssetDepartments_DepartmentName')
BEGIN
CREATE UNIQUE INDEX UX_AssetDepartments_DepartmentName ON AssetDepartments(DepartmentName);
END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'UX_AssetProjects_ProjectName')
BEGIN
CREATE UNIQUE INDEX UX_AssetProjects_ProjectName ON AssetProjects(ProjectName);
END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetBorrowRequests_AssetId')
BEGIN
CREATE INDEX IX_AssetBorrowRequests_AssetId ON AssetBorrowRequests(AssetId);
END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetBorrowRequests_BorrowDate')
BEGIN
CREATE INDEX IX_AssetBorrowRequests_BorrowDate ON AssetBorrowRequests(BorrowDate DESC);
END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetBorrowRequests_RequestStatus')
BEGIN
CREATE INDEX IX_AssetBorrowRequests_RequestStatus ON AssetBorrowRequests(RequestStatus);
END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetBorrowRequests_RequestType')
BEGIN
CREATE INDEX IX_AssetBorrowRequests_RequestType ON AssetBorrowRequests(RequestType);
END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetExportHistory_AssetId')
BEGIN
CREATE INDEX IX_AssetExportHistory_AssetId ON AssetExportHistory(AssetId);
END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetExportHistory_ExportedDate')
BEGIN
CREATE INDEX IX_AssetExportHistory_ExportedDate ON AssetExportHistory(ExportedDate DESC);
END
PRINT 'Indexes created successfully.';
-- ===========================================
-- 7. INSERT INITIAL DATA
-- 11. INSERT INITIAL DATA
-- ===========================================
-- Check if admin user exists
@@ -198,7 +519,7 @@ BEGIN
END
-- ===========================================
-- 8. DISPLAY DATABASE INFORMATION
-- 12. DISPLAY DATABASE INFORMATION
-- ===========================================
PRINT '';
PRINT '========================================';

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,7 @@
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Password</label>
<input type="password" id="accountPassword" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required placeholder="••••••••">
<input type="password" id="accountPassword" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required placeholder="********">
</div>
<div class="flex gap-3 pt-4">
<button type="button" class="flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeAccountModal()">Cancel</button>
@@ -59,7 +59,7 @@
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Password</label>
<div class="flex items-center gap-2">
<div id="viewAccountPassword" class="flex-1 border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50 text-slate-600">••••••••</div>
<div id="viewAccountPassword" class="flex-1 border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50 text-slate-600">********</div>
<button type="button" class="p-2 rounded-lg hover:bg-slate-100 text-slate-400 transition-colors toggle-password">
<span class="material-symbols-outlined text-lg" id="toggleIcon">visibility</span>
</button>
@@ -210,21 +210,19 @@
<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">
<label id="assetCodeLabel" 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" placeholder="De trong de he thong tu tao">
<p id="assetCodeHint" class="mt-1 text-xs text-slate-500">Để trống khi thêm mới, hệ thống sẽ tự tạo mã.</p>
<p id="assetCodeError" class="mt-1 text-xs font-semibold text-red-600 hidden"></p>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Tên tài sản</label>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Tên tài sản <span class="text-red-600">*</span></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">
<p id="assetNameError" class="mt-1 text-xs font-semibold text-red-600 hidden"></p>
</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>
<input type="text" id="assetStatusInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" readonly value="Trong kho">
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Model</label>
@@ -256,11 +254,15 @@
</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">
<select id="assetDepartmentInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3">
<option value="">-- Chọn phòng ban --</option>
</select>
</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">
<select id="assetProjectInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3">
<option value="">-- Chọn dự án --</option>
</select>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Vị trí</label>
@@ -297,11 +299,11 @@
</div>
</div>
<!-- Borrow Asset Modal -->
<!-- Export 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>
<h3 class="text-base font-extrabold text-slate-900">Xuất 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>
@@ -318,13 +320,19 @@
<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>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Số lượng xuất</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>
<option value="">-- Chn người mượn --</option>
</select>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Xuất cho dự án</label>
<select id="borrowAssetProjectInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required>
<option value="">-- Chọn dự án --</option>
</select>
</div>
<div>
@@ -335,10 +343,195 @@
<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 class="md:col-span-2">
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Ghi chú xuất</label>
<textarea id="borrowAssetNoteInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 h-20 resize-none" placeholder="Nhập ghi chú (nếu có)"></textarea>
</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>
<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 xuất</button>
</div>
</form>
</div>
</div>
<!-- Asset Export History Modal -->
<div class="modal-backdrop fixed inset-0 z-[110] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetExportHistoryModal" style="z-index: 125;">
<div class="modal-content w-full max-w-6xl 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">Lịch sử xuất tài sản</h3>
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeAssetExportHistoryModal()">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<div class="p-6 pt-4 overflow-auto">
<div class="rounded-xl border border-slate-200 overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse" style="min-width: 1100px;">
<thead class="bg-slate-50 border-b border-slate-200">
<tr>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Ngày giờ</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Tài sản</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Số lượng</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Dự án</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Người phụ trách</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Người xuất</th>
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Ghi chú</th>
</tr>
</thead>
<tbody id="assetExportHistoryTableBody" class="divide-y divide-slate-100">
<tr>
<td colspan="7" class="px-4 py-8 text-sm text-center text-slate-500">Chưa có dữ liệu lịch sử xuất.</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Asset Borrow Request Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetBorrowRequestModal">
<div class="modal-content w-full max-w-lg bg-white rounded-xl shadow-2xl border border-slate-200 overflow-visible m-4">
<div class="px-6 py-4 border-b border-slate-100 flex items-center justify-between bg-slate-50">
<h3 id="assetBorrowRequestModalTitle" class="text-base font-extrabold text-slate-900">Tạo đơn mượn tài sản</h3>
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeAssetBorrowRequestModal()">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<form id="assetBorrowRequestForm" class="p-6 space-y-4 relative">
<input type="hidden" id="assetBorrowRequestTypeInput" value="borrow">
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Tên đầy đủ</label>
<input type="text" id="assetBorrowRequesterInput" 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">Tên tài sản</label>
<div id="assetBorrowProductPicker" class="relative z-[130]">
<input type="hidden" id="assetBorrowProductInput">
<button
type="button"
id="assetBorrowProductDisplayBtn"
class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 text-left flex items-center justify-between gap-2"
>
<span id="assetBorrowProductDisplayText" class="flex-1 min-w-0 truncate text-slate-600">-- Chọn tài sản --</span>
<span class="material-symbols-outlined text-base text-slate-400">expand_more</span>
</button>
<div
id="assetBorrowProductDropdown"
class="hidden absolute left-0 right-0 w-full max-w-full mt-1 bg-white border border-slate-200 rounded-lg shadow-xl z-[140] overflow-hidden"
>
<div class="p-2 border-b border-slate-100">
<input
type="text"
id="assetBorrowProductSearchInput"
class="w-full border border-slate-200 rounded-md text-sm py-2 px-2.5"
placeholder="Tìm theo mã hoặc tên tài sản..."
autocomplete="off"
>
</div>
<div
id="assetBorrowProductList"
class="overflow-auto"
style="max-height: 224px; overflow: auto; overscroll-behavior: contain;"
></div>
<div id="assetBorrowProductLoading" class="hidden px-3 py-2 text-xs text-slate-500 bg-slate-50 border-t border-slate-100">
Đang tải thêm tài sản...
</div>
</div>
</div>
</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">Số lượng</label>
<input type="number" id="assetBorrowQuantityInput" 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">Đơn vị</label>
<input type="text" id="assetBorrowUnitInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 bg-slate-50" readonly>
</div>
</div>
<div>
<label id="assetBorrowDateLabel" class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Ngày mượn</label>
<input type="date" id="assetBorrowDateInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required>
</div>
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Ghi chú</label>
<textarea id="assetBorrowNoteInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 h-20 resize-none" placeholder="Nhập ghi chú (nếu có)"></textarea>
</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="closeAssetBorrowRequestModal()">Hủy</button>
<button type="submit" id="assetBorrowRequestSubmitBtn" class="flex-1 px-4 py-2 bg-primary hover:bg-primary-dim text-on-primary rounded-lg text-xs font-bold">Tạo đơn mượn</button>
</div>
</form>
</div>
</div>
<!-- Pending Asset Requests Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetPendingRequestsModal" style="z-index: 120;">
<div class="modal-content w-full max-w-6xl 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">Đơn chờ xử lý</h3>
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeAssetPendingRequestsModal()">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<div class="p-6 grid grid-cols-1 md:grid-cols-2 gap-4 overflow-y-auto">
<div class="rounded-xl border border-slate-200 overflow-hidden">
<div class="px-4 py-3 bg-blue-50 border-b border-blue-100 flex items-center justify-between">
<h4 class="text-sm font-extrabold text-blue-700">Đơn mượn</h4>
<span id="pendingBorrowCountBadge" class="inline-flex items-center justify-center min-w-[24px] h-6 px-2 rounded-full bg-blue-600 text-white text-xs font-extrabold">0</span>
</div>
<div id="pendingBorrowRequestsList" class="p-3 space-y-3 max-h-[60vh] overflow-y-auto"></div>
</div>
<div class="rounded-xl border border-slate-200 overflow-hidden">
<div class="px-4 py-3 bg-emerald-50 border-b border-emerald-100 flex items-center justify-between">
<h4 class="text-sm font-extrabold text-emerald-700">Đơn trả</h4>
<span id="pendingReturnCountBadge" class="inline-flex items-center justify-center min-w-[24px] h-6 px-2 rounded-full bg-emerald-600 text-white text-xs font-extrabold">0</span>
</div>
<div id="pendingReturnRequestsList" class="p-3 space-y-3 max-h-[60vh] overflow-y-auto"></div>
</div>
</div>
</div>
</div>
<!-- Confirm Delete/Cancel Asset Request Modal -->
<div class="modal-backdrop fixed inset-0 z-[120] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetRequestDeleteConfirmModal" style="z-index: 140;">
<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ác nhận thao tác</h3>
</div>
<div class="p-6">
<p id="assetRequestDeleteConfirmMessage" class="text-sm text-slate-600 mb-6">Bạn có chắc muốn thực hiện thao tác này?</p>
<div class="flex gap-3">
<button type="button" class="cancel-asset-request-delete-confirm flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg">Đóng</button>
<button type="button" id="confirmAssetRequestDeleteBtn" class="flex-1 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-xs font-bold">Xóa đơn</button>
</div>
</div>
</div>
</div>
<!-- Reject Asset Request Modal -->
<div class="modal-backdrop fixed inset-0 z-[110] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetRequestRejectModal" style="z-index: 130;">
<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">Từ chối đơn</h3>
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeAssetRequestRejectModal()">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<form id="assetRequestRejectForm" class="p-6 space-y-4">
<input type="hidden" id="assetRequestRejectIdInput">
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Lý do từ chối</label>
<textarea id="assetRequestRejectReasonInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 h-24 resize-none" placeholder="Nhập lý do từ chối..." required></textarea>
</div>
<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="closeAssetRequestRejectModal()">Hủy</button>
<button type="submit" class="flex-1 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-xs font-bold">Xác nhận từ chối</button>
</div>
</form>
</div>
@@ -377,3 +570,101 @@
</div>
</div>
</div>
<!-- Bulk Delete Assets Confirm Modal -->
<div class="modal-backdrop fixed inset-0 z-[110] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="bulkDeleteAssetsConfirmModal" style="z-index: 130;">
<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 đã chọn</h3>
</div>
<div class="p-6">
<p id="bulkDeleteAssetsConfirmMessage" class="text-sm text-slate-600 mb-6">Bạn có chắc muốn xóa các tài sản đã chọn?</p>
<div class="mb-4 rounded-lg border border-red-100 bg-red-50 px-3 py-2 text-xs text-red-700">
Số lượng: <strong id="bulkDeleteAssetsConfirmCount">0</strong> tài sản
</div>
<div class="flex gap-3">
<button type="button" class="cancel-bulk-asset-delete-confirm flex-1 px-4 py-2 text-xs font-bold text-slate-600 border border-slate-200 rounded-lg" onclick="closeBulkDeleteAssetsConfirmModal()">Hủy</button>
<button type="button" id="confirmBulkAssetDeleteBtn" class="flex-1 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-xs font-bold">Xóa</button>
</div>
</div>
</div>
</div>
<!-- Add/Edit Asset Department Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetDepartmentModal">
<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 flex items-center justify-between bg-slate-50">
<h3 class="text-base font-extrabold text-slate-900" id="assetDepartmentModalTitle">Thêm phòng ban</h3>
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeAssetDepartmentModal()">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<form id="assetDepartmentForm" class="p-6 space-y-4">
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Tên phòng ban</label>
<input type="text" id="assetDepartmentNameInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required placeholder="Ví dụ: Kỹ thuật">
</div>
<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="closeAssetDepartmentModal()">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</button>
</div>
</form>
</div>
</div>
<!-- Delete Asset Department Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="deleteAssetDepartmentModal">
<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 phòng ban</h3>
</div>
<div class="p-6">
<p class="text-sm text-slate-600 mb-6">Bạn có chắc muốn xóa phòng ban <strong id="deleteAssetDepartmentName">-</strong>? Các tài sản đang gắn phòng ban này sẽ được để trống phòng ban.</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="closeDeleteAssetDepartmentModal()">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-department">Xóa</button>
</div>
</div>
</div>
</div>
<!-- Add/Edit Asset Project Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetProjectModal">
<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 flex items-center justify-between bg-slate-50">
<h3 class="text-base font-extrabold text-slate-900" id="assetProjectModalTitle">Thêm dự án</h3>
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeAssetProjectModal()">
<span class="material-symbols-outlined">close</span>
</button>
</div>
<form id="assetProjectForm" class="p-6 space-y-4">
<div>
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Tên dự án</label>
<input type="text" id="assetProjectNameInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required placeholder="Ví dụ: AGV">
</div>
<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="closeAssetProjectModal()">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</button>
</div>
</form>
</div>
</div>
<!-- Delete Asset Project Modal -->
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="deleteAssetProjectModal">
<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 dự á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 dự án <strong id="deleteAssetProjectName">-</strong>? Các tài sản đang gắn dự án này sẽ được để trống dự án.</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="closeDeleteAssetProjectModal()">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-project">Xóa</button>
</div>
</div>
</div>
</div>

View File

@@ -240,6 +240,18 @@
<span class="material-symbols-outlined">inventory_2</span>
<span>Tài sản</span>
</a>
<a href="#asset-borrows" data-nav="asset-borrows" class="flex items-center gap-3 px-3 py-2 text-slate-600 dark:text-slate-400 hover:text-slate-900 hover:bg-slate-200/50 transition-all group cursor-pointer rounded-r-lg">
<span class="material-symbols-outlined">assignment_returned</span>
<span>Mượn/Trả tài sản</span>
</a>
<a href="#asset-departments" data-nav="asset-departments" class="flex items-center gap-3 px-3 py-2 text-slate-600 dark:text-slate-400 hover:text-slate-900 hover:bg-slate-200/50 transition-all group cursor-pointer rounded-r-lg">
<span class="material-symbols-outlined">apartment</span>
<span> Phòng Ban</span>
</a>
<a href="#asset-projects" data-nav="asset-projects" class="flex items-center gap-3 px-3 py-2 text-slate-600 dark:text-slate-400 hover:text-slate-900 hover:bg-slate-200/50 transition-all group cursor-pointer rounded-r-lg">
<span class="material-symbols-outlined">workspaces</span>
<span>Dự án</span>
</a>
</div>
</div>
@@ -269,6 +281,11 @@
</button>
</div>
<div class="topbar-actions flex items-center gap-4">
<button id="pendingAssetRequestsBtn" type="button" class="hidden relative flex items-center gap-2 px-3 py-2 rounded-lg border border-amber-200 bg-amber-50 hover:bg-amber-100 text-amber-800 transition-colors" title="Đơn chờ xử lý">
<span class="material-symbols-outlined text-base">notifications_active</span>
<span class="text-xs font-bold">Đơn chờ</span>
<span id="pendingAssetRequestsBadge" class="hidden absolute -top-2.5 -right-2.5 min-w-[22px] h-[22px] px-1.5 rounded-[999px] bg-red-600 text-white text-xs font-extrabold leading-[22px] text-center ring-2 ring-white">0</span>
</button>
<button id="profileBtn" type="button" class="profile-btn flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 transition-colors" title="Sửa hồ sơ">
<span class="material-symbols-outlined text-slate-600 dark:text-slate-400">account_circle</span>
<div class="profile-meta flex flex-col">
@@ -288,6 +305,6 @@
</div>
</main>
<script src="../js/app.js?v=20260421-4"></script>
<script src="../js/app.js?v=20260424-1"></script>
</body>
</html>

View File

@@ -42,7 +42,7 @@
<p class="text-xs uppercase tracking-widest text-on-surface-variant font-bold mt-2">Account Management System</p>
</div>
<div class="flex gap-2 mb-6" role="tablist" aria-label="Auth switcher">
<div id="authTabs" class="flex gap-2 mb-6" role="tablist" aria-label="Auth switcher">
<button id="loginTab" type="button" class="flex-1 py-2.5 rounded-lg text-xs font-bold uppercase tracking-wider border border-outline-variant/30 bg-primary text-on-primary shadow-sm">Đăng nhập</button>
<button id="registerTab" type="button" class="flex-1 py-2.5 rounded-lg text-xs font-bold uppercase tracking-wider border border-outline-variant/30 bg-surface-container-low text-on-surface-variant">Đăng ký</button>
</div>
@@ -86,6 +86,7 @@
</div>
<!-- Remember Me Checkbox -->
<div class="flex items-center justify-between gap-2">
<div class="flex items-center">
<input
type="checkbox"
@@ -95,6 +96,10 @@
/>
<label for="remember" class="ml-2.5 text-xs font-medium text-on-surface-variant cursor-pointer">Remember me</label>
</div>
<button id="forgotPasswordLink" type="button" class="text-xs font-semibold text-primary hover:text-primary-dim transition-colors bg-transparent border-0 p-0">
Forgot password?
</button>
</div>
<!-- Login Button -->
<button
@@ -117,6 +122,115 @@
</div>
</form>
<form id="forgotPasswordForm" class="space-y-5 hidden">
<div class="bg-blue-50 border border-blue-100 rounded-lg px-4 py-3 text-xs text-blue-700">
Enter your username and registered email to receive a password reset link.
</div>
<div>
<label for="forgotUsername" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">Username</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
<span class="material-symbols-outlined text-base">person</span>
</span>
<input
type="text"
id="forgotUsername"
name="forgotUsername"
placeholder="Enter your username"
required
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
/>
</div>
</div>
<div>
<label for="forgotEmail" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">Registered Email</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
<span class="material-symbols-outlined text-base">mail</span>
</span>
<input
type="email"
id="forgotEmail"
name="forgotEmail"
placeholder="Enter your registered email"
required
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
/>
</div>
</div>
<button
type="submit"
class="w-full bg-primary hover:bg-primary-dim text-on-primary font-bold py-2.5 px-4 rounded-lg transition-all active:scale-95 duration-100 flex items-center justify-center gap-2 mt-4"
>
<span class="material-symbols-outlined text-sm">mark_email_unread</span>
<span>Send reset email</span>
</button>
<button id="forgotBackToLoginBtn" type="button" class="w-full bg-surface-container-low hover:bg-slate-200 text-on-surface-variant font-semibold py-2.5 px-4 rounded-lg transition-colors">
Back to sign in
</button>
<div id="forgotPasswordErrorMessage" class="hidden bg-error-container/20 text-error/80 border border-error/30 rounded-lg px-4 py-3 text-xs font-medium"></div>
<div id="forgotPasswordSuccessMessage" class="hidden bg-green-50 text-green-800 border border-green-200 rounded-lg px-4 py-3 text-xs font-medium"></div>
</form>
<form id="resetPasswordForm" class="space-y-5 hidden">
<div class="bg-blue-50 border border-blue-100 rounded-lg px-4 py-3 text-xs text-blue-700">
Set your new password below.
</div>
<div>
<label for="resetPassword" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">New Password</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
<span class="material-symbols-outlined text-base">lock_reset</span>
</span>
<input
type="password"
id="resetPassword"
name="resetPassword"
placeholder="Enter a new password"
required
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
/>
</div>
</div>
<div>
<label for="resetConfirmPassword" class="block text-xs font-bold text-on-surface-variant uppercase tracking-wider mb-2">Confirm New Password</label>
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-on-surface-variant/60">
<span class="material-symbols-outlined text-base">verified_user</span>
</span>
<input
type="password"
id="resetConfirmPassword"
name="resetConfirmPassword"
placeholder="Re-enter new password"
required
class="w-full pl-10 pr-4 py-2.5 bg-surface-container-low border border-outline-variant/30 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent transition-all text-sm font-medium"
/>
</div>
</div>
<button
type="submit"
class="w-full bg-primary hover:bg-primary-dim text-on-primary font-bold py-2.5 px-4 rounded-lg transition-all active:scale-95 duration-100 flex items-center justify-center gap-2 mt-4"
>
<span class="material-symbols-outlined text-sm">password</span>
<span>Reset password</span>
</button>
<button id="resetBackToLoginBtn" type="button" class="w-full bg-surface-container-low hover:bg-slate-200 text-on-surface-variant font-semibold py-2.5 px-4 rounded-lg transition-colors">
Back to sign in
</button>
<div id="resetPasswordErrorMessage" class="hidden bg-error-container/20 text-error/80 border border-error/30 rounded-lg px-4 py-3 text-xs font-medium"></div>
</form>
<!-- Register Form -->
<form id="registerForm" class="space-y-5 hidden">
<div>
@@ -211,12 +325,27 @@
</div>
<script>
// Simple login functionality
const authTabs = document.getElementById('authTabs');
const loginForm = document.getElementById('loginForm');
const errorMessage = document.getElementById('errorMessage');
const usernameInput = document.getElementById('username');
const passwordInput = document.getElementById('password');
const rememberCheckbox = document.getElementById('remember');
const forgotPasswordLink = document.getElementById('forgotPasswordLink');
const forgotPasswordForm = document.getElementById('forgotPasswordForm');
const forgotUsernameInput = document.getElementById('forgotUsername');
const forgotEmailInput = document.getElementById('forgotEmail');
const forgotBackToLoginBtn = document.getElementById('forgotBackToLoginBtn');
const forgotPasswordErrorMessage = document.getElementById('forgotPasswordErrorMessage');
const forgotPasswordSuccessMessage = document.getElementById('forgotPasswordSuccessMessage');
const resetPasswordForm = document.getElementById('resetPasswordForm');
const resetPasswordInput = document.getElementById('resetPassword');
const resetConfirmPasswordInput = document.getElementById('resetConfirmPassword');
const resetBackToLoginBtn = document.getElementById('resetBackToLoginBtn');
const resetPasswordErrorMessage = document.getElementById('resetPasswordErrorMessage');
const registerForm = document.getElementById('registerForm');
const registerErrorMessage = document.getElementById('registerErrorMessage');
const registerSuccessMessage = document.getElementById('registerSuccessMessage');
@@ -229,19 +358,31 @@
const regPasswordInput = document.getElementById('regPassword');
const loginTab = document.getElementById('loginTab');
const registerTab = document.getElementById('registerTab');
let pendingVerificationIdentifier = '';
let currentMode = 'login';
let resetToken = '';
const setMode = (mode) => {
currentMode = mode;
const isLogin = mode === 'login';
const isRegister = mode === 'register';
const isForgot = mode === 'forgot';
const isReset = mode === 'reset';
loginForm.classList.toggle('hidden', !isLogin);
registerForm.classList.toggle('hidden', isLogin);
registerForm.classList.toggle('hidden', !isRegister);
forgotPasswordForm.classList.toggle('hidden', !isForgot);
resetPasswordForm.classList.toggle('hidden', !isReset);
authTabs.classList.toggle('hidden', isReset);
errorMessage.classList.add('hidden');
registerErrorMessage.classList.add('hidden');
registerSuccessMessage.classList.add('hidden');
verifyNotice.classList.add('hidden');
forgotPasswordErrorMessage.classList.add('hidden');
forgotPasswordSuccessMessage.classList.add('hidden');
resetPasswordErrorMessage.classList.add('hidden');
const activate = (btn, active) => {
btn.classList.toggle('bg-primary', active);
@@ -251,29 +392,88 @@
btn.classList.toggle('text-on-surface-variant', !active);
};
activate(loginTab, isLogin);
activate(registerTab, !isLogin);
activate(loginTab, !isRegister);
activate(registerTab, isRegister);
};
// Check if already logged in
document.addEventListener('DOMContentLoaded', () => {
const currentUser = localStorage.getItem('currentUser');
if (currentUser) {
window.location.href = './index.html';
const clearResetQuery = () => {
const cleanUrl = `${window.location.origin}${window.location.pathname}`;
window.history.replaceState({}, document.title, cleanUrl);
};
const getInitialMode = () => {
const params = new URLSearchParams(window.location.search);
const mode = String(params.get('mode') || '').trim().toLowerCase();
const token = String(params.get('token') || '').trim();
if (mode === 'reset-password') {
resetToken = token;
return token ? 'reset' : 'forgot';
}
setMode('login');
if (mode === 'forgot-password') {
return 'forgot';
}
return 'login';
};
document.addEventListener('DOMContentLoaded', () => {
const initialMode = getInitialMode();
const currentUser = localStorage.getItem('currentUser');
if (currentUser && initialMode !== 'reset') {
window.location.href = './index.html';
return;
}
setMode(initialMode);
if (initialMode === 'forgot') {
forgotUsernameInput.value = usernameInput.value.trim();
}
if (initialMode === 'reset' && !resetToken) {
setMode('forgot');
forgotPasswordErrorMessage.textContent = 'Reset link is invalid. Please request a new password reset email.';
forgotPasswordErrorMessage.classList.remove('hidden');
}
// Restore remembered username
const rememberedUsername = localStorage.getItem('rememberedUsername');
if (rememberedUsername) {
usernameInput.value = rememberedUsername;
rememberCheckbox.checked = true;
if (!forgotUsernameInput.value) {
forgotUsernameInput.value = rememberedUsername;
}
}
});
loginTab.addEventListener('click', () => setMode('login'));
registerTab.addEventListener('click', () => setMode('register'));
loginTab.addEventListener('click', () => {
clearResetQuery();
setMode('login');
});
registerTab.addEventListener('click', () => {
clearResetQuery();
setMode('register');
});
forgotPasswordLink.addEventListener('click', () => {
setMode('forgot');
forgotUsernameInput.value = usernameInput.value.trim();
forgotEmailInput.focus();
});
forgotBackToLoginBtn.addEventListener('click', () => {
clearResetQuery();
setMode('login');
});
resetBackToLoginBtn.addEventListener('click', () => {
resetToken = '';
clearResetQuery();
setMode('login');
});
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
@@ -284,7 +484,6 @@
const password = passwordInput.value;
try {
// Call backend login API
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
@@ -296,24 +495,20 @@
const data = await response.json();
if (data.success && data.user) {
// Store user info from backend
localStorage.setItem('currentUser', JSON.stringify(data.user));
// Handle remember me
if (rememberCheckbox.checked) {
localStorage.setItem('rememberedUsername', username);
} else {
localStorage.removeItem('rememberedUsername');
}
// Redirect to dashboard
window.location.href = './index.html';
} else if (data.requiresEmailVerification) {
pendingVerificationIdentifier = data.username || data.email || username;
verifyNoticeText.textContent = data.message || 'Please confirm your email before signing in';
verifyNotice.classList.remove('hidden');
} else {
// Show error
errorMessage.textContent = data.message || 'Invalid username or password';
errorMessage.classList.remove('hidden');
passwordInput.value = '';
@@ -326,6 +521,108 @@
}
});
forgotPasswordForm.addEventListener('submit', async (e) => {
e.preventDefault();
forgotPasswordErrorMessage.classList.add('hidden');
forgotPasswordSuccessMessage.classList.add('hidden');
const payload = {
username: forgotUsernameInput.value.trim(),
email: forgotEmailInput.value.trim()
};
if (!payload.username || !payload.email) {
forgotPasswordErrorMessage.textContent = 'Username and email are required.';
forgotPasswordErrorMessage.classList.remove('hidden');
return;
}
try {
const response = await fetch('/api/auth/forgot-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
const data = await response.json();
if (response.ok && data?.success) {
const lines = [data.message || 'If the account exists, a password reset email has been sent.'];
if (data.resetPreviewUrl) {
lines.push(`Development reset link: ${data.resetPreviewUrl}`);
}
forgotPasswordSuccessMessage.textContent = lines.join(' ');
forgotPasswordSuccessMessage.classList.remove('hidden');
} else {
forgotPasswordErrorMessage.textContent = data?.message || 'Forgot password request failed.';
forgotPasswordErrorMessage.classList.remove('hidden');
}
} catch (error) {
forgotPasswordErrorMessage.textContent = 'Connection error. Please try again.';
forgotPasswordErrorMessage.classList.remove('hidden');
console.error('Forgot password error:', error);
}
});
resetPasswordForm.addEventListener('submit', async (e) => {
e.preventDefault();
resetPasswordErrorMessage.classList.add('hidden');
const newPassword = resetPasswordInput.value;
const confirmPassword = resetConfirmPasswordInput.value;
if (!resetToken) {
resetPasswordErrorMessage.textContent = 'Reset link is invalid. Please request a new one.';
resetPasswordErrorMessage.classList.remove('hidden');
return;
}
if (!newPassword || newPassword.length < 6) {
resetPasswordErrorMessage.textContent = 'New password must be at least 6 characters.';
resetPasswordErrorMessage.classList.remove('hidden');
return;
}
if (newPassword !== confirmPassword) {
resetPasswordErrorMessage.textContent = 'Confirm password does not match.';
resetPasswordErrorMessage.classList.remove('hidden');
return;
}
try {
const response = await fetch('/api/auth/reset-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
token: resetToken,
newPassword
})
});
const data = await response.json();
if (response.ok && data?.success) {
resetToken = '';
resetPasswordInput.value = '';
resetConfirmPasswordInput.value = '';
clearResetQuery();
setMode('login');
verifyNoticeText.textContent = data.message || 'Password reset successful. Please sign in.';
verifyNotice.classList.remove('hidden');
passwordInput.focus();
} else {
resetPasswordErrorMessage.textContent = data?.message || 'Password reset failed.';
resetPasswordErrorMessage.classList.remove('hidden');
}
} catch (error) {
resetPasswordErrorMessage.textContent = 'Connection error. Please try again.';
resetPasswordErrorMessage.classList.remove('hidden');
console.error('Reset password error:', error);
}
});
registerForm.addEventListener('submit', async (e) => {
e.preventDefault();
registerErrorMessage.classList.add('hidden');

BIN
tmp-import-source.xls Normal file

Binary file not shown.

BIN
tmp-user-import.xls Normal file

Binary file not shown.