Compare commits

..

23 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
61c4415ff7 push 2026-04-22 16:59:13 +07:00
1408294922 import 2026-04-22 16:57:48 +07:00
5f3e38a3d4 Update README.md 2026-04-20 08:32:13 +00:00
a8933011dd delete 2026-04-20 08:31:37 +00:00
aada849671 update day 2026-04-20 15:30:56 +07:00
3b813a85da sercurity 2026-04-20 15:27:27 +07:00
30e326f92a role 2026-04-20 13:45:29 +07:00
ee10a7e480 sercurity 2026-04-03 09:06:38 +07:00
e59785e06b readme 2026-04-02 15:45:27 +07:00
27c2a4c51e reponsive 2026-04-02 15:41:23 +07:00
14 changed files with 10246 additions and 209 deletions

11
.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
@@ -23,3 +23,12 @@ DB_CONNECT_TIMEOUT=30000
# Security
BCRYPT_ROUNDS=12
# Email Verification (SMTP)
APP_BASE_URL=https://accrobot.pnkr.asia/
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=robotics.gitlab.2fa@gmail.com
SMTP_PASS=jaljalhreomqruqo
SMTP_FROM=robotics.gitlab.2fa@gmail.com

View File

@@ -146,9 +146,6 @@ docker compose -f docker-compose.image.yml ps
# Xem log (tìm "Server running")
docker compose -f docker-compose.image.yml logs --tail=50 accmanager
# Test HTTP
curl -I http://127.0.0.1:3000
```
### Nếu gặp lỗi
@@ -174,15 +171,15 @@ curl -I http://127.0.0.1:3000
1. Chọn version mới (ví dụ `1.0.3`)
2. Build:
```powershell
docker build -t toiiiiday/accmanager:1.0.3 .
docker build -t toiiiiday/accmanager:"version" .
```
3. Push:
```powershell
docker push toiiiiday/accmanager:1.0.3
docker push toiiiiday/accmanager:"version"
```
4. Cập nhật `.env`:
```env
DOCKER_IMAGE=toiiiiday/accmanager:1.0.3
DOCKER_IMAGE=toiiiiday/accmanager:"version"
```
5. Copy `.env` lên server:
```powershell
@@ -244,25 +241,7 @@ docker compose --env-file .env -f docker-compose.image.yml up -d accmanager
- Mở inbound port 80, 443 từ Internet
- Port 3000 chỉ nội bộ (không public)
### Trong Nginx Proxy Manager
1. **Add Proxy Host**
- Domain Names: `pnkr.asia` (domain của bạn)
- Scheme: `http`
- Forward Hostname/IP: `172.20.235.176`
- Forward Port: `3000`
- Websocket Support: ON
- Block Common Exploits: ON
2. **SSL**
- Request a new SSL Certificate (Let's Encrypt)
- Force SSL: ON
- HTTP/2 Support: ON
- HSTS: ON (sau khi test ổn định)
3. **Access**
- http://pnkr.asia → auto redirect sang https
- https://pnkr.asia ✅
---

View File

@@ -56,20 +56,7 @@ He thong da ho tro xac thuc email cho:
- Dang ky tai khoan moi.
- Doi email trong phan ho so ca nhan.
Can cau hinh cac bien moi truong SMTP sau:
- `APP_BASE_URL`: URL truy cap he thong (vi du `http://localhost:3000` hoac domain production).
- `SMTP_HOST`: Host SMTP (vi du `smtp.gmail.com`).
- `SMTP_PORT`: Port SMTP (thuong la `587` cho TLS hoac `465` cho SSL).
- `SMTP_SECURE`: `true` neu dung SSL (port 465), nguoc lai `false`.
- `SMTP_USER`: Tai khoan gui email.
- `SMTP_PASS`: Mat khau app/email.
- `SMTP_FROM`: Dia chi hien thi nguoi gui (co the de trong de dung `SMTP_USER`).
- `EMAIL_VERIFY_TOKEN_TTL_MINUTES`: So phut het han link xac thuc (mac dinh 30).
Neu chua cau hinh SMTP, he thong van tao tai khoan nhung se tra ve link xac thuc dang preview cho moi truong dev.
---
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

@@ -81,7 +81,327 @@ BEGIN
END
-- ===========================================
-- 4. CREATE AUDIT LOG TABLE
-- 4. CREATE ASSET INVENTORY TABLE
-- ===========================================
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetInventory')
BEGIN
CREATE TABLE AssetInventory (
AssetId INT PRIMARY KEY IDENTITY(1,1),
AssetCode NVARCHAR(100) NOT NULL UNIQUE,
AssetName NVARCHAR(255) NOT NULL,
Model NVARCHAR(255),
SerialNumber NVARCHAR(100),
Quantity INT NOT NULL DEFAULT 0,
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),
Location NVARCHAR(150),
Custodian NVARCHAR(100),
Borrower NVARCHAR(255),
ExportedBy NVARCHAR(100),
PurchaseDate DATE NULL,
PurchasePrice DECIMAL(18,2) NULL,
Status NVARCHAR(30) NOT NULL DEFAULT 'in_use',
Notes NVARCHAR(MAX),
CreatedBy INT NULL,
CreatedDate DATETIME DEFAULT GETDATE(),
UpdatedDate DATETIME DEFAULT GETDATE(),
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL
);
PRINT 'Table AssetInventory created successfully.';
END
IF COL_LENGTH('dbo.AssetInventory', 'Borrower') IS NULL
BEGIN
ALTER TABLE AssetInventory ADD Borrower NVARCHAR(255) NULL;
END
IF COL_LENGTH('dbo.AssetInventory', 'ExportedBy') IS NULL
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 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
@@ -100,7 +420,7 @@ BEGIN
END
-- ===========================================
-- 5. CREATE INDEXES
-- 10. CREATE INDEXES
-- ===========================================
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Users_Username')
BEGIN
@@ -117,10 +437,65 @@ BEGIN
CREATE INDEX IX_Accounts_AppId ON Accounts(AppId);
END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetInventory_AssetCode')
BEGIN
CREATE INDEX IX_AssetInventory_AssetCode ON AssetInventory(AssetCode);
END
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetInventory_Status')
BEGIN
CREATE INDEX IX_AssetInventory_Status ON AssetInventory(Status);
END
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.';
-- ===========================================
-- 6. INSERT INITIAL DATA
-- 11. INSERT INITIAL DATA
-- ===========================================
-- Check if admin user exists
@@ -144,7 +519,7 @@ BEGIN
END
-- ===========================================
-- 7. DISPLAY DATABASE INFORMATION
-- 12. DISPLAY DATABASE INFORMATION
-- ===========================================
PRINT '';
PRINT '========================================';

