time
This commit is contained in:
@@ -3,6 +3,8 @@ FROM node:20-bookworm-slim
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV TZ=Asia/Ho_Chi_Minh
|
||||
ENV APP_TIME_ZONE=Asia/Ho_Chi_Minh
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev && npm cache clean --force
|
||||
|
||||
@@ -10,10 +10,14 @@ const nodemailer = require('nodemailer');
|
||||
const multer = require('multer');
|
||||
const XLSX = require('xlsx');
|
||||
const dotenv = require('dotenv');
|
||||
const app = express();
|
||||
|
||||
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) {
|
||||
const value = process.env[name];
|
||||
if (value === undefined) {
|
||||
@@ -48,6 +52,52 @@ const PASSWORD_RESET_TOKEN_TTL_MINUTES = Number(process.env.PASSWORD_RESET_TOKEN
|
||||
|
||||
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) {
|
||||
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 fromName = sanitizeAssetCodeToken(payload.assetName);
|
||||
const base = (fromModel || fromSerial || fromName || 'ASSET').slice(0, 32);
|
||||
const now = new Date();
|
||||
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 timestamp = formatAppTimestampForCode(new Date(), true);
|
||||
const randomSuffix = String(Math.floor(Math.random() * 100)).padStart(2, '0');
|
||||
return `AST-${base}-${timestamp}${randomSuffix}`;
|
||||
}
|
||||
@@ -1598,7 +1639,8 @@ const sqlConfig = {
|
||||
trustServerCertificate: DB_TRUST_CERTIFICATE,
|
||||
enableKeepAlive: true,
|
||||
connectTimeout: DB_CONNECT_TIMEOUT,
|
||||
encrypt: DB_ENCRYPT
|
||||
encrypt: DB_ENCRYPT,
|
||||
useUTC: false
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1619,7 +1661,8 @@ async function initializeDatabase() {
|
||||
connectTimeout: DB_CONNECT_TIMEOUT,
|
||||
database: 'master',
|
||||
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() {
|
||||
const queries = [
|
||||
// Users Table
|
||||
@@ -1696,7 +1830,7 @@ async function createTables() {
|
||||
FullName NVARCHAR(100),
|
||||
Role NVARCHAR(50) NOT NULL,
|
||||
Status NVARCHAR(20) DEFAULT 'Active',
|
||||
CreatedDate DATETIME DEFAULT GETDATE(),
|
||||
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||
LastLogin DATETIME,
|
||||
IsActive BIT DEFAULT 1
|
||||
)
|
||||
@@ -1713,8 +1847,8 @@ async function createTables() {
|
||||
Icon NVARCHAR(50),
|
||||
Description NVARCHAR(500),
|
||||
Url NVARCHAR(255),
|
||||
CreatedDate DATETIME DEFAULT GETDATE(),
|
||||
UpdatedDate DATETIME DEFAULT GETDATE()
|
||||
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME()))
|
||||
)
|
||||
END`,
|
||||
|
||||
@@ -1731,8 +1865,8 @@ async function createTables() {
|
||||
AccessLevel NVARCHAR(50),
|
||||
Status NVARCHAR(20) DEFAULT 'Active',
|
||||
Notes NVARCHAR(MAX),
|
||||
CreatedDate DATETIME DEFAULT GETDATE(),
|
||||
UpdatedDate DATETIME DEFAULT GETDATE(),
|
||||
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||
FOREIGN KEY (UserId) REFERENCES Users(UserId) 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',
|
||||
Notes NVARCHAR(MAX),
|
||||
CreatedBy INT NULL,
|
||||
CreatedDate DATETIME DEFAULT GETDATE(),
|
||||
UpdatedDate DATETIME DEFAULT GETDATE(),
|
||||
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL
|
||||
)
|
||||
END`,
|
||||
@@ -1777,8 +1911,8 @@ async function createTables() {
|
||||
CREATE TABLE AssetDepartments (
|
||||
DepartmentId INT PRIMARY KEY IDENTITY(1,1),
|
||||
DepartmentName NVARCHAR(100) NOT NULL,
|
||||
CreatedDate DATETIME DEFAULT GETDATE(),
|
||||
UpdatedDate DATETIME DEFAULT GETDATE()
|
||||
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME()))
|
||||
)
|
||||
END`,
|
||||
|
||||
@@ -1788,8 +1922,8 @@ async function createTables() {
|
||||
CREATE TABLE AssetProjects (
|
||||
ProjectId INT PRIMARY KEY IDENTITY(1,1),
|
||||
ProjectName NVARCHAR(150) NOT NULL,
|
||||
CreatedDate DATETIME DEFAULT GETDATE(),
|
||||
UpdatedDate DATETIME DEFAULT GETDATE()
|
||||
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME()))
|
||||
)
|
||||
END`,
|
||||
|
||||
@@ -1805,15 +1939,15 @@ async function createTables() {
|
||||
BorrowQuantity INT NOT NULL DEFAULT 1,
|
||||
ReturnedQuantity INT NOT NULL DEFAULT 0,
|
||||
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,
|
||||
RejectReason NVARCHAR(1000) NULL,
|
||||
CreatedBy INT NULL,
|
||||
ProcessedBy INT NULL,
|
||||
ProcessedByName NVARCHAR(100) NULL,
|
||||
ProcessedDate DATETIME NULL,
|
||||
CreatedDate DATETIME DEFAULT GETDATE(),
|
||||
UpdatedDate DATETIME DEFAULT GETDATE(),
|
||||
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||
FOREIGN KEY (AssetId) REFERENCES AssetInventory(AssetId) ON DELETE CASCADE,
|
||||
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL,
|
||||
FOREIGN KEY (ProcessedBy) REFERENCES Users(UserId) ON DELETE SET NULL
|
||||
@@ -1828,7 +1962,7 @@ async function createTables() {
|
||||
BorrowId INT NOT NULL,
|
||||
ReturnId INT NOT NULL,
|
||||
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 (ReturnId) REFERENCES AssetBorrowRequests(BorrowId) ON DELETE NO ACTION
|
||||
)
|
||||
@@ -1848,9 +1982,9 @@ async function createTables() {
|
||||
ExportedByName NVARCHAR(100) NOT NULL,
|
||||
ExportNote NVARCHAR(1000) NULL,
|
||||
CreatedBy INT NULL,
|
||||
ExportedDate DATETIME NOT NULL DEFAULT GETDATE(),
|
||||
CreatedDate DATETIME NOT NULL DEFAULT GETDATE(),
|
||||
UpdatedDate DATETIME NOT NULL DEFAULT GETDATE(),
|
||||
ExportedDate 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
|
||||
)
|
||||
END`,
|
||||
@@ -1882,9 +2016,9 @@ async function createTables() {
|
||||
ActionNote NVARCHAR(1000) NULL,
|
||||
CreatedBy INT NULL,
|
||||
CreatedByName NVARCHAR(100) NULL,
|
||||
ActionDate DATETIME NOT NULL DEFAULT GETDATE(),
|
||||
CreatedDate DATETIME NOT NULL DEFAULT GETDATE(),
|
||||
UpdatedDate DATETIME NOT NULL DEFAULT GETDATE(),
|
||||
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
|
||||
)
|
||||
@@ -1901,7 +2035,7 @@ async function createTables() {
|
||||
RecordId INT,
|
||||
OldValue NVARCHAR(MAX),
|
||||
NewValue NVARCHAR(MAX),
|
||||
Timestamp DATETIME DEFAULT GETDATE(),
|
||||
Timestamp DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||
FOREIGN KEY (UserId) REFERENCES Users(UserId)
|
||||
)
|
||||
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','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','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','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);`);
|
||||
@@ -2007,7 +2141,7 @@ async function createTables() {
|
||||
ADD CONSTRAINT FK_AssetBorrowRequests_ProcessedBy
|
||||
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(`
|
||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'AssetBorrowRequestLinks')
|
||||
BEGIN
|
||||
@@ -2016,7 +2150,7 @@ async function createTables() {
|
||||
BorrowId INT NOT NULL,
|
||||
ReturnId INT NOT NULL,
|
||||
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 (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','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','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','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','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','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(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(DATEADD(HOUR, 7, SYSUTCDATETIME()));`);
|
||||
await pool.request().query(`
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
@@ -2102,7 +2236,7 @@ async function createTables() {
|
||||
THEN 'returned'
|
||||
ELSE borrowRows.RequestStatus
|
||||
END,
|
||||
UpdatedDate = GETDATE()
|
||||
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
|
||||
FROM AssetBorrowRequests borrowRows
|
||||
INNER JOIN (
|
||||
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','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(`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
|
||||
await pool.request().query(`UPDATE Applications SET Url = '' WHERE Url IS NULL;`);
|
||||
} catch (err) {
|
||||
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
|
||||
try {
|
||||
await syncAssetDepartmentsFromInventory();
|
||||
@@ -2198,11 +2338,11 @@ async function createTables() {
|
||||
.input('role', sql.NVarChar, 'admin')
|
||||
.query(`IF NOT EXISTS (SELECT * FROM Users WHERE Username = @username)
|
||||
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
|
||||
UPDATE Users
|
||||
SET EmailVerified = 1,
|
||||
EmailVerifiedAt = ISNULL(EmailVerifiedAt, GETDATE())
|
||||
EmailVerifiedAt = ISNULL(EmailVerifiedAt, DATEADD(HOUR, 7, SYSUTCDATETIME()))
|
||||
WHERE Username = @username`);
|
||||
console.log('[OK] Admin user created: admin / admin');
|
||||
} catch (err) {
|
||||
@@ -2287,7 +2427,7 @@ app.post('/api/auth/login', async (req, res) => {
|
||||
// Update last login
|
||||
await pool.request()
|
||||
.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({
|
||||
success: true,
|
||||
@@ -2387,7 +2527,7 @@ app.post('/api/auth/register', async (req, res) => {
|
||||
.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)
|
||||
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 {
|
||||
result = await pool.request()
|
||||
.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)
|
||||
.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
|
||||
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];
|
||||
@@ -2475,7 +2615,7 @@ app.get('/api/auth/verify-email', async (req, res) => {
|
||||
.input('userId', sql.Int, verifiedUser.UserId)
|
||||
.query(`UPDATE Users
|
||||
SET EmailVerified = 1,
|
||||
EmailVerifiedAt = GETDATE(),
|
||||
EmailVerifiedAt = DATEADD(HOUR, 7, SYSUTCDATETIME()),
|
||||
EmailVerifyToken = NULL,
|
||||
EmailVerifyTokenExpires = NULL
|
||||
WHERE UserId = @userId`);
|
||||
@@ -2493,7 +2633,7 @@ app.get('/api/auth/verify-email', async (req, res) => {
|
||||
|
||||
await pool.request()
|
||||
.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({
|
||||
success: true,
|
||||
@@ -2540,7 +2680,7 @@ app.post('/api/auth/resend-verification', async (req, res) => {
|
||||
.input('tokenTtlMinutes', sql.Int, EMAIL_VERIFY_TOKEN_TTL_MINUTES)
|
||||
.query(`UPDATE Users
|
||||
SET EmailVerifyToken = @tokenHash,
|
||||
EmailVerifyTokenExpires = DATEADD(MINUTE, @tokenTtlMinutes, GETDATE())
|
||||
EmailVerifyTokenExpires = DATEADD(MINUTE, @tokenTtlMinutes, DATEADD(HOUR, 7, SYSUTCDATETIME()))
|
||||
WHERE UserId = @userId`);
|
||||
|
||||
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)
|
||||
.query(`UPDATE Users
|
||||
SET PasswordResetToken = @tokenHash,
|
||||
PasswordResetTokenExpires = DATEADD(MINUTE, @tokenTtlMinutes, GETDATE())
|
||||
PasswordResetTokenExpires = DATEADD(MINUTE, @tokenTtlMinutes, DATEADD(HOUR, 7, SYSUTCDATETIME()))
|
||||
WHERE UserId = @userId`);
|
||||
|
||||
const emailResult = await sendPasswordResetEmail({
|
||||
@@ -2946,7 +3086,7 @@ app.put('/api/users/me', async (req, res) => {
|
||||
SET FullName = @fullname,
|
||||
Email = @email
|
||||
${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`);
|
||||
|
||||
let emailResult = { sent: true };
|
||||
@@ -3201,7 +3341,7 @@ app.put('/api/applications/:id', async (req, res) => {
|
||||
Icon = @icon,
|
||||
Description = @description,
|
||||
Url = @url,
|
||||
UpdatedDate = GETDATE()
|
||||
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
|
||||
WHERE AppId = @appId`);
|
||||
|
||||
res.json({ success: true, message: 'Application updated' });
|
||||
@@ -3304,7 +3444,7 @@ app.put('/api/accounts/:id', async (req, res) => {
|
||||
Email = @email,
|
||||
AccessLevel = @accessLevel,
|
||||
Notes = @notes,
|
||||
UpdatedDate = GETDATE()
|
||||
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
|
||||
WHERE AccountId = @accountId`);
|
||||
|
||||
res.json({ success: true, message: 'Account updated' });
|
||||
@@ -3454,7 +3594,7 @@ app.put('/api/asset-departments/:id', requireAssetOrAdmin, async (req, res) => {
|
||||
.query(`
|
||||
UPDATE AssetDepartments
|
||||
SET DepartmentName = @departmentName,
|
||||
UpdatedDate = GETDATE()
|
||||
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
|
||||
WHERE DepartmentId = @departmentId
|
||||
`);
|
||||
|
||||
@@ -3464,7 +3604,7 @@ app.put('/api/asset-departments/:id', requireAssetOrAdmin, async (req, res) => {
|
||||
.query(`
|
||||
UPDATE AssetInventory
|
||||
SET Department = @newDepartmentName,
|
||||
UpdatedDate = GETDATE()
|
||||
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
|
||||
WHERE LOWER(LTRIM(RTRIM(Department))) = LOWER(@oldDepartmentName)
|
||||
`);
|
||||
|
||||
@@ -3518,7 +3658,7 @@ app.delete('/api/asset-departments/:id', requireAssetOrAdmin, async (req, res) =
|
||||
.query(`
|
||||
UPDATE AssetInventory
|
||||
SET Department = NULL,
|
||||
UpdatedDate = GETDATE()
|
||||
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
|
||||
WHERE LOWER(LTRIM(RTRIM(Department))) = LOWER(@departmentName)
|
||||
`);
|
||||
|
||||
@@ -3672,7 +3812,7 @@ app.put('/api/asset-projects/:id', requireAssetOrAdmin, async (req, res) => {
|
||||
.query(`
|
||||
UPDATE AssetProjects
|
||||
SET ProjectName = @projectName,
|
||||
UpdatedDate = GETDATE()
|
||||
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
|
||||
WHERE ProjectId = @projectId
|
||||
`);
|
||||
|
||||
@@ -3682,7 +3822,7 @@ app.put('/api/asset-projects/:id', requireAssetOrAdmin, async (req, res) => {
|
||||
.query(`
|
||||
UPDATE AssetInventory
|
||||
SET Project = @newProjectName,
|
||||
UpdatedDate = GETDATE()
|
||||
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
|
||||
WHERE LOWER(LTRIM(RTRIM(Project))) = LOWER(@oldProjectName)
|
||||
`);
|
||||
|
||||
@@ -3736,7 +3876,7 @@ app.delete('/api/asset-projects/:id', requireAssetOrAdmin, async (req, res) => {
|
||||
.query(`
|
||||
UPDATE AssetInventory
|
||||
SET Project = NULL,
|
||||
UpdatedDate = GETDATE()
|
||||
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
|
||||
WHERE LOWER(LTRIM(RTRIM(Project))) = LOWER(@projectName)
|
||||
`);
|
||||
|
||||
@@ -4560,7 +4700,7 @@ app.post('/api/asset-borrows/:id/process', requireAssetOrAdmin, async (req, res)
|
||||
UsedQuantity = @usedQuantity,
|
||||
Status = @status,
|
||||
ExportedBy = @exportedBy,
|
||||
UpdatedDate = GETDATE()
|
||||
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
|
||||
WHERE AssetId = @assetId
|
||||
`);
|
||||
} else {
|
||||
@@ -4619,7 +4759,7 @@ app.post('/api/asset-borrows/:id/process', requireAssetOrAdmin, async (req, res)
|
||||
UsedQuantity = @usedQuantity,
|
||||
Status = @status,
|
||||
ExportedBy = CASE WHEN @borrower IS NULL THEN NULL ELSE @exportedBy END,
|
||||
UpdatedDate = GETDATE()
|
||||
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
|
||||
WHERE AssetId = @assetId
|
||||
`);
|
||||
|
||||
@@ -4665,7 +4805,7 @@ app.post('/api/asset-borrows/:id/process', requireAssetOrAdmin, async (req, res)
|
||||
THEN 'returned'
|
||||
ELSE RequestStatus
|
||||
END,
|
||||
UpdatedDate = GETDATE()
|
||||
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
|
||||
WHERE BorrowId = @borrowId
|
||||
`);
|
||||
}
|
||||
@@ -4742,7 +4882,7 @@ app.post('/api/asset-borrows/:id/process', requireAssetOrAdmin, async (req, res)
|
||||
THEN 'returned'
|
||||
ELSE RequestStatus
|
||||
END,
|
||||
UpdatedDate = GETDATE()
|
||||
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
|
||||
WHERE BorrowId = @borrowId
|
||||
`);
|
||||
|
||||
@@ -4764,8 +4904,8 @@ app.post('/api/asset-borrows/:id/process', requireAssetOrAdmin, async (req, res)
|
||||
RejectReason = @rejectReason,
|
||||
ProcessedBy = @processedBy,
|
||||
ProcessedByName = @processedByName,
|
||||
ProcessedDate = GETDATE(),
|
||||
UpdatedDate = GETDATE()
|
||||
ProcessedDate = DATEADD(HOUR, 7, SYSUTCDATETIME()),
|
||||
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
|
||||
WHERE BorrowId = @borrowId
|
||||
`);
|
||||
|
||||
@@ -5194,7 +5334,7 @@ app.post('/api/assets/:id/damage-disposal', requireAssetOrAdmin, async (req, res
|
||||
NewQuantity = @newQuantity,
|
||||
UsedQuantity = @usedQuantity,
|
||||
Status = @status,
|
||||
UpdatedDate = GETDATE()
|
||||
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
|
||||
WHERE AssetId = @assetId
|
||||
`);
|
||||
|
||||
@@ -5449,7 +5589,7 @@ app.post('/api/assets/:id/export', requireAssetOrAdmin, async (req, res) => {
|
||||
Status = @status,
|
||||
ExportedBy = @exportedBy,
|
||||
Notes = @notes,
|
||||
UpdatedDate = GETDATE()
|
||||
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
|
||||
WHERE AssetId = @assetId
|
||||
`);
|
||||
|
||||
@@ -5651,7 +5791,7 @@ app.put('/api/assets/:id', requireAssetOrAdmin, async (req, res) => {
|
||||
PurchasePrice = @purchasePrice,
|
||||
Status = @status,
|
||||
Notes = @notes,
|
||||
UpdatedDate = GETDATE()
|
||||
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
|
||||
WHERE AssetId = @assetId
|
||||
`);
|
||||
|
||||
@@ -5868,7 +6008,7 @@ app.post('/api/assets/import', requireAssetOrAdmin, upload.single('file'), async
|
||||
PurchasePrice = @purchasePrice,
|
||||
Status = @status,
|
||||
Notes = @notes,
|
||||
UpdatedDate = GETDATE()
|
||||
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
|
||||
WHEN NOT MATCHED THEN
|
||||
INSERT (
|
||||
AssetCode, AssetName, Model, SerialNumber,
|
||||
|
||||
@@ -32,7 +32,7 @@ BEGIN
|
||||
FullName NVARCHAR(100),
|
||||
Role NVARCHAR(50) NOT NULL,
|
||||
Status NVARCHAR(20) DEFAULT 'Active',
|
||||
CreatedDate DATETIME DEFAULT GETDATE(),
|
||||
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||
LastLogin DATETIME,
|
||||
IsActive BIT DEFAULT 1
|
||||
);
|
||||
@@ -51,8 +51,8 @@ BEGIN
|
||||
Status NVARCHAR(20) DEFAULT 'online',
|
||||
Icon NVARCHAR(50),
|
||||
Description NVARCHAR(500),
|
||||
CreatedDate DATETIME DEFAULT GETDATE(),
|
||||
UpdatedDate DATETIME DEFAULT GETDATE()
|
||||
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME()))
|
||||
);
|
||||
PRINT 'Table Applications created successfully.';
|
||||
END
|
||||
@@ -72,8 +72,8 @@ BEGIN
|
||||
AccessLevel NVARCHAR(50),
|
||||
Status NVARCHAR(20) DEFAULT 'Active',
|
||||
Notes NVARCHAR(MAX),
|
||||
CreatedDate DATETIME DEFAULT GETDATE(),
|
||||
UpdatedDate DATETIME DEFAULT GETDATE(),
|
||||
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||
FOREIGN KEY (UserId) REFERENCES Users(UserId) 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',
|
||||
Notes NVARCHAR(MAX),
|
||||
CreatedBy INT NULL,
|
||||
CreatedDate DATETIME DEFAULT GETDATE(),
|
||||
UpdatedDate DATETIME DEFAULT GETDATE(),
|
||||
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL
|
||||
);
|
||||
PRINT 'Table AssetInventory created successfully.';
|
||||
@@ -175,8 +175,8 @@ BEGIN
|
||||
CREATE TABLE AssetDepartments (
|
||||
DepartmentId INT PRIMARY KEY IDENTITY(1,1),
|
||||
DepartmentName NVARCHAR(100) NOT NULL,
|
||||
CreatedDate DATETIME DEFAULT GETDATE(),
|
||||
UpdatedDate DATETIME DEFAULT GETDATE()
|
||||
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME()))
|
||||
);
|
||||
PRINT 'Table AssetDepartments created successfully.';
|
||||
END
|
||||
@@ -204,8 +204,8 @@ BEGIN
|
||||
CREATE TABLE AssetProjects (
|
||||
ProjectId INT PRIMARY KEY IDENTITY(1,1),
|
||||
ProjectName NVARCHAR(150) NOT NULL,
|
||||
CreatedDate DATETIME DEFAULT GETDATE(),
|
||||
UpdatedDate DATETIME DEFAULT GETDATE()
|
||||
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME()))
|
||||
);
|
||||
PRINT 'Table AssetProjects created successfully.';
|
||||
END
|
||||
@@ -224,15 +224,15 @@ BEGIN
|
||||
BorrowQuantity INT NOT NULL DEFAULT 1,
|
||||
ReturnedQuantity INT NOT NULL DEFAULT 0,
|
||||
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,
|
||||
RejectReason NVARCHAR(1000) NULL,
|
||||
CreatedBy INT NULL,
|
||||
ProcessedBy INT NULL,
|
||||
ProcessedByName NVARCHAR(100) NULL,
|
||||
ProcessedDate DATETIME NULL,
|
||||
CreatedDate DATETIME DEFAULT GETDATE(),
|
||||
UpdatedDate DATETIME DEFAULT GETDATE(),
|
||||
CreatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||
UpdatedDate DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||
FOREIGN KEY (AssetId) REFERENCES AssetInventory(AssetId) ON DELETE CASCADE,
|
||||
FOREIGN KEY (CreatedBy) REFERENCES Users(UserId) ON DELETE SET NULL,
|
||||
FOREIGN KEY (ProcessedBy) REFERENCES Users(UserId) ON DELETE SET NULL
|
||||
@@ -247,12 +247,12 @@ END
|
||||
|
||||
IF COL_LENGTH('dbo.AssetBorrowRequests', 'BorrowDate') IS NULL
|
||||
BEGIN
|
||||
ALTER TABLE AssetBorrowRequests ADD BorrowDate DATE NOT NULL CONSTRAINT DF_AssetBorrowRequests_BorrowDate DEFAULT(CAST(GETDATE() AS DATE));
|
||||
ALTER TABLE AssetBorrowRequests ADD BorrowDate DATE NOT NULL CONSTRAINT DF_AssetBorrowRequests_BorrowDate DEFAULT(CAST(DATEADD(HOUR, 7, SYSUTCDATETIME()) AS DATE));
|
||||
END
|
||||
|
||||
IF COL_LENGTH('dbo.AssetBorrowRequests', 'UpdatedDate') IS NULL
|
||||
BEGIN
|
||||
ALTER TABLE AssetBorrowRequests ADD UpdatedDate DATETIME NOT NULL CONSTRAINT DF_AssetBorrowRequests_UpdatedDate DEFAULT(GETDATE());
|
||||
ALTER TABLE AssetBorrowRequests ADD UpdatedDate DATETIME NOT NULL CONSTRAINT DF_AssetBorrowRequests_UpdatedDate DEFAULT(DATEADD(HOUR, 7, SYSUTCDATETIME()));
|
||||
END
|
||||
|
||||
IF COL_LENGTH('dbo.AssetBorrowRequests', 'RequestType') IS NULL
|
||||
@@ -319,7 +319,7 @@ BEGIN
|
||||
BorrowId INT NOT NULL,
|
||||
ReturnId INT NOT NULL,
|
||||
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 (ReturnId) REFERENCES AssetBorrowRequests(BorrowId) ON DELETE NO ACTION
|
||||
);
|
||||
@@ -373,7 +373,7 @@ BEGIN
|
||||
THEN 'returned'
|
||||
ELSE borrowRows.RequestStatus
|
||||
END,
|
||||
UpdatedDate = GETDATE()
|
||||
UpdatedDate = DATEADD(HOUR, 7, SYSUTCDATETIME())
|
||||
FROM AssetBorrowRequests borrowRows
|
||||
INNER JOIN (
|
||||
SELECT links.BorrowId, SUM(ISNULL(links.Quantity, 0)) AS ReturnedQuantity
|
||||
@@ -401,9 +401,9 @@ BEGIN
|
||||
ExportedByName NVARCHAR(100) NOT NULL,
|
||||
ExportNote NVARCHAR(1000) NULL,
|
||||
CreatedBy INT NULL,
|
||||
ExportedDate DATETIME NOT NULL DEFAULT GETDATE(),
|
||||
CreatedDate DATETIME NOT NULL DEFAULT GETDATE(),
|
||||
UpdatedDate DATETIME NOT NULL DEFAULT GETDATE(),
|
||||
ExportedDate 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
|
||||
);
|
||||
PRINT 'Table AssetExportHistory created successfully.';
|
||||
@@ -451,17 +451,17 @@ END
|
||||
|
||||
IF COL_LENGTH('dbo.AssetExportHistory', 'ExportedDate') IS NULL
|
||||
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
|
||||
|
||||
IF COL_LENGTH('dbo.AssetExportHistory', 'CreatedDate') IS NULL
|
||||
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
|
||||
|
||||
IF COL_LENGTH('dbo.AssetExportHistory', 'UpdatedDate') IS NULL
|
||||
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
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = 'FK_AssetExportHistory_CreatedBy')
|
||||
@@ -512,9 +512,9 @@ BEGIN
|
||||
ActionNote NVARCHAR(1000) NULL,
|
||||
CreatedBy INT NULL,
|
||||
CreatedByName NVARCHAR(100) NULL,
|
||||
ActionDate DATETIME NOT NULL DEFAULT GETDATE(),
|
||||
CreatedDate DATETIME NOT NULL DEFAULT GETDATE(),
|
||||
UpdatedDate DATETIME NOT NULL DEFAULT GETDATE(),
|
||||
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
|
||||
);
|
||||
@@ -534,7 +534,7 @@ BEGIN
|
||||
RecordId INT,
|
||||
OldValue NVARCHAR(MAX),
|
||||
NewValue NVARCHAR(MAX),
|
||||
Timestamp DATETIME DEFAULT GETDATE(),
|
||||
Timestamp DATETIME DEFAULT (DATEADD(HOUR, 7, SYSUTCDATETIME())),
|
||||
FOREIGN KEY (UserId) REFERENCES Users(UserId)
|
||||
);
|
||||
PRINT 'Table AuditLog created successfully.';
|
||||
|
||||
@@ -8,6 +8,8 @@ services:
|
||||
environment:
|
||||
NODE_ENV: ${NODE_ENV:-production}
|
||||
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_USER: ${DB_USER:-sa}
|
||||
DB_PASSWORD: ${DB_PASSWORD:-changeme}
|
||||
|
||||
@@ -10,6 +10,8 @@ services:
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
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_USER: ${DB_USER:-sa}
|
||||
DB_PASSWORD: ${DB_PASSWORD:-changeme}
|
||||
|
||||
@@ -1,6 +1,34 @@
|
||||
// VaultSentinel - Account Management Application
|
||||
// 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 {
|
||||
constructor() {
|
||||
// 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">
|
||||
<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">
|
||||
<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 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' };
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!value) return '-';
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return String(value);
|
||||
return date.toLocaleDateString();
|
||||
return APP_DATE_FORMATTER.format(date);
|
||||
}
|
||||
|
||||
toDateInputValue(value) {
|
||||
if (!value) return '';
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return '';
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
const parts = this.getAppTimeParts(value);
|
||||
if (!parts) return '';
|
||||
return `${parts.year}-${parts.month}-${parts.day}`;
|
||||
}
|
||||
|
||||
formatBorrowerDisplay(name, quantity = 1) {
|
||||
@@ -5019,16 +5075,7 @@ class AccountManager {
|
||||
.slice(0, 32);
|
||||
|
||||
const base = toToken(payload.model) || toToken(payload.serialNumber) || toToken(payload.assetName) || 'ASSET';
|
||||
const now = new Date();
|
||||
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 timestamp = this.formatTimestampForCode(new Date(), true);
|
||||
const randomSuffix = String(Math.floor(Math.random() * 100)).padStart(2, '0');
|
||||
return `AST-${base}-${timestamp}${randomSuffix}`;
|
||||
}
|
||||
@@ -6481,8 +6528,7 @@ class AccountManager {
|
||||
const workbook = window.XLSX.utils.book_new();
|
||||
window.XLSX.utils.book_append_sheet(workbook, worksheet, 'TaiSan');
|
||||
|
||||
const now = new Date();
|
||||
const timestamp = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`;
|
||||
const timestamp = this.formatTimestampForCode(new Date()).slice(0, 8);
|
||||
window.XLSX.writeFile(workbook, `danh-sach-tai-san-${timestamp}.xlsx`);
|
||||
this.notifySuccess('Xuất Excel thành công');
|
||||
}
|
||||
@@ -7418,7 +7464,7 @@ class AccountManager {
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return String(value);
|
||||
}
|
||||
return date.toLocaleString();
|
||||
return APP_DATE_TIME_FORMATTER.format(date);
|
||||
}
|
||||
|
||||
// ========== Users Management ==========
|
||||
|
||||
Reference in New Issue
Block a user