refactor: remover código morto e exports não utilizados

Remove 6 componentes não utilizados (dashboard-grid, expenses/income
by category widgets, installment analysis panels, fatura-warning-dialog).

Remove funções/tipos não utilizados: successResult, generateApiToken,
validateApiToken, getTodayUTC/Local, formatDateForDb, getDateInfo,
calculatePercentage, roundToDecimals, safeParseInt/Float, isPeriodValid,
getLastPeriods, normalizeWhitespace, formatCurrency wrapper,
InboxItemInput, InboxBatchInput, ProcessInboxInput, DiscardInboxInput,
LancamentosColumnId, 5 funções de anticipation-helpers.

Redireciona imports de formatCurrency para lib/lancamentos/formatting-helpers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Felipe Coutinho
2026-02-26 17:19:33 +00:00
parent c7eabc513e
commit 8de22b9930
22 changed files with 8 additions and 1014 deletions

View File

@@ -1,26 +0,0 @@
import WidgetCard from "@/components/widget-card";
import type { DashboardData } from "@/lib/dashboard/fetch-dashboard-data";
import { widgetsConfig } from "@/lib/dashboard/widgets/widgets-config";
type DashboardGridProps = {
data: DashboardData;
period: string;
};
export function DashboardGrid({ data, period }: DashboardGridProps) {
return (
<section className="grid grid-cols-1 gap-3 @4xl/main:grid-cols-2 @6xl/main:grid-cols-3">
{widgetsConfig.map((widget) => (
<WidgetCard
key={widget.id}
title={widget.title}
subtitle={widget.subtitle}
icon={widget.icon}
action={widget.action}
>
{widget.component({ data, period })}
</WidgetCard>
))}
</section>
);
}

View File

