diff --git a/Dockerfile b/Dockerfile index ce7b8fe..9ce880d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 @@ -12,4 +14,4 @@ COPY public ./public EXPOSE 3000 -CMD ["node", "backend/server.js"] \ No newline at end of file +CMD ["node", "backend/server.js"] diff --git a/backend/server.js b/backend/server.js index c442192..dbd06a1 100644 --- a/backend/server.js +++ b/backend/server.js @@ -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, diff --git a/database/setup.sql b/database/setup.sql index e68ef25..48c0062 100644 --- a/database/setup.sql +++ b/database/setup.sql @@ -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.'; diff --git a/docker-compose.image.yml b/docker-compose.image.yml index f265fd0..a8b0e23 100644 --- a/docker-compose.image.yml +++ b/docker-compose.image.yml @@ -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} @@ -23,4 +25,4 @@ services: SMTP_USER: ${SMTP_USER:-} SMTP_PASS: ${SMTP_PASS:-} SMTP_FROM: ${SMTP_FROM:-} - EMAIL_VERIFY_TOKEN_TTL_MINUTES: ${EMAIL_VERIFY_TOKEN_TTL_MINUTES:-30} \ No newline at end of file + EMAIL_VERIFY_TOKEN_TTL_MINUTES: ${EMAIL_VERIFY_TOKEN_TTL_MINUTES:-30} diff --git a/docker-compose.yml b/docker-compose.yml index df6093b..7285f39 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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} @@ -25,4 +27,4 @@ services: SMTP_USER: ${SMTP_USER:-} SMTP_PASS: ${SMTP_PASS:-} SMTP_FROM: ${SMTP_FROM:-} - EMAIL_VERIFY_TOKEN_TTL_MINUTES: ${EMAIL_VERIFY_TOKEN_TTL_MINUTES:-30} \ No newline at end of file + EMAIL_VERIFY_TOKEN_TTL_MINUTES: ${EMAIL_VERIFY_TOKEN_TTL_MINUTES:-30} diff --git a/public/js/app.js b/public/js/app.js index fa8f752..696a12a 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -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 {
Last Updated
- ${new Date().toLocaleDateString()} + ${APP_DATE_FORMATTER.format(new Date())}
@@ -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 ==========