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>
This commit is contained in:
Felipe Coutinho
2026-05-06 18:42:54 +00:00
parent b9b843b9db
commit 7d0781b035
229 changed files with 415 additions and 872 deletions

View File

@@ -1,5 +1,5 @@
import { LogoIcon } from "@/shared/components/logo-icon";
import { LogoText } from "@/shared/components/logo-text";
import { LogoIcon } from "@/shared/components/brand/logo-icon";
import { LogoText } from "@/shared/components/brand/logo-text";
import { cn } from "@/shared/utils/ui";
interface LogoProps {

View File

@@ -27,9 +27,9 @@ const sizeVariants = {
},
} as const;
export type CategoryIconBadgeSize = keyof typeof sizeVariants;
type CategoryIconBadgeSize = keyof typeof sizeVariants;
export interface CategoryIconBadgeProps {
interface CategoryIconBadgeProps {
/** Nome do ícone Remix (ex: "RiShoppingBag3Line") */
icon?: string | null;
/** Nome da categoria — define cor e iniciais de fallback */

View File

@@ -1,8 +1,3 @@
export type {
CategoryIconBadgeProps,
CategoryIconBadgeSize,
} from "./category-icon-badge";
export { CategoryIconBadge } from "./category-icon-badge";
export { EstablishmentLogo } from "./establishment-logo";
export { EstablishmentLogoPicker } from "./establishment-logo-picker";
export { LogoPrefetchProvider } from "./logo-prefetch-provider";

View File

@@ -41,5 +41,3 @@ export function useMonthPeriod() {
buildHref,
};
}
export { PERIOD_PARAM as MONTH_PERIOD_PARAM };

View File

@@ -14,15 +14,15 @@ type AppNavbarProps = {
email: string;
image: string | null;
};
pagadorAvatarUrl: string | null;
preLancamentosCount?: number;
payerAvatarUrl: string | null;
inboxPendingCount?: number;
notificationsSnapshot: DashboardNotificationsSnapshot;
};
export async function AppNavbar({
user,
pagadorAvatarUrl,
preLancamentosCount = 0,
payerAvatarUrl,
inboxPendingCount = 0,
notificationsSnapshot,
}: AppNavbarProps) {
const updateCheck = await checkForUpdate();
@@ -36,14 +36,14 @@ export async function AppNavbar({
unreadCount={notificationsSnapshot.unreadCount}
visibleCount={notificationsSnapshot.visibleCount}
budgetNotifications={notificationsSnapshot.budgetNotifications}
preLancamentosCount={preLancamentosCount}
inboxPendingCount={inboxPendingCount}
/>
<RefreshPageButton variant="navbar" />
<AnimatedThemeToggler variant="navbar" />
</div>
<NavbarUser
user={user}
pagadorAvatarUrl={pagadorAvatarUrl}
payerAvatarUrl={payerAvatarUrl}
updateCheck={updateCheck}
/>
</NavbarShell>

View File

@@ -26,7 +26,7 @@ export type NavItem = {
hideOnMobile?: boolean;
};
export type NavSection = {
type NavSection = {
label: string;
items: NavItem[];
};

View File

@@ -1,5 +1,5 @@
import Link from "next/link";
import { Logo } from "@/shared/components/logo";
import { Logo } from "@/shared/components/brand/logo";
type NavbarShellProps = {
logoHref?: string;

View File

@@ -45,13 +45,13 @@ type NavbarUserProps = {
email: string;
image: string | null;
};
pagadorAvatarUrl: string | null;
payerAvatarUrl: string | null;
updateCheck: UpdateCheckResult;
};
export function NavbarUser({
user,
pagadorAvatarUrl,
payerAvatarUrl,
updateCheck,
}: NavbarUserProps) {
const router = useRouter();
@@ -65,8 +65,8 @@ export function NavbarUser({
setTimeout(() => setCopied(false), 2000);
}
const avatarSrc = pagadorAvatarUrl
? getAvatarSrc(pagadorAvatarUrl)
const avatarSrc = payerAvatarUrl
? getAvatarSrc(payerAvatarUrl)
: user.image || getAvatarSrc(null);
const isDataUrl = avatarSrc.startsWith("data:");

View File

@@ -25,7 +25,7 @@ export function NotificationBell(props: NotificationBellProps) {
hasArchivedItems,
archivedDashboardCount,
hasVisibleItems,
displayedPreLancamentosCount,
displayedInboxPendingCount,
displayedBudgetNotifications,
invoiceNotifications,
boletoNotifications,
@@ -62,7 +62,7 @@ export function NotificationBell(props: NotificationBellProps) {
{hasVisibleItems ? (
<NotificationBellContent
displayedPreLancamentosCount={displayedPreLancamentosCount}
displayedInboxPendingCount={displayedInboxPendingCount}
displayedBudgetNotifications={displayedBudgetNotifications}
invoiceNotifications={invoiceNotifications}
boletoNotifications={boletoNotifications}

View File

@@ -14,7 +14,7 @@ import {
RiTimeLine,
} from "@remixicon/react";
import Image from "next/image";
import StatusDot from "@/shared/components/status-dot";
import StatusDot from "@/shared/components/feedback/status-dot";
import { buttonVariants } from "@/shared/components/ui/button";
import {
Tooltip,
@@ -33,7 +33,7 @@ import type {
} from "./types";
type NotificationBellContentProps = {
displayedPreLancamentosCount: number;
displayedInboxPendingCount: number;
displayedBudgetNotifications: ResolvedBudgetNotification[];
invoiceNotifications: ResolvedDashboardNotification[];
boletoNotifications: ResolvedDashboardNotification[];
@@ -361,7 +361,7 @@ function formatDueDateDetail(
// ---------------------------------------------------------------------------
export function NotificationBellContent({
displayedPreLancamentosCount,
displayedInboxPendingCount,
displayedBudgetNotifications,
invoiceNotifications,
boletoNotifications,
@@ -372,7 +372,7 @@ export function NotificationBellContent({
}: NotificationBellContentProps) {
return (
<div className="max-h-[460px] overflow-y-auto p-2">
{displayedPreLancamentosCount > 0 && (
{displayedInboxPendingCount > 0 && (
<div>
<SectionLabel
icon={<RiAtLine className="size-3" />}
@@ -382,9 +382,9 @@ export function NotificationBellContent({
icon={<RiAtLine className="size-5 text-primary" />}
isOverdue={false}
title={
displayedPreLancamentosCount === 1
displayedInboxPendingCount === 1
? "1 pré-lançamento pendente"
: `${displayedPreLancamentosCount} pré-lançamentos pendentes`
: `${displayedInboxPendingCount} pré-lançamentos pendentes`
}
detail="Aguardando revisão"
onNavigate={onInboxNavigate}

View File

@@ -34,5 +34,5 @@ export type NotificationBellProps = {
unreadCount: number;
visibleCount: number;
budgetNotifications: BudgetNotification[];
preLancamentosCount?: number;
inboxPendingCount?: number;
};

View File

@@ -33,7 +33,7 @@ type UseNotificationBellReturn = {
hasArchivedItems: boolean;
archivedDashboardCount: number;
hasVisibleItems: boolean;
displayedPreLancamentosCount: number;
displayedInboxPendingCount: number;
displayedBudgetNotifications: ResolvedBudgetNotification[];
invoiceNotifications: ResolvedDashboardNotification[];
boletoNotifications: ResolvedDashboardNotification[];
@@ -82,7 +82,7 @@ export function useNotificationBell({
unreadCount: initialUnreadCount,
visibleCount: initialVisibleCount,
budgetNotifications,
preLancamentosCount = 0,
inboxPendingCount = 0,
}: NotificationBellProps): UseNotificationBellReturn {
const [open, setOpen] = useState(false);
const [viewMode, setViewMode] = useState<NotificationViewMode>("active");
@@ -178,19 +178,19 @@ export function useNotificationBell({
const displayedDashboardCount = showArchived
? displayedDashboardCountFromItems
: activeDashboardCount;
const displayedPreLancamentosCount = showArchived ? 0 : preLancamentosCount;
const effectiveUnreadCount = unreadDashboardCountValue + preLancamentosCount;
const displayedInboxPendingCount = showArchived ? 0 : inboxPendingCount;
const effectiveUnreadCount = unreadDashboardCountValue + inboxPendingCount;
const displayCount =
effectiveUnreadCount > 99 ? "99+" : effectiveUnreadCount.toString();
const hasUnreadNotifications = effectiveUnreadCount > 0;
const hasVisibleItems =
displayedDashboardCount + displayedPreLancamentosCount > 0;
displayedDashboardCount + displayedInboxPendingCount > 0;
const hasArchivedItems = archivedDashboardCount > 0;
const hasDashboardNotificationItems = dashboardNotificationCount > 0;
const hasAnySourceItems =
allResolvedNotifications.length +
allResolvedBudgetNotifications.length +
preLancamentosCount >
inboxPendingCount >
0;
const headerCountLabel = `${effectiveUnreadCount} ${effectiveUnreadCount === 1 ? "pendente" : "pendentes"}`;
@@ -306,7 +306,7 @@ export function useNotificationBell({
hasArchivedItems,
archivedDashboardCount,
hasVisibleItems,
displayedPreLancamentosCount,
displayedInboxPendingCount,
displayedBudgetNotifications,
invoiceNotifications,
boletoNotifications,

View File

@@ -3,10 +3,8 @@
* Facilita a importação em outros componentes
*/
export { AccountStatementCardSkeleton } from "./account-statement-card-skeleton";
export { CategoryReportSkeleton } from "./category-report-skeleton";
export { DashboardGridSkeleton } from "./dashboard-grid-skeleton";
export { DashboardMetricsCardsSkeleton } from "./dashboard-metrics-cards-skeleton";
export { FilterSkeleton } from "./filter-skeleton";
export { InvoiceSummaryCardSkeleton } from "./invoice-summary-card-skeleton";
export { TransactionsTableSkeleton } from "./transactions-table-skeleton";
export { WidgetSkeleton } from "./widget-skeleton";

View File

@@ -9,8 +9,8 @@ import {
DialogHeader,
DialogTitle,
} from "@/shared/components/ui/dialog";
import type { WidgetCardProps } from "@/shared/components/widget-card";
import WidgetCard from "@/shared/components/widget-card";
import type { WidgetCardProps } from "@/shared/components/widgets/widget-card";
import WidgetCard from "@/shared/components/widgets/widget-card";
const OVERFLOW_THRESHOLD_PX = 16;
const EXPANDABLE_CONTENT_CLASSNAME =

View File

@@ -2,7 +2,7 @@ import {
PAYMENT_METHODS,
TRANSACTION_CONDITIONS,
TRANSACTION_TYPES,
} from "@/features/transactions/constants";
} from "@/features/transactions/lib/constants";
export const INITIAL_BALANCE_CATEGORY_NAME = "Saldo inicial";
export const INITIAL_BALANCE_NOTE = "saldo inicial";

View File

@@ -23,7 +23,7 @@ export function handleActionError(error: unknown): ActionResult {
/**
* Configuration for revalidation after mutations
*/
export const revalidateConfig = {
const revalidateConfig = {
cards: ["/cards", "/accounts", "/transactions"],
accounts: ["/accounts", "/transactions"],
categories: ["/categories"],

View File

@@ -4,7 +4,7 @@ import { drizzleAdapter } from "better-auth/adapters/drizzle";
import type { GoogleProfile } from "better-auth/social-providers";
import { seedDefaultCategoriesForUser } from "@/shared/lib/categories/defaults";
import { db, schema } from "@/shared/lib/db";
import { ensureDefaultPagadorForUser } from "@/shared/lib/payers/defaults";
import { ensureDefaultPayerForUser } from "@/shared/lib/payers/defaults";
import { normalizeNameFromEmail } from "@/shared/lib/payers/utils";
// ============================================================================
@@ -131,7 +131,7 @@ export const auth = betterAuth({
// Se falhar aqui, o usuário já foi criado - considere usar queue para retry
try {
await seedDefaultCategoriesForUser(user.id);
await ensureDefaultPagadorForUser({
await ensureDefaultPayerForUser({
id: user.id,
name: user.name ?? undefined,
email: user.email ?? undefined,

View File

@@ -5,12 +5,12 @@
* - /lib/category-icons.ts
*/
export type CategoryIconOption = {
type CategoryIconOption = {
label: string;
value: string;
};
export const CATEGORY_ICON_OPTIONS: CategoryIconOption[] = [
const CATEGORY_ICON_OPTIONS: CategoryIconOption[] = [
// Finanças
{ label: "Dinheiro", value: "RiMoneyDollarCircleLine" },
{ label: "Carteira", value: "RiWallet3Line" },
@@ -156,7 +156,7 @@ export const CATEGORY_ICON_OPTIONS: CategoryIconOption[] = [
{ label: "Nuvem Upload", value: "RiCloudUploadLine" },
];
export type CategoryIconGroup = {
type CategoryIconGroup = {
label: string;
icons: CategoryIconOption[];
};

View File

@@ -4,7 +4,7 @@ import type { EligibleInstallment } from "./anticipation-types";
* Formata o resumo de parcelas antecipadas
* Exemplo: "Parcelas 1-3 de 12" ou "Parcela 5 de 12"
*/
export function formatAnticipatedInstallmentsRange(
function formatAnticipatedInstallmentsRange(
installments: EligibleInstallment[],
): string {
const numbers = installments
@@ -35,16 +35,6 @@ export function formatAnticipatedInstallmentsRange(
}
}
/**
* Calcula quantas parcelas restam após uma antecipação
*/
export function calculateRemainingInstallments(
totalInstallments: number,
anticipatedCount: number,
): number {
return Math.max(0, totalInstallments - anticipatedCount);
}
/**
* Gera descrição automática para o lançamento de antecipação
*/

View File

@@ -1,10 +1,3 @@
import type {
Category,
InstallmentAnticipation,
Payer,
Transaction,
} from "@/db/schema";
/**
* Parcela elegível para antecipação
*/
@@ -22,15 +15,6 @@ export type EligibleInstallment = {
payerId: string | null;
};
/**
* Antecipação com dados completos
*/
export type InstallmentAnticipationWithRelations = InstallmentAnticipation & {
transaction: Transaction | null;
payer: Payer | null;
category: Category | null;
};
/**
* Input para criar antecipação
*/

View File

@@ -3,8 +3,6 @@ import { establishmentLogos } from "@/db/schema";
import { db } from "@/shared/lib/db";
import { toNameKey } from "@/shared/lib/logo";
export { toNameKey };
/**
* Busca o domínio salvo para um único estabelecimento.
*/

View File

@@ -2,7 +2,7 @@ import { and, eq } from "drizzle-orm";
import { payerShares, payers, user as usersTable } from "@/db/schema";
import { db } from "@/shared/lib/db";
export type PayerWithAccess = Omit<typeof payers.$inferSelect, "shareCode"> & {
type PayerWithAccess = Omit<typeof payers.$inferSelect, "shareCode"> & {
shareCode: string | null;
canEdit: boolean;
sharedByName: string | null;

View File

@@ -18,7 +18,7 @@ interface SeedUserLike {
image?: string | null;
}
export async function ensureDefaultPagadorForUser(user: SeedUserLike) {
export async function ensureDefaultPayerForUser(user: SeedUserLike) {
const userId = user.id;
if (!userId) {

View File

@@ -47,7 +47,7 @@ export type PayerCardUsageItem = {
amount: number;
};
export type PayerBoletoStats = {
type PayerBoletoStats = {
totalAmount: number;
paidAmount: number;
pendingAmount: number;
@@ -196,7 +196,7 @@ export async function fetchPayerHistory({
}));
}
export async function fetchPagadorCardUsage({
export async function fetchPayerCardUsage({
userId,
payerId,
period,
@@ -239,7 +239,7 @@ export async function fetchPagadorCardUsage({
return items.sort((a, b) => b.amount - a.amount);
}
export async function fetchPagadorBoletoStats({
export async function fetchPayerBoletoStats({
userId,
payerId,
period,
@@ -288,7 +288,7 @@ export async function fetchPagadorBoletoStats({
};
}
export async function fetchPagadorBoletoItems({
export async function fetchPayerBoletoItems({
userId,
payerId,
period,
@@ -330,7 +330,7 @@ export async function fetchPagadorBoletoItems({
return items;
}
export async function fetchPagadorPaymentStatus({
export async function fetchPayerPaymentStatus({
userId,
payerId,
period,

View File

@@ -8,7 +8,7 @@ import { formatDateTime } from "@/shared/utils/date";
type ActionType = "created" | "deleted";
export type NotificationEntry = {
type NotificationEntry = {
payerId: string;
name: string | null;
amount: number;
@@ -20,10 +20,10 @@ export type NotificationEntry = {
note: string | null;
};
export type PayerNotificationRequest = {
type PayerNotificationRequest = {
userLabel: string;
action: ActionType;
entriesByPagador: Map<string, NotificationEntry[]>;
entriesByPayer: Map<string, NotificationEntry[]>;
};
type PayerNotificationRecipient = {
@@ -113,11 +113,11 @@ const buildHtmlBody = ({
export async function sendPayerAutoEmails({
userLabel,
action,
entriesByPagador,
entriesByPayer,
}: PayerNotificationRequest) {
"use server";
if (entriesByPagador.size === 0) {
if (entriesByPayer.size === 0) {
return;
}
@@ -131,7 +131,7 @@ export async function sendPayerAutoEmails({
return;
}
const pagadorIds = Array.from(entriesByPagador.keys());
const pagadorIds = Array.from(entriesByPayer.keys());
if (pagadorIds.length === 0) {
return;
}
@@ -154,7 +154,7 @@ export async function sendPayerAutoEmails({
return;
}
const entries = entriesByPagador.get(payer.id);
const entries = entriesByPayer.get(payer.id);
if (!entries || entries.length === 0) {
return;
}
@@ -186,7 +186,7 @@ export async function sendPayerAutoEmails({
});
}
export type RawNotificationRecord = {
type RawNotificationRecord = {
payerId: string | null;
name: string | null;
amount: string | number | null;

View File

@@ -12,25 +12,6 @@ export const uuidSchema = (entityName: string = "ID") =>
.string({ message: `${entityName} inválido.` })
.uuid(`${entityName} inválido.`);
/**
* Optional/nullable decimal string schema
*/
export const optionalDecimalSchema = z.union([
z.number().nullable(),
z
.string()
.trim()
.optional()
.transform((value) =>
value && value.length > 0 ? value.replace(",", ".") : null,
)
.refine(
(value) => value === null || !Number.isNaN(Number.parseFloat(value)),
"Informe um valor numérico válido.",
)
.transform((value) => (value === null ? null : Number.parseFloat(value))),
]);
/**
* Required positive decimal schema — accepts number or numeric string.
*/

View File

@@ -35,14 +35,14 @@ export type InsightCategoryId = keyof typeof INSIGHT_CATEGORIES;
/**
* Schema para item individual de insight
*/
export const InsightItemSchema = z.object({
const InsightItemSchema = z.object({
text: z.string().min(1),
});
/**
* Schema para categoria de insights
*/
export const InsightCategorySchema = z.object({
const InsightCategorySchema = z.object({
category: z.enum([
"behaviors",
"triggers",

View File

@@ -1,8 +1,8 @@
export type NotificationType = "overdue" | "due_soon";
type NotificationType = "overdue" | "due_soon";
export type BudgetStatus = "exceeded" | "critical";
type BudgetStatus = "exceeded" | "critical";
export type DashboardNotificationStateFields = {
type DashboardNotificationStateFields = {
notificationKey: string;
fingerprint: string;
href: string;

View File

@@ -15,13 +15,6 @@ export const currencyFormatter = new Intl.NumberFormat("pt-BR", {
maximumFractionDigits: 2,
});
export const currencyFormatterNoCents = new Intl.NumberFormat("pt-BR", {
style: "currency",
currency: "BRL",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
});
export const formatCurrency = (
value: number,
options: CurrencyFormatOptions = {},
@@ -47,19 +40,6 @@ export const formatCurrencyCompact = (
/**
* Formats a decimal number for database storage (2 decimal places)
* @param value - The number to format
* @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;
}
return (Math.round(value * 100) / 100).toFixed(2);
}
/**
* Formats a decimal number for database storage (non-nullable version)
* @param value - The number to format
* @returns Formatted string with 2 decimal places
*/
export function formatDecimalForDbRequired(value: number): string {

View File

@@ -205,7 +205,7 @@ const MONTH_MAP = new Map<string, number>(
const normalize = (value: string | null | undefined) =>
(value ?? "").trim().toLowerCase();
export type ParsedPeriod = {
type ParsedPeriod = {
period: string;
monthName: string;
year: number;
@@ -254,7 +254,7 @@ export function parsePeriodParam(
* @param year - Year number
* @returns URL param string in "mes-ano" format
*/
export function formatPeriodParam(monthName: string, year: number): string {
function formatPeriodParam(monthName: string, year: number): string {
return `${normalize(monthName)}-${year}`;
}
@@ -323,15 +323,6 @@ export function displayPeriod(period: string): string {
return `${capitalize(monthName)} de ${year}`;
}
/**
* Alias for displayPeriod - formats period for display
* @example
* formatMonthLabel("2024-01") // "Janeiro de 2024"
*/
export function formatMonthLabel(period: string): string {
return displayPeriod(period);
}
/**
* Formats period for short display with full year
* @example