@@ -1,173 +0,0 @@
import {
RiArrowDownSFill,
RiArrowUpSFill,
RiExternalLinkLine,
RiPieChartLine,
RiWallet3Line,
} from "@remixicon/react";
import Link from "next/link";
import MoneyValues from "@/components/money-values";
import type { ExpensesByCategoryData } from "@/lib/dashboard/categories/expenses-by-category";
import { getIconComponent } from "@/lib/utils/icons";
import { formatPeriodForUrl } from "@/lib/utils/period";
import { WidgetEmptyState } from "../widget-empty-state";
type ExpensesByCategoryWidgetProps = {
data: ExpensesByCategoryData;
period: string;
};
const buildInitials = (value: 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 formatPercentage = (value: number) => {
return `${Math.abs(value).toFixed(0)}%`;
};
export function ExpensesByCategoryWidget({
data,
period,
}: ExpensesByCategoryWidgetProps) {
const periodParam = formatPeriodForUrl(period);
if (data.categories.length === 0) {
return (
<WidgetEmptyState
icon={<RiPieChartLine className="size-6 text-muted-foreground" />}
title="Nenhuma despesa encontrada"
description="Quando houver despesas registradas, elas aparecerão aqui."
/>
);
}
return (
<div className="flex flex-col px-0">
{data.categories.map((category) => {
const IconComponent = category.categoryIcon
? getIconComponent(category.categoryIcon)
: null;
const initials = buildInitials(category.categoryName);
const hasIncrease =
category.percentageChange !== null && category.percentageChange > 0;
const hasDecrease =
category.percentageChange !== null && category.percentageChange < 0;
const hasBudget = category.budgetAmount !== null;
const budgetExceeded =
hasBudget &&
category.budgetUsedPercentage !== null &&
category.budgetUsedPercentage > 100;
const formatCurrency = (value: number) =>
new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(value);
const exceededAmount =
budgetExceeded && category.budgetAmount
? category.currentAmount - category.budgetAmount
: 0;
return (
<div
key={category.categoryId}
className="flex flex-col py-2 border-b border-dashed last:border-0"
>
<div className="flex items-center justify-between gap-3">
<div className="flex min-w-0 flex-1 items-center gap-2">
<div className="flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-full bg-muted">
{IconComponent ? (
<IconComponent className="size-4 text-foreground" />
) : (
<span className="text-xs font-semibold uppercase text-muted-foreground">
{initials}
</span>
)}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<Link
href={`/categorias/${category.categoryId}?periodo=${periodParam}`}
className="flex max-w-full items-center gap-1 text-sm font-medium text-foreground underline-offset-2 hover:underline"
>
<span className="truncate">{category.categoryName}</span>
<RiExternalLinkLine
className="size-3 shrink-0 text-muted-foreground"
aria-hidden
/>
</Link>
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<span>
{formatPercentage(category.percentageOfTotal)} da despesa
total
</span>
</div>
</div>
</div>
<div className="flex shrink-0 flex-col items-end gap-0.5">
<MoneyValues
className="text-foreground"
amount={category.currentAmount}
/>
{category.percentageChange !== null && (
<span
className={`flex items-center gap-0.5 text-xs ${
hasIncrease
? "text-destructive"
: hasDecrease
? "text-success"
: "text-muted-foreground"
}`}
>
{hasIncrease && <RiArrowUpSFill className="size-3" />}
{hasDecrease && <RiArrowDownSFill className="size-3" />}
{formatPercentage(category.percentageChange)}
</span>
)}
</div>
</div>
{hasBudget && category.budgetUsedPercentage !== null && (
<div className="ml-11 flex items-center gap-1.5 text-xs">
<RiWallet3Line
className={`size-3 ${
budgetExceeded ? "text-destructive" : "text-info"
}`}
/>
<span
className={budgetExceeded ? "text-destructive" : "text-info"}
>
{budgetExceeded ? (
<>
{formatPercentage(category.budgetUsedPercentage)} do
limite - excedeu em {formatCurrency(exceededAmount)}
</>
) : (
<>
{formatPercentage(category.budgetUsedPercentage)} do
limite
</>
)}
</span>
</div>
)}
</div>
);
})}
</div>
);
}

View File

@@ -1,178 +0,0 @@
import {
RiArrowDownSFill,
RiArrowUpSFill,
RiExternalLinkLine,
RiPieChartLine,
RiWallet3Line,
} from "@remixicon/react";
import Link from "next/link";
import MoneyValues from "@/components/money-values";
import type { IncomeByCategoryData } from "@/lib/dashboard/categories/income-by-category";
import { getIconComponent } from "@/lib/utils/icons";
import { formatPeriodForUrl } from "@/lib/utils/period";
import { WidgetEmptyState } from "../widget-empty-state";
type IncomeByCategoryWidgetProps = {
data: IncomeByCategoryData;
period: string;
};
const buildInitials = (value: 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 formatPercentage = (value: number) => {
return `${Math.abs(value).toFixed(1)}%`;
};
export function IncomeByCategoryWidget({
data,
period,
}: IncomeByCategoryWidgetProps) {
const periodParam = formatPeriodForUrl(period);
if (data.categories.length === 0) {
return (
<WidgetEmptyState
icon={<RiPieChartLine className="size-6 text-muted-foreground" />}
title="Nenhuma receita encontrada"
description="Quando houver receitas registradas, elas aparecerão aqui."
/>
);
}
return (
<div className="flex flex-col gap-2 px-0">
{data.categories.map((category) => {
const IconComponent = category.categoryIcon
? getIconComponent(category.categoryIcon)
: null;
const initials = buildInitials(category.categoryName);
const hasIncrease =
category.percentageChange !== null && category.percentageChange > 0;
const hasDecrease =
category.percentageChange !== null && category.percentageChange < 0;
const hasBudget = category.budgetAmount !== null;
const budgetExceeded =
hasBudget &&
category.budgetUsedPercentage !== null &&
category.budgetUsedPercentage > 100;
const formatCurrency = (value: number) =>
new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
}).format(value);
const exceededAmount =
budgetExceeded && category.budgetAmount
? category.currentAmount - category.budgetAmount
: 0;
return (
<div
key={category.categoryId}
className="flex flex-col gap-1.5 py-2 border-b border-dashed last:border-0"
>
<div className="flex items-center justify-between gap-3">
<div className="flex min-w-0 flex-1 items-center gap-2">
<div className="flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-full bg-muted">
{IconComponent ? (
<IconComponent className="size-4 text-foreground" />
) : (
<span className="text-xs font-semibold uppercase text-muted-foreground">
{initials}
</span>
)}
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<Link
href={`/categorias/${category.categoryId}?periodo=${periodParam}`}
className="flex max-w-full items-center gap-1 text-sm font-medium text-foreground underline-offset-2 hover:underline"
>
<span className="truncate">{category.categoryName}</span>
<RiExternalLinkLine
className="size-3 shrink-0 text-muted-foreground"
aria-hidden
/>
</Link>
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<span>
{formatPercentage(category.percentageOfTotal)} da receita
total
</span>
</div>
</div>
</div>
<div className="flex shrink-0 flex-col items-end gap-0.5">
<MoneyValues
className="text-foreground"
amount={category.currentAmount}
/>
{category.percentageChange !== null && (
<span
className={`flex items-center gap-0.5 text-xs ${
hasIncrease
? "text-success"
: hasDecrease
? "text-destructive"
: "text-muted-foreground"
}`}
>
{hasIncrease && <RiArrowUpSFill className="size-3" />}
{hasDecrease && <RiArrowDownSFill className="size-3" />}
{formatPercentage(category.percentageChange)}
</span>
)}
</div>
</div>
{hasBudget &&
category.budgetUsedPercentage !== null &&
category.budgetAmount !== null && (
<div className="ml-11 flex items-center gap-1.5 text-xs">
<RiWallet3Line
className={`size-3 ${
budgetExceeded ? "text-destructive" : "text-info"
}`}
/>
<span
className={
budgetExceeded ? "text-destructive" : "text-info"
}
>
{budgetExceeded ? (
<>
{formatPercentage(category.budgetUsedPercentage)} do
limite {formatCurrency(category.budgetAmount)} - excedeu
em {formatCurrency(exceededAmount)}
</>
) : (
<>
{formatPercentage(category.budgetUsedPercentage)} do
limite {formatCurrency(category.budgetAmount)}
</>
)}
</span>
</div>
)}
</div>
);
})}
</div>
);
}

