refactor: migrate from ESLint to Biome and extract SQL queries to data.ts

- Replace ESLint with Biome for linting and formatting
- Configure Biome with tabs, double quotes, and organized imports
- Move all SQL/Drizzle queries from page.tsx files to data.ts files
- Create new data.ts files for: ajustes, dashboard, relatorios/categorias
- Update existing data.ts files: extrato, fatura (add lancamentos queries)
- Remove all drizzle-orm imports from page.tsx files
- Update README.md with new tooling info

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Felipe Coutinho
2026-01-27 13:15:37 +00:00
parent 8ffe61c59b
commit a7f63fb77a
442 changed files with 66141 additions and 69292 deletions

View File

@@ -1,168 +1,168 @@
export type Operator = "add" | "subtract" | "multiply" | "divide";
export const OPERATOR_SYMBOLS: Record<Operator, string> = {
add: "+",
subtract: "-",
multiply: "×",
divide: "÷",
add: "+",
subtract: "-",
multiply: "×",
divide: "÷",
};
export function formatNumber(value: number): string {
if (!Number.isFinite(value)) {
return "Erro";
}
if (!Number.isFinite(value)) {
return "Erro";
}
const rounded = Number(Math.round(value * 1e10) / 1e10);
return rounded.toString();
const rounded = Number(Math.round(value * 1e10) / 1e10);
return rounded.toString();
}
export function formatLocaleValue(rawValue: string): string {
if (rawValue === "Erro") {
return rawValue;
}
if (rawValue === "Erro") {
return rawValue;
}
const isNegative = rawValue.startsWith("-");
const unsignedValue = isNegative ? rawValue.slice(1) : rawValue;
const isNegative = rawValue.startsWith("-");
const unsignedValue = isNegative ? rawValue.slice(1) : rawValue;
if (unsignedValue === "") {
return isNegative ? "-0" : "0";
}
if (unsignedValue === "") {
return isNegative ? "-0" : "0";
}
const hasDecimalSeparator = unsignedValue.includes(".");
const [integerPartRaw, decimalPartRaw] = unsignedValue.split(".");
const hasDecimalSeparator = unsignedValue.includes(".");
const [integerPartRaw, decimalPartRaw] = unsignedValue.split(".");
const integerPart = integerPartRaw || "0";
const decimalPart = hasDecimalSeparator ? (decimalPartRaw ?? "") : undefined;
const integerPart = integerPartRaw || "0";
const decimalPart = hasDecimalSeparator ? (decimalPartRaw ?? "") : undefined;
const numericInteger = Number(integerPart);
const formattedInteger = Number.isFinite(numericInteger)
? numericInteger.toLocaleString("pt-BR")
: integerPart;
const numericInteger = Number(integerPart);
const formattedInteger = Number.isFinite(numericInteger)
? numericInteger.toLocaleString("pt-BR")
: integerPart;
if (decimalPart === undefined) {
return `${isNegative ? "-" : ""}${formattedInteger}`;
}
if (decimalPart === undefined) {
return `${isNegative ? "-" : ""}${formattedInteger}`;
}
return `${isNegative ? "-" : ""}${formattedInteger},${decimalPart}`;
return `${isNegative ? "-" : ""}${formattedInteger},${decimalPart}`;
}
export function performOperation(
a: number,
b: number,
operator: Operator,
a: number,
b: number,
operator: Operator,
): number {
switch (operator) {
case "add":
return a + b;
case "subtract":
return a - b;
case "multiply":
return a * b;
case "divide":
return b === 0 ? Infinity : a / b;
default:
return b;
}
switch (operator) {
case "add":
return a + b;
case "subtract":
return a - b;
case "multiply":
return a * b;
case "divide":
return b === 0 ? Infinity : a / b;
default:
return b;
}
}
// Trata colagem de valores com formatação brasileira (ponto para milhar, vírgula para decimal)
// e variações simples em formato internacional.
export function normalizeClipboardNumber(rawValue: string): string | null {
const trimmed = rawValue.trim();
if (!trimmed) {
return null;
}
const trimmed = rawValue.trim();
if (!trimmed) {
return null;
}
const match = trimmed.match(/-?[\d.,\s]+/);
if (!match) {
return null;
}
const match = trimmed.match(/-?[\d.,\s]+/);
if (!match) {
return null;
}
let extracted = match[0].replace(/\s+/g, "");
if (!extracted) {
return null;
}
let extracted = match[0].replace(/\s+/g, "");
if (!extracted) {
return null;
}
const isNegative = extracted.startsWith("-");
if (isNegative) {
extracted = extracted.slice(1);
}
const isNegative = extracted.startsWith("-");
if (isNegative) {
extracted = extracted.slice(1);
}
extracted = extracted.replace(/[^\d.,]/g, "");
if (!extracted) {
return null;
}
extracted = extracted.replace(/[^\d.,]/g, "");
if (!extracted) {
return null;
}
const countOccurrences = (char: string) =>
(extracted.match(new RegExp(`\\${char}`, "g")) ?? []).length;
const countOccurrences = (char: string) =>
(extracted.match(new RegExp(`\\${char}`, "g")) ?? []).length;
const hasComma = extracted.includes(",");
const hasDot = extracted.includes(".");
const hasComma = extracted.includes(",");
const hasDot = extracted.includes(".");
let decimalSeparator: "," | "." | null = null;
let decimalSeparator: "," | "." | null = null;
if (hasComma && hasDot) {
decimalSeparator =
extracted.lastIndexOf(",") > extracted.lastIndexOf(".") ? "," : ".";
} else if (hasComma) {
const commaCount = countOccurrences(",");
if (commaCount > 1) {
decimalSeparator = null;
} else {
const digitsAfterComma =
extracted.length - extracted.lastIndexOf(",") - 1;
decimalSeparator =
digitsAfterComma > 0 && digitsAfterComma <= 2 ? "," : null;
}
} else if (hasDot) {
const dotCount = countOccurrences(".");
if (dotCount > 1) {
decimalSeparator = null;
} else {
const digitsAfterDot = extracted.length - extracted.lastIndexOf(".") - 1;
const decimalCandidate = extracted.slice(extracted.lastIndexOf(".") + 1);
const allZeros = /^0+$/.test(decimalCandidate);
const shouldTreatAsDecimal =
digitsAfterDot > 0 &&
digitsAfterDot <= 3 &&
!(digitsAfterDot === 3 && allZeros);
decimalSeparator = shouldTreatAsDecimal ? "." : null;
}
}
if (hasComma && hasDot) {
decimalSeparator =
extracted.lastIndexOf(",") > extracted.lastIndexOf(".") ? "," : ".";
} else if (hasComma) {
const commaCount = countOccurrences(",");
if (commaCount > 1) {
decimalSeparator = null;
} else {
const digitsAfterComma =
extracted.length - extracted.lastIndexOf(",") - 1;
decimalSeparator =
digitsAfterComma > 0 && digitsAfterComma <= 2 ? "," : null;
}
} else if (hasDot) {
const dotCount = countOccurrences(".");
if (dotCount > 1) {
decimalSeparator = null;
} else {
const digitsAfterDot = extracted.length - extracted.lastIndexOf(".") - 1;
const decimalCandidate = extracted.slice(extracted.lastIndexOf(".") + 1);
const allZeros = /^0+$/.test(decimalCandidate);
const shouldTreatAsDecimal =
digitsAfterDot > 0 &&
digitsAfterDot <= 3 &&
!(digitsAfterDot === 3 && allZeros);
decimalSeparator = shouldTreatAsDecimal ? "." : null;
}
}
let integerPart = extracted;
let decimalPart = "";
let integerPart = extracted;
let decimalPart = "";
if (decimalSeparator) {
const decimalIndex = extracted.lastIndexOf(decimalSeparator);
integerPart = extracted.slice(0, decimalIndex);
decimalPart = extracted.slice(decimalIndex + 1);
}
if (decimalSeparator) {
const decimalIndex = extracted.lastIndexOf(decimalSeparator);
integerPart = extracted.slice(0, decimalIndex);
decimalPart = extracted.slice(decimalIndex + 1);
}
integerPart = integerPart.replace(/[^\d]/g, "");
decimalPart = decimalPart.replace(/[^\d]/g, "");
integerPart = integerPart.replace(/[^\d]/g, "");
decimalPart = decimalPart.replace(/[^\d]/g, "");
if (!integerPart) {
integerPart = "0";
}
if (!integerPart) {
integerPart = "0";
}
let normalized = integerPart;
if (decimalPart) {
normalized = `${integerPart}.${decimalPart}`;
}
let normalized = integerPart;
if (decimalPart) {
normalized = `${integerPart}.${decimalPart}`;
}
if (isNegative && Number(normalized) !== 0) {
normalized = `-${normalized}`;
}
if (isNegative && Number(normalized) !== 0) {
normalized = `-${normalized}`;
}
if (!/^(-?\d+(\.\d+)?)$/.test(normalized)) {
return null;
}
if (!/^(-?\d+(\.\d+)?)$/.test(normalized)) {
return null;
}
const numericValue = Number(normalized);
if (!Number.isFinite(numericValue)) {
return null;
}
const numericValue = Number(normalized);
if (!Number.isFinite(numericValue)) {
return null;
}
return normalized;
return normalized;
}

