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, limitAvailable: limitAmount,
currentInvoiceAmount: 0, currentInvoiceAmount: 0,
currentInvoiceLabel: "", currentInvoiceLabel: "",
currentInvoiceStatus: null,
}; };
const { totalAmount, invoiceStatus, paymentDate } = invoiceData; const { totalAmount, invoiceStatus, paymentDate } = invoiceData;

View File

@@ -10,6 +10,7 @@ import {
} from "@remixicon/react"; } from "@remixicon/react";
import Image from "next/image"; import Image from "next/image";
import MoneyValues from "@/shared/components/money-values"; import MoneyValues from "@/shared/components/money-values";
import { Badge } from "@/shared/components/ui/badge";
import { import {
Card, Card,
CardContent, CardContent,
@@ -23,6 +24,10 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@/shared/components/ui/tooltip"; } from "@/shared/components/ui/tooltip";
import { resolveCardBrandAsset } from "@/shared/lib/cards/brand-assets"; 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 { resolveLogoSrc } from "@/shared/lib/logo";
import { cn } from "@/shared/utils/ui"; import { cn } from "@/shared/utils/ui";
@@ -37,6 +42,7 @@ interface CardItemProps {
limitAvailable?: number; limitAvailable?: number;
currentInvoiceAmount: number; currentInvoiceAmount: number;
currentInvoiceLabel: string; currentInvoiceLabel: string;
currentInvoiceStatus: InvoicePaymentStatus | null;
accountName: string; accountName: string;
logo?: string | null; logo?: string | null;
note?: string | null; note?: string | null;
@@ -58,6 +64,7 @@ export function CardItem({
limitAvailable, limitAvailable,
currentInvoiceAmount, currentInvoiceAmount,
currentInvoiceLabel, currentInvoiceLabel,
currentInvoiceStatus,
accountName: _accountName, accountName: _accountName,
logo, logo,
note, note,
@@ -80,6 +87,8 @@ export function CardItem({
const logoPath = resolveLogoSrc(logo); const logoPath = resolveLogoSrc(logo);
const brandAsset = resolveCardBrandAsset(brand); const brandAsset = resolveCardBrandAsset(brand);
const isInactive = status?.toLowerCase() === "inativo"; const isInactive = status?.toLowerCase() === "inativo";
const isCurrentInvoicePaid =
currentInvoiceStatus === INVOICE_PAYMENT_STATUS.PAID;
return ( return (
<Card className="flex flex-col p-6 w-full"> <Card className="flex flex-col p-6 w-full">
@@ -175,10 +184,17 @@ export function CardItem({
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
{currentInvoiceLabel} {currentInvoiceLabel}
</span> </span>
<div className="flex flex-wrap items-center gap-2">
<MoneyValues <MoneyValues
amount={currentInvoiceAmount} amount={currentInvoiceAmount}
className="text-xl font-semibold text-info" className="text-xl font-semibold text-info"
/> />
{isCurrentInvoicePaid ? (
<Badge variant="success" className="text-xs">
Paga
</Badge>
) : null}
</div>
</div> </div>
<div className="flex gap-2 justify-between w-full"> <div className="flex gap-2 justify-between w-full">

View File

@@ -144,6 +144,7 @@ export function CardsPage({
limitAvailable={card.limitAvailable ?? card.limit ?? null} limitAvailable={card.limitAvailable ?? card.limit ?? null}
currentInvoiceAmount={card.currentInvoiceAmount} currentInvoiceAmount={card.currentInvoiceAmount}
currentInvoiceLabel={card.currentInvoiceLabel} currentInvoiceLabel={card.currentInvoiceLabel}
currentInvoiceStatus={card.currentInvoiceStatus}
accountName={card.accountName} accountName={card.accountName}
logo={card.logo} logo={card.logo}
note={card.note} note={card.note}

View File

@@ -1,3 +1,5 @@
import type { InvoicePaymentStatus } from "@/shared/lib/invoices";
export type Card = { export type Card = {
id: string; id: string;
name: string; name: string;
@@ -14,6 +16,7 @@ export type Card = {
limitAvailable: number; limitAvailable: number;
currentInvoiceAmount: number; currentInvoiceAmount: number;
currentInvoiceLabel: string; currentInvoiceLabel: string;
currentInvoiceStatus: InvoicePaymentStatus | null;
}; };
export type CardFormValues = { export type CardFormValues = {

View File

@@ -11,7 +11,11 @@ import {
} from "drizzle-orm"; } from "drizzle-orm";
import { cards, financialAccounts, invoices, transactions } from "@/db/schema"; import { cards, financialAccounts, invoices, transactions } from "@/db/schema";
import { db } from "@/shared/lib/db"; 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 { loadLogoOptions } from "@/shared/lib/logo/options";
import { import {
formatPeriodMonthShort, formatPeriodMonthShort,
@@ -33,6 +37,7 @@ type CardData = {
limitAvailable: number; limitAvailable: number;
currentInvoiceAmount: number; currentInvoiceAmount: number;
currentInvoiceLabel: string; currentInvoiceLabel: string;
currentInvoiceStatus: InvoicePaymentStatus | null;
accountId: string; accountId: string;
accountName: string; accountName: string;
}; };
@@ -48,6 +53,12 @@ function formatCurrentInvoiceLabel(period: string) {
return `Fatura ${formatPeriodMonthShort(period)}. ${year}`; 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( async function fetchCardsByStatus(
userId: string, userId: string,
archived: boolean, archived: boolean,
@@ -58,8 +69,14 @@ async function fetchCardsByStatus(
}> { }> {
const currentPeriod = getCurrentPeriod(); const currentPeriod = getCurrentPeriod();
const currentInvoiceLabel = formatCurrentInvoiceLabel(currentPeriod); const currentInvoiceLabel = formatCurrentInvoiceLabel(currentPeriod);
const [cardRows, accountRows, logoOptions, usageRows, invoiceRows] = const [
await Promise.all([ cardRows,
accountRows,
logoOptions,
usageRows,
invoiceRows,
invoiceStatusRows,
] = await Promise.all([
db.query.cards.findMany({ db.query.cards.findMany({
orderBy: (table, { desc }) => [desc(table.name)], orderBy: (table, { desc }) => [desc(table.name)],
where: and( where: and(
@@ -130,6 +147,15 @@ async function fetchCardsByStatus(
), ),
) )
.groupBy(transactions.cardId), .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>(); const usageMap = new Map<string, number>();
@@ -144,6 +170,13 @@ async function fetchCardsByStatus(
invoiceMap.set(row.cardId, Math.abs(Number(row.total ?? 0))); 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) => ({ const cardList = cardRows.map((card) => ({
id: card.id, id: card.id,
@@ -166,6 +199,7 @@ async function fetchCardsByStatus(
})(), })(),
currentInvoiceAmount: invoiceMap.get(card.id) ?? 0, currentInvoiceAmount: invoiceMap.get(card.id) ?? 0,
currentInvoiceLabel, currentInvoiceLabel,
currentInvoiceStatus: invoiceStatusMap.get(card.id) ?? null,
accountId: card.accountId, accountId: card.accountId,
accountName: accountName:
(card.financialAccount as { name?: string } | null)?.name ?? (card.financialAccount as { name?: string } | null)?.name ??

View File

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