/** * Utilitários para manipulação de datas no backend * Resolve problemas de timezone ao trabalhar com datas no formato YYYY-MM-DD */ /** * Converte uma string de data no formato YYYY-MM-DD para um objeto Date local * No ambiente Convex, as datas são tratadas como UTC, então precisamos garantir * que a data seja interpretada corretamente. * * @param dateString - String no formato YYYY-MM-DD * @returns Date objeto representando a data * * @example * parseLocalDate('2024-01-15') // Retorna Date para 15/01/2024 */ export function parseLocalDate(dateString: string): Date { if (!dateString || typeof dateString !== 'string') { throw new Error('dateString deve ser uma string válida'); } // Validar formato YYYY-MM-DD const dateRegex = /^\d{4}-\d{2}-\d{2}$/; if (!dateRegex.test(dateString)) { throw new Error('dateString deve estar no formato YYYY-MM-DD'); } // Extrair ano, mês e dia const [year, month, day] = dateString.split('-').map(Number); // No Convex, criar a data usando UTC para evitar problemas de timezone // Usamos UTC para garantir consistência, mas mantemos a data correta const date = new Date(Date.UTC(year, month - 1, day, 0, 0, 0, 0)); // Validar se a data é válida if (isNaN(date.getTime())) { throw new Error(`Data inválida: ${dateString}`); } return date; } /** * Formata uma data para o formato brasileiro (DD/MM/YYYY) * * @param date - Date objeto ou string no formato YYYY-MM-DD * @returns String formatada no formato DD/MM/YYYY */ export function formatarDataBR(date: Date | string): string { let dateObj: Date; if (typeof date === 'string') { dateObj = parseLocalDate(date); } else { dateObj = date; } // Usar UTC para garantir consistência const day = dateObj.getUTCDate().toString().padStart(2, '0'); const month = (dateObj.getUTCMonth() + 1).toString().padStart(2, '0'); const year = dateObj.getUTCFullYear(); return `${day}/${month}/${year}`; } function pad2(n: number): string { return String(n).padStart(2, '0'); } /** * Retorna a data atual (UTC) no formato YYYY-MM-DD. * Útil para comparações lexicográficas com strings YYYY-MM-DD persistidas no banco. */ export function getTodayYMD(): string { const now = new Date(); const y = now.getUTCFullYear(); const m = now.getUTCMonth() + 1; const d = now.getUTCDate(); return `${y}-${pad2(m)}-${pad2(d)}`; } /** * Soma meses (calendário) a uma data YYYY-MM-DD, mantendo o dia quando possível, * e fazendo clamp para o último dia do mês quando necessário. * * Ex.: 2025-03-31 + (-1) mês => 2025-02-28 (ou 29 em ano bissexto) */ export function addMonthsClampedYMD(dateString: string, deltaMonths: number): string { const base = parseLocalDate(dateString); // UTC midnight const year = base.getUTCFullYear(); const monthIndex = base.getUTCMonth(); // 0..11 const day = base.getUTCDate(); const totalMonths = monthIndex + deltaMonths; const newYear = year + Math.floor(totalMonths / 12); let newMonthIndex = totalMonths % 12; if (newMonthIndex < 0) { newMonthIndex += 12; } // Último dia do mês alvo const lastDay = new Date(Date.UTC(newYear, newMonthIndex + 1, 0)).getUTCDate(); const newDay = Math.min(day, lastDay); return `${newYear}-${pad2(newMonthIndex + 1)}-${pad2(newDay)}`; } /** * Retorna o maior (mais recente) entre duas datas YYYY-MM-DD (lexicograficamente). * Se uma delas for null/undefined, retorna a outra. */ export function maxYMD(a?: string | null, b?: string | null): string | null { if (!a && !b) return null; if (!a) return b ?? null; if (!b) return a; return a >= b ? a : b; } /** * Checa se `date` está dentro do intervalo [inicio..fim], onde * `inicio` e `fim` são YYYY-MM-DD (ou null para aberto). */ export function isWithinRangeYMD( date: string, inicio?: string | null, fim?: string | null ): boolean { const start = inicio ?? '0000-01-01'; const end = fim ?? '9999-12-31'; return start <= date && date <= end; }