Files
sgse-app/packages/backend/convex/utils/datas.ts

132 lines
3.9 KiB
TypeScript

/**
* 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;
}