View File

@@ -1,52 +0,0 @@
"use client";
import { RiPieChartLine } from "@remixicon/react";
import MoneyValues from "@/components/money-values";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
type AnalysisSummaryPanelProps = {
totalInstallments: number;
grandTotal: number;
selectedCount: number;
};
export function AnalysisSummaryPanel({
totalInstallments,
grandTotal,
selectedCount,
}: AnalysisSummaryPanelProps) {
return (
<Card className="border-primary/20">
<CardHeader className="border-b">
<div className="flex items-center gap-2">
<RiPieChartLine className="size-4 text-primary" />
<CardTitle className="text-base">Resumo</CardTitle>
</div>
</CardHeader>
<CardContent className="flex flex-col gap-3 pt-4">
{/* Total geral */}
<div className="flex flex-col items-center gap-2 rounded-lg bg-primary/10 p-3">
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
Total Selecionado
</p>
<MoneyValues
amount={grandTotal}
className="text-2xl font-bold text-primary"
/>
<p className="text-xs text-muted-foreground">
{selectedCount} {selectedCount === 1 ? "parcela" : "parcelas"}
</p>
</div>
{/* Mensagem quando nada está selecionado */}
{selectedCount === 0 && (
<div className="rounded-full bg-muted/50 p-3 text-center">
<p className="text-xs text-muted-foreground">
Selecione parcelas para ver o resumo
</p>
</div>
)}
</CardContent>
</Card>
);
}

View File