View File

@@ -3,44 +3,44 @@
* Usadas para colorir ícones e backgrounds de categorias
*/
export const CATEGORY_COLORS = [
"#ef4444", // red
"#3b82f6", // blue
"#10b981", // emerald
"#f59e0b", // amber
"#8b5cf6", // violet
"#ec4899", // pink
"#14b8a6", // teal
"#f97316", // orange
"#6366f1", // indigo
"#84cc16", // lime
"#ef4444", // red
"#3b82f6", // blue
"#10b981", // emerald
"#f59e0b", // amber
"#8b5cf6", // violet
"#ec4899", // pink
"#14b8a6", // teal
"#f97316", // orange
"#6366f1", // indigo
"#84cc16", // lime
] as const;
/**
* Retorna a cor para um índice específico (com ciclo)
*/
export function getCategoryColor(index: number): string {
return CATEGORY_COLORS[index % CATEGORY_COLORS.length];
return CATEGORY_COLORS[index % CATEGORY_COLORS.length];
}
/**
* Retorna a cor de background com transparência
*/
export function getCategoryBgColor(index: number): string {
const color = getCategoryColor(index);
return `${color}15`;
const color = getCategoryColor(index);
return `${color}15`;
}
/**
* Gera iniciais a partir de um nome
*/
export function buildCategoryInitials(value: string): string {
const parts = value.trim().split(/\s+/).filter(Boolean);
if (parts.length === 0) return "CT";
if (parts.length === 1) {
const firstPart = parts[0];
return firstPart ? firstPart.slice(0, 2).toUpperCase() : "CT";
}
const firstChar = parts[0]?.[0] ?? "";
const secondChar = parts[1]?.[0] ?? "";
return `${firstChar}${secondChar}`.toUpperCase() || "CT";
const parts = value.trim().split(/\s+/).filter(Boolean);
if (parts.length === 0) return "CT";
if (parts.length === 1) {
const firstPart = parts[0];
return firstPart ? firstPart.slice(0, 2).toUpperCase() : "CT";
}
const firstChar = parts[0]?.[0] ?? "";
const secondChar = parts[1]?.[0] ?? "";
return `${firstChar}${secondChar}`.toUpperCase() || "CT";
}

