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, 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`; }