relative-dates.ts2.8 KBView on GitHub
/**
 * Relative date utilities for CRM filters.
 *
 * When a date filter has `dateRelative: true`, the stored `dateFromDaysOffset` / `dateToDaysOffset`
 * represent a day count relative to today (0 = today, -7 = 7 days ago, +3 = 3 days from now).
 * These offsets are resolved to absolute ISO dates at query time so the server receives
 * normal dates — keeping filters automatically up-to-date as days pass.
 */

import { startOfDay, differenceInCalendarDays, addDays, format } from 'date-fns';

/**
 * Compute the day offset between a date and today.
 * Returns 0 for today, -1 for yesterday, +1 for tomorrow, etc.
 */
export function computeDaysOffset(date: Date): number {
  const today = startOfDay(new Date());
  const target = startOfDay(date);
  return differenceInCalendarDays(target, today);
}

/**
 * Resolve a day offset to an absolute Date, normalized to start of day (local midnight).
 * The returned Date is stable within a calendar day.
 */
export function resolveOffsetToDate(offsetDays: number): Date {
  return startOfDay(addDays(new Date(), offsetDays));
}

/**
 * Resolve a day offset to an ISO date string suitable for filter queries.
 */
export function resolveOffsetToISO(offsetDays: number): string {
  return resolveOffsetToDate(offsetDays).toISOString();
}

/**
 * Format a day offset for badge/label display.
 * Examples: "Today", "Yesterday", "Tomorrow", "7d ago", "3d from now"
 */
export function formatDaysOffset(offsetDays: number): string {
  if (offsetDays === 0) return 'Today';
  if (offsetDays === -1) return 'Yesterday';
  if (offsetDays === 1) return 'Tomorrow';
  if (offsetDays < 0) return `${Math.abs(offsetDays)}d ago`;
  return `${offsetDays}d from now`;
}

/**
 * Format a day offset with the resolved date for popover display.
 * Examples: "Today (Feb 28)", "7d ago (Feb 21)"
 */
export function formatDaysOffsetWithDate(offsetDays: number): string {
  const label = formatDaysOffset(offsetDays);
  const resolved = resolveOffsetToDate(offsetDays);
  return `${label} (${format(resolved, 'MMM d')})`;
}

/**
 * Get a stable string representing "today" for use as a cache key epoch.
 * Changes once per calendar day, causing React Query cache invalidation
 * for queries that use relative date filters.
 */
export function getTodayEpoch(): string {
  return new Date().toDateString();
}

/**
 * Due-date cutoff that defines a conversation's "Suggested Action".
 *
 * The Suggested Action cell shows the most overdue todo task due on or before the end of
 * today (ConversationItem/VirtualizedTableRow), falling back to last activity when there is
 * none. Filters on that column must use the same cutoff, or "has an action" would include
 * rows whose cell shows no action at all.
 */
export function currentActionDueCutoff(): string {
  const endOfToday = new Date();
  endOfToday.setHours(23, 59, 59, 999);
  return endOfToday.toISOString();
}