313 lines
8.4 KiB
JavaScript
313 lines
8.4 KiB
JavaScript
const { DateTime } = require('luxon');
|
|
|
|
const TIMEZONE = 'America/New_York';
|
|
const DB_TIMEZONE = 'UTC-05:00';
|
|
const BUSINESS_DAY_START_HOUR = 1; // 1 AM Eastern
|
|
const WEEK_START_DAY = 7; // Sunday (Luxon uses 1 = Monday, 7 = Sunday)
|
|
const DB_DATETIME_FORMAT = 'yyyy-LL-dd HH:mm:ss';
|
|
|
|
const isDateTime = (value) => DateTime.isDateTime(value);
|
|
|
|
const ensureDateTime = (value, { zone = TIMEZONE } = {}) => {
|
|
if (!value) return null;
|
|
|
|
if (isDateTime(value)) {
|
|
return value.setZone(zone);
|
|
}
|
|
|
|
if (value instanceof Date) {
|
|
return DateTime.fromJSDate(value, { zone });
|
|
}
|
|
|
|
if (typeof value === 'number') {
|
|
return DateTime.fromMillis(value, { zone });
|
|
}
|
|
|
|
if (typeof value === 'string') {
|
|
let dt = DateTime.fromISO(value, { zone, setZone: true });
|
|
if (!dt.isValid) {
|
|
dt = DateTime.fromSQL(value, { zone });
|
|
}
|
|
return dt.isValid ? dt : null;
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
const getNow = () => DateTime.now().setZone(TIMEZONE);
|
|
|
|
const getDayStart = (input = getNow()) => {
|
|
const dt = ensureDateTime(input);
|
|
if (!dt || !dt.isValid) {
|
|
const fallback = getNow();
|
|
return fallback.set({
|
|
hour: BUSINESS_DAY_START_HOUR,
|
|
minute: 0,
|
|
second: 0,
|
|
millisecond: 0
|
|
});
|
|
}
|
|
|
|
const sameDayStart = dt.set({
|
|
hour: BUSINESS_DAY_START_HOUR,
|
|
minute: 0,
|
|
second: 0,
|
|
millisecond: 0
|
|
});
|
|
|
|
return dt.hour < BUSINESS_DAY_START_HOUR
|
|
? sameDayStart.minus({ days: 1 })
|
|
: sameDayStart;
|
|
};
|
|
|
|
const getDayEnd = (input = getNow()) => {
|
|
return getDayStart(input).plus({ days: 1 }).minus({ milliseconds: 1 });
|
|
};
|
|
|
|
const getWeekStart = (input = getNow()) => {
|
|
const dt = ensureDateTime(input);
|
|
if (!dt || !dt.isValid) {
|
|
return getDayStart();
|
|
}
|
|
|
|
const startOfWeek = dt.set({ weekday: WEEK_START_DAY }).startOf('day');
|
|
const normalized = startOfWeek > dt ? startOfWeek.minus({ weeks: 1 }) : startOfWeek;
|
|
return normalized.set({
|
|
hour: BUSINESS_DAY_START_HOUR,
|
|
minute: 0,
|
|
second: 0,
|
|
millisecond: 0
|
|
});
|
|
};
|
|
|
|
const getRangeForTimeRange = (timeRange = 'today', now = getNow()) => {
|
|
const current = ensureDateTime(now);
|
|
if (!current || !current.isValid) {
|
|
throw new Error('Invalid reference time for range calculation');
|
|
}
|
|
|
|
switch (timeRange) {
|
|
case 'today': {
|
|
return {
|
|
start: getDayStart(current),
|
|
end: getDayEnd(current)
|
|
};
|
|
}
|
|
case 'yesterday': {
|
|
const target = current.minus({ days: 1 });
|
|
return {
|
|
start: getDayStart(target),
|
|
end: getDayEnd(target)
|
|
};
|
|
}
|
|
case 'twoDaysAgo': {
|
|
const target = current.minus({ days: 2 });
|
|
return {
|
|
start: getDayStart(target),
|
|
end: getDayEnd(target)
|
|
};
|
|
}
|
|
case 'thisWeek': {
|
|
return {
|
|
start: getWeekStart(current),
|
|
end: getDayEnd(current)
|
|
};
|
|
}
|
|
case 'lastWeek': {
|
|
const lastWeek = current.minus({ weeks: 1 });
|
|
const weekStart = getWeekStart(lastWeek);
|
|
const weekEnd = weekStart.plus({ days: 6 });
|
|
return {
|
|
start: weekStart,
|
|
end: getDayEnd(weekEnd)
|
|
};
|
|
}
|
|
case 'thisMonth': {
|
|
const dayStart = getDayStart(current);
|
|
const monthStart = dayStart.startOf('month').set({ hour: BUSINESS_DAY_START_HOUR });
|
|
return {
|
|
start: monthStart,
|
|
end: getDayEnd(current)
|
|
};
|
|
}
|
|
case 'lastMonth': {
|
|
const lastMonth = current.minus({ months: 1 });
|
|
const monthStart = lastMonth
|
|
.startOf('month')
|
|
.set({ hour: BUSINESS_DAY_START_HOUR, minute: 0, second: 0, millisecond: 0 });
|
|
const monthEnd = monthStart.plus({ months: 1 }).minus({ days: 1 });
|
|
return {
|
|
start: monthStart,
|
|
end: getDayEnd(monthEnd)
|
|
};
|
|
}
|
|
case 'last7days': {
|
|
const dayStart = getDayStart(current);
|
|
return {
|
|
start: dayStart.minus({ days: 6 }),
|
|
end: getDayEnd(current)
|
|
};
|
|
}
|
|
case 'last30days': {
|
|
const dayStart = getDayStart(current);
|
|
return {
|
|
start: dayStart.minus({ days: 29 }),
|
|
end: getDayEnd(current)
|
|
};
|
|
}
|
|
case 'last90days': {
|
|
const dayStart = getDayStart(current);
|
|
return {
|
|
start: dayStart.minus({ days: 89 }),
|
|
end: getDayEnd(current)
|
|
};
|
|
}
|
|
case 'previous7days': {
|
|
const currentPeriodStart = getDayStart(current).minus({ days: 6 });
|
|
const previousEndDay = currentPeriodStart.minus({ days: 1 });
|
|
const previousStartDay = previousEndDay.minus({ days: 6 });
|
|
return {
|
|
start: getDayStart(previousStartDay),
|
|
end: getDayEnd(previousEndDay)
|
|
};
|
|
}
|
|
case 'previous30days': {
|
|
const currentPeriodStart = getDayStart(current).minus({ days: 29 });
|
|
const previousEndDay = currentPeriodStart.minus({ days: 1 });
|
|
const previousStartDay = previousEndDay.minus({ days: 29 });
|
|
return {
|
|
start: getDayStart(previousStartDay),
|
|
end: getDayEnd(previousEndDay)
|
|
};
|
|
}
|
|
case 'previous90days': {
|
|
const currentPeriodStart = getDayStart(current).minus({ days: 89 });
|
|
const previousEndDay = currentPeriodStart.minus({ days: 1 });
|
|
const previousStartDay = previousEndDay.minus({ days: 89 });
|
|
return {
|
|
start: getDayStart(previousStartDay),
|
|
end: getDayEnd(previousEndDay)
|
|
};
|
|
}
|
|
default:
|
|
throw new Error(`Unknown time range: ${timeRange}`);
|
|
}
|
|
};
|
|
|
|
const toDatabaseSqlString = (dt) => {
|
|
const normalized = ensureDateTime(dt);
|
|
if (!normalized || !normalized.isValid) {
|
|
throw new Error('Invalid datetime provided for SQL conversion');
|
|
}
|
|
const dbTime = normalized.setZone(DB_TIMEZONE, { keepLocalTime: true });
|
|
return dbTime.toFormat(DB_DATETIME_FORMAT);
|
|
};
|
|
|
|
const formatBusinessDate = (input) => {
|
|
const dt = ensureDateTime(input);
|
|
if (!dt || !dt.isValid) return '';
|
|
return dt.setZone(TIMEZONE).toFormat('LLL d, yyyy');
|
|
};
|
|
|
|
const getTimeRangeLabel = (timeRange) => {
|
|
const labels = {
|
|
today: 'Today',
|
|
yesterday: 'Yesterday',
|
|
twoDaysAgo: 'Two Days Ago',
|
|
thisWeek: 'This Week',
|
|
lastWeek: 'Last Week',
|
|
thisMonth: 'This Month',
|
|
lastMonth: 'Last Month',
|
|
last7days: 'Last 7 Days',
|
|
last30days: 'Last 30 Days',
|
|
last90days: 'Last 90 Days',
|
|
previous7days: 'Previous 7 Days',
|
|
previous30days: 'Previous 30 Days',
|
|
previous90days: 'Previous 90 Days'
|
|
};
|
|
|
|
return labels[timeRange] || timeRange;
|
|
};
|
|
|
|
const getTimeRangeConditions = (timeRange, startDate, endDate) => {
|
|
if (timeRange === 'custom' && startDate && endDate) {
|
|
const start = ensureDateTime(startDate);
|
|
const end = ensureDateTime(endDate);
|
|
|
|
if (!start || !start.isValid || !end || !end.isValid) {
|
|
throw new Error('Invalid custom date range provided');
|
|
}
|
|
|
|
return {
|
|
whereClause: 'date_placed >= ? AND date_placed <= ?',
|
|
params: [toDatabaseSqlString(start), toDatabaseSqlString(end)],
|
|
dateRange: {
|
|
start: start.toUTC().toISO(),
|
|
end: end.toUTC().toISO(),
|
|
label: `${formatBusinessDate(start)} - ${formatBusinessDate(end)}`
|
|
}
|
|
};
|
|
}
|
|
|
|
const normalizedRange = timeRange || 'today';
|
|
const range = getRangeForTimeRange(normalizedRange);
|
|
|
|
return {
|
|
whereClause: 'date_placed >= ? AND date_placed <= ?',
|
|
params: [toDatabaseSqlString(range.start), toDatabaseSqlString(range.end)],
|
|
dateRange: {
|
|
start: range.start.toUTC().toISO(),
|
|
end: range.end.toUTC().toISO(),
|
|
label: getTimeRangeLabel(normalizedRange)
|
|
}
|
|
};
|
|
};
|
|
|
|
const getBusinessDayBounds = (timeRange) => {
|
|
const range = getRangeForTimeRange(timeRange);
|
|
return {
|
|
start: range.start.toJSDate(),
|
|
end: range.end.toJSDate()
|
|
};
|
|
};
|
|
|
|
const parseBusinessDate = (mysqlDatetime) => {
|
|
if (!mysqlDatetime || mysqlDatetime === '0000-00-00 00:00:00') {
|
|
return null;
|
|
}
|
|
|
|
const dt = DateTime.fromSQL(mysqlDatetime, { zone: DB_TIMEZONE });
|
|
if (!dt.isValid) {
|
|
console.error('[timeUtils] Failed to parse MySQL datetime:', mysqlDatetime, dt.invalidExplanation);
|
|
return null;
|
|
}
|
|
|
|
return dt.toUTC().toJSDate();
|
|
};
|
|
|
|
const formatMySQLDate = (input) => {
|
|
if (!input) return null;
|
|
|
|
const dt = ensureDateTime(input, { zone: 'utc' });
|
|
if (!dt || !dt.isValid) return null;
|
|
|
|
return dt.setZone(DB_TIMEZONE).toFormat(DB_DATETIME_FORMAT);
|
|
};
|
|
|
|
module.exports = {
|
|
getBusinessDayBounds,
|
|
getTimeRangeConditions,
|
|
formatBusinessDate,
|
|
getTimeRangeLabel,
|
|
parseBusinessDate,
|
|
formatMySQLDate,
|
|
// Expose helpers for tests or advanced consumers
|
|
_internal: {
|
|
getDayStart,
|
|
getDayEnd,
|
|
getWeekStart,
|
|
getRangeForTimeRange,
|
|
BUSINESS_DAY_START_HOUR
|
|
}
|
|
};
|