mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 11:01:45 +00:00
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>
227 lines
7.1 KiB
TypeScript
227 lines
7.1 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
RiBarChartBoxLine,
|
|
RiExternalLinkLine,
|
|
RiEyeLine,
|
|
RiEyeOffLine,
|
|
} from "@remixicon/react";
|
|
import Image from "next/image";
|
|
import Link from "next/link";
|
|
import { useTransition } from "react";
|
|
import { toast } from "sonner";
|
|
import type { DashboardAccount } from "@/features/dashboard/lib/accounts-queries";
|
|
import { updateMyAccountsWidgetPreference } from "@/features/dashboard/widget-registry/widget-actions";
|
|
import MoneyValues from "@/shared/components/money-values";
|
|
import { Badge } from "@/shared/components/ui/badge";
|
|
import { Button } from "@/shared/components/ui/button";
|
|
import { CardFooter } from "@/shared/components/ui/card";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
} from "@/shared/components/ui/tooltip";
|
|
import { WidgetEmptyState } from "@/shared/components/widgets/widget-empty-state";
|
|
import { isAccountInactive } from "@/shared/lib/accounts/constants";
|
|
import { resolveLogoSrc } from "@/shared/lib/logo";
|
|
import { formatPeriodForUrl } from "@/shared/utils/period";
|
|
|
|
type MyAccountsWidgetProps = {
|
|
accounts: DashboardAccount[];
|
|
showExcludedAccounts: boolean;
|
|
onShowExcludedAccountsChange?: (value: boolean) => void;
|
|
totalBalance: number;
|
|
period: string;
|
|
};
|
|
|
|
export function MyAccountsWidget({
|
|
accounts,
|
|
showExcludedAccounts,
|
|
onShowExcludedAccountsChange,
|
|
totalBalance,
|
|
period,
|
|
}: MyAccountsWidgetProps) {
|
|
const [isPending, startTransition] = useTransition();
|
|
|
|
const activeAccounts = accounts.filter(
|
|
(account) => !isAccountInactive(account.status),
|
|
);
|
|
const excludedAccountsCount = activeAccounts.filter(
|
|
(account) => account.excludeFromBalance,
|
|
).length;
|
|
const visibleAccounts = showExcludedAccounts
|
|
? activeAccounts
|
|
: activeAccounts.filter((account) => !account.excludeFromBalance);
|
|
const displayedAccounts = visibleAccounts.slice(0, 5);
|
|
const remainingCount = visibleAccounts.length - displayedAccounts.length;
|
|
const hiddenExcludedAccountsCount = showExcludedAccounts
|
|
? 0
|
|
: excludedAccountsCount;
|
|
const toggleButtonLabel = showExcludedAccounts
|
|
? "Ocultar contas não consideradas"
|
|
: "Mostrar contas não consideradas";
|
|
|
|
const handleToggleExcludedAccounts = () => {
|
|
const nextShowExcludedAccounts = !showExcludedAccounts;
|
|
onShowExcludedAccountsChange?.(nextShowExcludedAccounts);
|
|
|
|
startTransition(async () => {
|
|
const result = await updateMyAccountsWidgetPreference({
|
|
showExcludedAccounts: nextShowExcludedAccounts,
|
|
});
|
|
|
|
if (!result.success) {
|
|
onShowExcludedAccountsChange?.(!nextShowExcludedAccounts);
|
|
toast.error(result.error ?? "Erro ao salvar preferência");
|
|
}
|
|
});
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="flex items-start justify-between gap-3 py-1">
|
|
<div className="space-y-1">
|
|
<p className="text-sm text-muted-foreground">Saldo Total</p>
|
|
<MoneyValues className="text-2xl font-medium" amount={totalBalance} />
|
|
</div>
|
|
|
|
{excludedAccountsCount > 0 ? (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon-sm"
|
|
disabled={isPending}
|
|
className="mt-0.5 text-muted-foreground"
|
|
aria-label={toggleButtonLabel}
|
|
onClick={handleToggleExcludedAccounts}
|
|
>
|
|
{showExcludedAccounts ? (
|
|
<RiEyeOffLine className="size-4" aria-hidden />
|
|
) : (
|
|
<RiEyeLine className="size-4" aria-hidden />
|
|
)}
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="left" className="max-w-xs">
|
|
<p className="text-xs">{toggleButtonLabel}</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
) : null}
|
|
</div>
|
|
|
|
{hiddenExcludedAccountsCount > 0 ? (
|
|
<p className="pb-2 text-xs text-muted-foreground">
|
|
{hiddenExcludedAccountsCount}{" "}
|
|
{hiddenExcludedAccountsCount === 1
|
|
? "conta não considerada oculta"
|
|
: "contas não consideradas ocultas"}
|
|
</p>
|
|
) : null}
|
|
|
|
<div>
|
|
{activeAccounts.length === 0 ? (
|
|
<div className="-mt-10">
|
|
<WidgetEmptyState
|
|
icon={
|
|
<RiBarChartBoxLine className="size-6 text-muted-foreground" />
|
|
}
|
|
title="Você ainda não adicionou nenhuma conta"
|
|
description="Cadastre suas contas bancárias para acompanhar os saldos e movimentações."
|
|
/>
|
|
</div>
|
|
) : displayedAccounts.length === 0 ? (
|
|
<div className="-mt-10">
|
|
<WidgetEmptyState
|
|
icon={<RiEyeOffLine className="size-6 text-muted-foreground" />}
|
|
title="As contas não consideradas estão ocultas"
|
|
description="Use o botão no topo do widget para mostrá-las novamente."
|
|
/>
|
|
</div>
|
|
) : (
|
|
<ul className="flex flex-col">
|
|
{displayedAccounts.map((account, index) => {
|
|
const logoSrc = resolveLogoSrc(account.logo);
|
|
|
|
return (
|
|
<div
|
|
key={account.id}
|
|
className="flex items-center justify-between transition-all duration-300 py-1.5 "
|
|
>
|
|
<div className="flex min-w-0 flex-1 items-center gap-2 py-1">
|
|
<div className="relative size-9.5 overflow-hidden">
|
|
{logoSrc ? (
|
|
<Image
|
|
src={logoSrc}
|
|
alt={`Logo da conta ${account.name}`}
|
|
fill
|
|
sizes="38px"
|
|
className="object-contain rounded-full"
|
|
priority={index === 0}
|
|
/>
|
|
) : null}
|
|
</div>
|
|
|
|
<div className="min-w-0">
|
|
<Link
|
|
prefetch
|
|
href={`/accounts/${
|
|
account.id
|
|
}/statement?periodo=${formatPeriodForUrl(period)}`}
|
|
className="inline-flex max-w-full items-center gap-1 text-sm font-medium text-foreground underline-offset-2 hover:text-primary hover:underline"
|
|
>
|
|
<span className="truncate">{account.name}</span>
|
|
<RiExternalLinkLine
|
|
className="size-3 shrink-0 text-muted-foreground"
|
|
aria-hidden
|
|
/>
|
|
</Link>
|
|
|
|
{account.excludeFromBalance ? (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<span className="inline-flex cursor-help ml-2">
|
|
<Badge className="font-normal" variant="info">
|
|
Não considerada
|
|
</Badge>
|
|
</span>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="top" className="max-w-xs">
|
|
<p className="text-xs">
|
|
Esta conta aparece na lista, mas não entra no
|
|
cálculo do saldo total porque está marcada para
|
|
desconsiderar do saldo total.
|
|
</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
) : null}
|
|
|
|
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
|
<span className="truncate">{account.accountType}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col items-end gap-0.5 text-right">
|
|
<MoneyValues
|
|
className="font-medium"
|
|
amount={account.balance}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
|
|
{remainingCount > 0 ? (
|
|
<CardFooter className="border-border/60 border-t pt-4 text-sm text-muted-foreground">
|
|
+{remainingCount} contas não exibidas
|
|
</CardFooter>
|
|
) : null}
|
|
</>
|
|
);
|
|
}
|