import { DateTime } from 'luxon'; export class TimeManager { constructor(dayStartHour = 1) { this.timezone = 'America/New_York'; this.dayStartHour = dayStartHour; // Hour (0-23) when the business day starts this.weekStartDay = 7; // 7 = Sunday in Luxon } /** * Get the start of the current business day * If current time is before dayStartHour, return previous day at dayStartHour */ getDayStart(dt = this.getNow()) { if (!dt.isValid) { console.error("[TimeManager] Invalid datetime provided to getDayStart"); return this.getNow(); } const dayStart = dt.set({ hour: this.dayStartHour, minute: 0, second: 0, millisecond: 0 }); return dt.hour < this.dayStartHour ? dayStart.minus({ days: 1 }) : dayStart; } /** * Get the end of the current business day * End is defined as dayStartHour - 1 minute on the next day */ getDayEnd(dt = this.getNow()) { if (!dt.isValid) { console.error("[TimeManager] Invalid datetime provided to getDayEnd"); return this.getNow(); } const nextDay = this.getDayStart(dt).plus({ days: 1 }); return nextDay.minus({ minutes: 1 }); } /** * Get the start of the week containing the given date * Aligns with custom day start time and starts on Sunday */ getWeekStart(dt = this.getNow()) { if (!dt.isValid) { console.error("[TimeManager] Invalid datetime provided to getWeekStart"); return this.getNow(); } // Set to start of week (Sunday) and adjust hour const weekStart = dt.set({ weekday: this.weekStartDay }).startOf('day'); // If the week start time would be after the given time, go back a week if (weekStart > dt) { return weekStart.minus({ weeks: 1 }).set({ hour: this.dayStartHour }); } return weekStart.set({ hour: this.dayStartHour }); } /** * Convert any date input to a Luxon DateTime in Eastern time */ toDateTime(date) { if (!date) return null; if (date instanceof DateTime) { return date.setZone(this.timezone); } // If it's an ISO string or Date object, parse it const dt = DateTime.fromISO(date instanceof Date ? date.toISOString() : date); if (!dt.isValid) { console.error("[TimeManager] Invalid date input:", date); return null; } return dt.setZone(this.timezone); } /** * Format a date for API requests (UTC ISO string) */ formatForAPI(date) { if (!date) return null; // Parse the input date const dt = this.toDateTime(date); if (!dt || !dt.isValid) { console.error("[TimeManager] Invalid date for API:", date); return null; } // Convert to UTC for API request const utc = dt.toUTC(); console.log("[TimeManager] API date conversion:", { input: date, eastern: dt.toISO(), utc: utc.toISO(), offset: dt.offset }); return utc.toISO(); } /** * Format a date for display (in Eastern time) */ formatForDisplay(date) { const dt = this.toDateTime(date); if (!dt || !dt.isValid) return ''; return dt.toFormat('LLL d, yyyy h:mm a'); } /** * Validate if a date range is valid */ isValidDateRange(start, end) { const startDt = this.toDateTime(start); const endDt = this.toDateTime(end); return startDt && endDt && endDt > startDt; } /** * Get the current time in Eastern timezone */ getNow() { return DateTime.now().setZone(this.timezone); } /** * Get a date range for the last N hours */ getLastNHours(hours) { const now = this.getNow(); return { start: now.minus({ hours }), end: now }; } /** * Get a date range for the last N days * Aligns with custom day start time */ getLastNDays(days) { const now = this.getNow(); const dayStart = this.getDayStart(now); return { start: dayStart.minus({ days }), end: this.getDayEnd(now) }; } /** * Get a date range for a specific time period * All ranges align with custom day start time */ getDateRange(period) { const now = this.getNow(); // Normalize period to handle both 'last' and 'previous' prefixes const normalizedPeriod = period.startsWith('previous') ? period.replace('previous', 'last') : period; switch (normalizedPeriod) { case 'custom': { // Custom ranges are handled separately via getCustomRange console.warn('[TimeManager] Custom ranges should use getCustomRange method'); return null; } case 'today': { return { start: this.getDayStart(now), end: this.getDayEnd(now) }; } case 'yesterday': { const yesterday = now.minus({ days: 1 }); return { start: this.getDayStart(yesterday), end: this.getDayEnd(yesterday) }; } case 'last7days': { // For last 7 days, we want to include today and the previous 6 days // So if today is 12/17, we want 12/11 1am to 12/17 12:59am const dayStart = this.getDayStart(now); return { start: dayStart.minus({ days: 6 }), // 6 days ago from start of today end: this.getDayEnd(now) // end of today }; } case 'last30days': { // Include today and previous 29 days const dayStart = this.getDayStart(now); return { start: dayStart.minus({ days: 29 }), // 29 days ago from start of today end: this.getDayEnd(now) // end of today }; } case 'last90days': { // Include today and previous 89 days const dayStart = this.getDayStart(now); return { start: dayStart.minus({ days: 89 }), // 89 days ago from start of today end: this.getDayEnd(now) // end of today }; } case 'thisWeek': { // Get the start of the week (Sunday) with custom hour const weekStart = this.getWeekStart(now); return { start: weekStart, end: this.getDayEnd(now) }; } case 'lastWeek': { const lastWeek = now.minus({ weeks: 1 }); const weekStart = this.getWeekStart(lastWeek); const weekEnd = weekStart.plus({ days: 6 }); // 6 days after start = Saturday return { start: weekStart, end: this.getDayEnd(weekEnd) }; } case 'thisMonth': { const dayStart = this.getDayStart(now); const monthStart = dayStart.startOf('month').set({ hour: this.dayStartHour }); return { start: monthStart, end: this.getDayEnd(now) }; } case 'lastMonth': { const lastMonth = now.minus({ months: 1 }); const monthStart = lastMonth.startOf('month').set({ hour: this.dayStartHour }); const monthEnd = monthStart.plus({ months: 1 }).minus({ days: 1 }); return { start: monthStart, end: this.getDayEnd(monthEnd) }; } default: console.warn(`[TimeManager] Unknown period: ${period}`); return null; } } /** * Format a duration in milliseconds to a human-readable string */ formatDuration(ms) { return DateTime.fromMillis(ms).toFormat("hh'h' mm'm' ss's'"); } /** * Get relative time string (e.g., "2 hours ago") */ getRelativeTime(date) { const dt = this.toDateTime(date); if (!dt) return ''; return dt.toRelative(); } /** * Get a custom date range using exact dates and times provided * @param {string} startDate - ISO string or Date for range start * @param {string} endDate - ISO string or Date for range end * @returns {Object} Object with start and end DateTime objects */ getCustomRange(startDate, endDate) { if (!startDate || !endDate) { console.error("[TimeManager] Custom range requires both start and end dates"); return null; } const start = this.toDateTime(startDate); const end = this.toDateTime(endDate); if (!start || !end || !start.isValid || !end.isValid) { console.error("[TimeManager] Invalid dates provided for custom range"); return null; } // Validate the range if (end < start) { console.error("[TimeManager] End date must be after start date"); return null; } return { start, end }; } /** * Get the previous period's date range based on the current period * @param {string} period - The current period * @param {DateTime} now - The current datetime (optional) * @returns {Object} Object with start and end DateTime objects */ getPreviousPeriod(period, now = this.getNow()) { switch (period) { case 'today': { const yesterday = now.minus({ days: 1 }); return { start: this.getDayStart(yesterday), end: this.getDayEnd(yesterday) }; } case 'yesterday': { const twoDaysAgo = now.minus({ days: 2 }); return { start: this.getDayStart(twoDaysAgo), end: this.getDayEnd(twoDaysAgo) }; } case 'last7days': case 'previous7days': { const dayStart = this.getDayStart(now); const currentStart = dayStart.minus({ days: 6 }); const prevEnd = currentStart.minus({ milliseconds: 1 }); const prevStart = prevEnd.minus({ days: 6 }); return { start: prevStart, end: prevEnd }; } case 'last30days': case 'previous30days': { const dayStart = this.getDayStart(now); const currentStart = dayStart.minus({ days: 29 }); const prevEnd = currentStart.minus({ milliseconds: 1 }); const prevStart = prevEnd.minus({ days: 29 }); return { start: prevStart, end: prevEnd }; } case 'last90days': case 'previous90days': { const dayStart = this.getDayStart(now); const currentStart = dayStart.minus({ days: 89 }); const prevEnd = currentStart.minus({ milliseconds: 1 }); const prevStart = prevEnd.minus({ days: 89 }); return { start: prevStart, end: prevEnd }; } case 'thisWeek': { const weekStart = this.getWeekStart(now); const prevEnd = weekStart.minus({ milliseconds: 1 }); const prevStart = this.getWeekStart(prevEnd); return { start: prevStart, end: prevEnd }; } case 'lastWeek': { const lastWeekStart = this.getWeekStart(now.minus({ weeks: 1 })); const prevEnd = lastWeekStart.minus({ milliseconds: 1 }); const prevStart = this.getWeekStart(prevEnd); return { start: prevStart, end: prevEnd }; } case 'thisMonth': { const monthStart = now.startOf('month').set({ hour: this.dayStartHour }); const prevEnd = monthStart.minus({ milliseconds: 1 }); const prevStart = prevEnd.startOf('month').set({ hour: this.dayStartHour }); return { start: prevStart, end: prevEnd }; } case 'lastMonth': { const lastMonthStart = now.minus({ months: 1 }).startOf('month').set({ hour: this.dayStartHour }); const prevEnd = lastMonthStart.minus({ milliseconds: 1 }); const prevStart = prevEnd.startOf('month').set({ hour: this.dayStartHour }); return { start: prevStart, end: prevEnd }; } case 'twoDaysAgo': { const twoDaysAgo = now.minus({ days: 2 }); return { start: this.getDayStart(twoDaysAgo), end: this.getDayEnd(twoDaysAgo) }; } default: console.warn(`[TimeManager] No previous period defined for: ${period}`); return null; } } groupEventsByInterval(events, interval = 'day', property = null) { if (!events?.length) return []; const groupedData = new Map(); const now = DateTime.now().setZone('America/New_York'); for (const event of events) { const datetime = DateTime.fromISO(event.attributes.datetime); let groupKey; switch (interval) { case 'hour': groupKey = datetime.startOf('hour').toISO(); break; case 'day': groupKey = datetime.startOf('day').toISO(); break; case 'week': groupKey = datetime.startOf('week').toISO(); break; case 'month': groupKey = datetime.startOf('month').toISO(); break; default: groupKey = datetime.startOf('day').toISO(); } const existingGroup = groupedData.get(groupKey) || { datetime: groupKey, count: 0, value: 0 }; existingGroup.count++; if (property) { // Extract property value from event const props = event.attributes?.event_properties || event.attributes?.properties || {}; let value = 0; if (property === '$value') { // Special case for $value - use event value value = Number(event.attributes?.value || 0); } else { // Otherwise get from properties value = Number(props[property] || 0); } existingGroup.value = (existingGroup.value || 0) + value; } groupedData.set(groupKey, existingGroup); } // Convert to array and sort by datetime return Array.from(groupedData.values()) .sort((a, b) => DateTime.fromISO(a.datetime) - DateTime.fromISO(b.datetime)); } }