feat(finance): refina fluxos de transacoes e pagadores

This commit is contained in:
Felipe Coutinho
2026-03-09 17:13:44 +00:00
parent 69da27276c
commit ada1377640
58 changed files with 1288 additions and 1559 deletions

View File

@@ -1,22 +1,11 @@
import { RiBankCard2Line } from "@remixicon/react";
import Image from "next/image";
import MoneyValues from "@/components/money-values";
import MoneyValues from "@/components/shared/money-values";
import { WidgetEmptyState } from "@/components/shared/widget-empty-state";
import { CardContent } from "@/components/ui/card";
import { WidgetEmptyState } from "@/components/widget-empty-state";
import { resolveLogoSrc } from "@/lib/logo";
import type { PagadorCardUsageItem } from "@/lib/pagadores/details";
const resolveLogoPath = (logo?: string | null) => {
if (!logo) return null;
if (
logo.startsWith("http://") ||
logo.startsWith("https://") ||
logo.startsWith("data:")
) {
return logo;
}
return logo.startsWith("/") ? logo : `/logos/${logo}`;
};
const buildInitials = (value: string) => {
const parts = value.trim().split(/\s+/).filter(Boolean);
if (parts.length === 0) return "CC";
@@ -50,7 +39,7 @@ export function PagadorCardUsageCard({ items }: PagadorCardUsageCardProps) {
<CardContent className="flex flex-col gap-4 px-0">
<ul className="flex flex-col">
{items.map((item) => {
const logoPath = resolveLogoPath(item.logo);
const logoPath = resolveLogoSrc(item.logo);
const initials = buildInitials(item.name);
return (
<li

View File

@@ -0,0 +1,365 @@
"use client";
import {
RiBankCard2Line,
RiBillLine,
RiExchangeDollarLine,
RiMailLine,
RiMailSendLine,
RiVerifiedBadgeFill,
} from "@remixicon/react";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState, useTransition } from "react";
import { toast } from "sonner";
import { sendPagadorSummaryAction } from "@/app/(dashboard)/pagadores/[pagadorId]/actions";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
import { getAvatarSrc } from "@/lib/pagadores/utils";
import { formatCurrency } from "@/lib/utils/currency";
import { formatDateTime } from "@/lib/utils/date";
import type { PagadorInfo, PagadorSummaryPreview } from "./types";
type PagadorHeaderCardProps = {
pagador: PagadorInfo;
selectedPeriod: string;
summary: PagadorSummaryPreview;
};
export function PagadorHeaderCard({
pagador,
selectedPeriod,
summary,
}: PagadorHeaderCardProps) {
const router = useRouter();
const [isSending, startTransition] = useTransition();
const [confirmOpen, setConfirmOpen] = useState(false);
const avatarSrc = getAvatarSrc(pagador.avatarUrl);
const createdAtLabel = formatDate(pagador.createdAt);
const isAdmin = pagador.role === PAGADOR_ROLE_ADMIN;
const lastMailLabel =
formatDateTime(pagador.lastMailAt, {
dateStyle: "short",
timeStyle: "short",
}) ?? "Nunca enviado";
const disableSend = isSending || !pagador.email || !pagador.canEdit;
const openConfirmDialog = () => {
if (!pagador.email) {
toast.error("Cadastre um e-mail para este pagador antes de enviar.");
return;
}
setConfirmOpen(true);
};
const handleSendSummary = () => {
if (!pagador.email) {
toast.error("Cadastre um e-mail para este pagador antes de enviar.");
return;
}
startTransition(async () => {
const result = await sendPagadorSummaryAction({
pagadorId: pagador.id,
period: selectedPeriod,
});
if (!result.success) {
toast.error(result.error);
return;
}
toast.success(result.message);
setConfirmOpen(false);
router.refresh();
});
};
const getStatusBadgeVariant = (status: string): "success" | "outline" => {
const normalizedStatus = status.toLowerCase();
if (normalizedStatus === "ativo") {
return "success";
}
return "outline";
};
return (
<Card className="mb-2 border gap-4">
<CardHeader className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div className="flex flex-1 items-start gap-4">
<div className="relative flex size-16 shrink-0 items-center justify-center overflow-hidden">
<Image
src={avatarSrc}
alt={`Avatar de ${pagador.name}`}
width={64}
height={64}
className="h-full w-full rounded-full object-cover"
/>
</div>
<div className="flex flex-1 flex-col gap-2">
<div className="flex flex-wrap items-center gap-2">
<CardTitle className="text-xl font-semibold text-foreground">
{pagador.name}
</CardTitle>
{isAdmin ? (
<RiVerifiedBadgeFill
className="size-4 text-sky-500"
aria-hidden
/>
) : null}
<Badge
variant={getStatusBadgeVariant(pagador.status)}
className="text-xs"
>
{pagador.status}
</Badge>
{pagador.isAutoSend ? (
<Badge variant="info" className="gap-1 text-xs">
<RiMailSendLine className="size-3.5" aria-hidden />
Envio automático
</Badge>
) : null}
</div>
<CardDescription className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm">
<span>Criado em {createdAtLabel}</span>
<span className="hidden text-border/80 sm:inline"></span>
{pagador.email ? (
<Link
prefetch
href={`mailto:${pagador.email}`}
className="inline-flex items-center gap-1.5 text-primary"
>
<RiMailLine className="size-4" aria-hidden />
{pagador.email}
</Link>
) : (
<span>Sem e-mail cadastrado</span>
)}
</CardDescription>
</div>
</div>
<div className="flex w-full flex-col items-stretch gap-2 lg:w-auto lg:items-end">
{pagador.canEdit ? (
<>
<Button
type="button"
size="sm"
onClick={openConfirmDialog}
disabled={disableSend}
className="w-full min-w-[180px] lg:w-auto"
>
{isSending ? "Enviando..." : "Enviar resumo"}
</Button>
<span className="text-xs text-muted-foreground">
Último envio: {lastMailLabel}
</span>
</>
) : (
<Badge variant="outline" className="justify-center text-xs">
Acesso somente leitura
</Badge>
)}
</div>
</CardHeader>
{pagador.canEdit ? (
<Dialog
open={confirmOpen}
onOpenChange={(open) => {
if (isSending) return;
setConfirmOpen(open);
}}
>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Confirmar envio do resumo</DialogTitle>
<DialogDescription>
Resumo de{" "}
<span className="font-semibold text-foreground">
{summary.periodLabel}
</span>{" "}
para{" "}
<span className="font-medium text-foreground">
{pagador.email}
</span>
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="rounded-lg border p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex size-10 items-center justify-center rounded-full bg-primary/10">
<RiExchangeDollarLine className="size-5 text-primary" />
</div>
<div>
<p className="text-sm font-medium text-muted-foreground">
Total de Despesas
</p>
<p className="text-2xl font-bold text-foreground">
{formatCurrency(summary.totalExpenses)}
</p>
</div>
</div>
<div className="text-right">
<p className="text-sm text-muted-foreground">
{summary.lancamentoCount} lançamentos
</p>
</div>
</div>
</div>
<div className="grid gap-3 sm:grid-cols-3">
<div className="rounded-lg border p-3">
<div className="mb-2 flex items-center gap-2 text-muted-foreground">
<RiBankCard2Line className="size-4" />
<span className="text-xs font-semibold uppercase">
Cartões
</span>
</div>
<p className="text-lg font-bold text-foreground">
{formatCurrency(summary.paymentSplits.card)}
</p>
</div>
<div className="rounded-lg border p-3">
<div className="mb-2 flex items-center gap-2 text-muted-foreground">
<RiBillLine className="size-4" />
<span className="text-xs font-semibold uppercase">
Boletos
</span>
</div>
<p className="text-lg font-bold text-foreground">
{formatCurrency(summary.paymentSplits.boleto)}
</p>
</div>
<div className="rounded-lg border p-3">
<div className="mb-2 flex items-center gap-2 text-muted-foreground">
<RiExchangeDollarLine className="size-4" />
<span className="text-xs font-semibold uppercase">
Pix/Débito
</span>
</div>
<p className="text-lg font-bold text-foreground">
{formatCurrency(summary.paymentSplits.instant)}
</p>
</div>
</div>
<div className="space-y-3">
{summary.cardUsage.length > 0 && (
<div className="rounded-lg border p-3">
<div className="mb-2 flex items-center gap-2">
<RiBankCard2Line className="size-4 text-muted-foreground" />
<span className="text-xs font-semibold uppercase text-muted-foreground">
Cartões Utilizados
</span>
</div>
<div className="space-y-1">
{summary.cardUsage.map((card, index) => (
<div
key={index}
className="flex items-center justify-between text-sm"
>
<span className="text-foreground">{card.name}</span>
<span className="font-medium text-foreground">
{formatCurrency(card.amount)}
</span>
</div>
))}
</div>
</div>
)}
{(summary.boletoStats.paidCount > 0 ||
summary.boletoStats.pendingCount > 0) && (
<div className="rounded-lg border p-3">
<div className="mb-2 flex items-center gap-2">
<RiBillLine className="size-4 text-muted-foreground" />
<span className="text-xs font-semibold uppercase text-muted-foreground">
Status de Boletos
</span>
</div>
<div className="grid gap-2 sm:grid-cols-2">
<div>
<p className="text-xs text-muted-foreground">Pagos</p>
<p className="text-sm font-semibold text-success">
{formatCurrency(summary.boletoStats.paidAmount)}{" "}
<span className="text-xs font-normal">
({summary.boletoStats.paidCount})
</span>
</p>
</div>
<div>
<p className="text-xs text-muted-foreground">
Pendentes
</p>
<p className="text-sm font-semibold text-warning">
{formatCurrency(summary.boletoStats.pendingAmount)}{" "}
<span className="text-xs font-normal">
({summary.boletoStats.pendingCount})
</span>
</p>
</div>
</div>
</div>
)}
</div>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
disabled={isSending}
onClick={() => setConfirmOpen(false)}
>
Cancelar
</Button>
<Button
type="button"
onClick={handleSendSummary}
disabled={disableSend}
>
{isSending ? "Enviando..." : "Confirmar envio"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
) : null}
</Card>
);
}
const formatDate = (value: string) => {
return (
formatDateTime(value, {
day: "2-digit",
month: "long",
year: "numeric",
}) ?? "—"
);
};

View File

@@ -15,7 +15,7 @@ import {
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import { WidgetEmptyState } from "@/components/widget-empty-state";
import { WidgetEmptyState } from "@/components/shared/widget-empty-state";
import { currencyFormatter } from "@/lib/lancamentos/formatting-helpers";
import type { PagadorHistoryPoint } from "@/lib/pagadores/details";

View File

@@ -1,136 +1,26 @@
"use client";
import {
RiBankCard2Line,
RiBillLine,
RiExchangeDollarLine,
RiMailLine,
RiMailSendLine,
RiUser3Line,
RiVerifiedBadgeFill,
} from "@remixicon/react";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { type ReactNode, useMemo, useState, useTransition } from "react";
import { toast } from "sonner";
import { sendPagadorSummaryAction } from "@/app/(dashboard)/pagadores/[pagadorId]/actions";
import { RiUser3Line } from "@remixicon/react";
import type { ReactNode } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
import { getAvatarSrc } from "@/lib/pagadores/utils";
import { formatDateTime } from "@/lib/utils/date";
import { cn } from "@/lib/utils/ui";
type PagadorInfo = {
id: string;
name: string;
email: string | null;
avatarUrl: string | null;
status: string;
note: string | null;
role: string | null;
isAutoSend: boolean;
createdAt: string;
lastMailAt: string | null;
shareCode: string | null;
canEdit: boolean;
};
type PagadorSummaryPreview = {
periodLabel: string;
totalExpenses: number;
paymentSplits: {
card: number;
boleto: number;
instant: number;
};
cardUsage: { name: string; amount: number }[];
boletoStats: {
totalAmount: number;
paidAmount: number;
pendingAmount: number;
paidCount: number;
pendingCount: number;
};
lancamentoCount: number;
};
import type { PagadorInfo } from "./types";
type PagadorInfoCardProps = {
pagador: PagadorInfo;
selectedPeriod: string;
summary: PagadorSummaryPreview;
};
export function PagadorInfoCard({
pagador,
selectedPeriod,
summary,
}: PagadorInfoCardProps) {
const router = useRouter();
const [isSending, startTransition] = useTransition();
const [confirmOpen, setConfirmOpen] = useState(false);
export function PagadorInfoCard({ pagador }: PagadorInfoCardProps) {
const showSensitiveDetails = pagador.canEdit;
const avatarSrc = getAvatarSrc(pagador.avatarUrl);
const createdAtLabel = formatDate(pagador.createdAt);
const isAdmin = pagador.role === PAGADOR_ROLE_ADMIN;
const lastMailLabel = useMemo(() => {
if (!pagador.lastMailAt) {
return "Nunca enviado";
}
const date = new Date(pagador.lastMailAt);
if (Number.isNaN(date.getTime())) {
return "Nunca enviado";
}
return date.toLocaleString("pt-BR", {
dateStyle: "short",
timeStyle: "short",
});
}, [pagador.lastMailAt]);
const disableSend = isSending || !pagador.email || !pagador.canEdit;
const openConfirmDialog = () => {
if (!pagador.email) {
toast.error("Cadastre um e-mail para este pagador antes de enviar.");
return;
}
setConfirmOpen(true);
};
const handleSendSummary = () => {
if (!pagador.email) {
toast.error("Cadastre um e-mail para este pagador antes de enviar.");
return;
}
startTransition(async () => {
const result = await sendPagadorSummaryAction({
pagadorId: pagador.id,
period: selectedPeriod,
});
if (!result.success) {
toast.error(result.error);
return;
}
toast.success(result.message);
setConfirmOpen(false);
router.refresh();
});
};
const getStatusBadgeVariant = (status: string): "success" | "secondary" => {
const getStatusBadgeVariant = (status: string): "success" | "outline" => {
const normalizedStatus = status.toLowerCase();
if (normalizedStatus === "ativo") {
return "success";
@@ -140,84 +30,18 @@ export function PagadorInfoCard({
return (
<Card className="border gap-4">
<CardHeader className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div className="flex flex-1 items-start gap-4">
<div className="relative flex size-16 shrink-0 items-center justify-center overflow-hidden">
<Image
src={avatarSrc}
alt={`Avatar de ${pagador.name}`}
width={64}
height={64}
className="h-full w-full object-cover rounded-full"
/>
</div>
<div className="flex flex-1 flex-col gap-1">
<div className="flex flex-wrap items-center gap-2">
<CardTitle className="text-xl font-semibold text-foreground">
{pagador.name}
</CardTitle>
{isAdmin ? (
<RiVerifiedBadgeFill
className="size-4 text-sky-500"
aria-hidden
/>
) : null}
{pagador.isAutoSend ? (
<RiMailSendLine
className="size-4 text-primary"
aria-label="Envio automático habilitado"
/>
) : null}
</div>
<span className="text-sm text-muted-foreground">
Criado em {createdAtLabel}
</span>
</div>
</div>
<div className="flex w-full flex-col items-stretch gap-2 lg:w-auto lg:items-end">
{pagador.canEdit ? (
<>
<Button
type="button"
size="sm"
onClick={openConfirmDialog}
disabled={disableSend}
className="w-full min-w-[180px] lg:w-auto"
>
{isSending ? "Enviando..." : "Enviar resumo"}
</Button>
<span className="text-xs text-muted-foreground">
Último envio: {lastMailLabel}
</span>
</>
) : (
<span className="text-xs font-medium text-warning">
Acesso somente leitura
</span>
)}
</div>
<CardHeader className="gap-1.5">
<CardTitle className="text-lg font-semibold">
Detalhes do pagador
</CardTitle>
<CardDescription>
{showSensitiveDetails
? "Informações cadastrais e preferências de envio."
: "Informações cadastrais visíveis para este compartilhamento."}
</CardDescription>
</CardHeader>
<CardContent className="grid gap-4 border-t border-dashed border-border/60 pt-6 text-sm sm:grid-cols-2">
<InfoItem
label="E-mail"
value={
pagador.email ? (
<Link
prefetch
href={`mailto:${pagador.email}`}
className="inline-flex items-center gap-2 text-primary"
>
<RiMailLine className="size-4" aria-hidden />
{pagador.email}
</Link>
) : (
"Sem e-mail cadastrado"
)
}
/>
<InfoItem
label="Status"
value={
@@ -239,11 +63,19 @@ export function PagadorInfoCard({
</span>
}
/>
<InfoItem
label="Envio automático"
value={pagador.isAutoSend ? "Ativado" : "Desativado"}
/>
{!pagador.email ? (
{showSensitiveDetails ? (
<InfoItem
label="Envio automático"
value={pagador.isAutoSend ? "Ativado" : "Desativado"}
/>
) : null}
{showSensitiveDetails ? (
<InfoItem
label="Último envio"
value={formatDateTime(pagador.lastMailAt) ?? "Nunca enviado"}
/>
) : null}
{showSensitiveDetails && !pagador.email ? (
<InfoItem
label="Aviso"
value={
@@ -254,219 +86,29 @@ export function PagadorInfoCard({
className="sm:col-span-2"
/>
) : null}
<InfoItem
label="Observações"
value={
pagador.note ? (
<span className="text-muted-foreground">{pagador.note}</span>
) : (
"Sem observações"
)
}
className="sm:col-span-2"
/>
{showSensitiveDetails ? (
<InfoItem
label="Observações"
value={
pagador.note ? (
<span className="text-muted-foreground">{pagador.note}</span>
) : (
"Sem observações"
)
}
className="sm:col-span-2"
/>
) : null}
</CardContent>
{pagador.canEdit ? (
<Dialog
open={confirmOpen}
onOpenChange={(open) => {
if (isSending) return;
setConfirmOpen(open);
}}
>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Confirmar envio do resumo</DialogTitle>
<DialogDescription>
Resumo de{" "}
<span className="font-semibold text-foreground">
{summary.periodLabel}
</span>{" "}
para{" "}
<span className="font-medium text-foreground">
{pagador.email}
</span>
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{/* Total Geral */}
<div className="rounded-lg border p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="flex size-10 items-center justify-center rounded-full bg-primary/10">
<RiExchangeDollarLine className="size-5 text-primary" />
</div>
<div>
<p className="text-sm font-medium text-muted-foreground">
Total de Despesas
</p>
<p className="text-2xl font-bold text-foreground">
{formatCurrency(summary.totalExpenses)}
</p>
</div>
</div>
<div className="text-right">
<p className="text-sm text-muted-foreground">
{summary.lancamentoCount} lançamentos
</p>
</div>
</div>
</div>
{/* Grid de Formas de Pagamento */}
<div className="grid gap-3 sm:grid-cols-3">
{/* Cartões */}
<div className="rounded-lg border p-3">
<div className="flex items-center gap-2 text-muted-foreground mb-2">
<RiBankCard2Line className="size-4" />
<span className="text-xs font-semibold uppercase">
Cartões
</span>
</div>
<p className="text-lg font-bold text-foreground">
{formatCurrency(summary.paymentSplits.card)}
</p>
</div>
{/* Boletos */}
<div className="rounded-lg border p-3">
<div className="flex items-center gap-2 text-muted-foreground mb-2">
<RiBillLine className="size-4" />
<span className="text-xs font-semibold uppercase">
Boletos
</span>
</div>
<p className="text-lg font-bold text-foreground">
{formatCurrency(summary.paymentSplits.boleto)}
</p>
</div>
{/* Instantâneo */}
<div className="rounded-lg border p-3">
<div className="flex items-center gap-2 text-muted-foreground mb-2">
<RiExchangeDollarLine className="size-4" />
<span className="text-xs font-semibold uppercase">
Pix/Débito
</span>
</div>
<p className="text-lg font-bold text-foreground">
{formatCurrency(summary.paymentSplits.instant)}
</p>
</div>
</div>
{/* Detalhes Adicionais */}
<div className="space-y-3">
{/* Cartões Utilizados */}
{summary.cardUsage.length > 0 && (
<div className="rounded-lg border p-3">
<div className="flex items-center gap-2 mb-2">
<RiBankCard2Line className="size-4 text-muted-foreground" />
<span className="text-xs font-semibold uppercase text-muted-foreground">
Cartões Utilizados
</span>
</div>
<div className="space-y-1">
{summary.cardUsage.map((card, index) => (
<div
key={index}
className="flex items-center justify-between text-sm"
>
<span className="text-foreground">{card.name}</span>
<span className="font-medium text-foreground">
{formatCurrency(card.amount)}
</span>
</div>
))}
</div>
</div>
)}
{/* Status de Boletos */}
{(summary.boletoStats.paidCount > 0 ||
summary.boletoStats.pendingCount > 0) && (
<div className="rounded-lg border p-3">
<div className="flex items-center gap-2 mb-2">
<RiBillLine className="size-4 text-muted-foreground" />
<span className="text-xs font-semibold uppercase text-muted-foreground">
Status de Boletos
</span>
</div>
<div className="grid gap-2 sm:grid-cols-2">
<div>
<p className="text-xs text-muted-foreground">Pagos</p>
<p className="text-sm font-semibold text-success">
{formatCurrency(summary.boletoStats.paidAmount)}{" "}
<span className="text-xs font-normal">
({summary.boletoStats.paidCount})
</span>
</p>
</div>
<div>
<p className="text-xs text-muted-foreground">
Pendentes
</p>
<p className="text-sm font-semibold text-warning">
{formatCurrency(summary.boletoStats.pendingAmount)}{" "}
<span className="text-xs font-normal">
({summary.boletoStats.pendingCount})
</span>
</p>
</div>
</div>
</div>
)}
</div>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
disabled={isSending}
onClick={() => setConfirmOpen(false)}
>
Cancelar
</Button>
<Button
type="button"
onClick={handleSendSummary}
disabled={disableSend}
>
{isSending ? "Enviando..." : "Confirmar envio"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
) : null}
</Card>
);
}
const formatDate = (value: string) => {
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "—";
return date.toLocaleDateString("pt-BR", {
day: "2-digit",
month: "long",
year: "numeric",
});
};
const resolveRoleLabel = (role: string | null) => {
if (role === PAGADOR_ROLE_ADMIN) return "Administrador";
return "Pagador";
};
const formatCurrency = (value: number) =>
value.toLocaleString("pt-BR", {
style: "currency",
currency: "BRL",
maximumFractionDigits: 2,
});
type InfoItemProps = {
label: string;
value: ReactNode;

View File

@@ -7,6 +7,7 @@ import { toast } from "sonner";
import { deletePagadorShareAction } from "@/app/(dashboard)/pagadores/actions";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { formatDateTime } from "@/lib/utils/date";
interface PagadorLeaveShareCardProps {
shareId: string;
@@ -37,11 +38,12 @@ export function PagadorLeaveShareCard({
});
};
const formattedDate = new Date(createdAt).toLocaleDateString("pt-BR", {
day: "2-digit",
month: "long",
year: "numeric",
});
const formattedDate =
formatDateTime(createdAt, {
day: "2-digit",
month: "long",
year: "numeric",
}) ?? "—";
return (
<Card className="border">

View File

@@ -1,5 +1,5 @@
import type { CSSProperties } from "react";
import MoneyValues from "@/components/money-values";
import MoneyValues from "@/components/shared/money-values";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import type { PagadorMonthlyBreakdown } from "@/lib/pagadores/details";
import { cn } from "@/lib/utils/ui";

View File

@@ -5,40 +5,17 @@ import {
RiWallet3Line,
} from "@remixicon/react";
import { EstabelecimentoLogo } from "@/components/lancamentos/shared/estabelecimento-logo";
import MoneyValues from "@/components/money-values";
import MoneyValues from "@/components/shared/money-values";
import { WidgetEmptyState } from "@/components/shared/widget-empty-state";
import { CardContent } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { WidgetEmptyState } from "@/components/widget-empty-state";
import { buildBillStatusLabel } from "@/lib/dashboard/bills-helpers";
import type {
PagadorBoletoItem,
PagadorPaymentStatusData,
} from "@/lib/pagadores/details";
import { cn } from "@/lib/utils/ui";
// --- Boleto helpers ---
const DATE_FORMATTER = new Intl.DateTimeFormat("pt-BR", {
day: "2-digit",
month: "short",
year: "numeric",
timeZone: "UTC",
});
const buildDateLabel = (value: string | null, prefix?: string) => {
if (!value) return null;
const [year, month, day] = value.split("-").map((part) => Number(part));
if (!year || !month || !day) return null;
const formatted = DATE_FORMATTER.format(
new Date(Date.UTC(year, month - 1, day)),
);
return prefix ? `${prefix} ${formatted}` : formatted;
};
const buildStatusLabel = (item: PagadorBoletoItem) => {
if (item.isSettled) return buildDateLabel(item.boletoPaymentDate, "Pago em");
return buildDateLabel(item.dueDate, "Vence em");
};
// --- PagadorBoletoCard ---
type PagadorBoletoCardProps = {
@@ -62,7 +39,7 @@ export function PagadorBoletoCard({ items }: PagadorBoletoCardProps) {
<CardContent className="flex flex-col gap-4 px-0">
<ul className="flex flex-col">
{items.map((item) => {
const statusLabel = buildStatusLabel(item);
const statusLabel = buildBillStatusLabel(item);
return (
<li
key={item.id}

View File

@@ -79,7 +79,7 @@ export function PagadorSharingCard({
return (
<Card className="border">
<CardHeader>
<CardTitle className="text-base font-semibold">
<CardTitle className="text-lg font-semibold">
Compartilhamentos
</CardTitle>
<p className="text-sm text-muted-foreground">

View File

@@ -0,0 +1,33 @@
export type PagadorInfo = {
id: string;
name: string;
email: string | null;
avatarUrl: string | null;
status: string;
note: string | null;
role: string | null;
isAutoSend: boolean;
createdAt: string;
lastMailAt: string | null;
shareCode: string | null;
canEdit: boolean;
};
export type PagadorSummaryPreview = {
periodLabel: string;
totalExpenses: number;
paymentSplits: {
card: number;
boleto: number;
instant: number;
};
cardUsage: { name: string; amount: number }[];
boletoStats: {
totalAmount: number;
paidAmount: number;
pendingAmount: number;
paidCount: number;
pendingCount: number;
};
lancamentoCount: number;
};