Time unification
This commit is contained in:
@@ -5,8 +5,12 @@ import {
|
||||
getTimeRangeConditions,
|
||||
formatBusinessDate,
|
||||
getBusinessDayBounds,
|
||||
customRange,
|
||||
previousRange,
|
||||
parseMySql,
|
||||
toMySqlBound,
|
||||
_internal as timeHelpers,
|
||||
} from '../utils/timeUtils.js';
|
||||
} from '../../../shared/business-time/index.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -83,7 +87,7 @@ router.get('/stats', async (req, res) => {
|
||||
FROM _order
|
||||
WHERE order_status = 15
|
||||
AND ${getCherryBoxClause(excludeCB)}
|
||||
AND ${whereClause.replace('date_placed', 'date_cancelled')}
|
||||
AND ${whereClause.replace(/date_placed/g, 'date_cancelled')}
|
||||
`;
|
||||
|
||||
const [cancelledResult] = await connection.execute(cancelledQuery, params);
|
||||
@@ -96,7 +100,7 @@ router.get('/stats', async (req, res) => {
|
||||
ABS(SUM(payment_amount)) as refundTotal
|
||||
FROM order_payment op
|
||||
JOIN _order o ON op.order_id = o.order_id
|
||||
WHERE payment_amount < 0 AND o.order_status > 15 AND ${getCherryBoxClauseAliased('o', excludeCB)} AND ${whereClause.replace('date_placed', 'o.date_placed')}
|
||||
WHERE payment_amount < 0 AND o.order_status > 15 AND ${getCherryBoxClauseAliased('o', excludeCB)} AND ${whereClause.replace(/date_placed/g, 'o.date_placed')}
|
||||
`;
|
||||
|
||||
const [refundStats] = await connection.execute(refundsQuery, params);
|
||||
@@ -108,7 +112,7 @@ router.get('/stats', async (req, res) => {
|
||||
FROM _order
|
||||
WHERE order_status IN (92, 95, 100)
|
||||
AND ${getCherryBoxClause(excludeCB)}
|
||||
AND ${whereClause.replace('date_placed', 'date_shipped')}
|
||||
AND ${whereClause.replace(/date_placed/g, 'date_shipped')}
|
||||
`;
|
||||
|
||||
const [shippedResult] = await connection.execute(shippedQuery, params);
|
||||
@@ -129,29 +133,29 @@ router.get('/stats', async (req, res) => {
|
||||
|
||||
const [bestDayResult] = await connection.execute(bestDayQuery, params);
|
||||
|
||||
// Peak hour query - uses selected time range for the card value
|
||||
// Peak hour query - uses selected time range for the card value.
|
||||
// date_placed stores Central wall-clock; the office reads the axis in
|
||||
// Eastern, and ET = CT + 1h year-round, so shift before taking HOUR().
|
||||
let peakHour = null;
|
||||
if (['today', 'yesterday'].includes(timeRange)) {
|
||||
const peakHourQuery = `
|
||||
SELECT
|
||||
HOUR(date_placed) as hour,
|
||||
HOUR(ADDTIME(date_placed, '01:00:00')) as hour,
|
||||
COUNT(*) as count
|
||||
FROM _order
|
||||
WHERE order_status > 15 AND ${getCherryBoxClause(excludeCB)} AND ${whereClause}
|
||||
GROUP BY HOUR(date_placed)
|
||||
GROUP BY HOUR(ADDTIME(date_placed, '01:00:00'))
|
||||
ORDER BY count DESC
|
||||
LIMIT 1
|
||||
`;
|
||||
|
||||
const [peakHourResult] = await connection.execute(peakHourQuery, params);
|
||||
if (peakHourResult.length > 0) {
|
||||
const hour = peakHourResult[0].hour;
|
||||
const date = new Date();
|
||||
date.setHours(hour, 0, 0);
|
||||
const hour = parseInt(peakHourResult[0].hour);
|
||||
peakHour = {
|
||||
hour,
|
||||
count: parseInt(peakHourResult[0].count),
|
||||
displayHour: date.toLocaleString("en-US", { hour: "numeric", hour12: true })
|
||||
displayHour: DateTime.fromObject({ hour }).toFormat('h a')
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -160,23 +164,27 @@ router.get('/stats', async (req, res) => {
|
||||
// Returns data ordered chronologically: [24hrs ago, 23hrs ago, ..., 1hr ago, current hour]
|
||||
let hourlyOrders = null;
|
||||
if (['today', 'yesterday'].includes(timeRange)) {
|
||||
// Get hourly counts AND current hour from MySQL to avoid timezone mismatch
|
||||
// Hourly counts in EASTERN display hours (column stores Central
|
||||
// wall-clock; ET = CT + 1h year-round). currentHour comes from the same
|
||||
// expression so the rolling window stays aligned.
|
||||
const hourlyQuery = `
|
||||
SELECT
|
||||
HOUR(date_placed) as hour,
|
||||
HOUR(ADDTIME(date_placed, '01:00:00')) as hour,
|
||||
COUNT(*) as count,
|
||||
HOUR(NOW()) as currentHour
|
||||
HOUR(ADDTIME(NOW(), '01:00:00')) as currentHour
|
||||
FROM _order
|
||||
WHERE order_status > 15
|
||||
AND ${getCherryBoxClause(excludeCB)}
|
||||
AND date_placed >= NOW() - INTERVAL 24 HOUR
|
||||
GROUP BY HOUR(date_placed)
|
||||
GROUP BY HOUR(ADDTIME(date_placed, '01:00:00'))
|
||||
`;
|
||||
|
||||
const [hourlyResult] = await connection.execute(hourlyQuery);
|
||||
|
||||
// Get current hour from MySQL (same timezone as the WHERE clause)
|
||||
const currentHour = hourlyResult.length > 0 ? parseInt(hourlyResult[0].currentHour) : new Date().getHours();
|
||||
// Current hour in Eastern (fallback computes it directly)
|
||||
const currentHour = hourlyResult.length > 0
|
||||
? parseInt(hourlyResult[0].currentHour)
|
||||
: DateTime.now().setZone(TIMEZONE).hour;
|
||||
|
||||
// Build map of hour -> count
|
||||
const hourCounts = {};
|
||||
@@ -208,7 +216,7 @@ router.get('/stats', async (req, res) => {
|
||||
JOIN _order o ON oi.order_id = o.order_id
|
||||
JOIN products p ON oi.prod_pid = p.pid
|
||||
JOIN product_categories pc ON p.company = pc.cat_id
|
||||
WHERE o.order_status > 15 AND ${getCherryBoxClauseAliased('o', excludeCB)} AND ${whereClause.replace('date_placed', 'o.date_placed')}
|
||||
WHERE o.order_status > 15 AND ${getCherryBoxClauseAliased('o', excludeCB)} AND ${whereClause.replace(/date_placed/g, 'o.date_placed')}
|
||||
GROUP BY pc.cat_id, pc.name
|
||||
HAVING revenue > 0
|
||||
ORDER BY revenue DESC
|
||||
@@ -233,7 +241,7 @@ router.get('/stats', async (req, res) => {
|
||||
JOIN product_categories pc ON pci.cat_id = pc.cat_id
|
||||
WHERE o.order_status > 15
|
||||
AND ${getCherryBoxClauseAliased('o', excludeCB)}
|
||||
AND ${whereClause.replace('date_placed', 'o.date_placed')}
|
||||
AND ${whereClause.replace(/date_placed/g, 'o.date_placed')}
|
||||
AND pc.type IN (10, 20, 11, 21, 12, 13)
|
||||
GROUP BY pc.cat_id, pc.name
|
||||
HAVING revenue > 0
|
||||
@@ -253,7 +261,7 @@ router.get('/stats', async (req, res) => {
|
||||
FROM _order
|
||||
WHERE order_status IN (92, 95, 100)
|
||||
AND ${getCherryBoxClause(excludeCB)}
|
||||
AND ${whereClause.replace('date_placed', 'date_shipped')}
|
||||
AND ${whereClause.replace(/date_placed/g, 'date_shipped')}
|
||||
GROUP BY ship_country, ship_state, ship_method_selected
|
||||
`;
|
||||
|
||||
@@ -425,7 +433,7 @@ router.get('/stats/details', async (req, res) => {
|
||||
WHERE op.payment_amount < 0
|
||||
AND o.order_status > 15
|
||||
AND ${getCherryBoxClauseAliased('o', excludeCB)}
|
||||
AND ${whereClause.replace('date_placed', 'op.payment_date')}
|
||||
AND ${whereClause.replace(/date_placed/g, 'op.payment_date')}
|
||||
GROUP BY DATE(op.payment_date)
|
||||
ORDER BY DATE(op.payment_date)
|
||||
`;
|
||||
@@ -457,7 +465,7 @@ router.get('/stats/details', async (req, res) => {
|
||||
FROM _order
|
||||
WHERE order_status = 15
|
||||
AND ${getCherryBoxClause(excludeCB)}
|
||||
AND ${whereClause.replace('date_placed', 'date_cancelled')}
|
||||
AND ${whereClause.replace(/date_placed/g, 'date_cancelled')}
|
||||
GROUP BY DATE(date_cancelled)
|
||||
ORDER BY DATE(date_cancelled)
|
||||
`;
|
||||
@@ -549,16 +557,10 @@ router.get('/stats/details', async (req, res) => {
|
||||
prevWhereClause = result.whereClause;
|
||||
prevParams = result.params;
|
||||
} else {
|
||||
// Custom date range - go back by the same duration
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
const duration = end.getTime() - start.getTime();
|
||||
|
||||
const prevEnd = new Date(start.getTime() - 1);
|
||||
const prevStart = new Date(prevEnd.getTime() - duration);
|
||||
|
||||
prevWhereClause = 'date_placed >= ? AND date_placed <= ?';
|
||||
prevParams = [prevStart.toISOString(), prevEnd.toISOString()];
|
||||
// Custom date range - go back by the same duration (business-day aware)
|
||||
const prev = previousRange(customRange(startDate, endDate));
|
||||
prevWhereClause = 'date_placed >= ? AND date_placed < ?';
|
||||
prevParams = [toMySqlBound(prev.start), toMySqlBound(prev.end)];
|
||||
}
|
||||
|
||||
// Get previous period daily data (optionally excludes Cherry Box orders)
|
||||
@@ -572,14 +574,14 @@ router.get('/stats/details', async (req, res) => {
|
||||
WHERE order_status > 15 AND ${getCherryBoxClause(excludeCB)} AND ${prevWhereClause} ${orderTypeFilter}
|
||||
GROUP BY DATE(date_placed)
|
||||
`;
|
||||
|
||||
|
||||
const [prevResults] = await connection.execute(prevQuery, prevParams);
|
||||
|
||||
|
||||
// Create a map for quick lookup of previous period data
|
||||
// (dateStrings: true — DATE columns arrive as 'YYYY-MM-DD' strings)
|
||||
const prevMap = new Map();
|
||||
prevResults.forEach(prev => {
|
||||
const key = new Date(prev.date).toISOString().split('T')[0];
|
||||
prevMap.set(key, prev);
|
||||
prevMap.set(String(prev.date), prev);
|
||||
});
|
||||
|
||||
// For period-to-period comparison, we need to map days by relative position
|
||||
@@ -636,8 +638,8 @@ router.get('/financials', async (req, res) => {
|
||||
|
||||
const formatDebugBound = (value) => {
|
||||
if (!value) return 'n/a';
|
||||
const parsed = DateTime.fromSQL(value, { zone: 'UTC-05:00' });
|
||||
if (!parsed.isValid) {
|
||||
const parsed = parseMySql(value);
|
||||
if (!parsed) {
|
||||
return `invalid(${value})`;
|
||||
}
|
||||
return parsed.setZone(TIMEZONE).toISO();
|
||||
@@ -771,7 +773,7 @@ router.get('/products', async (req, res) => {
|
||||
FROM order_items oi
|
||||
JOIN _order o ON oi.order_id = o.order_id
|
||||
JOIN products p ON oi.prod_pid = p.pid
|
||||
WHERE o.order_status > 15 AND ${getCherryBoxClauseAliased('o', excludeCB)} AND ${whereClause.replace('date_placed', 'o.date_placed')}
|
||||
WHERE o.order_status > 15 AND ${getCherryBoxClauseAliased('o', excludeCB)} AND ${whereClause.replace(/date_placed/g, 'o.date_placed')}
|
||||
GROUP BY p.pid, p.description
|
||||
ORDER BY totalRevenue DESC
|
||||
LIMIT 500
|
||||
@@ -1029,15 +1031,13 @@ function buildFinancialTotalsQuery(whereClause, excludeCherryBox = false) {
|
||||
}
|
||||
|
||||
function buildFinancialTrendQuery(whereClause, excludeCherryBox = false) {
|
||||
const businessDayOffset = BUSINESS_DAY_START_HOUR;
|
||||
// date_change stores Central wall-clock literals; business date = the
|
||||
// Central calendar date, so DATE_FORMAT(col) needs no hour shift.
|
||||
// Optionally join to _order to exclude Cherry Box orders
|
||||
if (excludeCherryBox) {
|
||||
return `
|
||||
SELECT
|
||||
DATE_FORMAT(
|
||||
DATE_SUB(r.date_change, INTERVAL ${businessDayOffset} HOUR),
|
||||
'%Y-%m-%d'
|
||||
) as businessDate,
|
||||
SELECT
|
||||
DATE_FORMAT(r.date_change, '%Y-%m-%d') as businessDate,
|
||||
SUM(r.sale_amount) as grossSales,
|
||||
SUM(r.refund_amount) as refunds,
|
||||
SUM(r.shipping_collected_amount + r.small_order_fee_amount + r.rush_fee_amount) as shippingFees,
|
||||
@@ -1054,11 +1054,8 @@ function buildFinancialTrendQuery(whereClause, excludeCherryBox = false) {
|
||||
`;
|
||||
}
|
||||
return `
|
||||
SELECT
|
||||
DATE_FORMAT(
|
||||
DATE_SUB(date_change, INTERVAL ${businessDayOffset} HOUR),
|
||||
'%Y-%m-%d'
|
||||
) as businessDate,
|
||||
SELECT
|
||||
DATE_FORMAT(date_change, '%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,
|
||||
@@ -1193,22 +1190,13 @@ function getPreviousPeriodRange(timeRange, startDate, endDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
|
||||
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) {
|
||||
try {
|
||||
const prev = previousRange(customRange(startDate, endDate));
|
||||
if (!prev) return null;
|
||||
return getTimeRangeConditions('custom', prev.start.toISO(), prev.end.toISO());
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const duration = end.getTime() - start.getTime();
|
||||
if (!Number.isFinite(duration) || duration <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const prevEnd = new Date(start.getTime() - 1);
|
||||
const prevStart = new Date(prevEnd.getTime() - duration);
|
||||
|
||||
return getTimeRangeConditions('custom', prevStart.toISOString(), prevEnd.toISOString());
|
||||
}
|
||||
|
||||
async function getPreviousPeriodData(connection, timeRange, startDate, endDate, excludeCherryBox = false) {
|
||||
@@ -1221,18 +1209,12 @@ async function getPreviousPeriodData(connection, timeRange, startDate, endDate,
|
||||
prevWhereClause = result.whereClause;
|
||||
prevParams = result.params;
|
||||
} else {
|
||||
// Custom date range - go back by the same duration
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
const duration = end.getTime() - start.getTime();
|
||||
|
||||
const prevEnd = new Date(start.getTime() - 1);
|
||||
const prevStart = new Date(prevEnd.getTime() - duration);
|
||||
|
||||
prevWhereClause = 'date_placed >= ? AND date_placed <= ?';
|
||||
prevParams = [prevStart.toISOString(), prevEnd.toISOString()];
|
||||
// Custom date range - go back by the same duration (business-day aware)
|
||||
const prev = previousRange(customRange(startDate, endDate));
|
||||
prevWhereClause = 'date_placed >= ? AND date_placed < ?';
|
||||
prevParams = [toMySqlBound(prev.start), toMySqlBound(prev.end)];
|
||||
}
|
||||
|
||||
|
||||
// Previous period query (optionally excludes Cherry Box orders)
|
||||
const prevQuery = `
|
||||
SELECT
|
||||
|
||||
Reference in New Issue
Block a user