mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-11 19:41:47 +00:00
feat(finance): refina fluxos de transacoes e pagadores
This commit is contained in:
@@ -14,6 +14,8 @@ import {
|
||||
fetchPagadorHistory,
|
||||
fetchPagadorMonthlyBreakdown,
|
||||
} from "@/lib/pagadores/details";
|
||||
import { formatCurrency } from "@/lib/utils/currency";
|
||||
import { formatDateTime } from "@/lib/utils/date";
|
||||
import { displayPeriod } from "@/lib/utils/period";
|
||||
|
||||
const inputSchema = z.object({
|
||||
@@ -27,20 +29,14 @@ type ActionResult =
|
||||
| { success: true; message: string }
|
||||
| { success: false; error: string };
|
||||
|
||||
const formatCurrency = (value: number) =>
|
||||
value.toLocaleString("pt-BR", {
|
||||
style: "currency",
|
||||
currency: "BRL",
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
|
||||
const formatDate = (value: Date | null | undefined) => {
|
||||
if (!value) return "—";
|
||||
return value.toLocaleDateString("pt-BR", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
});
|
||||
return (
|
||||
formatDateTime(value, {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
}) ?? "—"
|
||||
);
|
||||
};
|
||||
|
||||
// Escapa HTML para prevenir XSS
|
||||
@@ -515,25 +511,47 @@ export async function sendPagadorSummaryAction(
|
||||
.orderBy(desc(lancamentos.purchaseDate)),
|
||||
]);
|
||||
|
||||
const normalizedBoletos: BoletoItem[] = boletoRows.map((row) => ({
|
||||
const normalizedBoletos: BoletoItem[] = (
|
||||
boletoRows as Array<{
|
||||
name: string | null;
|
||||
amount: unknown;
|
||||
dueDate: Date | null;
|
||||
}>
|
||||
).map((row) => ({
|
||||
name: row.name ?? "Sem descrição",
|
||||
amount: Math.abs(Number(row.amount ?? 0)),
|
||||
dueDate: row.dueDate,
|
||||
}));
|
||||
|
||||
const normalizedLancamentos: LancamentoRow[] = lancamentoRows.map(
|
||||
(row) => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
paymentMethod: row.paymentMethod,
|
||||
condition: row.condition,
|
||||
transactionType: row.transactionType,
|
||||
purchaseDate: row.purchaseDate,
|
||||
amount: Number(row.amount ?? 0),
|
||||
}),
|
||||
);
|
||||
const normalizedLancamentos: LancamentoRow[] = (
|
||||
lancamentoRows as Array<{
|
||||
id: string;
|
||||
name: string | null;
|
||||
paymentMethod: string | null;
|
||||
condition: string | null;
|
||||
transactionType: string | null;
|
||||
purchaseDate: Date | null;
|
||||
amount: unknown;
|
||||
}>
|
||||
).map((row) => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
paymentMethod: row.paymentMethod,
|
||||
condition: row.condition,
|
||||
transactionType: row.transactionType,
|
||||
purchaseDate: row.purchaseDate,
|
||||
amount: Number(row.amount ?? 0),
|
||||
}));
|
||||
|
||||
const normalizedParcelados: ParceladoItem[] = parceladoRows.map((row) => {
|
||||
const normalizedParcelados: ParceladoItem[] = (
|
||||
parceladoRows as Array<{
|
||||
name: string | null;
|
||||
amount: unknown;
|
||||
installmentCount: number | null;
|
||||
currentInstallment: number | null;
|
||||
purchaseDate: Date | null;
|
||||
}>
|
||||
).map((row) => {
|
||||
const installmentAmount = Math.abs(Number(row.amount ?? 0));
|
||||
const installmentCount = row.installmentCount ?? 1;
|
||||
const totalAmount = installmentAmount * installmentCount;
|
||||
|
||||
@@ -1,56 +1,47 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
/**
|
||||
* Loading state para a página de detalhes do pagador
|
||||
* Layout: MonthPicker + Info do pagador + Tabs (Visão Geral / Lançamentos)
|
||||
* Loading state para a página de detalhes do pagador.
|
||||
* Layout: navegação mensal + tabs com card compartilhado do pagador.
|
||||
*/
|
||||
export default function PagadorDetailsLoading() {
|
||||
return (
|
||||
<main className="flex flex-col gap-6">
|
||||
{/* Month Picker placeholder */}
|
||||
<div className="h-[60px] animate-pulse rounded-2xl bg-foreground/10" />
|
||||
|
||||
{/* Info do Pagador (sempre visível) */}
|
||||
<div className="rounded-2xl border p-6 space-y-4">
|
||||
<div className="flex items-start gap-4">
|
||||
{/* Avatar */}
|
||||
<Skeleton className="size-20 rounded-full bg-foreground/10" />
|
||||
|
||||
<div className="flex-1 space-y-3">
|
||||
{/* Nome + Badge */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Skeleton className="h-7 w-48 rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-6 w-20 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<Skeleton className="h-5 w-64 rounded-2xl bg-foreground/10" />
|
||||
|
||||
{/* Status */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="size-2 rounded-full bg-foreground/10" />
|
||||
<Skeleton className="h-4 w-16 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Botões de ação */}
|
||||
<div className="flex gap-2">
|
||||
<Skeleton className="h-9 w-9 rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-9 w-9 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="space-y-6 pt-4">
|
||||
<div className="flex gap-2 border-b">
|
||||
<Skeleton className="h-10 w-32 rounded-t-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-10 w-32 rounded-t-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-10 w-36 rounded-t-2xl bg-foreground/10" />
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border p-6 space-y-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<Skeleton className="size-20 rounded-full bg-foreground/10" />
|
||||
|
||||
<div className="flex-1 space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Skeleton className="h-7 w-48 rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-6 w-20 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
|
||||
<Skeleton className="h-5 w-64 rounded-2xl bg-foreground/10" />
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="size-2 rounded-full bg-foreground/10" />
|
||||
<Skeleton className="h-4 w-16 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Skeleton className="h-9 w-9 rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-9 w-9 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Conteúdo da aba Visão Geral (grid de cards) */}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{/* Card de resumo mensal */}
|
||||
<div className="rounded-2xl border p-6 space-y-4 lg:col-span-2">
|
||||
<Skeleton className="h-6 w-48 rounded-2xl bg-foreground/10" />
|
||||
<div className="grid grid-cols-3 gap-4 pt-4">
|
||||
@@ -63,7 +54,6 @@ export default function PagadorDetailsLoading() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Outros cards */}
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="rounded-2xl border p-6 space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import { notFound } from "next/navigation";
|
||||
import { fetchUserPreferences } from "@/app/(dashboard)/ajustes/data";
|
||||
import { getRecentEstablishmentsAction } from "@/app/(dashboard)/lancamentos/actions";
|
||||
import { ExpandableWidgetCard } from "@/components/shared/expandable-widget-card";
|
||||
import { LancamentosPage as LancamentosSection } from "@/components/lancamentos/page/lancamentos-page";
|
||||
import type {
|
||||
ContaCartaoFilterOption,
|
||||
@@ -15,6 +16,7 @@ import type {
|
||||
} from "@/components/lancamentos/types";
|
||||
import MonthNavigation from "@/components/month-picker/month-navigation";
|
||||
import { PagadorCardUsageCard } from "@/components/pagadores/details/pagador-card-usage-card";
|
||||
import { PagadorHeaderCard } from "@/components/pagadores/details/pagador-header-card";
|
||||
import { PagadorHistoryCard } from "@/components/pagadores/details/pagador-history-card";
|
||||
import { PagadorInfoCard } from "@/components/pagadores/details/pagador-info-card";
|
||||
import { PagadorLeaveShareCard } from "@/components/pagadores/details/pagador-leave-share-card";
|
||||
@@ -25,7 +27,6 @@ import {
|
||||
} from "@/components/pagadores/details/pagador-payment-method-cards";
|
||||
import { PagadorSharingCard } from "@/components/pagadores/details/pagador-sharing-card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import WidgetCard from "@/components/widget-card";
|
||||
import type { pagadores } from "@/db/schema";
|
||||
import { getUserId } from "@/lib/auth/server";
|
||||
import {
|
||||
@@ -50,6 +51,7 @@ import {
|
||||
fetchPagadorHistory,
|
||||
fetchPagadorMonthlyBreakdown,
|
||||
fetchPagadorPaymentStatus,
|
||||
type PagadorCardUsageItem,
|
||||
} from "@/lib/pagadores/details";
|
||||
import { parsePeriodParam } from "@/lib/utils/period";
|
||||
import {
|
||||
@@ -232,6 +234,7 @@ export default async function Page({ params, searchParams }: PageProps) {
|
||||
label: pagador.name,
|
||||
slug: pagador.id,
|
||||
role: pagador.role,
|
||||
avatarUrl: pagador.avatarUrl,
|
||||
},
|
||||
],
|
||||
categoriaFiltersRaw: [],
|
||||
@@ -284,7 +287,7 @@ export default async function Page({ params, searchParams }: PageProps) {
|
||||
periodLabel,
|
||||
totalExpenses: monthlyBreakdown.totalExpenses,
|
||||
paymentSplits: monthlyBreakdown.paymentSplits,
|
||||
cardUsage: cardUsage.slice(0, 3).map((item) => ({
|
||||
cardUsage: cardUsage.slice(0, 3).map((item: PagadorCardUsageItem) => ({
|
||||
name: item.name,
|
||||
amount: item.amount,
|
||||
})),
|
||||
@@ -308,15 +311,14 @@ export default async function Page({ params, searchParams }: PageProps) {
|
||||
<TabsTrigger value="painel">Painel</TabsTrigger>
|
||||
<TabsTrigger value="lancamentos">Lançamentos</TabsTrigger>
|
||||
</TabsList>
|
||||
<PagadorHeaderCard
|
||||
pagador={pagadorData}
|
||||
selectedPeriod={selectedPeriod}
|
||||
summary={summaryPreview}
|
||||
/>
|
||||
|
||||
<TabsContent value="profile" className="space-y-4">
|
||||
<section>
|
||||
<PagadorInfoCard
|
||||
pagador={pagadorData}
|
||||
selectedPeriod={selectedPeriod}
|
||||
summary={summaryPreview}
|
||||
/>
|
||||
</section>
|
||||
<PagadorInfoCard pagador={pagadorData} />
|
||||
{canEdit && pagadorData.shareCode ? (
|
||||
<PagadorSharingCard
|
||||
pagadorId={pagador.id}
|
||||
@@ -343,27 +345,27 @@ export default async function Page({ params, searchParams }: PageProps) {
|
||||
</section>
|
||||
|
||||
<section className="grid gap-3 lg:grid-cols-3">
|
||||
<WidgetCard
|
||||
<ExpandableWidgetCard
|
||||
title="Minhas Faturas"
|
||||
subtitle="Valores por cartão neste período"
|
||||
icon={<RiBankCard2Line className="size-4" />}
|
||||
>
|
||||
<PagadorCardUsageCard items={cardUsage} />
|
||||
</WidgetCard>
|
||||
<WidgetCard
|
||||
</ExpandableWidgetCard>
|
||||
<ExpandableWidgetCard
|
||||
title="Boletos"
|
||||
subtitle="Boletos registrados neste período"
|
||||
icon={<RiBarcodeLine className="size-4" />}
|
||||
>
|
||||
<PagadorBoletoCard items={boletoItems} />
|
||||
</WidgetCard>
|
||||
<WidgetCard
|
||||
</ExpandableWidgetCard>
|
||||
<ExpandableWidgetCard
|
||||
title="Status de Pagamento"
|
||||
subtitle="Situação das despesas no período"
|
||||
icon={<RiWallet3Line className="size-4" />}
|
||||
>
|
||||
<PagadorPaymentStatusCard data={paymentStatus} />
|
||||
</WidgetCard>
|
||||
</ExpandableWidgetCard>
|
||||
</section>
|
||||
</TabsContent>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user