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";