refactor(core): centraliza hooks, providers e base compartilhada

This commit is contained in:
Felipe Coutinho
2026-03-09 17:11:55 +00:00
parent 2de5101058
commit 3e06a1d056
76 changed files with 3271 additions and 709 deletions

View File

@@ -37,6 +37,83 @@ const MONTH_NAMES = [
"dezembro",
] as const;
export const OPENMONETIS_TIME_ZONE = "America/Sao_Paulo";
type DateOnlyParts = {
year: number;
month: number;
day: number;
};
function capitalize(value: string): string {
return value.length > 0
? value[0]?.toUpperCase().concat(value.slice(1))
: value;
}
function buildDateOnlyString({ year, month, day }: DateOnlyParts): string {
return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
}
function parseDateOnlyParts(value: string): DateOnlyParts | null {
const match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match) {
return null;
}
const [, yearStr, monthStr, dayStr] = match;
const year = Number.parseInt(yearStr ?? "", 10);
const month = Number.parseInt(monthStr ?? "", 10);
const day = Number.parseInt(dayStr ?? "", 10);
if (
Number.isNaN(year) ||
Number.isNaN(month) ||
Number.isNaN(day) ||
month < 1 ||
month > 12 ||
day < 1 ||
day > 31
) {
return null;
}
const utcDate = new Date(Date.UTC(year, month - 1, day));
if (
utcDate.getUTCFullYear() !== year ||
utcDate.getUTCMonth() !== month - 1 ||
utcDate.getUTCDate() !== day
) {
return null;
}
return { year, month, day };
}
function getTimeZoneParts(
date: Date,
timeZone: string,
): { year: number; month: number; day: number; hour: number } {
const formatter = new Intl.DateTimeFormat("en-CA", {
timeZone,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
hour12: false,
});
const parts = formatter.formatToParts(date);
const getPart = (type: Intl.DateTimeFormatPartTypes) =>
parts.find((part) => part.type === type)?.value ?? "";
return {
year: Number.parseInt(getPart("year"), 10),
month: Number.parseInt(getPart("month"), 10),
day: Number.parseInt(getPart("day"), 10),
hour: Number.parseInt(getPart("hour"), 10),
};
}
// ============================================================================
// DATE CREATION & MANIPULATION
// ============================================================================
@@ -53,48 +130,146 @@ const MONTH_NAMES = [
* @returns Date object in local timezone
*/
export function parseLocalDateString(dateString: string): Date {
const [year, month, day] = dateString.split("-");
return new Date(
Number.parseInt(year ?? "0", 10),
Number.parseInt(month ?? "1", 10) - 1,
Number.parseInt(day ?? "1", 10),
);
const parts = parseDateOnlyParts(dateString);
if (!parts) {
return new Date(Number.NaN);
}
return new Date(parts.year, parts.month - 1, parts.day);
}
/**
* Safely parses a date string (YYYY-MM-DD) as UTC midnight
*/
export function parseUtcDateString(dateString: string): Date | null {
const parts = parseDateOnlyParts(dateString);
if (!parts) {
return null;
}
return new Date(Date.UTC(parts.year, parts.month - 1, parts.day));
}
/**
* Converts a Date or date string to YYYY-MM-DD
*/
export function toDateOnlyString(
value: Date | string | null | undefined,
): string | null {
if (!value) {
return null;
}
if (typeof value === "string") {
const directValue = value.slice(0, 10);
return parseDateOnlyParts(directValue) ? directValue : null;
}
if (Number.isNaN(value.getTime())) {
return null;
}
return buildDateOnlyString({
year: value.getUTCFullYear(),
month: value.getUTCMonth() + 1,
day: value.getUTCDate(),
});
}
/**
* Converts a local Date object to YYYY-MM-DD without timezone normalization
*/
export function toLocalDateString(
value: Date | null | undefined,
): string | null {
if (!value || Number.isNaN(value.getTime())) {
return null;
}
return buildDateOnlyString({
year: value.getFullYear(),
month: value.getMonth() + 1,
day: value.getDate(),
});
}
/**
* Gets today's date as YYYY-MM-DD string
* @returns Formatted date string
*/
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");
export function getTodayDateString(date: Date = new Date()): string {
return toLocalDateString(date) ?? "";
}
return `${year}-${month}-${day}`;
/**
* Gets a date string in YYYY-MM-DD format for a specific timezone
*/
export function getDateStringInTimeZone(
timeZone: string,
date: Date = new Date(),
): string {
const parts = getTimeZoneParts(date, timeZone);
return buildDateOnlyString(parts);
}
/**
* Gets today's date using the app business timezone
*/
export function getBusinessDateString(date: Date = new Date()): string {
return getDateStringInTimeZone(OPENMONETIS_TIME_ZONE, date);
}
/**
* Gets today's date as Date object
* @returns Date object for today
*/
export function getTodayDate(): Date {
return parseLocalDateString(getTodayDateString());
export function getTodayDate(date: Date = new Date()): Date {
return parseLocalDateString(getTodayDateString(date));
}
/**
* Gets today's date as Date object using the app business timezone
*/
export function getBusinessTodayDate(date: Date = new Date()): Date {
return parseLocalDateString(getBusinessDateString(date));
}
/**
* Gets today's info (date and period)
* @returns Object with date and period
*/
export function getTodayInfo(): { date: Date; period: string } {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth();
const day = now.getDate();
export function getTodayInfo(date: Date = new Date()): {
date: Date;
period: string;
} {
const today = getTodayDateString(date);
const parts = parseDateOnlyParts(today);
if (!parts) {
return { date: new Date(Number.NaN), period: "" };
}
return {
date: new Date(year, month, day),
period: `${year}-${String(month + 1).padStart(2, "0")}`,
date: new Date(parts.year, parts.month - 1, parts.day),
period: `${parts.year}-${String(parts.month).padStart(2, "0")}`,
};
}
/**
* Gets today's info using the app business timezone
*/
export function getBusinessTodayInfo(date: Date = new Date()): {
date: Date;
period: string;
} {
const today = getBusinessDateString(date);
const parts = parseDateOnlyParts(today);
if (!parts) {
return { date: new Date(Number.NaN), period: "" };
}
return {
date: new Date(parts.year, parts.month - 1, parts.day),
period: `${parts.year}-${String(parts.month).padStart(2, "0")}`,
};
}
@@ -126,12 +301,20 @@ export function addMonthsToDate(value: Date, offset: number): Date {
// ============================================================================
/**
* Formats a date string (YYYY-MM-DD) to short display format
* Formats a date value to short display format
* @example
* formatDate("2024-11-14") // "qui 14 nov"
*/
export function formatDate(value: string): string {
const parsed = parseLocalDateString(value);
export function formatDate(value: string | Date | null | undefined): string {
const dateString = toDateOnlyString(value);
if (!dateString) {
return "—";
}
const parsed = parseLocalDateString(dateString);
if (Number.isNaN(parsed.getTime())) {
return "—";
}
return new Intl.DateTimeFormat("pt-BR", {
weekday: "short",
@@ -143,6 +326,154 @@ export function formatDate(value: string): string {
.replace(" de", "");
}
/**
* Formats a date-only value (YYYY-MM-DD) using UTC to preserve the civil day
*/
export function formatDateOnly(
value: string | Date | null | undefined,
options: Intl.DateTimeFormatOptions = {},
): string | null {
const dateString = toDateOnlyString(value);
if (!dateString) {
return null;
}
const parsed = parseUtcDateString(dateString);
if (!parsed) {
return null;
}
return new Intl.DateTimeFormat("pt-BR", {
day: "2-digit",
month: "short",
year: "numeric",
timeZone: "UTC",
...options,
}).format(parsed);
}
export function formatDateTime(
value: string | Date | null | undefined,
options: Intl.DateTimeFormatOptions = {
dateStyle: "short",
timeStyle: "short",
},
): string | null {
if (!value) {
return null;
}
const parsed = value instanceof Date ? value : new Date(value);
if (Number.isNaN(parsed.getTime())) {
return null;
}
return new Intl.DateTimeFormat("pt-BR", options).format(parsed);
}
export function formatDateOnlyLabel(
value: string | Date | null | undefined,
prefix?: string,
options?: Intl.DateTimeFormatOptions,
): string | null {
const formatted = formatDateOnly(value, options);
if (!formatted) {
return null;
}
return prefix ? `${prefix} ${formatted}` : formatted;
}
export function formatDateTimeLabel(
value: string | Date | null | undefined,
prefix?: string,
options?: Intl.DateTimeFormatOptions,
): string | null {
const formatted = formatDateTime(value, options);
if (!formatted) {
return null;
}
return prefix ? `${prefix} ${formatted}` : formatted;
}
export function compareDateOnly(
left: string | Date | null | undefined,
right: string | Date | null | undefined,
): number {
const leftValue = toDateOnlyString(left);
const rightValue = toDateOnlyString(right);
if (!leftValue || !rightValue || leftValue === rightValue) {
return 0;
}
return leftValue < rightValue ? -1 : 1;
}
export function isDateOnlyPast(
value: string | Date | null | undefined,
reference: string | Date | null | undefined = getBusinessDateString(),
): boolean {
return compareDateOnly(value, reference) < 0;
}
export function isDateOnlyWithinDays(
value: string | Date | null | undefined,
daysThreshold: number,
reference: string | Date | null | undefined = getBusinessDateString(),
): boolean {
const dateValue = toDateOnlyString(value);
const referenceValue = toDateOnlyString(reference);
if (
!dateValue ||
!referenceValue ||
compareDateOnly(dateValue, referenceValue) < 0
) {
return false;
}
const targetDate = parseUtcDateString(dateValue);
const referenceDate = parseUtcDateString(referenceValue);
if (!targetDate || !referenceDate) {
return false;
}
const limitDate = new Date(referenceDate);
limitDate.setUTCDate(limitDate.getUTCDate() + daysThreshold);
return targetDate <= limitDate;
}
export function buildDateOnlyStringFromPeriodDay(
period: string,
dayValue: string | number,
): string | null {
const [yearPart, monthPart] = period.split("-");
const year = Number.parseInt(yearPart ?? "", 10);
const month = Number.parseInt(monthPart ?? "", 10);
const day = typeof dayValue === "number" ? dayValue : Number(dayValue);
if (
Number.isNaN(year) ||
Number.isNaN(month) ||
Number.isNaN(day) ||
month < 1 ||
month > 12 ||
day < 1
) {
return null;
}
const daysInMonth = new Date(year, month, 0).getDate();
const clampedDay = Math.min(day, daysInMonth);
return buildDateOnlyString({
year,
month,
day: clampedDay,
});
}
/**
* Formats a date to friendly long format
* @example
@@ -173,5 +504,39 @@ export function getGreeting(date: Date = new Date()): string {
return "Boa noite";
}
export function getGreetingInTimeZone(
timeZone: string,
date: Date = new Date(),
): string {
const { hour } = getTimeZoneParts(date, timeZone);
if (hour >= 5 && hour < 12) return "Bom dia";
if (hour >= 12 && hour < 18) return "Boa tarde";
return "Boa noite";
}
export function getBusinessGreeting(date: Date = new Date()): string {
return getGreetingInTimeZone(OPENMONETIS_TIME_ZONE, date);
}
export function formatCurrentDateInTimeZone(
timeZone: string,
date: Date = new Date(),
): string {
return capitalize(
new Intl.DateTimeFormat("pt-BR", {
weekday: "long",
day: "numeric",
month: "long",
year: "numeric",
hour12: false,
timeZone,
}).format(date),
);
}
export function formatBusinessCurrentDate(date: Date = new Date()): string {
return formatCurrentDateInTimeZone(OPENMONETIS_TIME_ZONE, date);
}
// Re-export MONTH_NAMES for convenience
export { MONTH_NAMES };