@@ -1,177 +0,0 @@
"use client";
import {
RiArrowDownSLine,
RiArrowRightSLine,
RiBillLine,
} from "@remixicon/react";
import { format, parse } from "date-fns";
import { ptBR } from "date-fns/locale";
import Image from "next/image";
import { useState } from "react";
import MoneyValues from "@/components/money-values";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { cn } from "@/lib/utils/ui";
import type { PendingInvoice } from "./types";
type PendingInvoiceCardProps = {
invoice: PendingInvoice;
isSelected: boolean;
onToggle: () => void;
};
export function PendingInvoiceCard({
invoice,
isSelected,
onToggle,
}: PendingInvoiceCardProps) {
const [isExpanded, setIsExpanded] = useState(false);
// Formatar período (YYYY-MM) para texto legível
const periodDate = parse(invoice.period, "yyyy-MM", new Date());
const periodText = format(periodDate, "MMMM 'de' yyyy", { locale: ptBR });
// Calcular data de vencimento aproximada
const dueDay = parseInt(invoice.dueDay, 10);
const dueDate = new Date(periodDate);
dueDate.setDate(dueDay);
const dueDateText = format(dueDate, "dd/MM/yyyy", { locale: ptBR });
return (
<Card className={cn(isSelected && "border-primary/50 bg-primary/5")}>
<CardContent className="flex flex-col gap-3 py-4">
{/* Header do card */}
<div className="flex items-start gap-3">
<Checkbox
checked={isSelected}
onCheckedChange={onToggle}
className="mt-1"
aria-label={`Selecionar fatura ${invoice.cartaoName} - ${periodText}`}
/>
<div className="min-w-0 flex-1">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
{invoice.cartaoLogo ? (
<Image
src={invoice.cartaoLogo}
alt={invoice.cartaoName}
width={24}
height={24}
className="size-6 rounded-full"
/>
) : (
<div className="flex size-6 items-center justify-center rounded-full bg-muted">
<RiBillLine className="size-4 text-muted-foreground" />
</div>
)}
<p className="font-medium">{invoice.cartaoName}</p>
</div>
<div className="mt-1 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<span className="capitalize">{periodText}</span>
<span>-</span>
<span>Vencimento: {dueDateText}</span>
</div>
</div>
<MoneyValues
amount={invoice.totalAmount}
className="shrink-0 text-sm font-semibold"
/>
</div>
{/* Badge de status */}
<div className="mt-2 flex flex-wrap gap-2">
<Badge variant="destructive" className="text-xs">
Pendente
</Badge>
<Badge variant="secondary" className="text-xs">
{invoice.lancamentos.length}{" "}
{invoice.lancamentos.length === 1
? "lançamento"
: "lançamentos"}
</Badge>
</div>
{/* Botão de expandir */}
<button
type="button"
onClick={() => setIsExpanded(!isExpanded)}
className="mt-3 flex items-center gap-1 text-xs font-medium text-primary hover:underline"
>
{isExpanded ? (
<>
<RiArrowDownSLine className="size-4" />
Ocultar lançamentos ({invoice.lancamentos.length})
</>
) : (
<>
<RiArrowRightSLine className="size-4" />
Ver lançamentos ({invoice.lancamentos.length})
</>
)}
</button>
</div>
</div>
{/* Lista de lançamentos expandida */}
{isExpanded && (
<div className="ml-9 mt-2 flex flex-col gap-2 border-l-2 border-muted pl-4">
{invoice.lancamentos.map((lancamento) => {
const purchaseDate = format(
lancamento.purchaseDate,
"dd/MM/yyyy",
{ locale: ptBR },
);
const installmentLabel =
lancamento.condition === "Parcelado" &&
lancamento.currentInstallment &&
lancamento.installmentCount
? `${lancamento.currentInstallment}/${lancamento.installmentCount}`
: null;
return (
<div
key={lancamento.id}
className="flex items-center gap-3 rounded-md border p-2"
>
<div className="flex min-w-0 flex-1 items-center justify-between gap-3">
<div className="min-w-0">
<p className="truncate text-sm font-medium">
{lancamento.name}
</p>
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<span>{purchaseDate}</span>
{installmentLabel && (
<>
<span>-</span>
<span>Parcela {installmentLabel}</span>
</>
)}
{lancamento.condition !== "Parcelado" && (
<>
<span>-</span>
<span>{lancamento.condition}</span>
</>
)}
</div>
</div>
<MoneyValues
amount={lancamento.amount}
className="shrink-0 text-sm"
/>
</div>
</div>
);
})}
</div>
)}
</CardContent>
</Card>
);
}

