mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 19:01:47 +00:00
refactor: agrega queries e cache do dashboard
This commit is contained in:
@@ -8,12 +8,11 @@ import {
|
||||
ne,
|
||||
not,
|
||||
or,
|
||||
sql,
|
||||
sum,
|
||||
} from "drizzle-orm";
|
||||
import { cards, categories, invoices, payers, transactions } from "@/db/schema";
|
||||
import { cards, categories, invoices, transactions } from "@/db/schema";
|
||||
import { db } from "@/shared/lib/db";
|
||||
import { PAYER_ROLE_ADMIN } from "@/shared/lib/payers/constants";
|
||||
import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
|
||||
import { formatDateOnly } from "@/shared/utils/date";
|
||||
import { safeToNumber } from "@/shared/utils/number";
|
||||
import {
|
||||
@@ -150,48 +149,51 @@ export async function fetchCartoesReportData(
|
||||
}
|
||||
|
||||
const cardIds = allCards.map((c) => c.id);
|
||||
const adminPayerId = await getAdminPayerId(userId);
|
||||
|
||||
// Fetch current period usage by card (recorrente só conta quando a data da ocorrência já passou)
|
||||
const currentUsageData = (await db
|
||||
.select({
|
||||
cardId: transactions.cardId,
|
||||
totalAmount: sum(transactions.amount).as("total"),
|
||||
})
|
||||
.from(transactions)
|
||||
.innerJoin(payers, eq(transactions.payerId, payers.id))
|
||||
.where(
|
||||
and(
|
||||
eq(transactions.userId, userId),
|
||||
eq(transactions.period, currentPeriod),
|
||||
eq(payers.role, PAYER_ROLE_ADMIN),
|
||||
eq(transactions.transactionType, DESPESA),
|
||||
inArray(transactions.cardId, cardIds),
|
||||
or(
|
||||
ne(transactions.condition, "Recorrente"),
|
||||
sql`${transactions.purchaseDate} <= current_date`,
|
||||
),
|
||||
),
|
||||
)
|
||||
.groupBy(transactions.cardId)) as CardUsageRow[];
|
||||
const currentUsageData = adminPayerId
|
||||
? ((await db
|
||||
.select({
|
||||
cardId: transactions.cardId,
|
||||
totalAmount: sum(transactions.amount).as("total"),
|
||||
})
|
||||
.from(transactions)
|
||||
.where(
|
||||
and(
|
||||
eq(transactions.userId, userId),
|
||||
eq(transactions.period, currentPeriod),
|
||||
eq(transactions.payerId, adminPayerId),
|
||||
eq(transactions.transactionType, DESPESA),
|
||||
inArray(transactions.cardId, cardIds),
|
||||
or(
|
||||
ne(transactions.condition, "Recorrente"),
|
||||
lte(transactions.purchaseDate, new Date()),
|
||||
),
|
||||
),
|
||||
)
|
||||
.groupBy(transactions.cardId)) as CardUsageRow[])
|
||||
: [];
|
||||
|
||||
// Fetch previous period usage by card
|
||||
const previousUsageData = (await db
|
||||
.select({
|
||||
cardId: transactions.cardId,
|
||||
totalAmount: sum(transactions.amount).as("total"),
|
||||
})
|
||||
.from(transactions)
|
||||
.innerJoin(payers, eq(transactions.payerId, payers.id))
|
||||
.where(
|
||||
and(
|
||||
eq(transactions.userId, userId),
|
||||
eq(transactions.period, previousPeriod),
|
||||
eq(payers.role, PAYER_ROLE_ADMIN),
|
||||
eq(transactions.transactionType, DESPESA),
|
||||
inArray(transactions.cardId, cardIds),
|
||||
),
|
||||
)
|
||||
.groupBy(transactions.cardId)) as CardUsageRow[];
|
||||
const previousUsageData = adminPayerId
|
||||
? ((await db
|
||||
.select({
|
||||
cardId: transactions.cardId,
|
||||
totalAmount: sum(transactions.amount).as("total"),
|
||||
})
|
||||
.from(transactions)
|
||||
.where(
|
||||
and(
|
||||
eq(transactions.userId, userId),
|
||||
eq(transactions.period, previousPeriod),
|
||||
eq(transactions.payerId, adminPayerId),
|
||||
eq(transactions.transactionType, DESPESA),
|
||||
inArray(transactions.cardId, cardIds),
|
||||
),
|
||||
)
|
||||
.groupBy(transactions.cardId)) as CardUsageRow[])
|
||||
: [];
|
||||
|
||||
const currentUsageMap = new Map<string, number>();
|
||||
for (const row of currentUsageData) {
|
||||
@@ -262,6 +264,7 @@ export async function fetchCartoesReportData(
|
||||
targetCardId,
|
||||
cardSummary,
|
||||
currentPeriod,
|
||||
adminPayerId,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -280,6 +283,7 @@ async function fetchCardDetail(
|
||||
cardId: string,
|
||||
cardSummary: CardSummary,
|
||||
currentPeriod: string,
|
||||
adminPayerId: string | null,
|
||||
): Promise<CardDetailData> {
|
||||
// Build period range for last 12 months
|
||||
const periods = buildPeriodWindow(currentPeriod, 12);
|
||||
@@ -287,25 +291,26 @@ async function fetchCardDetail(
|
||||
const startPeriod = periods[0];
|
||||
|
||||
// Fetch monthly usage
|
||||
const monthlyData = (await db
|
||||
.select({
|
||||
period: transactions.period,
|
||||
totalAmount: sum(transactions.amount).as("total"),
|
||||
})
|
||||
.from(transactions)
|
||||
.innerJoin(payers, eq(transactions.payerId, payers.id))
|
||||
.where(
|
||||
and(
|
||||
eq(transactions.userId, userId),
|
||||
eq(transactions.cardId, cardId),
|
||||
gte(transactions.period, startPeriod),
|
||||
lte(transactions.period, currentPeriod),
|
||||
eq(payers.role, PAYER_ROLE_ADMIN),
|
||||
eq(transactions.transactionType, DESPESA),
|
||||
),
|
||||
)
|
||||
.groupBy(transactions.period)
|
||||
.orderBy(transactions.period)) as MonthlyUsageRow[];
|
||||
const monthlyData = adminPayerId
|
||||
? ((await db
|
||||
.select({
|
||||
period: transactions.period,
|
||||
totalAmount: sum(transactions.amount).as("total"),
|
||||
})
|
||||
.from(transactions)
|
||||
.where(
|
||||
and(
|
||||
eq(transactions.userId, userId),
|
||||
eq(transactions.cardId, cardId),
|
||||
gte(transactions.period, startPeriod),
|
||||
lte(transactions.period, currentPeriod),
|
||||
eq(transactions.payerId, adminPayerId),
|
||||
eq(transactions.transactionType, DESPESA),
|
||||
),
|
||||
)
|
||||
.groupBy(transactions.period)
|
||||
.orderBy(transactions.period)) as MonthlyUsageRow[])
|
||||
: [];
|
||||
|
||||
const monthlyUsage = periods.map((period) => {
|
||||
const data = monthlyData.find((d) => d.period === period);
|
||||
@@ -317,23 +322,24 @@ async function fetchCardDetail(
|
||||
});
|
||||
|
||||
// Fetch category breakdown for current period
|
||||
const categoryData = (await db
|
||||
.select({
|
||||
categoryId: transactions.categoryId,
|
||||
totalAmount: sum(transactions.amount).as("total"),
|
||||
})
|
||||
.from(transactions)
|
||||
.innerJoin(payers, eq(transactions.payerId, payers.id))
|
||||
.where(
|
||||
and(
|
||||
eq(transactions.userId, userId),
|
||||
eq(transactions.cardId, cardId),
|
||||
eq(transactions.period, currentPeriod),
|
||||
eq(payers.role, PAYER_ROLE_ADMIN),
|
||||
eq(transactions.transactionType, DESPESA),
|
||||
),
|
||||
)
|
||||
.groupBy(transactions.categoryId)) as CategoryAmountRow[];
|
||||
const categoryData = adminPayerId
|
||||
? ((await db
|
||||
.select({
|
||||
categoryId: transactions.categoryId,
|
||||
totalAmount: sum(transactions.amount).as("total"),
|
||||
})
|
||||
.from(transactions)
|
||||
.where(
|
||||
and(
|
||||
eq(transactions.userId, userId),
|
||||
eq(transactions.cardId, cardId),
|
||||
eq(transactions.period, currentPeriod),
|
||||
eq(transactions.payerId, adminPayerId),
|
||||
eq(transactions.transactionType, DESPESA),
|
||||
),
|
||||
)
|
||||
.groupBy(transactions.categoryId)) as CategoryAmountRow[])
|
||||
: [];
|
||||
|
||||
// Fetch category names
|
||||
const categoryIds = categoryData
|
||||
@@ -378,27 +384,28 @@ async function fetchCardDetail(
|
||||
.slice(0, 10);
|
||||
|
||||
// Fetch top expenses for current period
|
||||
const topExpensesData = (await db
|
||||
.select({
|
||||
id: transactions.id,
|
||||
name: transactions.name,
|
||||
amount: transactions.amount,
|
||||
purchaseDate: transactions.purchaseDate,
|
||||
categoryId: transactions.categoryId,
|
||||
})
|
||||
.from(transactions)
|
||||
.innerJoin(payers, eq(transactions.payerId, payers.id))
|
||||
.where(
|
||||
and(
|
||||
eq(transactions.userId, userId),
|
||||
eq(transactions.cardId, cardId),
|
||||
eq(transactions.period, currentPeriod),
|
||||
eq(payers.role, PAYER_ROLE_ADMIN),
|
||||
eq(transactions.transactionType, DESPESA),
|
||||
),
|
||||
)
|
||||
.orderBy(transactions.amount)
|
||||
.limit(10)) as TopExpenseRow[];
|
||||
const topExpensesData = adminPayerId
|
||||
? ((await db
|
||||
.select({
|
||||
id: transactions.id,
|
||||
name: transactions.name,
|
||||
amount: transactions.amount,
|
||||
purchaseDate: transactions.purchaseDate,
|
||||
categoryId: transactions.categoryId,
|
||||
})
|
||||
.from(transactions)
|
||||
.where(
|
||||
and(
|
||||
eq(transactions.userId, userId),
|
||||
eq(transactions.cardId, cardId),
|
||||
eq(transactions.period, currentPeriod),
|
||||
eq(transactions.payerId, adminPayerId),
|
||||
eq(transactions.transactionType, DESPESA),
|
||||
),
|
||||
)
|
||||
.orderBy(transactions.amount)
|
||||
.limit(10)) as TopExpenseRow[])
|
||||
: [];
|
||||
|
||||
const topExpenses = topExpensesData.map((expense) => {
|
||||
const catInfo = expense.categoryId
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
eq,
|
||||
gte,
|
||||
ilike,
|
||||
inArray,
|
||||
isNull,
|
||||
lte,
|
||||
ne,
|
||||
@@ -13,23 +14,17 @@ import {
|
||||
sql,
|
||||
sum,
|
||||
} from "drizzle-orm";
|
||||
import {
|
||||
categories,
|
||||
financialAccounts,
|
||||
payers,
|
||||
transactions,
|
||||
} from "@/db/schema";
|
||||
import { categories, financialAccounts, transactions } from "@/db/schema";
|
||||
import {
|
||||
ACCOUNT_AUTO_INVOICE_NOTE_PREFIX,
|
||||
INITIAL_BALANCE_NOTE,
|
||||
} from "@/shared/lib/accounts/constants";
|
||||
import { db } from "@/shared/lib/db";
|
||||
import { PAYER_ROLE_ADMIN } from "@/shared/lib/payers/constants";
|
||||
import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
|
||||
import { safeToNumber } from "@/shared/utils/number";
|
||||
import { getPreviousPeriod } from "@/shared/utils/period";
|
||||
|
||||
const DESPESA = "Despesa";
|
||||
const TRANSFERENCIA = "Transferência";
|
||||
|
||||
export type EstablishmentData = {
|
||||
name: string;
|
||||
@@ -81,6 +76,48 @@ export async function fetchTopEstablishmentsData(
|
||||
const months = parseInt(periodFilter, 10);
|
||||
const periods = buildPeriodRange(currentPeriod, months);
|
||||
const startPeriod = periods[0];
|
||||
const adminPayerId = await getAdminPayerId(userId);
|
||||
const periodLabel =
|
||||
months === 3
|
||||
? "Últimos 3 meses"
|
||||
: months === 6
|
||||
? "Últimos 6 meses"
|
||||
: "Últimos 12 meses";
|
||||
|
||||
if (!adminPayerId) {
|
||||
return {
|
||||
establishments: [],
|
||||
topCategories: [],
|
||||
summary: {
|
||||
totalEstablishments: 0,
|
||||
totalTransactions: 0,
|
||||
totalSpent: 0,
|
||||
avgPerTransaction: 0,
|
||||
mostFrequent: null,
|
||||
highestSpending: null,
|
||||
},
|
||||
periodLabel,
|
||||
};
|
||||
}
|
||||
|
||||
const baseExpenseConditions = [
|
||||
eq(transactions.userId, userId),
|
||||
gte(transactions.period, startPeriod),
|
||||
lte(transactions.period, currentPeriod),
|
||||
eq(transactions.payerId, adminPayerId),
|
||||
eq(transactions.transactionType, DESPESA),
|
||||
] as const;
|
||||
const exclusionConditions = [
|
||||
or(
|
||||
isNull(transactions.note),
|
||||
not(ilike(transactions.note, `${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`)),
|
||||
),
|
||||
or(
|
||||
ne(transactions.note, INITIAL_BALANCE_NOTE),
|
||||
isNull(financialAccounts.excludeInitialBalanceFromIncome),
|
||||
eq(financialAccounts.excludeInitialBalanceFromIncome, false),
|
||||
),
|
||||
] as const;
|
||||
|
||||
// Fetch establishments with transaction count and total amount
|
||||
const establishmentsData = await db
|
||||
@@ -90,57 +127,41 @@ export async function fetchTopEstablishmentsData(
|
||||
totalAmount: sum(transactions.amount).as("total"),
|
||||
})
|
||||
.from(transactions)
|
||||
.innerJoin(payers, eq(transactions.payerId, payers.id))
|
||||
.leftJoin(
|
||||
financialAccounts,
|
||||
eq(transactions.accountId, financialAccounts.id),
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(transactions.userId, userId),
|
||||
gte(transactions.period, startPeriod),
|
||||
lte(transactions.period, currentPeriod),
|
||||
eq(payers.role, PAYER_ROLE_ADMIN),
|
||||
eq(transactions.transactionType, DESPESA),
|
||||
ne(transactions.transactionType, TRANSFERENCIA),
|
||||
or(
|
||||
isNull(transactions.note),
|
||||
not(ilike(transactions.note, `${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`)),
|
||||
),
|
||||
or(
|
||||
ne(transactions.note, INITIAL_BALANCE_NOTE),
|
||||
isNull(financialAccounts.excludeInitialBalanceFromIncome),
|
||||
eq(financialAccounts.excludeInitialBalanceFromIncome, false),
|
||||
),
|
||||
),
|
||||
)
|
||||
.where(and(...baseExpenseConditions, ...exclusionConditions))
|
||||
.groupBy(transactions.name)
|
||||
.orderBy(desc(sql`count`))
|
||||
.limit(50);
|
||||
|
||||
// Fetch categories for each establishment
|
||||
const _establishmentNames = establishmentsData.map(
|
||||
(e: (typeof establishmentsData)[0]) => e.name,
|
||||
);
|
||||
const establishmentNames = establishmentsData
|
||||
.map((est) => est.name)
|
||||
.filter((name): name is string => !!name);
|
||||
|
||||
const categoriesByEstablishment = await db
|
||||
.select({
|
||||
establishmentName: transactions.name,
|
||||
categoryId: transactions.categoryId,
|
||||
count: count().as("count"),
|
||||
})
|
||||
.from(transactions)
|
||||
.innerJoin(payers, eq(transactions.payerId, payers.id))
|
||||
.where(
|
||||
and(
|
||||
eq(transactions.userId, userId),
|
||||
gte(transactions.period, startPeriod),
|
||||
lte(transactions.period, currentPeriod),
|
||||
eq(payers.role, PAYER_ROLE_ADMIN),
|
||||
eq(transactions.transactionType, DESPESA),
|
||||
),
|
||||
)
|
||||
.groupBy(transactions.name, transactions.categoryId);
|
||||
const categoriesByEstablishment =
|
||||
establishmentNames.length > 0
|
||||
? await db
|
||||
.select({
|
||||
establishmentName: transactions.name,
|
||||
categoryId: transactions.categoryId,
|
||||
count: count().as("count"),
|
||||
})
|
||||
.from(transactions)
|
||||
.leftJoin(
|
||||
financialAccounts,
|
||||
eq(transactions.accountId, financialAccounts.id),
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
...baseExpenseConditions,
|
||||
...exclusionConditions,
|
||||
inArray(transactions.name, establishmentNames),
|
||||
),
|
||||
)
|
||||
.groupBy(transactions.name, transactions.categoryId)
|
||||
: [];
|
||||
|
||||
// Fetch all category names
|
||||
const allCategories = await db
|
||||
@@ -159,23 +180,33 @@ export async function fetchTopEstablishmentsData(
|
||||
|
||||
// Build establishment data with categories
|
||||
type EstablishmentRow = (typeof establishmentsData)[0];
|
||||
type CategoryByEstRow = (typeof categoriesByEstablishment)[0];
|
||||
const categoriesByEstablishmentMap = new Map<
|
||||
string,
|
||||
Array<{ name: string; count: number }>
|
||||
>();
|
||||
|
||||
for (const categoryRow of categoriesByEstablishment) {
|
||||
if (!categoryRow.establishmentName || !categoryRow.categoryId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const current =
|
||||
categoriesByEstablishmentMap.get(categoryRow.establishmentName) ?? [];
|
||||
current.push({
|
||||
name:
|
||||
categoryMap.get(categoryRow.categoryId as string)?.name ||
|
||||
"Sem categoria",
|
||||
count: Number(categoryRow.count) || 0,
|
||||
});
|
||||
categoriesByEstablishmentMap.set(categoryRow.establishmentName, current);
|
||||
}
|
||||
|
||||
const establishments: EstablishmentData[] = establishmentsData.map(
|
||||
(est: EstablishmentRow) => {
|
||||
const cnt = Number(est.count) || 0;
|
||||
const total = Math.abs(safeToNumber(est.totalAmount));
|
||||
|
||||
const estCategories = categoriesByEstablishment
|
||||
.filter(
|
||||
(c: CategoryByEstRow) =>
|
||||
c.establishmentName === est.name && c.categoryId,
|
||||
)
|
||||
.map((c: CategoryByEstRow) => ({
|
||||
name:
|
||||
categoryMap.get(c.categoryId as string)?.name || "Sem categoria",
|
||||
count: Number(c.count) || 0,
|
||||
}))
|
||||
const estCategories = (categoriesByEstablishmentMap.get(est.name) ?? [])
|
||||
.sort(
|
||||
(
|
||||
a: { name: string; count: number },
|
||||
@@ -202,29 +233,11 @@ export async function fetchTopEstablishmentsData(
|
||||
count: count().as("count"),
|
||||
})
|
||||
.from(transactions)
|
||||
.innerJoin(payers, eq(transactions.payerId, payers.id))
|
||||
.leftJoin(
|
||||
financialAccounts,
|
||||
eq(transactions.accountId, financialAccounts.id),
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(transactions.userId, userId),
|
||||
gte(transactions.period, startPeriod),
|
||||
lte(transactions.period, currentPeriod),
|
||||
eq(payers.role, PAYER_ROLE_ADMIN),
|
||||
eq(transactions.transactionType, DESPESA),
|
||||
or(
|
||||
isNull(transactions.note),
|
||||
not(ilike(transactions.note, `${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`)),
|
||||
),
|
||||
or(
|
||||
ne(transactions.note, INITIAL_BALANCE_NOTE),
|
||||
isNull(financialAccounts.excludeInitialBalanceFromIncome),
|
||||
eq(financialAccounts.excludeInitialBalanceFromIncome, false),
|
||||
),
|
||||
),
|
||||
)
|
||||
.where(and(...baseExpenseConditions, ...exclusionConditions))
|
||||
.groupBy(transactions.categoryId)
|
||||
.orderBy(sql`total ASC`)
|
||||
.limit(10);
|
||||
@@ -257,13 +270,6 @@ export async function fetchTopEstablishmentsData(
|
||||
const highestSpending =
|
||||
sortedBySpending.length > 0 ? sortedBySpending[0].name : null;
|
||||
|
||||
const periodLabel =
|
||||
months === 3
|
||||
? "Últimos 3 meses"
|
||||
: months === 6
|
||||
? "Últimos 6 meses"
|
||||
: "Últimos 12 meses";
|
||||
|
||||
return {
|
||||
establishments,
|
||||
topCategories,
|
||||
|
||||
Reference in New Issue
Block a user