refactor: atualiza transacoes dashboard e relatorios

This commit is contained in:
Felipe Coutinho
2026-03-14 12:51:22 +00:00
parent 43b0f0c47e
commit 6854017a8c
89 changed files with 2785 additions and 2705 deletions

View File

@@ -10,8 +10,8 @@ import {
getSingleParam,
} from "@/features/transactions/page-helpers";
import {
fetchLancamentoFilterSources,
fetchRecentEstablishments,
fetchTransactionFilterSources,
} from "@/features/transactions/queries";
import MonthNavigation from "@/shared/components/month-picker/month-navigation";
import { getUser } from "@/shared/lib/auth/server";
@@ -34,21 +34,21 @@ export default async function Page({ searchParams }: PageProps) {
await Promise.all([
fetchDashboardData(user.id, selectedPeriod),
fetchUserDashboardPreferences(user.id),
fetchLancamentoFilterSources(user.id),
fetchTransactionFilterSources(user.id),
fetchRecentEstablishments(user.id),
]);
const { dashboardWidgets } = preferences;
const sluggedFilters = buildSluggedFilters(filterSources);
const {
pagadorOptions,
splitPagadorOptions,
defaultPagadorId,
contaOptions,
cartaoOptions,
categoriaOptions,
payerOptions,
splitPayerOptions,
defaultPayerId,
accountOptions,
cardOptions,
categoryOptions,
} = buildOptionSets({
...sluggedFilters,
pagadorRows: filterSources.pagadorRows,
payerRows: filterSources.payerRows,
});
return (
@@ -61,12 +61,12 @@ export default async function Page({ searchParams }: PageProps) {
period={selectedPeriod}
initialPreferences={dashboardWidgets}
quickActionOptions={{
pagadorOptions,
splitPagadorOptions,
defaultPagadorId,
contaOptions,
cartaoOptions,
categoriaOptions,
payerOptions,
splitPayerOptions,
defaultPayerId,
accountOptions,
cardOptions,
categoryOptions,
estabelecimentos,
}}
/>

View File

@@ -1,5 +1,5 @@
import { redirect } from "next/navigation";
import type { Categoria } from "@/db/schema";
import type { Category } from "@/db/schema";
import { fetchCategoryChartData } from "@/features/reports/category-chart-queries";
import { fetchCategoryReport } from "@/features/reports/category-report-queries";
import { fetchUserCategories } from "@/features/reports/category-trends-queries";
@@ -38,7 +38,7 @@ export default async function Page({ searchParams }: PageProps) {
// Extract query params
const inicioParam = getSingleParam(resolvedSearchParams, "inicio");
const fimParam = getSingleParam(resolvedSearchParams, "fim");
const categoriasParam = getSingleParam(resolvedSearchParams, "categorias");
const categoriasParam = getSingleParam(resolvedSearchParams, "categories");
// Calculate default period (last 6 months)
const currentPeriod = getCurrentPeriod();
@@ -63,11 +63,11 @@ export default async function Page({ searchParams }: PageProps) {
}
// Fetch all categories for the user
const categoriaRows = await fetchUserCategories(userId);
const categoryRows = await fetchUserCategories(userId);
// Map to CategoryOption format
const categoryOptions: CategoryOption[] = categoriaRows.map(
(cat: Categoria): CategoryOption => ({
const categoryOptions: CategoryOption[] = categoryRows.map(
(cat: Category): CategoryOption => ({
id: cat.id,
name: cat.name,
icon: cat.icon,

View File

@@ -4,7 +4,7 @@ import { PeriodFilterButtons } from "@/features/reports/components/establishment
import { SummaryCards } from "@/features/reports/components/establishments/summary-cards";
import { TopCategories } from "@/features/reports/components/establishments/top-categories";
import {
fetchTopEstabelecimentosData,
fetchTopEstablishmentsData,
type PeriodFilter,
} from "@/features/reports/establishments/queries";
import { Card } from "@/shared/components/ui/card";
@@ -44,7 +44,7 @@ export default async function TopEstabelecimentosPage({
const { period: currentPeriod } = parsePeriodParam(periodoParam);
const periodFilter = validatePeriodFilter(mesesParam);
const data = await fetchTopEstabelecimentosData(
const data = await fetchTopEstablishmentsData(
user.id,
currentPeriod,
periodFilter,

View File

@@ -1,20 +1,20 @@
import { triggerRecurringGeneration } from "@/features/recurring/trigger-recurring-generation";
import { fetchUserPreferences } from "@/features/settings/queries";
import { LancamentosPage } from "@/features/transactions/components/page/transactions-page";
import { TransactionsPage } from "@/features/transactions/components/page/transactions-page";
import {
buildLancamentoWhere,
buildTransactionWhere,
buildOptionSets,
buildSluggedFilters,
buildSlugMaps,
extractLancamentoSearchFilters,
extractTransactionSearchFilters,
getSingleParam,
mapLancamentosData,
mapTransactionsData,
type ResolvedSearchParams,
} from "@/features/transactions/page-helpers";
import {
fetchLancamentoFilterSources,
fetchLancamentos,
fetchRecentEstablishments,
fetchTransactionFilterSources,
fetchTransactions,
} from "@/features/transactions/queries";
import MonthNavigation from "@/shared/components/month-picker/month-navigation";
import { getUserId } from "@/shared/lib/auth/server";
@@ -34,63 +34,63 @@ export default async function Page({ searchParams }: PageProps) {
const periodoParamRaw = getSingleParam(resolvedSearchParams, "periodo");
const { period: selectedPeriod } = parsePeriodParam(periodoParamRaw);
const searchFilters = extractLancamentoSearchFilters(resolvedSearchParams);
const searchFilters = extractTransactionSearchFilters(resolvedSearchParams);
const [filterSources, userPreferences] = await Promise.all([
fetchLancamentoFilterSources(userId),
fetchTransactionFilterSources(userId),
fetchUserPreferences(userId),
]);
const sluggedFilters = buildSluggedFilters(filterSources);
const slugMaps = buildSlugMaps(sluggedFilters);
const filters = buildLancamentoWhere({
const filters = buildTransactionWhere({
userId,
period: selectedPeriod,
filters: searchFilters,
slugMaps,
});
const [lancamentoRows, estabelecimentos] = await Promise.all([
fetchLancamentos(filters),
const [transactionRows, estabelecimentos] = await Promise.all([
fetchTransactions(filters),
fetchRecentEstablishments(userId),
]);
const lancamentosData = mapLancamentosData(lancamentoRows);
const transactionData = mapTransactionsData(transactionRows);
const {
pagadorOptions,
splitPagadorOptions,
defaultPagadorId,
contaOptions,
cartaoOptions,
categoriaOptions,
pagadorFilterOptions,
categoriaFilterOptions,
contaCartaoFilterOptions,
payerOptions,
splitPayerOptions,
defaultPayerId,
accountOptions,
cardOptions,
categoryOptions,
payerFilterOptions,
categoryFilterOptions,
accountCardFilterOptions,
} = buildOptionSets({
...sluggedFilters,
pagadorRows: filterSources.pagadorRows,
payerRows: filterSources.payerRows,
});
return (
<main className="flex flex-col gap-6">
<MonthNavigation />
<LancamentosPage
<TransactionsPage
currentUserId={userId}
lancamentos={lancamentosData}
pagadorOptions={pagadorOptions}
splitPagadorOptions={splitPagadorOptions}
defaultPagadorId={defaultPagadorId}
contaOptions={contaOptions}
cartaoOptions={cartaoOptions}
categoriaOptions={categoriaOptions}
pagadorFilterOptions={pagadorFilterOptions}
categoriaFilterOptions={categoriaFilterOptions}
contaCartaoFilterOptions={contaCartaoFilterOptions}
transactions={transactionData}
payerOptions={payerOptions}
splitPayerOptions={splitPayerOptions}
defaultPayerId={defaultPayerId}
accountOptions={accountOptions}
cardOptions={cardOptions}
categoryOptions={categoryOptions}
payerFilterOptions={payerFilterOptions}
categoryFilterOptions={categoryFilterOptions}
accountCardFilterOptions={accountCardFilterOptions}
selectedPeriod={selectedPeriod}
estabelecimentos={estabelecimentos}
noteAsColumn={userPreferences?.extratoNoteAsColumn ?? false}
columnOrder={userPreferences?.lancamentosColumnOrder ?? null}
noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
columnOrder={userPreferences?.transactionsColumnOrder ?? null}
/>
</main>
);

View File

@@ -10,9 +10,9 @@ const LEGEND_ITEMS: Array<{
label: string;
dotColor?: string;
}> = [
{ type: "lancamento", label: "Lançamentos" },
{ type: "transaction", label: "Lançamentos" },
{ type: "boleto", label: "Boleto com vencimento" },
{ type: "cartao", label: "Vencimento de cartão" },
{ type: "card", label: "Vencimento de cartão" },
{ label: "Pagamento fatura", dotColor: "bg-success" },
];

View File

@@ -16,7 +16,7 @@ export const EVENT_TYPE_STYLES: Record<
CalendarEvent["type"],
{ wrapper: string; dot: string; accent?: string }
> = {
lancamento: {
transaction: {
wrapper:
"bg-warning/10 text-warning dark:bg-warning/5 dark:text-warning border-l-4 border-warning",
dot: "bg-warning",
@@ -26,7 +26,7 @@ export const EVENT_TYPE_STYLES: Record<
"bg-info/10 text-info dark:bg-info/5 dark:text-info border-l-4 border-info",
dot: "bg-info",
},
cartao: {
card: {
wrapper:
"bg-violet-100 text-violet-600 dark:bg-violet-900/10 dark:text-violet-50 border-l-4 border-violet-500",
dot: "bg-violet-600",
@@ -38,18 +38,18 @@ const eventStyles = EVENT_TYPE_STYLES;
const formatCurrencyValue = (value: number | null | undefined) =>
currencyFormatter.format(Math.abs(value ?? 0));
const formatAmount = (event: Extract<CalendarEvent, { type: "lancamento" }>) =>
formatCurrencyValue(event.lancamento.amount);
const formatAmount = (event: Extract<CalendarEvent, { type: "transaction" }>) =>
formatCurrencyValue(event.transaction.amount);
const buildEventLabel = (event: CalendarEvent) => {
switch (event.type) {
case "lancamento": {
return event.lancamento.name;
case "transaction": {
return event.transaction.name;
}
case "boleto": {
return event.lancamento.name;
return event.transaction.name;
}
case "cartao": {
case "card": {
return event.card.name;
}
default:
@@ -59,13 +59,13 @@ const buildEventLabel = (event: CalendarEvent) => {
const buildEventComplement = (event: CalendarEvent) => {
switch (event.type) {
case "lancamento": {
case "transaction": {
return formatAmount(event);
}
case "boleto": {
return formatCurrencyValue(event.lancamento.amount);
return formatCurrencyValue(event.transaction.amount);
}
case "cartao": {
case "card": {
if (event.card.totalDue !== null) {
return formatCurrencyValue(event.card.totalDue);
}
@@ -78,8 +78,8 @@ const buildEventComplement = (event: CalendarEvent) => {
const isPagamentoFatura = (event: CalendarEvent) => {
return (
event.type === "lancamento" &&
event.lancamento.name.startsWith("Pagamento fatura -")
event.type === "transaction" &&
event.transaction.name.startsWith("Pagamento fatura -")
);
};

View File

@@ -50,14 +50,14 @@ const EventCard = ({
};
const renderLancamento = (
event: Extract<CalendarEvent, { type: "lancamento" }>,
event: Extract<CalendarEvent, { type: "transaction" }>,
) => {
const isReceita = event.lancamento.transactionType === "Receita";
const isReceita = event.transaction.transactionType === "Receita";
const isPagamentoFatura =
event.lancamento.name.startsWith("Pagamento fatura -");
event.transaction.name.startsWith("Pagamento fatura -");
return (
<EventCard type="lancamento" isPagamentoFatura={isPagamentoFatura}>
<EventCard type="transaction" isPagamentoFatura={isPagamentoFatura}>
<div className="flex items-start justify-between gap-3">
<div className="flex flex-col gap-1">
<span
@@ -65,13 +65,13 @@ const renderLancamento = (
isPagamentoFatura && "text-success"
}`}
>
{event.lancamento.name}
{event.transaction.name}
</span>
<div className="flex gap-1">
<Badge variant={"outline"}>{event.lancamento.condition}</Badge>
<Badge variant={"outline"}>{event.lancamento.paymentMethod}</Badge>
<Badge variant={"outline"}>{event.lancamento.categoriaName}</Badge>
<Badge variant={"outline"}>{event.transaction.condition}</Badge>
<Badge variant={"outline"}>{event.transaction.paymentMethod}</Badge>
<Badge variant={"outline"}>{event.transaction.categoriaName}</Badge>
</div>
</div>
<span
@@ -83,7 +83,7 @@ const renderLancamento = (
<MoneyValues
showPositiveSign
className="text-base"
amount={event.lancamento.amount}
amount={event.transaction.amount}
/>
</span>
</div>
@@ -92,8 +92,8 @@ const renderLancamento = (
};
const renderBoleto = (event: Extract<CalendarEvent, { type: "boleto" }>) => {
const isPaid = Boolean(event.lancamento.isSettled);
const dueDate = event.lancamento.dueDate;
const isPaid = Boolean(event.transaction.isSettled);
const dueDate = event.transaction.dueDate;
const dueDateLabel = formatFinancialDateLabel(dueDate, "Vence em", {
day: "2-digit",
month: "2-digit",
@@ -106,7 +106,7 @@ const renderBoleto = (event: Extract<CalendarEvent, { type: "boleto" }>) => {
<div className="flex flex-col gap-1">
<div className="flex gap-1 items-center">
<span className="text-sm font-semibold leading-tight">
{event.lancamento.name}
{event.transaction.name}
</span>
{dueDateLabel && (
@@ -119,24 +119,24 @@ const renderBoleto = (event: Extract<CalendarEvent, { type: "boleto" }>) => {
<Badge variant={"outline"}>{isPaid ? "Pago" : "Pendente"}</Badge>
</div>
<span className="font-semibold">
<MoneyValues amount={event.lancamento.amount} />
<MoneyValues amount={event.transaction.amount} />
</span>
</div>
</EventCard>
);
};
const renderCard = (event: Extract<CalendarEvent, { type: "cartao" }>) => (
<EventCard type="cartao">
const renderCard = (event: Extract<CalendarEvent, { type: "card" }>) => (
<EventCard type="card">
<div className="flex items-start justify-between gap-3">
<div className="flex flex-col gap-1">
<div className="flex gap-1 items-center">
<span className="text-sm font-semibold leading-tight">
Vencimento Fatura - {event.card.name}
Vencimento Invoice - {event.card.name}
</span>
</div>
<Badge variant={"outline"}>{event.card.status ?? "Fatura"}</Badge>
<Badge variant={"outline"}>{event.card.status ?? "Invoice"}</Badge>
</div>
{event.card.totalDue !== null ? (
<span className="font-semibold">
@@ -149,11 +149,11 @@ const renderCard = (event: Extract<CalendarEvent, { type: "cartao" }>) => (
const renderEvent = (event: CalendarEvent) => {
switch (event.type) {
case "lancamento":
case "transaction":
return renderLancamento(event);
case "boleto":
return renderBoleto(event);
case "cartao":
case "card":
return renderCard(event);
default:
return null;

View File

@@ -4,7 +4,7 @@ import { useMemo, useState } from "react";
import { CalendarGrid } from "@/features/calendar/components/calendar-grid";
import { CalendarLegend } from "@/features/calendar/components/calendar-legend";
import { EventModal } from "@/features/calendar/components/event-modal";
import { LancamentoDialog } from "@/features/transactions/components/dialogs/transaction-dialog/transaction-dialog";
import { TransactionDialog } from "@/features/transactions/components/dialogs/transaction-dialog/transaction-dialog";
import type {
CalendarDay,
CalendarEvent,
@@ -93,16 +93,16 @@ export function MonthlyCalendar({
onCreate={handleOpenCreate}
/>
<LancamentoDialog
<TransactionDialog
mode="create"
open={createOpen}
onOpenChange={handleCreateDialogChange}
pagadorOptions={formOptions.pagadorOptions}
splitPagadorOptions={formOptions.splitPagadorOptions}
defaultPagadorId={formOptions.defaultPagadorId}
contaOptions={formOptions.contaOptions}
cartaoOptions={formOptions.cartaoOptions}
categoriaOptions={formOptions.categoriaOptions}
payerOptions={formOptions.payerOptions}
splitPayerOptions={formOptions.splitPayerOptions}
defaultPayerId={formOptions.defaultPayerId}
accountOptions={formOptions.accountOptions}
cardOptions={formOptions.cardOptions}
categoryOptions={formOptions.categoryOptions}
estabelecimentos={formOptions.estabelecimentos}
defaultPeriod={period.period}
defaultPurchaseDate={createDate ?? undefined}

View File

@@ -1,16 +1,16 @@
import { and, eq, gte, lte, ne, or } from "drizzle-orm";
import { cartoes, lancamentos } from "@/db/schema";
import { cards, transactions } from "@/db/schema";
import {
buildOptionSets,
buildSluggedFilters,
mapLancamentosData,
mapTransactionsData,
} from "@/features/transactions/page-helpers";
import {
fetchLancamentoFilterSources,
fetchRecentEstablishments,
fetchTransactionFilterSources,
} from "@/features/transactions/queries";
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 type { CalendarData, CalendarEvent } from "@/shared/lib/types/calendar";
import { formatDateKey } from "@/shared/utils/calendar";
import { parsePeriod } from "@/shared/utils/period";
@@ -46,65 +46,62 @@ export const fetchCalendarData = async ({
const rangeStartKey = formatDateKey(rangeStart);
const rangeEndKey = formatDateKey(rangeEnd);
const [lancamentoRows, cardRows, filterSources] = await Promise.all([
db.query.lancamentos.findMany({
const [transactionRows, cardRows, filterSources] = await Promise.all([
db.query.transactions.findMany({
where: and(
eq(lancamentos.userId, userId),
ne(lancamentos.transactionType, TRANSACTION_TYPE_TRANSFERENCIA),
eq(transactions.userId, userId),
ne(transactions.transactionType, TRANSACTION_TYPE_TRANSFERENCIA),
or(
// Lançamentos cuja data de compra esteja no período do calendário
and(
gte(lancamentos.purchaseDate, rangeStart),
lte(lancamentos.purchaseDate, rangeEnd),
gte(transactions.purchaseDate, rangeStart),
lte(transactions.purchaseDate, rangeEnd),
),
// Boletos cuja data de vencimento esteja no período do calendário
and(
eq(lancamentos.paymentMethod, PAYMENT_METHOD_BOLETO),
gte(lancamentos.dueDate, rangeStart),
lte(lancamentos.dueDate, rangeEnd),
eq(transactions.paymentMethod, PAYMENT_METHOD_BOLETO),
gte(transactions.dueDate, rangeStart),
lte(transactions.dueDate, rangeEnd),
),
// Lançamentos de cartão do período (para calcular totais de vencimento)
and(
eq(lancamentos.period, period),
ne(lancamentos.paymentMethod, PAYMENT_METHOD_BOLETO),
eq(transactions.period, period),
ne(transactions.paymentMethod, PAYMENT_METHOD_BOLETO),
),
),
),
with: {
pagador: true,
conta: true,
cartao: true,
categoria: true,
payer: true,
financialAccount: true,
card: true,
category: true,
},
}),
db.query.cartoes.findMany({
where: eq(cartoes.userId, userId),
db.query.cards.findMany({
where: eq(cards.userId, userId),
}),
fetchLancamentoFilterSources(userId),
fetchTransactionFilterSources(userId),
]);
const lancamentosData = mapLancamentosData(lancamentoRows);
const transactionData = mapTransactionsData(transactionRows);
const events: CalendarEvent[] = [];
const cardTotals = new Map<string, number>();
for (const item of lancamentosData) {
for (const item of transactionData) {
if (
!item.cartaoId ||
!item.cardId ||
item.period !== period ||
item.pagadorRole !== PAGADOR_ROLE_ADMIN
item.pagadorRole !== PAYER_ROLE_ADMIN
) {
continue;
}
const amount = Math.abs(item.amount ?? 0);
cardTotals.set(
item.cartaoId,
(cardTotals.get(item.cartaoId) ?? 0) + amount,
);
cardTotals.set(item.cardId, (cardTotals.get(item.cardId) ?? 0) + amount);
}
for (const item of lancamentosData) {
for (const item of transactionData) {
const isBoleto = item.paymentMethod === PAYMENT_METHOD_BOLETO;
const isAdminPagador = item.pagadorRole === PAGADOR_ROLE_ADMIN;
const isAdminPagador = item.pagadorRole === PAYER_ROLE_ADMIN;
// Para boletos, exibir apenas na data de vencimento e apenas se for pagador admin
if (isBoleto) {
@@ -117,7 +114,7 @@ export const fetchCalendarData = async ({
id: `${item.id}:boleto`,
type: "boleto",
date: item.dueDate,
lancamento: item,
transaction: item,
});
}
} else {
@@ -129,9 +126,9 @@ export const fetchCalendarData = async ({
if (isWithinRange(purchaseDateKey, rangeStartKey, rangeEndKey)) {
events.push({
id: item.id,
type: "lancamento",
type: "transaction",
date: purchaseDateKey,
lancamento: item,
transaction: item,
});
}
}
@@ -155,7 +152,7 @@ export const fetchCalendarData = async ({
events.push({
id: `${card.id}:cartao`,
type: "cartao",
type: "card",
date: dueDateKey,
card: {
id: card.id,
@@ -171,9 +168,9 @@ export const fetchCalendarData = async ({
}
const typePriority: Record<CalendarEvent["type"], number> = {
lancamento: 0,
transaction: 0,
boleto: 1,
cartao: 2,
card: 2,
};
events.sort((a, b) => {
@@ -186,7 +183,7 @@ export const fetchCalendarData = async ({
const sluggedFilters = buildSluggedFilters(filterSources);
const optionSets = buildOptionSets({
...sluggedFilters,
pagadorRows: filterSources.pagadorRows,
payerRows: filterSources.payerRows,
});
const estabelecimentos = await fetchRecentEstablishments(userId);
@@ -194,12 +191,12 @@ export const fetchCalendarData = async ({
return {
events,
formOptions: {
pagadorOptions: optionSets.pagadorOptions,
splitPagadorOptions: optionSets.splitPagadorOptions,
defaultPagadorId: optionSets.defaultPagadorId,
contaOptions: optionSets.contaOptions,
cartaoOptions: optionSets.cartaoOptions,
categoriaOptions: optionSets.categoriaOptions,
payerOptions: optionSets.payerOptions,
splitPayerOptions: optionSets.splitPayerOptions,
defaultPayerId: optionSets.defaultPayerId,
accountOptions: optionSets.accountOptions,
cardOptions: optionSets.cardOptions,
categoryOptions: optionSets.categoryOptions,
estabelecimentos,
},
};

View File

@@ -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

View File

@@ -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 => {

View File

@@ -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));
}
}

View File

@@ -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,
};
}

View File

@@ -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) {

View File

@@ -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({

View File

@@ -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({

View File

@@ -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"

View File

@@ -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"
>

View File

@@ -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

View File

@@ -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">

View File

@@ -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 ${

View File

@@ -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>();

View File

@@ -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,

View File

@@ -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];

View File

@@ -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 => ({

View File

@@ -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(

View File

@@ -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),

View File

@@ -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,

View File

@@ -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 }>();

View File

@@ -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,
};
}

View File

@@ -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,
});

View File

@@ -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(),
}));
}

View File

@@ -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

View File

@@ -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,
};
}

View File

@@ -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));

View File

@@ -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));

View File

@@ -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() };

View File

@@ -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 {

View File

@@ -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,
};
}

View File

@@ -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,

View File

@@ -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);

View File

@@ -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),
);

View File

@@ -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,
}),

View File

@@ -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,
}),

View File

@@ -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 };

View File

@@ -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

View File

@@ -8,18 +8,18 @@ import { generateObject } from "ai";
import { getDay } from "date-fns";
import { and, eq, isNull, ne, or, sql } from "drizzle-orm";
import {
cartoes,
categorias,
contas,
insightsSalvos,
lancamentos,
orcamentos,
pagadores,
budgets,
cards,
categories,
financialAccounts,
payers,
savedInsights,
transactions,
} from "@/db/schema";
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/shared/lib/accounts/constants";
import { getUser } from "@/shared/lib/auth/server";
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 {
type InsightsResponse,
InsightsResponseSchema,
@@ -62,92 +62,92 @@ async function aggregateMonthData(userId: string, period: string) {
] = await Promise.all([
db
.select({
transactionType: lancamentos.transactionType,
totalAmount: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`,
transactionType: transactions.transactionType,
totalAmount: sql<number>`coalesce(sum(${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),
eq(lancamentos.period, period),
eq(pagadores.role, PAGADOR_ROLE_ADMIN),
ne(lancamentos.transactionType, TRANSFERENCIA),
eq(transactions.userId, userId),
eq(transactions.period, period),
eq(payers.role, PAYER_ROLE_ADMIN),
ne(transactions.transactionType, TRANSFERENCIA),
or(
isNull(lancamentos.note),
isNull(transactions.note),
sql`${
lancamentos.note
transactions.note
} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`,
),
),
)
.groupBy(lancamentos.transactionType),
.groupBy(transactions.transactionType),
db
.select({
transactionType: lancamentos.transactionType,
totalAmount: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`,
transactionType: transactions.transactionType,
totalAmount: sql<number>`coalesce(sum(${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),
eq(lancamentos.period, previousPeriod),
eq(pagadores.role, PAGADOR_ROLE_ADMIN),
ne(lancamentos.transactionType, TRANSFERENCIA),
eq(transactions.userId, userId),
eq(transactions.period, previousPeriod),
eq(payers.role, PAYER_ROLE_ADMIN),
ne(transactions.transactionType, TRANSFERENCIA),
or(
isNull(lancamentos.note),
isNull(transactions.note),
sql`${
lancamentos.note
transactions.note
} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`,
),
),
)
.groupBy(lancamentos.transactionType),
.groupBy(transactions.transactionType),
db
.select({
transactionType: lancamentos.transactionType,
totalAmount: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`,
transactionType: transactions.transactionType,
totalAmount: sql<number>`coalesce(sum(${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),
eq(lancamentos.period, twoMonthsAgo),
eq(pagadores.role, PAGADOR_ROLE_ADMIN),
ne(lancamentos.transactionType, TRANSFERENCIA),
eq(transactions.userId, userId),
eq(transactions.period, twoMonthsAgo),
eq(payers.role, PAYER_ROLE_ADMIN),
ne(transactions.transactionType, TRANSFERENCIA),
or(
isNull(lancamentos.note),
isNull(transactions.note),
sql`${
lancamentos.note
transactions.note
} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`,
),
),
)
.groupBy(lancamentos.transactionType),
.groupBy(transactions.transactionType),
db
.select({
transactionType: lancamentos.transactionType,
totalAmount: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`,
transactionType: transactions.transactionType,
totalAmount: sql<number>`coalesce(sum(${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),
eq(lancamentos.period, threeMonthsAgo),
eq(pagadores.role, PAGADOR_ROLE_ADMIN),
ne(lancamentos.transactionType, TRANSFERENCIA),
eq(transactions.userId, userId),
eq(transactions.period, threeMonthsAgo),
eq(payers.role, PAYER_ROLE_ADMIN),
ne(transactions.transactionType, TRANSFERENCIA),
or(
isNull(lancamentos.note),
isNull(transactions.note),
sql`${
lancamentos.note
transactions.note
} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`,
),
),
)
.groupBy(lancamentos.transactionType),
.groupBy(transactions.transactionType),
]);
// Calcular totais dos últimos 3 meses
@@ -187,107 +187,107 @@ async function aggregateMonthData(userId: string, period: string) {
// Buscar despesas por categoria (top 5)
const expensesByCategory = await db
.select({
categoryName: categorias.name,
total: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`,
categoryName: categories.name,
total: sql<number>`coalesce(sum(${transactions.amount}), 0)`,
})
.from(lancamentos)
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
.innerJoin(categorias, eq(lancamentos.categoriaId, categorias.id))
.from(transactions)
.innerJoin(payers, eq(transactions.payerId, payers.id))
.innerJoin(categories, eq(transactions.categoryId, categories.id))
.where(
and(
eq(lancamentos.userId, userId),
eq(lancamentos.period, period),
eq(lancamentos.transactionType, "Despesa"),
eq(pagadores.role, PAGADOR_ROLE_ADMIN),
eq(categorias.type, "despesa"),
eq(transactions.userId, userId),
eq(transactions.period, period),
eq(transactions.transactionType, "Despesa"),
eq(payers.role, PAYER_ROLE_ADMIN),
eq(categories.type, "despesa"),
or(
isNull(lancamentos.note),
isNull(transactions.note),
sql`${
lancamentos.note
transactions.note
} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`,
),
),
)
.groupBy(categorias.name)
.orderBy(sql`sum(${lancamentos.amount}) ASC`)
.groupBy(categories.name)
.orderBy(sql`sum(${transactions.amount}) ASC`)
.limit(5);
// Buscar orçamentos e uso
const budgetsData = await db
.select({
categoryName: categorias.name,
budgetAmount: orcamentos.amount,
spent: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`,
categoryName: categories.name,
budgetAmount: budgets.amount,
spent: sql<number>`coalesce(sum(${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, categorias.id),
eq(lancamentos.period, period),
eq(lancamentos.userId, userId),
eq(lancamentos.transactionType, "Despesa"),
eq(transactions.categoryId, categories.id),
eq(transactions.period, period),
eq(transactions.userId, userId),
eq(transactions.transactionType, "Despesa"),
),
)
.where(and(eq(orcamentos.userId, userId), eq(orcamentos.period, period)))
.groupBy(categorias.name, orcamentos.amount);
.where(and(eq(budgets.userId, userId), eq(budgets.period, period)))
.groupBy(categories.name, budgets.amount);
// Buscar métricas de cartões
const cardsData = await db
.select({
totalLimit: sql<number>`coalesce(sum(${cartoes.limit}), 0)`,
totalLimit: sql<number>`coalesce(sum(${cards.limit}), 0)`,
cardCount: sql<number>`count(*)`,
})
.from(cartoes)
.where(and(eq(cartoes.userId, userId), eq(cartoes.status, "ativo")));
.from(cards)
.where(and(eq(cards.userId, userId), eq(cards.status, "ativo")));
// Buscar saldo total das contas
// Buscar saldo total das financialAccounts
const accountsData = await db
.select({
totalBalance: sql<number>`coalesce(sum(${contas.initialBalance}), 0)`,
totalBalance: sql<number>`coalesce(sum(${financialAccounts.initialBalance}), 0)`,
accountCount: sql<number>`count(*)`,
})
.from(contas)
.from(financialAccounts)
.where(
and(
eq(contas.userId, userId),
eq(contas.status, "ativa"),
eq(contas.excludeFromBalance, false),
eq(financialAccounts.userId, userId),
eq(financialAccounts.status, "ativa"),
eq(financialAccounts.excludeFromBalance, false),
),
);
// Calcular ticket médio das transações
const avgTicketData = await db
.select({
avgAmount: sql<number>`coalesce(avg(abs(${lancamentos.amount})), 0)`,
avgAmount: sql<number>`coalesce(avg(abs(${transactions.amount})), 0)`,
transactionCount: sql<number>`count(*)`,
})
.from(lancamentos)
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
.from(transactions)
.innerJoin(payers, eq(transactions.payerId, payers.id))
.where(
and(
eq(lancamentos.userId, userId),
eq(lancamentos.period, period),
eq(pagadores.role, PAGADOR_ROLE_ADMIN),
ne(lancamentos.transactionType, TRANSFERENCIA),
eq(transactions.userId, userId),
eq(transactions.period, period),
eq(payers.role, PAYER_ROLE_ADMIN),
ne(transactions.transactionType, TRANSFERENCIA),
),
);
// Buscar gastos por dia da semana
const dayOfWeekSpending = await db
.select({
purchaseDate: lancamentos.purchaseDate,
amount: lancamentos.amount,
purchaseDate: transactions.purchaseDate,
amount: transactions.amount,
})
.from(lancamentos)
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
.from(transactions)
.innerJoin(payers, eq(transactions.payerId, payers.id))
.where(
and(
eq(lancamentos.userId, userId),
eq(lancamentos.period, period),
eq(lancamentos.transactionType, "Despesa"),
eq(pagadores.role, PAGADOR_ROLE_ADMIN),
eq(transactions.userId, userId),
eq(transactions.period, period),
eq(transactions.transactionType, "Despesa"),
eq(payers.role, PAYER_ROLE_ADMIN),
),
);
@@ -303,45 +303,45 @@ async function aggregateMonthData(userId: string, period: string) {
// Buscar métodos de pagamento (agregado)
const paymentMethodsData = await db
.select({
paymentMethod: lancamentos.paymentMethod,
total: sql<number>`coalesce(sum(abs(${lancamentos.amount})), 0)`,
paymentMethod: transactions.paymentMethod,
total: 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),
eq(lancamentos.period, period),
eq(lancamentos.transactionType, "Despesa"),
eq(pagadores.role, PAGADOR_ROLE_ADMIN),
eq(transactions.userId, userId),
eq(transactions.period, period),
eq(transactions.transactionType, "Despesa"),
eq(payers.role, PAYER_ROLE_ADMIN),
),
)
.groupBy(lancamentos.paymentMethod);
.groupBy(transactions.paymentMethod);
// Buscar transações dos últimos 3 meses para análise de recorrência
const last3MonthsTransactions = await db
.select({
name: lancamentos.name,
amount: lancamentos.amount,
period: lancamentos.period,
condition: lancamentos.condition,
installmentCount: lancamentos.installmentCount,
currentInstallment: lancamentos.currentInstallment,
categoryName: categorias.name,
name: transactions.name,
amount: transactions.amount,
period: transactions.period,
condition: transactions.condition,
installmentCount: transactions.installmentCount,
currentInstallment: transactions.currentInstallment,
categoryName: categories.name,
})
.from(lancamentos)
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
.leftJoin(categorias, eq(lancamentos.categoriaId, categorias.id))
.from(transactions)
.innerJoin(payers, eq(transactions.payerId, payers.id))
.leftJoin(categories, eq(transactions.categoryId, categories.id))
.where(
and(
eq(lancamentos.userId, userId),
sql`${lancamentos.period} IN (${period}, ${previousPeriod}, ${twoMonthsAgo})`,
eq(lancamentos.transactionType, "Despesa"),
eq(pagadores.role, PAGADOR_ROLE_ADMIN),
ne(lancamentos.transactionType, TRANSFERENCIA),
eq(transactions.userId, userId),
sql`${transactions.period} IN (${period}, ${previousPeriod}, ${twoMonthsAgo})`,
eq(transactions.transactionType, "Despesa"),
eq(payers.role, PAYER_ROLE_ADMIN),
ne(transactions.transactionType, TRANSFERENCIA),
),
)
.orderBy(lancamentos.name);
.orderBy(transactions.name);
// Análise de recorrência
const transactionsByName = new Map<
@@ -656,7 +656,7 @@ DADOS IMPORTANTES PARA SUA ANÁLISE:
- Comprometimento futuro de R$ ${aggregatedData.installments.futureCommitment.toFixed(2)}
- Use isso para alertas sobre comprometimento de renda futura
Organize suas observações nas 4 categorias especificadas no prompt do sistema:
Organize suas observações nas 4 categories especificadas no prompt do sistema:
1. Comportamentos Observados (behaviors): 3-6 itens
2. Gatilhos de Consumo (triggers): 3-6 itens
3. Recomendações Práticas (recommendations): 3-6 itens
@@ -697,11 +697,11 @@ export async function saveInsightsAction(
// Verificar se já existe um insight salvo para este período
const existing = await db
.select()
.from(insightsSalvos)
.from(savedInsights)
.where(
and(
eq(insightsSalvos.userId, user.id),
eq(insightsSalvos.period, period),
eq(savedInsights.userId, user.id),
eq(savedInsights.period, period),
),
)
.limit(1);
@@ -709,7 +709,7 @@ export async function saveInsightsAction(
if (existing.length > 0) {
// Atualizar existente
const updated = await db
.update(insightsSalvos)
.update(savedInsights)
.set({
modelId,
data: JSON.stringify(data),
@@ -717,13 +717,13 @@ export async function saveInsightsAction(
})
.where(
and(
eq(insightsSalvos.userId, user.id),
eq(insightsSalvos.period, period),
eq(savedInsights.userId, user.id),
eq(savedInsights.period, period),
),
)
.returning({
id: insightsSalvos.id,
createdAt: insightsSalvos.createdAt,
id: savedInsights.id,
createdAt: savedInsights.createdAt,
});
const updatedRecord = updated[0];
@@ -745,7 +745,7 @@ export async function saveInsightsAction(
// Criar novo
const result = await db
.insert(insightsSalvos)
.insert(savedInsights)
.values({
userId: user.id,
period,
@@ -753,8 +753,8 @@ export async function saveInsightsAction(
data: JSON.stringify(data),
})
.returning({
id: insightsSalvos.id,
createdAt: insightsSalvos.createdAt,
id: savedInsights.id,
createdAt: savedInsights.createdAt,
});
const insertedRecord = result[0];
@@ -796,11 +796,11 @@ export async function loadSavedInsightsAction(period: string): Promise<
const result = await db
.select()
.from(insightsSalvos)
.from(savedInsights)
.where(
and(
eq(insightsSalvos.userId, user.id),
eq(insightsSalvos.period, period),
eq(savedInsights.userId, user.id),
eq(savedInsights.period, period),
),
)
.limit(1);
@@ -849,11 +849,11 @@ export async function deleteSavedInsightsAction(
const user = await getUser();
await db
.delete(insightsSalvos)
.delete(savedInsights)
.where(
and(
eq(insightsSalvos.userId, user.id),
eq(insightsSalvos.period, period),
eq(savedInsights.userId, user.id),
eq(savedInsights.period, period),
),
);

View File

@@ -12,7 +12,7 @@ import { db } from "@/shared/lib/db";
import { recurringSeriesActionSchema } from "@/shared/lib/schemas/recurring-series";
import type { ActionResult } from "@/shared/lib/types/actions";
const revalidate = () => revalidateForEntity("recorrentes");
const revalidate = () => revalidateForEntity("recurring");
async function findRecurringSeriesForUser(userId: string, seriesId: string) {
const [series] = await db

View File

@@ -1,5 +1,5 @@
import { and, eq } from "drizzle-orm";
import { lancamentos, recurringSeries } from "@/db/schema";
import { recurringSeries, transactions } from "@/db/schema";
import { db } from "@/shared/lib/db";
import {
addMonthsToPeriod,
@@ -76,7 +76,7 @@ export async function generateRecurringTransactions(
const template = series.templateData;
// Create all lancamentos for missing periods in a transaction
// Create all transactions for missing periods in a transaction
await db.transaction(async (tx: typeof db) => {
const records = periodsToGenerate.map((period) => {
const purchaseDate = computePurchaseDate(period, series.dayOfMonth);
@@ -86,10 +86,10 @@ export async function generateRecurringTransactions(
transactionType: template.transactionType,
paymentMethod: template.paymentMethod,
condition: "Recorrente" as const,
categoriaId: template.categoriaId,
contaId: template.contaId,
cartaoId: template.cartaoId,
pagadorId: template.pagadorId,
categoryId: template.categoryId,
accountId: template.accountId,
cardId: template.cardId,
payerId: template.payerId,
note: template.note,
purchaseDate,
period,
@@ -104,7 +104,7 @@ export async function generateRecurringTransactions(
};
});
await tx.insert(lancamentos).values(records);
await tx.insert(transactions).values(records);
// Update lastGeneratedPeriod to the last period we generated
const lastPeriod =

View File

@@ -11,15 +11,9 @@ import {
sql,
sum,
} from "drizzle-orm";
import {
cartoes,
categorias,
faturas,
lancamentos,
pagadores,
} from "@/db/schema";
import { cards, categories, invoices, payers, transactions } from "@/db/schema";
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 { formatDateOnly } from "@/shared/utils/date";
import { safeToNumber } from "@/shared/utils/number";
import {
@@ -90,7 +84,7 @@ type CardRow = {
};
type CardUsageRow = {
cartaoId: string | null;
cardId: string | null;
totalAmount: unknown;
};
@@ -100,7 +94,7 @@ type MonthlyUsageRow = {
};
type CategoryAmountRow = {
categoriaId: string | null;
categoryId: string | null;
totalAmount: unknown;
};
@@ -115,7 +109,7 @@ type TopExpenseRow = {
name: string;
amount: unknown;
purchaseDate: Date | string | null;
categoriaId: string | null;
categoryId: string | null;
};
type InvoiceStatusRow = {
@@ -133,16 +127,16 @@ export async function fetchCartoesReportData(
// Fetch all active cards (not inactive)
const allCards = (await db
.select({
id: cartoes.id,
name: cartoes.name,
brand: cartoes.brand,
logo: cartoes.logo,
limit: cartoes.limit,
status: cartoes.status,
id: cards.id,
name: cards.name,
brand: cards.brand,
logo: cards.logo,
limit: cards.limit,
status: cards.status,
})
.from(cartoes)
.from(cards)
.where(
and(eq(cartoes.userId, userId), not(ilike(cartoes.status, "inativo"))),
and(eq(cards.userId, userId), not(ilike(cards.status, "inativo"))),
)) as CardRow[];
if (allCards.length === 0) {
@@ -160,67 +154,61 @@ export async function fetchCartoesReportData(
// Fetch current period usage by card (recorrente só conta quando a data da ocorrência já passou)
const currentUsageData = (await db
.select({
cartaoId: lancamentos.cartaoId,
totalAmount: sum(lancamentos.amount).as("total"),
cardId: transactions.cardId,
totalAmount: sum(transactions.amount).as("total"),
})
.from(lancamentos)
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
.from(transactions)
.innerJoin(payers, eq(transactions.payerId, payers.id))
.where(
and(
eq(lancamentos.userId, userId),
eq(lancamentos.period, currentPeriod),
eq(pagadores.role, PAGADOR_ROLE_ADMIN),
eq(lancamentos.transactionType, DESPESA),
inArray(lancamentos.cartaoId, cardIds),
eq(transactions.userId, userId),
eq(transactions.period, currentPeriod),
eq(payers.role, PAYER_ROLE_ADMIN),
eq(transactions.transactionType, DESPESA),
inArray(transactions.cardId, cardIds),
or(
ne(lancamentos.condition, "Recorrente"),
sql`${lancamentos.purchaseDate} <= current_date`,
ne(transactions.condition, "Recorrente"),
sql`${transactions.purchaseDate} <= current_date`,
),
),
)
.groupBy(lancamentos.cartaoId)) as CardUsageRow[];
.groupBy(transactions.cardId)) as CardUsageRow[];
// Fetch previous period usage by card
const previousUsageData = (await db
.select({
cartaoId: lancamentos.cartaoId,
totalAmount: sum(lancamentos.amount).as("total"),
cardId: transactions.cardId,
totalAmount: sum(transactions.amount).as("total"),
})
.from(lancamentos)
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
.from(transactions)
.innerJoin(payers, eq(transactions.payerId, payers.id))
.where(
and(
eq(lancamentos.userId, userId),
eq(lancamentos.period, previousPeriod),
eq(pagadores.role, PAGADOR_ROLE_ADMIN),
eq(lancamentos.transactionType, DESPESA),
inArray(lancamentos.cartaoId, cardIds),
eq(transactions.userId, userId),
eq(transactions.period, previousPeriod),
eq(payers.role, PAYER_ROLE_ADMIN),
eq(transactions.transactionType, DESPESA),
inArray(transactions.cardId, cardIds),
),
)
.groupBy(lancamentos.cartaoId)) as CardUsageRow[];
.groupBy(transactions.cardId)) as CardUsageRow[];
const currentUsageMap = new Map<string, number>();
for (const row of currentUsageData) {
if (row.cartaoId) {
currentUsageMap.set(
row.cartaoId,
Math.abs(safeToNumber(row.totalAmount)),
);
if (row.cardId) {
currentUsageMap.set(row.cardId, Math.abs(safeToNumber(row.totalAmount)));
}
}
const previousUsageMap = new Map<string, number>();
for (const row of previousUsageData) {
if (row.cartaoId) {
previousUsageMap.set(
row.cartaoId,
Math.abs(safeToNumber(row.totalAmount)),
);
if (row.cardId) {
previousUsageMap.set(row.cardId, Math.abs(safeToNumber(row.totalAmount)));
}
}
// Build card summaries
const cards: CardSummary[] = allCards.map((card) => {
const cardSummaries: CardSummary[] = allCards.map((card) => {
const limit = safeToNumber(card.limit);
const currentUsage = currentUsageMap.get(card.id) || 0;
const previousUsage = previousUsageMap.get(card.id) || 0;
@@ -252,22 +240,22 @@ export async function fetchCartoesReportData(
};
});
// Sort cards by usage (descending)
cards.sort((a, b) => b.currentUsage - a.currentUsage);
// Sort cardSummaries by usage (descending)
cardSummaries.sort((a, b) => b.currentUsage - a.currentUsage);
// Calculate totals
const totalLimit = cards.reduce((acc, c) => acc + c.limit, 0);
const totalUsage = cards.reduce((acc, c) => acc + c.currentUsage, 0);
const totalLimit = cardSummaries.reduce((acc, c) => acc + c.limit, 0);
const totalUsage = cardSummaries.reduce((acc, c) => acc + c.currentUsage, 0);
const totalUsagePercent =
totalLimit > 0 ? (totalUsage / totalLimit) * 100 : 0;
// Fetch selected card details if provided
let selectedCard: CardDetailData | null = null;
const targetCardId =
selectedCartaoId || (cards.length > 0 ? cards[0].id : null);
selectedCartaoId || (cardSummaries.length > 0 ? cardSummaries[0].id : null);
if (targetCardId) {
const cardSummary = cards.find((c) => c.id === targetCardId);
const cardSummary = cardSummaries.find((c) => c.id === targetCardId);
if (cardSummary) {
selectedCard = await fetchCardDetail(
userId,
@@ -279,7 +267,7 @@ export async function fetchCartoesReportData(
}
return {
cards,
cards: cardSummaries,
totalLimit,
totalUsage,
totalUsagePercent,
@@ -301,23 +289,23 @@ async function fetchCardDetail(
// Fetch monthly usage
const monthlyData = (await db
.select({
period: lancamentos.period,
totalAmount: sum(lancamentos.amount).as("total"),
period: transactions.period,
totalAmount: sum(transactions.amount).as("total"),
})
.from(lancamentos)
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
.from(transactions)
.innerJoin(payers, eq(transactions.payerId, payers.id))
.where(
and(
eq(lancamentos.userId, userId),
eq(lancamentos.cartaoId, cardId),
gte(lancamentos.period, startPeriod),
lte(lancamentos.period, currentPeriod),
eq(pagadores.role, PAGADOR_ROLE_ADMIN),
eq(lancamentos.transactionType, DESPESA),
eq(transactions.userId, userId),
eq(transactions.cardId, cardId),
gte(transactions.period, startPeriod),
lte(transactions.period, currentPeriod),
eq(payers.role, PAYER_ROLE_ADMIN),
eq(transactions.transactionType, DESPESA),
),
)
.groupBy(lancamentos.period)
.orderBy(lancamentos.period)) as MonthlyUsageRow[];
.groupBy(transactions.period)
.orderBy(transactions.period)) as MonthlyUsageRow[];
const monthlyUsage = periods.map((period) => {
const data = monthlyData.find((d) => d.period === period);
@@ -331,37 +319,37 @@ async function fetchCardDetail(
// Fetch category breakdown for current period
const categoryData = (await db
.select({
categoriaId: lancamentos.categoriaId,
totalAmount: sum(lancamentos.amount).as("total"),
categoryId: transactions.categoryId,
totalAmount: sum(transactions.amount).as("total"),
})
.from(lancamentos)
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
.from(transactions)
.innerJoin(payers, eq(transactions.payerId, payers.id))
.where(
and(
eq(lancamentos.userId, userId),
eq(lancamentos.cartaoId, cardId),
eq(lancamentos.period, currentPeriod),
eq(pagadores.role, PAGADOR_ROLE_ADMIN),
eq(lancamentos.transactionType, DESPESA),
eq(transactions.userId, userId),
eq(transactions.cardId, cardId),
eq(transactions.period, currentPeriod),
eq(payers.role, PAYER_ROLE_ADMIN),
eq(transactions.transactionType, DESPESA),
),
)
.groupBy(lancamentos.categoriaId)) as CategoryAmountRow[];
.groupBy(transactions.categoryId)) as CategoryAmountRow[];
// Fetch category names
const categoryIds = categoryData
.map((c) => c.categoriaId)
.map((c) => c.categoryId)
.filter((id): id is string => id !== null);
const categoryNames =
categoryIds.length > 0
? ((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))) as CategoryInfoRow[])
.from(categories)
.where(inArray(categories.id, categoryIds))) as CategoryInfoRow[])
: ([] as CategoryInfoRow[]);
const categoryNameMap = new Map(categoryNames.map((c) => [c.id, c]));
@@ -374,11 +362,11 @@ async function fetchCardDetail(
const categoryBreakdown = categoryData
.map((cat) => {
const amount = Math.abs(safeToNumber(cat.totalAmount));
const catInfo = cat.categoriaId
? categoryNameMap.get(cat.categoriaId)
const catInfo = cat.categoryId
? categoryNameMap.get(cat.categoryId)
: null;
return {
id: cat.categoriaId || "sem-categoria",
id: cat.categoryId || "sem-categoria",
name: catInfo?.name || "Sem categoria",
icon: catInfo?.icon || null,
amount,
@@ -392,29 +380,29 @@ async function fetchCardDetail(
// Fetch top expenses for current period
const topExpensesData = (await db
.select({
id: lancamentos.id,
name: lancamentos.name,
amount: lancamentos.amount,
purchaseDate: lancamentos.purchaseDate,
categoriaId: lancamentos.categoriaId,
id: transactions.id,
name: transactions.name,
amount: transactions.amount,
purchaseDate: transactions.purchaseDate,
categoryId: transactions.categoryId,
})
.from(lancamentos)
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
.from(transactions)
.innerJoin(payers, eq(transactions.payerId, payers.id))
.where(
and(
eq(lancamentos.userId, userId),
eq(lancamentos.cartaoId, cardId),
eq(lancamentos.period, currentPeriod),
eq(pagadores.role, PAGADOR_ROLE_ADMIN),
eq(lancamentos.transactionType, DESPESA),
eq(transactions.userId, userId),
eq(transactions.cardId, cardId),
eq(transactions.period, currentPeriod),
eq(payers.role, PAYER_ROLE_ADMIN),
eq(transactions.transactionType, DESPESA),
),
)
.orderBy(lancamentos.amount)
.orderBy(transactions.amount)
.limit(10)) as TopExpenseRow[];
const topExpenses = topExpensesData.map((expense) => {
const catInfo = expense.categoriaId
? categoryNameMap.get(expense.categoriaId)
const catInfo = expense.categoryId
? categoryNameMap.get(expense.categoryId)
: null;
return {
id: expense.id,
@@ -433,19 +421,19 @@ async function fetchCardDetail(
// Fetch invoice status for last 6 months
const invoiceData = (await db
.select({
period: faturas.period,
status: faturas.paymentStatus,
period: invoices.period,
status: invoices.paymentStatus,
})
.from(faturas)
.from(invoices)
.where(
and(
eq(faturas.userId, userId),
eq(faturas.cartaoId, cardId),
gte(faturas.period, startPeriod),
lte(faturas.period, currentPeriod),
eq(invoices.userId, userId),
eq(invoices.cardId, cardId),
gte(invoices.period, startPeriod),
lte(invoices.period, currentPeriod),
),
)
.orderBy(faturas.period)) as InvoiceStatusRow[];
.orderBy(invoices.period)) as InvoiceStatusRow[];
const invoiceStatus = periods.map((period) => {
const invoice = invoiceData.find((i) => i.period === period);

View File

@@ -1,8 +1,8 @@
import { and, eq, inArray, isNull, or, sql } from "drizzle-orm";
import { categorias, lancamentos } from "@/db/schema";
import { categories, transactions } from "@/db/schema";
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } 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";
import { formatPeriodMonthShort } from "@/shared/utils/period";
import { generatePeriodRange } from "./utils";
@@ -35,56 +35,56 @@ export async function fetchCategoryChartData(
): Promise<CategoryChartData> {
const periods = generatePeriodRange(startPeriod, endPeriod);
const adminPagadorId = await getAdminPagadorId(userId);
if (!adminPagadorId) {
const adminPayerId = await getAdminPayerId(userId);
if (!adminPayerId) {
return { months: [], categories: [], chartData: [], allCategories: [] };
}
const whereConditions = [
eq(lancamentos.userId, userId),
eq(lancamentos.pagadorId, adminPagadorId),
inArray(lancamentos.period, periods),
or(eq(categorias.type, "despesa"), eq(categorias.type, "receita")),
eq(transactions.userId, userId),
eq(transactions.payerId, adminPayerId),
inArray(transactions.period, periods),
or(eq(categories.type, "despesa"), eq(categories.type, "receita")),
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}%`}`,
),
];
if (categoryIds && categoryIds.length > 0) {
whereConditions.push(inArray(categorias.id, categoryIds));
whereConditions.push(inArray(categories.id, categoryIds));
}
const [rows, allCategoriesRows] = await Promise.all([
db
.select({
categoryId: categorias.id,
categoryName: categorias.name,
categoryIcon: categorias.icon,
categoryType: categorias.type,
period: lancamentos.period,
total: sql<number>`coalesce(sum(abs(${lancamentos.amount})), 0)`,
categoryId: categories.id,
categoryName: categories.name,
categoryIcon: categories.icon,
categoryType: categories.type,
period: transactions.period,
total: sql<number>`coalesce(sum(abs(${transactions.amount})), 0)`,
})
.from(lancamentos)
.innerJoin(categorias, eq(lancamentos.categoriaId, categorias.id))
.from(transactions)
.innerJoin(categories, eq(transactions.categoryId, categories.id))
.where(and(...whereConditions))
.groupBy(
categorias.id,
categorias.name,
categorias.icon,
categorias.type,
lancamentos.period,
categories.id,
categories.name,
categories.icon,
categories.type,
transactions.period,
),
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),
]);
const allCategories = allCategoriesRows.map(
@@ -143,12 +143,12 @@ export async function fetchCategoryChartData(
formatPeriodMonthShort(period).toUpperCase(),
);
const categories = Array.from(categoryMap.values()).map((cat) => ({
const categoryList = Array.from(categoryMap.values()).map((cat) => ({
id: cat.id,
name: cat.name,
icon: cat.icon,
type: cat.type,
}));
return { months, categories, chartData, allCategories };
return { months, categories: categoryList, chartData, allCategories };
}

View File

@@ -1,8 +1,8 @@
import { and, eq, inArray, isNull, or, sql } from "drizzle-orm";
import { categorias, lancamentos } from "@/db/schema";
import { categories, transactions } from "@/db/schema";
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } 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";
import type {
CategoryReportData,
@@ -28,47 +28,47 @@ export async function fetchCategoryReport(
// Generate all periods in the range
const periods = generatePeriodRange(startPeriod, endPeriod);
const adminPagadorId = await getAdminPagadorId(userId);
if (!adminPagadorId) {
const adminPayerId = await getAdminPayerId(userId);
if (!adminPayerId) {
return { categories: [], periods, totals: new Map(), grandTotal: 0 };
}
// Build WHERE conditions
const whereConditions = [
eq(lancamentos.userId, userId),
eq(lancamentos.pagadorId, adminPagadorId),
inArray(lancamentos.period, periods),
or(eq(categorias.type, "despesa"), eq(categorias.type, "receita")),
eq(transactions.userId, userId),
eq(transactions.payerId, adminPayerId),
inArray(transactions.period, periods),
or(eq(categories.type, "despesa"), eq(categories.type, "receita")),
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}%`}`,
),
];
// Add optional category filter
if (categoryIds && categoryIds.length > 0) {
whereConditions.push(inArray(categorias.id, categoryIds));
whereConditions.push(inArray(categories.id, categoryIds));
}
// Query to get aggregated data by category and period
const rows = await db
.select({
categoryId: categorias.id,
categoryName: categorias.name,
categoryIcon: categorias.icon,
categoryType: categorias.type,
period: lancamentos.period,
total: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`,
categoryId: categories.id,
categoryName: categories.name,
categoryIcon: categories.icon,
categoryType: categories.type,
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(...whereConditions))
.groupBy(
categorias.id,
categorias.name,
categorias.icon,
categorias.type,
lancamentos.period,
categories.id,
categories.name,
categories.icon,
categories.type,
transactions.period,
);
// Process results into CategoryReportData structure
@@ -171,10 +171,10 @@ export async function fetchCategoryReport(
}
// Convert to array and sort
const categories = Array.from(categoryMap.values());
const categoryList = Array.from(categoryMap.values());
// Sort: despesas first (by total desc), then receitas (by total desc)
categories.sort((a, b) => {
categoryList.sort((a, b) => {
// First by type: despesa comes before receita
if (a.type !== b.type) {
return a.type === "despesa" ? -1 : 1;
@@ -185,12 +185,12 @@ export async function fetchCategoryReport(
// Calculate grand total
let grandTotal = 0;
for (const categoryItem of categories) {
for (const categoryItem of categoryList) {
grandTotal += categoryItem.total;
}
return {
categories,
categories: categoryList,
periods,
totals: periodTotalsMap,
grandTotal,

View File

@@ -1,12 +1,10 @@
import { asc, eq } from "drizzle-orm";
import { type Categoria, categorias } from "@/db/schema";
import { type Category, categories } from "@/db/schema";
import { db } from "@/shared/lib/db";
export async function fetchUserCategories(
userId: string,
): Promise<Categoria[]> {
return db.query.categorias.findMany({
where: eq(categorias.userId, userId),
orderBy: [asc(categorias.name)],
export async function fetchUserCategories(userId: string): Promise<Category[]> {
return db.query.categories.findMany({
where: eq(categories.userId, userId),
orderBy: [asc(categories.name)],
});
}

View File

@@ -24,7 +24,7 @@ export function CardCategoryBreakdown({ data }: CardCategoryBreakdownProps) {
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-1.5 text-base">
<RiPieChartLine className="size-4 text-primary" />
Gastos por Categoria
Gastos por Category
</CardTitle>
</CardHeader>
<CardContent>
@@ -45,7 +45,7 @@ export function CardCategoryBreakdown({ data }: CardCategoryBreakdownProps) {
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-1.5 text-base">
<RiPieChartLine className="size-4 text-primary" />
Gastos por Categoria
Gastos por Category
</CardTitle>
</CardHeader>

View File

@@ -150,7 +150,7 @@ export function CategoryReportChart({ data }: CategoryReportChartProps) {
<Card className="pt-0">
<CardHeader className="flex items-center gap-2 space-y-0 border-b py-5 sm:flex-row">
<div className="grid flex-1 gap-1">
<CardTitle>Evolução por Categoria</CardTitle>
<CardTitle>Evolução por Category</CardTitle>
<CardDescription>{periodLabel}</CardDescription>
</div>
<Select value={limit} onValueChange={setLimit}>

View File

@@ -54,7 +54,7 @@ export function CategoryReportExport({
// Build CSV content
const headers = [
"Categoria",
"Category",
...data.periods.map(formatPeriodLabel),
"Total",
];
@@ -129,7 +129,7 @@ export function CategoryReportExport({
// Build data array
const headers = [
"Categoria",
"Category",
...data.periods.map(formatPeriodLabel),
"Total",
];
@@ -175,7 +175,7 @@ export function CategoryReportExport({
// Set column widths
ws["!cols"] = [
{ wch: 20 }, // Categoria
{ wch: 20 }, // Category
...data.periods.map(() => ({ wch: 15 })), // Periods
{ wch: 15 }, // Total
];
@@ -249,7 +249,7 @@ export function CategoryReportExport({
// Build table data
const headers = [
["Categoria", ...data.periods.map(formatPeriodLabel), "Total"],
["Category", ...data.periods.map(formatPeriodLabel), "Total"],
];
const body: string[][] = [];
@@ -310,7 +310,7 @@ export function CategoryReportExport({
fontStyle: "bold",
},
columnStyles: {
0: { cellWidth: 35 }, // Categoria column wider
0: { cellWidth: 35 }, // Category column wider
},
didParseCell: (cellData) => {
// Style totals row

View File

@@ -129,7 +129,7 @@ export function CategoryReportFilters({
const selectedText =
selectedCategories.length === 0
? "Categoria"
? "Category"
: selectedCategories.length === categories.length
? "Todas"
: selectedCategories.length === 1

View File

@@ -65,7 +65,7 @@ export function CategoryTable({
<TableHeader>
<TableRow>
<TableHead className="w-[240px] min-w-[240px] font-bold">
Categoria
Category
</TableHead>
{periods.map((period) => (
<TableHead

View File

@@ -13,13 +13,18 @@ import {
sql,
sum,
} from "drizzle-orm";
import { categorias, contas, lancamentos, pagadores } from "@/db/schema";
import {
categories,
financialAccounts,
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 { safeToNumber } from "@/shared/utils/number";
import { getPreviousPeriod } from "@/shared/utils/period";
@@ -68,7 +73,7 @@ function buildPeriodRange(currentPeriod: string, months: number): string[] {
return periods;
}
export async function fetchTopEstabelecimentosData(
export async function fetchTopEstablishmentsData(
userId: string,
currentPeriod: string,
periodFilter: PeriodFilter = "6",
@@ -80,33 +85,36 @@ export async function fetchTopEstabelecimentosData(
// Fetch establishments with transaction count and total amount
const establishmentsData = await db
.select({
name: lancamentos.name,
name: transactions.name,
count: count().as("count"),
totalAmount: sum(lancamentos.amount).as("total"),
totalAmount: sum(transactions.amount).as("total"),
})
.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),
gte(lancamentos.period, startPeriod),
lte(lancamentos.period, currentPeriod),
eq(pagadores.role, PAGADOR_ROLE_ADMIN),
eq(lancamentos.transactionType, DESPESA),
ne(lancamentos.transactionType, TRANSFERENCIA),
eq(transactions.userId, userId),
gte(transactions.period, startPeriod),
lte(transactions.period, currentPeriod),
eq(payers.role, PAYER_ROLE_ADMIN),
eq(transactions.transactionType, DESPESA),
ne(transactions.transactionType, TRANSFERENCIA),
or(
isNull(lancamentos.note),
not(ilike(lancamentos.note, `${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`)),
isNull(transactions.note),
not(ilike(transactions.note, `${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`)),
),
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),
),
),
)
.groupBy(lancamentos.name)
.groupBy(transactions.name)
.orderBy(desc(sql`count`))
.limit(50);
@@ -117,32 +125,32 @@ export async function fetchTopEstabelecimentosData(
const categoriesByEstablishment = await db
.select({
establishmentName: lancamentos.name,
categoriaId: lancamentos.categoriaId,
establishmentName: transactions.name,
categoryId: transactions.categoryId,
count: count().as("count"),
})
.from(lancamentos)
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
.from(transactions)
.innerJoin(payers, eq(transactions.payerId, payers.id))
.where(
and(
eq(lancamentos.userId, userId),
gte(lancamentos.period, startPeriod),
lte(lancamentos.period, currentPeriod),
eq(pagadores.role, PAGADOR_ROLE_ADMIN),
eq(lancamentos.transactionType, DESPESA),
eq(transactions.userId, userId),
gte(transactions.period, startPeriod),
lte(transactions.period, currentPeriod),
eq(payers.role, PAYER_ROLE_ADMIN),
eq(transactions.transactionType, DESPESA),
),
)
.groupBy(lancamentos.name, lancamentos.categoriaId);
.groupBy(transactions.name, transactions.categoryId);
// Fetch all category names
const allCategories = await db
.select({
id: categorias.id,
name: categorias.name,
icon: categorias.icon,
id: categories.id,
name: categories.name,
icon: categories.icon,
})
.from(categorias)
.where(eq(categorias.userId, userId));
.from(categories)
.where(eq(categories.userId, userId));
type CategoryInfo = { id: string; name: string; icon: string | null };
const categoryMap = new Map<string, CategoryInfo>(
@@ -161,11 +169,11 @@ export async function fetchTopEstabelecimentosData(
const estCategories = categoriesByEstablishment
.filter(
(c: CategoryByEstRow) =>
c.establishmentName === est.name && c.categoriaId,
c.establishmentName === est.name && c.categoryId,
)
.map((c: CategoryByEstRow) => ({
name:
categoryMap.get(c.categoriaId as string)?.name || "Sem categoria",
categoryMap.get(c.categoryId as string)?.name || "Sem categoria",
count: Number(c.count) || 0,
}))
.sort(
@@ -189,43 +197,46 @@ export async function fetchTopEstabelecimentosData(
// Fetch top categories by spending
const topCategoriesData = await db
.select({
categoriaId: lancamentos.categoriaId,
totalAmount: sum(lancamentos.amount).as("total"),
categoryId: transactions.categoryId,
totalAmount: sum(transactions.amount).as("total"),
count: count().as("count"),
})
.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),
gte(lancamentos.period, startPeriod),
lte(lancamentos.period, currentPeriod),
eq(pagadores.role, PAGADOR_ROLE_ADMIN),
eq(lancamentos.transactionType, DESPESA),
eq(transactions.userId, userId),
gte(transactions.period, startPeriod),
lte(transactions.period, currentPeriod),
eq(payers.role, PAYER_ROLE_ADMIN),
eq(transactions.transactionType, DESPESA),
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}%`)),
),
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),
),
),
)
.groupBy(lancamentos.categoriaId)
.groupBy(transactions.categoryId)
.orderBy(sql`total ASC`)
.limit(10);
type TopCategoryRow = (typeof topCategoriesData)[0];
const topCategories: TopCategoryData[] = topCategoriesData
.filter((c: TopCategoryRow) => c.categoriaId)
.filter((c: TopCategoryRow) => c.categoryId)
.map((cat: TopCategoryRow) => {
const catInfo = categoryMap.get(cat.categoriaId as string);
const catInfo = categoryMap.get(cat.categoryId as string);
return {
id: cat.categoriaId as string,
id: cat.categoryId as string,
name: catInfo?.name || "Sem categoria",
icon: catInfo?.icon || null,
totalAmount: Math.abs(safeToNumber(cat.totalAmount)),

File diff suppressed because it is too large Load Diff

View File

@@ -3,10 +3,10 @@
import { and, asc, desc, eq, inArray, isNull, or } from "drizzle-orm";
import { z } from "zod";
import {
antecipacoesParcelas,
categorias,
lancamentos,
pagadores,
categories,
installmentAnticipations,
payers,
transactions,
} from "@/db/schema";
import {
handleActionError,
@@ -47,8 +47,8 @@ const createAnticipationSchema = z.object({
.min(0, "Informe um desconto maior ou igual a zero.")
.optional()
.default(0),
pagadorId: uuidSchema("Pagador").optional(),
categoriaId: uuidSchema("Categoria").optional(),
payerId: uuidSchema("Payer").optional(),
categoryId: uuidSchema("Category").optional(),
note: z.string().trim().optional(),
});
@@ -72,16 +72,16 @@ export async function getEligibleInstallmentsAction(
const validatedSeriesId = uuidSchema("Série").parse(seriesId);
// Buscar todas as parcelas da série que estão elegíveis
const rows = await db.query.lancamentos.findMany({
const rows = await db.query.transactions.findMany({
where: and(
eq(lancamentos.seriesId, validatedSeriesId),
eq(lancamentos.userId, user.id),
eq(lancamentos.condition, "Parcelado"),
eq(transactions.seriesId, validatedSeriesId),
eq(transactions.userId, user.id),
eq(transactions.condition, "Parcelado"),
// Apenas parcelas não pagas e não antecipadas
or(eq(lancamentos.isSettled, false), isNull(lancamentos.isSettled)),
eq(lancamentos.isAnticipated, false),
or(eq(transactions.isSettled, false), isNull(transactions.isSettled)),
eq(transactions.isAnticipated, false),
),
orderBy: [asc(lancamentos.currentInstallment)],
orderBy: [asc(transactions.currentInstallment)],
columns: {
id: true,
name: true,
@@ -92,8 +92,8 @@ export async function getEligibleInstallmentsAction(
currentInstallment: true,
installmentCount: true,
paymentMethod: true,
categoriaId: true,
pagadorId: true,
categoryId: true,
payerId: true,
},
});
@@ -107,8 +107,8 @@ export async function getEligibleInstallmentsAction(
currentInstallment: row.currentInstallment,
installmentCount: row.installmentCount,
paymentMethod: row.paymentMethod,
categoriaId: row.categoriaId,
pagadorId: row.pagadorId,
categoryId: row.categoryId,
payerId: row.payerId,
}));
return {
@@ -132,13 +132,13 @@ export async function createInstallmentAnticipationAction(
const data = createAnticipationSchema.parse(input);
// 1. Validar parcelas selecionadas
const installments = await db.query.lancamentos.findMany({
const installments = await db.query.transactions.findMany({
where: and(
inArray(lancamentos.id, data.installmentIds),
eq(lancamentos.userId, user.id),
eq(lancamentos.seriesId, data.seriesId),
or(eq(lancamentos.isSettled, false), isNull(lancamentos.isSettled)),
eq(lancamentos.isAnticipated, false),
inArray(transactions.id, data.installmentIds),
eq(transactions.userId, user.id),
eq(transactions.seriesId, data.seriesId),
or(eq(transactions.isSettled, false), isNull(transactions.isSettled)),
eq(transactions.isAnticipated, false),
),
});
@@ -187,8 +187,8 @@ export async function createInstallmentAnticipationAction(
// 4. Criar lançamento e antecipação em transação
await db.transaction(async (tx: typeof db) => {
// 4.1. Criar o lançamento de antecipação (com desconto aplicado)
const [newLancamento] = await tx
.insert(lancamentos)
const [newLancamento] = (await tx
.insert(transactions)
.values({
name: generateAnticipationDescription(
firstInstallment.name,
@@ -202,10 +202,10 @@ export async function createInstallmentAnticipationAction(
period: data.anticipationPeriod,
dueDate: null,
isSettled: false,
pagadorId: data.pagadorId ?? firstInstallment.pagadorId,
categoriaId: data.categoriaId ?? firstInstallment.categoriaId,
cartaoId: firstInstallment.cartaoId,
contaId: firstInstallment.contaId,
payerId: data.payerId ?? firstInstallment.payerId,
categoryId: data.categoryId ?? firstInstallment.categoryId,
cardId: firstInstallment.cardId,
accountId: firstInstallment.accountId,
note:
data.note ||
generateAnticipationNote(
@@ -219,8 +219,8 @@ export async function createInstallmentAnticipationAction(
currentInstallment: inst.currentInstallment,
installmentCount: inst.installmentCount,
paymentMethod: inst.paymentMethod,
categoriaId: inst.categoriaId,
pagadorId: inst.pagadorId,
categoryId: inst.categoryId,
payerId: inst.payerId,
})),
),
userId: user.id,
@@ -234,11 +234,11 @@ export async function createInstallmentAnticipationAction(
anticipationId: null,
boletoPaymentDate: null,
})
.returning();
.returning()) as Array<typeof transactions.$inferSelect>;
// 4.2. Criar registro de antecipação
const [anticipation] = await tx
.insert(antecipacoesParcelas)
const [anticipation] = (await tx
.insert(installmentAnticipations)
.values({
seriesId: data.seriesId,
anticipationPeriod: data.anticipationPeriod,
@@ -247,26 +247,26 @@ export async function createInstallmentAnticipationAction(
totalAmount: formatDecimalForDbRequired(totalAmount),
installmentCount: installments.length,
discount: formatDecimalForDbRequired(discount),
lancamentoId: newLancamento.id,
pagadorId: data.pagadorId ?? firstInstallment.pagadorId,
categoriaId: data.categoriaId ?? firstInstallment.categoriaId,
transactionId: newLancamento.id,
payerId: data.payerId ?? firstInstallment.payerId,
categoryId: data.categoryId ?? firstInstallment.categoryId,
note: data.note || null,
userId: user.id,
})
.returning();
.returning()) as Array<typeof installmentAnticipations.$inferSelect>;
// 4.3. Marcar parcelas como antecipadas e zerar seus valores
await tx
.update(lancamentos)
.update(transactions)
.set({
isAnticipated: true,
anticipationId: anticipation.id,
amount: "0", // Zera o valor para não contar em dobro
})
.where(inArray(lancamentos.id, data.installmentIds));
.where(inArray(transactions.id, data.installmentIds));
});
revalidateForEntity("lancamentos");
revalidateForEntity("transactions");
return {
success: true,
@@ -296,40 +296,43 @@ export async function getInstallmentAnticipationsAction(
// Usar query builder ao invés de db.query para evitar problemas de tipagem
const anticipations = await db
.select({
id: antecipacoesParcelas.id,
seriesId: antecipacoesParcelas.seriesId,
anticipationPeriod: antecipacoesParcelas.anticipationPeriod,
anticipationDate: antecipacoesParcelas.anticipationDate,
id: installmentAnticipations.id,
seriesId: installmentAnticipations.seriesId,
anticipationPeriod: installmentAnticipations.anticipationPeriod,
anticipationDate: installmentAnticipations.anticipationDate,
anticipatedInstallmentIds:
antecipacoesParcelas.anticipatedInstallmentIds,
totalAmount: antecipacoesParcelas.totalAmount,
installmentCount: antecipacoesParcelas.installmentCount,
discount: antecipacoesParcelas.discount,
lancamentoId: antecipacoesParcelas.lancamentoId,
pagadorId: antecipacoesParcelas.pagadorId,
categoriaId: antecipacoesParcelas.categoriaId,
note: antecipacoesParcelas.note,
userId: antecipacoesParcelas.userId,
createdAt: antecipacoesParcelas.createdAt,
installmentAnticipations.anticipatedInstallmentIds,
totalAmount: installmentAnticipations.totalAmount,
installmentCount: installmentAnticipations.installmentCount,
discount: installmentAnticipations.discount,
transactionId: installmentAnticipations.transactionId,
payerId: installmentAnticipations.payerId,
categoryId: installmentAnticipations.categoryId,
note: installmentAnticipations.note,
userId: installmentAnticipations.userId,
createdAt: installmentAnticipations.createdAt,
// Joins
lancamento: lancamentos,
pagador: pagadores,
categoria: categorias,
transaction: transactions,
payer: payers,
category: categories,
})
.from(antecipacoesParcelas)
.from(installmentAnticipations)
.leftJoin(
lancamentos,
eq(antecipacoesParcelas.lancamentoId, lancamentos.id),
transactions,
eq(installmentAnticipations.transactionId, transactions.id),
)
.leftJoin(payers, eq(installmentAnticipations.payerId, payers.id))
.leftJoin(
categories,
eq(installmentAnticipations.categoryId, categories.id),
)
.leftJoin(pagadores, eq(antecipacoesParcelas.pagadorId, pagadores.id))
.leftJoin(categorias, eq(antecipacoesParcelas.categoriaId, categorias.id))
.where(
and(
eq(antecipacoesParcelas.seriesId, validatedSeriesId),
eq(antecipacoesParcelas.userId, user.id),
eq(installmentAnticipations.seriesId, validatedSeriesId),
eq(installmentAnticipations.userId, user.id),
),
)
.orderBy(desc(antecipacoesParcelas.createdAt));
.orderBy(desc(installmentAnticipations.createdAt));
return {
success: true,
@@ -358,32 +361,32 @@ export async function cancelInstallmentAnticipationAction(
// 1. Buscar antecipação usando query builder
const anticipationRows = await tx
.select({
id: antecipacoesParcelas.id,
seriesId: antecipacoesParcelas.seriesId,
anticipationPeriod: antecipacoesParcelas.anticipationPeriod,
anticipationDate: antecipacoesParcelas.anticipationDate,
id: installmentAnticipations.id,
seriesId: installmentAnticipations.seriesId,
anticipationPeriod: installmentAnticipations.anticipationPeriod,
anticipationDate: installmentAnticipations.anticipationDate,
anticipatedInstallmentIds:
antecipacoesParcelas.anticipatedInstallmentIds,
totalAmount: antecipacoesParcelas.totalAmount,
installmentCount: antecipacoesParcelas.installmentCount,
discount: antecipacoesParcelas.discount,
lancamentoId: antecipacoesParcelas.lancamentoId,
pagadorId: antecipacoesParcelas.pagadorId,
categoriaId: antecipacoesParcelas.categoriaId,
note: antecipacoesParcelas.note,
userId: antecipacoesParcelas.userId,
createdAt: antecipacoesParcelas.createdAt,
lancamento: lancamentos,
installmentAnticipations.anticipatedInstallmentIds,
totalAmount: installmentAnticipations.totalAmount,
installmentCount: installmentAnticipations.installmentCount,
discount: installmentAnticipations.discount,
transactionId: installmentAnticipations.transactionId,
payerId: installmentAnticipations.payerId,
categoryId: installmentAnticipations.categoryId,
note: installmentAnticipations.note,
userId: installmentAnticipations.userId,
createdAt: installmentAnticipations.createdAt,
transaction: transactions,
})
.from(antecipacoesParcelas)
.from(installmentAnticipations)
.leftJoin(
lancamentos,
eq(antecipacoesParcelas.lancamentoId, lancamentos.id),
transactions,
eq(installmentAnticipations.transactionId, transactions.id),
)
.where(
and(
eq(antecipacoesParcelas.id, data.anticipationId),
eq(antecipacoesParcelas.userId, user.id),
eq(installmentAnticipations.id, data.anticipationId),
eq(installmentAnticipations.userId, user.id),
),
)
.limit(1);
@@ -395,7 +398,7 @@ export async function cancelInstallmentAnticipationAction(
}
// 2. Verificar se o lançamento já foi pago
if (anticipation.lancamento?.isSettled === true) {
if (anticipation.transaction?.isSettled === true) {
throw new Error(
"Não é possível cancelar uma antecipação já paga. Remova o pagamento primeiro.",
);
@@ -408,7 +411,7 @@ export async function cancelInstallmentAnticipationAction(
// 4. Remover flag de antecipação e restaurar valores das parcelas
await tx
.update(lancamentos)
.update(transactions)
.set({
isAnticipated: false,
anticipationId: null,
@@ -416,23 +419,23 @@ export async function cancelInstallmentAnticipationAction(
})
.where(
inArray(
lancamentos.id,
transactions.id,
anticipation.anticipatedInstallmentIds as string[],
),
);
// 5. Deletar lançamento de antecipação
await tx
.delete(lancamentos)
.where(eq(lancamentos.id, anticipation.lancamentoId));
.delete(transactions)
.where(eq(transactions.id, anticipation.transactionId));
// 6. Deletar registro de antecipação
await tx
.delete(antecipacoesParcelas)
.where(eq(antecipacoesParcelas.id, data.anticipationId));
.delete(installmentAnticipations)
.where(eq(installmentAnticipations.id, data.anticipationId));
});
revalidateForEntity("lancamentos");
revalidateForEntity("transactions");
return {
success: true,
@@ -455,15 +458,15 @@ export async function getAnticipationDetailsAction(
// Validar anticipationId
const validatedId = uuidSchema("Antecipação").parse(anticipationId);
const anticipation = await db.query.antecipacoesParcelas.findFirst({
const anticipation = await db.query.installmentAnticipations.findFirst({
where: and(
eq(antecipacoesParcelas.id, validatedId),
eq(antecipacoesParcelas.userId, user.id),
eq(installmentAnticipations.id, validatedId),
eq(installmentAnticipations.userId, user.id),
),
with: {
lancamento: true,
pagador: true,
categoria: true,
transaction: true,
payer: true,
category: true,
},
});

View File

@@ -2,9 +2,9 @@ import type { SelectOption } from "@/features/transactions/components/types";
import { capitalize } from "@/shared/utils/string";
/**
* Group label for categorias
* Group label for category options
*/
type CategoriaGroup = {
type CategoryGroup = {
label: string;
options: SelectOption[];
};
@@ -24,15 +24,15 @@ function normalizeCategoryGroupLabel(value: string): string {
}
/**
* Groups and sorts categoria options by their group property
* @param categoriaOptions - Array of categoria select options
* @returns Array of grouped and sorted categoria options
* Groups and sorts category options by their group property
* @param categoryOptions - Array of category select options
* @returns Array of grouped and sorted category options
*/
export function groupAndSortCategorias(
categoriaOptions: SelectOption[],
): CategoriaGroup[] {
// Group categorias by their group property
const groups = categoriaOptions.reduce<Record<string, SelectOption[]>>(
export function groupAndSortCategories(
categoryOptions: SelectOption[],
): CategoryGroup[] {
// Group category options by their group property
const groups = categoryOptions.reduce<Record<string, SelectOption[]>>(
(acc, option) => {
const key = option.group ?? "Outros";
if (!acc[key]) {
@@ -63,11 +63,11 @@ export function groupAndSortCategorias(
}
/**
* Filters secondary pagador options to exclude the primary pagador
* Filters secondary payer options to exclude the primary payer
*/
export function filterSecondaryPagadorOptions(
export function filterSecondaryPayerOptions(
allOptions: SelectOption[],
primaryPagadorId?: string,
primaryPayerId?: string,
): SelectOption[] {
return allOptions.filter((option) => option.value !== primaryPagadorId);
return allOptions.filter((option) => option.value !== primaryPayerId);
}

View File

@@ -20,8 +20,8 @@ export const LANCAMENTOS_COLUMN_LABELS: Record<string, string> = {
amount: "Valor",
condition: "Condição",
paymentMethod: "Forma de Pagamento",
categoriaName: "Categoria",
pagadorName: "Pagador",
categoriaName: "Category",
pagadorName: "Payer",
note: "Anotação",
contaCartao: "Conta/Cartão",
};

View File

@@ -55,8 +55,8 @@ interface AnticipateInstallmentsDialogProps {
type AnticipationFormValues = {
anticipationPeriod: string;
discount: string;
pagadorId: string;
categoriaId: string;
payerId: string;
categoryId: string;
note: string;
};
@@ -90,8 +90,8 @@ export function AnticipateInstallmentsDialog({
useFormState<AnticipationFormValues>({
anticipationPeriod: defaultPeriod,
discount: "0",
pagadorId: "",
categoriaId: "",
payerId: "",
categoryId: "",
note: "",
});
@@ -119,8 +119,8 @@ export function AnticipateInstallmentsDialog({
replaceForm({
anticipationPeriod: defaultPeriod,
discount: "0",
pagadorId: first.pagadorId ?? "",
categoriaId: first.categoriaId ?? "",
payerId: first.payerId ?? "",
categoryId: first.categoryId ?? "",
note: "",
});
}
@@ -182,8 +182,8 @@ export function AnticipateInstallmentsDialog({
installmentIds: selectedIds,
anticipationPeriod: formState.anticipationPeriod,
discount: Number(formState.discount) || 0,
pagadorId: formState.pagadorId || undefined,
categoriaId: formState.categoriaId || undefined,
payerId: formState.payerId || undefined,
categoryId: formState.categoryId || undefined,
note: formState.note || undefined,
});
@@ -269,11 +269,11 @@ export function AnticipateInstallmentsDialog({
</Field>
<Field className="gap-1">
<FieldLabel htmlFor="anticipation-pagador">Pagador</FieldLabel>
<FieldLabel htmlFor="anticipation-pagador">Payer</FieldLabel>
<FieldContent>
<Select
value={formState.pagadorId}
onValueChange={(value) => updateField("pagadorId", value)}
value={formState.payerId}
onValueChange={(value) => updateField("payerId", value)}
disabled={isPending}
>
<SelectTrigger id="anticipation-pagador" className="w-full">
@@ -292,12 +292,12 @@ export function AnticipateInstallmentsDialog({
<Field className="gap-1">
<FieldLabel htmlFor="anticipation-categoria">
Categoria
Category
</FieldLabel>
<FieldContent>
<Select
value={formState.categoriaId}
onValueChange={(value) => updateField("categoriaId", value)}
value={formState.categoryId}
onValueChange={(value) => updateField("categoryId", value)}
disabled={isPending}
>
<SelectTrigger

View File

@@ -29,7 +29,7 @@ interface AnticipationHistoryDialogProps {
lancamentoName: string;
open?: boolean;
onOpenChange?: (open: boolean) => void;
onViewLancamento?: (lancamentoId: string) => void;
onViewLancamento?: (transactionId: string) => void;
}
export function AnticipationHistoryDialog({

View File

@@ -2,8 +2,8 @@
import { useMemo, useState, useTransition } from "react";
import { toast } from "sonner";
import { createLancamentoAction } from "@/features/transactions/actions";
import { groupAndSortCategorias } from "@/features/transactions/categoria-helpers";
import { createTransactionAction } from "@/features/transactions/actions";
import { groupAndSortCategories } from "@/features/transactions/category-helpers";
import { Button } from "@/shared/components/ui/button";
import {
Dialog,
@@ -24,46 +24,46 @@ import {
SelectValue,
} from "@/shared/components/ui/select";
import {
CategoriaSelectContent,
ContaCartaoSelectContent,
PagadorSelectContent,
CategorySelectContent,
AccountCardSelectContent,
PayerSelectContent,
} from "../select-items";
import type { LancamentoItem, SelectOption } from "../types";
import type { SelectOption, TransactionItem } from "../types";
interface BulkImportDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
items: LancamentoItem[];
pagadorOptions: SelectOption[];
contaOptions: SelectOption[];
cartaoOptions: SelectOption[];
categoriaOptions: SelectOption[];
defaultPagadorId?: string | null;
items: TransactionItem[];
payerOptions: SelectOption[];
accountOptions: SelectOption[];
cardOptions: SelectOption[];
categoryOptions: SelectOption[];
defaultPayerId?: string | null;
}
export function BulkImportDialog({
open,
onOpenChange,
items,
pagadorOptions,
contaOptions,
cartaoOptions,
categoriaOptions,
defaultPagadorId,
payerOptions,
accountOptions,
cardOptions,
categoryOptions,
defaultPayerId,
}: BulkImportDialogProps) {
const [pagadorId, setPagadorId] = useState<string | undefined>(
defaultPagadorId ?? undefined,
const [payerId, setPagadorId] = useState<string | undefined>(
defaultPayerId ?? undefined,
);
const [categoriaId, setCategoriaId] = useState<string | undefined>(undefined);
const [contaId, setContaId] = useState<string | undefined>(undefined);
const [cartaoId, setCartaoId] = useState<string | undefined>(undefined);
const [categoryId, setCategoriaId] = useState<string | undefined>(undefined);
const [accountId, setContaId] = useState<string | undefined>(undefined);
const [cardId, setCartaoId] = useState<string | undefined>(undefined);
const [isPending, startTransition] = useTransition();
type CreateLancamentoInput = Parameters<typeof createLancamentoAction>[0];
type CreateTransactionInput = Parameters<typeof createTransactionAction>[0];
// Reset form when dialog opens/closes
const handleOpenChange = (newOpen: boolean) => {
if (!newOpen) {
setPagadorId(defaultPagadorId ?? undefined);
setPagadorId(defaultPayerId ?? undefined);
setCategoriaId(undefined);
setContaId(undefined);
setCartaoId(undefined);
@@ -71,30 +71,30 @@ export function BulkImportDialog({
onOpenChange(newOpen);
};
const categoriaGroups = useMemo(() => {
const categoryGroups = useMemo(() => {
// Get unique transaction types from items
const transactionTypes = new Set(items.map((item) => item.transactionType));
// Filter categories based on transaction types
const filtered = categoriaOptions.filter((option) => {
const filtered = categoryOptions.filter((option) => {
if (!option.group) return false;
return Array.from(transactionTypes).some(
(type) => option.group?.toLowerCase() === type.toLowerCase(),
);
});
return groupAndSortCategorias(filtered);
}, [categoriaOptions, items]);
return groupAndSortCategories(filtered);
}, [categoryOptions, items]);
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!pagadorId) {
if (!payerId) {
toast.error("Selecione o pagador.");
return;
}
if (!categoriaId) {
if (!categoryId) {
toast.error("Selecione a categoria.");
return;
}
@@ -110,32 +110,32 @@ export function BulkImportDialog({
const isCredit = item.paymentMethod === "Cartão de crédito";
// Validate payment method fields
if (isCredit && !cartaoId) {
if (isCredit && !cardId) {
toast.error("Selecione um cartão de crédito.");
return;
}
if (!isCredit && !contaId) {
if (!isCredit && !accountId) {
toast.error("Selecione uma conta.");
return;
}
const payload: CreateLancamentoInput = {
const payload: CreateTransactionInput = {
purchaseDate: item.purchaseDate,
period: item.period,
name: item.name,
transactionType:
item.transactionType as CreateLancamentoInput["transactionType"],
item.transactionType as CreateTransactionInput["transactionType"],
amount: sanitizedAmount,
condition: item.condition as CreateLancamentoInput["condition"],
condition: item.condition as CreateTransactionInput["condition"],
paymentMethod:
item.paymentMethod as CreateLancamentoInput["paymentMethod"],
pagadorId: pagadorId ?? null,
secondaryPagadorId: undefined,
item.paymentMethod as CreateTransactionInput["paymentMethod"],
payerId: payerId ?? null,
secondaryPayerId: undefined,
isSplit: false,
contaId: isCredit ? null : (contaId ?? null),
cartaoId: isCredit ? (cartaoId ?? null) : null,
categoriaId: categoriaId ?? null,
accountId: isCredit ? null : (accountId ?? null),
cardId: isCredit ? (cardId ?? null) : null,
categoryId: categoryId ?? null,
note: item.note ?? null,
isSettled: isCredit ? null : Boolean(item.isSettled),
installmentCount:
@@ -152,7 +152,7 @@ export function BulkImportDialog({
: undefined,
};
const result = await createLancamentoAction(payload);
const result = await createTransactionAction(payload);
if (result.success) {
successCount++;
@@ -203,17 +203,17 @@ export function BulkImportDialog({
<form className="space-y-4" onSubmit={handleSubmit}>
<div className="space-y-2">
<Label htmlFor="pagador">Pagador *</Label>
<Select value={pagadorId} onValueChange={setPagadorId}>
<Label htmlFor="pagador">Payer *</Label>
<Select value={payerId} onValueChange={setPagadorId}>
<SelectTrigger id="pagador" className="w-full">
<SelectValue placeholder="Selecione o pagador">
{pagadorId &&
{payerId &&
(() => {
const selectedOption = pagadorOptions.find(
(opt) => opt.value === pagadorId,
const selectedOption = payerOptions.find(
(opt) => opt.value === payerId,
);
return selectedOption ? (
<PagadorSelectContent
<PayerSelectContent
label={selectedOption.label}
avatarUrl={selectedOption.avatarUrl}
/>
@@ -222,9 +222,9 @@ export function BulkImportDialog({
</SelectValue>
</SelectTrigger>
<SelectContent>
{pagadorOptions.map((option) => (
{payerOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<PagadorSelectContent
<PayerSelectContent
label={option.label}
avatarUrl={option.avatarUrl}
/>
@@ -235,17 +235,17 @@ export function BulkImportDialog({
</div>
<div className="space-y-2">
<Label htmlFor="categoria">Categoria *</Label>
<Select value={categoriaId} onValueChange={setCategoriaId}>
<Label htmlFor="categoria">Category *</Label>
<Select value={categoryId} onValueChange={setCategoriaId}>
<SelectTrigger id="categoria" className="w-full">
<SelectValue placeholder="Selecione a categoria">
{categoriaId &&
{categoryId &&
(() => {
const selectedOption = categoriaOptions.find(
(opt) => opt.value === categoriaId,
const selectedOption = categoryOptions.find(
(opt) => opt.value === categoryId,
);
return selectedOption ? (
<CategoriaSelectContent
<CategorySelectContent
label={selectedOption.label}
icon={selectedOption.icon}
/>
@@ -254,12 +254,12 @@ export function BulkImportDialog({
</SelectValue>
</SelectTrigger>
<SelectContent>
{categoriaGroups.map((group) => (
{categoryGroups.map((group) => (
<SelectGroup key={group.label}>
<SelectLabel>{group.label}</SelectLabel>
{group.options.map((option) => (
<SelectItem key={option.value} value={option.value}>
<CategoriaSelectContent
<CategorySelectContent
label={option.label}
icon={option.icon}
/>
@@ -274,18 +274,18 @@ export function BulkImportDialog({
{hasNonCredit && (
<div className="space-y-2">
<Label htmlFor="conta">
Conta {hasCredit ? "(para não cartão)" : "*"}
FinancialAccount {hasCredit ? "(para não cartão)" : "*"}
</Label>
<Select value={contaId} onValueChange={setContaId}>
<Select value={accountId} onValueChange={setContaId}>
<SelectTrigger id="conta" className="w-full">
<SelectValue placeholder="Selecione a conta">
{contaId &&
{accountId &&
(() => {
const selectedOption = contaOptions.find(
(opt) => opt.value === contaId,
const selectedOption = accountOptions.find(
(opt) => opt.value === accountId,
);
return selectedOption ? (
<ContaCartaoSelectContent
<AccountCardSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={false}
@@ -295,9 +295,9 @@ export function BulkImportDialog({
</SelectValue>
</SelectTrigger>
<SelectContent>
{contaOptions.map((option) => (
{accountOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<ContaCartaoSelectContent
<AccountCardSelectContent
label={option.label}
logo={option.logo}
isCartao={false}
@@ -314,16 +314,16 @@ export function BulkImportDialog({
<Label htmlFor="cartao">
Cartão {hasNonCredit ? "(para cartão de crédito)" : "*"}
</Label>
<Select value={cartaoId} onValueChange={setCartaoId}>
<Select value={cardId} onValueChange={setCartaoId}>
<SelectTrigger id="cartao" className="w-full">
<SelectValue placeholder="Selecione o cartão">
{cartaoId &&
{cardId &&
(() => {
const selectedOption = cartaoOptions.find(
(opt) => opt.value === cartaoId,
const selectedOption = cardOptions.find(
(opt) => opt.value === cardId,
);
return selectedOption ? (
<ContaCartaoSelectContent
<AccountCardSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={true}
@@ -333,9 +333,9 @@ export function BulkImportDialog({
</SelectValue>
</SelectTrigger>
<SelectContent>
{cartaoOptions.map((option) => (
{cardOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<ContaCartaoSelectContent
<AccountCardSelectContent
label={option.label}
logo={option.logo}
isCartao={true}

View File

@@ -3,10 +3,10 @@
import { RiAddLine, RiDeleteBinLine } from "@remixicon/react";
import { useMemo, useState } from "react";
import { toast } from "sonner";
import { groupAndSortCategorias } from "@/features/transactions/categoria-helpers";
import { groupAndSortCategories } from "@/features/transactions/category-helpers";
import {
LANCAMENTO_PAYMENT_METHODS,
type LANCAMENTO_TRANSACTION_TYPES,
PAYMENT_METHODS,
type TRANSACTION_TYPES,
} from "@/features/transactions/constants";
import { Button } from "@/shared/components/ui/button";
import { CurrencyInput } from "@/shared/components/ui/currency-input";
@@ -44,9 +44,9 @@ import {
periodToDate,
} from "@/shared/utils/period";
import {
CategoriaSelectContent,
ContaCartaoSelectContent,
PagadorSelectContent,
CategorySelectContent,
AccountCardSelectContent,
PayerSelectContent,
PaymentMethodSelectContent,
TransactionTypeSelectContent,
} from "../select-items";
@@ -54,11 +54,9 @@ import { EstabelecimentoInput } from "../shared/establishment-input";
import type { SelectOption } from "../types";
/** Payment methods sem Boleto para este modal */
const MASS_ADD_PAYMENT_METHODS = LANCAMENTO_PAYMENT_METHODS.filter(
(m) => m !== "Boleto",
);
type MassAddTransactionType = (typeof LANCAMENTO_TRANSACTION_TYPES)[number];
type MassAddPaymentMethod = (typeof LANCAMENTO_PAYMENT_METHODS)[number];
const MASS_ADD_PAYMENT_METHODS = PAYMENT_METHODS.filter((m) => m !== "Boleto");
type MassAddTransactionType = (typeof TRANSACTION_TYPES)[number];
type MassAddPaymentMethod = (typeof PAYMENT_METHODS)[number];
function InlinePeriodPicker({
period,
@@ -71,7 +69,7 @@ function InlinePeriodPicker({
return (
<div className="-mt-1">
<span className="text-xs text-muted-foreground">Fatura de </span>
<span className="text-xs text-muted-foreground">Invoice de </span>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
@@ -99,18 +97,18 @@ interface MassAddDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSubmit: (data: MassAddFormData) => Promise<void>;
pagadorOptions: SelectOption[];
contaOptions: SelectOption[];
cartaoOptions: SelectOption[];
categoriaOptions: SelectOption[];
payerOptions: SelectOption[];
accountOptions: SelectOption[];
cardOptions: SelectOption[];
categoryOptions: SelectOption[];
estabelecimentos: string[];
selectedPeriod: string;
defaultPagadorId?: string | null;
defaultCartaoId?: string | null;
defaultPayerId?: string | null;
defaultCardId?: string | null;
}
export type MassAddFormData = Parameters<
typeof import("@/features/transactions/actions").createMassLancamentosAction
typeof import("@/features/transactions/actions").createMassTransactionsAction
>[0];
interface TransactionRow {
@@ -118,22 +116,22 @@ interface TransactionRow {
purchaseDate: string;
name: string;
amount: string;
categoriaId: string | undefined;
pagadorId: string | undefined;
categoryId: string | undefined;
payerId: string | undefined;
}
export function MassAddDialog({
open,
onOpenChange,
onSubmit,
pagadorOptions,
contaOptions,
cartaoOptions,
categoriaOptions,
payerOptions,
accountOptions,
cardOptions,
categoryOptions,
estabelecimentos,
selectedPeriod,
defaultPagadorId,
defaultCartaoId,
defaultPayerId,
defaultCardId,
}: MassAddDialogProps) {
const [loading, setLoading] = useState(false);
@@ -141,16 +139,16 @@ export function MassAddDialog({
const [transactionType, setTransactionType] =
useState<MassAddTransactionType>("Despesa");
const [paymentMethod, setPaymentMethod] = useState<MassAddPaymentMethod>(
LANCAMENTO_PAYMENT_METHODS[0],
PAYMENT_METHODS[0],
);
const [period, setPeriod] = useState<string>(selectedPeriod);
const [contaId, setContaId] = useState<string | undefined>(undefined);
const [cartaoId, setCartaoId] = useState<string | undefined>(
defaultCartaoId ?? undefined,
const [accountId, setContaId] = useState<string | undefined>(undefined);
const [cardId, setCartaoId] = useState<string | undefined>(
defaultCardId ?? undefined,
);
// Quando defaultCartaoId está definido, exibe apenas o cartão específico
const isLockedToCartao = !!defaultCartaoId;
// Quando defaultCardId está definido, exibe apenas o cartão específico
const isLockedToCartao = !!defaultCardId;
const isCartaoSelected = paymentMethod === "Cartão de crédito";
@@ -161,18 +159,18 @@ export function MassAddDialog({
purchaseDate: getTodayDateString(),
name: "",
amount: "",
categoriaId: undefined,
pagadorId: defaultPagadorId ?? undefined,
categoryId: undefined,
payerId: defaultPayerId ?? undefined,
},
]);
// Categorias agrupadas e filtradas por tipo de transação
const groupedCategorias = useMemo(() => {
const filtered = categoriaOptions.filter(
const filtered = categoryOptions.filter(
(option) => option.group?.toLowerCase() === transactionType.toLowerCase(),
);
return groupAndSortCategorias(filtered);
}, [categoriaOptions, transactionType]);
return groupAndSortCategories(filtered);
}, [categoryOptions, transactionType]);
const addTransaction = () => {
setTransactions([
@@ -182,8 +180,8 @@ export function MassAddDialog({
purchaseDate: getTodayDateString(),
name: "",
amount: "",
categoriaId: undefined,
pagadorId: defaultPagadorId ?? undefined,
categoryId: undefined,
payerId: defaultPayerId ?? undefined,
},
]);
};
@@ -208,11 +206,11 @@ export function MassAddDialog({
const handleSubmit = async () => {
// Validate conta/cartao selection
if (isCartaoSelected && !cartaoId) {
if (isCartaoSelected && !cardId) {
toast.error("Selecione um cartão para continuar");
return;
}
if (!isCartaoSelected && !contaId) {
if (!isCartaoSelected && !accountId) {
toast.error("Selecione uma conta para continuar");
return;
}
@@ -236,15 +234,15 @@ export function MassAddDialog({
paymentMethod,
condition: "À vista",
period,
contaId,
cartaoId,
accountId,
cardId,
},
transactions: transactions.map((t) => ({
purchaseDate: t.purchaseDate,
name: t.name.trim(),
amount: Number(t.amount.trim()),
categoriaId: t.categoriaId,
pagadorId: t.pagadorId,
categoryId: t.categoryId,
payerId: t.payerId,
})),
};
@@ -254,18 +252,18 @@ export function MassAddDialog({
onOpenChange(false);
// Reset form
setTransactionType("Despesa");
setPaymentMethod(LANCAMENTO_PAYMENT_METHODS[0]);
setPaymentMethod(PAYMENT_METHODS[0]);
setPeriod(selectedPeriod);
setContaId(undefined);
setCartaoId(defaultCartaoId ?? undefined);
setCartaoId(defaultCardId ?? undefined);
setTransactions([
{
id: crypto.randomUUID(),
purchaseDate: getTodayDateString(),
name: "",
amount: "",
categoriaId: undefined,
pagadorId: defaultPagadorId ?? undefined,
categoryId: undefined,
payerId: defaultPayerId ?? undefined,
},
]);
} catch (_error) {
@@ -356,19 +354,19 @@ export function MassAddDialog({
<div className="space-y-2">
<Label htmlFor="cartao">Cartão</Label>
<Select
value={cartaoId}
value={cardId}
onValueChange={setCartaoId}
disabled={isLockedToCartao}
>
<SelectTrigger id="cartao" className="w-full">
<SelectValue placeholder="Selecione">
{cartaoId &&
{cardId &&
(() => {
const selectedOption = cartaoOptions.find(
(opt) => opt.value === cartaoId,
const selectedOption = cardOptions.find(
(opt) => opt.value === cardId,
);
return selectedOption ? (
<ContaCartaoSelectContent
<AccountCardSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={true}
@@ -378,22 +376,22 @@ export function MassAddDialog({
</SelectValue>
</SelectTrigger>
<SelectContent>
{cartaoOptions.length === 0 ? (
{cardOptions.length === 0 ? (
<div className="px-2 py-6 text-center">
<p className="text-sm text-muted-foreground">
Nenhum cartão cadastrado
</p>
</div>
) : (
cartaoOptions
cardOptions
.filter(
(option) =>
!isLockedToCartao ||
option.value === defaultCartaoId,
option.value === defaultCardId,
)
.map((option) => (
<SelectItem key={option.value} value={option.value}>
<ContaCartaoSelectContent
<AccountCardSelectContent
label={option.label}
logo={option.logo}
isCartao={true}
@@ -403,7 +401,7 @@ export function MassAddDialog({
)}
</SelectContent>
</Select>
{cartaoId ? (
{cardId ? (
<InlinePeriodPicker
period={period}
onPeriodChange={setPeriod}
@@ -412,20 +410,20 @@ export function MassAddDialog({
</div>
) : null}
{/* Conta (for non-credit-card methods) */}
{/* FinancialAccount (for non-credit-card methods) */}
{!isCartaoSelected ? (
<div className="space-y-2">
<Label htmlFor="conta">Conta</Label>
<Select value={contaId} onValueChange={setContaId}>
<Label htmlFor="conta">FinancialAccount</Label>
<Select value={accountId} onValueChange={setContaId}>
<SelectTrigger id="conta" className="w-full">
<SelectValue placeholder="Selecione">
{contaId &&
{accountId &&
(() => {
const selectedOption = contaOptions.find(
(opt) => opt.value === contaId,
const selectedOption = accountOptions.find(
(opt) => opt.value === accountId,
);
return selectedOption ? (
<ContaCartaoSelectContent
<AccountCardSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={false}
@@ -435,16 +433,16 @@ export function MassAddDialog({
</SelectValue>
</SelectTrigger>
<SelectContent>
{contaOptions.length === 0 ? (
{accountOptions.length === 0 ? (
<div className="px-2 py-6 text-center">
<p className="text-sm text-muted-foreground">
Nenhuma conta cadastrada
</p>
</div>
) : (
contaOptions.map((option) => (
accountOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<ContaCartaoSelectContent
<AccountCardSelectContent
label={option.label}
logo={option.logo}
isCartao={false}
@@ -536,26 +534,26 @@ export function MassAddDialog({
htmlFor={`pagador-${transaction.id}`}
className="sr-only"
>
Pagador {index + 1}
Payer {index + 1}
</Label>
<Select
value={transaction.pagadorId}
value={transaction.payerId}
onValueChange={(value) =>
updateTransaction(transaction.id, "pagadorId", value)
updateTransaction(transaction.id, "payerId", value)
}
>
<SelectTrigger
id={`pagador-${transaction.id}`}
className="w-32 truncate"
>
<SelectValue placeholder="Pagador">
{transaction.pagadorId &&
<SelectValue placeholder="Payer">
{transaction.payerId &&
(() => {
const selectedOption = pagadorOptions.find(
(opt) => opt.value === transaction.pagadorId,
const selectedOption = payerOptions.find(
(opt) => opt.value === transaction.payerId,
);
return selectedOption ? (
<PagadorSelectContent
<PayerSelectContent
label={selectedOption.label}
avatarUrl={selectedOption.avatarUrl}
/>
@@ -564,9 +562,9 @@ export function MassAddDialog({
</SelectValue>
</SelectTrigger>
<SelectContent>
{pagadorOptions.map((option) => (
{payerOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<PagadorSelectContent
<PayerSelectContent
label={option.label}
avatarUrl={option.avatarUrl}
/>
@@ -581,23 +579,19 @@ export function MassAddDialog({
htmlFor={`categoria-${transaction.id}`}
className="sr-only"
>
Categoria {index + 1}
Category {index + 1}
</Label>
<Select
value={transaction.categoriaId}
value={transaction.categoryId}
onValueChange={(value) =>
updateTransaction(
transaction.id,
"categoriaId",
value,
)
updateTransaction(transaction.id, "categoryId", value)
}
>
<SelectTrigger
id={`categoria-${transaction.id}`}
className="w-32 truncate"
>
<SelectValue placeholder="Categoria" />
<SelectValue placeholder="Category" />
</SelectTrigger>
<SelectContent>
{groupedCategorias.map((group) => (
@@ -608,7 +602,7 @@ export function MassAddDialog({
key={option.value}
value={option.value}
>
<CategoriaSelectContent
<CategorySelectContent
label={option.label}
icon={option.icon}
/>

View File

@@ -25,29 +25,29 @@ import { Separator } from "@/shared/components/ui/separator";
import { parseLocalDateString } from "@/shared/utils/date";
import { getPaymentMethodIcon } from "@/shared/utils/icons";
import { InstallmentTimeline } from "../shared/installment-timeline";
import type { LancamentoItem } from "../types";
import type { TransactionItem } from "../types";
interface LancamentoDetailsDialogProps {
interface TransactionDetailsDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
lancamento: LancamentoItem | null;
transaction: TransactionItem | null;
}
export function LancamentoDetailsDialog({
export function TransactionDetailsDialog({
open,
onOpenChange,
lancamento,
}: LancamentoDetailsDialogProps) {
if (!lancamento) return null;
transaction,
}: TransactionDetailsDialogProps) {
if (!transaction) return null;
const isInstallment =
lancamento.condition?.toLowerCase() === "parcelado" &&
lancamento.currentInstallment &&
lancamento.installmentCount;
transaction.condition?.toLowerCase() === "parcelado" &&
transaction.currentInstallment &&
transaction.installmentCount;
const valorParcela = Math.abs(lancamento.amount);
const totalParcelas = lancamento.installmentCount ?? 1;
const parcelaAtual = lancamento.currentInstallment ?? 1;
const valorParcela = Math.abs(transaction.amount);
const totalParcelas = transaction.installmentCount ?? 1;
const parcelaAtual = transaction.currentInstallment ?? 1;
const valorTotal = isInstallment
? valorParcela * totalParcelas
: valorParcela;
@@ -62,10 +62,10 @@ export function LancamentoDetailsDialog({
<CardHeader className="flex flex-row items-start border-b sm:border-b-0">
<div>
<DialogTitle className="group flex items-center gap-2 text-lg">
#{lancamento.id}
#{transaction.id}
</DialogTitle>
<CardDescription>
{formatDate(lancamento.purchaseDate)}
{formatDate(transaction.purchaseDate)}
</CardDescription>
</div>
</CardHeader>
@@ -73,11 +73,11 @@ export function LancamentoDetailsDialog({
<CardContent className="text-sm">
<div className="grid gap-3">
<ul className="grid gap-3">
<DetailRow label="Descrição" value={lancamento.name} />
<DetailRow label="Descrição" value={transaction.name} />
<DetailRow
label="Período"
value={formatPeriod(lancamento.period)}
value={formatPeriod(transaction.period)}
/>
<li className="flex items-center justify-between">
@@ -85,21 +85,21 @@ export function LancamentoDetailsDialog({
Forma de Pagamento
</span>
<span className="flex items-center gap-1.5">
{getPaymentMethodIcon(lancamento.paymentMethod)}
{getPaymentMethodIcon(transaction.paymentMethod)}
<span className="capitalize">
{lancamento.paymentMethod}
{transaction.paymentMethod}
</span>
</span>
</li>
<DetailRow
label={lancamento.cartaoName ? "Cartão" : "Conta"}
value={lancamento.cartaoName ?? lancamento.contaName ?? "—"}
label={transaction.cartaoName ? "Cartão" : "FinancialAccount"}
value={transaction.cartaoName ?? transaction.contaName ?? "—"}
/>
<DetailRow
label="Categoria"
value={lancamento.categoriaName ?? "—"}
label="Category"
value={transaction.categoriaName ?? "—"}
/>
<li className="flex items-center justify-between">
@@ -109,37 +109,37 @@ export function LancamentoDetailsDialog({
<span className="capitalize">
<Badge
variant={getTransactionBadgeVariant(
lancamento.categoriaName === "Saldo inicial"
transaction.categoriaName === "Saldo inicial"
? "Saldo inicial"
: lancamento.transactionType,
: transaction.transactionType,
)}
>
{lancamento.categoriaName === "Saldo inicial"
{transaction.categoriaName === "Saldo inicial"
? "Saldo Inicial"
: lancamento.transactionType}
: transaction.transactionType}
</Badge>
</span>
</li>
<DetailRow
label="Condição"
value={formatCondition(lancamento.condition)}
value={formatCondition(transaction.condition)}
/>
<li className="flex items-center justify-between">
<span className="text-muted-foreground">Responsável</span>
<span className="flex items-center gap-2 capitalize">
<span>{lancamento.pagadorName}</span>
<span>{transaction.pagadorName}</span>
</span>
</li>
<DetailRow
label="Status"
value={lancamento.isSettled ? "Pago" : "Pendente"}
value={transaction.isSettled ? "Pago" : "Pendente"}
/>
{lancamento.note && (
<DetailRow label="Notas" value={lancamento.note} />
{transaction.note && (
<DetailRow label="Notas" value={transaction.note} />
)}
</ul>
@@ -148,11 +148,11 @@ export function LancamentoDetailsDialog({
<li className="mt-4">
<InstallmentTimeline
purchaseDate={parseLocalDateString(
lancamento.purchaseDate,
transaction.purchaseDate,
)}
currentInstallment={parcelaAtual}
totalInstallments={totalParcelas}
period={lancamento.period}
period={transaction.period}
/>
</li>
)}
@@ -169,10 +169,10 @@ export function LancamentoDetailsDialog({
/>
)}
{lancamento.recurrenceCount && (
{transaction.recurrenceCount && (
<DetailRow
label="Quantidade de Recorrências"
value={`${lancamento.recurrenceCount} meses`}
value={`${transaction.recurrenceCount} meses`}
/>
)}

View File

@@ -1,6 +1,6 @@
"use client";
import { LANCAMENTO_TRANSACTION_TYPES } from "@/features/transactions/constants";
import { TRANSACTION_TYPES } from "@/features/transactions/constants";
import { Label } from "@/shared/components/ui/label";
import {
Select,
@@ -13,7 +13,7 @@ import {
} from "@/shared/components/ui/select";
import { cn } from "@/shared/utils/ui";
import {
CategoriaSelectContent,
CategorySelectContent,
TransactionTypeSelectContent,
} from "../../select-items";
import type { CategorySectionProps } from "./transaction-dialog-types";
@@ -21,8 +21,8 @@ import type { CategorySectionProps } from "./transaction-dialog-types";
export function CategorySection({
formState,
onFieldChange,
categoriaOptions,
categoriaGroups,
categoryOptions,
categoryGroups,
isUpdateMode,
hideTransactionType = false,
}: CategorySectionProps) {
@@ -47,13 +47,13 @@ export function CategorySection({
</SelectValue>
</SelectTrigger>
<SelectContent>
{LANCAMENTO_TRANSACTION_TYPES.filter(
(type) => type !== "Transferência",
).map((type) => (
<SelectItem key={type} value={type}>
<TransactionTypeSelectContent label={type} />
</SelectItem>
))}
{TRANSACTION_TYPES.filter((type) => type !== "Transferência").map(
(type) => (
<SelectItem key={type} value={type}>
<TransactionTypeSelectContent label={type} />
</SelectItem>
),
)}
</SelectContent>
</Select>
</div>
@@ -65,20 +65,20 @@ export function CategorySection({
showTransactionTypeField ? "md:w-1/2" : "md:w-full",
)}
>
<Label htmlFor="categoria">Categoria</Label>
<Label htmlFor="categoria">Category</Label>
<Select
value={formState.categoriaId}
onValueChange={(value) => onFieldChange("categoriaId", value)}
value={formState.categoryId}
onValueChange={(value) => onFieldChange("categoryId", value)}
>
<SelectTrigger id="categoria" className="w-full">
<SelectValue placeholder="Selecione">
{formState.categoriaId &&
{formState.categoryId &&
(() => {
const selectedOption = categoriaOptions.find(
(opt) => opt.value === formState.categoriaId,
const selectedOption = categoryOptions.find(
(opt) => opt.value === formState.categoryId,
);
return selectedOption ? (
<CategoriaSelectContent
<CategorySelectContent
label={selectedOption.label}
icon={selectedOption.icon}
/>
@@ -87,12 +87,12 @@ export function CategorySection({
</SelectValue>
</SelectTrigger>
<SelectContent>
{categoriaGroups.map((group) => (
{categoryGroups.map((group) => (
<SelectGroup key={group.label}>
<SelectLabel>{group.label}</SelectLabel>
{group.options.map((option) => (
<SelectItem key={option.value} value={option.value}>
<CategoriaSelectContent
<CategorySelectContent
label={option.label}
icon={option.icon}
/>

View File

@@ -1,6 +1,6 @@
"use client";
import { LANCAMENTO_CONDITIONS } from "@/features/transactions/constants";
import { TRANSACTION_CONDITIONS } from "@/features/transactions/constants";
import { Label } from "@/shared/components/ui/label";
import {
Select,
@@ -64,7 +64,7 @@ export function ConditionSection({
</SelectValue>
</SelectTrigger>
<SelectContent>
{LANCAMENTO_CONDITIONS.map((condition) => (
{TRANSACTION_CONDITIONS.map((condition) => (
<SelectItem key={condition} value={condition}>
<ConditionSelectContent label={condition} />
</SelectItem>

View File

@@ -9,16 +9,16 @@ import {
SelectTrigger,
SelectValue,
} from "@/shared/components/ui/select";
import { PagadorSelectContent } from "../../select-items";
import type { PagadorSectionProps } from "./transaction-dialog-types";
import { PayerSelectContent } from "../../select-items";
import type { PayerSectionProps } from "./transaction-dialog-types";
export function PagadorSection({
export function PayerSection({
formState,
onFieldChange,
pagadorOptions,
secondaryPagadorOptions,
payerOptions,
secondaryPayerOptions,
totalAmount,
}: PagadorSectionProps) {
}: PayerSectionProps) {
const handlePrimaryAmountChange = (value: string) => {
onFieldChange("primarySplitAmount", value);
const numericValue = Number.parseFloat(value) || 0;
@@ -36,24 +36,24 @@ export function PagadorSection({
return (
<div className="flex w-full flex-col gap-2 md:flex-row">
<div className="w-full space-y-1">
<Label htmlFor="pagador">Pagador</Label>
<Label htmlFor="payer">Payer</Label>
<div className="flex gap-2">
<Select
value={formState.pagadorId}
onValueChange={(value) => onFieldChange("pagadorId", value)}
value={formState.payerId}
onValueChange={(value) => onFieldChange("payerId", value)}
>
<SelectTrigger
id="pagador"
id="payer"
className={formState.isSplit ? "w-[55%]" : "w-full"}
>
<SelectValue placeholder="Selecione">
{formState.pagadorId &&
{formState.payerId &&
(() => {
const selectedOption = pagadorOptions.find(
(opt) => opt.value === formState.pagadorId,
const selectedOption = payerOptions.find(
(opt) => opt.value === formState.payerId,
);
return selectedOption ? (
<PagadorSelectContent
<PayerSelectContent
label={selectedOption.label}
avatarUrl={selectedOption.avatarUrl}
/>
@@ -62,9 +62,9 @@ export function PagadorSection({
</SelectValue>
</SelectTrigger>
<SelectContent>
{pagadorOptions.map((option) => (
{payerOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<PagadorSelectContent
<PayerSelectContent
label={option.label}
avatarUrl={option.avatarUrl}
/>
@@ -85,27 +85,27 @@ export function PagadorSection({
{formState.isSplit ? (
<div className="w-full space-y-1 mb-1">
<Label htmlFor="secondaryPagador">Dividir com</Label>
<Label htmlFor="secondaryPayer">Dividir com</Label>
<div className="flex gap-2">
<Select
value={formState.secondaryPagadorId}
value={formState.secondaryPayerId}
onValueChange={(value) =>
onFieldChange("secondaryPagadorId", value)
onFieldChange("secondaryPayerId", value)
}
>
<SelectTrigger
id="secondaryPagador"
disabled={secondaryPagadorOptions.length === 0}
id="secondaryPayer"
disabled={secondaryPayerOptions.length === 0}
className="w-[55%]"
>
<SelectValue placeholder="Selecione">
{formState.secondaryPagadorId &&
{formState.secondaryPayerId &&
(() => {
const selectedOption = secondaryPagadorOptions.find(
(opt) => opt.value === formState.secondaryPagadorId,
const selectedOption = secondaryPayerOptions.find(
(opt) => opt.value === formState.secondaryPayerId,
);
return selectedOption ? (
<PagadorSelectContent
<PayerSelectContent
label={selectedOption.label}
avatarUrl={selectedOption.avatarUrl}
/>
@@ -114,9 +114,9 @@ export function PagadorSection({
</SelectValue>
</SelectTrigger>
<SelectContent>
{secondaryPagadorOptions.map((option) => (
{secondaryPayerOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<PagadorSelectContent
<PayerSelectContent
label={option.label}
avatarUrl={option.avatarUrl}
/>

View File

@@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
import { LANCAMENTO_PAYMENT_METHODS } from "@/features/transactions/constants";
import { PAYMENT_METHODS } from "@/features/transactions/constants";
import { Label } from "@/shared/components/ui/label";
import { MonthPicker } from "@/shared/components/ui/month-picker";
import {
@@ -23,7 +23,7 @@ import {
} from "@/shared/utils/period";
import { cn } from "@/shared/utils/ui";
import {
ContaCartaoSelectContent,
AccountCardSelectContent,
PaymentMethodSelectContent,
} from "../../select-items";
import type { PaymentMethodSectionProps } from "./transaction-dialog-types";
@@ -39,7 +39,7 @@ function InlinePeriodPicker({
return (
<div className="ml-1">
<span className="text-xs text-muted-foreground">Fatura de </span>
<span className="text-xs text-muted-foreground">Invoice de </span>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
@@ -66,11 +66,11 @@ function InlinePeriodPicker({
export function PaymentMethodSection({
formState,
onFieldChange,
contaOptions,
cartaoOptions,
accountOptions,
cardOptions,
isUpdateMode,
disablePaymentMethod,
disableCartaoSelect,
disableCardSelect,
}: PaymentMethodSectionProps) {
const isCartaoSelected = formState.paymentMethod === "Cartão de crédito";
const showContaSelect = [
@@ -85,10 +85,10 @@ export function PaymentMethodSection({
// Filtrar contas apenas do tipo "Pré-Pago | VR/VA" quando forma de pagamento for "Pré-Pago | VR/VA"
const filteredContaOptions =
formState.paymentMethod === "Pré-Pago | VR/VA"
? contaOptions.filter(
? accountOptions.filter(
(option) => option.accountType === "Pré-Pago | VR/VA",
)
: contaOptions;
: accountOptions;
return (
<>
@@ -120,7 +120,7 @@ export function PaymentMethodSection({
</SelectValue>
</SelectTrigger>
<SelectContent>
{LANCAMENTO_PAYMENT_METHODS.map((method) => (
{PAYMENT_METHODS.map((method) => (
<SelectItem key={method} value={method}>
<PaymentMethodSelectContent label={method} />
</SelectItem>
@@ -133,23 +133,23 @@ export function PaymentMethodSection({
<div className="space-y-1 w-full md:w-1/2">
<Label htmlFor="cartao">Cartão</Label>
<Select
value={formState.cartaoId}
onValueChange={(value) => onFieldChange("cartaoId", value)}
disabled={disableCartaoSelect}
value={formState.cardId}
onValueChange={(value) => onFieldChange("cardId", value)}
disabled={disableCardSelect}
>
<SelectTrigger
id="cartao"
className="w-full"
disabled={disableCartaoSelect}
disabled={disableCardSelect}
>
<SelectValue placeholder="Selecione">
{formState.cartaoId &&
{formState.cardId &&
(() => {
const selectedOption = cartaoOptions.find(
(opt) => opt.value === formState.cartaoId,
const selectedOption = cardOptions.find(
(opt) => opt.value === formState.cardId,
);
return selectedOption ? (
<ContaCartaoSelectContent
<AccountCardSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={true}
@@ -159,16 +159,16 @@ export function PaymentMethodSection({
</SelectValue>
</SelectTrigger>
<SelectContent>
{cartaoOptions.length === 0 ? (
{cardOptions.length === 0 ? (
<div className="px-2 py-6 text-center">
<p className="text-sm text-muted-foreground">
Nenhum cartão cadastrado
</p>
</div>
) : (
cartaoOptions.map((option) => (
cardOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<ContaCartaoSelectContent
<AccountCardSelectContent
label={option.label}
logo={option.logo}
isCartao={true}
@@ -178,7 +178,7 @@ export function PaymentMethodSection({
)}
</SelectContent>
</Select>
{formState.cartaoId ? (
{formState.cardId ? (
<InlinePeriodPicker
period={formState.period}
onPeriodChange={(value) => onFieldChange("period", value)}
@@ -194,20 +194,20 @@ export function PaymentMethodSection({
!isUpdateMode ? "md:w-1/2" : "md:w-full",
)}
>
<Label htmlFor="conta">Conta</Label>
<Label htmlFor="conta">FinancialAccount</Label>
<Select
value={formState.contaId}
onValueChange={(value) => onFieldChange("contaId", value)}
value={formState.accountId}
onValueChange={(value) => onFieldChange("accountId", value)}
>
<SelectTrigger id="conta" className="w-full">
<SelectValue placeholder="Selecione">
{formState.contaId &&
{formState.accountId &&
(() => {
const selectedOption = filteredContaOptions.find(
(opt) => opt.value === formState.contaId,
(opt) => opt.value === formState.accountId,
);
return selectedOption ? (
<ContaCartaoSelectContent
<AccountCardSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={false}
@@ -226,7 +226,7 @@ export function PaymentMethodSection({
) : (
filteredContaOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<ContaCartaoSelectContent
<AccountCardSelectContent
label={option.label}
logo={option.logo}
isCartao={false}
@@ -252,18 +252,18 @@ export function PaymentMethodSection({
>
<Label htmlFor="cartaoUpdate">Cartão</Label>
<Select
value={formState.cartaoId}
onValueChange={(value) => onFieldChange("cartaoId", value)}
value={formState.cardId}
onValueChange={(value) => onFieldChange("cardId", value)}
>
<SelectTrigger id="cartaoUpdate" className="w-full">
<SelectValue placeholder="Selecione">
{formState.cartaoId &&
{formState.cardId &&
(() => {
const selectedOption = cartaoOptions.find(
(opt) => opt.value === formState.cartaoId,
const selectedOption = cardOptions.find(
(opt) => opt.value === formState.cardId,
);
return selectedOption ? (
<ContaCartaoSelectContent
<AccountCardSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={true}
@@ -273,16 +273,16 @@ export function PaymentMethodSection({
</SelectValue>
</SelectTrigger>
<SelectContent>
{cartaoOptions.length === 0 ? (
{cardOptions.length === 0 ? (
<div className="px-2 py-6 text-center">
<p className="text-sm text-muted-foreground">
Nenhum cartão cadastrado
</p>
</div>
) : (
cartaoOptions.map((option) => (
cardOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<ContaCartaoSelectContent
<AccountCardSelectContent
label={option.label}
logo={option.logo}
isCartao={true}
@@ -292,7 +292,7 @@ export function PaymentMethodSection({
)}
</SelectContent>
</Select>
{formState.cartaoId ? (
{formState.cardId ? (
<InlinePeriodPicker
period={formState.period}
onPeriodChange={(value) => onFieldChange("period", value)}
@@ -308,20 +308,20 @@ export function PaymentMethodSection({
!isUpdateMode ? "md:w-1/2" : "md:w-full",
)}
>
<Label htmlFor="contaUpdate">Conta</Label>
<Label htmlFor="contaUpdate">FinancialAccount</Label>
<Select
value={formState.contaId}
onValueChange={(value) => onFieldChange("contaId", value)}
value={formState.accountId}
onValueChange={(value) => onFieldChange("accountId", value)}
>
<SelectTrigger id="contaUpdate" className="w-full">
<SelectValue placeholder="Selecione">
{formState.contaId &&
{formState.accountId &&
(() => {
const selectedOption = filteredContaOptions.find(
(opt) => opt.value === formState.contaId,
(opt) => opt.value === formState.accountId,
);
return selectedOption ? (
<ContaCartaoSelectContent
<AccountCardSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={false}
@@ -340,7 +340,7 @@ export function PaymentMethodSection({
) : (
filteredContaOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<ContaCartaoSelectContent
<AccountCardSelectContent
label={option.label}
logo={option.logo}
isCartao={false}

View File

@@ -1,28 +1,28 @@
import type { LancamentoFormState } from "@/features/transactions/form-helpers";
import type { LancamentoItem, SelectOption } from "../../types";
import type { TransactionFormState } from "@/features/transactions/form-helpers";
import type { SelectOption, TransactionItem } from "../../types";
export type FormState = LancamentoFormState;
export type FormState = TransactionFormState;
export interface LancamentoDialogProps {
export interface TransactionDialogProps {
mode: "create" | "update";
trigger?: React.ReactNode;
open?: boolean;
onOpenChange?: (open: boolean) => void;
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[];
lancamento?: LancamentoItem;
transaction?: TransactionItem;
defaultPeriod?: string;
defaultCartaoId?: string | null;
defaultCardId?: string | null;
defaultPaymentMethod?: string | null;
defaultPurchaseDate?: string | null;
defaultName?: string | null;
defaultAmount?: string | null;
lockCartaoSelection?: boolean;
lockCardSelection?: boolean;
lockPaymentMethod?: boolean;
isImporting?: boolean;
defaultTransactionType?: "Despesa" | "Receita";
@@ -33,11 +33,11 @@ export interface LancamentoDialogProps {
onBulkEditRequest?: (data: {
id: string;
name: string;
categoriaId: string | undefined;
categoryId: string | undefined;
note: string;
pagadorId: string | undefined;
contaId: string | undefined;
cartaoId: string | undefined;
payerId: string | undefined;
accountId: string | undefined;
cardId: string | undefined;
amount: number;
dueDate: string | null;
boletoPaymentDate: string | null;
@@ -57,8 +57,8 @@ export interface BasicFieldsSectionProps extends BaseFieldSectionProps {
}
export interface CategorySectionProps extends BaseFieldSectionProps {
categoriaOptions: SelectOption[];
categoriaGroups: Array<{
categoryOptions: SelectOption[];
categoryGroups: Array<{
label: string;
options: SelectOption[];
}>;
@@ -70,18 +70,18 @@ export interface SplitAndSettlementSectionProps extends BaseFieldSectionProps {
showSettledToggle: boolean;
}
export interface PagadorSectionProps extends BaseFieldSectionProps {
pagadorOptions: SelectOption[];
secondaryPagadorOptions: SelectOption[];
export interface PayerSectionProps extends BaseFieldSectionProps {
payerOptions: SelectOption[];
secondaryPayerOptions: SelectOption[];
totalAmount: number;
}
export interface PaymentMethodSectionProps extends BaseFieldSectionProps {
contaOptions: SelectOption[];
cartaoOptions: SelectOption[];
accountOptions: SelectOption[];
cardOptions: SelectOption[];
isUpdateMode: boolean;
disablePaymentMethod: boolean;
disableCartaoSelect: boolean;
disableCardSelect: boolean;
}
export interface BoletoFieldsSectionProps extends BaseFieldSectionProps {

View File

@@ -3,16 +3,16 @@ import { RiAddLine } from "@remixicon/react";
import { useEffect, useMemo, useState, useTransition } from "react";
import { toast } from "sonner";
import {
createLancamentoAction,
updateLancamentoAction,
createTransactionAction,
updateTransactionAction,
} from "@/features/transactions/actions";
import {
filterSecondaryPagadorOptions,
groupAndSortCategorias,
} from "@/features/transactions/categoria-helpers";
filterSecondaryPayerOptions,
groupAndSortCategories,
} from "@/features/transactions/category-helpers";
import {
applyFieldDependencies,
buildLancamentoInitialState,
buildTransactionInitialState,
deriveCreditCardPeriod,
} from "@/features/transactions/form-helpers";
import { Button } from "@/shared/components/ui/button";
@@ -36,41 +36,41 @@ import { BoletoFieldsSection } from "./boleto-fields-section";
import { CategorySection } from "./category-section";
import { ConditionSection } from "./condition-section";
import { NoteSection } from "./note-section";
import { PagadorSection } from "./pagador-section";
import { PayerSection } from "./payer-section";
import { PaymentMethodSection } from "./payment-method-section";
import { SplitAndSettlementSection } from "./split-settlement-section";
import type {
FormState,
LancamentoDialogProps,
TransactionDialogProps,
} from "./transaction-dialog-types";
export function LancamentoDialog({
export function TransactionDialog({
mode,
trigger,
open,
onOpenChange,
pagadorOptions,
splitPagadorOptions,
defaultPagadorId,
contaOptions,
cartaoOptions,
categoriaOptions,
payerOptions,
splitPayerOptions,
defaultPayerId,
accountOptions,
cardOptions,
categoryOptions,
estabelecimentos,
lancamento,
transaction,
defaultPeriod,
defaultCartaoId,
defaultCardId,
defaultPaymentMethod,
defaultPurchaseDate,
defaultName,
defaultAmount,
lockCartaoSelection,
lockCardSelection,
lockPaymentMethod,
isImporting = false,
defaultTransactionType,
forceShowTransactionType = false,
onSuccess,
onBulkEditRequest,
}: LancamentoDialogProps) {
}: TransactionDialogProps) {
const [dialogOpen, setDialogOpen] = useControlledState(
open,
false,
@@ -78,8 +78,8 @@ export function LancamentoDialog({
);
const [formState, setFormState] = useState<FormState>(() =>
buildLancamentoInitialState(lancamento, defaultPagadorId, defaultPeriod, {
defaultCartaoId,
buildTransactionInitialState(transaction, defaultPayerId, defaultPeriod, {
defaultCardId,
defaultPaymentMethod,
defaultPurchaseDate,
defaultName,
@@ -93,12 +93,12 @@ export function LancamentoDialog({
useEffect(() => {
if (dialogOpen) {
const initial = buildLancamentoInitialState(
lancamento,
defaultPagadorId,
const initial = buildTransactionInitialState(
transaction,
defaultPayerId,
defaultPeriod,
{
defaultCartaoId,
defaultCardId,
defaultPaymentMethod,
defaultPurchaseDate,
defaultName,
@@ -108,15 +108,13 @@ export function LancamentoDialog({
},
);
// Derive credit card period on open when cartaoId is pre-filled
// Derive credit card period on open when cardId is pre-filled
if (
initial.paymentMethod === "Cartão de crédito" &&
initial.cartaoId &&
initial.cardId &&
initial.purchaseDate
) {
const card = cartaoOptions.find(
(opt) => opt.value === initial.cartaoId,
);
const card = cardOptions.find((opt) => opt.value === initial.cardId);
if (card?.closingDay) {
initial.period = deriveCreditCardPeriod(
initial.purchaseDate,
@@ -131,45 +129,45 @@ export function LancamentoDialog({
}
}, [
dialogOpen,
lancamento,
defaultPagadorId,
transaction,
defaultPayerId,
defaultPeriod,
defaultCartaoId,
defaultCardId,
defaultPaymentMethod,
defaultPurchaseDate,
defaultName,
defaultAmount,
defaultTransactionType,
isImporting,
cartaoOptions,
cardOptions,
]);
const primaryPagador = formState.pagadorId;
const primaryPayerId = formState.payerId;
const secondaryPagadorOptions = useMemo(
() => filterSecondaryPagadorOptions(splitPagadorOptions, primaryPagador),
[splitPagadorOptions, primaryPagador],
const secondaryPayerOptions = useMemo(
() => filterSecondaryPayerOptions(splitPayerOptions, primaryPayerId),
[splitPayerOptions, primaryPayerId],
);
const categoriaGroups = useMemo(() => {
const filtered = categoriaOptions.filter(
const categoryGroups = useMemo(() => {
const filtered = categoryOptions.filter(
(option) =>
option.group?.toLowerCase() === formState.transactionType.toLowerCase(),
);
return groupAndSortCategorias(filtered);
}, [categoriaOptions, formState.transactionType]);
return groupAndSortCategories(filtered);
}, [categoryOptions, formState.transactionType]);
type CreateLancamentoInput = Parameters<typeof createLancamentoAction>[0];
type UpdateLancamentoInput = Parameters<typeof updateLancamentoAction>[0];
type CreateTransactionInput = Parameters<typeof createTransactionAction>[0];
type UpdateTransactionInput = Parameters<typeof updateTransactionAction>[0];
const totalAmount = useMemo(() => {
const parsed = Number.parseFloat(formState.amount);
return Number.isNaN(parsed) ? 0 : Math.abs(parsed);
}, [formState.amount]);
function getCardInfo(cartaoId: string | undefined) {
if (!cartaoId) return null;
const card = cartaoOptions.find((opt) => opt.value === cartaoId);
function getCardInfo(cardId: string | undefined) {
if (!cardId) return null;
const card = cardOptions.find((opt) => opt.value === cardId);
if (!card) return null;
return {
closingDay: card.closingDay ?? null,
@@ -182,9 +180,9 @@ export function LancamentoDialog({
value: FormState[Key],
) {
setFormState((prev) => {
const effectiveCartaoId =
key === "cartaoId" ? (value as string) : prev.cartaoId;
const cardInfo = getCardInfo(effectiveCartaoId);
const effectiveCardId =
key === "cardId" ? (value as string) : prev.cardId;
const cardInfo = getCardInfo(effectiveCardId);
const dependencies = applyFieldDependencies(key, value, prev, cardInfo);
@@ -214,7 +212,7 @@ export function LancamentoDialog({
return;
}
if (formState.isSplit && !formState.pagadorId) {
if (formState.isSplit && !formState.payerId) {
const message =
"Selecione o pagador principal para dividir o lançamento.";
setErrorMessage(message);
@@ -222,7 +220,7 @@ export function LancamentoDialog({
return;
}
if (formState.isSplit && !formState.secondaryPagadorId) {
if (formState.isSplit && !formState.secondaryPayerId) {
const message =
"Selecione o pagador secundário para dividir o lançamento.";
setErrorMessage(message);
@@ -240,7 +238,7 @@ export function LancamentoDialog({
const sanitizedAmount = Math.abs(amountValue);
if (!formState.categoriaId) {
if (!formState.categoryId) {
const message = "Selecione uma categoria.";
setErrorMessage(message);
toast.error(message);
@@ -248,32 +246,32 @@ export function LancamentoDialog({
}
if (formState.paymentMethod === "Cartão de crédito") {
if (!formState.cartaoId) {
if (!formState.cardId) {
const message = "Selecione o cartão.";
setErrorMessage(message);
toast.error(message);
return;
}
} else if (!formState.contaId) {
} else if (!formState.accountId) {
const message = "Selecione a conta.";
setErrorMessage(message);
toast.error(message);
return;
}
const payload: CreateLancamentoInput = {
const payload: CreateTransactionInput = {
purchaseDate: formState.purchaseDate,
period: formState.period,
name: formState.name.trim(),
transactionType:
formState.transactionType as CreateLancamentoInput["transactionType"],
formState.transactionType as CreateTransactionInput["transactionType"],
amount: sanitizedAmount,
condition: formState.condition as CreateLancamentoInput["condition"],
condition: formState.condition as CreateTransactionInput["condition"],
paymentMethod:
formState.paymentMethod as CreateLancamentoInput["paymentMethod"],
pagadorId: formState.pagadorId ?? null,
secondaryPagadorId: formState.isSplit
? formState.secondaryPagadorId
formState.paymentMethod as CreateTransactionInput["paymentMethod"],
payerId: formState.payerId ?? null,
secondaryPayerId: formState.isSplit
? formState.secondaryPayerId
: undefined,
isSplit: formState.isSplit,
primarySplitAmount: formState.isSplit
@@ -282,9 +280,9 @@ export function LancamentoDialog({
secondarySplitAmount: formState.isSplit
? Number.parseFloat(formState.secondarySplitAmount) || undefined
: undefined,
contaId: formState.contaId ?? null,
cartaoId: formState.cartaoId ?? null,
categoriaId: formState.categoriaId ?? null,
accountId: formState.accountId ?? null,
cardId: formState.cardId ?? null,
categoryId: formState.categoryId ?? null,
note: formState.note.trim() || null,
isSettled:
formState.paymentMethod === "Cartão de crédito"
@@ -309,7 +307,7 @@ export function LancamentoDialog({
startTransition(async () => {
if (mode === "create") {
const result = await createLancamentoAction(payload);
const result = await createTransactionAction(payload);
if (result.success) {
toast.success(result.message);
@@ -324,18 +322,18 @@ export function LancamentoDialog({
}
// Update mode
const hasSeriesId = Boolean(lancamento?.seriesId);
const hasSeriesId = Boolean(transaction?.seriesId);
if (hasSeriesId && onBulkEditRequest) {
// Para lançamentos em série, abre o diálogo de bulk action
onBulkEditRequest({
id: lancamento?.id ?? "",
id: transaction?.id ?? "",
name: formState.name.trim(),
categoriaId: formState.categoriaId,
categoryId: formState.categoryId,
note: formState.note.trim() || "",
pagadorId: formState.pagadorId,
contaId: formState.contaId,
cartaoId: formState.cartaoId,
payerId: formState.payerId,
accountId: formState.accountId,
cardId: formState.cardId,
amount: sanitizedAmount,
dueDate:
formState.paymentMethod === "Boleto"
@@ -350,12 +348,12 @@ export function LancamentoDialog({
}
// Atualização normal para lançamentos únicos ou todos os campos
const updatePayload: UpdateLancamentoInput = {
id: lancamento?.id ?? "",
const updatePayload: UpdateTransactionInput = {
id: transaction?.id ?? "",
...payload,
};
const result = await updateLancamentoAction(updatePayload);
const result = await updateTransactionAction(updatePayload);
if (result.success) {
toast.success(result.message);
@@ -369,15 +367,15 @@ export function LancamentoDialog({
});
};
const isCopyMode = mode === "create" && Boolean(lancamento) && !isImporting;
const isImportMode = mode === "create" && Boolean(lancamento) && isImporting;
const isCopyMode = mode === "create" && Boolean(transaction) && !isImporting;
const isImportMode = mode === "create" && Boolean(transaction) && isImporting;
const isNewWithType =
mode === "create" && !lancamento && defaultTransactionType;
mode === "create" && !transaction && defaultTransactionType;
const title =
mode === "create"
? isImportMode
? "Importar para Minha Conta"
? "Importar para Minha FinancialAccount"
: isCopyMode
? "Copiar lançamento"
: isNewWithType
@@ -405,7 +403,7 @@ export function LancamentoDialog({
const showSettledToggle = formState.paymentMethod !== "Cartão de crédito";
const isUpdateMode = mode === "update";
const disablePaymentMethod = Boolean(lockPaymentMethod && mode === "create");
const disableCartaoSelect = Boolean(lockCartaoSelection && mode === "create");
const disableCardSelect = Boolean(lockCardSelection && mode === "create");
return (
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
@@ -430,8 +428,8 @@ export function LancamentoDialog({
<CategorySection
formState={formState}
onFieldChange={handleFieldChange}
categoriaOptions={categoriaOptions}
categoriaGroups={categoriaGroups}
categoryOptions={categoryOptions}
categoryGroups={categoryGroups}
isUpdateMode={isUpdateMode}
hideTransactionType={
Boolean(isNewWithType) && !forceShowTransactionType
@@ -446,22 +444,22 @@ export function LancamentoDialog({
/>
) : null}
<PagadorSection
<PayerSection
formState={formState}
onFieldChange={handleFieldChange}
pagadorOptions={pagadorOptions}
secondaryPagadorOptions={secondaryPagadorOptions}
payerOptions={payerOptions}
secondaryPayerOptions={secondaryPayerOptions}
totalAmount={totalAmount}
/>
<PaymentMethodSection
formState={formState}
onFieldChange={handleFieldChange}
contaOptions={contaOptions}
cartaoOptions={cartaoOptions}
accountOptions={accountOptions}
cardOptions={cardOptions}
isUpdateMode={isUpdateMode}
disablePaymentMethod={disablePaymentMethod}
disableCartaoSelect={disableCartaoSelect}
disableCardSelect={disableCardSelect}
/>
{showDueDate ? (

View File

@@ -3,12 +3,12 @@
import { useState } from "react";
import { toast } from "sonner";
import {
createMassLancamentosAction,
deleteLancamentoAction,
deleteLancamentoBulkAction,
deleteMultipleLancamentosAction,
toggleLancamentoSettlementAction,
updateLancamentoBulkAction,
createMassTransactionsAction,
deleteMultipleTransactionsAction,
deleteTransactionAction,
deleteTransactionBulkAction,
toggleTransactionSettlementAction,
updateTransactionBulkAction,
} from "@/features/transactions/actions";
import { ConfirmActionDialog } from "@/shared/components/confirm-action-dialog";
@@ -23,88 +23,88 @@ import {
MassAddDialog,
type MassAddFormData,
} from "../dialogs/mass-add-dialog";
import { LancamentoDetailsDialog } from "../dialogs/transaction-details-dialog";
import { LancamentoDialog } from "../dialogs/transaction-dialog/transaction-dialog";
import { LancamentosTable } from "../table/transactions-table";
import { TransactionDetailsDialog } from "../dialogs/transaction-details-dialog";
import { TransactionDialog } from "../dialogs/transaction-dialog/transaction-dialog";
import { TransactionsTable } from "../table/transactions-table";
import type {
ContaCartaoFilterOption,
LancamentoFilterOption,
LancamentoItem,
AccountCardFilterOption,
SelectOption,
TransactionFilterOption,
TransactionItem,
} from "../types";
interface LancamentosPageProps {
interface TransactionsPageProps {
currentUserId: string;
lancamentos: LancamentoItem[];
pagadorOptions: SelectOption[];
splitPagadorOptions: SelectOption[];
defaultPagadorId: string | null;
contaOptions: SelectOption[];
cartaoOptions: SelectOption[];
categoriaOptions: SelectOption[];
pagadorFilterOptions: LancamentoFilterOption[];
categoriaFilterOptions: LancamentoFilterOption[];
contaCartaoFilterOptions: ContaCartaoFilterOption[];
transactions: TransactionItem[];
payerOptions: SelectOption[];
splitPayerOptions: SelectOption[];
defaultPayerId: string | null;
accountOptions: SelectOption[];
cardOptions: SelectOption[];
categoryOptions: SelectOption[];
payerFilterOptions: TransactionFilterOption[];
categoryFilterOptions: TransactionFilterOption[];
accountCardFilterOptions: AccountCardFilterOption[];
selectedPeriod: string;
estabelecimentos: string[];
allowCreate?: boolean;
noteAsColumn?: boolean;
columnOrder?: string[] | null;
defaultCartaoId?: string | null;
defaultCardId?: string | null;
defaultPaymentMethod?: string | null;
lockCartaoSelection?: boolean;
lockCardSelection?: boolean;
lockPaymentMethod?: boolean;
// Opções específicas para o dialog de importação (quando visualizando dados de outro usuário)
importPagadorOptions?: SelectOption[];
importSplitPagadorOptions?: SelectOption[];
importDefaultPagadorId?: string | null;
importContaOptions?: SelectOption[];
importCartaoOptions?: SelectOption[];
importCategoriaOptions?: SelectOption[];
importPayerOptions?: SelectOption[];
importSplitPayerOptions?: SelectOption[];
importDefaultPayerId?: string | null;
importAccountOptions?: SelectOption[];
importCardOptions?: SelectOption[];
importCategoryOptions?: SelectOption[];
}
export function LancamentosPage({
export function TransactionsPage({
currentUserId,
lancamentos,
pagadorOptions,
splitPagadorOptions,
defaultPagadorId,
contaOptions,
cartaoOptions,
categoriaOptions,
pagadorFilterOptions,
categoriaFilterOptions,
contaCartaoFilterOptions,
transactions: transactionList,
payerOptions,
splitPayerOptions,
defaultPayerId,
accountOptions,
cardOptions,
categoryOptions,
payerFilterOptions,
categoryFilterOptions,
accountCardFilterOptions,
selectedPeriod,
estabelecimentos,
allowCreate = true,
noteAsColumn = false,
columnOrder = null,
defaultCartaoId,
defaultCardId,
defaultPaymentMethod,
lockCartaoSelection,
lockCardSelection,
lockPaymentMethod,
importPagadorOptions,
importSplitPagadorOptions,
importDefaultPagadorId,
importContaOptions,
importCartaoOptions,
importCategoriaOptions,
}: LancamentosPageProps) {
const [selectedLancamento, setSelectedLancamento] =
useState<LancamentoItem | null>(null);
importPayerOptions,
importSplitPayerOptions,
importDefaultPayerId,
importAccountOptions,
importCardOptions,
importCategoryOptions,
}: TransactionsPageProps) {
const [selectedTransaction, setSelectedTransaction] =
useState<TransactionItem | null>(null);
const [editOpen, setEditOpen] = useState(false);
const [createOpen, setCreateOpen] = useState(false);
const [copyOpen, setCopyOpen] = useState(false);
const [lancamentoToCopy, setLancamentoToCopy] =
useState<LancamentoItem | null>(null);
const [transactionToCopy, setTransactionToCopy] =
useState<TransactionItem | null>(null);
const [importOpen, setImportOpen] = useState(false);
const [lancamentoToImport, setLancamentoToImport] =
useState<LancamentoItem | null>(null);
const [transactionToImport, setTransactionToImport] =
useState<TransactionItem | null>(null);
const [massAddOpen, setMassAddOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false);
const [lancamentoToDelete, setLancamentoToDelete] =
useState<LancamentoItem | null>(null);
const [transactionToDelete, setTransactionToDelete] =
useState<TransactionItem | null>(null);
const [detailsOpen, setDetailsOpen] = useState(false);
const [settlementLoadingId, setSettlementLoadingId] = useState<string | null>(
null,
@@ -114,32 +114,32 @@ export function LancamentosPage({
const [pendingEditData, setPendingEditData] = useState<{
id: string;
name: string;
categoriaId: string | undefined;
categoryId: string | undefined;
note: string;
pagadorId: string | undefined;
contaId: string | undefined;
cartaoId: string | undefined;
payerId: string | undefined;
accountId: string | undefined;
cardId: string | undefined;
amount: number;
dueDate: string | null;
boletoPaymentDate: string | null;
lancamento: LancamentoItem;
transaction: TransactionItem;
} | null>(null);
const [pendingDeleteData, setPendingDeleteData] =
useState<LancamentoItem | null>(null);
useState<TransactionItem | null>(null);
const [multipleBulkDeleteOpen, setMultipleBulkDeleteOpen] = useState(false);
const [pendingMultipleDeleteData, setPendingMultipleDeleteData] = useState<
LancamentoItem[]
TransactionItem[]
>([]);
const [anticipateOpen, setAnticipateOpen] = useState(false);
const [anticipationHistoryOpen, setAnticipationHistoryOpen] = useState(false);
const [selectedForAnticipation, setSelectedForAnticipation] =
useState<LancamentoItem | null>(null);
useState<TransactionItem | null>(null);
const [bulkImportOpen, setBulkImportOpen] = useState(false);
const [lancamentosToImport, setLancamentosToImport] = useState<
LancamentoItem[]
const [transactionsToImport, setTransactionsToImport] = useState<
TransactionItem[]
>([]);
const handleToggleSettlement = async (item: LancamentoItem) => {
const handleToggleSettlement = async (item: TransactionItem) => {
if (item.paymentMethod === "Cartão de crédito") {
toast.info(
"Pagamentos com cartão são conciliados automaticamente. Ajuste pelo cartão.",
@@ -163,7 +163,7 @@ export function LancamentosPage({
try {
setSettlementLoadingId(item.id);
const result = await toggleLancamentoSettlementAction({
const result = await toggleTransactionSettlementAction({
id: item.id,
value: nextValue,
});
@@ -185,12 +185,12 @@ export function LancamentosPage({
};
const handleDelete = async () => {
if (!lancamentoToDelete) {
if (!transactionToDelete) {
return;
}
const result = await deleteLancamentoAction({
id: lancamentoToDelete.id,
const result = await deleteTransactionAction({
id: transactionToDelete.id,
});
if (!result.success) {
@@ -207,7 +207,7 @@ export function LancamentosPage({
return;
}
const result = await deleteLancamentoBulkAction({
const result = await deleteTransactionBulkAction({
id: pendingDeleteData.id,
scope,
});
@@ -225,22 +225,22 @@ export function LancamentosPage({
const handleBulkEditRequest = (data: {
id: string;
name: string;
categoriaId: string | undefined;
categoryId: string | undefined;
note: string;
pagadorId: string | undefined;
contaId: string | undefined;
cartaoId: string | undefined;
payerId: string | undefined;
accountId: string | undefined;
cardId: string | undefined;
amount: number;
dueDate: string | null;
boletoPaymentDate: string | null;
}) => {
if (!selectedLancamento) {
if (!selectedTransaction) {
return;
}
setPendingEditData({
...data,
lancamento: selectedLancamento,
transaction: selectedTransaction,
});
setEditOpen(false);
setBulkEditOpen(true);
@@ -251,15 +251,15 @@ export function LancamentosPage({
return;
}
const result = await updateLancamentoBulkAction({
const result = await updateTransactionBulkAction({
id: pendingEditData.id,
scope,
name: pendingEditData.name,
categoriaId: pendingEditData.categoriaId,
categoryId: pendingEditData.categoryId,
note: pendingEditData.note,
pagadorId: pendingEditData.pagadorId,
contaId: pendingEditData.contaId,
cartaoId: pendingEditData.cartaoId,
payerId: pendingEditData.payerId,
accountId: pendingEditData.accountId,
cardId: pendingEditData.cardId,
amount: pendingEditData.amount,
dueDate: pendingEditData.dueDate,
boletoPaymentDate: pendingEditData.boletoPaymentDate,
@@ -276,7 +276,7 @@ export function LancamentosPage({
};
const handleMassAddSubmit = async (data: MassAddFormData) => {
const result = await createMassLancamentosAction(data);
const result = await createMassTransactionsAction(data);
if (!result.success) {
toast.error(result.error);
@@ -286,7 +286,7 @@ export function LancamentosPage({
toast.success(result.message);
};
const handleMultipleBulkDelete = (items: LancamentoItem[]) => {
const handleMultipleBulkDelete = (items: TransactionItem[]) => {
// Se todos os selecionados são da mesma série (parcelado/recorrente), abrir dialog de escopo
const withSeries = items.filter((i) => i.seriesId);
const sameSeries =
@@ -308,7 +308,7 @@ export function LancamentosPage({
}
const ids = pendingMultipleDeleteData.map((item) => item.id);
const result = await deleteMultipleLancamentosAction({ ids });
const result = await deleteMultipleTransactionsAction({ ids });
if (!result.success) {
toast.error(result.error);
@@ -333,61 +333,61 @@ export function LancamentosPage({
setMassAddOpen(true);
};
const handleEdit = (item: LancamentoItem) => {
setSelectedLancamento(item);
const handleEdit = (item: TransactionItem) => {
setSelectedTransaction(item);
setEditOpen(true);
};
const handleCopy = (item: LancamentoItem) => {
setLancamentoToCopy(item);
const handleCopy = (item: TransactionItem) => {
setTransactionToCopy(item);
setCopyOpen(true);
};
const handleImport = (item: LancamentoItem) => {
setLancamentoToImport(item);
const handleImport = (item: TransactionItem) => {
setTransactionToImport(item);
setImportOpen(true);
};
const handleBulkImport = (items: LancamentoItem[]) => {
setLancamentosToImport(items);
const handleBulkImport = (items: TransactionItem[]) => {
setTransactionsToImport(items);
setBulkImportOpen(true);
};
const handleConfirmDelete = (item: LancamentoItem) => {
const handleConfirmDelete = (item: TransactionItem) => {
if (item.seriesId) {
setPendingDeleteData(item);
setBulkDeleteOpen(true);
} else {
setLancamentoToDelete(item);
setTransactionToDelete(item);
setDeleteOpen(true);
}
};
const handleViewDetails = (item: LancamentoItem) => {
setSelectedLancamento(item);
const handleViewDetails = (item: TransactionItem) => {
setSelectedTransaction(item);
setDetailsOpen(true);
};
const handleAnticipate = (item: LancamentoItem) => {
const handleAnticipate = (item: TransactionItem) => {
setSelectedForAnticipation(item);
setAnticipateOpen(true);
};
const handleViewAnticipationHistory = (item: LancamentoItem) => {
const handleViewAnticipationHistory = (item: TransactionItem) => {
setSelectedForAnticipation(item);
setAnticipationHistoryOpen(true);
};
return (
<>
<LancamentosTable
data={lancamentos}
<TransactionsTable
data={transactionList}
currentUserId={currentUserId}
noteAsColumn={noteAsColumn}
columnOrder={columnOrder}
pagadorFilterOptions={pagadorFilterOptions}
categoriaFilterOptions={categoriaFilterOptions}
contaCartaoFilterOptions={contaCartaoFilterOptions}
payerFilterOptions={payerFilterOptions}
categoryFilterOptions={categoryFilterOptions}
accountCardFilterOptions={accountCardFilterOptions}
selectedPeriod={selectedPeriod}
onCreate={allowCreate ? handleCreate : undefined}
onMassAdd={allowCreate ? handleMassAdd : undefined}
@@ -405,111 +405,111 @@ export function LancamentosPage({
/>
{allowCreate ? (
<LancamentoDialog
<TransactionDialog
mode="create"
open={createOpen}
onOpenChange={setCreateOpen}
pagadorOptions={pagadorOptions}
splitPagadorOptions={splitPagadorOptions}
defaultPagadorId={defaultPagadorId}
contaOptions={contaOptions}
cartaoOptions={cartaoOptions}
categoriaOptions={categoriaOptions}
payerOptions={payerOptions}
splitPayerOptions={splitPayerOptions}
defaultPayerId={defaultPayerId}
accountOptions={accountOptions}
cardOptions={cardOptions}
categoryOptions={categoryOptions}
estabelecimentos={estabelecimentos}
defaultPeriod={selectedPeriod}
defaultCartaoId={defaultCartaoId}
defaultCardId={defaultCardId}
defaultPaymentMethod={defaultPaymentMethod}
lockCartaoSelection={lockCartaoSelection}
lockCardSelection={lockCardSelection}
lockPaymentMethod={lockPaymentMethod}
defaultTransactionType={transactionTypeForCreate ?? undefined}
/>
) : null}
<LancamentoDialog
<TransactionDialog
mode="create"
open={copyOpen && !!lancamentoToCopy}
open={copyOpen && !!transactionToCopy}
onOpenChange={(open) => {
setCopyOpen(open);
if (!open) {
setLancamentoToCopy(null);
setTransactionToCopy(null);
}
}}
pagadorOptions={pagadorOptions}
splitPagadorOptions={splitPagadorOptions}
defaultPagadorId={defaultPagadorId}
contaOptions={contaOptions}
cartaoOptions={cartaoOptions}
categoriaOptions={categoriaOptions}
payerOptions={payerOptions}
splitPayerOptions={splitPayerOptions}
defaultPayerId={defaultPayerId}
accountOptions={accountOptions}
cardOptions={cardOptions}
categoryOptions={categoryOptions}
estabelecimentos={estabelecimentos}
lancamento={lancamentoToCopy ?? undefined}
transaction={transactionToCopy ?? undefined}
defaultPeriod={selectedPeriod}
/>
<LancamentoDialog
<TransactionDialog
mode="create"
open={importOpen && !!lancamentoToImport}
open={importOpen && !!transactionToImport}
onOpenChange={(open) => {
setImportOpen(open);
if (!open) {
setLancamentoToImport(null);
setTransactionToImport(null);
}
}}
pagadorOptions={importPagadorOptions ?? pagadorOptions}
splitPagadorOptions={importSplitPagadorOptions ?? splitPagadorOptions}
defaultPagadorId={importDefaultPagadorId ?? defaultPagadorId}
contaOptions={importContaOptions ?? contaOptions}
cartaoOptions={importCartaoOptions ?? cartaoOptions}
categoriaOptions={importCategoriaOptions ?? categoriaOptions}
payerOptions={importPayerOptions ?? payerOptions}
splitPayerOptions={importSplitPayerOptions ?? splitPayerOptions}
defaultPayerId={importDefaultPayerId ?? defaultPayerId}
accountOptions={importAccountOptions ?? accountOptions}
cardOptions={importCardOptions ?? cardOptions}
categoryOptions={importCategoryOptions ?? categoryOptions}
estabelecimentos={estabelecimentos}
lancamento={lancamentoToImport ?? undefined}
transaction={transactionToImport ?? undefined}
defaultPeriod={selectedPeriod}
isImporting={true}
/>
<BulkImportDialog
open={bulkImportOpen && lancamentosToImport.length > 0}
open={bulkImportOpen && transactionsToImport.length > 0}
onOpenChange={setBulkImportOpen}
items={lancamentosToImport}
pagadorOptions={importPagadorOptions ?? pagadorOptions}
contaOptions={importContaOptions ?? contaOptions}
cartaoOptions={importCartaoOptions ?? cartaoOptions}
categoriaOptions={importCategoriaOptions ?? categoriaOptions}
defaultPagadorId={importDefaultPagadorId ?? defaultPagadorId}
items={transactionsToImport}
payerOptions={importPayerOptions ?? payerOptions}
accountOptions={importAccountOptions ?? accountOptions}
cardOptions={importCardOptions ?? cardOptions}
categoryOptions={importCategoryOptions ?? categoryOptions}
defaultPayerId={importDefaultPayerId ?? defaultPayerId}
/>
<LancamentoDialog
<TransactionDialog
mode="update"
open={editOpen && !!selectedLancamento}
open={editOpen && !!selectedTransaction}
onOpenChange={setEditOpen}
pagadorOptions={pagadorOptions}
splitPagadorOptions={splitPagadorOptions}
defaultPagadorId={defaultPagadorId}
contaOptions={contaOptions}
cartaoOptions={cartaoOptions}
categoriaOptions={categoriaOptions}
payerOptions={payerOptions}
splitPayerOptions={splitPayerOptions}
defaultPayerId={defaultPayerId}
accountOptions={accountOptions}
cardOptions={cardOptions}
categoryOptions={categoryOptions}
estabelecimentos={estabelecimentos}
lancamento={selectedLancamento ?? undefined}
transaction={selectedTransaction ?? undefined}
defaultPeriod={selectedPeriod}
onBulkEditRequest={handleBulkEditRequest}
/>
<LancamentoDetailsDialog
open={detailsOpen && !!selectedLancamento}
<TransactionDetailsDialog
open={detailsOpen && !!selectedTransaction}
onOpenChange={(open) => {
setDetailsOpen(open);
if (!open) {
setSelectedLancamento(null);
setSelectedTransaction(null);
}
}}
lancamento={detailsOpen ? selectedLancamento : null}
transaction={detailsOpen ? selectedTransaction : null}
/>
<ConfirmActionDialog
open={deleteOpen && !!lancamentoToDelete}
open={deleteOpen && !!transactionToDelete}
onOpenChange={setDeleteOpen}
title={
lancamentoToDelete
? `Remover lançamento "${lancamentoToDelete.name}"?`
transactionToDelete
? `Remover lançamento "${transactionToDelete.name}"?`
: "Remover lançamento?"
}
description="Essa ação é irreversível e removerá o lançamento de forma permanente."
@@ -517,7 +517,7 @@ export function LancamentosPage({
pendingLabel="Removendo..."
confirmVariant="destructive"
onConfirm={handleDelete}
disabled={!lancamentoToDelete}
disabled={!transactionToDelete}
/>
<BulkActionDialog
@@ -543,16 +543,16 @@ export function LancamentosPage({
onOpenChange={setBulkEditOpen}
actionType="edit"
seriesType={
pendingEditData?.lancamento.condition === "Parcelado"
pendingEditData?.transaction.condition === "Parcelado"
? "installment"
: "recurring"
}
currentNumber={
pendingEditData?.lancamento.currentInstallment ?? undefined
pendingEditData?.transaction.currentInstallment ?? undefined
}
totalCount={
pendingEditData?.lancamento.installmentCount ??
pendingEditData?.lancamento.recurrenceCount ??
pendingEditData?.transaction.installmentCount ??
pendingEditData?.transaction.recurrenceCount ??
undefined
}
onConfirm={handleBulkEdit}
@@ -563,14 +563,14 @@ export function LancamentosPage({
open={massAddOpen}
onOpenChange={setMassAddOpen}
onSubmit={handleMassAddSubmit}
pagadorOptions={pagadorOptions}
contaOptions={contaOptions}
cartaoOptions={cartaoOptions}
categoriaOptions={categoriaOptions}
payerOptions={payerOptions}
accountOptions={accountOptions}
cardOptions={cardOptions}
categoryOptions={categoryOptions}
estabelecimentos={estabelecimentos}
selectedPeriod={selectedPeriod}
defaultPagadorId={defaultPagadorId}
defaultCartaoId={defaultCartaoId}
defaultPayerId={defaultPayerId}
defaultCardId={defaultCardId}
/>
) : null}
@@ -595,12 +595,12 @@ export function LancamentosPage({
onOpenChange={setAnticipateOpen}
seriesId={selectedForAnticipation.seriesId as string}
lancamentoName={selectedForAnticipation.name}
categorias={categoriaOptions.map((c) => ({
categorias={categoryOptions.map((c) => ({
id: c.value,
name: c.label,
icon: c.icon ?? null,
}))}
pagadores={pagadorOptions.map((p) => ({
pagadores={payerOptions.map((p) => ({
id: p.value,
name: p.label,
}))}
@@ -614,10 +614,12 @@ export function LancamentosPage({
onOpenChange={setAnticipationHistoryOpen}
seriesId={selectedForAnticipation.seriesId as string}
lancamentoName={selectedForAnticipation.name}
onViewLancamento={(lancamentoId) => {
const lancamento = lancamentos.find((l) => l.id === lancamentoId);
if (lancamento) {
setSelectedLancamento(lancamento);
onViewLancamento={(transactionId) => {
const transaction = transactionList.find(
(l) => l.id === transactionId,
);
if (transaction) {
setSelectedTransaction(transaction);
setDetailsOpen(true);
setAnticipationHistoryOpen(false);
}

View File

@@ -20,7 +20,7 @@ type SelectItemContentProps = {
icon?: string | null;
};
export function PagadorSelectContent({
export function PayerSelectContent({
label,
avatarUrl,
}: SelectItemContentProps) {
@@ -40,10 +40,7 @@ export function PagadorSelectContent({
);
}
export function CategoriaSelectContent({
label,
icon,
}: SelectItemContentProps) {
export function CategorySelectContent({ label, icon }: SelectItemContentProps) {
return (
<span className="flex items-center gap-2">
<CategoryIcon name={icon} className="size-4" />
@@ -89,7 +86,7 @@ export function ConditionSelectContent({ label }: { label: string }) {
);
}
export function ContaCartaoSelectContent({
export function AccountCardSelectContent({
label,
logo,
isCartao,

View File

@@ -23,7 +23,7 @@ import { displayPeriod } from "@/shared/utils/period";
interface AnticipationCardProps {
anticipation: InstallmentAnticipationWithRelations;
onViewLancamento?: (lancamentoId: string) => void;
onViewLancamento?: (transactionId: string) => void;
onCanceled?: () => void;
}
@@ -34,7 +34,7 @@ export function AnticipationCard({
}: AnticipationCardProps) {
const [isPending, startTransition] = useTransition();
const isSettled = anticipation.lancamento.isSettled === true;
const isSettled = anticipation.transaction.isSettled === true;
const canCancel = !isSettled;
const formatDate = (date: Date) => {
@@ -57,7 +57,7 @@ export function AnticipationCard({
};
const handleViewLancamento = () => {
onViewLancamento?.(anticipation.lancamentoId);
onViewLancamento?.(anticipation.transactionId);
};
return (
@@ -132,19 +132,17 @@ export function AnticipationCard({
</dd>
</div>
{anticipation.pagador && (
{anticipation.payer && (
<div>
<dt className="text-muted-foreground">Pagador</dt>
<dd className="mt-1 font-medium">{anticipation.pagador.name}</dd>
<dt className="text-muted-foreground">Payer</dt>
<dd className="mt-1 font-medium">{anticipation.payer.name}</dd>
</div>
)}
{anticipation.categoria && (
{anticipation.category && (
<div>
<dt className="text-muted-foreground">Categoria</dt>
<dd className="mt-1 font-medium">
{anticipation.categoria.name}
</dd>
<dt className="text-muted-foreground">Category</dt>
<dd className="mt-1 font-medium">{anticipation.category.name}</dd>
</div>
)}
</dl>

View File

@@ -14,9 +14,9 @@ import {
useTransition,
} from "react";
import {
LANCAMENTO_CONDITIONS,
LANCAMENTO_PAYMENT_METHODS,
LANCAMENTO_TRANSACTION_TYPES,
PAYMENT_METHODS,
TRANSACTION_CONDITIONS,
TRANSACTION_TYPES,
} from "@/features/transactions/constants";
import { Button } from "@/shared/components/ui/button";
import {
@@ -52,14 +52,17 @@ import {
} from "@/shared/components/ui/select";
import { cn } from "@/shared/utils/ui";
import {
CategoriaSelectContent,
AccountCardSelectContent,
CategorySelectContent,
ConditionSelectContent,
ContaCartaoSelectContent,
PagadorSelectContent,
PayerSelectContent,
PaymentMethodSelectContent,
TransactionTypeSelectContent,
} from "../select-items";
import type { ContaCartaoFilterOption, LancamentoFilterOption } from "../types";
import type {
AccountCardFilterOption,
TransactionFilterOption,
} from "../types";
const FILTER_EMPTY_VALUE = "__all";
@@ -124,23 +127,23 @@ function FilterSelect({
);
}
interface LancamentosFiltersProps {
pagadorOptions: LancamentoFilterOption[];
categoriaOptions: LancamentoFilterOption[];
contaCartaoOptions: ContaCartaoFilterOption[];
interface TransactionsFiltersProps {
payerOptions: TransactionFilterOption[];
categoryOptions: TransactionFilterOption[];
accountCardOptions: AccountCardFilterOption[];
className?: string;
exportButton?: ReactNode;
hideAdvancedFilters?: boolean;
}
export function LancamentosFilters({
pagadorOptions,
categoriaOptions,
contaCartaoOptions,
export function TransactionsFilters({
payerOptions,
categoryOptions,
accountCardOptions,
className,
exportButton,
hideAdvancedFilters = false,
}: LancamentosFiltersProps) {
}: TransactionsFiltersProps) {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
@@ -195,7 +198,7 @@ export function LancamentosFilters({
nextParams.set("periodo", periodValue);
}
setSearchValue("");
setCategoriaOpen(false);
setCategoryOpen(false);
startTransition(() => {
const target = nextParams.toString()
? `${pathname}?${nextParams.toString()}`
@@ -204,13 +207,13 @@ export function LancamentosFilters({
});
};
const pagadorSelectOptions = pagadorOptions.map((option) => ({
const payerSelectOptions = payerOptions.map((option) => ({
value: option.slug,
label: option.label,
avatarUrl: option.avatarUrl,
}));
const contaOptions = contaCartaoOptions
const accountOptions = accountCardOptions
.filter((option) => option.kind === "conta")
.map((option) => ({
value: option.slug,
@@ -218,7 +221,7 @@ export function LancamentosFilters({
logo: option.logo,
}));
const cartaoOptions = contaCartaoOptions
const cardOptions = accountCardOptions
.filter((option) => option.kind === "cartao")
.map((option) => ({
value: option.slug,
@@ -226,34 +229,34 @@ export function LancamentosFilters({
logo: option.logo,
}));
const categoriaValue = getParamValue("categoria");
const selectedCategoria =
categoriaValue !== FILTER_EMPTY_VALUE
? categoriaOptions.find((option) => option.slug === categoriaValue)
const categoryValue = getParamValue("category");
const selectedCategory =
categoryValue !== FILTER_EMPTY_VALUE
? categoryOptions.find((option) => option.slug === categoryValue)
: null;
const pagadorValue = getParamValue("pagador");
const selectedPagador =
pagadorValue !== FILTER_EMPTY_VALUE
? pagadorOptions.find((option) => option.slug === pagadorValue)
const payerValue = getParamValue("payer");
const selectedPayer =
payerValue !== FILTER_EMPTY_VALUE
? payerOptions.find((option) => option.slug === payerValue)
: null;
const contaCartaoValue = getParamValue("contaCartao");
const selectedContaCartao =
contaCartaoValue !== FILTER_EMPTY_VALUE
? contaCartaoOptions.find((option) => option.slug === contaCartaoValue)
const accountCardValue = getParamValue("accountCard");
const selectedAccountCard =
accountCardValue !== FILTER_EMPTY_VALUE
? accountCardOptions.find((option) => option.slug === accountCardValue)
: null;
const [categoriaOpen, setCategoriaOpen] = useState(false);
const [categoryOpen, setCategoryOpen] = useState(false);
const [drawerOpen, setDrawerOpen] = useState(false);
const hasActiveFilters =
searchParams.get("transacao") ||
searchParams.get("condicao") ||
searchParams.get("pagamento") ||
searchParams.get("pagador") ||
searchParams.get("categoria") ||
searchParams.get("contaCartao");
searchParams.get("type") ||
searchParams.get("condition") ||
searchParams.get("payment") ||
searchParams.get("payer") ||
searchParams.get("category") ||
searchParams.get("accountCard");
const handleResetFilters = () => {
handleReset();
@@ -315,9 +318,9 @@ export function LancamentosFilters({
Tipo de Lançamento
</label>
<FilterSelect
param="transacao"
param="type"
placeholder="Todos"
options={buildStaticOptions(LANCAMENTO_TRANSACTION_TYPES)}
options={buildStaticOptions(TRANSACTION_TYPES)}
widthClass="w-full border-dashed"
disabled={isPending}
getParamValue={getParamValue}
@@ -333,9 +336,9 @@ export function LancamentosFilters({
Condição de Lançamento
</label>
<FilterSelect
param="condicao"
param="condition"
placeholder="Todas"
options={buildStaticOptions(LANCAMENTO_CONDITIONS)}
options={buildStaticOptions(TRANSACTION_CONDITIONS)}
widthClass="w-full border-dashed"
disabled={isPending}
getParamValue={getParamValue}
@@ -351,9 +354,9 @@ export function LancamentosFilters({
Forma de Pagamento
</label>
<FilterSelect
param="pagamento"
param="payment"
placeholder="Todos"
options={buildStaticOptions(LANCAMENTO_PAYMENT_METHODS)}
options={buildStaticOptions(PAYMENT_METHODS)}
widthClass="w-full border-dashed"
disabled={isPending}
getParamValue={getParamValue}
@@ -365,12 +368,12 @@ export function LancamentosFilters({
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Pagador</label>
<label className="text-sm font-medium">Payer</label>
<Select
value={getParamValue("pagador")}
value={getParamValue("payer")}
onValueChange={(value) =>
handleFilterChange(
"pagador",
"payer",
value === FILTER_EMPTY_VALUE ? null : value,
)
}
@@ -381,10 +384,10 @@ export function LancamentosFilters({
disabled={isPending}
>
<span className="truncate">
{selectedPagador ? (
<PagadorSelectContent
label={selectedPagador.label}
avatarUrl={selectedPagador.avatarUrl}
{selectedPayer ? (
<PayerSelectContent
label={selectedPayer.label}
avatarUrl={selectedPayer.avatarUrl}
/>
) : (
"Todos"
@@ -393,9 +396,9 @@ export function LancamentosFilters({
</SelectTrigger>
<SelectContent>
<SelectItem value={FILTER_EMPTY_VALUE}>Todos</SelectItem>
{pagadorSelectOptions.map((option) => (
{payerSelectOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<PagadorSelectContent
<PayerSelectContent
label={option.label}
avatarUrl={option.avatarUrl}
/>
@@ -406,25 +409,25 @@ export function LancamentosFilters({
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Categoria</label>
<label className="text-sm font-medium">Category</label>
<Popover
open={categoriaOpen}
onOpenChange={setCategoriaOpen}
open={categoryOpen}
onOpenChange={setCategoryOpen}
modal
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={categoriaOpen}
aria-expanded={categoryOpen}
className="w-full justify-between text-sm border-dashed"
disabled={isPending}
>
<span className="truncate flex items-center gap-2">
{selectedCategoria ? (
<CategoriaSelectContent
label={selectedCategoria.label}
icon={selectedCategoria.icon}
{selectedCategory ? (
<CategorySelectContent
label={selectedCategory.label}
icon={selectedCategory.icon}
/>
) : (
"Todas"
@@ -442,29 +445,29 @@ export function LancamentosFilters({
<CommandItem
value={FILTER_EMPTY_VALUE}
onSelect={() => {
handleFilterChange("categoria", null);
setCategoriaOpen(false);
handleFilterChange("category", null);
setCategoryOpen(false);
}}
>
Todas
{categoriaValue === FILTER_EMPTY_VALUE ? (
{categoryValue === FILTER_EMPTY_VALUE ? (
<RiCheckLine className="ml-auto size-4" />
) : null}
</CommandItem>
{categoriaOptions.map((option) => (
{categoryOptions.map((option) => (
<CommandItem
key={option.slug}
value={option.slug}
onSelect={() => {
handleFilterChange("categoria", option.slug);
setCategoriaOpen(false);
handleFilterChange("category", option.slug);
setCategoryOpen(false);
}}
>
<CategoriaSelectContent
<CategorySelectContent
label={option.label}
icon={option.icon}
/>
{categoriaValue === option.slug ? (
{categoryValue === option.slug ? (
<RiCheckLine className="ml-auto size-4" />
) : null}
</CommandItem>
@@ -479,10 +482,10 @@ export function LancamentosFilters({
<div className="space-y-2">
<label className="text-sm font-medium">Conta/Cartão</label>
<Select
value={getParamValue("contaCartao")}
value={getParamValue("accountCard")}
onValueChange={(value) =>
handleFilterChange(
"contaCartao",
"accountCard",
value === FILTER_EMPTY_VALUE ? null : value,
)
}
@@ -493,11 +496,11 @@ export function LancamentosFilters({
disabled={isPending}
>
<span className="truncate">
{selectedContaCartao ? (
<ContaCartaoSelectContent
label={selectedContaCartao.label}
logo={selectedContaCartao.logo}
isCartao={selectedContaCartao.kind === "cartao"}
{selectedAccountCard ? (
<AccountCardSelectContent
label={selectedAccountCard.label}
logo={selectedAccountCard.logo}
isCartao={selectedAccountCard.kind === "cartao"}
/>
) : (
"Todos"
@@ -506,12 +509,12 @@ export function LancamentosFilters({
</SelectTrigger>
<SelectContent>
<SelectItem value={FILTER_EMPTY_VALUE}>Todos</SelectItem>
{contaOptions.length > 0 ? (
{accountOptions.length > 0 ? (
<SelectGroup>
<SelectLabel>Contas</SelectLabel>
{contaOptions.map((option) => (
{accountOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<ContaCartaoSelectContent
<AccountCardSelectContent
label={option.label}
logo={option.logo}
isCartao={false}
@@ -520,12 +523,12 @@ export function LancamentosFilters({
))}
</SelectGroup>
) : null}
{cartaoOptions.length > 0 ? (
{cardOptions.length > 0 ? (
<SelectGroup>
<SelectLabel>Cartões</SelectLabel>
{cartaoOptions.map((option) => (
{cardOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<ContaCartaoSelectContent
<AccountCardSelectContent
label={option.label}
logo={option.logo}
isCartao={true}

View File

@@ -79,25 +79,25 @@ import { formatDate } from "@/shared/utils/date";
import { getConditionIcon, getPaymentMethodIcon } from "@/shared/utils/icons";
import { cn } from "@/shared/utils/ui";
import { EstabelecimentoLogo } from "../shared/establishment-logo";
import { LancamentosExport } from "../transactions-export";
import { TransactionsExport } from "../transactions-export";
import type {
ContaCartaoFilterOption,
LancamentoFilterOption,
LancamentoItem,
AccountCardFilterOption,
TransactionFilterOption,
TransactionItem,
} from "../types";
import { LancamentosFilters } from "./transactions-filters";
import { TransactionsFilters } from "./transactions-filters";
type BuildColumnsArgs = {
currentUserId: string;
noteAsColumn: boolean;
onEdit?: (item: LancamentoItem) => void;
onCopy?: (item: LancamentoItem) => void;
onImport?: (item: LancamentoItem) => void;
onConfirmDelete?: (item: LancamentoItem) => void;
onViewDetails?: (item: LancamentoItem) => void;
onToggleSettlement?: (item: LancamentoItem) => void;
onAnticipate?: (item: LancamentoItem) => void;
onViewAnticipationHistory?: (item: LancamentoItem) => void;
onEdit?: (item: TransactionItem) => void;
onCopy?: (item: TransactionItem) => void;
onImport?: (item: TransactionItem) => void;
onConfirmDelete?: (item: TransactionItem) => void;
onViewDetails?: (item: TransactionItem) => void;
onToggleSettlement?: (item: TransactionItem) => void;
onAnticipate?: (item: TransactionItem) => void;
onViewAnticipationHistory?: (item: TransactionItem) => void;
isSettlementLoading: (id: string) => boolean;
showActions: boolean;
};
@@ -115,7 +115,7 @@ const buildColumns = ({
onViewAnticipationHistory,
isSettlementLoading,
showActions,
}: BuildColumnsArgs): ColumnDef<LancamentoItem>[] => {
}: BuildColumnsArgs): ColumnDef<TransactionItem>[] => {
const noop = () => undefined;
const handleEdit = onEdit ?? noop;
const handleCopy = onCopy ?? noop;
@@ -126,7 +126,7 @@ const buildColumns = ({
const handleAnticipate = onAnticipate ?? noop;
const handleViewAnticipationHistory = onViewAnticipationHistory ?? noop;
const columns: ColumnDef<LancamentoItem>[] = [
const columns: ColumnDef<TransactionItem>[] = [
{
id: "select",
header: ({ table }) => (
@@ -380,7 +380,7 @@ const buildColumns = ({
accessorKey: "pagadorName",
header: "Pagador",
cell: ({ row }) => {
const { pagadorId, pagadorName, pagadorAvatar } = row.original;
const { payerId, pagadorName, pagadorAvatar } = row.original;
const label = pagadorName?.trim() || "Sem pagador";
const displayName = label.split(/\s+/)[0] ?? label;
@@ -398,7 +398,7 @@ const buildColumns = ({
</>
);
if (!pagadorId) {
if (!payerId) {
return (
<span className="inline-flex items-center gap-2">{content}</span>
);
@@ -406,7 +406,7 @@ const buildColumns = ({
return (
<Link
href={`/payers/${pagadorId}`}
href={`/payers/${payerId}`}
className="inline-flex items-center gap-2 hover:underline"
title={label}
>
@@ -424,17 +424,17 @@ const buildColumns = ({
contaName,
cartaoLogo,
contaLogo,
cartaoId,
contaId,
cardId,
accountId,
userId,
} = row.original;
const isCartao = Boolean(cartaoName);
const label = cartaoName ?? contaName;
const logoSrc = resolveLogoSrc(cartaoLogo ?? contaLogo);
const href = cartaoId
? `/cards/${cartaoId}/invoice`
: contaId
? `/accounts/${contaId}/statement`
const href = cardId
? `/cards/${cardId}/invoice`
: accountId
? `/accounts/${accountId}/statement`
: null;
const isOwnData = userId === currentUserId;
@@ -458,7 +458,7 @@ const buildColumns = ({
<Tooltip>
<TooltipTrigger asChild>{content}</TooltipTrigger>
<TooltipContent side="top">
{isCartao ? "Cartão" : "Conta"}: {label}
{isCartao ? "Cartão" : "FinancialAccount"}: {label}
</TooltipContent>
</Tooltip>
);
@@ -484,7 +484,7 @@ const buildColumns = ({
</Link>
</TooltipTrigger>
<TooltipContent side="top">
{isCartao ? "Cartão" : "Conta"}: {label}
{isCartao ? "Cartão" : "FinancialAccount"}: {label}
</TooltipContent>
</Tooltip>
);
@@ -493,8 +493,8 @@ const buildColumns = ({
];
if (noteAsColumn) {
const contaCartaoIndex = columns.findIndex((c) => c.id === "contaCartao");
const noteColumn: ColumnDef<LancamentoItem> = {
const accountCardIndex = columns.findIndex((c) => c.id === "contaCartao");
const noteColumn: ColumnDef<TransactionItem> = {
accessorKey: "note",
header: "Anotação",
cell: ({ row }) => {
@@ -511,7 +511,7 @@ const buildColumns = ({
);
},
};
columns.splice(contaCartaoIndex, 0, noteColumn);
columns.splice(accountCardIndex, 0, noteColumn);
}
if (showActions) {
@@ -607,7 +607,7 @@ const buildColumns = ({
row.original.userId !== currentUserId && (
<DropdownMenuItem onSelect={() => handleImport(row.original)}>
<RiFileCopyLine className="size-4" />
Importar para Minha Conta
Importar para Minha FinancialAccount
</DropdownMenuItem>
)}
{row.original.userId === currentUserId && (
@@ -669,7 +669,7 @@ const buildColumns = ({
const FIXED_START_IDS = ["select", "purchaseDate"];
const FIXED_END_IDS = ["actions"];
function getColumnId(col: ColumnDef<LancamentoItem>): string {
function getColumnId(col: ColumnDef<TransactionItem>): string {
const c = col as { id?: string; accessorKey?: string };
return c.id ?? c.accessorKey ?? "";
}
@@ -686,15 +686,15 @@ function reorderColumnsByPreference<T>(
const fixedEnd: ColumnDef<T>[] = [];
for (const col of columns) {
const id = getColumnId(col as ColumnDef<LancamentoItem>);
const id = getColumnId(col as ColumnDef<TransactionItem>);
if (FIXED_START_IDS.includes(id)) fixedStart.push(col);
else if (FIXED_END_IDS.includes(id)) fixedEnd.push(col);
else reorderable.push(col);
}
const sorted = [...reorderable].sort((a, b) => {
const idA = getColumnId(a as ColumnDef<LancamentoItem>);
const idB = getColumnId(b as ColumnDef<LancamentoItem>);
const idA = getColumnId(a as ColumnDef<TransactionItem>);
const idB = getColumnId(b as ColumnDef<TransactionItem>);
const indexA = order.indexOf(idA);
const indexB = order.indexOf(idB);
if (indexA === -1 && indexB === -1) return 0;
@@ -707,39 +707,39 @@ function reorderColumnsByPreference<T>(
}
type LancamentosTableProps = {
data: LancamentoItem[];
data: TransactionItem[];
currentUserId: string;
noteAsColumn?: boolean;
columnOrder?: string[] | null;
pagadorFilterOptions?: LancamentoFilterOption[];
categoriaFilterOptions?: LancamentoFilterOption[];
contaCartaoFilterOptions?: ContaCartaoFilterOption[];
payerFilterOptions?: TransactionFilterOption[];
categoryFilterOptions?: TransactionFilterOption[];
accountCardFilterOptions?: AccountCardFilterOption[];
selectedPeriod?: string;
onCreate?: (type: "Despesa" | "Receita") => void;
onMassAdd?: () => void;
onEdit?: (item: LancamentoItem) => void;
onCopy?: (item: LancamentoItem) => void;
onImport?: (item: LancamentoItem) => void;
onConfirmDelete?: (item: LancamentoItem) => void;
onBulkDelete?: (items: LancamentoItem[]) => void;
onBulkImport?: (items: LancamentoItem[]) => void;
onViewDetails?: (item: LancamentoItem) => void;
onToggleSettlement?: (item: LancamentoItem) => void;
onAnticipate?: (item: LancamentoItem) => void;
onViewAnticipationHistory?: (item: LancamentoItem) => void;
onEdit?: (item: TransactionItem) => void;
onCopy?: (item: TransactionItem) => void;
onImport?: (item: TransactionItem) => void;
onConfirmDelete?: (item: TransactionItem) => void;
onBulkDelete?: (items: TransactionItem[]) => void;
onBulkImport?: (items: TransactionItem[]) => void;
onViewDetails?: (item: TransactionItem) => void;
onToggleSettlement?: (item: TransactionItem) => void;
onAnticipate?: (item: TransactionItem) => void;
onViewAnticipationHistory?: (item: TransactionItem) => void;
isSettlementLoading?: (id: string) => boolean;
showActions?: boolean;
showFilters?: boolean;
};
export function LancamentosTable({
export function TransactionsTable({
data,
currentUserId,
noteAsColumn = false,
columnOrder: columnOrderPreference = null,
pagadorFilterOptions = [],
categoriaFilterOptions = [],
contaCartaoFilterOptions = [],
payerFilterOptions = [],
categoryFilterOptions = [],
accountCardFilterOptions = [],
selectedPeriod,
onCreate,
onMassAdd,
@@ -904,15 +904,15 @@ export function LancamentosTable({
)}
{showFilters ? (
<LancamentosFilters
pagadorOptions={pagadorFilterOptions}
categoriaOptions={categoriaFilterOptions}
contaCartaoOptions={contaCartaoFilterOptions}
<TransactionsFilters
payerOptions={payerFilterOptions}
categoryOptions={categoryFilterOptions}
accountCardOptions={accountCardFilterOptions}
className="w-full lg:flex-1 lg:justify-end"
hideAdvancedFilters={hasOtherUserData}
exportButton={
selectedPeriod ? (
<LancamentosExport
<TransactionsExport
lancamentos={data}
period={selectedPeriod}
/>

View File

@@ -25,14 +25,14 @@ import {
loadExportLogoDataUrl,
} from "@/shared/utils/export-branding";
import { displayPeriod } from "@/shared/utils/period";
import type { LancamentoItem } from "./types";
import type { TransactionItem } from "./types";
interface LancamentosExportProps {
lancamentos: LancamentoItem[];
lancamentos: TransactionItem[];
period: string;
}
export function LancamentosExport({
export function TransactionsExport({
lancamentos,
period,
}: LancamentosExportProps) {
@@ -52,21 +52,21 @@ export function LancamentosExport({
);
};
const getContaCartaoName = (lancamento: LancamentoItem) => {
if (lancamento.contaName) return lancamento.contaName;
if (lancamento.cartaoName) return lancamento.cartaoName;
const getContaCartaoName = (transaction: TransactionItem) => {
if (transaction.contaName) return transaction.contaName;
if (transaction.cartaoName) return transaction.cartaoName;
return "-";
};
const getNameWithInstallment = (lancamento: LancamentoItem) => {
const getNameWithInstallment = (transaction: TransactionItem) => {
const isInstallment =
lancamento.condition.trim().toLowerCase() === "parcelado";
transaction.condition.trim().toLowerCase() === "parcelado";
if (!isInstallment || !lancamento.installmentCount) {
return lancamento.name;
if (!isInstallment || !transaction.installmentCount) {
return transaction.name;
}
return `${lancamento.name} (${lancamento.currentInstallment ?? 1}/${lancamento.installmentCount})`;
return `${transaction.name} (${transaction.currentInstallment ?? 1}/${transaction.installmentCount})`;
};
const exportToCSV = () => {
@@ -80,9 +80,9 @@ export function LancamentosExport({
"Condição",
"Pagamento",
"Valor",
"Categoria",
"Category",
"Conta/Cartão",
"Pagador",
"Payer",
];
const rows: string[][] = [];
@@ -138,9 +138,9 @@ export function LancamentosExport({
"Condição",
"Pagamento",
"Valor",
"Categoria",
"Category",
"Conta/Cartão",
"Pagador",
"Payer",
];
const rows: (string | number)[][] = [];
@@ -168,9 +168,9 @@ export function LancamentosExport({
{ wch: 15 }, // Condição
{ wch: 20 }, // Pagamento
{ wch: 15 }, // Valor
{ wch: 20 }, // Categoria
{ wch: 20 }, // Category
{ wch: 20 }, // Conta/Cartão
{ wch: 20 }, // Pagador
{ wch: 20 }, // Payer
];
const wb = XLSX.utils.book_new();
@@ -241,7 +241,7 @@ export function LancamentosExport({
"Valor",
"Categoria",
"Conta/Cartão",
"Pagador",
"Payer",
],
];
@@ -281,7 +281,7 @@ export function LancamentosExport({
5: { cellWidth: 24 }, // Valor
6: { cellWidth: 30 }, // Categoria
7: { cellWidth: 30 }, // Conta/Cartão
8: { cellWidth: 31 }, // Pagador
8: { cellWidth: 31 }, // Payer
},
didParseCell: (cellData) => {
if (cellData.section === "body" && cellData.column.index === 5) {

View File

@@ -1,4 +1,4 @@
export type LancamentoItem = {
export type TransactionItem = {
id: string;
userId: string;
name: string;
@@ -8,17 +8,17 @@ export type LancamentoItem = {
amount: number;
condition: string;
paymentMethod: string;
pagadorId: string | null;
payerId: string | null;
pagadorName: string | null;
pagadorAvatar: string | null;
pagadorRole: string | null;
contaId: string | null;
accountId: string | null;
contaName: string | null;
contaLogo: string | null;
cartaoId: string | null;
cardId: string | null;
cartaoName: string | null;
cartaoLogo: string | null;
categoriaId: string | null;
categoryId: string | null;
categoriaName: string | null;
categoriaType: string | null;
categoriaIcon: string | null;
@@ -50,14 +50,14 @@ export type SelectOption = {
dueDay?: string | null;
};
export type LancamentoFilterOption = {
export type TransactionFilterOption = {
slug: string;
label: string;
icon?: string | null;
avatarUrl?: string | null;
};
export type ContaCartaoFilterOption = LancamentoFilterOption & {
export type AccountCardFilterOption = TransactionFilterOption & {
kind: "conta" | "cartao";
logo?: string | null;
};

View File

@@ -1,16 +1,16 @@
export const LANCAMENTO_TRANSACTION_TYPES = [
export const TRANSACTION_TYPES = [
"Despesa",
"Receita",
"Transferência",
] as const;
export const LANCAMENTO_CONDITIONS = [
export const TRANSACTION_CONDITIONS = [
"À vista",
"Parcelado",
"Recorrente",
] as const;
export const LANCAMENTO_PAYMENT_METHODS = [
export const PAYMENT_METHODS = [
"Cartão de crédito",
"Cartão de débito",
"Pix",

View File

@@ -1,10 +1,10 @@
import type { LancamentoItem } from "@/features/transactions/components/types";
import type { TransactionItem } from "@/features/transactions/components/types";
import { getTodayDateString } from "@/shared/utils/date";
import { derivePeriodFromDate, getNextPeriod } from "@/shared/utils/period";
import {
LANCAMENTO_CONDITIONS,
LANCAMENTO_PAYMENT_METHODS,
LANCAMENTO_TRANSACTION_TYPES,
PAYMENT_METHODS,
TRANSACTION_CONDITIONS,
TRANSACTION_TYPES,
} from "./constants";
/**
@@ -68,7 +68,7 @@ export type SplitType = "equal" | "60-40" | "70-30" | "80-20" | "custom";
/**
* Form state type for lancamento dialog
*/
export type LancamentoFormState = {
export type TransactionFormState = {
purchaseDate: string;
period: string;
name: string;
@@ -76,15 +76,15 @@ export type LancamentoFormState = {
amount: string;
condition: string;
paymentMethod: string;
pagadorId: string | undefined;
secondaryPagadorId: string | undefined;
payerId: string | undefined;
secondaryPayerId: string | undefined;
isSplit: boolean;
splitType: SplitType;
primarySplitAmount: string;
secondarySplitAmount: string;
contaId: string | undefined;
cartaoId: string | undefined;
categoriaId: string | undefined;
accountId: string | undefined;
cardId: string | undefined;
categoryId: string | undefined;
installmentCount: string;
recurrenceCount: string;
dueDate: string;
@@ -97,7 +97,7 @@ export type LancamentoFormState = {
* Initial state overrides for lancamento form
*/
export type LancamentoFormOverrides = {
defaultCartaoId?: string | null;
defaultCardId?: string | null;
defaultPaymentMethod?: string | null;
defaultPurchaseDate?: string | null;
defaultName?: string | null;
@@ -109,20 +109,20 @@ export type LancamentoFormOverrides = {
/**
* Builds initial form state from lancamento data and defaults
*/
export function buildLancamentoInitialState(
lancamento?: LancamentoItem,
defaultPagadorId?: string | null,
export function buildTransactionInitialState(
transaction?: TransactionItem,
defaultPayerId?: string | null,
preferredPeriod?: string,
overrides?: LancamentoFormOverrides,
): LancamentoFormState {
const purchaseDate = lancamento?.purchaseDate
? lancamento.purchaseDate.slice(0, 10)
): TransactionFormState {
const purchaseDate = transaction?.purchaseDate
? transaction.purchaseDate.slice(0, 10)
: (overrides?.defaultPurchaseDate ?? "");
const paymentMethod =
lancamento?.paymentMethod ??
transaction?.paymentMethod ??
overrides?.defaultPaymentMethod ??
LANCAMENTO_PAYMENT_METHODS[0];
PAYMENT_METHODS[0];
const derivedPeriod = derivePeriodFromDate(purchaseDate);
const fallbackPeriod =
@@ -132,28 +132,28 @@ export function buildLancamentoInitialState(
// Quando importando, usar valores padrão do usuário logado ao invés dos valores do lançamento original
const isImporting = overrides?.isImporting ?? false;
const fallbackPagadorId = isImporting
? (defaultPagadorId ?? null)
: (lancamento?.pagadorId ?? defaultPagadorId ?? null);
const fallbackPayerId = isImporting
? (defaultPayerId ?? null)
: (transaction?.payerId ?? defaultPayerId ?? null);
const boletoPaymentDate =
lancamento?.boletoPaymentDate ??
(paymentMethod === "Boleto" && (lancamento?.isSettled ?? false)
transaction?.boletoPaymentDate ??
(paymentMethod === "Boleto" && (transaction?.isSettled ?? false)
? getTodayDateString()
: "");
// Calcular o valor correto para importação de parcelados
let amountValue = overrides?.defaultAmount ?? "";
if (!amountValue && typeof lancamento?.amount === "number") {
let baseAmount = Math.abs(lancamento.amount);
if (!amountValue && typeof transaction?.amount === "number") {
let baseAmount = Math.abs(transaction.amount);
// Se está importando e é parcelado, usar o valor total (parcela * quantidade)
if (
isImporting &&
lancamento.condition === "Parcelado" &&
lancamento.installmentCount
transaction.condition === "Parcelado" &&
transaction.installmentCount
) {
baseAmount = baseAmount * lancamento.installmentCount;
baseAmount = baseAmount * transaction.installmentCount;
}
amountValue = (Math.round(baseAmount * 100) / 100).toFixed(2);
@@ -162,51 +162,51 @@ export function buildLancamentoInitialState(
return {
purchaseDate,
period:
lancamento?.period && /^\d{4}-\d{2}$/.test(lancamento.period)
? lancamento.period
transaction?.period && /^\d{4}-\d{2}$/.test(transaction.period)
? transaction.period
: fallbackPeriod,
name: lancamento?.name ?? overrides?.defaultName ?? "",
name: transaction?.name ?? overrides?.defaultName ?? "",
transactionType:
lancamento?.transactionType ??
transaction?.transactionType ??
overrides?.defaultTransactionType ??
LANCAMENTO_TRANSACTION_TYPES[0],
TRANSACTION_TYPES[0],
amount: amountValue,
condition: lancamento?.condition ?? LANCAMENTO_CONDITIONS[0],
condition: transaction?.condition ?? TRANSACTION_CONDITIONS[0],
paymentMethod,
pagadorId: fallbackPagadorId ?? undefined,
secondaryPagadorId: undefined,
payerId: fallbackPayerId ?? undefined,
secondaryPayerId: undefined,
isSplit: false,
splitType: "equal",
primarySplitAmount: "",
secondarySplitAmount: "",
contaId:
accountId:
paymentMethod === "Cartão de crédito"
? undefined
: isImporting
? undefined
: (lancamento?.contaId ?? undefined),
cartaoId:
: (transaction?.accountId ?? undefined),
cardId:
paymentMethod === "Cartão de crédito"
? isImporting
? (overrides?.defaultCartaoId ?? undefined)
: (lancamento?.cartaoId ?? overrides?.defaultCartaoId ?? undefined)
? (overrides?.defaultCardId ?? undefined)
: (transaction?.cardId ?? overrides?.defaultCardId ?? undefined)
: undefined,
categoriaId: isImporting
categoryId: isImporting
? undefined
: (lancamento?.categoriaId ?? undefined),
installmentCount: lancamento?.installmentCount
? String(lancamento.installmentCount)
: (transaction?.categoryId ?? undefined),
installmentCount: transaction?.installmentCount
? String(transaction.installmentCount)
: "",
recurrenceCount: lancamento?.recurrenceCount
? String(lancamento.recurrenceCount)
recurrenceCount: transaction?.recurrenceCount
? String(transaction.recurrenceCount)
: "",
dueDate: lancamento?.dueDate ?? "",
dueDate: transaction?.dueDate ?? "",
boletoPaymentDate,
note: lancamento?.note ?? "",
note: transaction?.note ?? "",
isSettled:
paymentMethod === "Cartão de crédito"
? null
: (lancamento?.isSettled ?? true),
: (transaction?.isSettled ?? true),
};
}
@@ -248,12 +248,12 @@ export function calculateSplitAmounts(
* This function encapsulates the business logic for field interdependencies
*/
export function applyFieldDependencies(
key: keyof LancamentoFormState,
value: LancamentoFormState[keyof LancamentoFormState],
currentState: LancamentoFormState,
key: keyof TransactionFormState,
value: TransactionFormState[keyof TransactionFormState],
currentState: TransactionFormState,
cardInfo?: { closingDay: string | null; dueDay: string | null } | null,
): Partial<LancamentoFormState> {
const updates: Partial<LancamentoFormState> = {};
): Partial<TransactionFormState> {
const updates: Partial<TransactionFormState> = {};
// Auto-derive period from purchaseDate
if (key === "purchaseDate" && typeof value === "string" && value) {
@@ -276,11 +276,8 @@ export function applyFieldDependencies(
}
}
// Auto-derive period when cartaoId changes (credit card selected)
if (
key === "cartaoId" &&
currentState.paymentMethod === "Cartão de crédito"
) {
// Auto-derive period when cardId changes (credit card selected)
if (key === "cardId" && currentState.paymentMethod === "Cartão de crédito") {
if (typeof value === "string" && value && currentState.purchaseDate) {
updates.period = deriveCreditCardPeriod(
currentState.purchaseDate,
@@ -303,10 +300,10 @@ export function applyFieldDependencies(
// When payment method changes, adjust related fields
if (key === "paymentMethod" && typeof value === "string") {
if (value === "Cartão de crédito") {
updates.contaId = undefined;
updates.accountId = undefined;
updates.isSettled = null;
} else {
updates.cartaoId = undefined;
updates.cardId = undefined;
updates.isSettled = currentState.isSettled ?? true;
}
@@ -314,7 +311,7 @@ export function applyFieldDependencies(
if (value === "Cartão de crédito") {
if (
currentState.purchaseDate &&
currentState.cartaoId &&
currentState.cardId &&
cardInfo?.closingDay
) {
updates.period = deriveCreditCardPeriod(
@@ -350,7 +347,7 @@ export function applyFieldDependencies(
// When split is disabled, clear secondary pagador and split fields
if (key === "isSplit" && value === false) {
updates.secondaryPagadorId = undefined;
updates.secondaryPayerId = undefined;
updates.splitType = "equal";
updates.primarySplitAmount = "";
updates.secondarySplitAmount = "";
@@ -383,10 +380,10 @@ export function applyFieldDependencies(
}
// When primary pagador changes, clear secondary if it matches
if (key === "pagadorId" && typeof value === "string") {
const secondaryValue = currentState.secondaryPagadorId;
if (key === "payerId" && typeof value === "string") {
const secondaryValue = currentState.secondaryPayerId;
if (secondaryValue && secondaryValue === value) {
updates.secondaryPagadorId = undefined;
updates.secondaryPayerId = undefined;
}
}

View File

@@ -1,41 +1,41 @@
import type { SQL } from "drizzle-orm";
import { and, eq, ilike, isNotNull, or } from "drizzle-orm";
import {
cartoes,
type categorias,
contas,
lancamentos,
type pagadores,
cards,
type categories,
financialAccounts,
type payers,
transactions,
} from "@/db/schema";
import type { SelectOption } from "@/features/transactions/components/types";
import {
LANCAMENTO_CONDITIONS,
LANCAMENTO_PAYMENT_METHODS,
LANCAMENTO_TRANSACTION_TYPES,
PAYMENT_METHODS,
TRANSACTION_CONDITIONS,
TRANSACTION_TYPES,
} from "@/features/transactions/constants";
import { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/shared/lib/accounts/constants";
import {
PAGADOR_ROLE_ADMIN,
PAGADOR_ROLE_TERCEIRO,
PAYER_ROLE_ADMIN,
PAYER_ROLE_THIRD_PARTY,
} from "@/shared/lib/payers/constants";
import { toDateOnlyString } from "@/shared/utils/date";
type PagadorRow = typeof pagadores.$inferSelect;
type ContaRow = typeof contas.$inferSelect;
type CartaoRow = typeof cartoes.$inferSelect;
type CategoriaRow = typeof categorias.$inferSelect;
type PayerRow = typeof payers.$inferSelect;
type AccountRow = typeof financialAccounts.$inferSelect;
type CardRow = typeof cards.$inferSelect;
type CategoryRow = typeof categories.$inferSelect;
export type ResolvedSearchParams =
| Record<string, string | string[] | undefined>
| undefined;
export type LancamentoSearchFilters = {
export type TransactionSearchFilters = {
transactionFilter: string | null;
conditionFilter: string | null;
paymentFilter: string | null;
pagadorFilter: string | null;
categoriaFilter: string | null;
contaCartaoFilter: string | null;
payerFilter: string | null;
categoryFilter: string | null;
accountCardFilter: string | null;
searchFilter: string | null;
};
@@ -45,23 +45,23 @@ type BaseSluggedOption = {
slug: string;
};
type PagadorSluggedOption = BaseSluggedOption & {
type PayerSluggedOption = BaseSluggedOption & {
role: string | null;
avatarUrl: string | null;
};
type CategoriaSluggedOption = BaseSluggedOption & {
type CategorySluggedOption = BaseSluggedOption & {
type: string | null;
icon: string | null;
};
type ContaSluggedOption = BaseSluggedOption & {
type AccountSluggedOption = BaseSluggedOption & {
kind: "conta";
logo: string | null;
accountType: string | null;
};
type CartaoSluggedOption = BaseSluggedOption & {
type CardSluggedOption = BaseSluggedOption & {
kind: "cartao";
logo: string | null;
closingDay: string | null;
@@ -69,17 +69,17 @@ type CartaoSluggedOption = BaseSluggedOption & {
};
export type SluggedFilters = {
pagadorFiltersRaw: PagadorSluggedOption[];
categoriaFiltersRaw: CategoriaSluggedOption[];
contaFiltersRaw: ContaSluggedOption[];
cartaoFiltersRaw: CartaoSluggedOption[];
payerFiltersRaw: PayerSluggedOption[];
categoryFiltersRaw: CategorySluggedOption[];
accountFiltersRaw: AccountSluggedOption[];
cardFiltersRaw: CardSluggedOption[];
};
export type SlugMaps = {
pagador: Map<string, string>;
categoria: Map<string, string>;
conta: Map<string, string>;
cartao: Map<string, string>;
payer: Map<string, string>;
category: Map<string, string>;
financialAccount: Map<string, string>;
card: Map<string, string>;
};
export type FilterOption = {
@@ -87,20 +87,20 @@ export type FilterOption = {
label: string;
};
export type ContaCartaoFilterOption = FilterOption & {
export type AccountCardFilterOption = FilterOption & {
kind: "conta" | "cartao";
};
export type LancamentoOptionSets = {
pagadorOptions: SelectOption[];
splitPagadorOptions: SelectOption[];
defaultPagadorId: string | null;
contaOptions: SelectOption[];
cartaoOptions: SelectOption[];
categoriaOptions: SelectOption[];
pagadorFilterOptions: FilterOption[];
categoriaFilterOptions: FilterOption[];
contaCartaoFilterOptions: ContaCartaoFilterOption[];
export type TransactionOptionSets = {
payerOptions: SelectOption[];
splitPayerOptions: SelectOption[];
defaultPayerId: string | null;
accountOptions: SelectOption[];
cardOptions: SelectOption[];
categoryOptions: SelectOption[];
payerFilterOptions: FilterOption[];
categoryFilterOptions: FilterOption[];
accountCardFilterOptions: AccountCardFilterOption[];
};
export const getSingleParam = (
@@ -114,15 +114,15 @@ export const getSingleParam = (
return Array.isArray(value) ? (value[0] ?? null) : value;
};
export const extractLancamentoSearchFilters = (
export const extractTransactionSearchFilters = (
params: ResolvedSearchParams,
): LancamentoSearchFilters => ({
transactionFilter: getSingleParam(params, "transacao"),
conditionFilter: getSingleParam(params, "condicao"),
paymentFilter: getSingleParam(params, "pagamento"),
pagadorFilter: getSingleParam(params, "pagador"),
categoriaFilter: getSingleParam(params, "categoria"),
contaCartaoFilter: getSingleParam(params, "contaCartao"),
): TransactionSearchFilters => ({
transactionFilter: getSingleParam(params, "type"),
conditionFilter: getSingleParam(params, "condition"),
paymentFilter: getSingleParam(params, "payment"),
payerFilter: getSingleParam(params, "payer"),
categoryFilter: getSingleParam(params, "category"),
accountCardFilter: getSingleParam(params, "accountCard"),
searchFilter: getSingleParam(params, "q"),
});
@@ -179,177 +179,178 @@ export const toOption = (
});
export const buildSluggedFilters = ({
pagadorRows,
categoriaRows,
contaRows,
cartaoRows,
payerRows,
categoryRows,
accountRows,
cardRows,
}: {
pagadorRows: PagadorRow[];
categoriaRows: CategoriaRow[];
contaRows: ContaRow[];
cartaoRows: CartaoRow[];
payerRows: PayerRow[];
categoryRows: CategoryRow[];
accountRows: AccountRow[];
cardRows: CardRow[];
}): SluggedFilters => {
const pagadorSlugger = createSlugGenerator();
const categoriaSlugger = createSlugGenerator();
const contaCartaoSlugger = createSlugGenerator();
const payerSlugger = createSlugGenerator();
const categorySlugger = createSlugGenerator();
const accountCardSlugger = createSlugGenerator();
const pagadorFiltersRaw = pagadorRows.map((pagador) => {
const label = normalizeLabel(pagador.name);
const payerFiltersRaw = payerRows.map((payer) => {
const label = normalizeLabel(payer.name);
return {
id: pagador.id,
id: payer.id,
label,
slug: pagadorSlugger(label),
role: pagador.role ?? null,
avatarUrl: pagador.avatarUrl ?? null,
slug: payerSlugger(label),
role: payer.role ?? null,
avatarUrl: payer.avatarUrl ?? null,
};
});
const categoriaFiltersRaw = categoriaRows.map((categoria) => {
const label = normalizeLabel(categoria.name);
const categoryFiltersRaw = categoryRows.map((category) => {
const label = normalizeLabel(category.name);
return {
id: categoria.id,
id: category.id,
label,
slug: categoriaSlugger(label),
type: categoria.type ?? null,
icon: categoria.icon ?? null,
slug: categorySlugger(label),
type: category.type ?? null,
icon: category.icon ?? null,
};
});
const contaFiltersRaw = contaRows.map((conta) => {
const label = normalizeLabel(conta.name);
const accountFiltersRaw = accountRows.map((account) => {
const label = normalizeLabel(account.name);
return {
id: conta.id,
id: account.id,
label,
slug: contaCartaoSlugger(label),
slug: accountCardSlugger(label),
kind: "conta" as const,
logo: conta.logo ?? null,
accountType: conta.accountType ?? null,
logo: account.logo ?? null,
accountType: account.accountType ?? null,
};
});
const cartaoFiltersRaw = cartaoRows.map((cartao) => {
const label = normalizeLabel(cartao.name);
const cardFiltersRaw = cardRows.map((card) => {
const label = normalizeLabel(card.name);
return {
id: cartao.id,
id: card.id,
label,
slug: contaCartaoSlugger(label),
slug: accountCardSlugger(label),
kind: "cartao" as const,
logo: cartao.logo ?? null,
closingDay: cartao.closingDay ?? null,
dueDay: cartao.dueDay ?? null,
logo: card.logo ?? null,
closingDay: card.closingDay ?? null,
dueDay: card.dueDay ?? null,
};
});
return {
pagadorFiltersRaw,
categoriaFiltersRaw,
contaFiltersRaw,
cartaoFiltersRaw,
payerFiltersRaw,
categoryFiltersRaw,
accountFiltersRaw,
cardFiltersRaw,
};
};
export const buildSlugMaps = ({
pagadorFiltersRaw,
categoriaFiltersRaw,
contaFiltersRaw,
cartaoFiltersRaw,
payerFiltersRaw,
categoryFiltersRaw,
accountFiltersRaw,
cardFiltersRaw,
}: SluggedFilters): SlugMaps => ({
pagador: new Map(pagadorFiltersRaw.map(({ slug, id }) => [slug, id])),
categoria: new Map(categoriaFiltersRaw.map(({ slug, id }) => [slug, id])),
conta: new Map(contaFiltersRaw.map(({ slug, id }) => [slug, id])),
cartao: new Map(cartaoFiltersRaw.map(({ slug, id }) => [slug, id])),
payer: new Map(payerFiltersRaw.map(({ slug, id }) => [slug, id])),
category: new Map(categoryFiltersRaw.map(({ slug, id }) => [slug, id])),
financialAccount: new Map(
accountFiltersRaw.map(({ slug, id }) => [slug, id]),
),
card: new Map(cardFiltersRaw.map(({ slug, id }) => [slug, id])),
});
const isValidTransaction = (
value: string | null,
): value is (typeof LANCAMENTO_TRANSACTION_TYPES)[number] =>
!!value &&
(LANCAMENTO_TRANSACTION_TYPES as readonly string[]).includes(value ?? "");
): value is (typeof TRANSACTION_TYPES)[number] =>
!!value && (TRANSACTION_TYPES as readonly string[]).includes(value ?? "");
const isValidCondition = (
value: string | null,
): value is (typeof LANCAMENTO_CONDITIONS)[number] =>
!!value && (LANCAMENTO_CONDITIONS as readonly string[]).includes(value ?? "");
): value is (typeof TRANSACTION_CONDITIONS)[number] =>
!!value &&
(TRANSACTION_CONDITIONS as readonly string[]).includes(value ?? "");
const isValidPaymentMethod = (
value: string | null,
): value is (typeof LANCAMENTO_PAYMENT_METHODS)[number] =>
!!value &&
(LANCAMENTO_PAYMENT_METHODS as readonly string[]).includes(value ?? "");
): value is (typeof PAYMENT_METHODS)[number] =>
!!value && (PAYMENT_METHODS as readonly string[]).includes(value ?? "");
const buildSearchPattern = (value: string | null) =>
value ? `%${value.trim().replace(/\s+/g, "%")}%` : null;
export const buildLancamentoWhere = ({
export const buildTransactionWhere = ({
userId,
period,
filters,
slugMaps,
cardId,
accountId,
pagadorId,
payerId,
}: {
userId: string;
period: string;
filters: LancamentoSearchFilters;
filters: TransactionSearchFilters;
slugMaps: SlugMaps;
cardId?: string;
accountId?: string;
pagadorId?: string;
payerId?: string;
}): SQL[] => {
const where: SQL[] = [
eq(lancamentos.userId, userId),
eq(lancamentos.period, period),
eq(transactions.userId, userId),
eq(transactions.period, period),
];
if (pagadorId) {
where.push(eq(lancamentos.pagadorId, pagadorId));
if (payerId) {
where.push(eq(transactions.payerId, payerId));
}
if (cardId) {
where.push(eq(lancamentos.cartaoId, cardId));
where.push(eq(transactions.cardId, cardId));
}
if (accountId) {
where.push(eq(lancamentos.contaId, accountId));
where.push(eq(transactions.accountId, accountId));
}
if (isValidTransaction(filters.transactionFilter)) {
where.push(eq(lancamentos.transactionType, filters.transactionFilter));
where.push(eq(transactions.transactionType, filters.transactionFilter));
}
if (isValidCondition(filters.conditionFilter)) {
where.push(eq(lancamentos.condition, filters.conditionFilter));
where.push(eq(transactions.condition, filters.conditionFilter));
}
if (isValidPaymentMethod(filters.paymentFilter)) {
where.push(eq(lancamentos.paymentMethod, filters.paymentFilter));
where.push(eq(transactions.paymentMethod, filters.paymentFilter));
}
if (!pagadorId && filters.pagadorFilter) {
const id = slugMaps.pagador.get(filters.pagadorFilter);
if (!payerId && filters.payerFilter) {
const id = slugMaps.payer.get(filters.payerFilter);
if (id) {
where.push(eq(lancamentos.pagadorId, id));
where.push(eq(transactions.payerId, id));
}
}
if (filters.categoriaFilter) {
const id = slugMaps.categoria.get(filters.categoriaFilter);
if (filters.categoryFilter) {
const id = slugMaps.category.get(filters.categoryFilter);
if (id) {
where.push(eq(lancamentos.categoriaId, id));
where.push(eq(transactions.categoryId, id));
}
}
if (filters.contaCartaoFilter) {
const contaId = slugMaps.conta.get(filters.contaCartaoFilter);
const relatedCartaoId = contaId
if (filters.accountCardFilter) {
const accountId = slugMaps.financialAccount.get(filters.accountCardFilter);
const relatedCardId = accountId
? null
: slugMaps.cartao.get(filters.contaCartaoFilter);
if (contaId) {
where.push(eq(lancamentos.contaId, contaId));
: slugMaps.card.get(filters.accountCardFilter);
if (accountId) {
where.push(eq(transactions.accountId, accountId));
}
if (!contaId && relatedCartaoId) {
where.push(eq(lancamentos.cartaoId, relatedCartaoId));
if (!accountId && relatedCardId) {
where.push(eq(transactions.cardId, relatedCardId));
}
}
@@ -357,12 +358,15 @@ export const buildLancamentoWhere = ({
if (searchPattern) {
where.push(
or(
ilike(lancamentos.name, searchPattern),
ilike(lancamentos.note, searchPattern),
ilike(lancamentos.paymentMethod, searchPattern),
ilike(lancamentos.condition, searchPattern),
and(isNotNull(contas.name), ilike(contas.name, searchPattern)),
and(isNotNull(cartoes.name), ilike(cartoes.name, searchPattern)),
ilike(transactions.name, searchPattern),
ilike(transactions.note, searchPattern),
ilike(transactions.paymentMethod, searchPattern),
ilike(transactions.condition, searchPattern),
and(
isNotNull(financialAccounts.name),
ilike(financialAccounts.name, searchPattern),
),
and(isNotNull(cards.name), ilike(cards.name, searchPattern)),
) as SQL,
);
}
@@ -370,38 +374,38 @@ export const buildLancamentoWhere = ({
return where;
};
type LancamentoRowWithRelations = typeof lancamentos.$inferSelect & {
pagador?: PagadorRow | null;
conta?: ContaRow | null;
cartao?: CartaoRow | null;
categoria?: CategoriaRow | null;
type TransactionRowWithRelations = Partial<typeof transactions.$inferSelect> & {
payer?: PayerRow | null;
financialAccount?: AccountRow | null;
card?: CardRow | null;
category?: CategoryRow | null;
};
export const mapLancamentosData = (rows: LancamentoRowWithRelations[]) =>
export const mapTransactionsData = (rows: TransactionRowWithRelations[]) =>
rows.map((item) => ({
id: item.id,
userId: item.userId,
name: item.name,
id: item.id ?? "",
userId: item.userId ?? "",
name: item.name ?? "",
purchaseDate: toDateOnlyString(item.purchaseDate) ?? "",
period: item.period ?? "",
transactionType: item.transactionType,
transactionType: item.transactionType ?? "",
amount: Number(item.amount ?? 0),
condition: item.condition,
paymentMethod: item.paymentMethod,
pagadorId: item.pagadorId ?? null,
pagadorName: item.pagador?.name ?? null,
pagadorAvatar: item.pagador?.avatarUrl ?? null,
pagadorRole: item.pagador?.role ?? null,
contaId: item.contaId ?? null,
contaName: item.conta?.name ?? null,
contaLogo: item.conta?.logo ?? null,
cartaoId: item.cartaoId ?? null,
cartaoName: item.cartao?.name ?? null,
cartaoLogo: item.cartao?.logo ?? null,
categoriaId: item.categoriaId ?? null,
categoriaName: item.categoria?.name ?? null,
categoriaType: item.categoria?.type ?? null,
categoriaIcon: item.categoria?.icon ?? null,
condition: item.condition ?? "",
paymentMethod: item.paymentMethod ?? "",
payerId: item.payerId ?? null,
pagadorName: item.payer?.name ?? null,
pagadorAvatar: item.payer?.avatarUrl ?? null,
pagadorRole: item.payer?.role ?? null,
accountId: item.accountId ?? null,
contaName: item.financialAccount?.name ?? null,
contaLogo: item.financialAccount?.logo ?? null,
cardId: item.cardId ?? null,
cartaoName: item.card?.name ?? null,
cartaoLogo: item.card?.logo ?? null,
categoryId: item.categoryId ?? null,
categoriaName: item.category?.name ?? null,
categoriaType: item.category?.type ?? null,
categoriaIcon: item.category?.icon ?? null,
installmentCount: item.installmentCount ?? null,
recurrenceCount: item.recurrenceCount ?? null,
currentInstallment: item.currentInstallment ?? null,
@@ -417,8 +421,8 @@ export const mapLancamentosData = (rows: LancamentoRowWithRelations[]) =>
seriesId: item.seriesId ?? null,
readonly:
Boolean(item.note?.startsWith(ACCOUNT_AUTO_INVOICE_NOTE_PREFIX)) ||
item.categoria?.name === "Saldo inicial" ||
item.categoria?.name === "Pagamentos",
item.category?.name === "Saldo inicial" ||
item.category?.name === "Pagamentos",
}));
const sortByLabel = <T extends { label: string }>(items: T[]) =>
@@ -427,45 +431,44 @@ const sortByLabel = <T extends { label: string }>(items: T[]) =>
);
export const buildOptionSets = ({
pagadorFiltersRaw,
categoriaFiltersRaw,
contaFiltersRaw,
cartaoFiltersRaw,
pagadorRows,
payerFiltersRaw,
categoryFiltersRaw,
accountFiltersRaw,
cardFiltersRaw,
payerRows,
limitCartaoId,
limitContaId,
}: SluggedFilters & {
pagadorRows: PagadorRow[];
payerRows: PayerRow[];
limitCartaoId?: string;
limitContaId?: string;
}): LancamentoOptionSets => {
const pagadorOptions = sortByLabel(
pagadorFiltersRaw.map(({ id, label, role, slug, avatarUrl }) =>
}): TransactionOptionSets => {
const payerOptions = sortByLabel(
payerFiltersRaw.map(({ id, label, role, slug, avatarUrl }) =>
toOption(id, label, role, undefined, slug, avatarUrl),
),
);
const pagadorFilterOptions = sortByLabel(
pagadorFiltersRaw.map(({ slug, label, avatarUrl }) => ({
const payerFilterOptions = sortByLabel(
payerFiltersRaw.map(({ slug, label, avatarUrl }) => ({
slug,
label,
avatarUrl,
})),
);
const defaultPagadorId =
pagadorRows.find((pagador) => pagador.role === PAGADOR_ROLE_ADMIN)?.id ??
null;
const defaultPayerId =
payerRows.find((payer) => payer.role === PAYER_ROLE_ADMIN)?.id ?? null;
const splitPagadorOptions = pagadorOptions.filter(
(option) => option.role === PAGADOR_ROLE_TERCEIRO,
const splitPayerOptions = payerOptions.filter(
(option) => option.role === PAYER_ROLE_THIRD_PARTY,
);
const contaOptionsSource = limitContaId
? contaFiltersRaw.filter((conta) => conta.id === limitContaId)
: contaFiltersRaw;
? accountFiltersRaw.filter((conta) => conta.id === limitContaId)
: accountFiltersRaw;
const contaOptions = sortByLabel(
const accountOptions = sortByLabel(
contaOptionsSource.map(({ id, label, slug, logo, accountType }) =>
toOption(
id,
@@ -482,10 +485,10 @@ export const buildOptionSets = ({
);
const cartaoOptionsSource = limitCartaoId
? cartaoFiltersRaw.filter((cartao) => cartao.id === limitCartaoId)
: cartaoFiltersRaw;
? cardFiltersRaw.filter((cartao) => cartao.id === limitCartaoId)
: cardFiltersRaw;
const cartaoOptions = sortByLabel(
const cardOptions = sortByLabel(
cartaoOptionsSource.map(({ id, label, slug, logo, closingDay, dueDay }) =>
toOption(
id,
@@ -503,18 +506,18 @@ export const buildOptionSets = ({
),
);
const categoriaOptions = sortByLabel(
categoriaFiltersRaw.map(({ id, label, type, slug, icon }) =>
const categoryOptions = sortByLabel(
categoryFiltersRaw.map(({ id, label, type, slug, icon }) =>
toOption(id, label, undefined, type, slug, undefined, undefined, icon),
),
);
const categoriaFilterOptions = sortByLabel(
categoriaFiltersRaw.map(({ slug, label, icon }) => ({ slug, label, icon })),
const categoryFilterOptions = sortByLabel(
categoryFiltersRaw.map(({ slug, label, icon }) => ({ slug, label, icon })),
);
const contaCartaoFilterOptions = sortByLabel(
[...contaFiltersRaw, ...cartaoFiltersRaw]
const accountCardFilterOptions = sortByLabel(
[...accountFiltersRaw, ...cardFiltersRaw]
.filter(
(option) =>
(limitCartaoId && option.kind === "cartao"
@@ -528,14 +531,14 @@ export const buildOptionSets = ({
);
return {
pagadorOptions,
splitPagadorOptions,
defaultPagadorId,
contaOptions,
cartaoOptions,
categoriaOptions,
pagadorFilterOptions,
categoriaFilterOptions,
contaCartaoFilterOptions,
payerOptions,
splitPayerOptions,
defaultPayerId,
accountOptions,
cardOptions,
categoryOptions,
payerFilterOptions,
categoryFilterOptions,
accountCardFilterOptions,
};
};

View File

@@ -1,69 +1,73 @@
import { and, desc, eq, gte, isNull, ne, or, type SQL } from "drizzle-orm";
import {
cartoes,
categorias,
contas,
lancamentos,
pagadores,
cards,
categories,
financialAccounts,
payers,
transactions,
} from "@/db/schema";
import { INITIAL_BALANCE_NOTE } from "@/shared/lib/accounts/constants";
import { db } from "@/shared/lib/db";
export async function fetchLancamentoFilterSources(userId: string) {
const [pagadorRows, contaRows, cartaoRows, categoriaRows] = await Promise.all(
[
db.query.pagadores.findMany({
where: eq(pagadores.userId, userId),
}),
db.query.contas.findMany({
where: and(eq(contas.userId, userId), eq(contas.status, "Ativa")),
}),
db.query.cartoes.findMany({
where: and(eq(cartoes.userId, userId), eq(cartoes.status, "Ativo")),
}),
db.query.categorias.findMany({
where: eq(categorias.userId, userId),
}),
],
);
export async function fetchTransactionFilterSources(userId: string) {
const [payerRows, accountRows, cardRows, categoryRows] = await Promise.all([
db.query.payers.findMany({
where: eq(payers.userId, userId),
}),
db.query.financialAccounts.findMany({
where: and(
eq(financialAccounts.userId, userId),
eq(financialAccounts.status, "Ativa"),
),
}),
db.query.cards.findMany({
where: and(eq(cards.userId, userId), eq(cards.status, "Ativo")),
}),
db.query.categories.findMany({
where: eq(categories.userId, userId),
}),
]);
return { pagadorRows, contaRows, cartaoRows, categoriaRows };
return { payerRows, accountRows, cardRows, categoryRows };
}
export async function fetchLancamentos(filters: SQL[]) {
const lancamentoRows = await db
export async function fetchTransactions(filters: SQL[]) {
const transactionRows = await db
.select({
lancamento: lancamentos,
pagador: pagadores,
conta: contas,
cartao: cartoes,
categoria: categorias,
transaction: transactions,
payer: payers,
financialAccount: financialAccounts,
card: cards,
category: categories,
})
.from(lancamentos)
.leftJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
.leftJoin(contas, eq(lancamentos.contaId, contas.id))
.leftJoin(cartoes, eq(lancamentos.cartaoId, cartoes.id))
.leftJoin(categorias, eq(lancamentos.categoriaId, categorias.id))
.from(transactions)
.leftJoin(payers, eq(transactions.payerId, payers.id))
.leftJoin(
financialAccounts,
eq(transactions.accountId, financialAccounts.id),
)
.leftJoin(cards, eq(transactions.cardId, cards.id))
.leftJoin(categories, eq(transactions.categoryId, categories.id))
.where(
and(
...filters,
// Excluir saldos iniciais de contas que têm excludeInitialBalanceFromIncome = true
// Excluir saldos iniciais de financialAccounts que têm excludeInitialBalanceFromIncome = true
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),
),
),
)
.orderBy(desc(lancamentos.purchaseDate), desc(lancamentos.createdAt));
.orderBy(desc(transactions.purchaseDate), desc(transactions.createdAt));
// Transformar resultado para o formato esperado
return lancamentoRows.map((row) => ({
...row.lancamento,
pagador: row.pagador,
conta: row.conta,
cartao: row.cartao,
categoria: row.categoria,
return transactionRows.map((row) => ({
...row.transaction,
payer: row.payer,
financialAccount: row.financialAccount,
card: row.card,
category: row.category,
}));
}
@@ -74,15 +78,15 @@ export async function fetchRecentEstablishments(
threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);
const results = await db
.select({ name: lancamentos.name })
.from(lancamentos)
.select({ name: transactions.name })
.from(transactions)
.where(
and(
eq(lancamentos.userId, userId),
gte(lancamentos.purchaseDate, threeMonthsAgo),
eq(transactions.userId, userId),
gte(transactions.purchaseDate, threeMonthsAgo),
),
)
.orderBy(desc(lancamentos.purchaseDate));
.orderBy(desc(transactions.purchaseDate));
const uniqueNames = Array.from(
new Set<string>(

View File

@@ -111,7 +111,7 @@ export function CategoryReportSkeleton() {
/**
* Skeleton para a tabela de relatórios de categorias
* Mantém a estrutura de colunas: Categoria, Tipo, múltiplos períodos, Total
* Mantém a estrutura de colunas: Category, Tipo, múltiplos períodos, Total
*/
function CategoryReportTableSkeleton() {
// Simula 6 períodos (colunas)
@@ -122,7 +122,7 @@ function CategoryReportTableSkeleton() {
<Table>
<TableHeader>
<TableRow>
{/* Categoria */}
{/* Category */}
<TableHead className="w-[280px] min-w-[280px]">
<Skeleton className="h-4 w-20 rounded-2xl bg-foreground/10" />
</TableHead>

View File

@@ -24,8 +24,8 @@ export function TransactionsTableSkeleton() {
<TableHead className="w-[120px]">Valor</TableHead>
<TableHead className="w-[120px]">Condição</TableHead>
<TableHead className="w-[120px]">Pagamento</TableHead>
<TableHead className="w-[140px]">Pagador</TableHead>
<TableHead className="w-[140px]">Categoria</TableHead>
<TableHead className="w-[140px]">Payer</TableHead>
<TableHead className="w-[140px]">Category</TableHead>
<TableHead className="w-[140px]">Conta/Cartão</TableHead>
<TableHead className="w-[80px]">Ações</TableHead>
</TableRow>

View File

@@ -1,8 +1,8 @@
import type {
AntecipacaoParcela,
Categoria,
Lancamento,
Pagador,
Category,
InstallmentAnticipation,
Payer,
Transaction,
} from "@/db/schema";
/**
@@ -18,17 +18,17 @@ export type EligibleInstallment = {
currentInstallment: number | null;
installmentCount: number | null;
paymentMethod: string;
categoriaId: string | null;
pagadorId: string | null;
categoryId: string | null;
payerId: string | null;
};
/**
* Antecipação com dados completos
*/
export type InstallmentAnticipationWithRelations = AntecipacaoParcela & {
lancamento: Lancamento;
pagador: Pagador | null;
categoria: Categoria | null;
export type InstallmentAnticipationWithRelations = InstallmentAnticipation & {
transaction: Transaction;
payer: Payer | null;
category: Category | null;
};
/**
@@ -39,8 +39,8 @@ export type CreateAnticipationInput = {
installmentIds: string[];
anticipationPeriod: string;
discount?: number;
pagadorId?: string;
categoriaId?: string;
payerId?: string;
categoryId?: string;
note?: string;
};

View File

@@ -1,24 +1,24 @@
import type {
LancamentoItem,
SelectOption,
TransactionItem,
} from "@/features/transactions/components/types";
export type CalendarEvent =
| {
id: string;
type: "lancamento";
type: "transaction";
date: string;
lancamento: LancamentoItem;
transaction: TransactionItem;
}
| {
id: string;
type: "boleto";
date: string;
lancamento: LancamentoItem;
transaction: TransactionItem;
}
| {
id: string;
type: "cartao";
type: "card";
date: string;
card: {
id: string;
@@ -47,12 +47,12 @@ export type CalendarDay = {
};
export type CalendarFormOptions = {
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[];
};