mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-06-10 15:26:00 +00:00
refactor: atualiza transacoes dashboard e relatorios
This commit is contained in:
@@ -28,7 +28,7 @@ type CategoryBreakdownRow = {
|
||||
};
|
||||
|
||||
type CategoryBudgetRow = {
|
||||
categoriaId: string | null;
|
||||
categoryId: string | null;
|
||||
amount: unknown;
|
||||
};
|
||||
|
||||
@@ -43,8 +43,8 @@ export function buildCategoryBreakdownData({
|
||||
}): DashboardCategoryBreakdownData {
|
||||
const budgetMap = new Map<string, number>();
|
||||
for (const row of budgetRows) {
|
||||
if (row.categoriaId) {
|
||||
budgetMap.set(row.categoriaId, toNumber(row.amount));
|
||||
if (row.categoryId) {
|
||||
budgetMap.set(row.categoryId, toNumber(row.amount));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
import { and, desc, eq, isNull, ne, or, sql } from "drizzle-orm";
|
||||
import { categorias, contas, lancamentos, pagadores } from "@/db/schema";
|
||||
import { mapLancamentosData } from "@/features/transactions/page-helpers";
|
||||
import {
|
||||
categories,
|
||||
financialAccounts,
|
||||
payers,
|
||||
transactions,
|
||||
} from "@/db/schema";
|
||||
import { mapTransactionsData } from "@/features/transactions/page-helpers";
|
||||
import {
|
||||
ACCOUNT_AUTO_INVOICE_NOTE_PREFIX,
|
||||
INITIAL_BALANCE_NOTE,
|
||||
} from "@/shared/lib/accounts/constants";
|
||||
import type { CategoryType } from "@/shared/lib/categories/constants";
|
||||
import { db } from "@/shared/lib/db";
|
||||
import { PAGADOR_ROLE_ADMIN } from "@/shared/lib/payers/constants";
|
||||
import { PAYER_ROLE_ADMIN } from "@/shared/lib/payers/constants";
|
||||
import { calculatePercentageChange } from "@/shared/utils/math";
|
||||
import { safeToNumber as toNumber } from "@/shared/utils/number";
|
||||
import { getPreviousPeriod } from "@/shared/utils/period";
|
||||
|
||||
type MappedLancamentos = ReturnType<typeof mapLancamentosData>;
|
||||
type MappedLancamentos = ReturnType<typeof mapTransactionsData>;
|
||||
|
||||
export type CategoryDetailData = {
|
||||
category: {
|
||||
@@ -34,8 +39,8 @@ export async function fetchCategoryDetails(
|
||||
categoryId: string,
|
||||
period: string,
|
||||
): Promise<CategoryDetailData | null> {
|
||||
const category = await db.query.categorias.findFirst({
|
||||
where: and(eq(categorias.userId, userId), eq(categorias.id, categoryId)),
|
||||
const category = await db.query.categories.findFirst({
|
||||
where: and(eq(categories.userId, userId), eq(categories.id, categoryId)),
|
||||
});
|
||||
|
||||
if (!category) {
|
||||
@@ -46,35 +51,35 @@ export async function fetchCategoryDetails(
|
||||
const transactionType = category.type === "receita" ? "Receita" : "Despesa";
|
||||
|
||||
const sanitizedNote = or(
|
||||
isNull(lancamentos.note),
|
||||
sql`${lancamentos.note} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`,
|
||||
isNull(transactions.note),
|
||||
sql`${transactions.note} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`,
|
||||
);
|
||||
|
||||
const currentRows = await db.query.lancamentos.findMany({
|
||||
const currentRows = await db.query.transactions.findMany({
|
||||
where: and(
|
||||
eq(lancamentos.userId, userId),
|
||||
eq(lancamentos.categoriaId, categoryId),
|
||||
eq(lancamentos.transactionType, transactionType),
|
||||
eq(lancamentos.period, period),
|
||||
eq(transactions.userId, userId),
|
||||
eq(transactions.categoryId, categoryId),
|
||||
eq(transactions.transactionType, transactionType),
|
||||
eq(transactions.period, period),
|
||||
sanitizedNote,
|
||||
),
|
||||
with: {
|
||||
pagador: true,
|
||||
conta: true,
|
||||
cartao: true,
|
||||
categoria: true,
|
||||
payer: true,
|
||||
financialAccount: true,
|
||||
card: true,
|
||||
category: true,
|
||||
},
|
||||
orderBy: [desc(lancamentos.purchaseDate), desc(lancamentos.createdAt)],
|
||||
orderBy: [desc(transactions.purchaseDate), desc(transactions.createdAt)],
|
||||
});
|
||||
|
||||
const filteredRows = currentRows.filter((row) => {
|
||||
// Filtrar apenas pagadores admin
|
||||
if (row.pagador?.role !== PAGADOR_ROLE_ADMIN) return false;
|
||||
// Filtrar apenas payers admin
|
||||
if (row.payer?.role !== PAYER_ROLE_ADMIN) return false;
|
||||
|
||||
// Excluir saldos iniciais se a conta tiver o flag ativo
|
||||
if (
|
||||
row.note === INITIAL_BALANCE_NOTE &&
|
||||
row.conta?.excludeInitialBalanceFromIncome
|
||||
row.financialAccount?.excludeInitialBalanceFromIncome
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -82,33 +87,36 @@ export async function fetchCategoryDetails(
|
||||
return true;
|
||||
});
|
||||
|
||||
const transactions = mapLancamentosData(filteredRows);
|
||||
const transactionList = mapTransactionsData(filteredRows);
|
||||
|
||||
const currentTotal = transactions.reduce(
|
||||
const currentTotal = transactionList.reduce(
|
||||
(total, transaction) => total + Math.abs(toNumber(transaction.amount)),
|
||||
0,
|
||||
);
|
||||
|
||||
const [previousTotalRow] = await db
|
||||
.select({
|
||||
total: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`,
|
||||
total: sql<number>`coalesce(sum(${transactions.amount}), 0)`,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
|
||||
.leftJoin(contas, eq(lancamentos.contaId, contas.id))
|
||||
.from(transactions)
|
||||
.innerJoin(payers, eq(transactions.payerId, payers.id))
|
||||
.leftJoin(
|
||||
financialAccounts,
|
||||
eq(transactions.accountId, financialAccounts.id),
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, userId),
|
||||
eq(lancamentos.categoriaId, categoryId),
|
||||
eq(lancamentos.transactionType, transactionType),
|
||||
eq(pagadores.role, PAGADOR_ROLE_ADMIN),
|
||||
eq(transactions.userId, userId),
|
||||
eq(transactions.categoryId, categoryId),
|
||||
eq(transactions.transactionType, transactionType),
|
||||
eq(payers.role, PAYER_ROLE_ADMIN),
|
||||
sanitizedNote,
|
||||
eq(lancamentos.period, previousPeriod),
|
||||
eq(transactions.period, previousPeriod),
|
||||
// Excluir saldos iniciais se a conta tiver o flag ativo
|
||||
or(
|
||||
ne(lancamentos.note, INITIAL_BALANCE_NOTE),
|
||||
isNull(contas.excludeInitialBalanceFromIncome),
|
||||
eq(contas.excludeInitialBalanceFromIncome, false),
|
||||
ne(transactions.note, INITIAL_BALANCE_NOTE),
|
||||
isNull(financialAccounts.excludeInitialBalanceFromIncome),
|
||||
eq(financialAccounts.excludeInitialBalanceFromIncome, false),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -131,6 +139,6 @@ export async function fetchCategoryDetails(
|
||||
currentTotal,
|
||||
previousTotal,
|
||||
percentageChange,
|
||||
transactions,
|
||||
transactions: transactionList,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { and, eq, inArray, isNull, or, sql } from "drizzle-orm";
|
||||
import { categorias, lancamentos, pagadores } from "@/db/schema";
|
||||
import { categories, payers, transactions } from "@/db/schema";
|
||||
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/shared/lib/accounts/constants";
|
||||
import { db } from "@/shared/lib/db";
|
||||
import { PAGADOR_ROLE_ADMIN } from "@/shared/lib/payers/constants";
|
||||
import { PAYER_ROLE_ADMIN } from "@/shared/lib/payers/constants";
|
||||
import { CATEGORY_COLORS } from "@/shared/utils/category-colors";
|
||||
import { safeToNumber as toNumber } from "@/shared/utils/number";
|
||||
import {
|
||||
@@ -56,14 +56,14 @@ export async function fetchAllCategories(
|
||||
): Promise<CategoryOption[]> {
|
||||
const result = await db
|
||||
.select({
|
||||
id: categorias.id,
|
||||
name: categorias.name,
|
||||
icon: categorias.icon,
|
||||
type: categorias.type,
|
||||
id: categories.id,
|
||||
name: categories.name,
|
||||
icon: categories.icon,
|
||||
type: categories.type,
|
||||
})
|
||||
.from(categorias)
|
||||
.where(eq(categorias.userId, userId))
|
||||
.orderBy(categorias.type, categorias.name);
|
||||
.from(categories)
|
||||
.where(eq(categories.userId, userId))
|
||||
.orderBy(categories.type, categories.name);
|
||||
|
||||
return result as CategoryOption[];
|
||||
}
|
||||
@@ -88,36 +88,36 @@ export async function fetchCategoryHistory(
|
||||
// Fetch monthly data for ALL categories with transactions
|
||||
const monthlyDataQuery = (await db
|
||||
.select({
|
||||
categoryId: categorias.id,
|
||||
categoryName: categorias.name,
|
||||
categoryIcon: categorias.icon,
|
||||
period: lancamentos.period,
|
||||
totalAmount: sql<string>`SUM(ABS(${lancamentos.amount}))`.as(
|
||||
categoryId: categories.id,
|
||||
categoryName: categories.name,
|
||||
categoryIcon: categories.icon,
|
||||
period: transactions.period,
|
||||
totalAmount: sql<string>`SUM(ABS(${transactions.amount}))`.as(
|
||||
"total_amount",
|
||||
),
|
||||
})
|
||||
.from(lancamentos)
|
||||
.innerJoin(categorias, eq(lancamentos.categoriaId, categorias.id))
|
||||
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
|
||||
.from(transactions)
|
||||
.innerJoin(categories, eq(transactions.categoryId, categories.id))
|
||||
.innerJoin(payers, eq(transactions.payerId, payers.id))
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, userId),
|
||||
eq(categorias.userId, userId),
|
||||
inArray(lancamentos.period, periods),
|
||||
eq(pagadores.role, PAGADOR_ROLE_ADMIN),
|
||||
eq(transactions.userId, userId),
|
||||
eq(categories.userId, userId),
|
||||
inArray(transactions.period, periods),
|
||||
eq(payers.role, PAYER_ROLE_ADMIN),
|
||||
or(
|
||||
isNull(lancamentos.note),
|
||||
isNull(transactions.note),
|
||||
sql`${
|
||||
lancamentos.note
|
||||
transactions.note
|
||||
} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`,
|
||||
),
|
||||
),
|
||||
)
|
||||
.groupBy(
|
||||
categorias.id,
|
||||
categorias.name,
|
||||
categorias.icon,
|
||||
lancamentos.period,
|
||||
categories.id,
|
||||
categories.name,
|
||||
categories.icon,
|
||||
transactions.period,
|
||||
)) as MonthlyCategoryRow[];
|
||||
|
||||
if (monthlyDataQuery.length === 0) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { and, eq, inArray, sql } from "drizzle-orm";
|
||||
import { categorias, lancamentos, orcamentos } from "@/db/schema";
|
||||
import { budgets, categories, transactions } from "@/db/schema";
|
||||
import {
|
||||
buildCategoryBreakdownData,
|
||||
type DashboardCategoryBreakdownData,
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
import {
|
||||
buildDashboardAdminFilters,
|
||||
excludeAutoInvoiceEntries,
|
||||
} from "@/features/dashboard/lancamento-filters";
|
||||
} from "@/features/dashboard/transaction-filters";
|
||||
import { db } from "@/shared/lib/db";
|
||||
import { getAdminPagadorId } from "@/shared/lib/payers/get-admin-id";
|
||||
import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
|
||||
import { getPreviousPeriod } from "@/shared/utils/period";
|
||||
|
||||
export type CategoryExpenseItem = DashboardCategoryBreakdownItem;
|
||||
@@ -22,45 +22,45 @@ export async function fetchExpensesByCategory(
|
||||
): Promise<ExpensesByCategoryData> {
|
||||
const previousPeriod = getPreviousPeriod(period);
|
||||
|
||||
const adminPagadorId = await getAdminPagadorId(userId);
|
||||
if (!adminPagadorId) {
|
||||
const adminPayerId = await getAdminPayerId(userId);
|
||||
if (!adminPayerId) {
|
||||
return { categories: [], currentTotal: 0, previousTotal: 0 };
|
||||
}
|
||||
|
||||
// Single query: GROUP BY categoriaId + period for both current and previous periods
|
||||
// Single query: GROUP BY categoryId + period for both current and previous periods
|
||||
const [rows, budgetRows] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
categoryId: categorias.id,
|
||||
categoryName: categorias.name,
|
||||
categoryIcon: categorias.icon,
|
||||
period: lancamentos.period,
|
||||
total: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`,
|
||||
categoryId: categories.id,
|
||||
categoryName: categories.name,
|
||||
categoryIcon: categories.icon,
|
||||
period: transactions.period,
|
||||
total: sql<number>`coalesce(sum(${transactions.amount}), 0)`,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.innerJoin(categorias, eq(lancamentos.categoriaId, categorias.id))
|
||||
.from(transactions)
|
||||
.innerJoin(categories, eq(transactions.categoryId, categories.id))
|
||||
.where(
|
||||
and(
|
||||
...buildDashboardAdminFilters({ userId, adminPagadorId }),
|
||||
inArray(lancamentos.period, [period, previousPeriod]),
|
||||
eq(lancamentos.transactionType, "Despesa"),
|
||||
eq(categorias.type, "despesa"),
|
||||
...buildDashboardAdminFilters({ userId, adminPayerId }),
|
||||
inArray(transactions.period, [period, previousPeriod]),
|
||||
eq(transactions.transactionType, "Despesa"),
|
||||
eq(categories.type, "despesa"),
|
||||
excludeAutoInvoiceEntries(),
|
||||
),
|
||||
)
|
||||
.groupBy(
|
||||
categorias.id,
|
||||
categorias.name,
|
||||
categorias.icon,
|
||||
lancamentos.period,
|
||||
categories.id,
|
||||
categories.name,
|
||||
categories.icon,
|
||||
transactions.period,
|
||||
),
|
||||
db
|
||||
.select({
|
||||
categoriaId: orcamentos.categoriaId,
|
||||
amount: orcamentos.amount,
|
||||
categoryId: budgets.categoryId,
|
||||
amount: budgets.amount,
|
||||
})
|
||||
.from(orcamentos)
|
||||
.where(and(eq(orcamentos.userId, userId), eq(orcamentos.period, period))),
|
||||
.from(budgets)
|
||||
.where(and(eq(budgets.userId, userId), eq(budgets.period, period))),
|
||||
]);
|
||||
|
||||
return buildCategoryBreakdownData({
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { and, eq, inArray, sql } from "drizzle-orm";
|
||||
import { categorias, contas, lancamentos, orcamentos } from "@/db/schema";
|
||||
import {
|
||||
budgets,
|
||||
categories,
|
||||
financialAccounts,
|
||||
transactions,
|
||||
} from "@/db/schema";
|
||||
import {
|
||||
buildCategoryBreakdownData,
|
||||
type DashboardCategoryBreakdownData,
|
||||
@@ -9,9 +14,9 @@ import {
|
||||
buildDashboardAdminFilters,
|
||||
excludeAutoInvoiceEntries,
|
||||
excludeInitialBalanceWhenConfigured,
|
||||
} from "@/features/dashboard/lancamento-filters";
|
||||
} from "@/features/dashboard/transaction-filters";
|
||||
import { db } from "@/shared/lib/db";
|
||||
import { getAdminPagadorId } from "@/shared/lib/payers/get-admin-id";
|
||||
import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
|
||||
import { getPreviousPeriod } from "@/shared/utils/period";
|
||||
|
||||
export type CategoryIncomeItem = DashboardCategoryBreakdownItem;
|
||||
@@ -23,47 +28,50 @@ export async function fetchIncomeByCategory(
|
||||
): Promise<IncomeByCategoryData> {
|
||||
const previousPeriod = getPreviousPeriod(period);
|
||||
|
||||
const adminPagadorId = await getAdminPagadorId(userId);
|
||||
if (!adminPagadorId) {
|
||||
const adminPayerId = await getAdminPayerId(userId);
|
||||
if (!adminPayerId) {
|
||||
return { categories: [], currentTotal: 0, previousTotal: 0 };
|
||||
}
|
||||
|
||||
// Single query: GROUP BY categoriaId + period for both current and previous periods
|
||||
// Single query: GROUP BY categoryId + period for both current and previous periods
|
||||
const [rows, budgetRows] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
categoryId: categorias.id,
|
||||
categoryName: categorias.name,
|
||||
categoryIcon: categorias.icon,
|
||||
period: lancamentos.period,
|
||||
total: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`,
|
||||
categoryId: categories.id,
|
||||
categoryName: categories.name,
|
||||
categoryIcon: categories.icon,
|
||||
period: transactions.period,
|
||||
total: sql<number>`coalesce(sum(${transactions.amount}), 0)`,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.innerJoin(categorias, eq(lancamentos.categoriaId, categorias.id))
|
||||
.leftJoin(contas, eq(lancamentos.contaId, contas.id))
|
||||
.from(transactions)
|
||||
.innerJoin(categories, eq(transactions.categoryId, categories.id))
|
||||
.leftJoin(
|
||||
financialAccounts,
|
||||
eq(transactions.accountId, financialAccounts.id),
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
...buildDashboardAdminFilters({ userId, adminPagadorId }),
|
||||
inArray(lancamentos.period, [period, previousPeriod]),
|
||||
eq(lancamentos.transactionType, "Receita"),
|
||||
eq(categorias.type, "receita"),
|
||||
...buildDashboardAdminFilters({ userId, adminPayerId }),
|
||||
inArray(transactions.period, [period, previousPeriod]),
|
||||
eq(transactions.transactionType, "Receita"),
|
||||
eq(categories.type, "receita"),
|
||||
excludeAutoInvoiceEntries(),
|
||||
excludeInitialBalanceWhenConfigured(),
|
||||
),
|
||||
)
|
||||
.groupBy(
|
||||
categorias.id,
|
||||
categorias.name,
|
||||
categorias.icon,
|
||||
lancamentos.period,
|
||||
categories.id,
|
||||
categories.name,
|
||||
categories.icon,
|
||||
transactions.period,
|
||||
),
|
||||
db
|
||||
.select({
|
||||
categoriaId: orcamentos.categoriaId,
|
||||
amount: orcamentos.amount,
|
||||
categoryId: budgets.categoryId,
|
||||
amount: budgets.amount,
|
||||
})
|
||||
.from(orcamentos)
|
||||
.where(and(eq(orcamentos.userId, userId), eq(orcamentos.period, period))),
|
||||
.from(budgets)
|
||||
.where(and(eq(budgets.userId, userId), eq(budgets.period, period))),
|
||||
]);
|
||||
|
||||
return buildCategoryBreakdownData({
|
||||
|
||||
Reference in New Issue
Block a user