mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 11:01:45 +00:00
feat: aprimora o fluxo de pagamento de faturas e boletos
This commit is contained in:
@@ -60,6 +60,7 @@
|
||||
"@vercel/speed-insights": "^1.3.1",
|
||||
"ai": "^6.0.124",
|
||||
"better-auth": "1.5.4",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
@@ -85,6 +86,7 @@
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.4.6",
|
||||
"@tailwindcss/postcss": "4.2.1",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@types/node": "25.4.0",
|
||||
"@types/pg": "^8.18.0",
|
||||
"@types/react": "19.2.14",
|
||||
|
||||
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@@ -107,6 +107,9 @@ importers:
|
||||
better-auth:
|
||||
specifier: 1.5.4
|
||||
version: 1.5.4(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(drizzle-kit@0.31.9)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@7.4.2(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.18.0)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.20.0)(postgres@3.4.7)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)))(mongodb@7.1.0)(mysql2@3.15.3)(next@16.1.6(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(pg@8.20.0)(prisma@7.4.2(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
canvas-confetti:
|
||||
specifier: ^1.9.4
|
||||
version: 1.9.4
|
||||
class-variance-authority:
|
||||
specifier: 0.7.1
|
||||
version: 0.7.1
|
||||
@@ -177,6 +180,9 @@ importers:
|
||||
'@tailwindcss/postcss':
|
||||
specifier: 4.2.1
|
||||
version: 4.2.1
|
||||
'@types/canvas-confetti':
|
||||
specifier: ^1.9.0
|
||||
version: 1.9.0
|
||||
'@types/node':
|
||||
specifier: 25.4.0
|
||||
version: 25.4.0
|
||||
@@ -2178,6 +2184,9 @@ packages:
|
||||
resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@types/canvas-confetti@1.9.0':
|
||||
resolution: {integrity: sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==}
|
||||
|
||||
'@types/d3-array@3.2.2':
|
||||
resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
|
||||
|
||||
@@ -2412,6 +2421,9 @@ packages:
|
||||
caniuse-lite@1.0.30001777:
|
||||
resolution: {integrity: sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==}
|
||||
|
||||
canvas-confetti@1.9.4:
|
||||
resolution: {integrity: sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==}
|
||||
|
||||
canvg@3.0.11:
|
||||
resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
@@ -5217,6 +5229,8 @@ snapshots:
|
||||
|
||||
'@tanstack/table-core@8.21.3': {}
|
||||
|
||||
'@types/canvas-confetti@1.9.0': {}
|
||||
|
||||
'@types/d3-array@3.2.2': {}
|
||||
|
||||
'@types/d3-color@3.1.3': {}
|
||||
@@ -5382,6 +5396,8 @@ snapshots:
|
||||
|
||||
caniuse-lite@1.0.30001777: {}
|
||||
|
||||
canvas-confetti@1.9.4: {}
|
||||
|
||||
canvg@3.0.11:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
|
||||
@@ -19,8 +19,8 @@ export function BillListItem({ bill, onPay }: BillListItemProps) {
|
||||
const overdue = isBillOverdue(bill);
|
||||
|
||||
return (
|
||||
<li className="flex items-center justify-between border-b border-dashed last:border-b-0 last:pb-0">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2 py-2">
|
||||
<li 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">
|
||||
<EstabelecimentoLogo name={bill.name} size={37} />
|
||||
|
||||
<div className="min-w-0">
|
||||
@@ -54,7 +54,7 @@ export function BillListItem({ bill, onPay }: BillListItemProps) {
|
||||
>
|
||||
{bill.isSettled ? (
|
||||
<span className="flex items-center gap-1 text-success">
|
||||
<RiCheckboxCircleFill className="size-3" /> Pago
|
||||
<RiCheckboxCircleFill className="size-4" /> Pago
|
||||
</span>
|
||||
) : overdue ? (
|
||||
<span className="overdue-blink">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
RiBarcodeFill,
|
||||
RiCheckboxCircleLine,
|
||||
RiCalendarLine,
|
||||
RiLoader4Line,
|
||||
RiMoneyDollarCircleLine,
|
||||
} from "@remixicon/react";
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "@/features/dashboard/bills-helpers";
|
||||
import type { DashboardBill } from "@/features/dashboard/bills-queries";
|
||||
import MoneyValues from "@/shared/components/money-values";
|
||||
import { PaymentSuccess } from "@/shared/components/payment-success";
|
||||
import { Badge } from "@/shared/components/ui/badge";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
import {
|
||||
@@ -68,68 +69,46 @@ export function BillPaymentDialog({
|
||||
}}
|
||||
>
|
||||
{modalState === "success" ? (
|
||||
<div className="flex flex-col items-center gap-4 py-6 text-center">
|
||||
<div className="flex size-16 items-center justify-center rounded-full bg-success/10 text-success">
|
||||
<RiCheckboxCircleLine className="size-8" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<DialogTitle className="text-base">
|
||||
Pagamento registrado!
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-sm">
|
||||
Atualizamos o status do boleto para pago. Em instantes ele
|
||||
aparecerá como baixado no histórico.
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<DialogFooter className="sm:justify-center">
|
||||
<Button type="button" onClick={onClose} className="sm:w-auto">
|
||||
Fechar
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
<PaymentSuccess
|
||||
title="Pagamento registrado!"
|
||||
description="Atualizamos o status do boleto para pago. Em instantes ele aparecerá como baixado no histórico."
|
||||
onClose={onClose}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Confirmar pagamento do boleto</DialogTitle>
|
||||
<DialogDescription>
|
||||
Confirme os dados para registrar o pagamento. Você poderá editar
|
||||
o lançamento depois, se necessário.
|
||||
</DialogDescription>
|
||||
<div className="mb-1 flex items-center gap-3">
|
||||
<div className="flex size-10 items-center justify-center rounded-full bg-primary/10">
|
||||
<RiBarcodeFill className="size-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<DialogTitle>Confirmar pagamento</DialogTitle>
|
||||
<DialogDescription className="mt-0.5 text-xs">
|
||||
Boleto
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
{bill ? (
|
||||
<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">
|
||||
<RiBarcodeFill className="size-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
Boleto
|
||||
</p>
|
||||
<p className="text-lg font-bold text-foreground">
|
||||
{bill.name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{dueLabel ? (
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{dueLabel}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{/* Card principal */}
|
||||
<div className="rounded-xl border bg-muted/30 p-4">
|
||||
<p className="mb-1 text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
||||
Boleto
|
||||
</p>
|
||||
<p className="text-base font-semibold text-foreground">
|
||||
{bill.name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="rounded-lg border p-3">
|
||||
<div className="mb-2 flex items-center gap-2 text-muted-foreground">
|
||||
<RiMoneyDollarCircleLine className="size-4" />
|
||||
<span className="text-xs font-semibold uppercase">
|
||||
Valor do Boleto
|
||||
{/* Métricas */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="rounded-xl border p-3">
|
||||
<div className="mb-1.5 flex items-center gap-1.5 text-muted-foreground">
|
||||
<RiMoneyDollarCircleLine className="size-3.5" />
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wide">
|
||||
Valor
|
||||
</span>
|
||||
</div>
|
||||
<MoneyValues
|
||||
@@ -137,22 +116,38 @@ export function BillPaymentDialog({
|
||||
className="text-lg font-bold"
|
||||
/>
|
||||
</div>
|
||||
<div className="rounded-lg border p-3">
|
||||
<div className="mb-2 flex items-center gap-2 text-muted-foreground">
|
||||
<RiCheckboxCircleLine className="size-4" />
|
||||
<span className="text-xs font-semibold uppercase">
|
||||
Status
|
||||
|
||||
<div className="rounded-xl border p-3">
|
||||
<div className="mb-1.5 flex items-center gap-1.5 text-muted-foreground">
|
||||
<RiCalendarLine className="size-3.5" />
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wide">
|
||||
Vencimento
|
||||
</span>
|
||||
</div>
|
||||
<Badge
|
||||
variant={getBillStatusBadgeVariant(
|
||||
bill.isSettled ? "Pago" : "Pendente",
|
||||
)}
|
||||
>
|
||||
{bill.isSettled ? "Pago" : "Pendente"}
|
||||
</Badge>
|
||||
<p className="text-sm font-semibold text-foreground">
|
||||
{dueLabel?.replace("Vencimento: ", "") ?? "—"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div className="flex items-center justify-between rounded-xl border p-3">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Status atual
|
||||
</span>
|
||||
<Badge
|
||||
variant={getBillStatusBadgeVariant(
|
||||
bill.isSettled ? "Pago" : "Pendente",
|
||||
)}
|
||||
>
|
||||
{bill.isSettled ? "Pago" : "Pendente"}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Aviso */}
|
||||
<p className="px-1 text-xs text-muted-foreground">
|
||||
Você poderá editar o lançamento depois, se necessário.
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -169,7 +164,6 @@ export function BillPaymentDialog({
|
||||
type="button"
|
||||
onClick={onConfirm}
|
||||
disabled={isProcessing || !bill || bill.isSettled}
|
||||
className="relative"
|
||||
>
|
||||
{isProcessing ? (
|
||||
<>
|
||||
|
||||
@@ -55,8 +55,8 @@ export function InvoiceListItem({ invoice, onPay }: InvoiceListItemProps) {
|
||||
);
|
||||
|
||||
return (
|
||||
<li className="flex items-center justify-between border-b border-dashed last:border-b-0 last:pb-0">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2 py-2">
|
||||
<div 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">
|
||||
<InvoiceLogo
|
||||
cardName={invoice.cardName}
|
||||
logo={invoice.logo}
|
||||
@@ -133,7 +133,7 @@ export function InvoiceListItem({ invoice, onPay }: InvoiceListItemProps) {
|
||||
>
|
||||
{isPaid ? (
|
||||
<span className="flex items-center gap-1 text-success">
|
||||
<RiCheckboxCircleFill className="size-3" /> Pago
|
||||
<RiCheckboxCircleFill className="size-4" /> Pago
|
||||
</span>
|
||||
) : isOverdue ? (
|
||||
<span className="overdue-blink">
|
||||
@@ -147,6 +147,6 @@ export function InvoiceListItem({ invoice, onPay }: InvoiceListItemProps) {
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
RiCheckboxCircleLine,
|
||||
RiBankCardLine,
|
||||
RiCalendarLine,
|
||||
RiLoader4Line,
|
||||
RiMoneyDollarCircleLine,
|
||||
} from "@remixicon/react";
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
} from "@/features/dashboard/invoices-helpers";
|
||||
import type { DashboardInvoice } from "@/features/dashboard/invoices-queries";
|
||||
import MoneyValues from "@/shared/components/money-values";
|
||||
import { PaymentSuccess } from "@/shared/components/payment-success";
|
||||
import { Badge } from "@/shared/components/ui/badge";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
import {
|
||||
@@ -46,6 +48,9 @@ export function InvoicePaymentDialog({
|
||||
}: InvoicePaymentDialogProps) {
|
||||
const isProcessing = modalState === "processing" || isPending;
|
||||
const paymentInfo = invoice ? formatInvoicePaymentDate(invoice.paidAt) : null;
|
||||
const dueInfo = invoice
|
||||
? parseInvoiceDueDate(invoice.period, invoice.dueDay)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
@@ -71,82 +76,56 @@ export function InvoicePaymentDialog({
|
||||
}}
|
||||
>
|
||||
{modalState === "success" ? (
|
||||
<div className="flex flex-col items-center gap-4 py-6 text-center">
|
||||
<div className="flex size-16 items-center justify-center rounded-full bg-success/10 text-success">
|
||||
<RiCheckboxCircleLine className="size-8" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<DialogTitle className="text-base">
|
||||
Pagamento confirmado!
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-sm">
|
||||
Atualizamos o status da fatura. O lançamento do pagamento
|
||||
aparecerá no extrato em instantes.
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<DialogFooter className="sm:justify-center">
|
||||
<Button type="button" onClick={onClose} className="sm:w-auto">
|
||||
Fechar
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
<PaymentSuccess
|
||||
title="Pagamento confirmado!"
|
||||
description="Atualizamos o status da fatura. O lançamento do pagamento aparecerá no extrato em instantes."
|
||||
onClose={onClose}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Confirmar pagamento</DialogTitle>
|
||||
<DialogDescription>
|
||||
Revise os dados antes de confirmar. Vamos registrar a fatura
|
||||
como paga.
|
||||
</DialogDescription>
|
||||
<div className="mb-1 flex items-center gap-3">
|
||||
<div className="flex size-10 items-center justify-center rounded-full bg-primary/10">
|
||||
<RiBankCardLine className="size-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<DialogTitle>Confirmar pagamento</DialogTitle>
|
||||
<DialogDescription className="mt-0.5 text-xs">
|
||||
Fatura do cartão
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
{invoice ? (
|
||||
<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">
|
||||
<InvoiceLogo
|
||||
cardName={invoice.cardName}
|
||||
logo={invoice.logo}
|
||||
size={40}
|
||||
tone="accent"
|
||||
containerClassName="size-10"
|
||||
fallbackClassName="text-xs"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
Cartão
|
||||
</p>
|
||||
<p className="text-lg font-bold text-foreground">
|
||||
{invoice.cardName}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
{invoice.paymentStatus !== INVOICE_PAYMENT_STATUS.PAID ? (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{
|
||||
parseInvoiceDueDate(invoice.period, invoice.dueDay)
|
||||
.label
|
||||
}
|
||||
</p>
|
||||
) : null}
|
||||
{invoice.paymentStatus === INVOICE_PAYMENT_STATUS.PAID &&
|
||||
paymentInfo ? (
|
||||
<p className="text-sm text-success">
|
||||
{paymentInfo.label}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{/* Card principal */}
|
||||
<div className="flex items-center gap-3 rounded-xl border bg-muted/30 p-4">
|
||||
<InvoiceLogo
|
||||
cardName={invoice.cardName}
|
||||
logo={invoice.logo}
|
||||
size={36}
|
||||
tone="accent"
|
||||
containerClassName="size-9 shrink-0"
|
||||
fallbackClassName="text-xs"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="text-[11px] font-medium text-muted-foreground uppercase tracking-wide">
|
||||
Cartão
|
||||
</p>
|
||||
<p className="truncate text-base font-semibold text-foreground">
|
||||
{invoice.cardName}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="rounded-lg border p-3">
|
||||
<div className="mb-2 flex items-center gap-2 text-muted-foreground">
|
||||
<RiMoneyDollarCircleLine className="size-4" />
|
||||
<span className="text-xs font-semibold uppercase">
|
||||
Valor da Invoice
|
||||
{/* Métricas */}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="rounded-xl border p-3">
|
||||
<div className="mb-1.5 flex items-center gap-1.5 text-muted-foreground">
|
||||
<RiMoneyDollarCircleLine className="size-3.5" />
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wide">
|
||||
Total da fatura
|
||||
</span>
|
||||
</div>
|
||||
<MoneyValues
|
||||
@@ -154,22 +133,43 @@ export function InvoicePaymentDialog({
|
||||
className="text-lg font-bold"
|
||||
/>
|
||||
</div>
|
||||
<div className="rounded-lg border p-3">
|
||||
<div className="mb-2 flex items-center gap-2 text-muted-foreground">
|
||||
<RiCheckboxCircleLine className="size-4" />
|
||||
<span className="text-xs font-semibold uppercase">
|
||||
Status
|
||||
|
||||
<div className="rounded-xl border p-3">
|
||||
<div className="mb-1.5 flex items-center gap-1.5 text-muted-foreground">
|
||||
<RiCalendarLine className="size-3.5" />
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wide">
|
||||
{invoice.paymentStatus === INVOICE_PAYMENT_STATUS.PAID
|
||||
? "Pago em"
|
||||
: "Vencimento"}
|
||||
</span>
|
||||
</div>
|
||||
<Badge
|
||||
variant={getInvoiceStatusBadgeVariant(
|
||||
INVOICE_STATUS_LABEL[invoice.paymentStatus],
|
||||
)}
|
||||
>
|
||||
{INVOICE_STATUS_LABEL[invoice.paymentStatus]}
|
||||
</Badge>
|
||||
<p className="text-sm font-semibold text-foreground">
|
||||
{invoice.paymentStatus === INVOICE_PAYMENT_STATUS.PAID
|
||||
? (paymentInfo?.label ?? "—")
|
||||
: (dueInfo?.label ?? "—")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div className="flex items-center justify-between rounded-xl border p-3">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Status atual
|
||||
</span>
|
||||
<Badge
|
||||
variant={getInvoiceStatusBadgeVariant(
|
||||
INVOICE_STATUS_LABEL[invoice.paymentStatus],
|
||||
)}
|
||||
>
|
||||
{INVOICE_STATUS_LABEL[invoice.paymentStatus]}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Aviso */}
|
||||
<p className="px-1 text-xs text-muted-foreground">
|
||||
Vamos registrar a fatura como paga. Você poderá editar depois
|
||||
se necessário.
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -186,7 +186,6 @@ export function InvoicePaymentDialog({
|
||||
type="button"
|
||||
onClick={onConfirm}
|
||||
disabled={isProcessing || !invoice}
|
||||
className="relative"
|
||||
>
|
||||
{isProcessing ? (
|
||||
<>
|
||||
|
||||
@@ -47,7 +47,7 @@ type ActionResult =
|
||||
| { success: false; error: string };
|
||||
|
||||
const successMessageByStatus: Record<InvoicePaymentStatus, string> = {
|
||||
[INVOICE_PAYMENT_STATUS.PAID]: "Invoice marcada como paga.",
|
||||
[INVOICE_PAYMENT_STATUS.PAID]: "Fatura marcada como paga.",
|
||||
[INVOICE_PAYMENT_STATUS.PENDING]: "Pagamento da fatura foi revertido.",
|
||||
};
|
||||
|
||||
|
||||
109
src/shared/components/payment-success.tsx
Normal file
109
src/shared/components/payment-success.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
"use client";
|
||||
|
||||
import { RiCheckboxCircleFill } from "@remixicon/react";
|
||||
import confetti from "canvas-confetti";
|
||||
import { useEffect } from "react";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
import {
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
} from "@/shared/components/ui/dialog";
|
||||
|
||||
// Tons baseados na primary: oklch(72% 0.163 50) ≈ laranja quente
|
||||
const PRIMARY_CONFETTI_COLORS = [
|
||||
"#e07a3a", // primary base
|
||||
"#f5a870", // primary claro
|
||||
"#ffd4a8", // primary muito claro
|
||||
"#b85520", // primary escuro
|
||||
"#8a3a10", // primary muito escuro
|
||||
"#f5c896", // tom pastel
|
||||
];
|
||||
|
||||
type PaymentSuccessProps = {
|
||||
title: string;
|
||||
description: string;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export function PaymentSuccess({
|
||||
title,
|
||||
description,
|
||||
onClose,
|
||||
}: PaymentSuccessProps) {
|
||||
useEffect(() => {
|
||||
const origin = { x: 0.5, y: 0.4 };
|
||||
|
||||
confetti({
|
||||
particleCount: 80,
|
||||
spread: 70,
|
||||
origin,
|
||||
colors: PRIMARY_CONFETTI_COLORS,
|
||||
startVelocity: 28,
|
||||
gravity: 1.2,
|
||||
scalar: 0.9,
|
||||
ticks: 200,
|
||||
});
|
||||
|
||||
const t1 = setTimeout(() => {
|
||||
confetti({
|
||||
particleCount: 40,
|
||||
spread: 50,
|
||||
origin: { x: 0.3, y: 0.45 },
|
||||
colors: PRIMARY_CONFETTI_COLORS,
|
||||
startVelocity: 22,
|
||||
gravity: 1.1,
|
||||
scalar: 0.8,
|
||||
ticks: 180,
|
||||
});
|
||||
}, 150);
|
||||
|
||||
const t2 = setTimeout(() => {
|
||||
confetti({
|
||||
particleCount: 40,
|
||||
spread: 50,
|
||||
origin: { x: 0.7, y: 0.45 },
|
||||
colors: PRIMARY_CONFETTI_COLORS,
|
||||
startVelocity: 22,
|
||||
gravity: 1.1,
|
||||
scalar: 0.8,
|
||||
ticks: 180,
|
||||
});
|
||||
}, 250);
|
||||
|
||||
return () => {
|
||||
clearTimeout(t1);
|
||||
clearTimeout(t2);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center gap-6 px-2 py-8 text-center">
|
||||
<div className="relative flex items-center justify-center">
|
||||
{/* Anel de pulso */}
|
||||
<span className="absolute inline-flex size-24 animate-ping rounded-full bg-primary opacity-10" />
|
||||
<span className="absolute inline-flex size-20 rounded-full bg-primary/15" />
|
||||
<div className="relative flex size-16 items-center justify-center rounded-full bg-primary shadow-lg shadow-primary/30">
|
||||
<RiCheckboxCircleFill className="size-8 text-primary-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<DialogTitle className="text-xl font-bold">{title}</DialogTitle>
|
||||
<DialogDescription className="text-sm leading-relaxed">
|
||||
{description}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="w-full sm:justify-center">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="w-full sm:w-auto sm:min-w-32"
|
||||
>
|
||||
Fechar
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user