Compare commits
4 Commits
e1b553ba79
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ff9826056 | |||
| 41e523ff35 | |||
| 12c6380ceb | |||
| cb4d5b9520 |
@@ -3,6 +3,8 @@ FROM node:20-bookworm-slim
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
ENV TZ=Asia/Ho_Chi_Minh
|
||||||
|
ENV APP_TIME_ZONE=Asia/Ho_Chi_Minh
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm ci --omit=dev && npm cache clean --force
|
RUN npm ci --omit=dev && npm cache clean --force
|
||||||
@@ -12,4 +14,4 @@ COPY public ./public
|
|||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
CMD ["node", "backend/server.js"]
|
CMD ["node", "backend/server.js"]
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -32,7 +32,7 @@ BEGIN
|
|||||||
FullName NVARCHAR(100),
|
FullName NVARCHAR(100),
|
||||||
Role NVARCHAR(50) NOT NULL,
|
Role NVARCHAR(50) NOT NULL,
|
||||||
Status NVARCHAR(20) DEFAULT 'Active',
|
Status NVARCHAR(20) DEFAULT 'Active',
|
||||||
CreatedDate DATETIME DEFAULT GETDATE(),
|
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||||
LastLogin DATETIME,
|
LastLogin DATETIME,
|
||||||
IsActive BIT DEFAULT 1
|
IsActive BIT DEFAULT 1
|
||||||
);
|
);
|
||||||
@@ -51,8 +51,8 @@ BEGIN
|
|||||||
Status NVARCHAR(20) DEFAULT 'online',
|
Status NVARCHAR(20) DEFAULT 'online',
|
||||||
Icon NVARCHAR(50),
|
Icon NVARCHAR(50),
|
||||||
Description NVARCHAR(500),
|
Description NVARCHAR(500),
|
||||||
CreatedDate DATETIME DEFAULT GETDATE(),
|
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||||
UpdatedDate DATETIME DEFAULT GETDATE()
|
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME()))
|
||||||
);
|
);
|
||||||
PRINT 'Table Applications created successfully.';
|
PRINT 'Table Applications created successfully.';
|
||||||
END
|
END
|
||||||
@@ -72,8 +72,8 @@ BEGIN
|
|||||||
AccessLevel NVARCHAR(50),
|
AccessLevel NVARCHAR(50),
|
||||||
Status NVARCHAR(20) DEFAULT 'Active',
|
Status NVARCHAR(20) DEFAULT 'Active',
|
||||||
Notes NVARCHAR(MAX),
|
Notes NVARCHAR(MAX),
|
||||||
CreatedDate DATETIME DEFAULT GETDATE(),
|
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||||
UpdatedDate DATETIME DEFAULT GETDATE(),
|
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||||
FOREIGN KEY (UserId) REFERENCES Users(UserId) ON DELETE CASCADE,
|
FOREIGN KEY (UserId) REFERENCES Users(UserId) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (AppId) REFERENCES Applications(AppId) ON DELETE CASCADE
|
FOREIGN KEY (AppId) REFERENCES Applications(AppId) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
@@ -109,8 +109,8 @@ BEGIN
|
|||||||
Status NVARCHAR(30) NOT NULL DEFAULT 'in_use',
|
Status NVARCHAR(30) NOT NULL DEFAULT 'in_use',
|
||||||
Notes NVARCHAR(MAX),
|
Notes NVARCHAR(MAX),
|
||||||
CreatedBy INT NULL,
|
CreatedBy INT NULL,
|
||||||
CreatedDate DATETIME DEFAULT GETDATE(),
|
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||||
UpdatedDate DATETIME DEFAULT GETDATE(),
|
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||||
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL
|
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL
|
||||||
);
|
);
|
||||||
PRINT 'Table AssetInventory created successfully.';
|
PRINT 'Table AssetInventory created successfully.';
|
||||||
@@ -175,8 +175,8 @@ BEGIN
|
|||||||
CREATE TABLE AssetDepartments (
|
CREATE TABLE AssetDepartments (
|
||||||
DepartmentId INT PRIMARY KEY IDENTITY(1,1),
|
DepartmentId INT PRIMARY KEY IDENTITY(1,1),
|
||||||
DepartmentName NVARCHAR(100) NOT NULL,
|
DepartmentName NVARCHAR(100) NOT NULL,
|
||||||
CreatedDate DATETIME DEFAULT GETDATE(),
|
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||||
UpdatedDate DATETIME DEFAULT GETDATE()
|
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME()))
|
||||||
);
|
);
|
||||||
PRINT 'Table AssetDepartments created successfully.';
|
PRINT 'Table AssetDepartments created successfully.';
|
||||||
END
|
END
|
||||||
@@ -204,8 +204,8 @@ BEGIN
|
|||||||
CREATE TABLE AssetProjects (
|
CREATE TABLE AssetProjects (
|
||||||
ProjectId INT PRIMARY KEY IDENTITY(1,1),
|
ProjectId INT PRIMARY KEY IDENTITY(1,1),
|
||||||
ProjectName NVARCHAR(150) NOT NULL,
|
ProjectName NVARCHAR(150) NOT NULL,
|
||||||
CreatedDate DATETIME DEFAULT GETDATE(),
|
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||||
UpdatedDate DATETIME DEFAULT GETDATE()
|
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME()))
|
||||||
);
|
);
|
||||||
PRINT 'Table AssetProjects created successfully.';
|
PRINT 'Table AssetProjects created successfully.';
|
||||||
END
|
END
|
||||||
@@ -224,15 +224,15 @@ BEGIN
|
|||||||
BorrowQuantity INT NOT NULL DEFAULT 1,
|
BorrowQuantity INT NOT NULL DEFAULT 1,
|
||||||
ReturnedQuantity INT NOT NULL DEFAULT 0,
|
ReturnedQuantity INT NOT NULL DEFAULT 0,
|
||||||
Unit NVARCHAR(50),
|
Unit NVARCHAR(50),
|
||||||
BorrowDate DATE NOT NULL DEFAULT CAST(GETDATE() AS DATE),
|
BorrowDate DATE NOT NULL DEFAULT (CAST(DATEADD(HOUR, 7, SYSUTCDATETIME()) AS DATE)),
|
||||||
RequestNote NVARCHAR(500) NULL,
|
RequestNote NVARCHAR(500) NULL,
|
||||||
RejectReason NVARCHAR(1000) NULL,
|
RejectReason NVARCHAR(1000) NULL,
|
||||||
CreatedBy INT NULL,
|
CreatedBy INT NULL,
|
||||||
ProcessedBy INT NULL,
|
ProcessedBy INT NULL,
|
||||||
ProcessedByName NVARCHAR(100) NULL,
|
ProcessedByName NVARCHAR(100) NULL,
|
||||||
ProcessedDate DATETIME NULL,
|
ProcessedDate DATETIME NULL,
|
||||||
CreatedDate DATETIME DEFAULT GETDATE(),
|
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||||
UpdatedDate DATETIME DEFAULT GETDATE(),
|
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||||
FOREIGN KEY (AssetId) REFERENCES AssetInventory(AssetId) ON DELETE CASCADE,
|
FOREIGN KEY (AssetId) REFERENCES AssetInventory(AssetId) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL,
|
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL,
|
||||||
FOREIGN KEY (ProcessedBy) REFERENCES Users(UserId) ON DELETE SET NULL
|
FOREIGN KEY (ProcessedBy) REFERENCES Users(UserId) ON DELETE SET NULL
|
||||||
@@ -247,12 +247,12 @@ END
|
|||||||
|
|
||||||
IF COL_LENGTH('dbo.AssetBorrowRequests', 'BorrowDate') IS NULL
|
IF COL_LENGTH('dbo.AssetBorrowRequests', 'BorrowDate') IS NULL
|
||||||
BEGIN
|
BEGIN
|
||||||
ALTER TABLE AssetBorrowRequests ADD BorrowDate DATE NOT NULL CONSTRAINT DF_AssetBorrowRequests_BorrowDate DEFAULT(CAST(GETDATE() AS DATE));
|
ALTER TABLE AssetBorrowRequests ADD BorrowDate DATE NOT NULL CONSTRAINT DF_AssetBorrowRequests_BorrowDate DEFAULT(CAST(DATEADD(HOUR, 7, SYSUTCDATETIME()) AS DATE));
|
||||||
END
|
END
|
||||||
|
|
||||||
IF COL_LENGTH('dbo.AssetBorrowRequests', 'UpdatedDate') IS NULL
|
IF COL_LENGTH('dbo.AssetBorrowRequests', 'UpdatedDate') IS NULL
|
||||||
BEGIN
|
BEGIN
|
||||||
ALTER TABLE AssetBorrowRequests ADD UpdatedDate DATETIME NOT NULL CONSTRAINT DF_AssetBorrowRequests_UpdatedDate DEFAULT(GETDATE());
|
ALTER TABLE AssetBorrowRequests ADD UpdatedDate DATETIME NOT NULL CONSTRAINT DF_AssetBorrowRequests_UpdatedDate DEFAULT(DATEADD(HOUR, 7, SYSUTCDATETIME()));
|
||||||
END
|
END
|
||||||
|
|
||||||
IF COL_LENGTH('dbo.AssetBorrowRequests', 'RequestType') IS NULL
|
IF COL_LENGTH('dbo.AssetBorrowRequests', 'RequestType') IS NULL
|
||||||
@@ -319,7 +319,7 @@ BEGIN
|
|||||||
BorrowId INT NOT NULL,
|
BorrowId INT NOT NULL,
|
||||||
ReturnId INT NOT NULL,
|
ReturnId INT NOT NULL,
|
||||||
Quantity INT NOT NULL CONSTRAINT DF_AssetBorrowRequestLinks_Quantity DEFAULT(1),
|
Quantity INT NOT NULL CONSTRAINT DF_AssetBorrowRequestLinks_Quantity DEFAULT(1),
|
||||||
CreatedDate DATETIME NOT NULL CONSTRAINT DF_AssetBorrowRequestLinks_CreatedDate DEFAULT(GETDATE()),
|
CreatedDate DATETIME NOT NULL CONSTRAINT DF_AssetBorrowRequestLinks_CreatedDate DEFAULT(DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||||
FOREIGN KEY (BorrowId) REFERENCES AssetBorrowRequests(BorrowId) ON DELETE NO ACTION,
|
FOREIGN KEY (BorrowId) REFERENCES AssetBorrowRequests(BorrowId) ON DELETE NO ACTION,
|
||||||
FOREIGN KEY (ReturnId) REFERENCES AssetBorrowRequests(BorrowId) ON DELETE NO ACTION
|
FOREIGN KEY (ReturnId) REFERENCES AssetBorrowRequests(BorrowId) ON DELETE NO ACTION
|
||||||
);
|
);
|
||||||
@@ -373,7 +373,7 @@ BEGIN
|
|||||||
THEN 'returned'
|
THEN 'returned'
|
||||||
ELSE borrowRows.RequestStatus
|
ELSE borrowRows.RequestStatus
|
||||||
END,
|
END,
|
||||||
UpdatedDate = GETDATE()
|
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
|
||||||
FROM AssetBorrowRequests borrowRows
|
FROM AssetBorrowRequests borrowRows
|
||||||
INNER JOIN (
|
INNER JOIN (
|
||||||
SELECT links.BorrowId, SUM(ISNULL(links.Quantity, 0)) AS ReturnedQuantity
|
SELECT links.BorrowId, SUM(ISNULL(links.Quantity, 0)) AS ReturnedQuantity
|
||||||
@@ -401,9 +401,9 @@ BEGIN
|
|||||||
ExportedByName NVARCHAR(100) NOT NULL,
|
ExportedByName NVARCHAR(100) NOT NULL,
|
||||||
ExportNote NVARCHAR(1000) NULL,
|
ExportNote NVARCHAR(1000) NULL,
|
||||||
CreatedBy INT NULL,
|
CreatedBy INT NULL,
|
||||||
ExportedDate DATETIME NOT NULL DEFAULT GETDATE(),
|
ExportedDate DATETIME NOT NULL DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||||
CreatedDate DATETIME NOT NULL DEFAULT GETDATE(),
|
CreatedDate DATETIME NOT NULL DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||||
UpdatedDate DATETIME NOT NULL DEFAULT GETDATE(),
|
UpdatedDate DATETIME NOT NULL DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||||
FOREIGN KEY (AssetId) REFERENCES AssetInventory(AssetId) ON DELETE CASCADE
|
FOREIGN KEY (AssetId) REFERENCES AssetInventory(AssetId) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
PRINT 'Table AssetExportHistory created successfully.';
|
PRINT 'Table AssetExportHistory created successfully.';
|
||||||
@@ -451,17 +451,17 @@ END
|
|||||||
|
|
||||||
IF COL_LENGTH('dbo.AssetExportHistory', 'ExportedDate') IS NULL
|
IF COL_LENGTH('dbo.AssetExportHistory', 'ExportedDate') IS NULL
|
||||||
BEGIN
|
BEGIN
|
||||||
ALTER TABLE AssetExportHistory ADD ExportedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_ExportedDate DEFAULT(GETDATE());
|
ALTER TABLE AssetExportHistory ADD ExportedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_ExportedDate DEFAULT(DATEADD(HOUR, 7, SYSUTCDATETIME()));
|
||||||
END
|
END
|
||||||
|
|
||||||
IF COL_LENGTH('dbo.AssetExportHistory', 'CreatedDate') IS NULL
|
IF COL_LENGTH('dbo.AssetExportHistory', 'CreatedDate') IS NULL
|
||||||
BEGIN
|
BEGIN
|
||||||
ALTER TABLE AssetExportHistory ADD CreatedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_CreatedDate DEFAULT(GETDATE());
|
ALTER TABLE AssetExportHistory ADD CreatedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_CreatedDate DEFAULT(DATEADD(HOUR, 7, SYSUTCDATETIME()));
|
||||||
END
|
END
|
||||||
|
|
||||||
IF COL_LENGTH('dbo.AssetExportHistory', 'UpdatedDate') IS NULL
|
IF COL_LENGTH('dbo.AssetExportHistory', 'UpdatedDate') IS NULL
|
||||||
BEGIN
|
BEGIN
|
||||||
ALTER TABLE AssetExportHistory ADD UpdatedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_UpdatedDate DEFAULT(GETDATE());
|
ALTER TABLE AssetExportHistory ADD UpdatedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_UpdatedDate DEFAULT(DATEADD(HOUR, 7, SYSUTCDATETIME()));
|
||||||
END
|
END
|
||||||
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = 'FK_AssetExportHistory_CreatedBy')
|
IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = 'FK_AssetExportHistory_CreatedBy')
|
||||||
@@ -484,7 +484,45 @@ BEGIN
|
|||||||
END
|
END
|
||||||
|
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
-- 9. CREATE AUDIT LOG TABLE
|
-- 9. CREATE ASSET DAMAGE/DISPOSAL HISTORY TABLE
|
||||||
|
-- ===========================================
|
||||||
|
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetDamageDisposalHistory')
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE AssetDamageDisposalHistory (
|
||||||
|
DamageHistoryId INT PRIMARY KEY IDENTITY(1,1),
|
||||||
|
AssetId INT NOT NULL,
|
||||||
|
AssetCode NVARCHAR(100) NOT NULL,
|
||||||
|
AssetName NVARCHAR(255) NOT NULL,
|
||||||
|
ActionType NVARCHAR(20) NOT NULL,
|
||||||
|
ActionLabel NVARCHAR(50) NOT NULL,
|
||||||
|
ActionQuantity INT NOT NULL DEFAULT 1,
|
||||||
|
Unit NVARCHAR(50) NULL,
|
||||||
|
PreviousQuantity INT NOT NULL DEFAULT 0,
|
||||||
|
NextQuantity INT NOT NULL DEFAULT 0,
|
||||||
|
PreviousImportInPeriod INT NOT NULL DEFAULT 0,
|
||||||
|
NextImportInPeriod INT NOT NULL DEFAULT 0,
|
||||||
|
PreviousExportInPeriod INT NOT NULL DEFAULT 0,
|
||||||
|
NextExportInPeriod INT NOT NULL DEFAULT 0,
|
||||||
|
PreviousEndingBalance INT NOT NULL DEFAULT 0,
|
||||||
|
NextEndingBalance INT NOT NULL DEFAULT 0,
|
||||||
|
PreviousNewQuantity INT NOT NULL DEFAULT 0,
|
||||||
|
NextNewQuantity INT NOT NULL DEFAULT 0,
|
||||||
|
PreviousUsedQuantity INT NOT NULL DEFAULT 0,
|
||||||
|
NextUsedQuantity INT NOT NULL DEFAULT 0,
|
||||||
|
ActionNote NVARCHAR(1000) NULL,
|
||||||
|
CreatedBy INT NULL,
|
||||||
|
CreatedByName NVARCHAR(100) NULL,
|
||||||
|
ActionDate DATETIME NOT NULL DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||||
|
CreatedDate DATETIME NOT NULL DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||||
|
UpdatedDate DATETIME NOT NULL DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||||
|
FOREIGN KEY (AssetId) REFERENCES AssetInventory(AssetId) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
PRINT 'Table AssetDamageDisposalHistory created successfully.';
|
||||||
|
END
|
||||||
|
|
||||||
|
-- ===========================================
|
||||||
|
-- 10. CREATE AUDIT LOG TABLE
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AuditLog')
|
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AuditLog')
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -496,14 +534,14 @@ BEGIN
|
|||||||
RecordId INT,
|
RecordId INT,
|
||||||
OldValue NVARCHAR(MAX),
|
OldValue NVARCHAR(MAX),
|
||||||
NewValue NVARCHAR(MAX),
|
NewValue NVARCHAR(MAX),
|
||||||
Timestamp DATETIME DEFAULT GETDATE(),
|
Timestamp DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||||
FOREIGN KEY (UserId) REFERENCES Users(UserId)
|
FOREIGN KEY (UserId) REFERENCES Users(UserId)
|
||||||
);
|
);
|
||||||
PRINT 'Table AuditLog created successfully.';
|
PRINT 'Table AuditLog created successfully.';
|
||||||
END
|
END
|
||||||
|
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
-- 10. CREATE INDEXES
|
-- 11. CREATE INDEXES
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Users_Username')
|
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_Users_Username')
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -590,10 +628,25 @@ BEGIN
|
|||||||
CREATE INDEX IX_AssetExportHistory_ExportedDate ON AssetExportHistory(ExportedDate DESC);
|
CREATE INDEX IX_AssetExportHistory_ExportedDate ON AssetExportHistory(ExportedDate DESC);
|
||||||
END
|
END
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetDamageDisposalHistory_AssetId')
|
||||||
|
BEGIN
|
||||||
|
CREATE INDEX IX_AssetDamageDisposalHistory_AssetId ON AssetDamageDisposalHistory(AssetId);
|
||||||
|
END
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetDamageDisposalHistory_ActionDate')
|
||||||
|
BEGIN
|
||||||
|
CREATE INDEX IX_AssetDamageDisposalHistory_ActionDate ON AssetDamageDisposalHistory(ActionDate DESC);
|
||||||
|
END
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_AssetDamageDisposalHistory_ActionType')
|
||||||
|
BEGIN
|
||||||
|
CREATE INDEX IX_AssetDamageDisposalHistory_ActionType ON AssetDamageDisposalHistory(ActionType);
|
||||||
|
END
|
||||||
|
|
||||||
PRINT 'Indexes created successfully.';
|
PRINT 'Indexes created successfully.';
|
||||||
|
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
-- 11. INSERT INITIAL DATA
|
-- 12. INSERT INITIAL DATA
|
||||||
-- ===========================================
|
-- ===========================================
|
||||||
|
|
||||||
-- Check if admin user exists
|
-- Check if admin user exists
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
NODE_ENV: ${NODE_ENV:-production}
|
NODE_ENV: ${NODE_ENV:-production}
|
||||||
PORT: 3000
|
PORT: 3000
|
||||||
|
TZ: ${TZ:-Asia/Ho_Chi_Minh}
|
||||||
|
APP_TIME_ZONE: ${APP_TIME_ZONE:-Asia/Ho_Chi_Minh}
|
||||||
DB_SERVER: ${DB_SERVER:-172.20.235.176}
|
DB_SERVER: ${DB_SERVER:-172.20.235.176}
|
||||||
DB_USER: ${DB_USER:-sa}
|
DB_USER: ${DB_USER:-sa}
|
||||||
DB_PASSWORD: ${DB_PASSWORD:-changeme}
|
DB_PASSWORD: ${DB_PASSWORD:-changeme}
|
||||||
@@ -23,4 +25,4 @@ services:
|
|||||||
SMTP_USER: ${SMTP_USER:-}
|
SMTP_USER: ${SMTP_USER:-}
|
||||||
SMTP_PASS: ${SMTP_PASS:-}
|
SMTP_PASS: ${SMTP_PASS:-}
|
||||||
SMTP_FROM: ${SMTP_FROM:-}
|
SMTP_FROM: ${SMTP_FROM:-}
|
||||||
EMAIL_VERIFY_TOKEN_TTL_MINUTES: ${EMAIL_VERIFY_TOKEN_TTL_MINUTES:-30}
|
EMAIL_VERIFY_TOKEN_TTL_MINUTES: ${EMAIL_VERIFY_TOKEN_TTL_MINUTES:-30}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
PORT: 3000
|
PORT: 3000
|
||||||
|
TZ: ${TZ:-Asia/Ho_Chi_Minh}
|
||||||
|
APP_TIME_ZONE: ${APP_TIME_ZONE:-Asia/Ho_Chi_Minh}
|
||||||
DB_SERVER: ${DB_SERVER:-172.20.235.176}
|
DB_SERVER: ${DB_SERVER:-172.20.235.176}
|
||||||
DB_USER: ${DB_USER:-sa}
|
DB_USER: ${DB_USER:-sa}
|
||||||
DB_PASSWORD: ${DB_PASSWORD:-changeme}
|
DB_PASSWORD: ${DB_PASSWORD:-changeme}
|
||||||
@@ -25,4 +27,4 @@ services:
|
|||||||
SMTP_USER: ${SMTP_USER:-}
|
SMTP_USER: ${SMTP_USER:-}
|
||||||
SMTP_PASS: ${SMTP_PASS:-}
|
SMTP_PASS: ${SMTP_PASS:-}
|
||||||
SMTP_FROM: ${SMTP_FROM:-}
|
SMTP_FROM: ${SMTP_FROM:-}
|
||||||
EMAIL_VERIFY_TOKEN_TTL_MINUTES: ${EMAIL_VERIFY_TOKEN_TTL_MINUTES:-30}
|
EMAIL_VERIFY_TOKEN_TTL_MINUTES: ${EMAIL_VERIFY_TOKEN_TTL_MINUTES:-30}
|
||||||
|
|||||||
472
public/js/app.js
472
public/js/app.js
@@ -1,6 +1,34 @@
|
|||||||
// VaultSentinel - Account Management Application
|
// VaultSentinel - Account Management Application
|
||||||
// Main JavaScript functionality
|
// Main JavaScript functionality
|
||||||
|
|
||||||
|
const APP_TIME_ZONE = 'Asia/Ho_Chi_Minh';
|
||||||
|
const APP_DATE_FORMATTER = new Intl.DateTimeFormat('vi-VN', {
|
||||||
|
timeZone: APP_TIME_ZONE,
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit'
|
||||||
|
});
|
||||||
|
const APP_DATE_TIME_FORMATTER = new Intl.DateTimeFormat('vi-VN', {
|
||||||
|
timeZone: APP_TIME_ZONE,
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hourCycle: 'h23'
|
||||||
|
});
|
||||||
|
const APP_TIME_PARTS_FORMATTER = new Intl.DateTimeFormat('en-CA', {
|
||||||
|
timeZone: APP_TIME_ZONE,
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hourCycle: 'h23'
|
||||||
|
});
|
||||||
|
|
||||||
class AccountManager {
|
class AccountManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
// Check if user is logged in
|
// Check if user is logged in
|
||||||
@@ -53,6 +81,7 @@ class AccountManager {
|
|||||||
this.assetProjects = [];
|
this.assetProjects = [];
|
||||||
this.assetProjectSearchTerm = '';
|
this.assetProjectSearchTerm = '';
|
||||||
this.assetExportHistories = [];
|
this.assetExportHistories = [];
|
||||||
|
this.assetDamageHistories = [];
|
||||||
this.selectedAssetIds = new Set();
|
this.selectedAssetIds = new Set();
|
||||||
this.mobileBreakpoint = 900;
|
this.mobileBreakpoint = 900;
|
||||||
this.boundResizeHandler = null;
|
this.boundResizeHandler = null;
|
||||||
@@ -71,6 +100,7 @@ class AccountManager {
|
|||||||
this.assetBorrowAutoRefreshTimer = undefined;
|
this.assetBorrowAutoRefreshTimer = undefined;
|
||||||
this.pendingAssetRequestDeleteConfirmResolver = undefined;
|
this.pendingAssetRequestDeleteConfirmResolver = undefined;
|
||||||
this.pendingBulkAssetDeleteConfirmResolver = undefined;
|
this.pendingBulkAssetDeleteConfirmResolver = undefined;
|
||||||
|
this.pendingAssetDamageId = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
configureNotifications() {
|
configureNotifications() {
|
||||||
@@ -525,15 +555,40 @@ class AccountManager {
|
|||||||
return `${code} - ${name}`.replace(/^\s*-\s*|\s*-\s*$/g, '').trim() || name || '-- Chọn tài sản --';
|
return `${code} - ${name}`.replace(/^\s*-\s*|\s*-\s*$/g, '').trim() || name || '-- Chọn tài sản --';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isBorrowAssetRequestMode() {
|
||||||
|
const typeInput = document.getElementById('assetBorrowRequestTypeInput');
|
||||||
|
return this.normalizeAssetRequestType(typeInput?.value || this.assetBorrowRequestType) === 'borrow';
|
||||||
|
}
|
||||||
|
|
||||||
|
isAssetAvailableForBorrow(asset) {
|
||||||
|
if (!asset) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const endingBalance = this.parseOptionalNonNegativeInteger(asset?.EndingBalance ?? asset?.endingBalance);
|
||||||
|
if (endingBalance !== null) {
|
||||||
|
return endingBalance > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = String(asset?.Status || asset?.status || '').trim().toLowerCase();
|
||||||
|
return status !== 'exported';
|
||||||
|
}
|
||||||
|
|
||||||
getAssetBorrowProductById(assetIdValue) {
|
getAssetBorrowProductById(assetIdValue) {
|
||||||
const assetId = Number(assetIdValue);
|
const assetId = Number(assetIdValue);
|
||||||
if (!Number.isFinite(assetId) || assetId <= 0) {
|
if (!Number.isFinite(assetId) || assetId <= 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.assetBorrowProductItems.find(item => Number(item?.AssetId) === assetId)
|
const asset = this.assetBorrowProductItems.find(item => Number(item?.AssetId) === assetId)
|
||||||
|| this.assets.find(item => Number(item?.AssetId) === assetId)
|
|| this.assets.find(item => Number(item?.AssetId) === assetId)
|
||||||
|| null;
|
|| null;
|
||||||
|
|
||||||
|
if (this.isBorrowAssetRequestMode() && !this.isAssetAvailableForBorrow(asset)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return asset;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAssetBorrowProductDisplay(assetIdValue) {
|
updateAssetBorrowProductDisplay(assetIdValue) {
|
||||||
@@ -654,9 +709,12 @@ class AccountManager {
|
|||||||
const encodedKeyword = encodeURIComponent(this.assetBorrowProductQuery);
|
const encodedKeyword = encodeURIComponent(this.assetBorrowProductQuery);
|
||||||
const offset = this.assetBorrowProductOffset;
|
const offset = this.assetBorrowProductOffset;
|
||||||
const limit = this.assetBorrowProductLimit;
|
const limit = this.assetBorrowProductLimit;
|
||||||
|
const borrowableOnly = this.isBorrowAssetRequestMode();
|
||||||
|
|
||||||
const appendRows = (rows = [], hasMore = false) => {
|
const appendRows = (rows = [], hasMore = false) => {
|
||||||
const source = Array.isArray(rows) ? rows : [];
|
const rowsArray = Array.isArray(rows) ? rows : [];
|
||||||
|
const source = rowsArray
|
||||||
|
.filter(asset => !borrowableOnly || this.isAssetAvailableForBorrow(asset));
|
||||||
if (source.length) {
|
if (source.length) {
|
||||||
const merged = new Map(
|
const merged = new Map(
|
||||||
this.assetBorrowProductItems.map(item => [String(item.AssetId), item])
|
this.assetBorrowProductItems.map(item => [String(item.AssetId), item])
|
||||||
@@ -665,8 +723,8 @@ class AccountManager {
|
|||||||
merged.set(String(item.AssetId), item);
|
merged.set(String(item.AssetId), item);
|
||||||
});
|
});
|
||||||
this.assetBorrowProductItems = Array.from(merged.values());
|
this.assetBorrowProductItems = Array.from(merged.values());
|
||||||
this.assetBorrowProductOffset += source.length;
|
|
||||||
}
|
}
|
||||||
|
this.assetBorrowProductOffset += rowsArray.length;
|
||||||
|
|
||||||
this.assetBorrowProductHasMore = Boolean(hasMore);
|
this.assetBorrowProductHasMore = Boolean(hasMore);
|
||||||
this.assetBorrowProductLoading = false;
|
this.assetBorrowProductLoading = false;
|
||||||
@@ -686,6 +744,10 @@ class AccountManager {
|
|||||||
const source = Array.isArray(this.assets) ? this.assets : [];
|
const source = Array.isArray(this.assets) ? this.assets : [];
|
||||||
const normalized = this.assetBorrowProductQuery.toLowerCase();
|
const normalized = this.assetBorrowProductQuery.toLowerCase();
|
||||||
const filtered = source.filter(asset => {
|
const filtered = source.filter(asset => {
|
||||||
|
if (borrowableOnly && !this.isAssetAvailableForBorrow(asset)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -705,7 +767,7 @@ class AccountManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${this.apiBase}/assets/search?q=${encodedKeyword}&limit=${limit}&offset=${offset}`, {
|
const response = await fetch(`${this.apiBase}/assets/search?q=${encodedKeyword}&limit=${limit}&offset=${offset}&borrowableOnly=${borrowableOnly ? '1' : '0'}`, {
|
||||||
headers: this.getAuthHeaders(false)
|
headers: this.getAuthHeaders(false)
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
@@ -1040,6 +1102,114 @@ class AccountManager {
|
|||||||
this.renderAssetExportHistoryModal();
|
this.renderAssetExportHistoryModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
normalizeAssetDamageType(value) {
|
||||||
|
const normalized = String(value || '').trim().toLowerCase();
|
||||||
|
return normalized === 'disposed' || normalized === 'thanh_ly' || normalized === 'thanh ly'
|
||||||
|
? 'disposed'
|
||||||
|
: 'damaged';
|
||||||
|
}
|
||||||
|
|
||||||
|
getAssetDamageTypeMeta(value) {
|
||||||
|
const type = this.normalizeAssetDamageType(value);
|
||||||
|
if (type === 'disposed') {
|
||||||
|
return {
|
||||||
|
value: 'disposed',
|
||||||
|
label: 'Thanh lý',
|
||||||
|
className: 'bg-slate-100 text-slate-700 border border-slate-200'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: 'damaged',
|
||||||
|
label: 'Hỏng',
|
||||||
|
className: 'bg-red-100 text-red-700 border border-red-200'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchAssetDamageHistories(limit = 300) {
|
||||||
|
try {
|
||||||
|
const safeLimit = Number.isFinite(Number(limit)) ? Math.max(1, Math.min(Number(limit), 2000)) : 300;
|
||||||
|
const res = await fetch(`${this.apiBase}/asset-damage-disposal-history?limit=${safeLimit}`, {
|
||||||
|
headers: this.getAuthHeaders(false)
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
this.assetDamageHistories = Array.isArray(data.data) ? data.data : [];
|
||||||
|
} else {
|
||||||
|
console.error('Load asset damage/disposal history failed:', data.message);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fetch asset damage/disposal history error:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildAssetDamageHistoryRowsHtml(rows = []) {
|
||||||
|
if (!Array.isArray(rows) || rows.length === 0) {
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td colspan="10" class="px-4 py-8 text-sm text-center text-slate-500">Chưa có dữ liệu tài sản hỏng/thanh lý.</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows.map(item => {
|
||||||
|
const typeMeta = this.getAssetDamageTypeMeta(item?.ActionType);
|
||||||
|
const assetLabel = [String(item?.AssetCode || '').trim(), String(item?.AssetName || '').trim()]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' - ') || '-';
|
||||||
|
const unit = String(item?.Unit || '').trim();
|
||||||
|
const quantityLabel = `${Number(item?.ActionQuantity) || 0}${unit ? ` ${unit}` : ''}`;
|
||||||
|
return `
|
||||||
|
<tr class="hover:bg-slate-50/80 transition-colors">
|
||||||
|
<td class="px-4 py-3 text-sm text-slate-600 whitespace-nowrap">${this.formatDateTime(item?.ActionDate || item?.CreatedDate)}</td>
|
||||||
|
<td class="px-4 py-3 text-sm">
|
||||||
|
<span class="inline-flex items-center px-2 py-1 rounded text-xs font-semibold ${typeMeta.className}">${typeMeta.label}</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-slate-700">${this.escapeHtml(assetLabel)}</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-slate-700 font-semibold">${this.escapeHtml(quantityLabel)}</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-slate-600">${Number(item?.PreviousQuantity) || 0} -> ${Number(item?.NextQuantity) || 0}</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-slate-600">${Number(item?.PreviousEndingBalance) || 0} -> ${Number(item?.NextEndingBalance) || 0}</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-slate-600">${Number(item?.PreviousNewQuantity) || 0} -> ${Number(item?.NextNewQuantity) || 0}</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-slate-600">${Number(item?.PreviousUsedQuantity) || 0} -> ${Number(item?.NextUsedQuantity) || 0}</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-slate-600">${this.escapeHtml(item?.CreatedByName || '-')}</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-slate-600 whitespace-pre-line">${this.escapeHtml(item?.ActionNote || '-')}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAssetDamageHistoryModal() {
|
||||||
|
const tbody = document.getElementById('assetDamageHistoryTableBody');
|
||||||
|
if (!tbody) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.innerHTML = this.buildAssetDamageHistoryRowsHtml(this.assetDamageHistories);
|
||||||
|
}
|
||||||
|
|
||||||
|
async openAssetDamageHistoryModal() {
|
||||||
|
if (!this.ensureAssetManagePermission('xem danh sach tai san hong/thanh ly')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modal = document.getElementById('assetDamageHistoryModal');
|
||||||
|
const tbody = document.getElementById('assetDamageHistoryTableBody');
|
||||||
|
if (!modal || !tbody) {
|
||||||
|
this.notifyFailure('Không tìm thấy bảng tài sản hỏng/thanh lý.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.innerHTML = `
|
||||||
|
<tr>
|
||||||
|
<td colspan="10" class="px-4 py-8 text-sm text-center text-slate-500">Đang tải dữ liệu tài sản hỏng/thanh lý...</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
modal.classList.add('open');
|
||||||
|
|
||||||
|
await this.fetchAssetDamageHistories();
|
||||||
|
this.renderAssetDamageHistoryModal();
|
||||||
|
}
|
||||||
|
|
||||||
async fetchRoles() {
|
async fetchRoles() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${this.apiBase}/roles`);
|
const res = await fetch(`${this.apiBase}/roles`);
|
||||||
@@ -1212,6 +1382,14 @@ class AccountManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const assetDamageForm = document.getElementById('assetDamageForm');
|
||||||
|
if (assetDamageForm) {
|
||||||
|
if (!assetDamageForm.dataset.boundSubmit) {
|
||||||
|
assetDamageForm.addEventListener('submit', (e) => this.handleAssetDamageSubmit(e));
|
||||||
|
assetDamageForm.dataset.boundSubmit = 'true';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const assetBorrowRequestForm = document.getElementById('assetBorrowRequestForm');
|
const assetBorrowRequestForm = document.getElementById('assetBorrowRequestForm');
|
||||||
if (assetBorrowRequestForm) {
|
if (assetBorrowRequestForm) {
|
||||||
if (!assetBorrowRequestForm.dataset.boundSubmit) {
|
if (!assetBorrowRequestForm.dataset.boundSubmit) {
|
||||||
@@ -1645,7 +1823,7 @@ class AccountManager {
|
|||||||
<div class="bg-surface-container-lowest p-4 rounded-xl border border-outline-variant/15 flex flex-col">
|
<div class="bg-surface-container-lowest p-4 rounded-xl border border-outline-variant/15 flex flex-col">
|
||||||
<span class="text-[10px] font-bold text-on-surface-variant uppercase tracking-wider mb-2">Last Updated</span>
|
<span class="text-[10px] font-bold text-on-surface-variant uppercase tracking-wider mb-2">Last Updated</span>
|
||||||
<div class="flex items-baseline justify-between">
|
<div class="flex items-baseline justify-between">
|
||||||
<span class="text-sm font-black text-on-surface">${new Date().toLocaleDateString()}</span>
|
<span class="text-sm font-black text-on-surface">${APP_DATE_FORMATTER.format(new Date())}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-primary-container/10 p-4 rounded-xl border border-primary/20 flex flex-col">
|
<div class="bg-primary-container/10 p-4 rounded-xl border border-primary/20 flex flex-col">
|
||||||
@@ -1829,7 +2007,7 @@ class AccountManager {
|
|||||||
<input id="appSearch" class="flex-1 bg-white border border-slate-200 rounded-md text-[11px] py-1 px-2 focus:ring-1 focus:ring-primary shadow-sm" placeholder="Search by name, type, description, url">
|
<input id="appSearch" class="flex-1 bg-white border border-slate-200 rounded-md text-[11px] py-1 px-2 focus:ring-1 focus:ring-primary shadow-sm" placeholder="Search by name, type, description, url">
|
||||||
</div>
|
</div>
|
||||||
<div class="table-wrap overflow-y-auto overflow-x-auto flex-1">
|
<div class="table-wrap overflow-y-auto overflow-x-auto flex-1">
|
||||||
<table class="w-full text-left border-collapse">
|
<table class="w-full text-left border-collapse" style="min-width: 1500px;">
|
||||||
<thead class="sticky top-0 bg-surface-container-lowest z-10">
|
<thead class="sticky top-0 bg-surface-container-lowest z-10">
|
||||||
<tr class="bg-surface-container-low/30 border-b border-outline-variant/10">
|
<tr class="bg-surface-container-low/30 border-b border-outline-variant/10">
|
||||||
<th class="px-6 py-2.5 text-[10px] font-bold text-on-surface-variant uppercase tracking-widest">Name</th>
|
<th class="px-6 py-2.5 text-[10px] font-bold text-on-surface-variant uppercase tracking-widest">Name</th>
|
||||||
@@ -1905,21 +2083,49 @@ class AccountManager {
|
|||||||
return { label: 'Đang sử dụng', className: 'bg-blue-100 text-blue-700' };
|
return { label: 'Đang sử dụng', className: 'bg-blue-100 text-blue-700' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAppTimeParts(value = new Date()) {
|
||||||
|
const date = value instanceof Date ? value : new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return null;
|
||||||
|
|
||||||
|
return APP_TIME_PARTS_FORMATTER.formatToParts(date).reduce((parts, part) => {
|
||||||
|
if (part.type !== 'literal') {
|
||||||
|
parts[part.type] = part.value;
|
||||||
|
}
|
||||||
|
return parts;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
formatTimestampForCode(value = new Date(), includeMilliseconds = false) {
|
||||||
|
const date = value instanceof Date ? value : new Date(value);
|
||||||
|
const parts = this.getAppTimeParts(date);
|
||||||
|
if (!parts) return '';
|
||||||
|
|
||||||
|
const timestamp = [
|
||||||
|
parts.year,
|
||||||
|
parts.month,
|
||||||
|
parts.day,
|
||||||
|
parts.hour,
|
||||||
|
parts.minute,
|
||||||
|
parts.second
|
||||||
|
].join('');
|
||||||
|
|
||||||
|
return includeMilliseconds
|
||||||
|
? `${timestamp}${String(date.getMilliseconds()).padStart(3, '0')}`
|
||||||
|
: timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
formatDateOnly(value) {
|
formatDateOnly(value) {
|
||||||
if (!value) return '-';
|
if (!value) return '-';
|
||||||
const date = new Date(value);
|
const date = new Date(value);
|
||||||
if (Number.isNaN(date.getTime())) return String(value);
|
if (Number.isNaN(date.getTime())) return String(value);
|
||||||
return date.toLocaleDateString();
|
return APP_DATE_FORMATTER.format(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
toDateInputValue(value) {
|
toDateInputValue(value) {
|
||||||
if (!value) return '';
|
if (!value) return '';
|
||||||
const date = new Date(value);
|
const parts = this.getAppTimeParts(value);
|
||||||
if (Number.isNaN(date.getTime())) return '';
|
if (!parts) return '';
|
||||||
const year = date.getFullYear();
|
return `${parts.year}-${parts.month}-${parts.day}`;
|
||||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
||||||
const day = String(date.getDate()).padStart(2, '0');
|
|
||||||
return `${year}-${month}-${day}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
formatBorrowerDisplay(name, quantity = 1) {
|
formatBorrowerDisplay(name, quantity = 1) {
|
||||||
@@ -3056,8 +3262,8 @@ class AccountManager {
|
|||||||
<td class="px-4 py-3 text-sm text-slate-600 whitespace-nowrap">
|
<td class="px-4 py-3 text-sm text-slate-600 whitespace-nowrap">
|
||||||
<span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-bold whitespace-nowrap ${typeMeta.className}">${typeMeta.label}</span>
|
<span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-bold whitespace-nowrap ${typeMeta.className}">${typeMeta.label}</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-sm text-slate-600">
|
<td class="px-4 py-3 text-sm text-slate-600 whitespace-nowrap">
|
||||||
<span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-bold ${statusMeta.className}">${statusMeta.label}</span>
|
<span class="inline-flex items-center px-2 py-1 rounded-md text-xs font-bold whitespace-nowrap ${statusMeta.className}">${statusMeta.label}</span>
|
||||||
${returnProgressHtml}
|
${returnProgressHtml}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-sm text-slate-600">${this.escapeHtml(item.Unit || '-')}</td>
|
<td class="px-4 py-3 text-sm text-slate-600">${this.escapeHtml(item.Unit || '-')}</td>
|
||||||
@@ -3184,7 +3390,7 @@ class AccountManager {
|
|||||||
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Đơn vị</th>
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Đơn vị</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">Dự án</th>
|
||||||
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Vị trí</th>
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Vị trí</th>
|
||||||
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Trạng thái</th>
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500 whitespace-nowrap">Trạng thái</th>
|
||||||
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Ghi chú</th>
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Ghi chú</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -3372,7 +3578,7 @@ class AccountManager {
|
|||||||
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Tên đầy đủ</th>
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Tên đầy đủ</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">Tài sản</th>
|
||||||
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500 whitespace-nowrap">Danh mục</th>
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500 whitespace-nowrap">Danh mục</th>
|
||||||
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Trạng thái</th>
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500 whitespace-nowrap">Trạng thái</th>
|
||||||
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Đơn vị</th>
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Đơn vị</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">Số lượng</th>
|
||||||
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Ngày</th>
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Ngày</th>
|
||||||
@@ -3773,7 +3979,7 @@ class AccountManager {
|
|||||||
|
|
||||||
await this.searchAssetBorrowProducts('', '', { reset: true });
|
await this.searchAssetBorrowProducts('', '', { reset: true });
|
||||||
if (!this.assetBorrowProductItems.length) {
|
if (!this.assetBorrowProductItems.length) {
|
||||||
this.notifyWarning('Hiện chưa có tài sản để tạo đơn.');
|
this.notifyWarning(isReturnRequest ? 'Hiện chưa có tài sản để tạo đơn trả.' : 'Hiện chưa có tài sản còn tồn cuối kỳ để tạo đơn mượn.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3805,6 +4011,19 @@ class AccountManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectedAsset = this.assetBorrowProductItems.find(item => Number(item?.AssetId) === assetId)
|
||||||
|
|| this.assets.find(item => Number(item?.AssetId) === assetId)
|
||||||
|
|| null;
|
||||||
|
if (requestType === 'borrow' && selectedAsset && !this.isAssetAvailableForBorrow(selectedAsset)) {
|
||||||
|
this.notifyWarning('Tài sản đã xuất hoặc hết tồn cuối kỳ, không thể tạo đơn mượn.');
|
||||||
|
await this.searchAssetBorrowProducts(
|
||||||
|
document.getElementById('assetBorrowProductSearchInput')?.value || '',
|
||||||
|
'',
|
||||||
|
{ reset: true }
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const borrowDate = String(dateInput?.value || '').trim() || this.toDateInputValue(new Date());
|
const borrowDate = String(dateInput?.value || '').trim() || this.toDateInputValue(new Date());
|
||||||
const unit = String(unitInput?.value || '').trim();
|
const unit = String(unitInput?.value || '').trim();
|
||||||
const borrowerName = String(requesterInput?.value || this.getCurrentUserDisplayName() || '').trim();
|
const borrowerName = String(requesterInput?.value || this.getCurrentUserDisplayName() || '').trim();
|
||||||
@@ -4233,14 +4452,22 @@ class AccountManager {
|
|||||||
<span class="material-symbols-outlined text-base">history</span>
|
<span class="material-symbols-outlined text-base">history</span>
|
||||||
Lịch sử xuất
|
Lịch sử xuất
|
||||||
</button>
|
</button>
|
||||||
|
<button id="openAssetDamageHistoryBtn" class="border border-red-200 text-red-700 px-3 py-2 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-all active:scale-95 ${canManageAssets ? 'hover:bg-red-50' : 'opacity-50 cursor-not-allowed'}" ${canManageAssets ? '' : 'disabled'}>
|
||||||
|
<span class="material-symbols-outlined text-base">inventory_2</span>
|
||||||
|
DS hỏng/TL
|
||||||
|
</button>
|
||||||
<button id="addAssetBtn" class="bg-primary text-on-primary px-4 py-2 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-all active:scale-95 ${canManageAssets ? 'hover:bg-primary-dim' : 'opacity-50 cursor-not-allowed'}" ${canManageAssets ? '' : 'disabled'}>
|
<button id="addAssetBtn" class="bg-primary text-on-primary px-4 py-2 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-all active:scale-95 ${canManageAssets ? 'hover:bg-primary-dim' : 'opacity-50 cursor-not-allowed'}" ${canManageAssets ? '' : 'disabled'}>
|
||||||
<span class="material-symbols-outlined text-base">add_box</span>
|
<span class="material-symbols-outlined text-base">add_box</span>
|
||||||
Thêm tài sản
|
Thêm tài sản
|
||||||
</button>
|
</button>
|
||||||
<button id="borrowAssetBtn" class="border border-primary text-primary px-4 py-2 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-all active:scale-95 ${canManageAssets ? 'hover:bg-primary/5' : 'opacity-50 cursor-not-allowed'}" ${canManageAssets ? '' : 'disabled'}>
|
<button id="borrowAssetBtn" class="border border-primary text-primary px-4 py-2 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-all active:scale-95 ${(canManageAssets && selectedCount === 1) ? 'hover:bg-primary/5' : 'opacity-50 cursor-not-allowed'}" ${(canManageAssets && selectedCount === 1) ? '' : 'disabled'}>
|
||||||
<span class="material-symbols-outlined text-base">handshake</span>
|
<span class="material-symbols-outlined text-base">handshake</span>
|
||||||
Xuất tài sản
|
Xuất tài sản
|
||||||
</button>
|
</button>
|
||||||
|
<button id="damageAssetBtn" class="border border-red-300 text-red-700 px-4 py-2 rounded-lg text-xs font-bold flex items-center gap-1.5 transition-all active:scale-95 ${(canManageAssets && selectedCount === 1) ? 'hover:bg-red-50' : 'opacity-50 cursor-not-allowed'}" ${(canManageAssets && selectedCount === 1) ? '' : 'disabled'}>
|
||||||
|
<span class="material-symbols-outlined text-base">broken_image</span>
|
||||||
|
Hỏng/Thanh lý
|
||||||
|
</button>
|
||||||
<input id="assetImportInput" type="file" accept=".xlsx,.xls" class="hidden" />
|
<input id="assetImportInput" type="file" accept=".xlsx,.xls" class="hidden" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -4564,6 +4791,16 @@ class AccountManager {
|
|||||||
borrowAssetBtn.disabled = disabled;
|
borrowAssetBtn.disabled = disabled;
|
||||||
borrowAssetBtn.classList.toggle('opacity-50', disabled);
|
borrowAssetBtn.classList.toggle('opacity-50', disabled);
|
||||||
borrowAssetBtn.classList.toggle('cursor-not-allowed', disabled);
|
borrowAssetBtn.classList.toggle('cursor-not-allowed', disabled);
|
||||||
|
borrowAssetBtn.classList.toggle('hover:bg-primary/5', !disabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
const damageAssetBtn = document.getElementById('damageAssetBtn');
|
||||||
|
if (damageAssetBtn) {
|
||||||
|
const disabled = !canManageAssets || selectedCount !== 1;
|
||||||
|
damageAssetBtn.disabled = disabled;
|
||||||
|
damageAssetBtn.classList.toggle('opacity-50', disabled);
|
||||||
|
damageAssetBtn.classList.toggle('cursor-not-allowed', disabled);
|
||||||
|
damageAssetBtn.classList.toggle('hover:bg-red-50', !disabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4838,16 +5075,7 @@ class AccountManager {
|
|||||||
.slice(0, 32);
|
.slice(0, 32);
|
||||||
|
|
||||||
const base = toToken(payload.model) || toToken(payload.serialNumber) || toToken(payload.assetName) || 'ASSET';
|
const base = toToken(payload.model) || toToken(payload.serialNumber) || toToken(payload.assetName) || 'ASSET';
|
||||||
const now = new Date();
|
const timestamp = this.formatTimestampForCode(new Date(), true);
|
||||||
const timestamp = [
|
|
||||||
String(now.getFullYear()),
|
|
||||||
String(now.getMonth() + 1).padStart(2, '0'),
|
|
||||||
String(now.getDate()).padStart(2, '0'),
|
|
||||||
String(now.getHours()).padStart(2, '0'),
|
|
||||||
String(now.getMinutes()).padStart(2, '0'),
|
|
||||||
String(now.getSeconds()).padStart(2, '0'),
|
|
||||||
String(now.getMilliseconds()).padStart(3, '0')
|
|
||||||
].join('');
|
|
||||||
const randomSuffix = String(Math.floor(Math.random() * 100)).padStart(2, '0');
|
const randomSuffix = String(Math.floor(Math.random() * 100)).padStart(2, '0');
|
||||||
return `AST-${base}-${timestamp}${randomSuffix}`;
|
return `AST-${base}-${timestamp}${randomSuffix}`;
|
||||||
}
|
}
|
||||||
@@ -5006,6 +5234,161 @@ class AccountManager {
|
|||||||
return asset;
|
return asset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSingleSelectedAssetForDamage(showWarning = true) {
|
||||||
|
const selectedIds = [...this.selectedAssetIds];
|
||||||
|
if (!selectedIds.length) {
|
||||||
|
if (showWarning) {
|
||||||
|
this.notifyWarning('Vui lòng chọn 1 tài sản để ghi nhận hỏng/thanh lý.');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedIds.length > 1) {
|
||||||
|
if (showWarning) {
|
||||||
|
this.notifyWarning('Chỉ chọn đúng 1 tài sản cho mỗi lần ghi nhận hỏng/thanh lý.');
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const assetId = Number(selectedIds[0]);
|
||||||
|
const asset = this.assets.find(item => Number(item?.AssetId) === assetId) || null;
|
||||||
|
if (!asset && showWarning) {
|
||||||
|
this.notifyFailure('Không tìm thấy tài sản đã chọn.');
|
||||||
|
}
|
||||||
|
return asset;
|
||||||
|
}
|
||||||
|
|
||||||
|
openAssetDamageModal() {
|
||||||
|
if (!this.ensureAssetManagePermission('ghi nhan tai san hong/thanh ly')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const asset = this.getSingleSelectedAssetForDamage(true);
|
||||||
|
if (!asset) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metrics = this.buildAssetQuantityMetrics(asset);
|
||||||
|
if (metrics.endingBalance <= 0) {
|
||||||
|
this.notifyWarning('Tài sản đã hết tồn cuối kỳ, không thể ghi nhận hỏng/thanh lý thêm.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pendingAssetDamageId = Number(asset.AssetId);
|
||||||
|
|
||||||
|
const modal = document.getElementById('assetDamageModal');
|
||||||
|
const assetIdInput = document.getElementById('assetDamageAssetIdInput');
|
||||||
|
const assetNameInput = document.getElementById('assetDamageAssetNameInput');
|
||||||
|
const typeInput = document.getElementById('assetDamageTypeInput');
|
||||||
|
const quantityInput = document.getElementById('assetDamageQuantityInput');
|
||||||
|
const currentQuantityInput = document.getElementById('assetDamageCurrentQuantityInput');
|
||||||
|
const currentEndingInput = document.getElementById('assetDamageCurrentEndingInput');
|
||||||
|
const currentNewInput = document.getElementById('assetDamageCurrentNewInput');
|
||||||
|
const currentUsedInput = document.getElementById('assetDamageCurrentUsedInput');
|
||||||
|
const actorInput = document.getElementById('assetDamageActorInput');
|
||||||
|
const noteInput = document.getElementById('assetDamageNoteInput');
|
||||||
|
|
||||||
|
if (!modal || !assetNameInput || !quantityInput || !currentQuantityInput || !currentEndingInput) {
|
||||||
|
this.notifyFailure('Không tìm thấy biểu mẫu hỏng/thanh lý tài sản.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stockSplit = this.normalizeAssetStockSplit(
|
||||||
|
metrics.endingBalance,
|
||||||
|
asset?.NewQuantity ?? metrics.endingBalance,
|
||||||
|
asset?.UsedQuantity ?? 0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (assetIdInput) {
|
||||||
|
assetIdInput.value = String(asset.AssetId || '');
|
||||||
|
}
|
||||||
|
assetNameInput.value = `${asset.AssetCode || ''} - ${asset.AssetName || ''}`.trim();
|
||||||
|
currentQuantityInput.value = String(metrics.quantity);
|
||||||
|
currentEndingInput.value = String(metrics.endingBalance);
|
||||||
|
if (currentNewInput) currentNewInput.value = String(stockSplit.newQuantity);
|
||||||
|
if (currentUsedInput) currentUsedInput.value = String(stockSplit.usedQuantity);
|
||||||
|
if (typeInput) typeInput.value = 'damaged';
|
||||||
|
quantityInput.value = '1';
|
||||||
|
quantityInput.min = '1';
|
||||||
|
quantityInput.max = String(metrics.endingBalance);
|
||||||
|
if (actorInput) actorInput.value = this.getCurrentUserDisplayName();
|
||||||
|
if (noteInput) noteInput.value = '';
|
||||||
|
|
||||||
|
modal.classList.add('open');
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleAssetDamageSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!this.ensureAssetManagePermission('ghi nhan tai san hong/thanh ly')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const assetIdInput = document.getElementById('assetDamageAssetIdInput');
|
||||||
|
const typeInput = document.getElementById('assetDamageTypeInput');
|
||||||
|
const quantityInput = document.getElementById('assetDamageQuantityInput');
|
||||||
|
const noteInput = document.getElementById('assetDamageNoteInput');
|
||||||
|
|
||||||
|
const selectedAssetId = Number(assetIdInput?.value || this.pendingAssetDamageId);
|
||||||
|
if (!Number.isFinite(selectedAssetId) || selectedAssetId <= 0) {
|
||||||
|
this.notifyFailure('Không xác định được tài sản cần ghi nhận.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const asset = this.assets.find(item => Number(item?.AssetId) === selectedAssetId);
|
||||||
|
if (!asset) {
|
||||||
|
this.notifyFailure('Không tìm thấy tài sản cần ghi nhận.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionType = this.normalizeAssetDamageType(typeInput?.value || 'damaged');
|
||||||
|
const actionMeta = this.getAssetDamageTypeMeta(actionType);
|
||||||
|
const actionQuantity = this.parseNonNegativeInteger(quantityInput?.value ?? 0, 0);
|
||||||
|
const currentMetrics = this.buildAssetQuantityMetrics(asset);
|
||||||
|
|
||||||
|
if (actionQuantity <= 0) {
|
||||||
|
this.notifyWarning('Số lượng phải lớn hơn 0.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actionQuantity > currentMetrics.endingBalance) {
|
||||||
|
this.notifyWarning(`Số lượng ${actionMeta.label.toLowerCase()} (${actionQuantity}) vượt quá tồn cuối kỳ (${currentMetrics.endingBalance}).`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${this.apiBase}/assets/${selectedAssetId}/damage-disposal`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: this.getAuthHeaders(true),
|
||||||
|
body: JSON.stringify({
|
||||||
|
actionType,
|
||||||
|
quantity: actionQuantity,
|
||||||
|
note: String(noteInput?.value || '').trim()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok || !data.success) {
|
||||||
|
this.notifyFailure(data.message || 'Ghi nhận hỏng/thanh lý thất bại');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pendingAssetDamageId = undefined;
|
||||||
|
document.getElementById('assetDamageModal')?.classList.remove('open');
|
||||||
|
this.notifySuccess(data.message || `Đã ghi nhận tài sản ${actionMeta.label.toLowerCase()}`);
|
||||||
|
await this.refreshAssetsUI();
|
||||||
|
|
||||||
|
const historyModal = document.getElementById('assetDamageHistoryModal');
|
||||||
|
if (historyModal?.classList.contains('open')) {
|
||||||
|
await this.fetchAssetDamageHistories();
|
||||||
|
this.renderAssetDamageHistoryModal();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.notifyFailure('Ghi nhận hỏng/thanh lý thất bại');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async openBorrowAssetModal() {
|
async openBorrowAssetModal() {
|
||||||
if (!this.ensureAssetManagePermission('xuat tai san')) {
|
if (!this.ensureAssetManagePermission('xuat tai san')) {
|
||||||
return;
|
return;
|
||||||
@@ -6145,8 +6528,7 @@ class AccountManager {
|
|||||||
const workbook = window.XLSX.utils.book_new();
|
const workbook = window.XLSX.utils.book_new();
|
||||||
window.XLSX.utils.book_append_sheet(workbook, worksheet, 'TaiSan');
|
window.XLSX.utils.book_append_sheet(workbook, worksheet, 'TaiSan');
|
||||||
|
|
||||||
const now = new Date();
|
const timestamp = this.formatTimestampForCode(new Date()).slice(0, 8);
|
||||||
const timestamp = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`;
|
|
||||||
window.XLSX.writeFile(workbook, `danh-sach-tai-san-${timestamp}.xlsx`);
|
window.XLSX.writeFile(workbook, `danh-sach-tai-san-${timestamp}.xlsx`);
|
||||||
this.notifySuccess('Xuất Excel thành công');
|
this.notifySuccess('Xuất Excel thành công');
|
||||||
}
|
}
|
||||||
@@ -6571,10 +6953,17 @@ class AccountManager {
|
|||||||
borrowAssetBtn.dataset.boundClick = 'true';
|
borrowAssetBtn.dataset.boundClick = 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const damageAssetBtn = document.getElementById('damageAssetBtn');
|
||||||
|
if (damageAssetBtn && !damageAssetBtn.dataset.boundClick) {
|
||||||
|
damageAssetBtn.addEventListener('click', () => this.openAssetDamageModal());
|
||||||
|
damageAssetBtn.dataset.boundClick = 'true';
|
||||||
|
}
|
||||||
|
|
||||||
const importAssetBtn = document.getElementById('importAssetBtn');
|
const importAssetBtn = document.getElementById('importAssetBtn');
|
||||||
const assetImportInput = document.getElementById('assetImportInput');
|
const assetImportInput = document.getElementById('assetImportInput');
|
||||||
const exportAssetBtn = document.getElementById('exportAssetBtn');
|
const exportAssetBtn = document.getElementById('exportAssetBtn');
|
||||||
const openAssetExportHistoryBtn = document.getElementById('openAssetExportHistoryBtn');
|
const openAssetExportHistoryBtn = document.getElementById('openAssetExportHistoryBtn');
|
||||||
|
const openAssetDamageHistoryBtn = document.getElementById('openAssetDamageHistoryBtn');
|
||||||
|
|
||||||
if (importAssetBtn && assetImportInput && !importAssetBtn.dataset.boundClick) {
|
if (importAssetBtn && assetImportInput && !importAssetBtn.dataset.boundClick) {
|
||||||
importAssetBtn.addEventListener('click', () => {
|
importAssetBtn.addEventListener('click', () => {
|
||||||
@@ -6600,6 +6989,11 @@ class AccountManager {
|
|||||||
openAssetExportHistoryBtn.addEventListener('click', () => this.openAssetExportHistoryModal());
|
openAssetExportHistoryBtn.addEventListener('click', () => this.openAssetExportHistoryModal());
|
||||||
openAssetExportHistoryBtn.dataset.boundClick = 'true';
|
openAssetExportHistoryBtn.dataset.boundClick = 'true';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (openAssetDamageHistoryBtn && !openAssetDamageHistoryBtn.dataset.boundClick) {
|
||||||
|
openAssetDamageHistoryBtn.addEventListener('click', () => this.openAssetDamageHistoryModal());
|
||||||
|
openAssetDamageHistoryBtn.dataset.boundClick = 'true';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupFilters() {
|
setupFilters() {
|
||||||
@@ -7070,7 +7464,7 @@ class AccountManager {
|
|||||||
if (Number.isNaN(date.getTime())) {
|
if (Number.isNaN(date.getTime())) {
|
||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
return date.toLocaleString();
|
return APP_DATE_TIME_FORMATTER.format(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Users Management ==========
|
// ========== Users Management ==========
|
||||||
@@ -7811,6 +8205,13 @@ function closeBorrowAssetModal() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeAssetDamageModal() {
|
||||||
|
const modal = document.getElementById('assetDamageModal');
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.remove('open');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function closeAssetExportHistoryModal() {
|
function closeAssetExportHistoryModal() {
|
||||||
const modal = document.getElementById('assetExportHistoryModal');
|
const modal = document.getElementById('assetExportHistoryModal');
|
||||||
if (modal) {
|
if (modal) {
|
||||||
@@ -7818,6 +8219,13 @@ function closeAssetExportHistoryModal() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeAssetDamageHistoryModal() {
|
||||||
|
const modal = document.getElementById('assetDamageHistoryModal');
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.remove('open');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function closeAssetBorrowRequestModal() {
|
function closeAssetBorrowRequestModal() {
|
||||||
const modal = document.getElementById('assetBorrowRequestModal');
|
const modal = document.getElementById('assetBorrowRequestModal');
|
||||||
const dropdown = document.getElementById('assetBorrowProductDropdown');
|
const dropdown = document.getElementById('assetBorrowProductDropdown');
|
||||||
|
|||||||
@@ -357,6 +357,66 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Asset Damage/Disposal Modal -->
|
||||||
|
<div class="modal-backdrop fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetDamageModal">
|
||||||
|
<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">Hỏng / Thanh lý tài sản</h3>
|
||||||
|
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeAssetDamageModal()">
|
||||||
|
<span class="material-symbols-outlined">close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form id="assetDamageForm" class="p-6 space-y-4">
|
||||||
|
<input type="hidden" id="assetDamageAssetIdInput">
|
||||||
|
<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="assetDamageAssetNameInput" 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">Lý do</label>
|
||||||
|
<select id="assetDamageTypeInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3" required>
|
||||||
|
<option value="damaged">Hỏng</option>
|
||||||
|
<option value="disposed">Thanh lý</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="text-[10px] font-bold uppercase text-slate-500 tracking-widest block mb-1">Số lượng</label>
|
||||||
|
<input type="number" id="assetDamageQuantityInput" 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">Số lượng hiện tại</label>
|
||||||
|
<input type="number" id="assetDamageCurrentQuantityInput" 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 cuối kỳ hiện tại</label>
|
||||||
|
<input type="number" id="assetDamageCurrentEndingInput" 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">SL hàng mới</label>
|
||||||
|
<input type="number" id="assetDamageCurrentNewInput" 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">SL đã qua sử dụng</label>
|
||||||
|
<input type="number" id="assetDamageCurrentUsedInput" 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">Người thao tác</label>
|
||||||
|
<input type="text" id="assetDamageActorInput" 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ú</label>
|
||||||
|
<textarea id="assetDamageNoteInput" class="w-full border border-slate-200 rounded-lg text-sm py-2.5 px-3 h-20 resize-none" placeholder="Nhập tình trạng, biên bản hoặc lý do chi tiết"></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="closeAssetDamageModal()">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</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Asset Export History Modal -->
|
<!-- 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-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="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);">
|
||||||
@@ -393,6 +453,45 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Asset Damage/Disposal History Modal -->
|
||||||
|
<div class="modal-backdrop fixed inset-0 z-[110] flex items-center justify-center bg-black/40 backdrop-blur-sm" id="assetDamageHistoryModal" style="z-index: 126;">
|
||||||
|
<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">Tài sản hỏng / thanh lý</h3>
|
||||||
|
<button class="p-1.5 rounded-lg hover:bg-slate-200 text-slate-400 transition-colors" onclick="closeAssetDamageHistoryModal()">
|
||||||
|
<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: 1280px;">
|
||||||
|
<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">Lý do</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">Tồn đầu kỳ</th>
|
||||||
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Tồn cuối kỳ</th>
|
||||||
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">SL hàng mới</th>
|
||||||
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">SL đã sử dụng</th>
|
||||||
|
<th class="px-4 py-2.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">Người thao tác</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="assetDamageHistoryTableBody" class="divide-y divide-slate-100">
|
||||||
|
<tr>
|
||||||
|
<td colspan="10" class="px-4 py-8 text-sm text-center text-slate-500">Chưa có dữ liệu tài sản hỏng/thanh lý.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Asset Borrow Request Modal -->
|
<!-- 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-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="modal-content w-full max-w-lg bg-white rounded-xl shadow-2xl border border-slate-200 overflow-visible m-4">
|
||||||
|
|||||||
@@ -309,6 +309,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="../js/app.js?v=20260515-4"></script>
|
<script src="../js/app.js?v=20260515-5"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user