This commit is contained in:
2026-05-15 14:26:30 +07:00
parent 41e523ff35
commit 1ff9826056
6 changed files with 315 additions and 123 deletions

View File

@@ -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"]

View File

@@ -10,10 +10,14 @@ const nodemailer = require('nodemailer');
const multer = require('multer'); const multer = require('multer');
const XLSX = require('xlsx'); const XLSX = require('xlsx');
const dotenv = require('dotenv'); const dotenv = require('dotenv');
const app = express();
dotenv.config(); dotenv.config();
const APP_TIME_ZONE = process.env.APP_TIME_ZONE || process.env.TZ || 'Asia/Ho_Chi_Minh';
process.env.TZ = APP_TIME_ZONE;
const app = express();
function envBool(name, defaultValue) { function envBool(name, defaultValue) {
const value = process.env[name]; const value = process.env[name];
if (value === undefined) { if (value === undefined) {
@@ -48,6 +52,52 @@ const PASSWORD_RESET_TOKEN_TTL_MINUTES = Number(process.env.PASSWORD_RESET_TOKEN
let mailTransporter; let mailTransporter;
const appTimePartsFormatter = 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'
});
function getAppTimeParts(value = new Date()) {
const date = value instanceof Date ? value : new Date(value);
if (Number.isNaN(date.getTime())) {
return null;
}
return appTimePartsFormatter.formatToParts(date).reduce((parts, part) => {
if (part.type !== 'literal') {
parts[part.type] = part.value;
}
return parts;
}, {});
}
function formatAppTimestampForCode(value = new Date(), includeMilliseconds = false) {
const date = value instanceof Date ? value : new Date(value);
const parts = 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;
}
function isBcryptHash(value) { function isBcryptHash(value) {
return typeof value === 'string' && /^\$2[aby]\$\d{2}\$[./A-Za-z0-9]{53}$/.test(value); return typeof value === 'string' && /^\$2[aby]\$\d{2}\$[./A-Za-z0-9]{53}$/.test(value);
} }
@@ -1113,16 +1163,7 @@ function generateManualAssetCodeFromPayload(payload = {}) {
const fromSerial = sanitizeAssetCodeToken(payload.serialNumber); const fromSerial = sanitizeAssetCodeToken(payload.serialNumber);
const fromName = sanitizeAssetCodeToken(payload.assetName); const fromName = sanitizeAssetCodeToken(payload.assetName);
const base = (fromModel || fromSerial || fromName || 'ASSET').slice(0, 32); const base = (fromModel || fromSerial || fromName || 'ASSET').slice(0, 32);
const now = new Date(); const timestamp = formatAppTimestampForCode(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}`;
} }
@@ -1598,7 +1639,8 @@ const sqlConfig = {
trustServerCertificate: DB_TRUST_CERTIFICATE, trustServerCertificate: DB_TRUST_CERTIFICATE,
enableKeepAlive: true, enableKeepAlive: true,
connectTimeout: DB_CONNECT_TIMEOUT, connectTimeout: DB_CONNECT_TIMEOUT,
encrypt: DB_ENCRYPT encrypt: DB_ENCRYPT,
useUTC: false
} }
}; };
@@ -1619,7 +1661,8 @@ async function initializeDatabase() {
connectTimeout: DB_CONNECT_TIMEOUT, connectTimeout: DB_CONNECT_TIMEOUT,
database: 'master', database: 'master',
trustServerCertificate: DB_TRUST_CERTIFICATE, trustServerCertificate: DB_TRUST_CERTIFICATE,
encrypt: DB_ENCRYPT encrypt: DB_ENCRYPT,
useUTC: false
} }
}); });
@@ -1683,6 +1726,97 @@ async function migrateLegacyPasswords() {
} }
} }
async function ensureAppTimeDefaultConstraints() {
await pool.request().query(`
DECLARE @schemaName SYSNAME = N'dbo';
DECLARE @defaults TABLE (
TableName SYSNAME NOT NULL,
ColumnName SYSNAME NOT NULL,
ConstraintName SYSNAME NOT NULL,
Definition NVARCHAR(MAX) NOT NULL
);
INSERT INTO @defaults (TableName, ColumnName, ConstraintName, Definition)
VALUES
(N'Users', N'CreatedDate', N'DF_Users_CreatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'Applications', N'CreatedDate', N'DF_Applications_CreatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'Applications', N'UpdatedDate', N'DF_Applications_UpdatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'Accounts', N'CreatedDate', N'DF_Accounts_CreatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'Accounts', N'UpdatedDate', N'DF_Accounts_UpdatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetInventory', N'CreatedDate', N'DF_AssetInventory_CreatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetInventory', N'UpdatedDate', N'DF_AssetInventory_UpdatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetDepartments', N'CreatedDate', N'DF_AssetDepartments_CreatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetDepartments', N'UpdatedDate', N'DF_AssetDepartments_UpdatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetProjects', N'CreatedDate', N'DF_AssetProjects_CreatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetProjects', N'UpdatedDate', N'DF_AssetProjects_UpdatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetBorrowRequests', N'BorrowDate', N'DF_AssetBorrowRequests_BorrowDate', N'(CAST(DATEADD(HOUR, 7, SYSUTCDATETIME()) AS DATE))'),
(N'AssetBorrowRequests', N'CreatedDate', N'DF_AssetBorrowRequests_CreatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetBorrowRequests', N'UpdatedDate', N'DF_AssetBorrowRequests_UpdatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetBorrowRequestLinks', N'CreatedDate', N'DF_AssetBorrowRequestLinks_CreatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetExportHistory', N'ExportedDate', N'DF_AssetExportHistory_ExportedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetExportHistory', N'CreatedDate', N'DF_AssetExportHistory_CreatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetExportHistory', N'UpdatedDate', N'DF_AssetExportHistory_UpdatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetDamageDisposalHistory', N'ActionDate', N'DF_AssetDamageDisposalHistory_ActionDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetDamageDisposalHistory', N'CreatedDate', N'DF_AssetDamageDisposalHistory_CreatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AssetDamageDisposalHistory', N'UpdatedDate', N'DF_AssetDamageDisposalHistory_UpdatedDate', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))'),
(N'AuditLog', N'Timestamp', N'DF_AuditLog_Timestamp', N'(DATEADD(HOUR, 7, SYSUTCDATETIME()))');
DECLARE @tableName SYSNAME;
DECLARE @columnName SYSNAME;
DECLARE @constraintName SYSNAME;
DECLARE @definition NVARCHAR(MAX);
DECLARE @existingName SYSNAME;
DECLARE @objectId INT;
DECLARE @sql NVARCHAR(MAX);
DECLARE default_cursor CURSOR LOCAL FAST_FORWARD FOR
SELECT TableName, ColumnName, ConstraintName, Definition
FROM @defaults;
OPEN default_cursor;
FETCH NEXT FROM default_cursor INTO @tableName, @columnName, @constraintName, @definition;
WHILE @@FETCH_STATUS = 0
BEGIN
SET @objectId = OBJECT_ID(QUOTENAME(@schemaName) + N'.' + QUOTENAME(@tableName), N'U');
IF @objectId IS NOT NULL
AND EXISTS (SELECT 1 FROM sys.columns WHERE object_id = @objectId AND name = @columnName)
BEGIN
SET @existingName = NULL;
SELECT @existingName = dc.name
FROM sys.default_constraints dc
INNER JOIN sys.columns c
ON c.object_id = dc.parent_object_id
AND c.column_id = dc.parent_column_id
WHERE dc.parent_object_id = @objectId
AND c.name = @columnName;
IF @existingName IS NOT NULL
BEGIN
SET @sql = N'ALTER TABLE '
+ QUOTENAME(@schemaName) + N'.' + QUOTENAME(@tableName)
+ N' DROP CONSTRAINT ' + QUOTENAME(@existingName);
EXEC sp_executesql @sql;
END
SET @sql = N'ALTER TABLE '
+ QUOTENAME(@schemaName) + N'.' + QUOTENAME(@tableName)
+ N' ADD CONSTRAINT ' + QUOTENAME(@constraintName)
+ N' DEFAULT ' + @definition
+ N' FOR ' + QUOTENAME(@columnName);
EXEC sp_executesql @sql;
END
FETCH NEXT FROM default_cursor INTO @tableName, @columnName, @constraintName, @definition;
END
CLOSE default_cursor;
DEALLOCATE default_cursor;
`);
}
async function createTables() { async function createTables() {
const queries = [ const queries = [
// Users Table // Users Table
@@ -1696,7 +1830,7 @@ async function createTables() {
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
) )
@@ -1713,8 +1847,8 @@ async function createTables() {
Icon NVARCHAR(50), Icon NVARCHAR(50),
Description NVARCHAR(500), Description NVARCHAR(500),
Url NVARCHAR(255), Url NVARCHAR(255),
CreatedDate DATETIME DEFAULT GETDATE(), CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
UpdatedDate DATETIME DEFAULT GETDATE() UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME()))
) )
END`, END`,
@@ -1731,8 +1865,8 @@ async function createTables() {
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
) )
@@ -1765,8 +1899,8 @@ async function createTables() {
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
) )
END`, END`,
@@ -1777,8 +1911,8 @@ async function createTables() {
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()))
) )
END`, END`,
@@ -1788,8 +1922,8 @@ async function createTables() {
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()))
) )
END`, END`,
@@ -1805,15 +1939,15 @@ async function createTables() {
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
@@ -1828,7 +1962,7 @@ async function createTables() {
BorrowId INT NOT NULL, BorrowId INT NOT NULL,
ReturnId INT NOT NULL, ReturnId INT NOT NULL,
Quantity INT NOT NULL DEFAULT 1, Quantity INT NOT NULL DEFAULT 1,
CreatedDate DATETIME NOT NULL DEFAULT GETDATE(), CreatedDate DATETIME NOT NULL 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
) )
@@ -1848,9 +1982,9 @@ async function createTables() {
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
) )
END`, END`,
@@ -1882,9 +2016,9 @@ async function createTables() {
ActionNote NVARCHAR(1000) NULL, ActionNote NVARCHAR(1000) NULL,
CreatedBy INT NULL, CreatedBy INT NULL,
CreatedByName NVARCHAR(100) NULL, CreatedByName NVARCHAR(100) NULL,
ActionDate DATETIME NOT NULL DEFAULT GETDATE(), ActionDate 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,
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL
) )
@@ -1901,7 +2035,7 @@ async function createTables() {
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)
) )
END` END`
@@ -1988,7 +2122,7 @@ async function createTables() {
await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','Borrower') IS NULL ALTER TABLE AssetInventory ADD Borrower NVARCHAR(255) NULL;`); await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','Borrower') IS NULL ALTER TABLE AssetInventory ADD Borrower NVARCHAR(255) NULL;`);
await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','ExportedBy') IS NULL ALTER TABLE AssetInventory ADD ExportedBy NVARCHAR(100) NULL;`); await pool.request().query(`IF COL_LENGTH('dbo.AssetInventory','ExportedBy') IS NULL ALTER TABLE AssetInventory ADD ExportedBy NVARCHAR(100) NULL;`);
await pool.request().query(`IF COL_LENGTH('dbo.AssetBorrowRequests','Unit') IS NULL ALTER TABLE AssetBorrowRequests ADD Unit NVARCHAR(50) NULL;`); await pool.request().query(`IF COL_LENGTH('dbo.AssetBorrowRequests','Unit') IS NULL ALTER TABLE AssetBorrowRequests ADD Unit NVARCHAR(50) NULL;`);
await pool.request().query(`IF COL_LENGTH('dbo.AssetBorrowRequests','BorrowDate') IS NULL ALTER TABLE AssetBorrowRequests ADD BorrowDate DATE NOT NULL CONSTRAINT DF_AssetBorrowRequests_BorrowDate DEFAULT(CAST(GETDATE() AS DATE));`); await pool.request().query(`IF COL_LENGTH('dbo.AssetBorrowRequests','BorrowDate') IS NULL ALTER TABLE AssetBorrowRequests ADD BorrowDate DATE NOT NULL CONSTRAINT DF_AssetBorrowRequests_BorrowDate DEFAULT(CAST(DATEADD(HOUR, 7, SYSUTCDATETIME()) AS DATE));`);
await pool.request().query(`IF COL_LENGTH('dbo.AssetBorrowRequests','RequestType') IS NULL ALTER TABLE AssetBorrowRequests ADD RequestType NVARCHAR(20) NOT NULL CONSTRAINT DF_AssetBorrowRequests_RequestType DEFAULT('borrow');`); await pool.request().query(`IF COL_LENGTH('dbo.AssetBorrowRequests','RequestType') IS NULL ALTER TABLE AssetBorrowRequests ADD RequestType NVARCHAR(20) NOT NULL CONSTRAINT DF_AssetBorrowRequests_RequestType DEFAULT('borrow');`);
await pool.request().query(`IF COL_LENGTH('dbo.AssetBorrowRequests','RequestStatus') IS NULL ALTER TABLE AssetBorrowRequests ADD RequestStatus NVARCHAR(20) NOT NULL CONSTRAINT DF_AssetBorrowRequests_RequestStatus DEFAULT('approved');`); await pool.request().query(`IF COL_LENGTH('dbo.AssetBorrowRequests','RequestStatus') IS NULL ALTER TABLE AssetBorrowRequests ADD RequestStatus NVARCHAR(20) NOT NULL CONSTRAINT DF_AssetBorrowRequests_RequestStatus DEFAULT('approved');`);
await pool.request().query(`IF COL_LENGTH('dbo.AssetBorrowRequests','ReturnedQuantity') IS NULL ALTER TABLE AssetBorrowRequests ADD ReturnedQuantity INT NOT NULL CONSTRAINT DF_AssetBorrowRequests_ReturnedQuantity DEFAULT(0);`); await pool.request().query(`IF COL_LENGTH('dbo.AssetBorrowRequests','ReturnedQuantity') IS NULL ALTER TABLE AssetBorrowRequests ADD ReturnedQuantity INT NOT NULL CONSTRAINT DF_AssetBorrowRequests_ReturnedQuantity DEFAULT(0);`);
@@ -2007,7 +2141,7 @@ async function createTables() {
ADD CONSTRAINT FK_AssetBorrowRequests_ProcessedBy ADD CONSTRAINT FK_AssetBorrowRequests_ProcessedBy
FOREIGN KEY (ProcessedBy) REFERENCES Users(UserId) ON DELETE SET NULL; FOREIGN KEY (ProcessedBy) REFERENCES Users(UserId) ON DELETE SET NULL;
`); `);
await pool.request().query(`IF COL_LENGTH('dbo.AssetBorrowRequests','UpdatedDate') IS NULL ALTER TABLE AssetBorrowRequests ADD UpdatedDate DATETIME NOT NULL CONSTRAINT DF_AssetBorrowRequests_UpdatedDate DEFAULT(GETDATE());`); await pool.request().query(`IF COL_LENGTH('dbo.AssetBorrowRequests','UpdatedDate') IS NULL ALTER TABLE AssetBorrowRequests ADD UpdatedDate DATETIME NOT NULL CONSTRAINT DF_AssetBorrowRequests_UpdatedDate DEFAULT(DATEADD(HOUR, 7, SYSUTCDATETIME()));`);
await pool.request().query(` await pool.request().query(`
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetBorrowRequestLinks') IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetBorrowRequestLinks')
BEGIN BEGIN
@@ -2016,7 +2150,7 @@ async function createTables() {
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
); );
@@ -2030,9 +2164,9 @@ async function createTables() {
await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','ExportedByName') IS NULL ALTER TABLE AssetExportHistory ADD ExportedByName NVARCHAR(100) NULL;`); await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','ExportedByName') IS NULL ALTER TABLE AssetExportHistory ADD ExportedByName NVARCHAR(100) NULL;`);
await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','ExportNote') IS NULL ALTER TABLE AssetExportHistory ADD ExportNote NVARCHAR(1000) NULL;`); await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','ExportNote') IS NULL ALTER TABLE AssetExportHistory ADD ExportNote NVARCHAR(1000) NULL;`);
await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','CreatedBy') IS NULL ALTER TABLE AssetExportHistory ADD CreatedBy INT NULL;`); await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','CreatedBy') IS NULL ALTER TABLE AssetExportHistory ADD CreatedBy INT NULL;`);
await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','ExportedDate') IS NULL ALTER TABLE AssetExportHistory ADD ExportedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_ExportedDate DEFAULT(GETDATE());`); await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','ExportedDate') IS NULL ALTER TABLE AssetExportHistory ADD ExportedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_ExportedDate DEFAULT(DATEADD(HOUR, 7, SYSUTCDATETIME()));`);
await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','CreatedDate') IS NULL ALTER TABLE AssetExportHistory ADD CreatedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_CreatedDate DEFAULT(GETDATE());`); await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','CreatedDate') IS NULL ALTER TABLE AssetExportHistory ADD CreatedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_CreatedDate DEFAULT(DATEADD(HOUR, 7, SYSUTCDATETIME()));`);
await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','UpdatedDate') IS NULL ALTER TABLE AssetExportHistory ADD UpdatedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_UpdatedDate DEFAULT(GETDATE());`); await pool.request().query(`IF COL_LENGTH('dbo.AssetExportHistory','UpdatedDate') IS NULL ALTER TABLE AssetExportHistory ADD UpdatedDate DATETIME NOT NULL CONSTRAINT DF_AssetExportHistory_UpdatedDate DEFAULT(DATEADD(HOUR, 7, SYSUTCDATETIME()));`);
await pool.request().query(` await pool.request().query(`
IF NOT EXISTS ( IF NOT EXISTS (
SELECT 1 SELECT 1
@@ -2102,7 +2236,7 @@ async function createTables() {
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
@@ -2164,13 +2298,19 @@ async function createTables() {
await pool.request().query(`IF COL_LENGTH('dbo.Users','EmailVerifyTokenExpires') IS NULL ALTER TABLE Users ADD EmailVerifyTokenExpires DATETIME NULL;`); await pool.request().query(`IF COL_LENGTH('dbo.Users','EmailVerifyTokenExpires') IS NULL ALTER TABLE Users ADD EmailVerifyTokenExpires DATETIME NULL;`);
await pool.request().query(`IF COL_LENGTH('dbo.Users','PasswordResetToken') IS NULL ALTER TABLE Users ADD PasswordResetToken NVARCHAR(255) NULL;`); await pool.request().query(`IF COL_LENGTH('dbo.Users','PasswordResetToken') IS NULL ALTER TABLE Users ADD PasswordResetToken NVARCHAR(255) NULL;`);
await pool.request().query(`IF COL_LENGTH('dbo.Users','PasswordResetTokenExpires') IS NULL ALTER TABLE Users ADD PasswordResetTokenExpires DATETIME NULL;`); await pool.request().query(`IF COL_LENGTH('dbo.Users','PasswordResetTokenExpires') IS NULL ALTER TABLE Users ADD PasswordResetTokenExpires DATETIME NULL;`);
await pool.request().query(`UPDATE Users SET EmailVerified = 1, EmailVerifiedAt = ISNULL(EmailVerifiedAt, GETDATE()) WHERE LOWER(ISNULL(Role, '')) = 'admin';`); await pool.request().query(`UPDATE Users SET EmailVerified = 1, EmailVerifiedAt = ISNULL(EmailVerifiedAt, DATEADD(HOUR, 7, SYSUTCDATETIME())) WHERE LOWER(ISNULL(Role, '')) = 'admin';`);
// Backfill Url to empty string to avoid undefined in responses // Backfill Url to empty string to avoid undefined in responses
await pool.request().query(`UPDATE Applications SET Url = '' WHERE Url IS NULL;`); await pool.request().query(`UPDATE Applications SET Url = '' WHERE Url IS NULL;`);
} catch (err) { } catch (err) {
console.error('Column addition error (Applications):', err.message); console.error('Column addition error (Applications):', err.message);
} }
try {
await ensureAppTimeDefaultConstraints();
} catch (err) {
console.error('Timezone default constraint error:', err.message);
}
// Sync legacy departments from AssetInventory to AssetDepartments // Sync legacy departments from AssetInventory to AssetDepartments
try { try {
await syncAssetDepartmentsFromInventory(); await syncAssetDepartmentsFromInventory();
@@ -2198,11 +2338,11 @@ async function createTables() {
.input('role', sql.NVarChar, 'admin') .input('role', sql.NVarChar, 'admin')
.query(`IF NOT EXISTS (SELECT * FROM Users WHERE Username = @username) .query(`IF NOT EXISTS (SELECT * FROM Users WHERE Username = @username)
INSERT INTO Users (Username, Password, ViewPassword, Email, FullName, Role, IsActive, EmailVerified, EmailVerifiedAt) INSERT INTO Users (Username, Password, ViewPassword, Email, FullName, Role, IsActive, EmailVerified, EmailVerifiedAt)
VALUES (@username, @password, @viewPassword, @email, @fullname, @role, 1, 1, GETDATE()) VALUES (@username, @password, @viewPassword, @email, @fullname, @role, 1, 1, DATEADD(HOUR, 7, SYSUTCDATETIME()))
ELSE ELSE
UPDATE Users UPDATE Users
SET EmailVerified = 1, SET EmailVerified = 1,
EmailVerifiedAt = ISNULL(EmailVerifiedAt, GETDATE()) EmailVerifiedAt = ISNULL(EmailVerifiedAt, DATEADD(HOUR, 7, SYSUTCDATETIME()))
WHERE Username = @username`); WHERE Username = @username`);
console.log('[OK] Admin user created: admin / admin'); console.log('[OK] Admin user created: admin / admin');
} catch (err) { } catch (err) {
@@ -2287,7 +2427,7 @@ app.post('/api/auth/login', async (req, res) => {
// Update last login // Update last login
await pool.request() await pool.request()
.input('userId', sql.Int, user.UserId) .input('userId', sql.Int, user.UserId)
.query('UPDATE Users SET LastLogin = GETDATE() WHERE UserId = @userId'); .query('UPDATE Users SET LastLogin = DATEADD(HOUR, 7, SYSUTCDATETIME()) WHERE UserId = @userId');
res.json({ res.json({
success: true, success: true,
@@ -2387,7 +2527,7 @@ app.post('/api/auth/register', async (req, res) => {
.input('tokenTtlMinutes', sql.Int, EMAIL_VERIFY_TOKEN_TTL_MINUTES) .input('tokenTtlMinutes', sql.Int, EMAIL_VERIFY_TOKEN_TTL_MINUTES)
.query(`INSERT INTO Users (Username, Password, ViewPassword, Email, FullName, RoleId, Role, Status, IsActive, EmailVerified, EmailVerifyToken, EmailVerifyTokenExpires) .query(`INSERT INTO Users (Username, Password, ViewPassword, Email, FullName, RoleId, Role, Status, IsActive, EmailVerified, EmailVerifyToken, EmailVerifyTokenExpires)
OUTPUT INSERTED.UserId, INSERTED.Username, INSERTED.Email, INSERTED.FullName, INSERTED.Role, INSERTED.RoleId OUTPUT INSERTED.UserId, INSERTED.Username, INSERTED.Email, INSERTED.FullName, INSERTED.Role, INSERTED.RoleId
VALUES (@username, @password, @viewPassword, @email, @fullname, @roleId, @role, 'Active', 1, 0, @emailVerifyToken, DATEADD(MINUTE, @tokenTtlMinutes, GETDATE()))`); VALUES (@username, @password, @viewPassword, @email, @fullname, @roleId, @role, 'Active', 1, 0, @emailVerifyToken, DATEADD(MINUTE, @tokenTtlMinutes, DATEADD(HOUR, 7, SYSUTCDATETIME())))`);
} else { } else {
result = await pool.request() result = await pool.request()
.input('username', sql.NVarChar, safeUsername) .input('username', sql.NVarChar, safeUsername)
@@ -2400,7 +2540,7 @@ app.post('/api/auth/register', async (req, res) => {
.input('tokenTtlMinutes', sql.Int, EMAIL_VERIFY_TOKEN_TTL_MINUTES) .input('tokenTtlMinutes', sql.Int, EMAIL_VERIFY_TOKEN_TTL_MINUTES)
.query(`INSERT INTO Users (Username, Password, ViewPassword, Email, FullName, Role, Status, IsActive, EmailVerified, EmailVerifyToken, EmailVerifyTokenExpires) .query(`INSERT INTO Users (Username, Password, ViewPassword, Email, FullName, Role, Status, IsActive, EmailVerified, EmailVerifyToken, EmailVerifyTokenExpires)
OUTPUT INSERTED.UserId, INSERTED.Username, INSERTED.Email, INSERTED.FullName, INSERTED.Role OUTPUT INSERTED.UserId, INSERTED.Username, INSERTED.Email, INSERTED.FullName, INSERTED.Role
VALUES (@username, @password, @viewPassword, @email, @fullname, @role, 'Active', 1, 0, @emailVerifyToken, DATEADD(MINUTE, @tokenTtlMinutes, GETDATE()))`); VALUES (@username, @password, @viewPassword, @email, @fullname, @role, 'Active', 1, 0, @emailVerifyToken, DATEADD(MINUTE, @tokenTtlMinutes, DATEADD(HOUR, 7, SYSUTCDATETIME())))`);
} }
const inserted = result.recordset[0]; const inserted = result.recordset[0];
@@ -2475,7 +2615,7 @@ app.get('/api/auth/verify-email', async (req, res) => {
.input('userId', sql.Int, verifiedUser.UserId) .input('userId', sql.Int, verifiedUser.UserId)
.query(`UPDATE Users .query(`UPDATE Users
SET EmailVerified = 1, SET EmailVerified = 1,
EmailVerifiedAt = GETDATE(), EmailVerifiedAt = DATEADD(HOUR, 7, SYSUTCDATETIME()),
EmailVerifyToken = NULL, EmailVerifyToken = NULL,
EmailVerifyTokenExpires = NULL EmailVerifyTokenExpires = NULL
WHERE UserId = @userId`); WHERE UserId = @userId`);
@@ -2493,7 +2633,7 @@ app.get('/api/auth/verify-email', async (req, res) => {
await pool.request() await pool.request()
.input('userId', sql.Int, verifiedUser.UserId) .input('userId', sql.Int, verifiedUser.UserId)
.query('UPDATE Users SET LastLogin = GETDATE() WHERE UserId = @userId'); .query('UPDATE Users SET LastLogin = DATEADD(HOUR, 7, SYSUTCDATETIME()) WHERE UserId = @userId');
res.json({ res.json({
success: true, success: true,
@@ -2540,7 +2680,7 @@ app.post('/api/auth/resend-verification', async (req, res) => {
.input('tokenTtlMinutes', sql.Int, EMAIL_VERIFY_TOKEN_TTL_MINUTES) .input('tokenTtlMinutes', sql.Int, EMAIL_VERIFY_TOKEN_TTL_MINUTES)
.query(`UPDATE Users .query(`UPDATE Users
SET EmailVerifyToken = @tokenHash, SET EmailVerifyToken = @tokenHash,
EmailVerifyTokenExpires = DATEADD(MINUTE, @tokenTtlMinutes, GETDATE()) EmailVerifyTokenExpires = DATEADD(MINUTE, @tokenTtlMinutes, DATEADD(HOUR, 7, SYSUTCDATETIME()))
WHERE UserId = @userId`); WHERE UserId = @userId`);
const emailResult = await sendVerificationEmail({ const emailResult = await sendVerificationEmail({
@@ -2607,7 +2747,7 @@ app.post('/api/auth/forgot-password', async (req, res) => {
.input('tokenTtlMinutes', sql.Int, PASSWORD_RESET_TOKEN_TTL_MINUTES) .input('tokenTtlMinutes', sql.Int, PASSWORD_RESET_TOKEN_TTL_MINUTES)
.query(`UPDATE Users .query(`UPDATE Users
SET PasswordResetToken = @tokenHash, SET PasswordResetToken = @tokenHash,
PasswordResetTokenExpires = DATEADD(MINUTE, @tokenTtlMinutes, GETDATE()) PasswordResetTokenExpires = DATEADD(MINUTE, @tokenTtlMinutes, DATEADD(HOUR, 7, SYSUTCDATETIME()))
WHERE UserId = @userId`); WHERE UserId = @userId`);
const emailResult = await sendPasswordResetEmail({ const emailResult = await sendPasswordResetEmail({
@@ -2946,7 +3086,7 @@ app.put('/api/users/me', async (req, res) => {
SET FullName = @fullname, SET FullName = @fullname,
Email = @email Email = @email
${shouldChangePassword ? ', Password = @password, ViewPassword = @viewPassword' : ''} ${shouldChangePassword ? ', Password = @password, ViewPassword = @viewPassword' : ''}
${emailChanged ? ', EmailVerified = 0, EmailVerifiedAt = NULL, EmailVerifyToken = @emailVerifyToken, EmailVerifyTokenExpires = DATEADD(MINUTE, @tokenTtlMinutes, GETDATE())' : ''} ${emailChanged ? ', EmailVerified = 0, EmailVerifiedAt = NULL, EmailVerifyToken = @emailVerifyToken, EmailVerifyTokenExpires = DATEADD(MINUTE, @tokenTtlMinutes, DATEADD(HOUR, 7, SYSUTCDATETIME()))' : ''}
WHERE UserId = @userId`); WHERE UserId = @userId`);
let emailResult = { sent: true }; let emailResult = { sent: true };
@@ -3201,7 +3341,7 @@ app.put('/api/applications/:id', async (req, res) => {
Icon = @icon, Icon = @icon,
Description = @description, Description = @description,
Url = @url, Url = @url,
UpdatedDate = GETDATE() UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE AppId = @appId`); WHERE AppId = @appId`);
res.json({ success: true, message: 'Application updated' }); res.json({ success: true, message: 'Application updated' });
@@ -3304,7 +3444,7 @@ app.put('/api/accounts/:id', async (req, res) => {
Email = @email, Email = @email,
AccessLevel = @accessLevel, AccessLevel = @accessLevel,
Notes = @notes, Notes = @notes,
UpdatedDate = GETDATE() UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE AccountId = @accountId`); WHERE AccountId = @accountId`);
res.json({ success: true, message: 'Account updated' }); res.json({ success: true, message: 'Account updated' });
@@ -3454,7 +3594,7 @@ app.put('/api/asset-departments/:id', requireAssetOrAdmin, async (req, res) => {
.query(` .query(`
UPDATE AssetDepartments UPDATE AssetDepartments
SET DepartmentName = @departmentName, SET DepartmentName = @departmentName,
UpdatedDate = GETDATE() UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE DepartmentId = @departmentId WHERE DepartmentId = @departmentId
`); `);
@@ -3464,7 +3604,7 @@ app.put('/api/asset-departments/:id', requireAssetOrAdmin, async (req, res) => {
.query(` .query(`
UPDATE AssetInventory UPDATE AssetInventory
SET Department = @newDepartmentName, SET Department = @newDepartmentName,
UpdatedDate = GETDATE() UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE LOWER(LTRIM(RTRIM(Department))) = LOWER(@oldDepartmentName) WHERE LOWER(LTRIM(RTRIM(Department))) = LOWER(@oldDepartmentName)
`); `);
@@ -3518,7 +3658,7 @@ app.delete('/api/asset-departments/:id', requireAssetOrAdmin, async (req, res) =
.query(` .query(`
UPDATE AssetInventory UPDATE AssetInventory
SET Department = NULL, SET Department = NULL,
UpdatedDate = GETDATE() UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE LOWER(LTRIM(RTRIM(Department))) = LOWER(@departmentName) WHERE LOWER(LTRIM(RTRIM(Department))) = LOWER(@departmentName)
`); `);
@@ -3672,7 +3812,7 @@ app.put('/api/asset-projects/:id', requireAssetOrAdmin, async (req, res) => {
.query(` .query(`
UPDATE AssetProjects UPDATE AssetProjects
SET ProjectName = @projectName, SET ProjectName = @projectName,
UpdatedDate = GETDATE() UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE ProjectId = @projectId WHERE ProjectId = @projectId
`); `);
@@ -3682,7 +3822,7 @@ app.put('/api/asset-projects/:id', requireAssetOrAdmin, async (req, res) => {
.query(` .query(`
UPDATE AssetInventory UPDATE AssetInventory
SET Project = @newProjectName, SET Project = @newProjectName,
UpdatedDate = GETDATE() UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE LOWER(LTRIM(RTRIM(Project))) = LOWER(@oldProjectName) WHERE LOWER(LTRIM(RTRIM(Project))) = LOWER(@oldProjectName)
`); `);
@@ -3736,7 +3876,7 @@ app.delete('/api/asset-projects/:id', requireAssetOrAdmin, async (req, res) => {
.query(` .query(`
UPDATE AssetInventory UPDATE AssetInventory
SET Project = NULL, SET Project = NULL,
UpdatedDate = GETDATE() UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE LOWER(LTRIM(RTRIM(Project))) = LOWER(@projectName) WHERE LOWER(LTRIM(RTRIM(Project))) = LOWER(@projectName)
`); `);
@@ -4560,7 +4700,7 @@ app.post('/api/asset-borrows/:id/process', requireAssetOrAdmin, async (req, res)
UsedQuantity = @usedQuantity, UsedQuantity = @usedQuantity,
Status = @status, Status = @status,
ExportedBy = @exportedBy, ExportedBy = @exportedBy,
UpdatedDate = GETDATE() UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE AssetId = @assetId WHERE AssetId = @assetId
`); `);
} else { } else {
@@ -4619,7 +4759,7 @@ app.post('/api/asset-borrows/:id/process', requireAssetOrAdmin, async (req, res)
UsedQuantity = @usedQuantity, UsedQuantity = @usedQuantity,
Status = @status, Status = @status,
ExportedBy = CASE WHEN @borrower IS NULL THEN NULL ELSE @exportedBy END, ExportedBy = CASE WHEN @borrower IS NULL THEN NULL ELSE @exportedBy END,
UpdatedDate = GETDATE() UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE AssetId = @assetId WHERE AssetId = @assetId
`); `);
@@ -4665,7 +4805,7 @@ app.post('/api/asset-borrows/:id/process', requireAssetOrAdmin, async (req, res)
THEN 'returned' THEN 'returned'
ELSE RequestStatus ELSE RequestStatus
END, END,
UpdatedDate = GETDATE() UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE BorrowId = @borrowId WHERE BorrowId = @borrowId
`); `);
} }
@@ -4742,7 +4882,7 @@ app.post('/api/asset-borrows/:id/process', requireAssetOrAdmin, async (req, res)
THEN 'returned' THEN 'returned'
ELSE RequestStatus ELSE RequestStatus
END, END,
UpdatedDate = GETDATE() UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE BorrowId = @borrowId WHERE BorrowId = @borrowId
`); `);
@@ -4764,8 +4904,8 @@ app.post('/api/asset-borrows/:id/process', requireAssetOrAdmin, async (req, res)
RejectReason = @rejectReason, RejectReason = @rejectReason,
ProcessedBy = @processedBy, ProcessedBy = @processedBy,
ProcessedByName = @processedByName, ProcessedByName = @processedByName,
ProcessedDate = GETDATE(), ProcessedDate = DATEADD(HOUR, 7, SYSUTCDATETIME()),
UpdatedDate = GETDATE() UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE BorrowId = @borrowId WHERE BorrowId = @borrowId
`); `);
@@ -5194,7 +5334,7 @@ app.post('/api/assets/:id/damage-disposal', requireAssetOrAdmin, async (req, res
NewQuantity = @newQuantity, NewQuantity = @newQuantity,
UsedQuantity = @usedQuantity, UsedQuantity = @usedQuantity,
Status = @status, Status = @status,
UpdatedDate = GETDATE() UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE AssetId = @assetId WHERE AssetId = @assetId
`); `);
@@ -5449,7 +5589,7 @@ app.post('/api/assets/:id/export', requireAssetOrAdmin, async (req, res) => {
Status = @status, Status = @status,
ExportedBy = @exportedBy, ExportedBy = @exportedBy,
Notes = @notes, Notes = @notes,
UpdatedDate = GETDATE() UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE AssetId = @assetId WHERE AssetId = @assetId
`); `);
@@ -5651,7 +5791,7 @@ app.put('/api/assets/:id', requireAssetOrAdmin, async (req, res) => {
PurchasePrice = @purchasePrice, PurchasePrice = @purchasePrice,
Status = @status, Status = @status,
Notes = @notes, Notes = @notes,
UpdatedDate = GETDATE() UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHERE AssetId = @assetId WHERE AssetId = @assetId
`); `);
@@ -5868,7 +6008,7 @@ app.post('/api/assets/import', requireAssetOrAdmin, upload.single('file'), async
PurchasePrice = @purchasePrice, PurchasePrice = @purchasePrice,
Status = @status, Status = @status,
Notes = @notes, Notes = @notes,
UpdatedDate = GETDATE() UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
WHEN NOT MATCHED THEN WHEN NOT MATCHED THEN
INSERT ( INSERT (
AssetCode, AssetName, Model, SerialNumber, AssetCode, AssetName, Model, SerialNumber,

View File

@@ -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')
@@ -512,9 +512,9 @@ BEGIN
ActionNote NVARCHAR(1000) NULL, ActionNote NVARCHAR(1000) NULL,
CreatedBy INT NULL, CreatedBy INT NULL,
CreatedByName NVARCHAR(100) NULL, CreatedByName NVARCHAR(100) NULL,
ActionDate DATETIME NOT NULL DEFAULT GETDATE(), ActionDate 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,
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL
); );
@@ -534,7 +534,7 @@ 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.';

View File

@@ -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}

View File

@@ -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}

View File

@@ -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
@@ -1795,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">
@@ -2055,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) {
@@ -5019,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}`;
} }
@@ -6481,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');
} }
@@ -7418,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 ==========