Files
openmonetis/src/features/cards/components/cards-page.tsx
Felipe Coutinho 7d0781b035 refactor: faxina arquitetural — código morto, identificadores em inglês e estrutura padronizada
Refatoração estrutural sem mudanças funcionais. Saldo líquido: −428 linhas.

Removido:
- 14 funções/constantes mortas verificadas via grep no repo todo: validateCategoriaOwnership,
  getInstallmentAnticipationsAction, getAnticipationDetailsAction, formatDecimalForDb,
  currencyFormatterNoCents, optionalDecimalSchema, formatMonthLabel,
  getGoalProgressStatusColorClass, MONTH_PERIOD_PARAM, calculateRemainingInstallments,
  e 5 funções fetch* não usadas em inbox/queries.ts.
- 1 tipo morto (ImportRow) + 2 órfãos consequentes (InstallmentAnticipationWithRelations,
  GoalProgressStatus convertido em interno).
- ~30 export keywords desnecessários (símbolos usados apenas no próprio arquivo).
- Re-exports mortos em barrels: EstablishmentLogoPicker, CategoryReportSkeleton,
  WidgetSkeleton, toNameKey.
- Arquivo features/reports/types.ts (barrel inteiro era órfão).

Padronizado (PT-BR→EN em identificadores expostos):
- 4 constantes globais (LANCAMENTOS_* → TRANSACTIONS_*).
- 12 tipos/interfaces (Lancamento*/Pagador*/Estabelecimento* → equivalentes EN).
- 13 funções/components exportados (fetchPagador*, EstabelecimentoInput, PagadorInfoCard, etc.).
- 5 props cross-file (preLancamentosCount → inboxPendingCount, pagadorAvatarUrl → payerAvatarUrl, etc.).
- Mantidas em PT-BR conforme exceção do CLAUDE.md: variáveis locais (pagador, categoria,
  lancamento), accessor key pagadorName (persistida em preferências), strings de UI.

Reorganizado:
- transactions/: 14 helpers soltos na raiz movidos para lib/; barrel actions.ts reduzido
  de 76 linhas de wrappers para 14 linhas de re-exports puros; anticipation-actions.ts
  movido para actions/anticipation.ts.
- dashboard/: 8 helpers soltos consolidados em dashboard/lib/.
- reports/: 5 query files na raiz consolidados em reports/lib/.
- payers/: detail-actions.ts (21KB) e detail-queries.ts movidos para payers/lib/.
- shared/components/: 9 dos 16 componentes soltos agrupados em brand/, widgets/, feedback/.
- shared/lib/fetch-json.ts movido para shared/utils/fetch-json.ts.

Validação: pnpm exec tsc --noEmit (0 erros), biome check (0 issues), knip (sem unused).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 18:42:54 +00:00

212 lines
5.3 KiB
TypeScript

