Fix time periods on financial overview, remove some logging
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"luxon": "^3.5.0",
|
||||
"morgan": "^1.10.0",
|
||||
"mysql2": "^3.6.5",
|
||||
"ssh2": "^1.14.0"
|
||||
@@ -826,6 +827,15 @@
|
||||
"url": "https://github.com/sponsors/wellwelwel"
|
||||
}
|
||||
},
|
||||
"node_modules/luxon": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz",
|
||||
"integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
|
||||
@@ -14,9 +14,10 @@
|
||||
"morgan": "^1.10.0",
|
||||
"ssh2": "^1.14.0",
|
||||
"mysql2": "^3.6.5",
|
||||
"compression": "^1.7.4"
|
||||
"compression": "^1.7.4",
|
||||
"luxon": "^3.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
const express = require('express');
|
||||
const { DateTime } = require('luxon');
|
||||
|
||||
const router = express.Router();
|
||||
const { getDbConnection, getPoolStatus } = require('../db/connection');
|
||||
const { getTimeRangeConditions, formatBusinessDate, getBusinessDayBounds } = require('../utils/timeUtils');
|
||||
const {
|
||||
getTimeRangeConditions,
|
||||
formatBusinessDate,
|
||||
getBusinessDayBounds,
|
||||
_internal: timeHelpers
|
||||
} = require('../utils/timeUtils');
|
||||
|
||||
const TIMEZONE = 'America/New_York';
|
||||
const BUSINESS_DAY_START_HOUR = timeHelpers?.BUSINESS_DAY_START_HOUR ?? 1;
|
||||
|
||||
// Image URL generation utility
|
||||
const getImageUrls = (pid, iid = 1) => {
|
||||
@@ -421,6 +431,24 @@ router.get('/financials', async (req, res) => {
|
||||
const { whereClause, params, dateRange } = getTimeRangeConditions(timeRange, startDate, endDate);
|
||||
const financialWhere = whereClause.replace(/date_placed/g, 'date_change');
|
||||
|
||||
const formatDebugBound = (value) => {
|
||||
if (!value) return 'n/a';
|
||||
const parsed = DateTime.fromSQL(value, { zone: 'UTC-05:00' });
|
||||
if (!parsed.isValid) {
|
||||
return `invalid(${value})`;
|
||||
}
|
||||
return parsed.setZone(TIMEZONE).toISO();
|
||||
};
|
||||
|
||||
console.log('[FINANCIALS] request params', {
|
||||
timeRange: timeRange || 'default',
|
||||
startDate,
|
||||
endDate,
|
||||
whereClause: financialWhere,
|
||||
params,
|
||||
boundsEastern: Array.isArray(params) ? params.map(formatDebugBound) : [],
|
||||
});
|
||||
|
||||
const [totalsRows] = await connection.execute(
|
||||
buildFinancialTotalsQuery(financialWhere),
|
||||
params
|
||||
@@ -428,6 +456,11 @@ router.get('/financials', async (req, res) => {
|
||||
|
||||
const totals = normalizeFinancialTotals(totalsRows[0]);
|
||||
|
||||
console.log('[FINANCIALS] totals query result', {
|
||||
rows: totalsRows.length,
|
||||
totals,
|
||||
});
|
||||
|
||||
const [trendRows] = await connection.execute(
|
||||
buildFinancialTrendQuery(financialWhere),
|
||||
params
|
||||
@@ -435,11 +468,25 @@ router.get('/financials', async (req, res) => {
|
||||
|
||||
const trend = trendRows.map(normalizeFinancialTrendRow);
|
||||
|
||||
console.log('[FINANCIALS] trend query result', {
|
||||
rows: trendRows.length,
|
||||
first: trend[0] || null,
|
||||
last: trend[trend.length - 1] || null,
|
||||
});
|
||||
|
||||
let previousTotals = null;
|
||||
let comparison = null;
|
||||
|
||||
const previousRange = getPreviousPeriodRange(timeRange, startDate, endDate);
|
||||
if (previousRange) {
|
||||
console.log('[FINANCIALS] previous range params', {
|
||||
timeRange: timeRange || 'default',
|
||||
prevWhere: previousRange.whereClause.replace(/date_placed/g, 'date_change'),
|
||||
params: previousRange.params,
|
||||
boundsEastern: Array.isArray(previousRange.params)
|
||||
? previousRange.params.map(formatDebugBound)
|
||||
: [],
|
||||
});
|
||||
const prevWhere = previousRange.whereClause.replace(/date_placed/g, 'date_change');
|
||||
const [previousRows] = await connection.execute(
|
||||
buildFinancialTotalsQuery(prevWhere),
|
||||
@@ -458,12 +505,37 @@ router.get('/financials', async (req, res) => {
|
||||
};
|
||||
}
|
||||
|
||||
const trendDebugSample = trend.slice(-3).map((item) => ({
|
||||
date: item.date,
|
||||
timestamp: item.timestamp,
|
||||
income: item.income,
|
||||
grossSales: item.grossSales,
|
||||
}));
|
||||
|
||||
const debugInfo = {
|
||||
serverTimeUtc: new Date().toISOString(),
|
||||
timeRange: timeRange || 'default',
|
||||
params,
|
||||
boundsEastern: Array.isArray(params) ? params.map(formatDebugBound) : [],
|
||||
trendCount: trend.length,
|
||||
trendSample: trendDebugSample,
|
||||
previousRange: previousRange
|
||||
? {
|
||||
params: previousRange.params,
|
||||
boundsEastern: Array.isArray(previousRange.params)
|
||||
? previousRange.params.map(formatDebugBound)
|
||||
: [],
|
||||
}
|
||||
: null,
|
||||
};
|
||||
|
||||
res.json({
|
||||
dateRange,
|
||||
totals,
|
||||
previousTotals,
|
||||
comparison,
|
||||
trend,
|
||||
debug: debugInfo,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in /financials:', error);
|
||||
@@ -662,44 +734,35 @@ function processShippingData(shippingResult, totalShipped) {
|
||||
}
|
||||
|
||||
function calculatePeriodProgress(timeRange) {
|
||||
const now = new Date();
|
||||
const easternTime = new Date(now.getTime() - (5 * 60 * 60 * 1000)); // UTC-5
|
||||
|
||||
switch (timeRange) {
|
||||
case 'today': {
|
||||
const { start } = getBusinessDayBounds('today');
|
||||
const businessStart = new Date(start);
|
||||
const businessEnd = new Date(businessStart);
|
||||
businessEnd.setDate(businessEnd.getDate() + 1);
|
||||
businessEnd.setHours(0, 59, 59, 999); // 12:59 AM next day
|
||||
|
||||
const elapsed = easternTime.getTime() - businessStart.getTime();
|
||||
const total = businessEnd.getTime() - businessStart.getTime();
|
||||
return Math.min(100, Math.max(0, (elapsed / total) * 100));
|
||||
}
|
||||
case 'thisWeek': {
|
||||
const startOfWeek = new Date(easternTime);
|
||||
startOfWeek.setDate(easternTime.getDate() - easternTime.getDay()); // Sunday
|
||||
startOfWeek.setHours(1, 0, 0, 0); // 1 AM business day start
|
||||
|
||||
const endOfWeek = new Date(startOfWeek);
|
||||
endOfWeek.setDate(endOfWeek.getDate() + 7);
|
||||
|
||||
const elapsed = easternTime.getTime() - startOfWeek.getTime();
|
||||
const total = endOfWeek.getTime() - startOfWeek.getTime();
|
||||
return Math.min(100, Math.max(0, (elapsed / total) * 100));
|
||||
}
|
||||
case 'thisMonth': {
|
||||
const startOfMonth = new Date(easternTime.getFullYear(), easternTime.getMonth(), 1, 1, 0, 0, 0);
|
||||
const endOfMonth = new Date(easternTime.getFullYear(), easternTime.getMonth() + 1, 1, 0, 59, 59, 999);
|
||||
|
||||
const elapsed = easternTime.getTime() - startOfMonth.getTime();
|
||||
const total = endOfMonth.getTime() - startOfMonth.getTime();
|
||||
return Math.min(100, Math.max(0, (elapsed / total) * 100));
|
||||
}
|
||||
default:
|
||||
return 100;
|
||||
if (!['today', 'thisWeek', 'thisMonth'].includes(timeRange)) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
const now = DateTime.now().setZone(TIMEZONE);
|
||||
|
||||
let range;
|
||||
try {
|
||||
range = timeHelpers.getRangeForTimeRange(timeRange, now);
|
||||
} catch (error) {
|
||||
console.error(`[STATS] Failed to derive range for ${timeRange}:`, error);
|
||||
return 100;
|
||||
}
|
||||
|
||||
if (!range?.start || !range?.end) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
const total = range.end.toMillis() - range.start.toMillis();
|
||||
if (total <= 0) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
const elapsed = Math.min(
|
||||
Math.max(now.toMillis() - range.start.toMillis(), 0),
|
||||
total
|
||||
);
|
||||
|
||||
return Math.min(100, Math.max(0, (elapsed / total) * 100));
|
||||
}
|
||||
|
||||
function buildFinancialTotalsQuery(whereClause) {
|
||||
@@ -718,9 +781,13 @@ function buildFinancialTotalsQuery(whereClause) {
|
||||
}
|
||||
|
||||
function buildFinancialTrendQuery(whereClause) {
|
||||
const businessDayOffset = BUSINESS_DAY_START_HOUR;
|
||||
return `
|
||||
SELECT
|
||||
DATE(date_change) as date,
|
||||
DATE_FORMAT(
|
||||
DATE_SUB(date_change, INTERVAL ${businessDayOffset} HOUR),
|
||||
'%Y-%m-%d'
|
||||
) as businessDate,
|
||||
SUM(sale_amount) as grossSales,
|
||||
SUM(refund_amount) as refunds,
|
||||
SUM(shipping_collected_amount + small_order_fee_amount + rush_fee_amount) as shippingFees,
|
||||
@@ -730,8 +797,8 @@ function buildFinancialTrendQuery(whereClause) {
|
||||
FROM report_sales_data
|
||||
WHERE ${whereClause}
|
||||
AND action IN (1, 2, 3)
|
||||
GROUP BY DATE(date_change)
|
||||
ORDER BY date ASC
|
||||
GROUP BY businessDate
|
||||
ORDER BY businessDate ASC
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -772,15 +839,48 @@ function normalizeFinancialTrendRow(row = {}) {
|
||||
const profit = income - cogs;
|
||||
const margin = income !== 0 ? (profit / income) * 100 : 0;
|
||||
let timestamp = null;
|
||||
let dateValue = row.businessDate || row.date || null;
|
||||
|
||||
if (row.date instanceof Date) {
|
||||
const resolveBusinessDayStart = (value) => {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let dt;
|
||||
if (value instanceof Date) {
|
||||
dt = DateTime.fromJSDate(value, { zone: TIMEZONE });
|
||||
} else if (typeof value === 'string') {
|
||||
dt = DateTime.fromISO(value, { zone: TIMEZONE });
|
||||
if (!dt.isValid) {
|
||||
dt = DateTime.fromSQL(value, { zone: TIMEZONE });
|
||||
}
|
||||
}
|
||||
|
||||
if (!dt || !dt.isValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hour = BUSINESS_DAY_START_HOUR;
|
||||
return dt.set({
|
||||
hour,
|
||||
minute: 0,
|
||||
second: 0,
|
||||
millisecond: 0,
|
||||
});
|
||||
};
|
||||
|
||||
const businessDayStart = resolveBusinessDayStart(dateValue);
|
||||
if (businessDayStart) {
|
||||
timestamp = businessDayStart.toUTC().toISO();
|
||||
dateValue = businessDayStart.toISO();
|
||||
} else if (row.date instanceof Date) {
|
||||
timestamp = new Date(row.date.getTime()).toISOString();
|
||||
} else if (typeof row.date === 'string') {
|
||||
timestamp = new Date(`${row.date}T00:00:00Z`).toISOString();
|
||||
}
|
||||
|
||||
return {
|
||||
date: row.date,
|
||||
date: dateValue,
|
||||
grossSales,
|
||||
refunds,
|
||||
shippingFees,
|
||||
|
||||
@@ -1,217 +1,219 @@
|
||||
// Time utilities for handling business day logic and time ranges
|
||||
// Business day is 1am-12:59am Eastern time (UTC-5)
|
||||
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');
|
||||
}
|
||||
|
||||
const getBusinessDayBounds = (timeRange) => {
|
||||
const now = new Date();
|
||||
const easternTime = new Date(now.getTime() - (5 * 60 * 60 * 1000)); // UTC-5
|
||||
|
||||
switch (timeRange) {
|
||||
case 'today': {
|
||||
const start = new Date(easternTime);
|
||||
start.setHours(1, 0, 0, 0); // 1 AM start of business day
|
||||
|
||||
const end = new Date(start);
|
||||
end.setDate(end.getDate() + 1);
|
||||
end.setHours(0, 59, 59, 999); // 12:59 AM next day
|
||||
|
||||
return { start, end };
|
||||
return {
|
||||
start: getDayStart(current),
|
||||
end: getDayEnd(current)
|
||||
};
|
||||
}
|
||||
|
||||
case 'yesterday': {
|
||||
const start = new Date(easternTime);
|
||||
start.setDate(start.getDate() - 1);
|
||||
start.setHours(1, 0, 0, 0);
|
||||
|
||||
const end = new Date(start);
|
||||
end.setDate(end.getDate() + 1);
|
||||
end.setHours(0, 59, 59, 999);
|
||||
|
||||
return { start, end };
|
||||
const target = current.minus({ days: 1 });
|
||||
return {
|
||||
start: getDayStart(target),
|
||||
end: getDayEnd(target)
|
||||
};
|
||||
}
|
||||
|
||||
case 'thisWeek': {
|
||||
const start = new Date(easternTime);
|
||||
start.setDate(easternTime.getDate() - easternTime.getDay()); // Sunday
|
||||
start.setHours(1, 0, 0, 0);
|
||||
|
||||
const end = new Date(easternTime);
|
||||
end.setDate(end.getDate() + 1);
|
||||
end.setHours(0, 59, 59, 999);
|
||||
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
case 'lastWeek': {
|
||||
const start = new Date(easternTime);
|
||||
start.setDate(easternTime.getDate() - easternTime.getDay() - 7); // Previous Sunday
|
||||
start.setHours(1, 0, 0, 0);
|
||||
|
||||
const end = new Date(start);
|
||||
end.setDate(end.getDate() + 7);
|
||||
end.setHours(0, 59, 59, 999);
|
||||
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
case 'thisMonth': {
|
||||
const start = new Date(easternTime.getFullYear(), easternTime.getMonth(), 1, 1, 0, 0, 0);
|
||||
const end = new Date(easternTime);
|
||||
end.setDate(end.getDate() + 1);
|
||||
end.setHours(0, 59, 59, 999);
|
||||
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
case 'lastMonth': {
|
||||
const start = new Date(easternTime.getFullYear(), easternTime.getMonth() - 1, 1, 1, 0, 0, 0);
|
||||
const end = new Date(easternTime.getFullYear(), easternTime.getMonth(), 1, 0, 59, 59, 999);
|
||||
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
case 'last7days': {
|
||||
const end = new Date(easternTime);
|
||||
end.setHours(0, 59, 59, 999);
|
||||
|
||||
const start = new Date(end);
|
||||
start.setDate(start.getDate() - 7);
|
||||
start.setHours(1, 0, 0, 0);
|
||||
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
case 'last30days': {
|
||||
const end = new Date(easternTime);
|
||||
end.setHours(0, 59, 59, 999);
|
||||
|
||||
const start = new Date(end);
|
||||
start.setDate(start.getDate() - 30);
|
||||
start.setHours(1, 0, 0, 0);
|
||||
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
case 'last90days': {
|
||||
const end = new Date(easternTime);
|
||||
end.setHours(0, 59, 59, 999);
|
||||
|
||||
const start = new Date(end);
|
||||
start.setDate(start.getDate() - 90);
|
||||
start.setHours(1, 0, 0, 0);
|
||||
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
case 'previous7days': {
|
||||
const end = new Date(easternTime);
|
||||
end.setDate(end.getDate() - 1);
|
||||
end.setHours(0, 59, 59, 999);
|
||||
|
||||
const start = new Date(end);
|
||||
start.setDate(start.getDate() - 6);
|
||||
start.setHours(1, 0, 0, 0);
|
||||
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
case 'previous30days': {
|
||||
const end = new Date(easternTime);
|
||||
end.setDate(end.getDate() - 1);
|
||||
end.setHours(0, 59, 59, 999);
|
||||
|
||||
const start = new Date(end);
|
||||
start.setDate(start.getDate() - 29);
|
||||
start.setHours(1, 0, 0, 0);
|
||||
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
case 'previous90days': {
|
||||
const end = new Date(easternTime);
|
||||
end.setDate(end.getDate() - 1);
|
||||
end.setHours(0, 59, 59, 999);
|
||||
|
||||
const start = new Date(end);
|
||||
start.setDate(start.getDate() - 89);
|
||||
start.setHours(1, 0, 0, 0);
|
||||
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
case 'twoDaysAgo': {
|
||||
const start = new Date(easternTime);
|
||||
start.setDate(start.getDate() - 2);
|
||||
start.setHours(1, 0, 0, 0);
|
||||
|
||||
const end = new Date(start);
|
||||
end.setDate(end.getDate() + 1);
|
||||
end.setHours(0, 59, 59, 999);
|
||||
|
||||
return { start, end };
|
||||
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 getTimeRangeConditions = (timeRange, startDate, endDate) => {
|
||||
if (timeRange === 'custom' && startDate && endDate) {
|
||||
// Custom date range
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
|
||||
// Convert to UTC-5 (Eastern time)
|
||||
const startUTC5 = new Date(start.getTime() - (5 * 60 * 60 * 1000));
|
||||
const endUTC5 = new Date(end.getTime() - (5 * 60 * 60 * 1000));
|
||||
|
||||
return {
|
||||
whereClause: 'date_placed >= ? AND date_placed <= ?',
|
||||
params: [
|
||||
startUTC5.toISOString().slice(0, 19).replace('T', ' '),
|
||||
endUTC5.toISOString().slice(0, 19).replace('T', ' ')
|
||||
],
|
||||
dateRange: {
|
||||
start: startDate,
|
||||
end: endDate,
|
||||
label: `${formatBusinessDate(start)} - ${formatBusinessDate(end)}`
|
||||
}
|
||||
};
|
||||
const toDatabaseSqlString = (dt) => {
|
||||
const normalized = ensureDateTime(dt);
|
||||
if (!normalized || !normalized.isValid) {
|
||||
throw new Error('Invalid datetime provided for SQL conversion');
|
||||
}
|
||||
|
||||
if (!timeRange) {
|
||||
timeRange = 'today';
|
||||
}
|
||||
|
||||
const { start, end } = getBusinessDayBounds(timeRange);
|
||||
|
||||
// Convert to MySQL datetime format (UTC-5)
|
||||
const startStr = start.toISOString().slice(0, 19).replace('T', ' ');
|
||||
const endStr = end.toISOString().slice(0, 19).replace('T', ' ');
|
||||
|
||||
return {
|
||||
whereClause: 'date_placed >= ? AND date_placed <= ?',
|
||||
params: [startStr, endStr],
|
||||
dateRange: {
|
||||
start: start.toISOString(),
|
||||
end: end.toISOString(),
|
||||
label: getTimeRangeLabel(timeRange)
|
||||
}
|
||||
};
|
||||
const dbTime = normalized.setZone(DB_TIMEZONE, { keepLocalTime: true });
|
||||
return dbTime.toFormat(DB_DATETIME_FORMAT);
|
||||
};
|
||||
|
||||
const formatBusinessDate = (date) => {
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
});
|
||||
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',
|
||||
@@ -221,32 +223,75 @@ const getTimeRangeLabel = (timeRange) => {
|
||||
last90days: 'Last 90 Days',
|
||||
previous7days: 'Previous 7 Days',
|
||||
previous30days: 'Previous 30 Days',
|
||||
previous90days: 'Previous 90 Days',
|
||||
twoDaysAgo: 'Two Days Ago'
|
||||
previous90days: 'Previous 90 Days'
|
||||
};
|
||||
|
||||
|
||||
return labels[timeRange] || timeRange;
|
||||
};
|
||||
|
||||
// Helper to convert MySQL datetime to JavaScript Date
|
||||
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;
|
||||
}
|
||||
|
||||
// MySQL datetime is stored in UTC-5, so we need to add 5 hours to get UTC
|
||||
const date = new Date(mysqlDatetime + ' UTC');
|
||||
date.setHours(date.getHours() + 5);
|
||||
return date;
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
// Helper to format date for MySQL queries
|
||||
const formatMySQLDate = (date) => {
|
||||
if (!date) return null;
|
||||
|
||||
// Convert to UTC-5 for storage
|
||||
const utc5Date = new Date(date.getTime() - (5 * 60 * 60 * 1000));
|
||||
return utc5Date.toISOString().slice(0, 19).replace('T', ' ');
|
||||
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 = {
|
||||
@@ -255,5 +300,13 @@ module.exports = {
|
||||
formatBusinessDate,
|
||||
getTimeRangeLabel,
|
||||
parseBusinessDate,
|
||||
formatMySQLDate
|
||||
};
|
||||
formatMySQLDate,
|
||||
// Expose helpers for tests or advanced consumers
|
||||
_internal: {
|
||||
getDayStart,
|
||||
getDayEnd,
|
||||
getWeekStart,
|
||||
getRangeForTimeRange,
|
||||
BUSINESS_DAY_START_HOUR
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user