View File

@@ -8,11 +8,11 @@
* @returns Formatted string with 2 decimal places, or null if input is null
*/
export function formatDecimalForDb(value: number | null): string | null {
if (value === null) {
return null;
}
if (value === null) {
return null;
}
return (Math.round(value * 100) / 100).toFixed(2);
return (Math.round(value * 100) / 100).toFixed(2);
}
/**
@@ -21,7 +21,7 @@ export function formatDecimalForDb(value: number | null): string | null {
* @returns Formatted string with 2 decimal places
*/
export function formatDecimalForDbRequired(value: number): string {
return (Math.round(value * 100) / 100).toFixed(2);
return (Math.round(value * 100) / 100).toFixed(2);
}
/**
@@ -30,7 +30,7 @@ export function formatDecimalForDbRequired(value: number): string {
* @returns Normalized string with period as decimal separator
*/
export function normalizeDecimalInput(value: string): string {
return value.replace(/\s/g, "").replace(",", ".");
return value.replace(/\s/g, "").replace(",", ".");
}
/**
@@ -39,11 +39,11 @@ export function normalizeDecimalInput(value: string): string {
* @returns Formatted string or empty string
*/
export function formatLimitInput(value?: number | null): string {
if (value === null || value === undefined || Number.isNaN(value)) {
return "";
}
if (value === null || value === undefined || Number.isNaN(value)) {
return "";
}
return (Math.round(value * 100) / 100).toFixed(2);
return (Math.round(value * 100) / 100).toFixed(2);
}
/**
@@ -52,9 +52,9 @@ export function formatLimitInput(value?: number | null): string {
* @returns Formatted string with default "0.00"
*/
export function formatInitialBalanceInput(value?: number | null): string {
if (value === null || value === undefined || Number.isNaN(value)) {
return "0.00";
}
if (value === null || value === undefined || Number.isNaN(value)) {
return "0.00";
}
return (Math.round(value * 100) / 100).toFixed(2);
return (Math.round(value * 100) / 100).toFixed(2);
}

View File

