feat: destaca fatura paga nos cartoes

This commit is contained in:
Felipe Coutinho
2026-06-06 16:31:33 -03:00
parent b443fb010a
commit 356801324c
6 changed files with 130 additions and 75 deletions

View File

@@ -136,6 +136,7 @@ export default async function Page({ params, searchParams }: PageProps) {
limitAvailable: limitAmount,
currentInvoiceAmount: 0,
currentInvoiceLabel: "",
currentInvoiceStatus: null,
};
const { totalAmount, invoiceStatus, paymentDate } = invoiceData;

View File

@@ -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 (
<Card className="flex flex-col p-6 w-full">
@@ -175,10 +184,17 @@ export function CardItem({
<span className="text-xs text-muted-foreground">
{currentInvoiceLabel}
</span>
<MoneyValues
amount={currentInvoiceAmount}
className="text-xl font-semibold text-info"
/>
<div className="flex flex-wrap items-center gap-2">
<MoneyValues
amount={currentInvoiceAmount}
className="text-xl font-semibold text-info"
/>
{isCurrentInvoicePaid ? (
<Badge variant="success" className="text-xs">
Paga
</Badge>
) : null}
</div>
</div>
<div className="flex gap-2 justify-between w-full">

View File

@@ -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}

View File

@@ -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 = {

View File

@@ -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<number>`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<number>`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<number>`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<number>`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<string, number>();
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<string, InvoicePaymentStatus>();
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 ??

View File

@@ -221,7 +221,7 @@ export function InboxWidget({
<span className="truncate">{item.sourceAppName}</span>
)}
<span className="text-muted-foreground/60">
{relativeTime(item.notificationTimestamp)}
{relativeTime(item.createdAt)}
</span>
</div>
</div>