"use client";
import { RiAddFill, RiBankCard2Line } from "@remixicon/react";
import { useRouter } from "next/navigation";
import { useMemo, useState } from "react";
import { toast } from "sonner";
import { deleteCardAction } from "@/features/cards/actions";
import { ConfirmActionDialog } from "@/shared/components/confirm-action-dialog";
import { EmptyState } from "@/shared/components/feedback/empty-state";
import { Button } from "@/shared/components/ui/button";
import { Card as UiCard } from "@/shared/components/ui/card";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/shared/components/ui/tabs";
import { CardDialog } from "./card-dialog";
import { CardItem } from "./card-item";
import type { Card as CreditCard } from "./types";
type AccountOption = {
id: string;
name: string;
logo: string | null;
};
interface CardsPageProps {
cards: CreditCard[];
archivedCards: CreditCard[];
accounts: AccountOption[];
logoOptions: string[];
}
export function CardsPage({
cards,
archivedCards,
accounts,
logoOptions,
}: CardsPageProps) {
const router = useRouter();
const [activeTab, setActiveTab] = useState("ativos");
const [editOpen, setEditOpen] = useState(false);
const [selectedCard, setSelectedCard] = useState<CreditCard | null>(null);
const [removeOpen, setRemoveOpen] = useState(false);
const [cardToRemove, setCardToRemove] = useState<CreditCard | null>(null);
const orderedCards = useMemo(
() =>
[...cards].sort((a, b) =>
a.name.localeCompare(b.name, "pt-BR", { sensitivity: "base" }),
),
[cards],
);
const orderedArchivedCards = useMemo(
() =>
[...archivedCards].sort((a, b) =>
a.name.localeCompare(b.name, "pt-BR", { sensitivity: "base" }),
),
[archivedCards],
);
const handleEdit = (card: CreditCard) => {
setSelectedCard(card);
setEditOpen(true);
};
const handleEditOpenChange = (open: boolean) => {
setEditOpen(open);
if (!open) {
setSelectedCard(null);
}
};
const handleRemoveRequest = (card: CreditCard) => {
setCardToRemove(card);
setRemoveOpen(true);
};
const handleInvoice = (card: CreditCard) => {
router.push(`/cards/${card.id}/invoice`);
};
const handleRemoveOpenChange = (open: boolean) => {
setRemoveOpen(open);
if (!open) {
setCardToRemove(null);
}
};
const handleRemoveConfirm = async () => {
if (!cardToRemove) {
return;
}
const result = await deleteCardAction({ id: cardToRemove.id });
if (result.success) {
toast.success(result.message);
return;
}
toast.error(result.error);
throw new Error(result.error);
};
const removeTitle = cardToRemove
? `Remover cartão "${cardToRemove.name}"?`
: "Remover cartão?";
const renderCardList = (list: CreditCard[], isArchived: boolean) => {
if (list.length === 0) {
return (
<UiCard className="flex w-full items-center justify-center py-12">
<EmptyState
media={<RiBankCard2Line className="size-6 text-primary" />}
title={
isArchived
? "Nenhum cartão arquivado"
: "Nenhum cartão cadastrado"
}
description={
isArchived
? "Os cartões arquivados aparecerão aqui."
: "Adicione seu primeiro cartão para acompanhar limites e faturas com mais controle."
}
/>
</UiCard>
);
}
return (
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 xl:grid-cols-3">
{list.map((card) => (
<CardItem
key={card.id}
name={card.name}
brand={card.brand}
status={card.status}
closingDay={card.closingDay}
dueDay={card.dueDay}
limit={card.limit}
limitInUse={card.limitInUse ?? null}
limitAvailable={card.limitAvailable ?? card.limit ?? null}
accountName={card.accountName}
logo={card.logo}
note={card.note}
onEdit={() => handleEdit(card)}
onInvoice={() => handleInvoice(card)}
onRemove={() => handleRemoveRequest(card)}
/>
))}
</div>
);
};
return (
<>
<div className="flex w-full flex-col gap-6">
<div className="flex">
<CardDialog
mode="create"
accounts={accounts}
logoOptions={logoOptions}
trigger={
<Button className="w-full sm:w-auto">
<RiAddFill className="size-4" />
Novo cartão
</Button>
}
/>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList>
<TabsTrigger value="ativos">Ativos</TabsTrigger>
<TabsTrigger value="inativos">Inativos</TabsTrigger>
</TabsList>
<TabsContent value="ativos" className="mt-4">
{renderCardList(orderedCards, false)}
</TabsContent>
<TabsContent value="inativos" className="mt-4">
{renderCardList(orderedArchivedCards, true)}
</TabsContent>
</Tabs>
</div>
<CardDialog
mode="update"
accounts={accounts}
logoOptions={logoOptions}
card={selectedCard ?? undefined}
open={editOpen && !!selectedCard}
onOpenChange={handleEditOpenChange}
/>
<ConfirmActionDialog
open={removeOpen && !!cardToRemove}
onOpenChange={handleRemoveOpenChange}
title={removeTitle}
description="Ao remover este cartão, os registros relacionados a ele serão excluídos permanentemente."
confirmLabel="Remover"
pendingLabel="Removendo..."
confirmVariant="destructive"
onConfirm={handleRemoveConfirm}
/>
</>
);
}