diff --git a/package.json b/package.json index 5fee50c..2c5046f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 91ae335..b0a26e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/features/dashboard/components/bills/bill-list-item.tsx b/src/features/dashboard/components/bills/bill-list-item.tsx index 7aace27..98e77ba 100644 --- a/src/features/dashboard/components/bills/bill-list-item.tsx +++ b/src/features/dashboard/components/bills/bill-list-item.tsx @@ -19,8 +19,8 @@ export function BillListItem({ bill, onPay }: BillListItemProps) { const overdue = isBillOverdue(bill); return ( -
  • -
    +
  • +
    @@ -54,7 +54,7 @@ export function BillListItem({ bill, onPay }: BillListItemProps) { > {bill.isSettled ? ( - Pago + Pago ) : overdue ? ( diff --git a/src/features/dashboard/components/bills/bill-payment-dialog.tsx b/src/features/dashboard/components/bills/bill-payment-dialog.tsx index 2d5ef99..644514d 100644 --- a/src/features/dashboard/components/bills/bill-payment-dialog.tsx +++ b/src/features/dashboard/components/bills/bill-payment-dialog.tsx @@ -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" ? ( -
    -
    - -
    -
    - - Pagamento registrado! - - - Atualizamos o status do boleto para pago. Em instantes ele - aparecerá como baixado no histórico. - -
    - - - -
    + ) : ( <> - Confirmar pagamento do boleto - - Confirme os dados para registrar o pagamento. Você poderá editar - o lançamento depois, se necessário. - +
    +
    + +
    +
    + Confirmar pagamento + + Boleto + +
    +
    {bill ? ( -
    -
    -
    -
    -
    - -
    -
    -

    - Boleto -

    -

    - {bill.name} -

    -
    -
    - {dueLabel ? ( -
    -

    - {dueLabel} -

    -
    - ) : null} -
    +
    + {/* Card principal */} +
    +

    + Boleto +

    +

    + {bill.name} +

    -
    -
    -
    - - - Valor do Boleto + {/* Métricas */} +
    +
    +
    + + + Valor
    -
    -
    - - - Status + +
    +
    + + + Vencimento
    - - {bill.isSettled ? "Pago" : "Pendente"} - +

    + {dueLabel?.replace("Vencimento: ", "") ?? "—"} +

    + + {/* Status */} +
    + + Status atual + + + {bill.isSettled ? "Pago" : "Pendente"} + +
    + + {/* Aviso */} +

    + Você poderá editar o lançamento depois, se necessário. +

    ) : null} @@ -169,7 +164,6 @@ export function BillPaymentDialog({ type="button" onClick={onConfirm} disabled={isProcessing || !bill || bill.isSettled} - className="relative" > {isProcessing ? ( <> diff --git a/src/features/dashboard/components/invoices/invoice-list-item.tsx b/src/features/dashboard/components/invoices/invoice-list-item.tsx index 40b83c7..acd72c5 100644 --- a/src/features/dashboard/components/invoices/invoice-list-item.tsx +++ b/src/features/dashboard/components/invoices/invoice-list-item.tsx @@ -55,8 +55,8 @@ export function InvoiceListItem({ invoice, onPay }: InvoiceListItemProps) { ); return ( -
  • -
    +
    +
    {isPaid ? ( - Pago + Pago ) : isOverdue ? ( @@ -147,6 +147,6 @@ export function InvoiceListItem({ invoice, onPay }: InvoiceListItemProps) { )}
    -
  • + ); } diff --git a/src/features/dashboard/components/invoices/invoice-payment-dialog.tsx b/src/features/dashboard/components/invoices/invoice-payment-dialog.tsx index 731ca17..7384555 100644 --- a/src/features/dashboard/components/invoices/invoice-payment-dialog.tsx +++ b/src/features/dashboard/components/invoices/invoice-payment-dialog.tsx @@ -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 ( {modalState === "success" ? ( -
    -
    - -
    -
    - - Pagamento confirmado! - - - Atualizamos o status da fatura. O lançamento do pagamento - aparecerá no extrato em instantes. - -
    - - - -
    + ) : ( <> - Confirmar pagamento - - Revise os dados antes de confirmar. Vamos registrar a fatura - como paga. - +
    +
    + +
    +
    + Confirmar pagamento + + Fatura do cartão + +
    +
    {invoice ? ( -
    -
    -
    -
    - -
    -

    - Cartão -

    -

    - {invoice.cardName} -

    -
    -
    -
    - {invoice.paymentStatus !== INVOICE_PAYMENT_STATUS.PAID ? ( -

    - { - parseInvoiceDueDate(invoice.period, invoice.dueDay) - .label - } -

    - ) : null} - {invoice.paymentStatus === INVOICE_PAYMENT_STATUS.PAID && - paymentInfo ? ( -

    - {paymentInfo.label} -

    - ) : null} -
    +
    + {/* Card principal */} +
    + +
    +

    + Cartão +

    +

    + {invoice.cardName} +

    -
    -
    -
    - - - Valor da Invoice + {/* Métricas */} +
    +
    +
    + + + Total da fatura
    -
    -
    - - - Status + +
    +
    + + + {invoice.paymentStatus === INVOICE_PAYMENT_STATUS.PAID + ? "Pago em" + : "Vencimento"}
    - - {INVOICE_STATUS_LABEL[invoice.paymentStatus]} - +

    + {invoice.paymentStatus === INVOICE_PAYMENT_STATUS.PAID + ? (paymentInfo?.label ?? "—") + : (dueInfo?.label ?? "—")} +

    + + {/* Status */} +
    + + Status atual + + + {INVOICE_STATUS_LABEL[invoice.paymentStatus]} + +
    + + {/* Aviso */} +

    + Vamos registrar a fatura como paga. Você poderá editar depois + se necessário. +

    ) : null} @@ -186,7 +186,6 @@ export function InvoicePaymentDialog({ type="button" onClick={onConfirm} disabled={isProcessing || !invoice} - className="relative" > {isProcessing ? ( <> diff --git a/src/features/invoices/actions.ts b/src/features/invoices/actions.ts index c3007d3..d159e13 100644 --- a/src/features/invoices/actions.ts +++ b/src/features/invoices/actions.ts @@ -47,7 +47,7 @@ type ActionResult = | { success: false; error: string }; const successMessageByStatus: Record = { - [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.", }; diff --git a/src/shared/components/payment-success.tsx b/src/shared/components/payment-success.tsx new file mode 100644 index 0000000..750ef92 --- /dev/null +++ b/src/shared/components/payment-success.tsx @@ -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 ( +
    +
    + {/* Anel de pulso */} + + +
    + +
    +
    + +
    + {title} + + {description} + +
    + + + + +
    + ); +}