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",
|
"@vercel/speed-insights": "^1.3.1",
|
||||||
"ai": "^6.0.124",
|
"ai": "^6.0.124",
|
||||||
"better-auth": "1.5.4",
|
"better-auth": "1.5.4",
|
||||||
|
"canvas-confetti": "^1.9.4",
|
||||||
"class-variance-authority": "0.7.1",
|
"class-variance-authority": "0.7.1",
|
||||||
"clsx": "2.1.1",
|
"clsx": "2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
@@ -85,6 +86,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.4.6",
|
"@biomejs/biome": "2.4.6",
|
||||||
"@tailwindcss/postcss": "4.2.1",
|
"@tailwindcss/postcss": "4.2.1",
|
||||||
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"@types/node": "25.4.0",
|
"@types/node": "25.4.0",
|
||||||
"@types/pg": "^8.18.0",
|
"@types/pg": "^8.18.0",
|
||||||
"@types/react": "19.2.14",
|
"@types/react": "19.2.14",
|
||||||
|
|||||||
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@@ -107,6 +107,9 @@ importers:
|
|||||||
better-auth:
|
better-auth:
|
||||||
specifier: 1.5.4
|
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)
|
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:
|
class-variance-authority:
|
||||||
specifier: 0.7.1
|
specifier: 0.7.1
|
||||||
version: 0.7.1
|
version: 0.7.1
|
||||||
@@ -177,6 +180,9 @@ importers:
|
|||||||
'@tailwindcss/postcss':
|
'@tailwindcss/postcss':
|
||||||
specifier: 4.2.1
|
specifier: 4.2.1
|
||||||
version: 4.2.1
|
version: 4.2.1
|
||||||
|
'@types/canvas-confetti':
|
||||||
|
specifier: ^1.9.0
|
||||||
|
version: 1.9.0
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: 25.4.0
|
specifier: 25.4.0
|
||||||
version: 25.4.0
|
version: 25.4.0
|
||||||
@@ -2178,6 +2184,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
|
resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
'@types/canvas-confetti@1.9.0':
|
||||||
|
resolution: {integrity: sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==}
|
||||||
|
|
||||||
'@types/d3-array@3.2.2':
|
'@types/d3-array@3.2.2':
|
||||||
resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
|
resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
|
||||||
|
|
||||||
@@ -2412,6 +2421,9 @@ packages:
|
|||||||
caniuse-lite@1.0.30001777:
|
caniuse-lite@1.0.30001777:
|
||||||
resolution: {integrity: sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==}
|
resolution: {integrity: sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==}
|
||||||
|
|
||||||
|
canvas-confetti@1.9.4:
|
||||||
|
resolution: {integrity: sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==}
|
||||||
|
|
||||||
canvg@3.0.11:
|
canvg@3.0.11:
|
||||||
resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==}
|
resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
@@ -5217,6 +5229,8 @@ snapshots:
|
|||||||
|
|
||||||
'@tanstack/table-core@8.21.3': {}
|
'@tanstack/table-core@8.21.3': {}
|
||||||
|
|
||||||
|
'@types/canvas-confetti@1.9.0': {}
|
||||||
|
|
||||||
'@types/d3-array@3.2.2': {}
|
'@types/d3-array@3.2.2': {}
|
||||||
|
|
||||||
'@types/d3-color@3.1.3': {}
|
'@types/d3-color@3.1.3': {}
|
||||||
@@ -5382,6 +5396,8 @@ snapshots:
|
|||||||
|
|
||||||
caniuse-lite@1.0.30001777: {}
|
caniuse-lite@1.0.30001777: {}
|
||||||
|
|
||||||
|
canvas-confetti@1.9.4: {}
|
||||||
|
|
||||||
canvg@3.0.11:
|
canvg@3.0.11:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.28.6
|
'@babel/runtime': 7.28.6
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ export function BillListItem({ bill, onPay }: BillListItemProps) {
|
|||||||
const overdue = isBillOverdue(bill);
|
const overdue = isBillOverdue(bill);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className="flex items-center justify-between border-b border-dashed last:border-b-0 last:pb-0">
|
<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-2">
|
<div className="flex min-w-0 flex-1 items-center gap-2 py-1">
|
||||||
<EstabelecimentoLogo name={bill.name} size={37} />
|
<EstabelecimentoLogo name={bill.name} size={37} />
|
||||||
|
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
@@ -54,7 +54,7 @@ export function BillListItem({ bill, onPay }: BillListItemProps) {
|
|||||||
>
|
>
|
||||||
{bill.isSettled ? (
|
{bill.isSettled ? (
|
||||||
<span className="flex items-center gap-1 text-success">
|
<span className="flex items-center gap-1 text-success">
|
||||||
<RiCheckboxCircleFill className="size-3" /> Pago
|
<RiCheckboxCircleFill className="size-4" /> Pago
|
||||||
</span>
|
</span>
|
||||||
) : overdue ? (
|
) : overdue ? (
|
||||||
<span className="overdue-blink">
|
<span className="overdue-blink">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
RiBarcodeFill,
|
RiBarcodeFill,
|
||||||
RiCheckboxCircleLine,
|
RiCalendarLine,
|
||||||
RiLoader4Line,
|
RiLoader4Line,
|
||||||
RiMoneyDollarCircleLine,
|
RiMoneyDollarCircleLine,
|
||||||
} from "@remixicon/react";
|
} from "@remixicon/react";
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from "@/features/dashboard/bills-helpers";
|
} from "@/features/dashboard/bills-helpers";
|
||||||
import type { DashboardBill } from "@/features/dashboard/bills-queries";
|
import type { DashboardBill } from "@/features/dashboard/bills-queries";
|
||||||
import MoneyValues from "@/shared/components/money-values";
|
import MoneyValues from "@/shared/components/money-values";
|
||||||
|
import { PaymentSuccess } from "@/shared/components/payment-success";
|
||||||
import { Badge } from "@/shared/components/ui/badge";
|
import { Badge } from "@/shared/components/ui/badge";
|
||||||
import { Button } from "@/shared/components/ui/button";
|
import { Button } from "@/shared/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -68,68 +69,46 @@ export function BillPaymentDialog({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{modalState === "success" ? (
|
{modalState === "success" ? (
|
||||||
<div className="flex flex-col items-center gap-4 py-6 text-center">
|
<PaymentSuccess
|
||||||
<div className="flex size-16 items-center justify-center rounded-full bg-success/10 text-success">
|
title="Pagamento registrado!"
|
||||||
<RiCheckboxCircleLine className="size-8" />
|
description="Atualizamos o status do boleto para pago. Em instantes ele aparecerá como baixado no histórico."
|
||||||
</div>
|
onClose={onClose}
|
||||||
<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>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Confirmar pagamento do boleto</DialogTitle>
|
<div className="mb-1 flex items-center gap-3">
|
||||||
<DialogDescription>
|
<div className="flex size-10 items-center justify-center rounded-full bg-primary/10">
|
||||||
Confirme os dados para registrar o pagamento. Você poderá editar
|
<RiBarcodeFill className="size-5 text-primary" />
|
||||||
o lançamento depois, se necessário.
|
</div>
|
||||||
</DialogDescription>
|
<div>
|
||||||
|
<DialogTitle>Confirmar pagamento</DialogTitle>
|
||||||
|
<DialogDescription className="mt-0.5 text-xs">
|
||||||
|
Boleto
|
||||||
|
</DialogDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{bill ? (
|
{bill ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-3">
|
||||||
<div className="rounded-lg border p-4">
|
{/* Card principal */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="rounded-xl border bg-muted/30 p-4">
|
||||||
<div className="flex items-center gap-3">
|
<p className="mb-1 text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
||||||
<div className="flex size-10 items-center justify-center rounded-full bg-primary/10">
|
Boleto
|
||||||
<RiBarcodeFill className="size-5 text-primary" />
|
</p>
|
||||||
</div>
|
<p className="text-base font-semibold text-foreground">
|
||||||
<div>
|
{bill.name}
|
||||||
<p className="text-sm font-medium text-muted-foreground">
|
</p>
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2">
|
{/* Métricas */}
|
||||||
<div className="rounded-lg border p-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="mb-2 flex items-center gap-2 text-muted-foreground">
|
<div className="rounded-xl border p-3">
|
||||||
<RiMoneyDollarCircleLine className="size-4" />
|
<div className="mb-1.5 flex items-center gap-1.5 text-muted-foreground">
|
||||||
<span className="text-xs font-semibold uppercase">
|
<RiMoneyDollarCircleLine className="size-3.5" />
|
||||||
Valor do Boleto
|
<span className="text-[11px] font-semibold uppercase tracking-wide">
|
||||||
|
Valor
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<MoneyValues
|
<MoneyValues
|
||||||
@@ -137,22 +116,38 @@ export function BillPaymentDialog({
|
|||||||
className="text-lg font-bold"
|
className="text-lg font-bold"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg border p-3">
|
|
||||||
<div className="mb-2 flex items-center gap-2 text-muted-foreground">
|
<div className="rounded-xl border p-3">
|
||||||
<RiCheckboxCircleLine className="size-4" />
|
<div className="mb-1.5 flex items-center gap-1.5 text-muted-foreground">
|
||||||
<span className="text-xs font-semibold uppercase">
|
<RiCalendarLine className="size-3.5" />
|
||||||
Status
|
<span className="text-[11px] font-semibold uppercase tracking-wide">
|
||||||
|
Vencimento
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Badge
|
<p className="text-sm font-semibold text-foreground">
|
||||||
variant={getBillStatusBadgeVariant(
|
{dueLabel?.replace("Vencimento: ", "") ?? "—"}
|
||||||
bill.isSettled ? "Pago" : "Pendente",
|
</p>
|
||||||
)}
|
|
||||||
>
|
|
||||||
{bill.isSettled ? "Pago" : "Pendente"}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -169,7 +164,6 @@ export function BillPaymentDialog({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onConfirm}
|
onClick={onConfirm}
|
||||||
disabled={isProcessing || !bill || bill.isSettled}
|
disabled={isProcessing || !bill || bill.isSettled}
|
||||||
className="relative"
|
|
||||||
>
|
>
|
||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -55,8 +55,8 @@ export function InvoiceListItem({ invoice, onPay }: InvoiceListItemProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className="flex items-center justify-between border-b border-dashed last:border-b-0 last:pb-0">
|
<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-2">
|
<div className="flex min-w-0 flex-1 items-center gap-2 py-1">
|
||||||
<InvoiceLogo
|
<InvoiceLogo
|
||||||
cardName={invoice.cardName}
|
cardName={invoice.cardName}
|
||||||
logo={invoice.logo}
|
logo={invoice.logo}
|
||||||
@@ -133,7 +133,7 @@ export function InvoiceListItem({ invoice, onPay }: InvoiceListItemProps) {
|
|||||||
>
|
>
|
||||||
{isPaid ? (
|
{isPaid ? (
|
||||||
<span className="flex items-center gap-1 text-success">
|
<span className="flex items-center gap-1 text-success">
|
||||||
<RiCheckboxCircleFill className="size-3" /> Pago
|
<RiCheckboxCircleFill className="size-4" /> Pago
|
||||||
</span>
|
</span>
|
||||||
) : isOverdue ? (
|
) : isOverdue ? (
|
||||||
<span className="overdue-blink">
|
<span className="overdue-blink">
|
||||||
@@ -147,6 +147,6 @@ export function InvoiceListItem({ invoice, onPay }: InvoiceListItemProps) {
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
RiCheckboxCircleLine,
|
RiBankCardLine,
|
||||||
|
RiCalendarLine,
|
||||||
RiLoader4Line,
|
RiLoader4Line,
|
||||||
RiMoneyDollarCircleLine,
|
RiMoneyDollarCircleLine,
|
||||||
} from "@remixicon/react";
|
} from "@remixicon/react";
|
||||||
@@ -11,6 +12,7 @@ import {
|
|||||||
} from "@/features/dashboard/invoices-helpers";
|
} from "@/features/dashboard/invoices-helpers";
|
||||||
import type { DashboardInvoice } from "@/features/dashboard/invoices-queries";
|
import type { DashboardInvoice } from "@/features/dashboard/invoices-queries";
|
||||||
import MoneyValues from "@/shared/components/money-values";
|
import MoneyValues from "@/shared/components/money-values";
|
||||||
|
import { PaymentSuccess } from "@/shared/components/payment-success";
|
||||||
import { Badge } from "@/shared/components/ui/badge";
|
import { Badge } from "@/shared/components/ui/badge";
|
||||||
import { Button } from "@/shared/components/ui/button";
|
import { Button } from "@/shared/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -46,6 +48,9 @@ export function InvoicePaymentDialog({
|
|||||||
}: InvoicePaymentDialogProps) {
|
}: InvoicePaymentDialogProps) {
|
||||||
const isProcessing = modalState === "processing" || isPending;
|
const isProcessing = modalState === "processing" || isPending;
|
||||||
const paymentInfo = invoice ? formatInvoicePaymentDate(invoice.paidAt) : null;
|
const paymentInfo = invoice ? formatInvoicePaymentDate(invoice.paidAt) : null;
|
||||||
|
const dueInfo = invoice
|
||||||
|
? parseInvoiceDueDate(invoice.period, invoice.dueDay)
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -71,82 +76,56 @@ export function InvoicePaymentDialog({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{modalState === "success" ? (
|
{modalState === "success" ? (
|
||||||
<div className="flex flex-col items-center gap-4 py-6 text-center">
|
<PaymentSuccess
|
||||||
<div className="flex size-16 items-center justify-center rounded-full bg-success/10 text-success">
|
title="Pagamento confirmado!"
|
||||||
<RiCheckboxCircleLine className="size-8" />
|
description="Atualizamos o status da fatura. O lançamento do pagamento aparecerá no extrato em instantes."
|
||||||
</div>
|
onClose={onClose}
|
||||||
<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>
|
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Confirmar pagamento</DialogTitle>
|
<div className="mb-1 flex items-center gap-3">
|
||||||
<DialogDescription>
|
<div className="flex size-10 items-center justify-center rounded-full bg-primary/10">
|
||||||
Revise os dados antes de confirmar. Vamos registrar a fatura
|
<RiBankCardLine className="size-5 text-primary" />
|
||||||
como paga.
|
</div>
|
||||||
</DialogDescription>
|
<div>
|
||||||
|
<DialogTitle>Confirmar pagamento</DialogTitle>
|
||||||
|
<DialogDescription className="mt-0.5 text-xs">
|
||||||
|
Fatura do cartão
|
||||||
|
</DialogDescription>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{invoice ? (
|
{invoice ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-3">
|
||||||
<div className="rounded-lg border p-4">
|
{/* Card principal */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center gap-3 rounded-xl border bg-muted/30 p-4">
|
||||||
<div className="flex items-center gap-3">
|
<InvoiceLogo
|
||||||
<InvoiceLogo
|
cardName={invoice.cardName}
|
||||||
cardName={invoice.cardName}
|
logo={invoice.logo}
|
||||||
logo={invoice.logo}
|
size={36}
|
||||||
size={40}
|
tone="accent"
|
||||||
tone="accent"
|
containerClassName="size-9 shrink-0"
|
||||||
containerClassName="size-10"
|
fallbackClassName="text-xs"
|
||||||
fallbackClassName="text-xs"
|
/>
|
||||||
/>
|
<div className="min-w-0">
|
||||||
<div>
|
<p className="text-[11px] font-medium text-muted-foreground uppercase tracking-wide">
|
||||||
<p className="text-sm font-medium text-muted-foreground">
|
Cartão
|
||||||
Cartão
|
</p>
|
||||||
</p>
|
<p className="truncate text-base font-semibold text-foreground">
|
||||||
<p className="text-lg font-bold text-foreground">
|
{invoice.cardName}
|
||||||
{invoice.cardName}
|
</p>
|
||||||
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-3 sm:grid-cols-2">
|
{/* Métricas */}
|
||||||
<div className="rounded-lg border p-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="mb-2 flex items-center gap-2 text-muted-foreground">
|
<div className="rounded-xl border p-3">
|
||||||
<RiMoneyDollarCircleLine className="size-4" />
|
<div className="mb-1.5 flex items-center gap-1.5 text-muted-foreground">
|
||||||
<span className="text-xs font-semibold uppercase">
|
<RiMoneyDollarCircleLine className="size-3.5" />
|
||||||
Valor da Invoice
|
<span className="text-[11px] font-semibold uppercase tracking-wide">
|
||||||
|
Total da fatura
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<MoneyValues
|
<MoneyValues
|
||||||
@@ -154,22 +133,43 @@ export function InvoicePaymentDialog({
|
|||||||
className="text-lg font-bold"
|
className="text-lg font-bold"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg border p-3">
|
|
||||||
<div className="mb-2 flex items-center gap-2 text-muted-foreground">
|
<div className="rounded-xl border p-3">
|
||||||
<RiCheckboxCircleLine className="size-4" />
|
<div className="mb-1.5 flex items-center gap-1.5 text-muted-foreground">
|
||||||
<span className="text-xs font-semibold uppercase">
|
<RiCalendarLine className="size-3.5" />
|
||||||
Status
|
<span className="text-[11px] font-semibold uppercase tracking-wide">
|
||||||
|
{invoice.paymentStatus === INVOICE_PAYMENT_STATUS.PAID
|
||||||
|
? "Pago em"
|
||||||
|
: "Vencimento"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Badge
|
<p className="text-sm font-semibold text-foreground">
|
||||||
variant={getInvoiceStatusBadgeVariant(
|
{invoice.paymentStatus === INVOICE_PAYMENT_STATUS.PAID
|
||||||
INVOICE_STATUS_LABEL[invoice.paymentStatus],
|
? (paymentInfo?.label ?? "—")
|
||||||
)}
|
: (dueInfo?.label ?? "—")}
|
||||||
>
|
</p>
|
||||||
{INVOICE_STATUS_LABEL[invoice.paymentStatus]}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -186,7 +186,6 @@ export function InvoicePaymentDialog({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onConfirm}
|
onClick={onConfirm}
|
||||||
disabled={isProcessing || !invoice}
|
disabled={isProcessing || !invoice}
|
||||||
className="relative"
|
|
||||||
>
|
>
|
||||||
{isProcessing ? (
|
{isProcessing ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ type ActionResult =
|
|||||||
| { success: false; error: string };
|
| { success: false; error: string };
|
||||||
|
|
||||||
const successMessageByStatus: Record<InvoicePaymentStatus, 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.",
|
[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