import { and, eq, gte, ilike, inArray, lte, not, sum } from "drizzle-orm"; import { cartoes, categorias, faturas, lancamentos, pagadores, } from "@/db/schema"; import { db } from "@/lib/db"; import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants"; import { safeToNumber } from "@/lib/utils/number"; import { getPreviousPeriod } from "@/lib/utils/period"; const DESPESA = "Despesa"; export type CardSummary = { id: string; name: string; brand: string | null; logo: string | null; limit: number; currentUsage: number; usagePercent: number; previousUsage: number; changePercent: number; trend: "up" | "down" | "stable"; status: string; }; export type CardDetailData = { card: CardSummary; monthlyUsage: { period: string; periodLabel: string; amount: number; }[]; categoryBreakdown: { id: string; name: string; icon: string | null; amount: number; percent: number; }[]; topExpenses: { id: string; name: string; amount: number; date: string; category: string | null; }[]; invoiceStatus: { period: string; status: string | null; amount: number; }[]; }; export type CartoesReportData = { cards: CardSummary[]; totalLimit: number; totalUsage: number; totalUsagePercent: number; selectedCard: CardDetailData | null; }; export async function fetchCartoesReportData( userId: string, currentPeriod: string, selectedCartaoId?: string | null, ): Promise { const previousPeriod = getPreviousPeriod(currentPeriod); // Fetch all active cards (not inactive) const allCards = await db .select({ id: cartoes.id, name: cartoes.name, brand: cartoes.brand, logo: cartoes.logo, limit: cartoes.limit, status: cartoes.status, }) .from(cartoes) .where( and(eq(cartoes.userId, userId), not(ilike(cartoes.status, "inativo"))), ); if (allCards.length === 0) { return { cards: [], totalLimit: 0, totalUsage: 0, totalUsagePercent: 0, selectedCard: null, }; } const cardIds = allCards.map((c) => c.id); // Fetch current period usage by card const currentUsageData = await db .select({ cartaoId: lancamentos.cartaoId, totalAmount: sum(lancamentos.amount).as("total"), }) .from(lancamentos) .innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id)) .where( and( eq(lancamentos.userId, userId), eq(lancamentos.period, currentPeriod), eq(pagadores.role, PAGADOR_ROLE_ADMIN), eq(lancamentos.transactionType, DESPESA), inArray(lancamentos.cartaoId, cardIds), ), ) .groupBy(lancamentos.cartaoId); // Fetch previous period usage by card const previousUsageData = await db .select({ cartaoId: lancamentos.cartaoId, totalAmount: sum(lancamentos.amount).as("total"), }) .from(lancamentos) .innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id)) .where( and( eq(lancamentos.userId, userId), eq(lancamentos.period, previousPeriod), eq(pagadores.role, PAGADOR_ROLE_ADMIN), eq(lancamentos.transactionType, DESPESA), inArray(lancamentos.cartaoId, cardIds), ), ) .groupBy(lancamentos.cartaoId); const currentUsageMap = new Map(); for (const row of currentUsageData) { if (row.cartaoId) { currentUsageMap.set( row.cartaoId, Math.abs(safeToNumber(row.totalAmount)), ); } } const previousUsageMap = new Map(); for (const row of previousUsageData) { if (row.cartaoId) { previousUsageMap.set( row.cartaoId, Math.abs(safeToNumber(row.totalAmount)), ); } } // Build card summaries const cards: CardSummary[] = allCards.map((card) => { const limit = safeToNumber(card.limit); const currentUsage = currentUsageMap.get(card.id) || 0; const previousUsage = previousUsageMap.get(card.id) || 0; const usagePercent = limit > 0 ? (currentUsage / limit) * 100 : 0; let changePercent = 0; let trend: "up" | "down" | "stable" = "stable"; if (previousUsage > 0) { changePercent = ((currentUsage - previousUsage) / previousUsage) * 100; if (changePercent > 5) trend = "up"; else if (changePercent < -5) trend = "down"; } else if (currentUsage > 0) { changePercent = 100; trend = "up"; } return { id: card.id, name: card.name, brand: card.brand, logo: card.logo, limit, currentUsage, usagePercent, previousUsage, changePercent, trend, status: card.status, }; }); // Sort cards by usage (descending) cards.sort((a, b) => b.currentUsage - a.currentUsage); // Calculate totals const totalLimit = cards.reduce((acc, c) => acc + c.limit, 0); const totalUsage = cards.reduce((acc, c) => acc + c.currentUsage, 0); const totalUsagePercent = totalLimit > 0 ? (totalUsage / totalLimit) * 100 : 0; // Fetch selected card details if provided let selectedCard: CardDetailData | null = null; const targetCardId = selectedCartaoId || (cards.length > 0 ? cards[0].id : null); if (targetCardId) { const cardSummary = cards.find((c) => c.id === targetCardId); if (cardSummary) { selectedCard = await fetchCardDetail( userId, targetCardId, cardSummary, currentPeriod, ); } } return { cards, totalLimit, totalUsage, totalUsagePercent, selectedCard, }; } async function fetchCardDetail( userId: string, cardId: string, cardSummary: CardSummary, currentPeriod: string, ): Promise { // Build period range for last 12 months const periods: string[] = []; let p = currentPeriod; for (let i = 0; i < 12; i++) { periods.unshift(p); p = getPreviousPeriod(p); } const startPeriod = periods[0]; const monthLabels = [ "Jan", "Fev", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Out", "Nov", "Dez", ]; // Fetch monthly usage const monthlyData = await db .select({ period: lancamentos.period, totalAmount: sum(lancamentos.amount).as("total"), }) .from(lancamentos) .innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id)) .where( and( eq(lancamentos.userId, userId), eq(lancamentos.cartaoId, cardId), gte(lancamentos.period, startPeriod), lte(lancamentos.period, currentPeriod), eq(pagadores.role, PAGADOR_ROLE_ADMIN), eq(lancamentos.transactionType, DESPESA), ), ) .groupBy(lancamentos.period) .orderBy(lancamentos.period); const monthlyUsage = periods.map((period) => { const data = monthlyData.find((d) => d.period === period); const [year, month] = period.split("-"); return { period, periodLabel: `${monthLabels[parseInt(month, 10) - 1]}/${year.slice(2)}`, amount: Math.abs(safeToNumber(data?.totalAmount)), }; }); // Fetch category breakdown for current period const categoryData = await db .select({ categoriaId: lancamentos.categoriaId, totalAmount: sum(lancamentos.amount).as("total"), }) .from(lancamentos) .innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id)) .where( and( eq(lancamentos.userId, userId), eq(lancamentos.cartaoId, cardId), eq(lancamentos.period, currentPeriod), eq(pagadores.role, PAGADOR_ROLE_ADMIN), eq(lancamentos.transactionType, DESPESA), ), ) .groupBy(lancamentos.categoriaId); // Fetch category names const categoryIds = categoryData .map((c) => c.categoriaId) .filter((id): id is string => id !== null); const categoryNames = categoryIds.length > 0 ? await db .select({ id: categorias.id, name: categorias.name, icon: categorias.icon, }) .from(categorias) .where(inArray(categorias.id, categoryIds)) : []; const categoryNameMap = new Map(categoryNames.map((c) => [c.id, c])); const totalCategoryAmount = categoryData.reduce( (acc, c) => acc + Math.abs(safeToNumber(c.totalAmount)), 0, ); const categoryBreakdown = categoryData .map((cat) => { const amount = Math.abs(safeToNumber(cat.totalAmount)); const catInfo = cat.categoriaId ? categoryNameMap.get(cat.categoriaId) : null; return { id: cat.categoriaId || "sem-categoria", name: catInfo?.name || "Sem categoria", icon: catInfo?.icon || null, amount, percent: totalCategoryAmount > 0 ? (amount / totalCategoryAmount) * 100 : 0, }; }) .sort((a, b) => b.amount - a.amount) .slice(0, 10); // Fetch top expenses for current period const topExpensesData = await db .select({ id: lancamentos.id, name: lancamentos.name, amount: lancamentos.amount, purchaseDate: lancamentos.purchaseDate, categoriaId: lancamentos.categoriaId, }) .from(lancamentos) .innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id)) .where( and( eq(lancamentos.userId, userId), eq(lancamentos.cartaoId, cardId), eq(lancamentos.period, currentPeriod), eq(pagadores.role, PAGADOR_ROLE_ADMIN), eq(lancamentos.transactionType, DESPESA), ), ) .orderBy(lancamentos.amount) .limit(10); const topExpenses = topExpensesData.map((expense) => { const catInfo = expense.categoriaId ? categoryNameMap.get(expense.categoriaId) : null; return { id: expense.id, name: expense.name, amount: Math.abs(safeToNumber(expense.amount)), date: expense.purchaseDate ? new Date(expense.purchaseDate).toLocaleDateString("pt-BR") : "", category: catInfo?.name || null, }; }); // Fetch invoice status for last 6 months const invoiceData = await db .select({ period: faturas.period, status: faturas.paymentStatus, }) .from(faturas) .where( and( eq(faturas.userId, userId), eq(faturas.cartaoId, cardId), gte(faturas.period, startPeriod), lte(faturas.period, currentPeriod), ), ) .orderBy(faturas.period); const invoiceStatus = periods.map((period) => { const invoice = invoiceData.find((i) => i.period === period); const usage = monthlyUsage.find((m) => m.period === period); return { period, status: invoice?.status || null, amount: usage?.amount || 0, }; }); return { card: cardSummary, monthlyUsage, categoryBreakdown, topExpenses, invoiceStatus, }; }