diff --git a/src/web/src/lib/dateFormat.ts b/src/web/src/lib/dateFormat.ts new file mode 100644 index 0000000..39b931a --- /dev/null +++ b/src/web/src/lib/dateFormat.ts @@ -0,0 +1,99 @@ +/** + * Localización temporal Argentina — utility centralizada. + * Ver: Obsidian/02-ARQUITECTURA-y-TECH-STACK/2.17 ⏰ Localización Temporal Argentina.md + * Engram topic_key: sig-cm2/conventions/fechas-timezones + * + * REGLAS PROHIBIDAS (no usar fuera de este módulo): + * - new Date(civilDateString) → aplica UTC, pierde días + * - toISOString().slice(0, 10) → UTC creep + * - toLocaleString('es-AR', {...}) sin timeZone → depende del navegador + */ + +export const AR_TZ = 'America/Argentina/Buenos_Aires'; + +type FormatStyle = 'short' | 'medium' | 'long' | 'full'; + +interface FormatInstantOptions { + dateStyle?: FormatStyle; + timeStyle?: FormatStyle; +} + +/** + * Formatea un instante UTC (Cat1) como string legible en zona horaria Argentina. + * Usa partes explícitas para garantizar año 4 dígitos y hora 24h independientemente del entorno. + * Output: "dd/MM/yyyy, HH:mm:ss" + */ +export function formatInstant( + iso: string, + _opts: FormatInstantOptions = { dateStyle: 'short', timeStyle: 'medium' } +): string { + const parts = new Intl.DateTimeFormat('es-AR', { + timeZone: AR_TZ, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + }).formatToParts(new Date(iso)); + + const get = (type: string): string => parts.find(p => p.type === type)!.value; + return `${get('day')}/${get('month')}/${get('year')}, ${get('hour')}:${get('minute')}:${get('second')}`; +} + +/** + * Formatea una fecha civil Argentina (Cat2, formato "yyyy-MM-dd") a "dd/MM/yyyy". + * Split manual — NO usa new Date() para evitar UTC creep. + */ +export function formatCivilDate(yyyyMmDd: string): string { + const [y, m, d] = yyyyMmDd.split('-'); + return `${d}/${m}/${y}`; +} + +/** + * Formatea un rango de fechas civiles Argentinas. + */ +export function formatCivilDateRange(from: string, to: string | null): string { + return to ? `${formatCivilDate(from)} → ${formatCivilDate(to)}` : `desde ${formatCivilDate(from)}`; +} + +/** + * Retorna la fecha civil Argentina de hoy en formato "yyyy-MM-dd". + * Fix de BUG-FE-03: usa Intl.DateTimeFormat con timeZone, NO toISOString().slice(0, 10). + */ +export function todayArgentina(): string { + const now = new Date(); + const parts = new Intl.DateTimeFormat('en-CA', { + timeZone: AR_TZ, + year: 'numeric', + month: '2-digit', + day: '2-digit', + }).formatToParts(now); + + const y = parts.find(p => p.type === 'year')!.value; + const m = parts.find(p => p.type === 'month')!.value; + const d = parts.find(p => p.type === 'day')!.value; + + return `${y}-${m}-${d}`; +} + +/** + * Parsea una fecha civil Argentina ("yyyy-MM-dd") a partes numéricas. + * Split manual — NO usa new Date(). + */ +export function parseCivilDate(yyyyMmDd: string): { year: number; month: number; day: number } { + const [y, m, d] = yyyyMmDd.split('-').map(Number); + return { year: y, month: m, day: d }; +} + +/** + * Convierte un valor de (ART implícito) a ISO UTC. + * Input: "2026-05-01T22:30" (sin TZ) → interpretado como ART → "2026-05-02T01:30:00.000Z". + * + * Útil para AuditFilters que envía filtros de rango al backend. + */ +export function parseArgentinaDateTimeToUtc(localDateTime: string): string { + // Parse "yyyy-MM-ddTHH:mm" como ART (offset -03:00) y convertir a ISO UTC + return new Date(`${localDateTime}:00-03:00`).toISOString(); +}