personal-lifecycle-tracker / frontend / src / services / dateUtils.ts
dateUtils.ts
Raw
import { ItemUrgency, LifecycleItem } from './types';

/**
 * Returns the current date in YYYY-MM-DD format based on standard ISO.
 */
export function getTodayDateString(): string {
  const now = new Date();
  const year = now.getFullYear();
  const month = String(now.getMonth() + 1).padStart(2, '0');
  const day = String(now.getDate()).padStart(2, '0');
  return `${year}-${month}-${day}`;
}

/**
 * Determines urgency classification for a lifecycle item.
 * - Completed items are always 'completed'.
 * - Items without due dates are 'no_date' (cannot be classified as upcoming or overdue).
 * - Items with dueDate < today are 'overdue'.
 * - Items with dueDate between today and today + upcomingWindowDays are 'upcoming'.
 * - Otherwise 'normal'.
 */
export function getItemUrgency(
  item: Pick<LifecycleItem, 'status' | 'dueDate'>,
  todayStr: string = getTodayDateString(),
  upcomingWindowDays: number = 30
): ItemUrgency {
  if (item.status === 'completed') {
    return 'completed';
  }

  if (!item.dueDate || item.dueDate.trim() === '') {
    return 'no_date';
  }

  const cleanDueDate = item.dueDate.trim().slice(0, 10);
  const cleanToday = todayStr.trim().slice(0, 10);

  if (cleanDueDate < cleanToday) {
    return 'overdue';
  }

  // Calculate day difference
  const due = new Date(`${cleanDueDate}T00:00:00Z`).getTime();
  const today = new Date(`${cleanToday}T00:00:00Z`).getTime();
  const diffDays = Math.round((due - today) / (1000 * 60 * 60 * 24));

  if (diffDays >= 0 && diffDays <= upcomingWindowDays) {
    return 'upcoming';
  }

  return 'normal';
}

/**
 * Computes difference in days from today to dueDate.
 * Negative means overdue.
 */
export function getDaysDifference(dueDate?: string, todayStr: string = getTodayDateString()): number | null {
  if (!dueDate || dueDate.trim() === '') return null;
  const cleanDueDate = dueDate.trim().slice(0, 10);
  const cleanToday = todayStr.trim().slice(0, 10);

  const due = new Date(`${cleanDueDate}T00:00:00Z`).getTime();
  const today = new Date(`${cleanToday}T00:00:00Z`).getTime();
  return Math.round((due - today) / (1000 * 60 * 60 * 24));
}

/**
 * Human readable formatting for date string.
 */
export function formatDisplayDate(dateStr?: string): string {
  if (!dateStr || dateStr.trim() === '') return 'No due date';
  const clean = dateStr.trim().slice(0, 10);
  const [year, month, day] = clean.split('-').map(Number);
  if (!year || !month || !day) return dateStr;

  const date = new Date(year, month - 1, day);
  return date.toLocaleDateString('en-US', {
    month: 'short',
    day: 'numeric',
    year: 'numeric',
  });
}

/**
 * Human-readable relative description for due dates.
 */
export function getRelativeDueDateLabel(
  dueDate?: string,
  status?: string,
  todayStr: string = getTodayDateString()
): string {
  if (status === 'completed') return 'Completed';
  if (!dueDate || dueDate.trim() === '') return 'No due date';

  const diff = getDaysDifference(dueDate, todayStr);
  if (diff === null) return 'No due date';

  if (diff < 0) {
    const abs = Math.abs(diff);
    return abs === 1 ? '1 day overdue' : `${abs} days overdue`;
  }
  if (diff === 0) {
    return 'Due today';
  }
  if (diff === 1) {
    return 'Due tomorrow';
  }
  return `Due in ${diff} days`;
}