mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 11:01:45 +00:00
refactor: atualiza transacoes dashboard e relatorios
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { and, eq, sql } from "drizzle-orm";
|
||||
import { contas, lancamentos, pagadores } from "@/db/schema";
|
||||
import { financialAccounts, payers, transactions } from "@/db/schema";
|
||||
import { INITIAL_BALANCE_NOTE } 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 { safeToNumber as toNumber } from "@/shared/utils/number";
|
||||
|
||||
type RawDashboardAccount = {
|
||||
@@ -36,49 +36,49 @@ export async function fetchDashboardAccounts(
|
||||
): Promise<DashboardAccountsSnapshot> {
|
||||
const rows = await db
|
||||
.select({
|
||||
id: contas.id,
|
||||
name: contas.name,
|
||||
accountType: contas.accountType,
|
||||
status: contas.status,
|
||||
logo: contas.logo,
|
||||
initialBalance: contas.initialBalance,
|
||||
excludeFromBalance: contas.excludeFromBalance,
|
||||
id: financialAccounts.id,
|
||||
name: financialAccounts.name,
|
||||
accountType: financialAccounts.accountType,
|
||||
status: financialAccounts.status,
|
||||
logo: financialAccounts.logo,
|
||||
initialBalance: financialAccounts.initialBalance,
|
||||
excludeFromBalance: financialAccounts.excludeFromBalance,
|
||||
balanceMovements: sql<number>`
|
||||
coalesce(
|
||||
sum(
|
||||
case
|
||||
when ${lancamentos.note} = ${INITIAL_BALANCE_NOTE} then 0
|
||||
else ${lancamentos.amount}
|
||||
when ${transactions.note} = ${INITIAL_BALANCE_NOTE} then 0
|
||||
else ${transactions.amount}
|
||||
end
|
||||
),
|
||||
0
|
||||
)
|
||||
`,
|
||||
})
|
||||
.from(contas)
|
||||
.from(financialAccounts)
|
||||
.leftJoin(
|
||||
lancamentos,
|
||||
transactions,
|
||||
and(
|
||||
eq(lancamentos.contaId, contas.id),
|
||||
eq(lancamentos.userId, userId),
|
||||
eq(lancamentos.isSettled, true),
|
||||
eq(transactions.accountId, financialAccounts.id),
|
||||
eq(transactions.userId, userId),
|
||||
eq(transactions.isSettled, true),
|
||||
),
|
||||
)
|
||||
.leftJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
|
||||
.leftJoin(payers, eq(transactions.payerId, payers.id))
|
||||
.where(
|
||||
and(
|
||||
eq(contas.userId, userId),
|
||||
sql`(${lancamentos.id} IS NULL OR ${pagadores.role} = ${PAGADOR_ROLE_ADMIN})`,
|
||||
eq(financialAccounts.userId, userId),
|
||||
sql`(${transactions.id} IS NULL OR ${payers.role} = ${PAYER_ROLE_ADMIN})`,
|
||||
),
|
||||
)
|
||||
.groupBy(
|
||||
contas.id,
|
||||
contas.name,
|
||||
contas.accountType,
|
||||
contas.status,
|
||||
contas.logo,
|
||||
contas.initialBalance,
|
||||
contas.excludeFromBalance,
|
||||
financialAccounts.id,
|
||||
financialAccounts.name,
|
||||
financialAccounts.accountType,
|
||||
financialAccounts.status,
|
||||
financialAccounts.logo,
|
||||
financialAccounts.initialBalance,
|
||||
financialAccounts.excludeFromBalance,
|
||||
);
|
||||
|
||||
const accounts = rows
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use server";
|
||||
|
||||
import { and, asc, eq } from "drizzle-orm";
|
||||
import { lancamentos } from "@/db/schema";
|
||||
import { transactions } from "@/db/schema";
|
||||
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 { toDateOnlyString } from "@/shared/utils/date";
|
||||
import { safeToNumber as toNumber } from "@/shared/utils/number";
|
||||
|
||||
@@ -37,33 +37,33 @@ export async function fetchDashboardBills(
|
||||
userId: string,
|
||||
period: string,
|
||||
): Promise<DashboardBillsSnapshot> {
|
||||
const adminPagadorId = await getAdminPagadorId(userId);
|
||||
if (!adminPagadorId) {
|
||||
const adminPayerId = await getAdminPayerId(userId);
|
||||
if (!adminPayerId) {
|
||||
return { bills: [], totalPendingAmount: 0, pendingCount: 0 };
|
||||
}
|
||||
|
||||
const rows = await db
|
||||
.select({
|
||||
id: lancamentos.id,
|
||||
name: lancamentos.name,
|
||||
amount: lancamentos.amount,
|
||||
dueDate: lancamentos.dueDate,
|
||||
boletoPaymentDate: lancamentos.boletoPaymentDate,
|
||||
isSettled: lancamentos.isSettled,
|
||||
id: transactions.id,
|
||||
name: transactions.name,
|
||||
amount: transactions.amount,
|
||||
dueDate: transactions.dueDate,
|
||||
boletoPaymentDate: transactions.boletoPaymentDate,
|
||||
isSettled: transactions.isSettled,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.from(transactions)
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, userId),
|
||||
eq(lancamentos.period, period),
|
||||
eq(lancamentos.paymentMethod, PAYMENT_METHOD_BOLETO),
|
||||
eq(lancamentos.pagadorId, adminPagadorId),
|
||||
eq(transactions.userId, userId),
|
||||
eq(transactions.period, period),
|
||||
eq(transactions.paymentMethod, PAYMENT_METHOD_BOLETO),
|
||||
eq(transactions.payerId, adminPayerId),
|
||||
),
|
||||
)
|
||||
.orderBy(
|
||||
asc(lancamentos.isSettled),
|
||||
asc(lancamentos.dueDate),
|
||||
asc(lancamentos.name),
|
||||
asc(transactions.isSettled),
|
||||
asc(transactions.dueDate),
|
||||
asc(transactions.name),
|
||||
);
|
||||
|
||||
const bills = rows.map((row: RawDashboardBill): DashboardBill => {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
widgetsConfig,
|
||||
} from "@/features/dashboard/widgets/widgets-config";
|
||||
import { NoteDialog } from "@/features/notes/components/note-dialog";
|
||||
import { LancamentoDialog } from "@/features/transactions/components/dialogs/transaction-dialog/transaction-dialog";
|
||||
import { TransactionDialog } from "@/features/transactions/components/dialogs/transaction-dialog/transaction-dialog";
|
||||
import type { SelectOption } from "@/features/transactions/components/types";
|
||||
import { ExpandableWidgetCard } from "@/shared/components/expandable-widget-card";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
@@ -48,12 +48,12 @@ type DashboardGridEditableProps = {
|
||||
period: string;
|
||||
initialPreferences: WidgetPreferences | null;
|
||||
quickActionOptions: {
|
||||
pagadorOptions: SelectOption[];
|
||||
splitPagadorOptions: SelectOption[];
|
||||
defaultPagadorId: string | null;
|
||||
contaOptions: SelectOption[];
|
||||
cartaoOptions: SelectOption[];
|
||||
categoriaOptions: SelectOption[];
|
||||
payerOptions: SelectOption[];
|
||||
splitPayerOptions: SelectOption[];
|
||||
defaultPayerId: string | null;
|
||||
accountOptions: SelectOption[];
|
||||
cardOptions: SelectOption[];
|
||||
categoryOptions: SelectOption[];
|
||||
estabelecimentos: string[];
|
||||
};
|
||||
};
|
||||
@@ -203,14 +203,14 @@ export function DashboardGridEditable({
|
||||
Ações rápidas
|
||||
</span>
|
||||
<div className="-mb-1 grid w-full grid-cols-3 gap-1 pb-1 sm:mb-0 sm:flex sm:w-auto sm:items-center sm:gap-2 sm:overflow-visible sm:pb-0">
|
||||
<LancamentoDialog
|
||||
<TransactionDialog
|
||||
mode="create"
|
||||
pagadorOptions={quickActionOptions.pagadorOptions}
|
||||
splitPagadorOptions={quickActionOptions.splitPagadorOptions}
|
||||
defaultPagadorId={quickActionOptions.defaultPagadorId}
|
||||
contaOptions={quickActionOptions.contaOptions}
|
||||
cartaoOptions={quickActionOptions.cartaoOptions}
|
||||
categoriaOptions={quickActionOptions.categoriaOptions}
|
||||
payerOptions={quickActionOptions.payerOptions}
|
||||
splitPayerOptions={quickActionOptions.splitPayerOptions}
|
||||
defaultPayerId={quickActionOptions.defaultPayerId}
|
||||
accountOptions={quickActionOptions.accountOptions}
|
||||
cardOptions={quickActionOptions.cardOptions}
|
||||
categoryOptions={quickActionOptions.categoryOptions}
|
||||
estabelecimentos={quickActionOptions.estabelecimentos}
|
||||
defaultPeriod={period}
|
||||
defaultTransactionType="Receita"
|
||||
@@ -228,14 +228,14 @@ export function DashboardGridEditable({
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<LancamentoDialog
|
||||
<TransactionDialog
|
||||
mode="create"
|
||||
pagadorOptions={quickActionOptions.pagadorOptions}
|
||||
splitPagadorOptions={quickActionOptions.splitPagadorOptions}
|
||||
defaultPagadorId={quickActionOptions.defaultPagadorId}
|
||||
contaOptions={quickActionOptions.contaOptions}
|
||||
cartaoOptions={quickActionOptions.cartaoOptions}
|
||||
categoriaOptions={quickActionOptions.categoriaOptions}
|
||||
payerOptions={quickActionOptions.payerOptions}
|
||||
splitPayerOptions={quickActionOptions.splitPayerOptions}
|
||||
defaultPayerId={quickActionOptions.defaultPayerId}
|
||||
accountOptions={quickActionOptions.accountOptions}
|
||||
cardOptions={quickActionOptions.cardOptions}
|
||||
categoryOptions={quickActionOptions.categoryOptions}
|
||||
estabelecimentos={quickActionOptions.estabelecimentos}
|
||||
defaultPeriod={period}
|
||||
defaultTransactionType="Despesa"
|
||||
|
||||
@@ -76,7 +76,7 @@ export function InvoiceListItem({ invoice, onPay }: InvoiceListItemProps) {
|
||||
{breakdown.map((share, index) => (
|
||||
<li
|
||||
key={`${invoice.id}-${
|
||||
share.pagadorId ?? share.pagadorName ?? index
|
||||
share.payerId ?? share.pagadorName ?? index
|
||||
}`}
|
||||
className="flex items-center gap-3"
|
||||
>
|
||||
|
||||
@@ -146,7 +146,7 @@ export function InvoicePaymentDialog({
|
||||
<div className="mb-2 flex items-center gap-2 text-muted-foreground">
|
||||
<RiMoneyDollarCircleLine className="size-4" />
|
||||
<span className="text-xs font-semibold uppercase">
|
||||
Valor da Fatura
|
||||
Valor da Invoice
|
||||
</span>
|
||||
</div>
|
||||
<MoneyValues
|
||||
|
||||
@@ -55,12 +55,14 @@ export function MyAccountsWidget({
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-3">
|
||||
<div className="relative size-10 overflow-hidden">
|
||||
<Image
|
||||
src={logoSrc}
|
||||
alt={`Logo da conta ${account.name}`}
|
||||
fill
|
||||
className="object-contain rounded-full"
|
||||
/>
|
||||
{logoSrc ? (
|
||||
<Image
|
||||
src={logoSrc}
|
||||
alt={`Logo da conta ${account.name}`}
|
||||
fill
|
||||
className="object-contain rounded-full"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="min-w-0">
|
||||
|
||||
@@ -21,7 +21,7 @@ import { getAvatarSrc } from "@/shared/lib/payers/utils";
|
||||
import { formatPercentage } from "@/shared/utils/percentage";
|
||||
|
||||
type PayersWidgetProps = {
|
||||
pagadores: DashboardPagador[];
|
||||
payers: DashboardPagador[];
|
||||
};
|
||||
|
||||
const buildInitials = (value: string) => {
|
||||
@@ -38,10 +38,10 @@ const buildInitials = (value: string) => {
|
||||
return `${firstChar}${secondChar}`.toUpperCase() || "??";
|
||||
};
|
||||
|
||||
export function PayersWidget({ pagadores }: PayersWidgetProps) {
|
||||
export function PayersWidget({ payers }: PayersWidgetProps) {
|
||||
return (
|
||||
<CardContent className="flex flex-col gap-4 px-0">
|
||||
{pagadores.length === 0 ? (
|
||||
{payers.length === 0 ? (
|
||||
<WidgetEmptyState
|
||||
icon={<RiGroupLine className="size-6 text-muted-foreground" />}
|
||||
title="Nenhum pagador para o período"
|
||||
@@ -49,25 +49,25 @@ export function PayersWidget({ pagadores }: PayersWidgetProps) {
|
||||
/>
|
||||
) : (
|
||||
<ul className="flex flex-col">
|
||||
{pagadores.map((pagador) => {
|
||||
const initials = buildInitials(pagador.name);
|
||||
{payers.map((payer) => {
|
||||
const initials = buildInitials(payer.name);
|
||||
const hasValidPercentageChange =
|
||||
typeof pagador.percentageChange === "number" &&
|
||||
Number.isFinite(pagador.percentageChange);
|
||||
typeof payer.percentageChange === "number" &&
|
||||
Number.isFinite(payer.percentageChange);
|
||||
const percentageChange = hasValidPercentageChange
|
||||
? pagador.percentageChange
|
||||
? payer.percentageChange
|
||||
: null;
|
||||
|
||||
return (
|
||||
<li
|
||||
key={pagador.id}
|
||||
key={payer.id}
|
||||
className="flex items-center justify-between border-b border-dashed last:border-b-0 last:pb-0"
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2 py-2">
|
||||
<Avatar className="size-10 shrink-0">
|
||||
<AvatarImage
|
||||
src={getAvatarSrc(pagador.avatarUrl)}
|
||||
alt={`Avatar de ${pagador.name}`}
|
||||
src={getAvatarSrc(payer.avatarUrl)}
|
||||
alt={`Avatar de ${payer.name}`}
|
||||
/>
|
||||
<AvatarFallback>{initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
@@ -75,13 +75,11 @@ export function PayersWidget({ pagadores }: PayersWidgetProps) {
|
||||
<div className="min-w-0">
|
||||
<Link
|
||||
prefetch
|
||||
href={`/payers/${pagador.id}`}
|
||||
href={`/payers/${payer.id}`}
|
||||
className="inline-flex max-w-full items-center gap-1 text-sm text-foreground underline-offset-2 hover:text-primary hover:underline"
|
||||
>
|
||||
<span className="truncate font-medium">
|
||||
{pagador.name}
|
||||
</span>
|
||||
{pagador.isAdmin && (
|
||||
<span className="truncate font-medium">{payer.name}</span>
|
||||
{payer.isAdmin && (
|
||||
<RiVerifiedBadgeFill
|
||||
className="size-4 shrink-0 text-blue-500"
|
||||
aria-hidden
|
||||
@@ -93,13 +91,13 @@ export function PayersWidget({ pagadores }: PayersWidgetProps) {
|
||||
/>
|
||||
</Link>
|
||||
<p className="truncate text-xs text-muted-foreground">
|
||||
{pagador.email ?? "Sem email cadastrado"}
|
||||
{payer.email ?? "Sem email cadastrado"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 flex-col items-end">
|
||||
<MoneyValues amount={pagador.totalExpenses} />
|
||||
<MoneyValues amount={payer.totalExpenses} />
|
||||
{percentageChange !== null && (
|
||||
<span
|
||||
className={`flex items-center gap-0.5 text-xs ${
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { and, asc, eq, gte, lte, ne, sum } from "drizzle-orm";
|
||||
import { contas, lancamentos } from "@/db/schema";
|
||||
import { financialAccounts, transactions } from "@/db/schema";
|
||||
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 { safeToNumber } from "@/shared/utils/number";
|
||||
import {
|
||||
addMonthsToPeriod,
|
||||
@@ -71,8 +71,8 @@ export async function fetchDashboardCardMetrics(
|
||||
): Promise<DashboardCardMetrics> {
|
||||
const previousPeriod = getPreviousPeriod(period);
|
||||
|
||||
const adminPagadorId = await getAdminPagadorId(userId);
|
||||
if (!adminPagadorId) {
|
||||
const adminPayerId = await getAdminPayerId(userId);
|
||||
if (!adminPayerId) {
|
||||
return {
|
||||
period,
|
||||
previousPeriod,
|
||||
@@ -88,24 +88,27 @@ export async function fetchDashboardCardMetrics(
|
||||
|
||||
const rows = await db
|
||||
.select({
|
||||
period: lancamentos.period,
|
||||
transactionType: lancamentos.transactionType,
|
||||
totalAmount: sum(lancamentos.amount).as("total"),
|
||||
period: transactions.period,
|
||||
transactionType: transactions.transactionType,
|
||||
totalAmount: sum(transactions.amount).as("total"),
|
||||
})
|
||||
.from(lancamentos)
|
||||
.leftJoin(contas, eq(lancamentos.contaId, contas.id))
|
||||
.from(transactions)
|
||||
.leftJoin(
|
||||
financialAccounts,
|
||||
eq(transactions.accountId, financialAccounts.id),
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
...buildDashboardAdminFilters({ userId, adminPagadorId }),
|
||||
gte(lancamentos.period, startPeriod),
|
||||
lte(lancamentos.period, period),
|
||||
ne(lancamentos.transactionType, TRANSFERENCIA),
|
||||
...buildDashboardAdminFilters({ userId, adminPayerId }),
|
||||
gte(transactions.period, startPeriod),
|
||||
lte(transactions.period, period),
|
||||
ne(transactions.transactionType, TRANSFERENCIA),
|
||||
excludeAutoInvoiceEntries(),
|
||||
excludeInitialBalanceWhenConfigured(),
|
||||
),
|
||||
)
|
||||
.groupBy(lancamentos.period, lancamentos.transactionType)
|
||||
.orderBy(asc(lancamentos.period), asc(lancamentos.transactionType));
|
||||
.groupBy(transactions.period, transactions.transactionType)
|
||||
.orderBy(asc(transactions.period), asc(transactions.transactionType));
|
||||
|
||||
const periodTotals = new Map<string, PeriodTotals>();
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { and, eq, isNotNull, isNull, or, sql } from "drizzle-orm";
|
||||
import { cartoes, lancamentos, pagadores } from "@/db/schema";
|
||||
import { cards, payers, 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 { PAGADOR_ROLE_ADMIN } from "@/shared/lib/payers/constants";
|
||||
import { PAYER_ROLE_ADMIN } from "@/shared/lib/payers/constants";
|
||||
import {
|
||||
buildDateOnlyStringFromPeriodDay,
|
||||
parseLocalDateString,
|
||||
@@ -46,7 +46,7 @@ export type InstallmentGroup = {
|
||||
seriesId: string;
|
||||
name: string;
|
||||
paymentMethod: string;
|
||||
cartaoId: string | null;
|
||||
cardId: string | null;
|
||||
cartaoName: string | null;
|
||||
cartaoDueDay: string | null;
|
||||
cartaoLogo: string | null;
|
||||
@@ -68,44 +68,44 @@ export async function fetchInstallmentAnalysis(
|
||||
// 1. Buscar todos os lançamentos parcelados não antecipados do pagador admin
|
||||
const installmentRows = await db
|
||||
.select({
|
||||
id: lancamentos.id,
|
||||
seriesId: lancamentos.seriesId,
|
||||
name: lancamentos.name,
|
||||
amount: lancamentos.amount,
|
||||
paymentMethod: lancamentos.paymentMethod,
|
||||
currentInstallment: lancamentos.currentInstallment,
|
||||
installmentCount: lancamentos.installmentCount,
|
||||
dueDate: lancamentos.dueDate,
|
||||
period: lancamentos.period,
|
||||
isAnticipated: lancamentos.isAnticipated,
|
||||
isSettled: lancamentos.isSettled,
|
||||
purchaseDate: lancamentos.purchaseDate,
|
||||
cartaoId: lancamentos.cartaoId,
|
||||
cartaoName: cartoes.name,
|
||||
cartaoDueDay: cartoes.dueDay,
|
||||
cartaoLogo: cartoes.logo,
|
||||
id: transactions.id,
|
||||
seriesId: transactions.seriesId,
|
||||
name: transactions.name,
|
||||
amount: transactions.amount,
|
||||
paymentMethod: transactions.paymentMethod,
|
||||
currentInstallment: transactions.currentInstallment,
|
||||
installmentCount: transactions.installmentCount,
|
||||
dueDate: transactions.dueDate,
|
||||
period: transactions.period,
|
||||
isAnticipated: transactions.isAnticipated,
|
||||
isSettled: transactions.isSettled,
|
||||
purchaseDate: transactions.purchaseDate,
|
||||
cardId: transactions.cardId,
|
||||
cartaoName: cards.name,
|
||||
cartaoDueDay: cards.dueDay,
|
||||
cartaoLogo: cards.logo,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.leftJoin(cartoes, eq(lancamentos.cartaoId, cartoes.id))
|
||||
.leftJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
|
||||
.from(transactions)
|
||||
.leftJoin(cards, eq(transactions.cardId, cards.id))
|
||||
.leftJoin(payers, eq(transactions.payerId, payers.id))
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, userId),
|
||||
eq(lancamentos.transactionType, "Despesa"),
|
||||
eq(lancamentos.condition, "Parcelado"),
|
||||
eq(lancamentos.isAnticipated, false),
|
||||
isNotNull(lancamentos.seriesId),
|
||||
eq(pagadores.role, PAGADOR_ROLE_ADMIN),
|
||||
eq(transactions.userId, userId),
|
||||
eq(transactions.transactionType, "Despesa"),
|
||||
eq(transactions.condition, "Parcelado"),
|
||||
eq(transactions.isAnticipated, false),
|
||||
isNotNull(transactions.seriesId),
|
||||
eq(payers.role, PAYER_ROLE_ADMIN),
|
||||
or(
|
||||
isNull(lancamentos.note),
|
||||
isNull(transactions.note),
|
||||
and(
|
||||
sql`${lancamentos.note} != ${INITIAL_BALANCE_NOTE}`,
|
||||
sql`${lancamentos.note} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`,
|
||||
sql`${transactions.note} != ${INITIAL_BALANCE_NOTE}`,
|
||||
sql`${transactions.note} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.orderBy(lancamentos.purchaseDate, lancamentos.currentInstallment);
|
||||
.orderBy(transactions.purchaseDate, transactions.currentInstallment);
|
||||
|
||||
// Agrupar por seriesId
|
||||
const seriesMap = new Map<string, InstallmentGroup>();
|
||||
@@ -140,7 +140,7 @@ export async function fetchInstallmentAnalysis(
|
||||
seriesId: row.seriesId,
|
||||
name: row.name,
|
||||
paymentMethod: row.paymentMethod,
|
||||
cartaoId: row.cartaoId,
|
||||
cardId: row.cardId,
|
||||
cartaoName: row.cartaoName,
|
||||
cartaoDueDay: row.cartaoDueDay,
|
||||
cartaoLogo: row.cartaoLogo,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { and, desc, eq, isNull, or, sql } from "drizzle-orm";
|
||||
import { lancamentos } from "@/db/schema";
|
||||
import { 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 { getAdminPagadorId } from "@/shared/lib/payers/get-admin-id";
|
||||
import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
|
||||
import { safeToNumber as toNumber } from "@/shared/utils/number";
|
||||
|
||||
export type InstallmentExpense = {
|
||||
@@ -28,42 +28,42 @@ export async function fetchInstallmentExpenses(
|
||||
userId: string,
|
||||
period: string,
|
||||
): Promise<InstallmentExpensesData> {
|
||||
const adminPagadorId = await getAdminPagadorId(userId);
|
||||
if (!adminPagadorId) {
|
||||
const adminPayerId = await getAdminPayerId(userId);
|
||||
if (!adminPayerId) {
|
||||
return { expenses: [] };
|
||||
}
|
||||
|
||||
const rows = await db
|
||||
.select({
|
||||
id: lancamentos.id,
|
||||
name: lancamentos.name,
|
||||
amount: lancamentos.amount,
|
||||
paymentMethod: lancamentos.paymentMethod,
|
||||
currentInstallment: lancamentos.currentInstallment,
|
||||
installmentCount: lancamentos.installmentCount,
|
||||
dueDate: lancamentos.dueDate,
|
||||
purchaseDate: lancamentos.purchaseDate,
|
||||
period: lancamentos.period,
|
||||
id: transactions.id,
|
||||
name: transactions.name,
|
||||
amount: transactions.amount,
|
||||
paymentMethod: transactions.paymentMethod,
|
||||
currentInstallment: transactions.currentInstallment,
|
||||
installmentCount: transactions.installmentCount,
|
||||
dueDate: transactions.dueDate,
|
||||
purchaseDate: transactions.purchaseDate,
|
||||
period: transactions.period,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.from(transactions)
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, userId),
|
||||
eq(lancamentos.period, period),
|
||||
eq(lancamentos.transactionType, "Despesa"),
|
||||
eq(lancamentos.condition, "Parcelado"),
|
||||
eq(lancamentos.isAnticipated, false),
|
||||
eq(lancamentos.pagadorId, adminPagadorId),
|
||||
eq(transactions.userId, userId),
|
||||
eq(transactions.period, period),
|
||||
eq(transactions.transactionType, "Despesa"),
|
||||
eq(transactions.condition, "Parcelado"),
|
||||
eq(transactions.isAnticipated, false),
|
||||
eq(transactions.payerId, adminPayerId),
|
||||
or(
|
||||
isNull(lancamentos.note),
|
||||
isNull(transactions.note),
|
||||
and(
|
||||
sql`${lancamentos.note} != ${INITIAL_BALANCE_NOTE}`,
|
||||
sql`${lancamentos.note} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`,
|
||||
sql`${transactions.note} != ${INITIAL_BALANCE_NOTE}`,
|
||||
sql`${transactions.note} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.orderBy(desc(lancamentos.purchaseDate), desc(lancamentos.createdAt));
|
||||
.orderBy(desc(transactions.purchaseDate), desc(transactions.createdAt));
|
||||
|
||||
type InstallmentExpenseRow = (typeof rows)[number];
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { and, desc, eq, isNull, or, sql } from "drizzle-orm";
|
||||
import { lancamentos } from "@/db/schema";
|
||||
import { 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 { getAdminPagadorId } from "@/shared/lib/payers/get-admin-id";
|
||||
import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
|
||||
import { safeToNumber as toNumber } from "@/shared/utils/number";
|
||||
|
||||
export type RecurringExpense = {
|
||||
@@ -24,37 +24,37 @@ export async function fetchRecurringExpenses(
|
||||
userId: string,
|
||||
period: string,
|
||||
): Promise<RecurringExpensesData> {
|
||||
const adminPagadorId = await getAdminPagadorId(userId);
|
||||
if (!adminPagadorId) {
|
||||
const adminPayerId = await getAdminPayerId(userId);
|
||||
if (!adminPayerId) {
|
||||
return { expenses: [] };
|
||||
}
|
||||
|
||||
const results = await db
|
||||
.select({
|
||||
id: lancamentos.id,
|
||||
name: lancamentos.name,
|
||||
amount: lancamentos.amount,
|
||||
paymentMethod: lancamentos.paymentMethod,
|
||||
recurrenceCount: lancamentos.recurrenceCount,
|
||||
id: transactions.id,
|
||||
name: transactions.name,
|
||||
amount: transactions.amount,
|
||||
paymentMethod: transactions.paymentMethod,
|
||||
recurrenceCount: transactions.recurrenceCount,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.from(transactions)
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, userId),
|
||||
eq(lancamentos.period, period),
|
||||
eq(lancamentos.transactionType, "Despesa"),
|
||||
eq(lancamentos.condition, "Recorrente"),
|
||||
eq(lancamentos.pagadorId, adminPagadorId),
|
||||
eq(transactions.userId, userId),
|
||||
eq(transactions.period, period),
|
||||
eq(transactions.transactionType, "Despesa"),
|
||||
eq(transactions.condition, "Recorrente"),
|
||||
eq(transactions.payerId, adminPayerId),
|
||||
or(
|
||||
isNull(lancamentos.note),
|
||||
isNull(transactions.note),
|
||||
and(
|
||||
sql`${lancamentos.note} != ${INITIAL_BALANCE_NOTE}`,
|
||||
sql`${lancamentos.note} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`,
|
||||
sql`${transactions.note} != ${INITIAL_BALANCE_NOTE}`,
|
||||
sql`${transactions.note} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.orderBy(desc(lancamentos.purchaseDate), desc(lancamentos.createdAt));
|
||||
.orderBy(desc(transactions.purchaseDate), desc(transactions.createdAt));
|
||||
|
||||
const expenses = results.map(
|
||||
(row): RecurringExpense => ({
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { and, asc, eq } from "drizzle-orm";
|
||||
import { cartoes, contas, lancamentos } from "@/db/schema";
|
||||
import { cards, financialAccounts, transactions } from "@/db/schema";
|
||||
import {
|
||||
buildDashboardAdminPeriodFilters,
|
||||
excludeAutoGeneratedEntryNotes,
|
||||
} 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 { safeToNumber as toNumber } from "@/shared/utils/number";
|
||||
|
||||
export type TopExpense = {
|
||||
@@ -26,8 +26,8 @@ export async function fetchTopExpenses(
|
||||
period: string,
|
||||
cardOnly: boolean = false,
|
||||
): Promise<TopExpensesData> {
|
||||
const adminPagadorId = await getAdminPagadorId(userId);
|
||||
if (!adminPagadorId) {
|
||||
const adminPayerId = await getAdminPayerId(userId);
|
||||
if (!adminPayerId) {
|
||||
return { expenses: [] };
|
||||
}
|
||||
|
||||
@@ -35,34 +35,37 @@ export async function fetchTopExpenses(
|
||||
...buildDashboardAdminPeriodFilters({
|
||||
userId,
|
||||
period,
|
||||
adminPagadorId,
|
||||
adminPayerId,
|
||||
}),
|
||||
eq(lancamentos.transactionType, "Despesa"),
|
||||
eq(transactions.transactionType, "Despesa"),
|
||||
excludeAutoGeneratedEntryNotes(),
|
||||
];
|
||||
|
||||
// Se cardOnly for true, filtra apenas pagamentos com cartão
|
||||
if (cardOnly) {
|
||||
conditions.push(eq(lancamentos.paymentMethod, "Cartão de Crédito"));
|
||||
conditions.push(eq(transactions.paymentMethod, "Cartão de Crédito"));
|
||||
}
|
||||
|
||||
const results = await db
|
||||
.select({
|
||||
id: lancamentos.id,
|
||||
name: lancamentos.name,
|
||||
amount: lancamentos.amount,
|
||||
purchaseDate: lancamentos.purchaseDate,
|
||||
paymentMethod: lancamentos.paymentMethod,
|
||||
cartaoId: lancamentos.cartaoId,
|
||||
contaId: lancamentos.contaId,
|
||||
cardLogo: cartoes.logo,
|
||||
accountLogo: contas.logo,
|
||||
id: transactions.id,
|
||||
name: transactions.name,
|
||||
amount: transactions.amount,
|
||||
purchaseDate: transactions.purchaseDate,
|
||||
paymentMethod: transactions.paymentMethod,
|
||||
cardId: transactions.cardId,
|
||||
accountId: transactions.accountId,
|
||||
cardLogo: cards.logo,
|
||||
accountLogo: financialAccounts.logo,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.leftJoin(cartoes, eq(lancamentos.cartaoId, cartoes.id))
|
||||
.leftJoin(contas, eq(lancamentos.contaId, contas.id))
|
||||
.from(transactions)
|
||||
.leftJoin(cards, eq(transactions.cardId, cards.id))
|
||||
.leftJoin(
|
||||
financialAccounts,
|
||||
eq(transactions.accountId, financialAccounts.id),
|
||||
)
|
||||
.where(and(...conditions))
|
||||
.orderBy(asc(lancamentos.amount))
|
||||
.orderBy(asc(transactions.amount))
|
||||
.limit(10);
|
||||
|
||||
const expenses = results.map(
|
||||
|
||||
@@ -11,7 +11,7 @@ import { fetchGoalsProgressData } from "./goals-progress-queries";
|
||||
import { fetchIncomeExpenseBalance } from "./income-expense-balance-queries";
|
||||
import { fetchDashboardInvoices } from "./invoices-queries";
|
||||
import { fetchDashboardNotes } from "./notes-queries";
|
||||
import { fetchDashboardPagadores } from "./payers-queries";
|
||||
import { fetchDashboardPayers } from "./payers-queries";
|
||||
import { fetchPaymentConditions } from "./payments/payment-conditions-queries";
|
||||
import { fetchPaymentMethods } from "./payments/payment-methods-queries";
|
||||
import { fetchPaymentStatus } from "./payments/payment-status-queries";
|
||||
@@ -49,7 +49,7 @@ async function fetchDashboardDataInternal(userId: string, period: string) {
|
||||
fetchGoalsProgressData(userId, period),
|
||||
fetchPaymentStatus(userId, period),
|
||||
fetchIncomeExpenseBalance(userId, period),
|
||||
fetchDashboardPagadores(userId, period),
|
||||
fetchDashboardPayers(userId, period),
|
||||
fetchDashboardNotes(userId),
|
||||
fetchPaymentConditions(userId, period),
|
||||
fetchPaymentMethods(userId, period),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { and, eq, ne, sql } from "drizzle-orm";
|
||||
import { categorias, lancamentos, orcamentos } from "@/db/schema";
|
||||
import { budgets, categories, transactions } from "@/db/schema";
|
||||
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 { safeToNumber as toNumber } from "@/shared/utils/number";
|
||||
|
||||
const BUDGET_CRITICAL_THRESHOLD = 80;
|
||||
@@ -49,9 +49,9 @@ export async function fetchGoalsProgressData(
|
||||
userId: string,
|
||||
period: string,
|
||||
): Promise<GoalsProgressData> {
|
||||
const adminPagadorId = await getAdminPagadorId(userId);
|
||||
const adminPayerId = await getAdminPayerId(userId);
|
||||
|
||||
if (!adminPagadorId) {
|
||||
if (!adminPayerId) {
|
||||
return {
|
||||
items: [],
|
||||
categories: [],
|
||||
@@ -64,45 +64,45 @@ export async function fetchGoalsProgressData(
|
||||
const [rows, categoryRows] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
orcamentoId: orcamentos.id,
|
||||
categoryId: categorias.id,
|
||||
categoryName: categorias.name,
|
||||
categoryIcon: categorias.icon,
|
||||
period: orcamentos.period,
|
||||
createdAt: orcamentos.createdAt,
|
||||
budgetAmount: orcamentos.amount,
|
||||
spentAmount: sql<number>`COALESCE(SUM(ABS(${lancamentos.amount})), 0)`,
|
||||
orcamentoId: budgets.id,
|
||||
categoryId: categories.id,
|
||||
categoryName: categories.name,
|
||||
categoryIcon: categories.icon,
|
||||
period: budgets.period,
|
||||
createdAt: budgets.createdAt,
|
||||
budgetAmount: budgets.amount,
|
||||
spentAmount: sql<number>`COALESCE(SUM(ABS(${transactions.amount})), 0)`,
|
||||
})
|
||||
.from(orcamentos)
|
||||
.innerJoin(categorias, eq(orcamentos.categoriaId, categorias.id))
|
||||
.from(budgets)
|
||||
.innerJoin(categories, eq(budgets.categoryId, categories.id))
|
||||
.leftJoin(
|
||||
lancamentos,
|
||||
transactions,
|
||||
and(
|
||||
eq(lancamentos.categoriaId, orcamentos.categoriaId),
|
||||
eq(lancamentos.userId, orcamentos.userId),
|
||||
eq(lancamentos.period, orcamentos.period),
|
||||
eq(lancamentos.pagadorId, adminPagadorId),
|
||||
eq(lancamentos.transactionType, "Despesa"),
|
||||
ne(lancamentos.condition, "cancelado"),
|
||||
eq(transactions.categoryId, budgets.categoryId),
|
||||
eq(transactions.userId, budgets.userId),
|
||||
eq(transactions.period, budgets.period),
|
||||
eq(transactions.payerId, adminPayerId),
|
||||
eq(transactions.transactionType, "Despesa"),
|
||||
ne(transactions.condition, "cancelado"),
|
||||
),
|
||||
)
|
||||
.where(and(eq(orcamentos.userId, userId), eq(orcamentos.period, period)))
|
||||
.where(and(eq(budgets.userId, userId), eq(budgets.period, period)))
|
||||
.groupBy(
|
||||
orcamentos.id,
|
||||
categorias.id,
|
||||
categorias.name,
|
||||
categorias.icon,
|
||||
orcamentos.period,
|
||||
orcamentos.createdAt,
|
||||
orcamentos.amount,
|
||||
budgets.id,
|
||||
categories.id,
|
||||
categories.name,
|
||||
categories.icon,
|
||||
budgets.period,
|
||||
budgets.createdAt,
|
||||
budgets.amount,
|
||||
),
|
||||
db.query.categorias.findMany({
|
||||
where: and(eq(categorias.userId, userId), eq(categorias.type, "despesa")),
|
||||
db.query.categories.findMany({
|
||||
where: and(eq(categories.userId, userId), eq(categories.type, "despesa")),
|
||||
orderBy: (category, { asc }) => [asc(category.name)],
|
||||
}),
|
||||
]);
|
||||
|
||||
const categories: GoalProgressCategory[] = categoryRows.map((category) => ({
|
||||
const categoryList: GoalProgressCategory[] = categoryRows.map((category) => ({
|
||||
id: category.id,
|
||||
name: category.name,
|
||||
icon: category.icon,
|
||||
@@ -139,7 +139,7 @@ export async function fetchGoalsProgressData(
|
||||
|
||||
return {
|
||||
items,
|
||||
categories,
|
||||
categories: categoryList,
|
||||
totalBudgets: items.length,
|
||||
exceededCount,
|
||||
criticalCount,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { and, eq, inArray, sql } from "drizzle-orm";
|
||||
import { contas, lancamentos } from "@/db/schema";
|
||||
import { financialAccounts, transactions } from "@/db/schema";
|
||||
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 { safeToNumber as toNumber } from "@/shared/utils/number";
|
||||
import {
|
||||
buildPeriodWindow,
|
||||
@@ -38,8 +38,8 @@ export async function fetchIncomeExpenseBalance(
|
||||
userId: string,
|
||||
currentPeriod: string,
|
||||
): Promise<IncomeExpenseBalanceData> {
|
||||
const adminPagadorId = await getAdminPagadorId(userId);
|
||||
if (!adminPagadorId) {
|
||||
const adminPayerId = await getAdminPayerId(userId);
|
||||
if (!adminPayerId) {
|
||||
return { months: [] };
|
||||
}
|
||||
|
||||
@@ -48,22 +48,25 @@ export async function fetchIncomeExpenseBalance(
|
||||
// Single query: GROUP BY period + transactionType instead of 12 separate queries
|
||||
const rows = await db
|
||||
.select({
|
||||
period: lancamentos.period,
|
||||
transactionType: lancamentos.transactionType,
|
||||
total: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`,
|
||||
period: transactions.period,
|
||||
transactionType: transactions.transactionType,
|
||||
total: sql<number>`coalesce(sum(${transactions.amount}), 0)`,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.leftJoin(contas, eq(lancamentos.contaId, contas.id))
|
||||
.from(transactions)
|
||||
.leftJoin(
|
||||
financialAccounts,
|
||||
eq(transactions.accountId, financialAccounts.id),
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
...buildDashboardAdminFilters({ userId, adminPagadorId }),
|
||||
inArray(lancamentos.period, periods),
|
||||
inArray(lancamentos.transactionType, ["Receita", "Despesa"]),
|
||||
...buildDashboardAdminFilters({ userId, adminPayerId }),
|
||||
inArray(transactions.period, periods),
|
||||
inArray(transactions.transactionType, ["Receita", "Despesa"]),
|
||||
excludeAutoInvoiceEntries(),
|
||||
excludeInitialBalanceWhenConfigured(),
|
||||
),
|
||||
)
|
||||
.groupBy(lancamentos.period, lancamentos.transactionType);
|
||||
.groupBy(transactions.period, transactions.transactionType);
|
||||
|
||||
// Build lookup from query results
|
||||
const dataMap = new Map<string, { income: number; expense: number }>();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { and, eq, ilike, isNotNull, sql } from "drizzle-orm";
|
||||
import { cartoes, faturas, lancamentos, pagadores } from "@/db/schema";
|
||||
import { cards, invoices, payers, transactions } from "@/db/schema";
|
||||
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/shared/lib/accounts/constants";
|
||||
import { db } from "@/shared/lib/db";
|
||||
import {
|
||||
@@ -28,14 +28,14 @@ type RawDashboardInvoice = {
|
||||
type RawInvoiceBreakdownRow = {
|
||||
cardId: string | null;
|
||||
period: string | null;
|
||||
pagadorId: string | null;
|
||||
payerId: string | null;
|
||||
pagadorName: string | null;
|
||||
pagadorAvatar: string | null;
|
||||
amount: number | string | null;
|
||||
};
|
||||
|
||||
export type InvoicePagadorBreakdown = {
|
||||
pagadorId: string | null;
|
||||
payerId: string | null;
|
||||
pagadorName: string;
|
||||
pagadorAvatar: string | null;
|
||||
amount: number;
|
||||
@@ -74,15 +74,15 @@ export async function fetchDashboardInvoices(
|
||||
): Promise<DashboardInvoicesSnapshot> {
|
||||
const paymentRows = await db
|
||||
.select({
|
||||
note: lancamentos.note,
|
||||
purchaseDate: lancamentos.purchaseDate,
|
||||
createdAt: lancamentos.createdAt,
|
||||
note: transactions.note,
|
||||
purchaseDate: transactions.purchaseDate,
|
||||
createdAt: transactions.createdAt,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.from(transactions)
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, userId),
|
||||
ilike(lancamentos.note, `${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`),
|
||||
eq(transactions.userId, userId),
|
||||
ilike(transactions.note, `${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -117,80 +117,77 @@ export async function fetchDashboardInvoices(
|
||||
}
|
||||
}
|
||||
|
||||
const [rows, breakdownRows]: [
|
||||
RawDashboardInvoice[],
|
||||
RawInvoiceBreakdownRow[],
|
||||
] = await Promise.all([
|
||||
const [rows, breakdownRows] = (await Promise.all([
|
||||
db
|
||||
.select({
|
||||
invoiceId: faturas.id,
|
||||
cardId: cartoes.id,
|
||||
cardName: cartoes.name,
|
||||
logo: cartoes.logo,
|
||||
dueDay: cartoes.dueDay,
|
||||
period: faturas.period,
|
||||
paymentStatus: faturas.paymentStatus,
|
||||
invoiceCreatedAt: faturas.createdAt,
|
||||
invoiceId: invoices.id,
|
||||
cardId: cards.id,
|
||||
cardName: cards.name,
|
||||
logo: cards.logo,
|
||||
dueDay: cards.dueDay,
|
||||
period: invoices.period,
|
||||
paymentStatus: invoices.paymentStatus,
|
||||
invoiceCreatedAt: invoices.createdAt,
|
||||
totalAmount: sql<number | null>`
|
||||
COALESCE(SUM(${lancamentos.amount}), 0)
|
||||
COALESCE(SUM(${transactions.amount}), 0)
|
||||
`,
|
||||
transactionCount: sql<number | null>`COUNT(${lancamentos.id})`,
|
||||
transactionCount: sql<number | null>`COUNT(${transactions.id})`,
|
||||
})
|
||||
.from(cartoes)
|
||||
.from(cards)
|
||||
.leftJoin(
|
||||
faturas,
|
||||
invoices,
|
||||
and(
|
||||
eq(faturas.cartaoId, cartoes.id),
|
||||
eq(faturas.userId, userId),
|
||||
eq(faturas.period, period),
|
||||
eq(invoices.cardId, cards.id),
|
||||
eq(invoices.userId, userId),
|
||||
eq(invoices.period, period),
|
||||
),
|
||||
)
|
||||
.leftJoin(
|
||||
lancamentos,
|
||||
transactions,
|
||||
and(
|
||||
eq(lancamentos.cartaoId, cartoes.id),
|
||||
eq(lancamentos.userId, userId),
|
||||
eq(lancamentos.period, period),
|
||||
eq(transactions.cardId, cards.id),
|
||||
eq(transactions.userId, userId),
|
||||
eq(transactions.period, period),
|
||||
),
|
||||
)
|
||||
.where(eq(cartoes.userId, userId))
|
||||
.where(eq(cards.userId, userId))
|
||||
.groupBy(
|
||||
faturas.id,
|
||||
cartoes.id,
|
||||
cartoes.name,
|
||||
cartoes.brand,
|
||||
cartoes.status,
|
||||
cartoes.logo,
|
||||
cartoes.dueDay,
|
||||
faturas.period,
|
||||
faturas.paymentStatus,
|
||||
invoices.id,
|
||||
cards.id,
|
||||
cards.name,
|
||||
cards.brand,
|
||||
cards.status,
|
||||
cards.logo,
|
||||
cards.dueDay,
|
||||
invoices.period,
|
||||
invoices.paymentStatus,
|
||||
),
|
||||
db
|
||||
.select({
|
||||
cardId: lancamentos.cartaoId,
|
||||
period: lancamentos.period,
|
||||
pagadorId: lancamentos.pagadorId,
|
||||
pagadorName: pagadores.name,
|
||||
pagadorAvatar: pagadores.avatarUrl,
|
||||
amount: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`,
|
||||
cardId: transactions.cardId,
|
||||
period: transactions.period,
|
||||
payerId: transactions.payerId,
|
||||
pagadorName: payers.name,
|
||||
pagadorAvatar: payers.avatarUrl,
|
||||
amount: sql<number>`coalesce(sum(${transactions.amount}), 0)`,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.leftJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
|
||||
.from(transactions)
|
||||
.leftJoin(payers, eq(transactions.payerId, payers.id))
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, userId),
|
||||
eq(lancamentos.period, period),
|
||||
isNotNull(lancamentos.cartaoId),
|
||||
eq(transactions.userId, userId),
|
||||
eq(transactions.period, period),
|
||||
isNotNull(transactions.cardId),
|
||||
),
|
||||
)
|
||||
.groupBy(
|
||||
lancamentos.cartaoId,
|
||||
lancamentos.period,
|
||||
lancamentos.pagadorId,
|
||||
pagadores.name,
|
||||
pagadores.avatarUrl,
|
||||
transactions.cardId,
|
||||
transactions.period,
|
||||
transactions.payerId,
|
||||
payers.name,
|
||||
payers.avatarUrl,
|
||||
),
|
||||
]);
|
||||
])) as [RawDashboardInvoice[], RawInvoiceBreakdownRow[]];
|
||||
|
||||
const breakdownMap = new Map<string, InvoicePagadorBreakdown[]>();
|
||||
for (const row of breakdownRows) {
|
||||
@@ -205,7 +202,7 @@ export async function fetchDashboardInvoices(
|
||||
const key = `${row.cardId}:${resolvedPeriod}`;
|
||||
const current = breakdownMap.get(key) ?? [];
|
||||
current.push({
|
||||
pagadorId: row.pagadorId ?? null,
|
||||
payerId: row.payerId ?? null,
|
||||
pagadorName: row.pagadorName?.trim() || "Sem pagador",
|
||||
pagadorAvatar: row.pagadorAvatar ?? null,
|
||||
amount,
|
||||
@@ -213,7 +210,7 @@ export async function fetchDashboardInvoices(
|
||||
breakdownMap.set(key, current);
|
||||
}
|
||||
|
||||
const invoices: DashboardInvoice[] = [];
|
||||
const invoiceList: DashboardInvoice[] = [];
|
||||
|
||||
for (const row of rows) {
|
||||
if (!row) {
|
||||
@@ -242,7 +239,7 @@ export async function fetchDashboardInvoices(
|
||||
? (paymentMap.get(paymentKey) ?? toDateOnlyString(row.invoiceCreatedAt))
|
||||
: null;
|
||||
|
||||
invoices.push({
|
||||
invoiceList.push({
|
||||
id: row.invoiceId ?? buildFallbackId(row.cardId, period),
|
||||
cardId: row.cardId,
|
||||
cardName: row.cardName,
|
||||
@@ -260,12 +257,12 @@ export async function fetchDashboardInvoices(
|
||||
});
|
||||
}
|
||||
|
||||
invoices.sort((a, b) => {
|
||||
invoiceList.sort((a, b) => {
|
||||
// Ordena do maior valor para o menor
|
||||
return Math.abs(b.totalAmount) - Math.abs(a.totalAmount);
|
||||
});
|
||||
|
||||
const totalPending = invoices.reduce((total, invoice) => {
|
||||
const totalPending = invoiceList.reduce((total, invoice) => {
|
||||
if (invoice.paymentStatus !== INVOICE_PAYMENT_STATUS.PENDING) {
|
||||
return total;
|
||||
}
|
||||
@@ -273,7 +270,7 @@ export async function fetchDashboardInvoices(
|
||||
}, 0);
|
||||
|
||||
return {
|
||||
invoices,
|
||||
invoices: invoiceList,
|
||||
totalPending,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ export const mapDashboardNoteToNote = (note: DashboardNote): Note => ({
|
||||
description: note.description,
|
||||
type: note.type,
|
||||
tasks: note.tasks,
|
||||
arquivada: note.arquivada,
|
||||
archived: note.archived,
|
||||
createdAt: note.createdAt,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { anotacoes } from "@/db/schema";
|
||||
import { notes } from "@/db/schema";
|
||||
import { db } from "@/shared/lib/db";
|
||||
|
||||
export type DashboardTask = {
|
||||
@@ -14,7 +14,7 @@ export type DashboardNote = {
|
||||
description: string;
|
||||
type: "nota" | "tarefa";
|
||||
tasks?: DashboardTask[];
|
||||
arquivada: boolean;
|
||||
archived: boolean;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
@@ -55,19 +55,19 @@ const parseTasks = (value: string | null): DashboardTask[] | undefined => {
|
||||
export async function fetchDashboardNotes(
|
||||
userId: string,
|
||||
): Promise<DashboardNote[]> {
|
||||
const notes = await db.query.anotacoes.findMany({
|
||||
where: and(eq(anotacoes.userId, userId), eq(anotacoes.arquivada, false)),
|
||||
const noteRows = await db.query.notes.findMany({
|
||||
where: and(eq(notes.userId, userId), eq(notes.archived, false)),
|
||||
orderBy: (note, { desc }) => [desc(note.createdAt)],
|
||||
limit: 5,
|
||||
});
|
||||
|
||||
return notes.map((note) => ({
|
||||
return noteRows.map((note) => ({
|
||||
id: note.id,
|
||||
title: (note.title ?? "").trim(),
|
||||
description: (note.description ?? "").trim(),
|
||||
type: (note.type ?? "nota") as "nota" | "tarefa",
|
||||
tasks: parseTasks(note.tasks),
|
||||
arquivada: note.arquivada,
|
||||
archived: note.archived,
|
||||
createdAt: note.createdAt.toISOString(),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
import { and, eq, lt, ne, sql } from "drizzle-orm";
|
||||
import {
|
||||
cartoes,
|
||||
categorias,
|
||||
faturas,
|
||||
lancamentos,
|
||||
orcamentos,
|
||||
budgets,
|
||||
cards,
|
||||
categories,
|
||||
invoices,
|
||||
transactions,
|
||||
} from "@/db/schema";
|
||||
import { db } from "@/shared/lib/db";
|
||||
import { INVOICE_PAYMENT_STATUS } from "@/shared/lib/invoices";
|
||||
import { getAdminPagadorId } from "@/shared/lib/payers/get-admin-id";
|
||||
import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
|
||||
import {
|
||||
buildDateOnlyStringFromPeriodDay,
|
||||
getBusinessDateString,
|
||||
@@ -67,128 +67,126 @@ export async function fetchDashboardNotifications(
|
||||
const today = getBusinessDateString();
|
||||
const DAYS_THRESHOLD = 5;
|
||||
|
||||
const adminPagadorId = await getAdminPagadorId(userId);
|
||||
const adminPayerId = await getAdminPayerId(userId);
|
||||
|
||||
// --- Faturas atrasadas (períodos anteriores) ---
|
||||
const overdueInvoices = await db
|
||||
.select({
|
||||
invoiceId: faturas.id,
|
||||
cardId: cartoes.id,
|
||||
cardName: cartoes.name,
|
||||
cardLogo: cartoes.logo,
|
||||
dueDay: cartoes.dueDay,
|
||||
period: faturas.period,
|
||||
invoiceId: invoices.id,
|
||||
cardId: cards.id,
|
||||
cardName: cards.name,
|
||||
cardLogo: cards.logo,
|
||||
dueDay: cards.dueDay,
|
||||
period: invoices.period,
|
||||
totalAmount: sql<number | null>`
|
||||
COALESCE(
|
||||
(SELECT SUM(${lancamentos.amount})
|
||||
FROM ${lancamentos}
|
||||
WHERE ${lancamentos.cartaoId} = ${cartoes.id}
|
||||
AND ${lancamentos.period} = ${faturas.period}
|
||||
AND ${lancamentos.userId} = ${faturas.userId}),
|
||||
(SELECT SUM(${transactions.amount})
|
||||
FROM ${transactions}
|
||||
WHERE ${transactions.cardId} = ${cards.id}
|
||||
AND ${transactions.period} = ${invoices.period}
|
||||
AND ${transactions.userId} = ${invoices.userId}),
|
||||
0
|
||||
)
|
||||
`,
|
||||
})
|
||||
.from(faturas)
|
||||
.innerJoin(cartoes, eq(faturas.cartaoId, cartoes.id))
|
||||
.from(invoices)
|
||||
.innerJoin(cards, eq(invoices.cardId, cards.id))
|
||||
.where(
|
||||
and(
|
||||
eq(faturas.userId, userId),
|
||||
eq(faturas.paymentStatus, INVOICE_PAYMENT_STATUS.PENDING),
|
||||
lt(faturas.period, currentPeriod),
|
||||
eq(invoices.userId, userId),
|
||||
eq(invoices.paymentStatus, INVOICE_PAYMENT_STATUS.PENDING),
|
||||
lt(invoices.period, currentPeriod),
|
||||
),
|
||||
);
|
||||
|
||||
// --- Faturas do período atual ---
|
||||
const currentInvoices = await db
|
||||
.select({
|
||||
invoiceId: faturas.id,
|
||||
cardId: cartoes.id,
|
||||
cardName: cartoes.name,
|
||||
cardLogo: cartoes.logo,
|
||||
dueDay: cartoes.dueDay,
|
||||
period: sql<string>`COALESCE(${faturas.period}, ${currentPeriod})`,
|
||||
paymentStatus: faturas.paymentStatus,
|
||||
invoiceId: invoices.id,
|
||||
cardId: cards.id,
|
||||
cardName: cards.name,
|
||||
cardLogo: cards.logo,
|
||||
dueDay: cards.dueDay,
|
||||
period: sql<string>`COALESCE(${invoices.period}, ${currentPeriod})`,
|
||||
paymentStatus: invoices.paymentStatus,
|
||||
totalAmount: sql<number | null>`
|
||||
COALESCE(SUM(${lancamentos.amount}), 0)
|
||||
COALESCE(SUM(${transactions.amount}), 0)
|
||||
`,
|
||||
transactionCount: sql<number | null>`COUNT(${lancamentos.id})`,
|
||||
transactionCount: sql<number | null>`COUNT(${transactions.id})`,
|
||||
})
|
||||
.from(cartoes)
|
||||
.from(cards)
|
||||
.leftJoin(
|
||||
faturas,
|
||||
invoices,
|
||||
and(
|
||||
eq(faturas.cartaoId, cartoes.id),
|
||||
eq(faturas.userId, userId),
|
||||
eq(faturas.period, currentPeriod),
|
||||
eq(invoices.cardId, cards.id),
|
||||
eq(invoices.userId, userId),
|
||||
eq(invoices.period, currentPeriod),
|
||||
),
|
||||
)
|
||||
.leftJoin(
|
||||
lancamentos,
|
||||
transactions,
|
||||
and(
|
||||
eq(lancamentos.cartaoId, cartoes.id),
|
||||
eq(lancamentos.userId, userId),
|
||||
eq(lancamentos.period, currentPeriod),
|
||||
eq(transactions.cardId, cards.id),
|
||||
eq(transactions.userId, userId),
|
||||
eq(transactions.period, currentPeriod),
|
||||
),
|
||||
)
|
||||
.where(eq(cartoes.userId, userId))
|
||||
.where(eq(cards.userId, userId))
|
||||
.groupBy(
|
||||
faturas.id,
|
||||
cartoes.id,
|
||||
cartoes.name,
|
||||
cartoes.logo,
|
||||
cartoes.dueDay,
|
||||
faturas.period,
|
||||
faturas.paymentStatus,
|
||||
invoices.id,
|
||||
cards.id,
|
||||
cards.name,
|
||||
cards.logo,
|
||||
cards.dueDay,
|
||||
invoices.period,
|
||||
invoices.paymentStatus,
|
||||
);
|
||||
|
||||
// --- Boletos não pagos ---
|
||||
const boletosConditions = [
|
||||
eq(lancamentos.userId, userId),
|
||||
eq(lancamentos.paymentMethod, PAYMENT_METHOD_BOLETO),
|
||||
eq(lancamentos.isSettled, false),
|
||||
eq(transactions.userId, userId),
|
||||
eq(transactions.paymentMethod, PAYMENT_METHOD_BOLETO),
|
||||
eq(transactions.isSettled, false),
|
||||
];
|
||||
if (adminPagadorId) {
|
||||
boletosConditions.push(eq(lancamentos.pagadorId, adminPagadorId));
|
||||
if (adminPayerId) {
|
||||
boletosConditions.push(eq(transactions.payerId, adminPayerId));
|
||||
}
|
||||
|
||||
const boletosRows = await db
|
||||
.select({
|
||||
id: lancamentos.id,
|
||||
name: lancamentos.name,
|
||||
amount: lancamentos.amount,
|
||||
dueDate: lancamentos.dueDate,
|
||||
period: lancamentos.period,
|
||||
id: transactions.id,
|
||||
name: transactions.name,
|
||||
amount: transactions.amount,
|
||||
dueDate: transactions.dueDate,
|
||||
period: transactions.period,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.from(transactions)
|
||||
.where(and(...boletosConditions));
|
||||
|
||||
// --- Orçamentos do período atual ---
|
||||
const budgetJoinConditions = [
|
||||
eq(lancamentos.categoriaId, orcamentos.categoriaId),
|
||||
eq(lancamentos.userId, orcamentos.userId),
|
||||
eq(lancamentos.period, orcamentos.period),
|
||||
eq(lancamentos.transactionType, "Despesa"),
|
||||
ne(lancamentos.condition, "cancelado"),
|
||||
eq(transactions.categoryId, budgets.categoryId),
|
||||
eq(transactions.userId, budgets.userId),
|
||||
eq(transactions.period, budgets.period),
|
||||
eq(transactions.transactionType, "Despesa"),
|
||||
ne(transactions.condition, "cancelado"),
|
||||
];
|
||||
if (adminPagadorId) {
|
||||
budgetJoinConditions.push(eq(lancamentos.pagadorId, adminPagadorId));
|
||||
if (adminPayerId) {
|
||||
budgetJoinConditions.push(eq(transactions.payerId, adminPayerId));
|
||||
}
|
||||
|
||||
const budgetRows = await db
|
||||
.select({
|
||||
orcamentoId: orcamentos.id,
|
||||
budgetAmount: orcamentos.amount,
|
||||
categoriaName: categorias.name,
|
||||
spentAmount: sql<number>`COALESCE(SUM(ABS(${lancamentos.amount})), 0)`,
|
||||
orcamentoId: budgets.id,
|
||||
budgetAmount: budgets.amount,
|
||||
categoriaName: categories.name,
|
||||
spentAmount: sql<number>`COALESCE(SUM(ABS(${transactions.amount})), 0)`,
|
||||
})
|
||||
.from(orcamentos)
|
||||
.innerJoin(categorias, eq(orcamentos.categoriaId, categorias.id))
|
||||
.leftJoin(lancamentos, and(...budgetJoinConditions))
|
||||
.where(
|
||||
and(eq(orcamentos.userId, userId), eq(orcamentos.period, currentPeriod)),
|
||||
)
|
||||
.groupBy(orcamentos.id, orcamentos.amount, categorias.name);
|
||||
.from(budgets)
|
||||
.innerJoin(categories, eq(budgets.categoryId, categories.id))
|
||||
.leftJoin(transactions, and(...budgetJoinConditions))
|
||||
.where(and(eq(budgets.userId, userId), eq(budgets.period, currentPeriod)))
|
||||
.groupBy(budgets.id, budgets.amount, categories.name);
|
||||
|
||||
// =====================
|
||||
// Processar notificações
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { and, desc, eq, inArray, isNull, or, sql } from "drizzle-orm";
|
||||
import { lancamentos, pagadores } from "@/db/schema";
|
||||
import { 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 { calculatePercentageChange } from "@/shared/utils/math";
|
||||
import { safeToNumber as toNumber } from "@/shared/utils/number";
|
||||
import { getPreviousPeriod } from "@/shared/utils/period";
|
||||
@@ -18,49 +18,49 @@ export type DashboardPagador = {
|
||||
isAdmin: boolean;
|
||||
};
|
||||
|
||||
export type DashboardPagadoresSnapshot = {
|
||||
pagadores: DashboardPagador[];
|
||||
export type DashboardPayersSnapshot = {
|
||||
payers: DashboardPagador[];
|
||||
totalExpenses: number;
|
||||
};
|
||||
|
||||
export async function fetchDashboardPagadores(
|
||||
export async function fetchDashboardPayers(
|
||||
userId: string,
|
||||
period: string,
|
||||
): Promise<DashboardPagadoresSnapshot> {
|
||||
): Promise<DashboardPayersSnapshot> {
|
||||
const previousPeriod = getPreviousPeriod(period);
|
||||
|
||||
const rows = await db
|
||||
.select({
|
||||
id: pagadores.id,
|
||||
name: pagadores.name,
|
||||
email: pagadores.email,
|
||||
avatarUrl: pagadores.avatarUrl,
|
||||
role: pagadores.role,
|
||||
period: lancamentos.period,
|
||||
totalExpenses: sql<number>`COALESCE(SUM(ABS(${lancamentos.amount})), 0)`,
|
||||
id: payers.id,
|
||||
name: payers.name,
|
||||
email: payers.email,
|
||||
avatarUrl: payers.avatarUrl,
|
||||
role: payers.role,
|
||||
period: transactions.period,
|
||||
totalExpenses: sql<number>`COALESCE(SUM(ABS(${transactions.amount})), 0)`,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
|
||||
.from(transactions)
|
||||
.innerJoin(payers, eq(transactions.payerId, payers.id))
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, userId),
|
||||
inArray(lancamentos.period, [period, previousPeriod]),
|
||||
eq(lancamentos.transactionType, "Despesa"),
|
||||
eq(transactions.userId, userId),
|
||||
inArray(transactions.period, [period, previousPeriod]),
|
||||
eq(transactions.transactionType, "Despesa"),
|
||||
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}%`}`,
|
||||
),
|
||||
),
|
||||
)
|
||||
.groupBy(
|
||||
pagadores.id,
|
||||
pagadores.name,
|
||||
pagadores.email,
|
||||
pagadores.avatarUrl,
|
||||
pagadores.role,
|
||||
lancamentos.period,
|
||||
payers.id,
|
||||
payers.name,
|
||||
payers.email,
|
||||
payers.avatarUrl,
|
||||
payers.role,
|
||||
transactions.period,
|
||||
)
|
||||
.orderBy(desc(sql`SUM(ABS(${lancamentos.amount}))`));
|
||||
.orderBy(desc(sql`SUM(ABS(${transactions.amount}))`));
|
||||
|
||||
const groupedPagadores = new Map<
|
||||
string,
|
||||
@@ -81,7 +81,7 @@ export async function fetchDashboardPagadores(
|
||||
name: row.name,
|
||||
email: row.email,
|
||||
avatarUrl: row.avatarUrl,
|
||||
isAdmin: row.role === PAGADOR_ROLE_ADMIN,
|
||||
isAdmin: row.role === PAYER_ROLE_ADMIN,
|
||||
currentExpenses: 0,
|
||||
previousExpenses: 0,
|
||||
};
|
||||
@@ -96,7 +96,7 @@ export async function fetchDashboardPagadores(
|
||||
groupedPagadores.set(row.id, entry);
|
||||
}
|
||||
|
||||
const pagadoresList = Array.from(groupedPagadores.values())
|
||||
const payerList = Array.from(groupedPagadores.values())
|
||||
.filter((p) => p.currentExpenses > 0)
|
||||
.map((pagador) => ({
|
||||
id: pagador.id,
|
||||
@@ -113,13 +113,13 @@ export async function fetchDashboardPagadores(
|
||||
}))
|
||||
.sort((a, b) => b.totalExpenses - a.totalExpenses);
|
||||
|
||||
const totalExpenses = pagadoresList.reduce(
|
||||
const totalExpenses = payerList.reduce(
|
||||
(sum, p) => sum + p.totalExpenses,
|
||||
0,
|
||||
);
|
||||
|
||||
return {
|
||||
pagadores: pagadoresList,
|
||||
payers: payerList,
|
||||
totalExpenses,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { and, eq, sql } from "drizzle-orm";
|
||||
import { lancamentos } from "@/db/schema";
|
||||
import { transactions } from "@/db/schema";
|
||||
import {
|
||||
buildDashboardAdminPeriodFilters,
|
||||
excludeAutoGeneratedEntryNotes,
|
||||
} 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 { safeToNumber as toNumber } from "@/shared/utils/number";
|
||||
|
||||
export type PaymentConditionSummary = {
|
||||
@@ -23,30 +23,30 @@ export async function fetchPaymentConditions(
|
||||
userId: string,
|
||||
period: string,
|
||||
): Promise<PaymentConditionsData> {
|
||||
const adminPagadorId = await getAdminPagadorId(userId);
|
||||
if (!adminPagadorId) {
|
||||
const adminPayerId = await getAdminPayerId(userId);
|
||||
if (!adminPayerId) {
|
||||
return { conditions: [] };
|
||||
}
|
||||
|
||||
const rows = await db
|
||||
.select({
|
||||
condition: lancamentos.condition,
|
||||
totalAmount: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`,
|
||||
transactions: sql<number>`count(${lancamentos.id})`,
|
||||
condition: transactions.condition,
|
||||
totalAmount: sql<number>`coalesce(sum(${transactions.amount}), 0)`,
|
||||
transactions: sql<number>`count(${transactions.id})`,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.from(transactions)
|
||||
.where(
|
||||
and(
|
||||
...buildDashboardAdminPeriodFilters({
|
||||
userId,
|
||||
period,
|
||||
adminPagadorId,
|
||||
adminPayerId,
|
||||
}),
|
||||
eq(lancamentos.transactionType, "Despesa"),
|
||||
eq(transactions.transactionType, "Despesa"),
|
||||
excludeAutoGeneratedEntryNotes(),
|
||||
),
|
||||
)
|
||||
.groupBy(lancamentos.condition);
|
||||
.groupBy(transactions.condition);
|
||||
|
||||
const summaries = rows.map((row: (typeof rows)[number]) => {
|
||||
const totalAmount = Math.abs(toNumber(row.totalAmount));
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { and, eq, sql } from "drizzle-orm";
|
||||
import { lancamentos } from "@/db/schema";
|
||||
import { transactions } from "@/db/schema";
|
||||
import {
|
||||
buildDashboardAdminPeriodFilters,
|
||||
excludeAutoGeneratedEntryNotes,
|
||||
} 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 { safeToNumber as toNumber } from "@/shared/utils/number";
|
||||
|
||||
export type PaymentMethodSummary = {
|
||||
@@ -23,30 +23,30 @@ export async function fetchPaymentMethods(
|
||||
userId: string,
|
||||
period: string,
|
||||
): Promise<PaymentMethodsData> {
|
||||
const adminPagadorId = await getAdminPagadorId(userId);
|
||||
if (!adminPagadorId) {
|
||||
const adminPayerId = await getAdminPayerId(userId);
|
||||
if (!adminPayerId) {
|
||||
return { methods: [] };
|
||||
}
|
||||
|
||||
const rows = await db
|
||||
.select({
|
||||
paymentMethod: lancamentos.paymentMethod,
|
||||
totalAmount: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`,
|
||||
transactions: sql<number>`count(${lancamentos.id})`,
|
||||
paymentMethod: transactions.paymentMethod,
|
||||
totalAmount: sql<number>`coalesce(sum(${transactions.amount}), 0)`,
|
||||
transactions: sql<number>`count(${transactions.id})`,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.from(transactions)
|
||||
.where(
|
||||
and(
|
||||
...buildDashboardAdminPeriodFilters({
|
||||
userId,
|
||||
period,
|
||||
adminPagadorId,
|
||||
adminPayerId,
|
||||
}),
|
||||
eq(lancamentos.transactionType, "Despesa"),
|
||||
eq(transactions.transactionType, "Despesa"),
|
||||
excludeAutoGeneratedEntryNotes(),
|
||||
),
|
||||
)
|
||||
.groupBy(lancamentos.paymentMethod);
|
||||
.groupBy(transactions.paymentMethod);
|
||||
|
||||
const summaries = rows.map((row: (typeof rows)[number]) => {
|
||||
const amount = Math.abs(toNumber(row.totalAmount));
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { and, inArray, sql } from "drizzle-orm";
|
||||
import { lancamentos } from "@/db/schema";
|
||||
import { transactions } from "@/db/schema";
|
||||
import {
|
||||
buildDashboardAdminPeriodFilters,
|
||||
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 { safeToNumber as toNumber } from "@/shared/utils/number";
|
||||
|
||||
export type PaymentStatusCategory = {
|
||||
@@ -29,41 +29,41 @@ export async function fetchPaymentStatus(
|
||||
userId: string,
|
||||
period: string,
|
||||
): Promise<PaymentStatusData> {
|
||||
const adminPagadorId = await getAdminPagadorId(userId);
|
||||
if (!adminPagadorId) {
|
||||
const adminPayerId = await getAdminPayerId(userId);
|
||||
if (!adminPayerId) {
|
||||
return { income: emptyCategory(), expenses: emptyCategory() };
|
||||
}
|
||||
|
||||
// Single query: GROUP BY transactionType instead of 2 separate queries
|
||||
const rows = await db
|
||||
.select({
|
||||
transactionType: lancamentos.transactionType,
|
||||
transactionType: transactions.transactionType,
|
||||
confirmed: sql<number>`
|
||||
coalesce(
|
||||
sum(case when ${lancamentos.isSettled} = true then ${lancamentos.amount} else 0 end),
|
||||
sum(case when ${transactions.isSettled} = true then ${transactions.amount} else 0 end),
|
||||
0
|
||||
)
|
||||
`,
|
||||
pending: sql<number>`
|
||||
coalesce(
|
||||
sum(case when ${lancamentos.isSettled} = false or ${lancamentos.isSettled} is null then ${lancamentos.amount} else 0 end),
|
||||
sum(case when ${transactions.isSettled} = false or ${transactions.isSettled} is null then ${transactions.amount} else 0 end),
|
||||
0
|
||||
)
|
||||
`,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.from(transactions)
|
||||
.where(
|
||||
and(
|
||||
...buildDashboardAdminPeriodFilters({
|
||||
userId,
|
||||
period,
|
||||
adminPagadorId,
|
||||
adminPayerId,
|
||||
}),
|
||||
inArray(lancamentos.transactionType, ["Receita", "Despesa"]),
|
||||
inArray(transactions.transactionType, ["Receita", "Despesa"]),
|
||||
excludeAutoInvoiceEntries(),
|
||||
),
|
||||
)
|
||||
.groupBy(lancamentos.transactionType);
|
||||
.groupBy(transactions.transactionType);
|
||||
|
||||
const result = { income: emptyCategory(), expenses: emptyCategory() };
|
||||
|
||||
|
||||
@@ -11,10 +11,10 @@ export async function fetchUserDashboardPreferences(
|
||||
): Promise<UserDashboardPreferences> {
|
||||
const result = await db
|
||||
.select({
|
||||
dashboardWidgets: schema.preferenciasUsuario.dashboardWidgets,
|
||||
dashboardWidgets: schema.userPreferences.dashboardWidgets,
|
||||
})
|
||||
.from(schema.preferenciasUsuario)
|
||||
.where(eq(schema.preferenciasUsuario.userId, userId))
|
||||
.from(schema.userPreferences)
|
||||
.where(eq(schema.userPreferences.userId, userId))
|
||||
.limit(1);
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { and, desc, eq, inArray } from "drizzle-orm";
|
||||
import { cartoes, categorias, contas, lancamentos } from "@/db/schema";
|
||||
import {
|
||||
cards,
|
||||
categories,
|
||||
financialAccounts,
|
||||
transactions,
|
||||
} from "@/db/schema";
|
||||
import {
|
||||
buildDashboardAdminPeriodFilters,
|
||||
excludeAutoGeneratedEntryNotes,
|
||||
} 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 { safeToNumber as toNumber } from "@/shared/utils/number";
|
||||
|
||||
export type CategoryOption = {
|
||||
@@ -45,39 +50,42 @@ export async function fetchPurchasesByCategory(
|
||||
userId: string,
|
||||
period: string,
|
||||
): Promise<PurchasesByCategoryData> {
|
||||
const adminPagadorId = await getAdminPagadorId(userId);
|
||||
if (!adminPagadorId) {
|
||||
const adminPayerId = await getAdminPayerId(userId);
|
||||
if (!adminPayerId) {
|
||||
return { categories: [], transactionsByCategory: {} };
|
||||
}
|
||||
|
||||
const transactionsRows = await db
|
||||
.select({
|
||||
id: lancamentos.id,
|
||||
name: lancamentos.name,
|
||||
amount: lancamentos.amount,
|
||||
purchaseDate: lancamentos.purchaseDate,
|
||||
categoryId: lancamentos.categoriaId,
|
||||
categoryName: categorias.name,
|
||||
categoryType: categorias.type,
|
||||
cardLogo: cartoes.logo,
|
||||
accountLogo: contas.logo,
|
||||
id: transactions.id,
|
||||
name: transactions.name,
|
||||
amount: transactions.amount,
|
||||
purchaseDate: transactions.purchaseDate,
|
||||
categoryId: transactions.categoryId,
|
||||
categoryName: categories.name,
|
||||
categoryType: categories.type,
|
||||
cardLogo: cards.logo,
|
||||
accountLogo: financialAccounts.logo,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.innerJoin(categorias, eq(lancamentos.categoriaId, categorias.id))
|
||||
.leftJoin(cartoes, eq(lancamentos.cartaoId, cartoes.id))
|
||||
.leftJoin(contas, eq(lancamentos.contaId, contas.id))
|
||||
.from(transactions)
|
||||
.innerJoin(categories, eq(transactions.categoryId, categories.id))
|
||||
.leftJoin(cards, eq(transactions.cardId, cards.id))
|
||||
.leftJoin(
|
||||
financialAccounts,
|
||||
eq(transactions.accountId, financialAccounts.id),
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
...buildDashboardAdminPeriodFilters({
|
||||
userId,
|
||||
period,
|
||||
adminPagadorId,
|
||||
adminPayerId,
|
||||
}),
|
||||
inArray(categorias.type, ["despesa", "receita"]),
|
||||
inArray(categories.type, ["despesa", "receita"]),
|
||||
excludeAutoGeneratedEntryNotes(),
|
||||
),
|
||||
)
|
||||
.orderBy(desc(lancamentos.purchaseDate));
|
||||
.orderBy(desc(transactions.purchaseDate));
|
||||
|
||||
const transactionsByCategory: Record<string, CategoryTransaction[]> = {};
|
||||
const categoriesMap = new Map<string, CategoryOption>();
|
||||
@@ -120,8 +128,8 @@ export async function fetchPurchasesByCategory(
|
||||
}
|
||||
}
|
||||
|
||||
// Ordena as categorias: receitas primeiro, depois despesas (alfabeticamente dentro de cada tipo)
|
||||
const categories = Array.from(categoriesMap.values()).sort((a, b) => {
|
||||
// Ordena as categories: receitas primeiro, depois despesas (alfabeticamente dentro de cada tipo)
|
||||
const categoryList = Array.from(categoriesMap.values()).sort((a, b) => {
|
||||
// Receita vem antes de despesa
|
||||
if (a.type !== b.type) {
|
||||
return a.type === "receita" ? -1 : 1;
|
||||
@@ -131,7 +139,7 @@ export async function fetchPurchasesByCategory(
|
||||
});
|
||||
|
||||
return {
|
||||
categories,
|
||||
categories: categoryList,
|
||||
transactionsByCategory,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import type { RecurringSeriesTemplate } from "@/db/schema";
|
||||
import { categorias, recurringSeries } from "@/db/schema";
|
||||
import { categories, recurringSeries } from "@/db/schema";
|
||||
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 { safeToNumber as toNumber } from "@/shared/utils/number";
|
||||
import { addMonthsToPeriod } from "@/shared/utils/period";
|
||||
|
||||
@@ -26,7 +26,7 @@ export type RecurringSeriesData = {
|
||||
export async function fetchRecurringSeries(
|
||||
userId: string,
|
||||
): Promise<RecurringSeriesData> {
|
||||
const adminPagadorId = await getAdminPagadorId(userId);
|
||||
const adminPayerId = await getAdminPayerId(userId);
|
||||
|
||||
const rows = await db
|
||||
.select({
|
||||
@@ -50,19 +50,19 @@ export async function fetchRecurringSeries(
|
||||
|
||||
// Fetch category names for all series in one query
|
||||
const categoryIds = rows
|
||||
.map((r) => (r.templateData as RecurringSeriesTemplate).categoriaId)
|
||||
.map((r) => (r.templateData as RecurringSeriesTemplate).categoryId)
|
||||
.filter((id): id is string => id !== null);
|
||||
|
||||
const categoryMap = new Map<string, { name: string; icon: string | null }>();
|
||||
if (categoryIds.length > 0) {
|
||||
const cats = await db
|
||||
.select({
|
||||
id: categorias.id,
|
||||
name: categorias.name,
|
||||
icon: categorias.icon,
|
||||
id: categories.id,
|
||||
name: categories.name,
|
||||
icon: categories.icon,
|
||||
})
|
||||
.from(categorias)
|
||||
.where(inArray(categorias.id, categoryIds));
|
||||
.from(categories)
|
||||
.where(inArray(categories.id, categoryIds));
|
||||
for (const cat of cats) {
|
||||
categoryMap.set(cat.id, { name: cat.name, icon: cat.icon });
|
||||
}
|
||||
@@ -71,16 +71,14 @@ export async function fetchRecurringSeries(
|
||||
const series = rows
|
||||
.filter((row) => {
|
||||
// If admin pagador exists, only show series belonging to admin
|
||||
if (!adminPagadorId) return true;
|
||||
if (!adminPayerId) return true;
|
||||
const template = row.templateData as RecurringSeriesTemplate;
|
||||
return (
|
||||
template.pagadorId === adminPagadorId || template.pagadorId === null
|
||||
);
|
||||
return template.payerId === adminPayerId || template.payerId === null;
|
||||
})
|
||||
.map((row): RecurringSeriesItem => {
|
||||
const template = row.templateData as RecurringSeriesTemplate;
|
||||
const category = template.categoriaId
|
||||
? categoryMap.get(template.categoriaId)
|
||||
const category = template.categoryId
|
||||
? categoryMap.get(template.categoryId)
|
||||
: null;
|
||||
return {
|
||||
id: row.id,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { and, eq, sql } from "drizzle-orm";
|
||||
import { cartoes, contas, lancamentos } from "@/db/schema";
|
||||
import { cards, financialAccounts, transactions } from "@/db/schema";
|
||||
import {
|
||||
buildDashboardAdminPeriodFilters,
|
||||
excludeAutoGeneratedEntryNotes,
|
||||
} 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 { safeToNumber as toNumber } from "@/shared/utils/number";
|
||||
|
||||
export type TopEstablishment = {
|
||||
@@ -38,36 +38,41 @@ export async function fetchTopEstablishments(
|
||||
userId: string,
|
||||
period: string,
|
||||
): Promise<TopEstablishmentsData> {
|
||||
const adminPagadorId = await getAdminPagadorId(userId);
|
||||
if (!adminPagadorId) {
|
||||
const adminPayerId = await getAdminPayerId(userId);
|
||||
if (!adminPayerId) {
|
||||
return { establishments: [] };
|
||||
}
|
||||
|
||||
const rows = await db
|
||||
.select({
|
||||
name: lancamentos.name,
|
||||
totalAmount: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`,
|
||||
occurrences: sql<number>`count(${lancamentos.id})`,
|
||||
logo: sql<string | null>`max(coalesce(${cartoes.logo}, ${contas.logo}))`,
|
||||
name: transactions.name,
|
||||
totalAmount: sql<number>`coalesce(sum(${transactions.amount}), 0)`,
|
||||
occurrences: sql<number>`count(${transactions.id})`,
|
||||
logo: sql<
|
||||
string | null
|
||||
>`max(coalesce(${cards.logo}, ${financialAccounts.logo}))`,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.leftJoin(cartoes, eq(lancamentos.cartaoId, cartoes.id))
|
||||
.leftJoin(contas, eq(lancamentos.contaId, contas.id))
|
||||
.from(transactions)
|
||||
.leftJoin(cards, eq(transactions.cardId, cards.id))
|
||||
.leftJoin(
|
||||
financialAccounts,
|
||||
eq(transactions.accountId, financialAccounts.id),
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
...buildDashboardAdminPeriodFilters({
|
||||
userId,
|
||||
period,
|
||||
adminPagadorId,
|
||||
adminPayerId,
|
||||
}),
|
||||
eq(lancamentos.transactionType, "Despesa"),
|
||||
eq(transactions.transactionType, "Despesa"),
|
||||
excludeAutoGeneratedEntryNotes(),
|
||||
),
|
||||
)
|
||||
.groupBy(lancamentos.name)
|
||||
.groupBy(transactions.name)
|
||||
.orderBy(
|
||||
sql`count(${lancamentos.id}) DESC`,
|
||||
sql`ABS(sum(${lancamentos.amount})) DESC`,
|
||||
sql`count(${transactions.id}) DESC`,
|
||||
sql`ABS(sum(${transactions.amount})) DESC`,
|
||||
)
|
||||
.limit(10);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { and, eq, ilike, isNull, ne, not, or } from "drizzle-orm";
|
||||
import { contas, lancamentos } from "@/db/schema";
|
||||
import { financialAccounts, transactions } from "@/db/schema";
|
||||
import {
|
||||
ACCOUNT_AUTO_INVOICE_NOTE_PREFIX,
|
||||
INITIAL_BALANCE_NOTE,
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
|
||||
type DashboardAdminFiltersParams = {
|
||||
userId: string;
|
||||
adminPagadorId: string;
|
||||
adminPayerId: string;
|
||||
};
|
||||
|
||||
type DashboardAdminPeriodFiltersParams = DashboardAdminFiltersParams & {
|
||||
@@ -16,41 +16,41 @@ type DashboardAdminPeriodFiltersParams = DashboardAdminFiltersParams & {
|
||||
|
||||
export const buildDashboardAdminFilters = ({
|
||||
userId,
|
||||
adminPagadorId,
|
||||
adminPayerId,
|
||||
}: DashboardAdminFiltersParams) =>
|
||||
[
|
||||
eq(lancamentos.userId, userId),
|
||||
eq(lancamentos.pagadorId, adminPagadorId),
|
||||
eq(transactions.userId, userId),
|
||||
eq(transactions.payerId, adminPayerId),
|
||||
] as const;
|
||||
|
||||
export const buildDashboardAdminPeriodFilters = ({
|
||||
userId,
|
||||
period,
|
||||
adminPagadorId,
|
||||
adminPayerId,
|
||||
}: DashboardAdminPeriodFiltersParams) =>
|
||||
[
|
||||
...buildDashboardAdminFilters({ userId, adminPagadorId }),
|
||||
eq(lancamentos.period, period),
|
||||
...buildDashboardAdminFilters({ userId, adminPayerId }),
|
||||
eq(transactions.period, period),
|
||||
] as const;
|
||||
|
||||
export const excludeAutoInvoiceEntries = () =>
|
||||
or(
|
||||
isNull(lancamentos.note),
|
||||
not(ilike(lancamentos.note, `${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`)),
|
||||
isNull(transactions.note),
|
||||
not(ilike(transactions.note, `${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`)),
|
||||
);
|
||||
|
||||
export const excludeAutoGeneratedEntryNotes = () =>
|
||||
or(
|
||||
isNull(lancamentos.note),
|
||||
isNull(transactions.note),
|
||||
and(
|
||||
ne(lancamentos.note, INITIAL_BALANCE_NOTE),
|
||||
not(ilike(lancamentos.note, `${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`)),
|
||||
ne(transactions.note, INITIAL_BALANCE_NOTE),
|
||||
not(ilike(transactions.note, `${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`)),
|
||||
),
|
||||
);
|
||||
|
||||
export const excludeInitialBalanceWhenConfigured = () =>
|
||||
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),
|
||||
);
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
type PaymentDialogController,
|
||||
usePaymentDialogController,
|
||||
} from "@/features/dashboard/use-payment-dialog-controller";
|
||||
import { toggleLancamentoSettlementAction } from "@/features/transactions/actions";
|
||||
import { toggleTransactionSettlementAction } from "@/features/transactions/actions";
|
||||
|
||||
const EMPTY_BILLS: DashboardBill[] = [];
|
||||
|
||||
@@ -31,7 +31,7 @@ export function useBillWidgetController(
|
||||
getItemId: (bill) => bill.id,
|
||||
isItemConfirmed: (bill) => bill.isSettled,
|
||||
executeConfirm: (bill) =>
|
||||
toggleLancamentoSettlementAction({
|
||||
toggleTransactionSettlementAction({
|
||||
id: bill.id,
|
||||
value: true,
|
||||
}),
|
||||
|
||||
@@ -31,7 +31,7 @@ export function useInvoicesWidgetController(
|
||||
isItemConfirmed: (invoice) => isInvoicePaid(invoice.paymentStatus),
|
||||
executeConfirm: (invoice) =>
|
||||
updateInvoicePaymentStatusAction({
|
||||
cartaoId: invoice.cardId,
|
||||
cardId: invoice.cardId,
|
||||
period: invoice.period,
|
||||
status: INVOICE_PAYMENT_STATUS.PAID,
|
||||
}),
|
||||
|
||||
@@ -18,21 +18,21 @@ export async function updateWidgetPreferences(
|
||||
|
||||
// Check if preferences exist
|
||||
const existing = await db
|
||||
.select({ id: schema.preferenciasUsuario.id })
|
||||
.from(schema.preferenciasUsuario)
|
||||
.where(eq(schema.preferenciasUsuario.userId, user.id))
|
||||
.select({ id: schema.userPreferences.id })
|
||||
.from(schema.userPreferences)
|
||||
.where(eq(schema.userPreferences.userId, user.id))
|
||||
.limit(1);
|
||||
|
||||
if (existing.length > 0) {
|
||||
await db
|
||||
.update(schema.preferenciasUsuario)
|
||||
.update(schema.userPreferences)
|
||||
.set({
|
||||
dashboardWidgets: preferences,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(schema.preferenciasUsuario.userId, user.id));
|
||||
.where(eq(schema.userPreferences.userId, user.id));
|
||||
} else {
|
||||
await db.insert(schema.preferenciasUsuario).values({
|
||||
await db.insert(schema.userPreferences).values({
|
||||
userId: user.id,
|
||||
dashboardWidgets: preferences,
|
||||
});
|
||||
@@ -54,12 +54,12 @@ export async function resetWidgetPreferences(): Promise<{
|
||||
const user = await getUser();
|
||||
|
||||
await db
|
||||
.update(schema.preferenciasUsuario)
|
||||
.update(schema.userPreferences)
|
||||
.set({
|
||||
dashboardWidgets: null,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(schema.preferenciasUsuario.userId, user.id));
|
||||
.where(eq(schema.userPreferences.userId, user.id));
|
||||
|
||||
revalidatePath("/dashboard");
|
||||
return { success: true };
|
||||
|
||||
@@ -97,7 +97,7 @@ export const widgetsConfig: WidgetConfig[] = [
|
||||
subtitle: "Despesas por pagador no período",
|
||||
icon: <RiGroupLine className="size-4" />,
|
||||
component: ({ data }) => (
|
||||
<PayersWidget pagadores={data.pagadoresSnapshot.pagadores} />
|
||||
<PayersWidget payers={data.pagadoresSnapshot.payers} />
|
||||
),
|
||||
action: (
|
||||
<Link
|
||||
|
||||
Reference in New Issue
Block a user