View File

@@ -1,84 +0,0 @@
"use client";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { MONTH_NAMES } from "@/lib/utils/period";
export type FaturaWarning = {
nextPeriod: string;
cardName: string;
isPaid: boolean;
isAfterClosing: boolean;
closingDay: string | null;
currentPeriod: string;
};
export function formatPeriodDisplay(period: string): string {
const [yearStr, monthStr] = period.split("-");
const monthIndex = Number.parseInt(monthStr ?? "1", 10) - 1;
const monthName = MONTH_NAMES[monthIndex] ?? monthStr;
return `${monthName}/${yearStr}`;
}
function buildWarningMessage(warning: FaturaWarning): string {
const currentDisplay = formatPeriodDisplay(warning.currentPeriod);
if (warning.isPaid && warning.isAfterClosing) {
return `A fatura do ${warning.cardName} em ${currentDisplay} já está paga e fechou no dia ${warning.closingDay}.`;
}
if (warning.isPaid) {
return `A fatura do ${warning.cardName} em ${currentDisplay} já está paga.`;
}
return `A fatura do ${warning.cardName} fechou no dia ${warning.closingDay}.`;
}
interface FaturaWarningDialogProps {
warning: FaturaWarning | null;
onConfirm: (nextPeriod: string) => void;
onCancel: () => void;
}
export function FaturaWarningDialog({
warning,
onConfirm,
onCancel,
}: FaturaWarningDialogProps) {
if (!warning) return null;
return (
<AlertDialog
open
onOpenChange={(open) => {
if (!open) onCancel();
}}
>
<AlertDialogContent className="sm:max-w-md">
<AlertDialogHeader>
<AlertDialogTitle>Fatura indisponível</AlertDialogTitle>
<AlertDialogDescription>
{buildWarningMessage(warning)} Deseja registrá-lo em{" "}
<span className="font-medium text-foreground">
{formatPeriodDisplay(warning.nextPeriod)}
</span>
?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="flex-col gap-2 sm:flex-col">
<AlertDialogAction onClick={() => onConfirm(warning.nextPeriod)}>
Mover para {formatPeriodDisplay(warning.nextPeriod)}
</AlertDialogAction>
<AlertDialogCancel onClick={onCancel}>
Manter em {formatPeriodDisplay(warning.currentPeriod)}
</AlertDialogCancel>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}

View File

@@ -17,22 +17,5 @@ export interface InboxItem {
updatedAt: Date;
}
export interface ProcessInboxInput {
inboxItemId: string;
name: string;
amount: number;
purchaseDate: string;
condition: string;
paymentMethod: string;
categoriaId: string;
contaId?: string;
cartaoId?: string;
note?: string;
}
export interface DiscardInboxInput {
inboxItemId: string;
}
// Re-export the lancamentos SelectOption for use in inbox components
export type SelectOption = LancamentoSelectOption;

View File

@@ -6,7 +6,8 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { formatCurrency, formatPercentageChange } from "@/lib/relatorios/utils";
import { formatCurrency } from "@/lib/lancamentos/formatting-helpers";
import { formatPercentageChange } from "@/lib/relatorios/utils";
import { cn } from "@/lib/utils/ui";
interface CategoryCellProps {

View File

@@ -4,11 +4,12 @@ import Link from "next/link";
import { useMemo } from "react";
import { CategoryIconBadge } from "@/components/categorias/category-icon-badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { formatCurrency } from "@/lib/lancamentos/formatting-helpers";
import type {
CategoryReportData,
CategoryReportItem,
} from "@/lib/relatorios/types";
import { formatCurrency, formatPeriodLabel } from "@/lib/relatorios/utils";
import { formatPeriodLabel } from "@/lib/relatorios/utils";
import { formatPeriodForUrl } from "@/lib/utils/period";
import { CategoryCell } from "./category-cell";

View File

@@ -18,9 +18,9 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { formatCurrency } from "@/lib/lancamentos/formatting-helpers";
import type { CategoryReportData } from "@/lib/relatorios/types";
import {
formatCurrency,
formatPercentageChange,
formatPeriodLabel,
} from "@/lib/relatorios/utils";

View File

@@ -12,8 +12,9 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { formatCurrency } from "@/lib/lancamentos/formatting-helpers";
import type { CategoryReportItem } from "@/lib/relatorios/types";
import { formatCurrency, formatPeriodLabel } from "@/lib/relatorios/utils";
import { formatPeriodLabel } from "@/lib/relatorios/utils";
import { formatPeriodForUrl } from "@/lib/utils/period";
import DotIcon from "../dot-icon";
import { Card } from "../ui/card";

View File

@@ -5,16 +5,6 @@ export type ActionResult<TData = void> =
| { success: true; message: string; data?: TData }
| { success: false; error: string };
/**
* Success result helper
*/
export function successResult<TData = void>(
message: string,
data?: TData,
): ActionResult<TData> {
return { success: true, message, data };
}
/**
* Error result helper
*/

View File

@@ -144,14 +144,6 @@ export function generateTokenId(): string {
return crypto.randomUUID();
}
/**
* Generate a random API token with prefix
*/
export function generateApiToken(): string {
const randomPart = crypto.randomBytes(32).toString("base64url");
return `os_${randomPart}`;
}
/**
* Hash a token using SHA-256
*/
@@ -236,18 +228,6 @@ export function extractBearerToken(authHeader: string | null): string | null {
return match ? match[1] : null;
}
/**
* Validate an API token and return the payload
* @deprecated Use validateHashToken for os_xxx tokens
*/
export function validateApiToken(token: string): JwtPayload | null {
const payload = verifyJwt(token);
if (!payload || payload.type !== "api_access") {
return null;
}
return payload;
}
/**
* Validate a hash-based API token (os_xxx format)
* Returns the token hash for database lookup

View File

@@ -1,47 +1,5 @@
import type { Lancamento } from "@/db/schema";
import type { EligibleInstallment } from "./anticipation-types";
/**
* Calcula o valor total de antecipação baseado nas parcelas selecionadas
*/
export function calculateTotalAnticipationAmount(
installments: EligibleInstallment[],
): number {
return installments.reduce((sum, inst) => sum + Number(inst.amount), 0);
}
/**
* Valida se o período de antecipação é válido
* O período não pode ser anterior ao período da primeira parcela selecionada
*/
export function validateAnticipationPeriod(
period: string,
installments: EligibleInstallment[],
): boolean {
if (installments.length === 0) return false;
const earliestPeriod = installments.reduce((earliest, inst) => {
return inst.period < earliest ? inst.period : earliest;
}, installments[0].period);
return period >= earliestPeriod;
}
/**
* Formata os números das parcelas antecipadas em uma string legível
* Exemplo: "1, 2, 3" ou "5, 6, 7, 8"
*/
export function getAnticipatedInstallmentNumbers(
installments: EligibleInstallment[],
): string {
const numbers = installments
.map((inst) => inst.currentInstallment)
.filter((num): num is number => num !== null)
.sort((a, b) => a - b)
.join(", ");
return numbers;
}
/**
* Formata o resumo de parcelas antecipadas
* Exemplo: "Parcelas 1-3 de 12" ou "Parcela 5 de 12"
@@ -67,7 +25,7 @@ export function formatAnticipatedInstallmentsRange(
// Se as parcelas são consecutivas
const isConsecutive = numbers.every((num, i) => {
if (i === 0) return true;
return num === numbers[i - 1]! + 1;
return num === (numbers[i - 1] ?? 0) + 1;
});
if (isConsecutive) {
@@ -77,27 +35,6 @@ export function formatAnticipatedInstallmentsRange(
}
}
/**
* Verifica se uma antecipação pode ser cancelada
* Só pode cancelar se o lançamento de antecipação não foi pago
*/
export function canCancelAnticipation(lancamento: Lancamento): boolean {
return lancamento.isSettled !== true;
}
/**
* Ordena parcelas por número da parcela atual
*/
export function sortInstallmentsByNumber(
installments: EligibleInstallment[],
): EligibleInstallment[] {
return [...installments].sort((a, b) => {
const aNum = a.currentInstallment ?? 0;
const bNum = b.currentInstallment ?? 0;
return aNum - bNum;
});
}
/**
* Calcula quantas parcelas restam após uma antecipação
*/
@@ -108,18 +45,6 @@ export function calculateRemainingInstallments(
return Math.max(0, totalInstallments - anticipatedCount);
}
/**
* Valida se as parcelas selecionadas pertencem à mesma série
*/
export function validateInstallmentsSameSeries(
installments: EligibleInstallment[],
_seriesId: string,
): boolean {
// Esta validação será feita no servidor com os dados completos
// Aqui apenas retorna true como placeholder
return installments.length > 0;
}
/**
* Gera descrição automática para o lançamento de antecipação
*/

View File

@@ -14,8 +14,6 @@ export const LANCAMENTOS_REORDERABLE_COLUMN_IDS = [
"contaCartao",
] as const;
export type LancamentosColumnId = (typeof LANCAMENTOS_REORDERABLE_COLUMN_IDS)[number];
export const LANCAMENTOS_COLUMN_LABELS: Record<string, string> = {
name: "Estabelecimento",
transactionType: "Transação",

View File

@@ -1,4 +1,3 @@
import { currencyFormatter } from "@/lib/lancamentos/formatting-helpers";
import { calculatePercentageChange } from "@/lib/utils/math";
import { buildPeriodRange, MONTH_NAMES, parsePeriod } from "@/lib/utils/period";
import type { DateRangeValidation } from "./types";
@@ -95,17 +94,6 @@ export function validateDateRange(
}
}
/**
* Formats a number as Brazilian currency (R$ X.XXX,XX)
* Uses the shared currencyFormatter from formatting-helpers
*
* @param value - Numeric value to format
* @returns Formatted currency string
*/
export function formatCurrency(value: number): string {
return currencyFormatter.format(value);
}
/**
* Formats percentage change for display
* Format: "±X%" or "±X.X%" (one decimal if < 10%)

View File

@@ -14,6 +14,3 @@ export const inboxItemSchema = z.object({
export const inboxBatchSchema = z.object({
items: z.array(inboxItemSchema).min(1).max(50),
});
export type InboxItemInput = z.infer<typeof inboxItemSchema>;
export type InboxBatchInput = z.infer<typeof inboxBatchSchema>;

View File

@@ -61,57 +61,6 @@ export function parseLocalDateString(dateString: string): Date {
);
}
/**
* Gets today's date in UTC
* @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();
return new Date(Date.UTC(year, month, day));
}
/**
* Gets today's date in local timezone
* @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();
return new Date(year, month, day);
}
/**
* Gets today's period in YYYY-MM format (UTC)
* @returns Period string
*/
export function getTodayPeriodUTC(): string {
const now = new Date();
const year = now.getUTCFullYear();
const month = now.getUTCMonth();
return `${year}-${String(month + 1).padStart(2, "0")}`;
}
/**
* Formats date as YYYY-MM-DD string
* @param date - Date to format
* @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");
return `${year}-${month}-${day}`;
}
/**
* Gets today's date as YYYY-MM-DD string
* @returns Formatted date string
@@ -224,27 +173,5 @@ export function getGreeting(date: Date = new Date()): string {
return "Boa noite";
}
// ============================================================================
// DATE INFORMATION
// ============================================================================
/**
* Gets information about a date
* @param date - Date to analyze (defaults to now)
* @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),
};
}
// Re-export MONTH_NAMES for convenience
export { MONTH_NAMES };

View File

@@ -24,28 +24,3 @@ export function calculatePercentageChange(
// Protege contra valores absurdos (retorna null se > 1 milhão %)
return Number.isFinite(change) && Math.abs(change) < 1000000 ? change : null;
}
/**
* Calculates percentage of part relative to total
* @param part - Part value
* @param total - Total value
* @returns Percentage (0-100)
*/
export function calculatePercentage(part: number, total: number): number {
if (total === 0) {
return 0;
}
return (part / total) * 100;
}
/**
* Rounds number to specified decimal places
* @param value - Value to round
* @param decimals - Number of decimal places (default 2)
* @returns Rounded number
*/
export function roundToDecimals(value: number, decimals: number = 2): number {
const multiplier = 10 ** decimals;
return Math.round(value * multiplier) / multiplier;
}

View File

@@ -25,44 +25,3 @@ export function safeToNumber(value: unknown, defaultValue: number = 0): number {
const parsed = Number(value);
return Number.isNaN(parsed) ? defaultValue : parsed;
}
/**
* Safely parses integer from unknown value
* @param value - Value to parse
* @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);
}
if (typeof value === "string") {
const parsed = Number.parseInt(value, 10);
return Number.isNaN(parsed) ? defaultValue : parsed;
}
return defaultValue;
}
/**
* Safely parses float from unknown value
* @param value - Value to parse
* @param defaultValue - Default value if parsing fails
* @returns Parsed float or default value
*/
export function safeParseFloat(
value: unknown,
defaultValue: number = 0,
): number {
if (typeof value === "number") {
return value;
}
if (typeof value === "string") {
const parsed = Number.parseFloat(value);
return Number.isNaN(parsed) ? defaultValue : parsed;
}
return defaultValue;
}

View File

@@ -29,8 +29,6 @@ export const MONTH_NAMES = [
"dezembro",
] as const;
export type MonthName = (typeof MONTH_NAMES)[number];
// ============================================================================
// CORE PARSING & FORMATTING (YYYY-MM format)
// ============================================================================
@@ -63,20 +61,6 @@ export function formatPeriod(year: number, month: number): string {
return `${year}-${String(month).padStart(2, "0")}`;
}
/**
* Validates if period string is valid
* @param period - Period string to validate
* @returns True if valid, false otherwise
*/
export function isPeriodValid(period: string): boolean {
try {
parsePeriod(period);
return true;
} catch {
return false;
}
}
// ============================================================================
// PERIOD NAVIGATION
// ============================================================================
@@ -139,22 +123,6 @@ export function addMonthsToPeriod(period: string, offset: number): string {
return formatPeriod(nextYear, nextMonth);
}
/**
* Gets the last N periods including the current one
* @param current - Current period in YYYY-MM format
* @param length - Number of periods to return
* @returns Array of period strings
*/
export function getLastPeriods(current: string, length: number): string[] {
const periods: string[] = [];
for (let offset = length - 1; offset >= 0; offset -= 1) {
periods.push(addMonthsToPeriod(current, -offset));
}
return periods;
}
// ============================================================================
// PERIOD COMPARISON & RANGES
// ============================================================================

View File

@@ -23,15 +23,6 @@ export function normalizeFilePath(path: string | null | undefined): string {
return path?.split("/").filter(Boolean).pop() ?? "";
}
/**
* Normalizes whitespace in string (replaces multiple spaces with single space)
* @param value - String to normalize
* @returns String with normalized whitespace
*/
export function normalizeWhitespace(value: string): string {
return value.replace(/\s+/g, " ").trim();
}
/**
* Normalizes icon input - trims and returns null if empty
* @param icon - Icon string to normalize