From 356801324c1d31ce88711c9074afea08d9fc7f37 Mon Sep 17 00:00:00 2001 From: Felipe Coutinho Date: Sat, 6 Jun 2026 16:31:33 -0300 Subject: [PATCH] feat: destaca fatura paga nos cartoes --- .../cards/[cardId]/invoice/page.tsx | 1 + src/features/cards/components/card-item.tsx | 24 ++- src/features/cards/components/cards-page.tsx | 1 + src/features/cards/components/types.ts | 3 + src/features/cards/queries.ts | 174 +++++++++++------- .../components/widgets/inbox-widget.tsx | 2 +- 6 files changed, 130 insertions(+), 75 deletions(-) diff --git a/src/app/(dashboard)/cards/[cardId]/invoice/page.tsx b/src/app/(dashboard)/cards/[cardId]/invoice/page.tsx index c3fc878..5ff2ba3 100644 --- a/src/app/(dashboard)/cards/[cardId]/invoice/page.tsx +++ b/src/app/(dashboard)/cards/[cardId]/invoice/page.tsx @@ -136,6 +136,7 @@ export default async function Page({ params, searchParams }: PageProps) { limitAvailable: limitAmount, currentInvoiceAmount: 0, currentInvoiceLabel: "", + currentInvoiceStatus: null, }; const { totalAmount, invoiceStatus, paymentDate } = invoiceData; diff --git a/src/features/cards/components/card-item.tsx b/src/features/cards/components/card-item.tsx index 2e75426..6a30029 100644 --- a/src/features/cards/components/card-item.tsx +++ b/src/features/cards/components/card-item.tsx @@ -10,6 +10,7 @@ import { } from "@remixicon/react"; import Image from "next/image"; import MoneyValues from "@/shared/components/money-values"; +import { Badge } from "@/shared/components/ui/badge"; import { Card, CardContent, @@ -23,6 +24,10 @@ import { TooltipTrigger, } from "@/shared/components/ui/tooltip"; import { resolveCardBrandAsset } from "@/shared/lib/cards/brand-assets"; +import { + INVOICE_PAYMENT_STATUS, + type InvoicePaymentStatus, +} from "@/shared/lib/invoices"; import { resolveLogoSrc } from "@/shared/lib/logo"; import { cn } from "@/shared/utils/ui"; @@ -37,6 +42,7 @@ interface CardItemProps { limitAvailable?: number; currentInvoiceAmount: number; currentInvoiceLabel: string; + currentInvoiceStatus: InvoicePaymentStatus | null; accountName: string; logo?: string | null; note?: string | null; @@ -58,6 +64,7 @@ export function CardItem({ limitAvailable, currentInvoiceAmount, currentInvoiceLabel, + currentInvoiceStatus, accountName: _accountName, logo, note, @@ -80,6 +87,8 @@ export function CardItem({ const logoPath = resolveLogoSrc(logo); const brandAsset = resolveCardBrandAsset(brand); const isInactive = status?.toLowerCase() === "inativo"; + const isCurrentInvoicePaid = + currentInvoiceStatus === INVOICE_PAYMENT_STATUS.PAID; return ( @@ -175,10 +184,17 @@ export function CardItem({ {currentInvoiceLabel} - +
+ + {isCurrentInvoicePaid ? ( + + Paga + + ) : null} +
diff --git a/src/features/cards/components/cards-page.tsx b/src/features/cards/components/cards-page.tsx index 18684a7..47e2848 100644 --- a/src/features/cards/components/cards-page.tsx +++ b/src/features/cards/components/cards-page.tsx @@ -144,6 +144,7 @@ export function CardsPage({ limitAvailable={card.limitAvailable ?? card.limit ?? null} currentInvoiceAmount={card.currentInvoiceAmount} currentInvoiceLabel={card.currentInvoiceLabel} + currentInvoiceStatus={card.currentInvoiceStatus} accountName={card.accountName} logo={card.logo} note={card.note} diff --git a/src/features/cards/components/types.ts b/src/features/cards/components/types.ts index 6619661..9479012 100644 --- a/src/features/cards/components/types.ts +++ b/src/features/cards/components/types.ts @@ -1,3 +1,5 @@ +import type { InvoicePaymentStatus } from "@/shared/lib/invoices"; + export type Card = { id: string; name: string; @@ -14,6 +16,7 @@ export type Card = { limitAvailable: number; currentInvoiceAmount: number; currentInvoiceLabel: string; + currentInvoiceStatus: InvoicePaymentStatus | null; }; export type CardFormValues = { diff --git a/src/features/cards/queries.ts b/src/features/cards/queries.ts index 8eec3a0..e20af72 100644 --- a/src/features/cards/queries.ts +++ b/src/features/cards/queries.ts @@ -11,7 +11,11 @@ import { } from "drizzle-orm"; import { cards, financialAccounts, invoices, transactions } from "@/db/schema"; import { db } from "@/shared/lib/db"; -import { INVOICE_PAYMENT_STATUS } from "@/shared/lib/invoices"; +import { + INVOICE_PAYMENT_STATUS, + INVOICE_STATUS_VALUES, + type InvoicePaymentStatus, +} from "@/shared/lib/invoices"; import { loadLogoOptions } from "@/shared/lib/logo/options"; import { formatPeriodMonthShort, @@ -33,6 +37,7 @@ type CardData = { limitAvailable: number; currentInvoiceAmount: number; currentInvoiceLabel: string; + currentInvoiceStatus: InvoicePaymentStatus | null; accountId: string; accountName: string; }; @@ -48,6 +53,12 @@ function formatCurrentInvoiceLabel(period: string) { return `Fatura ${formatPeriodMonthShort(period)}. ${year}`; } +function parseInvoiceStatus(value: unknown): InvoicePaymentStatus | null { + return INVOICE_STATUS_VALUES.includes(value as InvoicePaymentStatus) + ? (value as InvoicePaymentStatus) + : null; +} + async function fetchCardsByStatus( userId: string, archived: boolean, @@ -58,79 +69,94 @@ async function fetchCardsByStatus( }> { const currentPeriod = getCurrentPeriod(); const currentInvoiceLabel = formatCurrentInvoiceLabel(currentPeriod); - const [cardRows, accountRows, logoOptions, usageRows, invoiceRows] = - await Promise.all([ - db.query.cards.findMany({ - orderBy: (table, { desc }) => [desc(table.name)], - where: and( - eq(cards.userId, userId), - archived - ? ilike(cards.status, "inativo") - : not(ilike(cards.status, "inativo")), - ), - with: { - financialAccount: { - columns: { - id: true, - name: true, - }, + const [ + cardRows, + accountRows, + logoOptions, + usageRows, + invoiceRows, + invoiceStatusRows, + ] = await Promise.all([ + db.query.cards.findMany({ + orderBy: (table, { desc }) => [desc(table.name)], + where: and( + eq(cards.userId, userId), + archived + ? ilike(cards.status, "inativo") + : not(ilike(cards.status, "inativo")), + ), + with: { + financialAccount: { + columns: { + id: true, + name: true, }, }, - }), - db.query.financialAccounts.findMany({ - orderBy: (table, { desc }) => [desc(table.name)], - where: eq(financialAccounts.userId, userId), - columns: { - id: true, - name: true, - logo: true, - }, - }), - loadLogoOptions(), - db - .select({ - cardId: transactions.cardId, - total: sql`coalesce(sum(${transactions.amount}), 0)`, - }) - .from(transactions) - .leftJoin( - invoices, - and( - eq(invoices.userId, transactions.userId), - eq(invoices.cardId, transactions.cardId), - eq(invoices.period, transactions.period), + }, + }), + db.query.financialAccounts.findMany({ + orderBy: (table, { desc }) => [desc(table.name)], + where: eq(financialAccounts.userId, userId), + columns: { + id: true, + name: true, + logo: true, + }, + }), + loadLogoOptions(), + db + .select({ + cardId: transactions.cardId, + total: sql`coalesce(sum(${transactions.amount}), 0)`, + }) + .from(transactions) + .leftJoin( + invoices, + and( + eq(invoices.userId, transactions.userId), + eq(invoices.cardId, transactions.cardId), + eq(invoices.period, transactions.period), + ), + ) + .where( + and( + eq(transactions.userId, userId), + isNotNull(transactions.cardId), + or( + isNull(invoices.paymentStatus), + ne(invoices.paymentStatus, INVOICE_PAYMENT_STATUS.PAID), ), - ) - .where( - and( - eq(transactions.userId, userId), - isNotNull(transactions.cardId), - or( - isNull(invoices.paymentStatus), - ne(invoices.paymentStatus, INVOICE_PAYMENT_STATUS.PAID), - ), - // Recorrente no cartão: só consome limite quando a data da ocorrência já passou - or( - ne(transactions.condition, "Recorrente"), - sql`${transactions.purchaseDate} <= current_date`, - ), + // Recorrente no cartão: só consome limite quando a data da ocorrência já passou + or( + ne(transactions.condition, "Recorrente"), + sql`${transactions.purchaseDate} <= current_date`, ), - ) - .groupBy(transactions.cardId), - db - .select({ - cardId: transactions.cardId, - total: sql`coalesce(sum(${transactions.amount}), 0)`, - }) - .from(transactions) - .where( - and( - eq(transactions.userId, userId), - eq(transactions.period, currentPeriod), - ), - ) - .groupBy(transactions.cardId), - ]); + ), + ) + .groupBy(transactions.cardId), + db + .select({ + cardId: transactions.cardId, + total: sql`coalesce(sum(${transactions.amount}), 0)`, + }) + .from(transactions) + .where( + and( + eq(transactions.userId, userId), + eq(transactions.period, currentPeriod), + ), + ) + .groupBy(transactions.cardId), + db + .select({ + cardId: invoices.cardId, + paymentStatus: invoices.paymentStatus, + }) + .from(invoices) + .where( + and(eq(invoices.userId, userId), eq(invoices.period, currentPeriod)), + ), + ]); const usageMap = new Map(); usageRows.forEach((row: { cardId: string | null; total: number | null }) => { @@ -144,6 +170,13 @@ async function fetchCardsByStatus( invoiceMap.set(row.cardId, Math.abs(Number(row.total ?? 0))); }, ); + const invoiceStatusMap = new Map(); + invoiceStatusRows.forEach((row) => { + if (!row.cardId) return; + const status = parseInvoiceStatus(row.paymentStatus); + if (!status) return; + invoiceStatusMap.set(row.cardId, status); + }); const cardList = cardRows.map((card) => ({ id: card.id, @@ -166,6 +199,7 @@ async function fetchCardsByStatus( })(), currentInvoiceAmount: invoiceMap.get(card.id) ?? 0, currentInvoiceLabel, + currentInvoiceStatus: invoiceStatusMap.get(card.id) ?? null, accountId: card.accountId, accountName: (card.financialAccount as { name?: string } | null)?.name ?? diff --git a/src/features/dashboard/components/widgets/inbox-widget.tsx b/src/features/dashboard/components/widgets/inbox-widget.tsx index f88d5fd..7e2a819 100644 --- a/src/features/dashboard/components/widgets/inbox-widget.tsx +++ b/src/features/dashboard/components/widgets/inbox-widget.tsx @@ -221,7 +221,7 @@ export function InboxWidget({ {item.sourceAppName} )} - {relativeTime(item.notificationTimestamp)} + {relativeTime(item.createdAt)}