Compare commits
11 Commits
b9f2626f40
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 395b1f6e85 | |||
| 9f14491562 | |||
| d88aa39bd6 | |||
| 8b2a9d7afe | |||
| 197186eac8 | |||
| 8bd67200ce | |||
| 4fb7f412bf | |||
| bc7a484a01 | |||
| d4800beb67 | |||
| 3961514f6c | |||
| 9526628334 |
@@ -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
|
||||
|
||||
2191
backend/server.js
2191
backend/server.js
File diff suppressed because it is too large
Load Diff
@@ -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
2952
public/js/app.js
2952
public/js/app.js
File diff suppressed because it is too large
Load Diff
@@ -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="">-- Chọn 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
BIN
tmp-import-source.xls
Normal file
Binary file not shown.
BIN
tmp-user-import.xls
Normal file
BIN
tmp-user-import.xls
Normal file
Binary file not shown.
Reference in New Issue
Block a user