@@ -13,28 +13,28 @@
// ============================================================================
const WEEKDAY_NAMES = [
"Domingo",
"Segunda",
"Terça",
"Quarta",
"Quinta",
"Sexta",
"Sábado",
"Domingo",
"Segunda",
"Terça",
"Quarta",
"Quinta",
"Sexta",
"Sábado",
] as const;
const MONTH_NAMES = [
"janeiro",
"fevereiro",
"março",
"abril",
"maio",
"junho",
"julho",
"agosto",
"setembro",
"outubro",
"novembro",
"dezembro",
"janeiro",
"fevereiro",
"março",
"abril",
"maio",
"junho",
"julho",
"agosto",
"setembro",
"outubro",
"novembro",
"dezembro",
] as const;
// ============================================================================
@@ -53,12 +53,12 @@ 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 [year, month, day] = dateString.split("-");
return new Date(
Number.parseInt(year ?? "0", 10),
Number.parseInt(month ?? "1", 10) - 1,
Number.parseInt(day ?? "1", 10),
);
}
/**
@@ -66,12 +66,12 @@ export function parseLocalDateString(dateString: string): Date {
* @returns Date object set to today at midnight UTC
*/
export function getTodayUTC(): Date {
const now = new Date();
const year = now.getUTCFullYear();
const month = now.getUTCMonth();
const day = now.getUTCDate();
const now = new Date();
const year = now.getUTCFullYear();
const month = now.getUTCMonth();
const day = now.getUTCDate();
return new Date(Date.UTC(year, month, day));
return new Date(Date.UTC(year, month, day));
}
/**
@@ -79,12 +79,12 @@ export function getTodayUTC(): Date {
* @returns Date object set to today at midnight local time
*/
export function getTodayLocal(): Date {
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth();
const day = now.getDate();
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth();
const day = now.getDate();
return new Date(year, month, day);
return new Date(year, month, day);
}
/**
@@ -92,11 +92,11 @@ export function getTodayLocal(): Date {
* @returns Period string
*/
export function getTodayPeriodUTC(): string {
const now = new Date();
const year = now.getUTCFullYear();
const month = now.getUTCMonth();
const now = new Date();
const year = now.getUTCFullYear();
const month = now.getUTCMonth();
return `${year}-${String(month + 1).padStart(2, "0")}`;
return `${year}-${String(month + 1).padStart(2, "0")}`;
}
/**
@@ -105,11 +105,11 @@ export function getTodayPeriodUTC(): string {
* @returns Formatted date string
*/
export function formatDateForDb(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
return `${year}-${month}-${day}`;
}
/**
@@ -117,12 +117,12 @@ export function formatDateForDb(date: Date): 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");
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}`;
return `${year}-${month}-${day}`;
}
/**
@@ -130,7 +130,7 @@ export function getTodayDateString(): string {
* @returns Date object for today
*/
export function getTodayDate(): Date {
return parseLocalDateString(getTodayDateString());
return parseLocalDateString(getTodayDateString());
}
/**
@@ -138,15 +138,15 @@ export function getTodayDate(): Date {
* @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();
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth();
const day = now.getDate();
return {
date: new Date(year, month, day),
period: `${year}-${String(month + 1).padStart(2, "0")}`,
};
return {
date: new Date(year, month, day),
period: `${year}-${String(month + 1).padStart(2, "0")}`,
};
}
/**
@@ -156,20 +156,20 @@ export function getTodayInfo(): { date: Date; period: string } {
* @returns New date with months added
*/
export function addMonthsToDate(value: Date, offset: number): Date {
const result = new Date(value);
const originalDay = result.getDate();
const result = new Date(value);
const originalDay = result.getDate();
result.setDate(1);
result.setMonth(result.getMonth() + offset);
result.setDate(1);
result.setMonth(result.getMonth() + offset);
const lastDay = new Date(
result.getFullYear(),
result.getMonth() + 1,
0
).getDate();
const lastDay = new Date(
result.getFullYear(),
result.getMonth() + 1,
0,
).getDate();
result.setDate(Math.min(originalDay, lastDay));
return result;
result.setDate(Math.min(originalDay, lastDay));
return result;
}
// ============================================================================
@@ -182,16 +182,16 @@ export function addMonthsToDate(value: Date, offset: number): Date {
* formatDate("2024-11-14") // "qui 14 nov"
*/
export function formatDate(value: string): string {
const parsed = parseLocalDateString(value);
const parsed = parseLocalDateString(value);
return new Intl.DateTimeFormat("pt-BR", {
weekday: "short",
day: "2-digit",
month: "short",
})
.format(parsed)
.replace(".", "")
.replace(" de", "");
return new Intl.DateTimeFormat("pt-BR", {
weekday: "short",
day: "2-digit",
month: "short",
})
.format(parsed)
.replace(".", "")
.replace(" de", "");
}
/**
@@ -200,12 +200,12 @@ export function formatDate(value: string): string {
* friendlyDate(new Date()) // "Segunda, 14 de novembro de 2025"
*/
export function friendlyDate(date: Date): string {
const weekday = WEEKDAY_NAMES[date.getDay()];
const day = date.getDate();
const month = MONTH_NAMES[date.getMonth()];
const year = date.getFullYear();
const weekday = WEEKDAY_NAMES[date.getDay()];
const day = date.getDate();
const month = MONTH_NAMES[date.getMonth()];
const year = date.getFullYear();
return `${weekday}, ${day} de ${month} de ${year}`;
return `${weekday}, ${day} de ${month} de ${year}`;
}
// ============================================================================
@@ -218,10 +218,10 @@ export function friendlyDate(date: Date): string {
* @returns "Bom dia", "Boa tarde", or "Boa noite"
*/
export function getGreeting(date: Date = new Date()): string {
const hour = date.getHours();
if (hour >= 5 && hour < 12) return "Bom dia";
if (hour >= 12 && hour < 18) return "Boa tarde";
return "Boa noite";
const hour = date.getHours();
if (hour >= 5 && hour < 12) return "Bom dia";
if (hour >= 12 && hour < 18) return "Boa tarde";
return "Boa noite";
}
// ============================================================================
@@ -234,16 +234,16 @@ export function getGreeting(date: Date = new Date()): string {
* @returns Object with date information
*/
export function getDateInfo(date: Date = new Date()) {
return {
date,
year: date.getFullYear(),
month: date.getMonth() + 1,
monthName: MONTH_NAMES[date.getMonth()],
day: date.getDate(),
weekday: WEEKDAY_NAMES[date.getDay()],
friendlyDisplay: friendlyDate(date),
greeting: getGreeting(date),
};
return {
date,
year: date.getFullYear(),
month: date.getMonth() + 1,
monthName: MONTH_NAMES[date.getMonth()],
day: date.getDate(),
weekday: WEEKDAY_NAMES[date.getDay()],
friendlyDisplay: friendlyDate(date),
greeting: getGreeting(date),
};
}
// Re-export MONTH_NAMES for convenience

View File

@@ -4,62 +4,62 @@ import type { ComponentType, ReactNode } from "react";
const ICON_CLASS = "h-4 w-4";
const normalizeKey = (value: string) =>
value
.normalize("NFD")
.replace(/\p{Diacritic}/gu, "")
.toLowerCase()
.replace(/[^a-z0-9]/g, "");
value
.normalize("NFD")
.replace(/\p{Diacritic}/gu, "")
.toLowerCase()
.replace(/[^a-z0-9]/g, "");
export const getIconComponent = (
iconName: string
iconName: string,
): ComponentType<{ className?: string }> | null => {
// Busca o ícone no objeto de ícones do Remix Icon
const icon = (RemixIcons as Record<string, unknown>)[iconName];
// Busca o ícone no objeto de ícones do Remix Icon
const icon = (RemixIcons as Record<string, unknown>)[iconName];
if (icon && typeof icon === "function") {
return icon as ComponentType<{ className?: string }>;
}
if (icon && typeof icon === "function") {
return icon as ComponentType<{ className?: string }>;
}
return null;
return null;
};
export const getConditionIcon = (condition: string): ReactNode => {
const key = normalizeKey(condition);
const key = normalizeKey(condition);
const registry: Record<string, ReactNode> = {
parcelado: <RemixIcons.RiLoader2Fill className={ICON_CLASS} aria-hidden />,
recorrente: <RemixIcons.RiRefreshLine className={ICON_CLASS} aria-hidden />,
avista: <RemixIcons.RiCheckLine className={ICON_CLASS} aria-hidden />,
vista: <RemixIcons.RiCheckLine className={ICON_CLASS} aria-hidden />,
};
const registry: Record<string, ReactNode> = {
parcelado: <RemixIcons.RiLoader2Fill className={ICON_CLASS} aria-hidden />,
recorrente: <RemixIcons.RiRefreshLine className={ICON_CLASS} aria-hidden />,
avista: <RemixIcons.RiCheckLine className={ICON_CLASS} aria-hidden />,
vista: <RemixIcons.RiCheckLine className={ICON_CLASS} aria-hidden />,
};
return registry[key] ?? null;
return registry[key] ?? null;
};
export const getPaymentMethodIcon = (paymentMethod: string): ReactNode => {
const key = normalizeKey(paymentMethod);
const key = normalizeKey(paymentMethod);
const registry: Record<string, ReactNode> = {
dinheiro: (
<RemixIcons.RiMoneyDollarCircleLine className={ICON_CLASS} aria-hidden />
),
pix: <RemixIcons.RiPixLine className={ICON_CLASS} aria-hidden />,
boleto: <RemixIcons.RiBarcodeLine className={ICON_CLASS} aria-hidden />,
credito: (
<RemixIcons.RiMoneyDollarCircleLine className={ICON_CLASS} aria-hidden />
),
cartaodecredito: (
<RemixIcons.RiBankCard2Line className={ICON_CLASS} aria-hidden />
),
cartaodedebito: (
<RemixIcons.RiBankCard2Line className={ICON_CLASS} aria-hidden />
),
debito: <RemixIcons.RiBankCard2Line className={ICON_CLASS} aria-hidden />,
prepagovrva: <RemixIcons.RiCouponLine className={ICON_CLASS} aria-hidden />,
transferenciabancaria: (
<RemixIcons.RiExchangeLine className={ICON_CLASS} aria-hidden />
),
};
const registry: Record<string, ReactNode> = {
dinheiro: (
<RemixIcons.RiMoneyDollarCircleLine className={ICON_CLASS} aria-hidden />
),
pix: <RemixIcons.RiPixLine className={ICON_CLASS} aria-hidden />,
boleto: <RemixIcons.RiBarcodeLine className={ICON_CLASS} aria-hidden />,
credito: (
<RemixIcons.RiMoneyDollarCircleLine className={ICON_CLASS} aria-hidden />
),
cartaodecredito: (
<RemixIcons.RiBankCard2Line className={ICON_CLASS} aria-hidden />
),
cartaodedebito: (
<RemixIcons.RiBankCard2Line className={ICON_CLASS} aria-hidden />
),
debito: <RemixIcons.RiBankCard2Line className={ICON_CLASS} aria-hidden />,
prepagovrva: <RemixIcons.RiCouponLine className={ICON_CLASS} aria-hidden />,
transferenciabancaria: (
<RemixIcons.RiExchangeLine className={ICON_CLASS} aria-hidden />
),
};
return registry[key] ?? null;
return registry[key] ?? null;
};

View File

@@ -9,20 +9,20 @@
* @returns Percentage change or null if previous is 0 and current is also 0
*/
export function calculatePercentageChange(
current: number,
previous: number
current: number,
previous: number,
): number | null {
const EPSILON = 0.01; // Considera valores menores que 1 centavo como zero
const EPSILON = 0.01; // Considera valores menores que 1 centavo como zero
if (Math.abs(previous) < EPSILON) {
if (Math.abs(current) < EPSILON) return null;
return current > 0 ? 100 : -100;
}
if (Math.abs(previous) < EPSILON) {
if (Math.abs(current) < EPSILON) return null;
return current > 0 ? 100 : -100;
}
const change = ((current - previous) / Math.abs(previous)) * 100;
const change = ((current - previous) / Math.abs(previous)) * 100;
// Protege contra valores absurdos (retorna null se > 1 milhão %)
return Number.isFinite(change) && Math.abs(change) < 1000000 ? change : null;
// Protege contra valores absurdos (retorna null se > 1 milhão %)
return Number.isFinite(change) && Math.abs(change) < 1000000 ? change : null;
}
/**
@@ -32,11 +32,11 @@ export function calculatePercentageChange(
* @returns Percentage (0-100)
*/
export function calculatePercentage(part: number, total: number): number {
if (total === 0) {
return 0;
}
if (total === 0) {
return 0;
}
return (part / total) * 100;
return (part / total) * 100;
}
/**
@@ -46,6 +46,6 @@ export function calculatePercentage(part: number, total: number): number {
* @returns Rounded number
*/
export function roundToDecimals(value: number, decimals: number = 2): number {
const multiplier = 10 ** decimals;
return Math.round(value * multiplier) / multiplier;
const multiplier = 10 ** decimals;
return Math.round(value * multiplier) / multiplier;
}

View File

@@ -8,25 +8,22 @@
* @param defaultValue - Default value if conversion fails
* @returns Converted number or default value
*/
export function safeToNumber(
value: unknown,
defaultValue: number = 0
): number {
if (typeof value === "number") {
return value;
}
export function safeToNumber(value: unknown, defaultValue: number = 0): number {
if (typeof value === "number") {
return value;
}
if (typeof value === "string") {
const parsed = Number(value);
return Number.isNaN(parsed) ? defaultValue : parsed;
}
if (typeof value === "string") {
const parsed = Number(value);
return Number.isNaN(parsed) ? defaultValue : parsed;
}
if (value === null || value === undefined) {
return defaultValue;
}
if (value === null || value === undefined) {
return defaultValue;
}
const parsed = Number(value);
return Number.isNaN(parsed) ? defaultValue : parsed;
const parsed = Number(value);
return Number.isNaN(parsed) ? defaultValue : parsed;
}
/**
@@ -35,20 +32,17 @@ export function safeToNumber(
* @param defaultValue - Default value if parsing fails
* @returns Parsed integer or default value
*/
export function safeParseInt(
value: unknown,
defaultValue: number = 0
): number {
if (typeof value === "number") {
return Math.trunc(value);
}
export function safeParseInt(value: unknown, defaultValue: number = 0): number {
if (typeof value === "number") {
return Math.trunc(value);
}
if (typeof value === "string") {
const parsed = Number.parseInt(value, 10);
return Number.isNaN(parsed) ? defaultValue : parsed;
}
if (typeof value === "string") {
const parsed = Number.parseInt(value, 10);
return Number.isNaN(parsed) ? defaultValue : parsed;
}
return defaultValue;
return defaultValue;
}
/**
@@ -58,17 +52,17 @@ export function safeParseInt(
* @returns Parsed float or default value
*/
export function safeParseFloat(
value: unknown,
defaultValue: number = 0
value: unknown,
defaultValue: number = 0,
): number {
if (typeof value === "number") {
return value;
}
if (typeof value === "number") {
return value;
}
if (typeof value === "string") {
const parsed = Number.parseFloat(value);
return Number.isNaN(parsed) ? defaultValue : parsed;
}
if (typeof value === "string") {
const parsed = Number.parseFloat(value);
return Number.isNaN(parsed) ? defaultValue : parsed;
}
return defaultValue;
return defaultValue;
}

View File

@@ -15,18 +15,18 @@
// ============================================================================
export const MONTH_NAMES = [
"janeiro",
"fevereiro",
"março",
"abril",
"maio",
"junho",
"julho",
"agosto",
"setembro",
"outubro",
"novembro",
"dezembro",
"janeiro",
"fevereiro",
"março",
"abril",
"maio",
"junho",
"julho",
"agosto",
"setembro",
"outubro",
"novembro",
"dezembro",
] as const;
export type MonthName = (typeof MONTH_NAMES)[number];
@@ -42,15 +42,15 @@ export type MonthName = (typeof MONTH_NAMES)[number];
* @throws Error if period format is invalid
*/
export function parsePeriod(period: string): { year: number; month: number } {
const [yearStr, monthStr] = period.split("-");
const year = Number.parseInt(yearStr ?? "", 10);
const month = Number.parseInt(monthStr ?? "", 10);
const [yearStr, monthStr] = period.split("-");
const year = Number.parseInt(yearStr ?? "", 10);
const month = Number.parseInt(monthStr ?? "", 10);
if (Number.isNaN(year) || Number.isNaN(month) || month < 1 || month > 12) {
throw new Error(`Período inválido: ${period}`);
}
if (Number.isNaN(year) || Number.isNaN(month) || month < 1 || month > 12) {
throw new Error(`Período inválido: ${period}`);
}
return { year, month };
return { year, month };
}
/**
@@ -60,7 +60,7 @@ export function parsePeriod(period: string): { year: number; month: number } {
* @returns Period string in YYYY-MM format
*/
export function formatPeriod(year: number, month: number): string {
return `${year}-${String(month).padStart(2, "0")}`;
return `${year}-${String(month).padStart(2, "0")}`;
}
/**
@@ -69,12 +69,12 @@ export function formatPeriod(year: number, month: number): string {
* @returns True if valid, false otherwise
*/
export function isPeriodValid(period: string): boolean {
try {
parsePeriod(period);
return true;
} catch {
return false;
}
try {
parsePeriod(period);
return true;
} catch {
return false;
}
}
// ============================================================================
@@ -87,8 +87,8 @@ export function isPeriodValid(period: string): boolean {
* getCurrentPeriod() // "2025-11"
*/
export function getCurrentPeriod(): string {
const now = new Date();
return formatPeriod(now.getFullYear(), now.getMonth() + 1);
const now = new Date();
return formatPeriod(now.getFullYear(), now.getMonth() + 1);
}
/**
@@ -97,13 +97,13 @@ export function getCurrentPeriod(): string {
* @returns Previous period string
*/
export function getPreviousPeriod(period: string): string {
const { year, month } = parsePeriod(period);
const { year, month } = parsePeriod(period);
if (month === 1) {
return formatPeriod(year - 1, 12);
}
if (month === 1) {
return formatPeriod(year - 1, 12);
}
return formatPeriod(year, month - 1);
return formatPeriod(year, month - 1);
}
/**
@@ -112,13 +112,13 @@ export function getPreviousPeriod(period: string): string {
* @returns Next period string
*/
export function getNextPeriod(period: string): string {
const { year, month } = parsePeriod(period);
const { year, month } = parsePeriod(period);
if (month === 12) {
return formatPeriod(year + 1, 1);
}
if (month === 12) {
return formatPeriod(year + 1, 1);
}
return formatPeriod(year, month + 1);
return formatPeriod(year, month + 1);
}
/**
@@ -128,15 +128,15 @@ export function getNextPeriod(period: string): string {
* @returns New period string
*/
export function addMonthsToPeriod(period: string, offset: number): string {
const { year: baseYear, month: baseMonth } = parsePeriod(period);
const { year: baseYear, month: baseMonth } = parsePeriod(period);
const date = new Date(baseYear, baseMonth - 1, 1);
date.setMonth(date.getMonth() + offset);
const date = new Date(baseYear, baseMonth - 1, 1);
date.setMonth(date.getMonth() + offset);
const nextYear = date.getFullYear();
const nextMonth = date.getMonth() + 1;
const nextYear = date.getFullYear();
const nextMonth = date.getMonth() + 1;
return formatPeriod(nextYear, nextMonth);
return formatPeriod(nextYear, nextMonth);
}
/**
@@ -146,13 +146,13 @@ export function addMonthsToPeriod(period: string, offset: number): string {
* @returns Array of period strings
*/
export function getLastPeriods(current: string, length: number): string[] {
const periods: string[] = [];
const periods: string[] = [];
for (let offset = length - 1; offset >= 0; offset -= 1) {
periods.push(addMonthsToPeriod(current, -offset));
}
for (let offset = length - 1; offset >= 0; offset -= 1) {
periods.push(addMonthsToPeriod(current, -offset));
}
return periods;
return periods;
}
// ============================================================================
@@ -166,8 +166,8 @@ export function getLastPeriods(current: string, length: number): string[] {
* @returns -1 if a < b, 0 if equal, 1 if a > b
*/
export function comparePeriods(a: string, b: string): number {
if (a === b) return 0;
return a < b ? -1 : 1;
if (a === b) return 0;
return a < b ? -1 : 1;
}
/**
@@ -177,34 +177,34 @@ export function comparePeriods(a: string, b: string): number {
* @returns Array of period strings
*/
export function buildPeriodRange(start: string, end: string): string[] {
const [startKey, endKey] =
comparePeriods(start, end) <= 0 ? [start, end] : [end, start];
const [startKey, endKey] =
comparePeriods(start, end) <= 0 ? [start, end] : [end, start];
const startParts = parsePeriod(startKey);
const endParts = parsePeriod(endKey);
const startParts = parsePeriod(startKey);
const endParts = parsePeriod(endKey);
const items: string[] = [];
let currentYear = startParts.year;
let currentMonth = startParts.month;
const items: string[] = [];
let currentYear = startParts.year;
let currentMonth = startParts.month;
while (
currentYear < endParts.year ||
(currentYear === endParts.year && currentMonth <= endParts.month)
) {
items.push(formatPeriod(currentYear, currentMonth));
while (
currentYear < endParts.year ||
(currentYear === endParts.year && currentMonth <= endParts.month)
) {
items.push(formatPeriod(currentYear, currentMonth));
if (currentYear === endParts.year && currentMonth === endParts.month) {
break;
}
if (currentYear === endParts.year && currentMonth === endParts.month) {
break;
}
currentMonth += 1;
if (currentMonth > 12) {
currentMonth = 1;
currentYear += 1;
}
}
currentMonth += 1;
if (currentMonth > 12) {
currentMonth = 1;
currentYear += 1;
}
}
return items;
return items;
}
// ============================================================================
@@ -214,12 +214,12 @@ export function buildPeriodRange(start: string, end: string): string[] {
const MONTH_MAP = new Map(MONTH_NAMES.map((name, index) => [name, index]));
const normalize = (value: string | null | undefined) =>
(value ?? "").trim().toLowerCase();
(value ?? "").trim().toLowerCase();
export type ParsedPeriod = {
period: string;
monthName: string;
year: number;
period: string;
monthName: string;
year: number;
};
/**
@@ -229,34 +229,34 @@ export type ParsedPeriod = {
* @returns Parsed period object
*/
export function parsePeriodParam(
periodParam: string | null | undefined,
referenceDate = new Date()
periodParam: string | null | undefined,
referenceDate = new Date(),
): ParsedPeriod {
const fallbackMonthIndex = referenceDate.getMonth();
const fallbackYear = referenceDate.getFullYear();
const fallbackPeriod = formatPeriod(fallbackYear, fallbackMonthIndex + 1);
const fallbackMonthIndex = referenceDate.getMonth();
const fallbackYear = referenceDate.getFullYear();
const fallbackPeriod = formatPeriod(fallbackYear, fallbackMonthIndex + 1);
if (!periodParam) {
const monthName = MONTH_NAMES[fallbackMonthIndex];
return { period: fallbackPeriod, monthName, year: fallbackYear };
}
if (!periodParam) {
const monthName = MONTH_NAMES[fallbackMonthIndex];
return { period: fallbackPeriod, monthName, year: fallbackYear };
}
const [rawMonth, rawYear] = periodParam.split("-");
const normalizedMonth = normalize(rawMonth);
const monthIndex = MONTH_MAP.get(normalizedMonth);
const parsedYear = Number.parseInt(rawYear ?? "", 10);
const [rawMonth, rawYear] = periodParam.split("-");
const normalizedMonth = normalize(rawMonth);
const monthIndex = MONTH_MAP.get(normalizedMonth);
const parsedYear = Number.parseInt(rawYear ?? "", 10);
if (monthIndex === undefined || Number.isNaN(parsedYear)) {
const monthName = MONTH_NAMES[fallbackMonthIndex];
return { period: fallbackPeriod, monthName, year: fallbackYear };
}
if (monthIndex === undefined || Number.isNaN(parsedYear)) {
const monthName = MONTH_NAMES[fallbackMonthIndex];
return { period: fallbackPeriod, monthName, year: fallbackYear };
}
const monthName = MONTH_NAMES[monthIndex];
return {
period: formatPeriod(parsedYear, monthIndex + 1),
monthName,
year: parsedYear,
};
const monthName = MONTH_NAMES[monthIndex];
return {
period: formatPeriod(parsedYear, monthIndex + 1),
monthName,
year: parsedYear,
};
}
/**
@@ -266,7 +266,7 @@ export function parsePeriodParam(
* @returns URL param string in "mes-ano" format
*/
export function formatPeriodParam(monthName: string, year: number): string {
return `${normalize(monthName)}-${year}`;
return `${normalize(monthName)}-${year}`;
}
/**
@@ -276,21 +276,21 @@ export function formatPeriodParam(monthName: string, year: number): string {
* formatPeriodForUrl("2025-01") // "janeiro-2025"
*/
export function formatPeriodForUrl(period: string): string {
const [yearStr, monthStr] = period.split("-");
const year = Number.parseInt(yearStr ?? "", 10);
const monthIndex = Number.parseInt(monthStr ?? "", 10) - 1;
const [yearStr, monthStr] = period.split("-");
const year = Number.parseInt(yearStr ?? "", 10);
const monthIndex = Number.parseInt(monthStr ?? "", 10) - 1;
if (
Number.isNaN(year) ||
Number.isNaN(monthIndex) ||
monthIndex < 0 ||
monthIndex > 11
) {
return period;
}
if (
Number.isNaN(year) ||
Number.isNaN(monthIndex) ||
monthIndex < 0 ||
monthIndex > 11
) {
return period;
}
const monthName = MONTH_NAMES[monthIndex] ?? "";
return formatPeriodParam(monthName, year);
const monthName = MONTH_NAMES[monthIndex] ?? "";
return formatPeriodParam(monthName, year);
}
// ============================================================================
@@ -298,9 +298,9 @@ export function formatPeriodForUrl(period: string): string {
// ============================================================================
function capitalize(value: string): string {
return value.length > 0
? value[0]?.toUpperCase().concat(value.slice(1))
: value;
return value.length > 0
? value[0]?.toUpperCase().concat(value.slice(1))
: value;
}
/**
@@ -309,9 +309,9 @@ function capitalize(value: string): string {
* displayPeriod("2025-11") // "Novembro de 2025"
*/
export function displayPeriod(period: string): string {
const { year, month } = parsePeriod(period);
const monthName = MONTH_NAMES[month - 1] ?? "";
return `${capitalize(monthName)} de ${year}`;
const { year, month } = parsePeriod(period);
const monthName = MONTH_NAMES[month - 1] ?? "";
return `${capitalize(monthName)} de ${year}`;
}
/**
@@ -320,7 +320,7 @@ export function displayPeriod(period: string): string {
* formatMonthLabel("2024-01") // "Janeiro de 2024"
*/
export function formatMonthLabel(period: string): string {
return displayPeriod(period);
return displayPeriod(period);
}
// ============================================================================
@@ -334,24 +334,23 @@ export function formatMonthLabel(period: string): string {
* derivePeriodFromDate() // current period
*/
export function derivePeriodFromDate(value?: string | null): string {
if (!value) {
return getCurrentPeriod();
}
if (!value) {
return getCurrentPeriod();
}
// Parse date string as local date to avoid timezone issues
// IMPORTANT: new Date("2025-11-25") treats the date as UTC midnight,
// which in Brazil (UTC-3) becomes 2025-11-26 03:00 local time!
const [year, month, day] = value.split("-");
const date = new Date(
Number.parseInt(year ?? "0", 10),
Number.parseInt(month ?? "1", 10) - 1,
Number.parseInt(day ?? "1", 10)
);
// Parse date string as local date to avoid timezone issues
// IMPORTANT: new Date("2025-11-25") treats the date as UTC midnight,
// which in Brazil (UTC-3) becomes 2025-11-26 03:00 local time!
const [year, month, day] = value.split("-");
const date = new Date(
Number.parseInt(year ?? "0", 10),
Number.parseInt(month ?? "1", 10) - 1,
Number.parseInt(day ?? "1", 10),
);
if (Number.isNaN(date.getTime())) {
return getCurrentPeriod();
}
if (Number.isNaN(date.getTime())) {
return getCurrentPeriod();
}
return formatPeriod(date.getFullYear(), date.getMonth() + 1);
return formatPeriod(date.getFullYear(), date.getMonth() + 1);
}

View File

@@ -8,10 +8,10 @@
* @returns Trimmed string or null if empty
*/
export function normalizeOptionalString(
value: string | null | undefined
value: string | null | undefined,
): string | null {
const trimmed = value?.trim() ?? "";
return trimmed.length > 0 ? trimmed : null;
const trimmed = value?.trim() ?? "";
return trimmed.length > 0 ? trimmed : null;
}
/**
@@ -20,7 +20,7 @@ export function normalizeOptionalString(
* @returns Filename without path
*/
export function normalizeFilePath(path: string | null | undefined): string {
return path?.split("/").filter(Boolean).pop() ?? "";
return path?.split("/").filter(Boolean).pop() ?? "";
}
/**
@@ -29,7 +29,7 @@ export function normalizeFilePath(path: string | null | undefined): string {
* @returns String with normalized whitespace
*/
export function normalizeWhitespace(value: string): string {
return value.replace(/\s+/g, " ").trim();
return value.replace(/\s+/g, " ").trim();
}
/**
@@ -38,6 +38,6 @@ export function normalizeWhitespace(value: string): string {
* @returns Trimmed icon string or null
*/
export function normalizeIconInput(icon?: string | null): string | null {
const trimmed = icon?.trim() ?? "";
return trimmed.length > 0 ? trimmed : null;
const trimmed = icon?.trim() ?? "";
return trimmed.length > 0 ? trimmed : null;
}

View File

@@ -4,7 +4,7 @@
* This module contains UI-related utilities, primarily for className manipulation.
*/
import { clsx, type ClassValue } from "clsx";
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
/**
@@ -13,5 +13,5 @@ import { twMerge } from "tailwind-merge";
* @returns Merged className string
*/
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
return twMerge(clsx(inputs));
}