commit
32a36cceff
444 files changed
+67186
No files matched your search
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Convert snake_case string to camelCase
|
||||
* @param {string} str - String in snake_case
|
||||
* @returns {string} String in camelCase
|
||||
*/
|
||||
function snakeToCamel(str) {
|
||||
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert all keys in an object from snake_case to camelCase
|
||||
* @param {Object} obj - Object with snake_case keys
|
||||
* @returns {Object} Object with camelCase keys
|
||||
*/
|
||||
function convertKeysToCamelCase(obj) {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(item => convertKeysToCamelCase(item));
|
||||
}
|
||||
|
||||
const converted = {};
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
const camelKey = snakeToCamel(key);
|
||||
converted[camelKey] = convertKeysToCamelCase(obj[key]);
|
||||
}
|
||||
}
|
||||
return converted;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
snakeToCamel,
|
||||
convertKeysToCamelCase
|
||||
};
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Date and Time Utilities for WIB (UTC+7) Timezone Handling
|
||||
* Implements D-1 data filtering logic for dashboard display before 17:00 WIB
|
||||
*/
|
||||
|
||||
const CUTOFF_HOUR = process.env.CUTOFF_HOUR_OVERRIDE
|
||||
? parseInt(process.env.CUTOFF_HOUR_OVERRIDE, 10)
|
||||
: 17;
|
||||
|
||||
/**
|
||||
* Get current date/time in WIB (Asia/Jakarta, UTC+7) timezone
|
||||
* @returns {Date} Current date in WIB
|
||||
*/
|
||||
function getCurrentWibDate() {
|
||||
const now = new Date();
|
||||
|
||||
// Convert to WIB using toLocaleString with Jakarta timezone
|
||||
const wibDateString = now.toLocaleString('en-US', {
|
||||
timeZone: 'Asia/Jakarta',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
|
||||
// Parse the formatted string back to Date object
|
||||
return new Date(wibDateString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current WIB time is before the cutoff hour (default 17:00)
|
||||
* @returns {boolean} True if before cutoff, false otherwise
|
||||
*/
|
||||
function isBeforeCutoffTime() {
|
||||
const wibNow = getCurrentWibDate();
|
||||
const currentHour = wibNow.getHours();
|
||||
return currentHour < CUTOFF_HOUR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the effective query date (D-1 if before cutoff, else today)
|
||||
* @returns {Date} Effective date for data queries
|
||||
*/
|
||||
function getEffectiveQueryDate() {
|
||||
const wibToday = getCurrentWibDate();
|
||||
|
||||
if (isBeforeCutoffTime()) {
|
||||
// Return D-1 (previous day)
|
||||
const yesterday = new Date(wibToday);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
return yesterday;
|
||||
}
|
||||
|
||||
return wibToday;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate effective day of cycle accounting for D-1 filter
|
||||
* @param {Date|string} cycleStartDate - Cycle start date
|
||||
* @returns {number} Effective cycle day (0-indexed from start date)
|
||||
*/
|
||||
function getEffectiveCycleDay(cycleStartDate) {
|
||||
const effectiveDate = getEffectiveQueryDate();
|
||||
const startDate = new Date(cycleStartDate);
|
||||
|
||||
// Reset hours to compare dates only
|
||||
effectiveDate.setHours(0, 0, 0, 0);
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
|
||||
const diffMs = effectiveDate.getTime() - startDate.getTime();
|
||||
|
||||
// Return -1 if effective date is before cycle start (edge case)
|
||||
if (diffMs < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const dayOfCycle = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
return dayOfCycle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date as YYYY-MM-DD for database queries
|
||||
* @param {Date} date - Date object to format
|
||||
* @returns {string} Date in YYYY-MM-DD format
|
||||
*/
|
||||
function formatDateForDb(date) {
|
||||
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}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get previous date (D-1) from a given date
|
||||
* @param {string} dateString - Date string in YYYY-MM-DD format
|
||||
* @returns {string} Previous date in YYYY-MM-DD format
|
||||
*/
|
||||
function getPreviousDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
date.setDate(date.getDate() - 1);
|
||||
return formatDateForDb(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create API response metadata for D-1 filtering
|
||||
* @param {boolean} isD1Applied - Whether D-1 filter was applied
|
||||
* @returns {object} Metadata object for API response
|
||||
*/
|
||||
function createResponseMetadata(isD1Applied = false) {
|
||||
const wibNow = getCurrentWibDate();
|
||||
const effectiveDate = getEffectiveQueryDate();
|
||||
|
||||
return {
|
||||
effectiveDate: formatDateForDb(effectiveDate),
|
||||
queryDate: formatDateForDb(wibNow),
|
||||
isD1Filter: isD1Applied,
|
||||
currentWibTime: wibNow.toLocaleTimeString('id-ID', {
|
||||
timeZone: 'Asia/Jakarta',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getCurrentWibDate,
|
||||
isBeforeCutoffTime,
|
||||
getEffectiveQueryDate,
|
||||
getEffectiveCycleDay,
|
||||
formatDateForDb,
|
||||
getPreviousDate,
|
||||
createResponseMetadata,
|
||||
CUTOFF_HOUR // Export for testing
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Custom Error Classes
|
||||
* For structured error handling with proper status codes
|
||||
*/
|
||||
|
||||
class AppError extends Error {
|
||||
constructor(message, statusCode = 500, code = 'INTERNAL_ERROR') {
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
this.code = code;
|
||||
this.name = this.constructor.name;
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
|
||||
class ValidationError extends AppError {
|
||||
constructor(message) {
|
||||
super(message, 400, 'VALIDATION_ERROR');
|
||||
}
|
||||
}
|
||||
|
||||
class NotFoundError extends AppError {
|
||||
constructor(resource) {
|
||||
super(`${resource} not found`, 404, 'NOT_FOUND');
|
||||
}
|
||||
}
|
||||
|
||||
class UnauthorizedError extends AppError {
|
||||
constructor(message = 'Unauthorized') {
|
||||
super(message, 401, 'UNAUTHORIZED');
|
||||
}
|
||||
}
|
||||
|
||||
class ForbiddenError extends AppError {
|
||||
constructor(message = 'Forbidden') {
|
||||
super(message, 403, 'FORBIDDEN');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
AppError,
|
||||
ValidationError,
|
||||
NotFoundError,
|
||||
UnauthorizedError,
|
||||
ForbiddenError,
|
||||
};
|
||||
Reference in new issue
Block a user