Time unification
This commit is contained in:
@@ -1,12 +1,25 @@
|
||||
import express from 'express';
|
||||
import { DateTime } from 'luxon';
|
||||
import { getDbConnection, getPoolStatus } from '../db/connection.js';
|
||||
import { getTimeRangeConditions, _internal as timeHelpers } from '../utils/timeUtils.js';
|
||||
import {
|
||||
getTimeRangeConditions,
|
||||
customRange,
|
||||
previousRange,
|
||||
parseMySql,
|
||||
BUSINESS_TZ,
|
||||
} from '../../../shared/business-time/index.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const TIMEZONE = 'America/New_York';
|
||||
|
||||
// timeclock.TimeStamp stores Central wall-clock literals; the driver returns
|
||||
// them as strings (dateStrings: true). Parse explicitly in business time.
|
||||
const punchTime = (punch) => {
|
||||
const dt = parseMySql(punch.TimeStamp);
|
||||
return dt ? dt.toJSDate() : new Date(NaN);
|
||||
};
|
||||
|
||||
// Punch types from the database
|
||||
const PUNCH_TYPES = {
|
||||
OUT: 0,
|
||||
@@ -40,7 +53,7 @@ function calculateHoursFromPunches(punches) {
|
||||
|
||||
byEmployee.forEach((employeePunches, employeeId) => {
|
||||
// Sort by timestamp
|
||||
employeePunches.sort((a, b) => new Date(a.TimeStamp) - new Date(b.TimeStamp));
|
||||
employeePunches.sort((a, b) => punchTime(a) - punchTime(b));
|
||||
|
||||
let hours = 0;
|
||||
let breakHours = 0;
|
||||
@@ -48,24 +61,24 @@ function calculateHoursFromPunches(punches) {
|
||||
let breakStart = null;
|
||||
|
||||
employeePunches.forEach(punch => {
|
||||
const punchTime = new Date(punch.TimeStamp);
|
||||
const punchAt = punchTime(punch);
|
||||
|
||||
switch (punch.PunchType) {
|
||||
case PUNCH_TYPES.IN:
|
||||
currentIn = punchTime;
|
||||
currentIn = punchAt;
|
||||
break;
|
||||
case PUNCH_TYPES.OUT:
|
||||
if (currentIn) {
|
||||
hours += (punchTime - currentIn) / (1000 * 60 * 60); // Convert ms to hours
|
||||
hours += (punchAt - currentIn) / (1000 * 60 * 60); // Convert ms to hours
|
||||
currentIn = null;
|
||||
}
|
||||
break;
|
||||
case PUNCH_TYPES.BREAK_START:
|
||||
breakStart = punchTime;
|
||||
breakStart = punchAt;
|
||||
break;
|
||||
case PUNCH_TYPES.BREAK_END:
|
||||
if (breakStart) {
|
||||
breakHours += (punchTime - breakStart) / (1000 * 60 * 60);
|
||||
breakHours += (punchAt - breakStart) / (1000 * 60 * 60);
|
||||
breakStart = null;
|
||||
}
|
||||
break;
|
||||
@@ -315,13 +328,13 @@ router.get('/', async (req, res) => {
|
||||
const periodDays = Math.max(1, (periodEnd - periodStart) / (1000 * 60 * 60 * 24));
|
||||
const weeksInPeriod = periodDays / 7;
|
||||
|
||||
// Get daily trend data for hours
|
||||
// Use DATE_FORMAT to get date string in Eastern timezone, avoiding JS timezone conversion issues
|
||||
// Business day starts at 1 AM, so subtract 1 hour before taking the date
|
||||
// Get daily trend data for hours.
|
||||
// TimeStamp stores Central wall-clock literals; business date = the
|
||||
// Central calendar date, so DATE_FORMAT(col) needs no hour shift.
|
||||
const trendWhere = whereClause.replace(/date_placed/g, 'tc.TimeStamp');
|
||||
const trendQuery = `
|
||||
SELECT
|
||||
DATE_FORMAT(DATE_SUB(tc.TimeStamp, INTERVAL 1 HOUR), '%Y-%m-%d') as date,
|
||||
DATE_FORMAT(tc.TimeStamp, '%Y-%m-%d') as date,
|
||||
tc.EmployeeID,
|
||||
tc.TimeStamp,
|
||||
tc.PunchType
|
||||
@@ -337,18 +350,19 @@ router.get('/', async (req, res) => {
|
||||
|
||||
// Get daily picking data for trend
|
||||
// Ship-together orders: only count main orders (is_sub = 0 or NULL)
|
||||
// Use DATE_FORMAT for consistent date string format
|
||||
// createddate is a Central wall-clock literal — DATE_FORMAT(col) is the
|
||||
// business date with no hour shift.
|
||||
const pickingTrendWhere = whereClause.replace(/date_placed/g, 'pt.createddate');
|
||||
const pickingTrendQuery = `
|
||||
SELECT
|
||||
DATE_FORMAT(DATE_SUB(pt.createddate, INTERVAL 1 HOUR), '%Y-%m-%d') as date,
|
||||
DATE_FORMAT(pt.createddate, '%Y-%m-%d') as date,
|
||||
COUNT(DISTINCT CASE WHEN ptb.is_sub = 0 OR ptb.is_sub IS NULL THEN ptb.orderid END) as ordersPicked,
|
||||
COALESCE(SUM(pt.totalpieces_picked), 0) as piecesPicked
|
||||
FROM picking_ticket pt
|
||||
LEFT JOIN picking_ticket_buckets ptb ON pt.pickingid = ptb.pickingid
|
||||
WHERE ${pickingTrendWhere}
|
||||
AND pt.closeddate IS NOT NULL
|
||||
GROUP BY DATE_FORMAT(DATE_SUB(pt.createddate, INTERVAL 1 HOUR), '%Y-%m-%d')
|
||||
GROUP BY DATE_FORMAT(pt.createddate, '%Y-%m-%d')
|
||||
ORDER BY date
|
||||
`;
|
||||
|
||||
@@ -376,13 +390,14 @@ router.get('/', async (req, res) => {
|
||||
byDate.get(date).push(row);
|
||||
});
|
||||
|
||||
// Generate all dates in the period range for complete trend data
|
||||
// Generate all business dates in the half-open period [start, end) for
|
||||
// complete trend data. Business date = Chicago calendar date.
|
||||
const allDatesInRange = [];
|
||||
const startDt = DateTime.fromJSDate(periodStart).setZone(TIMEZONE).startOf('day');
|
||||
const endDt = DateTime.fromJSDate(periodEnd).setZone(TIMEZONE).startOf('day');
|
||||
const startDt = DateTime.fromJSDate(periodStart).setZone(BUSINESS_TZ).startOf('day');
|
||||
const endDt = DateTime.fromJSDate(periodEnd).setZone(BUSINESS_TZ);
|
||||
|
||||
let currentDt = startDt;
|
||||
while (currentDt <= endDt) {
|
||||
while (currentDt < endDt) {
|
||||
allDatesInRange.push(currentDt.toFormat('yyyy-MM-dd'));
|
||||
currentDt = currentDt.plus({ days: 1 });
|
||||
}
|
||||
@@ -393,7 +408,8 @@ router.get('/', async (req, res) => {
|
||||
const { totalHours: dayHours, employeeHours: dayEmployeeHours } = calculateHoursFromPunches(punches);
|
||||
const picking = pickingByDate.get(date) || { ordersPicked: 0, piecesPicked: 0 };
|
||||
|
||||
// Parse date string in Eastern timezone to get proper ISO timestamp
|
||||
// The string is already the business date; the zone here is only for
|
||||
// the display timestamp label (office time).
|
||||
const dateDt = DateTime.fromFormat(date, 'yyyy-MM-dd', { zone: TIMEZONE });
|
||||
|
||||
return {
|
||||
@@ -646,22 +662,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());
|
||||
}
|
||||
|
||||
function getPreviousTimeRange(timeRange) {
|
||||
|
||||
Reference in New Issue
Block a user