178
package-lock.json generated
View File

@@ -14,7 +14,9 @@
"dotenv": "^16.3.1",
"express": "^4.18.2",
"mssql": "^9.1.1",
"nodemailer": "^8.0.4"
"multer": "^2.1.1",
"nodemailer": "^8.0.4",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@tailwindcss/container-queries": "^0.1.1",
@@ -569,6 +571,15 @@
"node": ">= 0.6"
}
},
"node_modules/adler-32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
@@ -599,6 +610,12 @@
"node": ">= 8"
}
},
"node_modules/append-field": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
"license": "MIT"
},
"node_modules/arg": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
@@ -906,6 +923,23 @@
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"license": "MIT"
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -993,6 +1027,19 @@
],
"license": "CC-BY-4.0"
},
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"crc-32": "~1.2.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -1018,6 +1065,15 @@
"fsevents": "~2.3.2"
}
},
"node_modules/codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/commander": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
@@ -1027,6 +1083,21 @@
"node": ">=16"
}
},
"node_modules/concat-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
"engines": [
"node >= 6.0"
],
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.0.2",
"typedarray": "^0.0.6"
}
},
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
@@ -1080,6 +1151,18 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"license": "Apache-2.0",
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -1600,6 +1683,15 @@
"node": ">= 0.6"
}
},
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@@ -2743,6 +2835,25 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/multer": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz",
"integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==",
"license": "MIT",
"dependencies": {
"append-field": "^1.0.0",
"busboy": "^1.6.0",
"concat-stream": "^2.0.0",
"type-is": "^1.6.18"
},
"engines": {
"node": ">= 10.16.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/mz": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
@@ -3769,6 +3880,18 @@
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
"license": "BSD-3-Clause"
},
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
"license": "Apache-2.0",
"dependencies": {
"frac": "~1.1.2"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@@ -3801,6 +3924,14 @@
"npm": ">=6"
}
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -4223,6 +4354,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT"
},
"node_modules/unbox-primitive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
@@ -4406,6 +4543,45 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"cfb": "~1.2.1",
"codepage": "~1.15.0",
"crc-32": "~1.2.1",
"ssf": "~0.11.2",
"wmf": "~1.0.1",
"word": "~0.3.0"
},
"bin": {
"xlsx": "bin/xlsx.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/yaml": {
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",

View File

@@ -23,7 +23,9 @@
"dotenv": "^16.3.1",
"express": "^4.18.2",
"mssql": "^9.1.1",
"nodemailer": "^8.0.4"
"multer": "^2.1.1",
"nodemailer": "^8.0.4",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@tailwindcss/container-queries": "^0.1.1",

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

@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Robot Manager Account - Account Management System</title>
<title>Robot Manager Account - Hệ thống quản lý</title>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet"/>
<!-- Material Symbols -->
@@ -12,6 +12,7 @@
<!-- Notiflix Notify -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/notiflix@3.2.7/dist/notiflix-3.2.7.min.css" />
<script src="https://cdn.jsdelivr.net/npm/notiflix@3.2.7/dist/notiflix-aio-3.2.7.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
<style>
.material-symbols-outlined {
font-family: 'Material Symbols Outlined';
@@ -43,11 +44,169 @@
.modal-backdrop.open .modal-content {
transform: scale(1);
}
.tree-label {
letter-spacing: 0.08em;
text-transform: uppercase;
font-size: 10px;
font-weight: 800;
color: #64748b;
padding: 0 0.75rem;
margin-bottom: 0.35rem;
}
.tree-branch {
margin-left: 0.75rem;
border-left: 1px dashed rgba(100, 116, 139, 0.5);
padding-left: 0.6rem;
display: flex;
flex-direction: column;
gap: 0.2rem;
}
body.app-shell {
overflow: hidden;
}
#mobileMenuBtn,
#sidebarBackdrop {
display: none;
}
@media (max-width: 900px) {
body.app-shell {
width: 100%;
min-height: 100dvh;
height: 100dvh;
overflow: hidden;
position: relative;
}
#mobileMenuBtn {
display: inline-flex;
align-items: center;
justify-content: center;
}
#sidebarBackdrop {
display: block;
position: fixed;
inset: 0;
z-index: 70;
background: rgba(15, 23, 42, 0.45);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease-in-out;
}
#appSidebar {
position: fixed;
inset: 0 auto 0 0;
height: 100dvh;
width: min(82vw, 16rem);
z-index: 80;
transform: translateX(-100%);
transition: transform 0.2s ease-in-out;
box-shadow: 0 20px 45px rgba(15, 23, 42, 0.35);
}
body.mobile-nav-open #sidebarBackdrop {
opacity: 1;
pointer-events: auto;
}
body.mobile-nav-open #appSidebar {
transform: translateX(0);
}
#appMain {
width: 100%;
min-width: 0;
height: 100dvh;
}
#appMain > header {
padding-left: 0.75rem;
padding-right: 0.75rem;
gap: 0.5rem;
}
.topbar-actions {
gap: 0.5rem;
}
#profileBtn {
padding: 0.4rem 0.55rem;
}
#profileBtn .profile-meta {
display: none;
}
.dashboard-header,
.page-header,
.page-filters,
.users-controls {
flex-direction: column;
align-items: stretch;
gap: 0.75rem;
}
.page-filters > div,
.users-controls > div,
.users-controls select {
width: 100%;
}
.dashboard-actions,
.page-header button,
.page-header > button {
width: 100%;
}
.dashboard-stats,
.apps-stats {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.table-wrap {
overflow-x: auto;
}
.table-wrap table {
min-width: 700px;
}
.page-pager {
gap: 0.5rem;
flex-direction: column;
align-items: flex-start;
}
.modal-backdrop {
align-items: flex-end;
}
.modal-backdrop .modal-content {
width: calc(100% - 1rem);
max-height: min(88dvh, 700px);
margin: 0.5rem;
overflow-y: auto;
border-radius: 0.9rem;
}
}
@media (max-width: 560px) {
.dashboard-stats,
.apps-stats {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body class="bg-background text-on-surface antialiased flex h-screen w-screen">
<body class="app-shell bg-background text-on-surface antialiased flex h-screen w-screen">
<!-- SideNavBar -->
<aside class="h-screen w-56 flex flex-col bg-slate-100 dark:bg-slate-900 font-manrope text-sm font-medium border-r border-outline-variant/10 shrink-0">
<aside id="appSidebar" class="h-screen w-56 flex flex-col bg-slate-100 dark:bg-slate-900 font-manrope text-sm font-medium border-r border-outline-variant/10 shrink-0">
<div class="flex flex-col h-full py-6">
<!-- Header -->
<div class="px-6 mb-8">
@@ -55,23 +214,53 @@
<div class="text-[10px] uppercase tracking-widest text-on-surface-variant mt-1.5 font-bold">Admin Console</div>
</div>
<!-- Primary Nav -->
<nav class="flex-1 px-3 space-y-1">
<a href="#dashboard" data-nav="dashboard" class="flex items-center gap-3 px-3 py-2 border-l-4 border-blue-600 bg-slate-200/80 dark:bg-slate-800 text-slate-900 dark:text-slate-50 font-bold group transition-all cursor-pointer">
<span class="material-symbols-outlined">dashboard</span>
<span>Dashboard</span>
</a>
<a href="#applications" data-nav="applications" 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">
<span class="material-symbols-outlined">apps</span>
<span>Applications</span>
</a>
<a href="#accounts" data-nav="accounts" 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">
<span class="material-symbols-outlined">manage_accounts</span>
<span>Accounts</span>
</a>
<a id="usersNav" href="#users" data-nav="users" 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" style="display: none;">
<span class="material-symbols-outlined">people</span>
<span>Users</span>
</a>
<nav class="flex-1 px-3 space-y-4">
<div>
<div class="tree-label">Tài khoản</div>
<div class="tree-branch">
<a href="#dashboard" data-nav="dashboard" class="flex items-center gap-3 px-3 py-2 border-l-4 border-blue-600 bg-slate-200/80 dark:bg-slate-800 text-slate-900 dark:text-slate-50 font-bold group transition-all cursor-pointer rounded-r-lg">
<span class="material-symbols-outlined">dashboard</span>
<span>Tổng quan</span>
</a>
<a href="#applications" data-nav="applications" 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">apps</span>
<span>Ứng dụng</span>
</a>
<a href="#accounts" data-nav="accounts" 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">manage_accounts</span>
<span>Tài khoản</span>
</a>
</div>
</div>
<div>
<div class="tree-label">Quản lý tài sản</div>
<div class="tree-branch">
<a href="#assets" data-nav="assets" 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">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>
<div id="usersSection" class="pt-2 border-t border-outline-variant/10" style="display: none;">
<a id="usersNav" href="#users" data-nav="users" 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-lg" style="display: none;">
<span class="material-symbols-outlined">people</span>
<span>Người dùng (Quản trị)</span>
</a>
</div>
</nav>
<!-- Footer -->
<div class="px-6 pt-4 border-t border-outline-variant/10">
@@ -80,22 +269,31 @@
</div>
</aside>
<button id="sidebarBackdrop" type="button" aria-label="Đóng menu điều hướng"></button>
<!-- Main Content -->
<main class="flex-1 flex flex-col h-screen min-w-0">
<main id="appMain" class="flex-1 flex flex-col h-screen min-w-0">
<!-- TopAppBar -->
<header class="h-14 flex items-center justify-between px-6 bg-slate-50/80 dark:bg-slate-950/80 backdrop-blur-xl border-b border-outline-variant/10 shrink-0">
<div class="flex items-center gap-4 flex-1">
<button id="mobileMenuBtn" type="button" class="p-2 rounded-lg text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors" aria-label="Mở menu điều hướng" aria-controls="appSidebar" aria-expanded="false">
<span class="material-symbols-outlined">menu</span>
</button>
</div>
<div class="flex items-center gap-4">
<button id="profileBtn" type="button" class="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="Edit profile">
<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="flex flex-col">
<span id="accountUsername" class="text-xs font-semibold text-slate-900 dark:text-slate-50">User Account</span>
<span id="accountRole" class="text-[10px] text-slate-500 dark:text-slate-400">Administrator</span>
<div class="profile-meta flex flex-col">
<span id="accountUsername" class="text-xs font-semibold text-slate-900 dark:text-slate-50">Tài khoản người dùng</span>
<span id="accountRole" class="text-[10px] text-slate-500 dark:text-slate-400">Quản trị viên</span>
</div>
</button>
<button id="logoutBtn" class="p-2 rounded-lg text-slate-600 dark:text-slate-300 hover:bg-red-100 dark:hover:bg-red-950 hover:text-red-700 dark:hover:text-red-300 transition-colors" title="Logout">
<button id="logoutBtn" class="p-2 rounded-lg text-slate-600 dark:text-slate-300 hover:bg-red-100 dark:hover:bg-red-950 hover:text-red-700 dark:hover:text-red-300 transition-colors" title="Đăng xuất">
<span class="material-symbols-outlined">logout</span>
</button>
</div>
@@ -107,6 +305,6 @@
</div>
</main>
<script src="../js/app.js"></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,14 +86,19 @@
</div>
<!-- Remember Me Checkbox -->
<div class="flex items-center">
<input
type="checkbox"
id="remember"
name="remember"
class="w-4 h-4 rounded border-outline-variant/30 text-primary focus:ring-2 focus:ring-primary/50 cursor-pointer"
/>
<label for="remember" class="ml-2.5 text-xs font-medium text-on-surface-variant cursor-pointer">Remember me</label>
<div class="flex items-center justify-between gap-2">
<div class="flex items-center">
<input
type="checkbox"
id="remember"
name="remember"
class="w-4 h-4 rounded border-outline-variant/30 text-primary focus:ring-2 focus:ring-primary/50 cursor-pointer"
/>
<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 -->
@@ -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,36 +495,134 @@
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 = '';
passwordInput.focus();
}
} catch (error) {
errorMessage.textContent = 'Connection error. Try admin / admin';
errorMessage.textContent = 'Connection error. Try again later.';
errorMessage.classList.remove('hidden');
console.error('Login error:', error);
}
});
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.