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

View File

@@ -1,5 +1,5 @@
import { redirect } from "next/navigation"; 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 { fetchCategoryChartData } from "@/features/reports/category-chart-queries";
import { fetchCategoryReport } from "@/features/reports/category-report-queries"; import { fetchCategoryReport } from "@/features/reports/category-report-queries";
import { fetchUserCategories } from "@/features/reports/category-trends-queries"; import { fetchUserCategories } from "@/features/reports/category-trends-queries";
@@ -38,7 +38,7 @@ export default async function Page({ searchParams }: PageProps) {
// Extract query params // Extract query params
const inicioParam = getSingleParam(resolvedSearchParams, "inicio"); const inicioParam = getSingleParam(resolvedSearchParams, "inicio");
const fimParam = getSingleParam(resolvedSearchParams, "fim"); const fimParam = getSingleParam(resolvedSearchParams, "fim");
const categoriasParam = getSingleParam(resolvedSearchParams, "categorias"); const categoriasParam = getSingleParam(resolvedSearchParams, "categories");
// Calculate default period (last 6 months) // Calculate default period (last 6 months)
const currentPeriod = getCurrentPeriod(); const currentPeriod = getCurrentPeriod();
@@ -63,11 +63,11 @@ export default async function Page({ searchParams }: PageProps) {
} }
// Fetch all categories for the user // Fetch all categories for the user
const categoriaRows = await fetchUserCategories(userId); const categoryRows = await fetchUserCategories(userId);
// Map to CategoryOption format // Map to CategoryOption format
const categoryOptions: CategoryOption[] = categoriaRows.map( const categoryOptions: CategoryOption[] = categoryRows.map(
(cat: Categoria): CategoryOption => ({ (cat: Category): CategoryOption => ({
id: cat.id, id: cat.id,
name: cat.name, name: cat.name,
icon: cat.icon, 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 { SummaryCards } from "@/features/reports/components/establishments/summary-cards";
import { TopCategories } from "@/features/reports/components/establishments/top-categories"; import { TopCategories } from "@/features/reports/components/establishments/top-categories";
import { import {
fetchTopEstabelecimentosData, fetchTopEstablishmentsData,
type PeriodFilter, type PeriodFilter,
} from "@/features/reports/establishments/queries"; } from "@/features/reports/establishments/queries";
import { Card } from "@/shared/components/ui/card"; import { Card } from "@/shared/components/ui/card";
@@ -44,7 +44,7 @@ export default async function TopEstabelecimentosPage({
const { period: currentPeriod } = parsePeriodParam(periodoParam); const { period: currentPeriod } = parsePeriodParam(periodoParam);
const periodFilter = validatePeriodFilter(mesesParam); const periodFilter = validatePeriodFilter(mesesParam);
const data = await fetchTopEstabelecimentosData( const data = await fetchTopEstablishmentsData(
user.id, user.id,
currentPeriod, currentPeriod,
periodFilter, periodFilter,

View File

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

View File

@@ -10,9 +10,9 @@ const LEGEND_ITEMS: Array<{
label: string; label: string;
dotColor?: string; dotColor?: string;
}> = [ }> = [
{ type: "lancamento", label: "Lançamentos" }, { type: "transaction", label: "Lançamentos" },
{ type: "boleto", label: "Boleto com vencimento" }, { 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" }, { label: "Pagamento fatura", dotColor: "bg-success" },
]; ];

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,8 @@
import { and, eq, sql } from "drizzle-orm"; 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 { INITIAL_BALANCE_NOTE } from "@/shared/lib/accounts/constants";
import { db } from "@/shared/lib/db"; 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"; import { safeToNumber as toNumber } from "@/shared/utils/number";
type RawDashboardAccount = { type RawDashboardAccount = {
@@ -36,49 +36,49 @@ export async function fetchDashboardAccounts(
): Promise<DashboardAccountsSnapshot> { ): Promise<DashboardAccountsSnapshot> {
const rows = await db const rows = await db
.select({ .select({
id: contas.id, id: financialAccounts.id,
name: contas.name, name: financialAccounts.name,
accountType: contas.accountType, accountType: financialAccounts.accountType,
status: contas.status, status: financialAccounts.status,
logo: contas.logo, logo: financialAccounts.logo,
initialBalance: contas.initialBalance, initialBalance: financialAccounts.initialBalance,
excludeFromBalance: contas.excludeFromBalance, excludeFromBalance: financialAccounts.excludeFromBalance,
balanceMovements: sql<number>` balanceMovements: sql<number>`
coalesce( coalesce(
sum( sum(
case case
when ${lancamentos.note} = ${INITIAL_BALANCE_NOTE} then 0 when ${transactions.note} = ${INITIAL_BALANCE_NOTE} then 0
else ${lancamentos.amount} else ${transactions.amount}
end end
), ),
0 0
) )
`, `,
}) })
.from(contas) .from(financialAccounts)
.leftJoin( .leftJoin(
lancamentos, transactions,
and( and(
eq(lancamentos.contaId, contas.id), eq(transactions.accountId, financialAccounts.id),
eq(lancamentos.userId, userId), eq(transactions.userId, userId),
eq(lancamentos.isSettled, true), eq(transactions.isSettled, true),
), ),
) )
.leftJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id)) .leftJoin(payers, eq(transactions.payerId, payers.id))
.where( .where(
and( and(
eq(contas.userId, userId), eq(financialAccounts.userId, userId),
sql`(${lancamentos.id} IS NULL OR ${pagadores.role} = ${PAGADOR_ROLE_ADMIN})`, sql`(${transactions.id} IS NULL OR ${payers.role} = ${PAYER_ROLE_ADMIN})`,
), ),
) )
.groupBy( .groupBy(
contas.id, financialAccounts.id,
contas.name, financialAccounts.name,
contas.accountType, financialAccounts.accountType,
contas.status, financialAccounts.status,
contas.logo, financialAccounts.logo,
contas.initialBalance, financialAccounts.initialBalance,
contas.excludeFromBalance, financialAccounts.excludeFromBalance,
); );
const accounts = rows const accounts = rows

View File

@@ -1,9 +1,9 @@
"use server"; "use server";
import { and, asc, eq } from "drizzle-orm"; import { and, asc, eq } from "drizzle-orm";
import { lancamentos } from "@/db/schema"; import { transactions } from "@/db/schema";
import { db } from "@/shared/lib/db"; 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 { toDateOnlyString } from "@/shared/utils/date";
import { safeToNumber as toNumber } from "@/shared/utils/number"; import { safeToNumber as toNumber } from "@/shared/utils/number";
@@ -37,33 +37,33 @@ export async function fetchDashboardBills(
userId: string, userId: string,
period: string, period: string,
): Promise<DashboardBillsSnapshot> { ): Promise<DashboardBillsSnapshot> {
const adminPagadorId = await getAdminPagadorId(userId); const adminPayerId = await getAdminPayerId(userId);
if (!adminPagadorId) { if (!adminPayerId) {
return { bills: [], totalPendingAmount: 0, pendingCount: 0 }; return { bills: [], totalPendingAmount: 0, pendingCount: 0 };
} }
const rows = await db const rows = await db
.select({ .select({
id: lancamentos.id, id: transactions.id,
name: lancamentos.name, name: transactions.name,
amount: lancamentos.amount, amount: transactions.amount,
dueDate: lancamentos.dueDate, dueDate: transactions.dueDate,
boletoPaymentDate: lancamentos.boletoPaymentDate, boletoPaymentDate: transactions.boletoPaymentDate,
isSettled: lancamentos.isSettled, isSettled: transactions.isSettled,
}) })
.from(lancamentos) .from(transactions)
.where( .where(
and( and(
eq(lancamentos.userId, userId), eq(transactions.userId, userId),
eq(lancamentos.period, period), eq(transactions.period, period),
eq(lancamentos.paymentMethod, PAYMENT_METHOD_BOLETO), eq(transactions.paymentMethod, PAYMENT_METHOD_BOLETO),
eq(lancamentos.pagadorId, adminPagadorId), eq(transactions.payerId, adminPayerId),
), ),
) )
.orderBy( .orderBy(
asc(lancamentos.isSettled), asc(transactions.isSettled),
asc(lancamentos.dueDate), asc(transactions.dueDate),
asc(lancamentos.name), asc(transactions.name),
); );
const bills = rows.map((row: RawDashboardBill): DashboardBill => { const bills = rows.map((row: RawDashboardBill): DashboardBill => {

View File

@@ -28,7 +28,7 @@ type CategoryBreakdownRow = {
}; };
type CategoryBudgetRow = { type CategoryBudgetRow = {
categoriaId: string | null; categoryId: string | null;
amount: unknown; amount: unknown;
}; };
@@ -43,8 +43,8 @@ export function buildCategoryBreakdownData({
}): DashboardCategoryBreakdownData { }): DashboardCategoryBreakdownData {
const budgetMap = new Map<string, number>(); const budgetMap = new Map<string, number>();
for (const row of budgetRows) { for (const row of budgetRows) {
if (row.categoriaId) { if (row.categoryId) {
budgetMap.set(row.categoriaId, toNumber(row.amount)); 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 { and, desc, eq, isNull, ne, or, sql } from "drizzle-orm";
import { categorias, contas, lancamentos, pagadores } from "@/db/schema"; import {
import { mapLancamentosData } from "@/features/transactions/page-helpers"; categories,
financialAccounts,
payers,
transactions,
} from "@/db/schema";
import { mapTransactionsData } from "@/features/transactions/page-helpers";
import { import {
ACCOUNT_AUTO_INVOICE_NOTE_PREFIX, ACCOUNT_AUTO_INVOICE_NOTE_PREFIX,
INITIAL_BALANCE_NOTE, INITIAL_BALANCE_NOTE,
} from "@/shared/lib/accounts/constants"; } from "@/shared/lib/accounts/constants";
import type { CategoryType } from "@/shared/lib/categories/constants"; import type { CategoryType } from "@/shared/lib/categories/constants";
import { db } from "@/shared/lib/db"; 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 { calculatePercentageChange } from "@/shared/utils/math";
import { safeToNumber as toNumber } from "@/shared/utils/number"; import { safeToNumber as toNumber } from "@/shared/utils/number";
import { getPreviousPeriod } from "@/shared/utils/period"; import { getPreviousPeriod } from "@/shared/utils/period";
type MappedLancamentos = ReturnType<typeof mapLancamentosData>; type MappedLancamentos = ReturnType<typeof mapTransactionsData>;
export type CategoryDetailData = { export type CategoryDetailData = {
category: { category: {
@@ -34,8 +39,8 @@ export async function fetchCategoryDetails(
categoryId: string, categoryId: string,
period: string, period: string,
): Promise<CategoryDetailData | null> { ): Promise<CategoryDetailData | null> {
const category = await db.query.categorias.findFirst({ const category = await db.query.categories.findFirst({
where: and(eq(categorias.userId, userId), eq(categorias.id, categoryId)), where: and(eq(categories.userId, userId), eq(categories.id, categoryId)),
}); });
if (!category) { if (!category) {
@@ -46,35 +51,35 @@ export async function fetchCategoryDetails(
const transactionType = category.type === "receita" ? "Receita" : "Despesa"; const transactionType = category.type === "receita" ? "Receita" : "Despesa";
const sanitizedNote = or( const sanitizedNote = or(
isNull(lancamentos.note), isNull(transactions.note),
sql`${lancamentos.note} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`, 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( where: and(
eq(lancamentos.userId, userId), eq(transactions.userId, userId),
eq(lancamentos.categoriaId, categoryId), eq(transactions.categoryId, categoryId),
eq(lancamentos.transactionType, transactionType), eq(transactions.transactionType, transactionType),
eq(lancamentos.period, period), eq(transactions.period, period),
sanitizedNote, sanitizedNote,
), ),
with: { with: {
pagador: true, payer: true,
conta: true, financialAccount: true,
cartao: true, card: true,
categoria: true, category: true,
}, },
orderBy: [desc(lancamentos.purchaseDate), desc(lancamentos.createdAt)], orderBy: [desc(transactions.purchaseDate), desc(transactions.createdAt)],
}); });
const filteredRows = currentRows.filter((row) => { const filteredRows = currentRows.filter((row) => {
// Filtrar apenas pagadores admin // Filtrar apenas payers admin
if (row.pagador?.role !== PAGADOR_ROLE_ADMIN) return false; if (row.payer?.role !== PAYER_ROLE_ADMIN) return false;
// Excluir saldos iniciais se a conta tiver o flag ativo // Excluir saldos iniciais se a conta tiver o flag ativo
if ( if (
row.note === INITIAL_BALANCE_NOTE && row.note === INITIAL_BALANCE_NOTE &&
row.conta?.excludeInitialBalanceFromIncome row.financialAccount?.excludeInitialBalanceFromIncome
) { ) {
return false; return false;
} }
@@ -82,33 +87,36 @@ export async function fetchCategoryDetails(
return true; 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)), (total, transaction) => total + Math.abs(toNumber(transaction.amount)),
0, 0,
); );
const [previousTotalRow] = await db const [previousTotalRow] = await db
.select({ .select({
total: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`, total: sql<number>`coalesce(sum(${transactions.amount}), 0)`,
}) })
.from(lancamentos) .from(transactions)
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id)) .innerJoin(payers, eq(transactions.payerId, payers.id))
.leftJoin(contas, eq(lancamentos.contaId, contas.id)) .leftJoin(
financialAccounts,
eq(transactions.accountId, financialAccounts.id),
)
.where( .where(
and( and(
eq(lancamentos.userId, userId), eq(transactions.userId, userId),
eq(lancamentos.categoriaId, categoryId), eq(transactions.categoryId, categoryId),
eq(lancamentos.transactionType, transactionType), eq(transactions.transactionType, transactionType),
eq(pagadores.role, PAGADOR_ROLE_ADMIN), eq(payers.role, PAYER_ROLE_ADMIN),
sanitizedNote, sanitizedNote,
eq(lancamentos.period, previousPeriod), eq(transactions.period, previousPeriod),
// Excluir saldos iniciais se a conta tiver o flag ativo // Excluir saldos iniciais se a conta tiver o flag ativo
or( or(
ne(lancamentos.note, INITIAL_BALANCE_NOTE), ne(transactions.note, INITIAL_BALANCE_NOTE),
isNull(contas.excludeInitialBalanceFromIncome), isNull(financialAccounts.excludeInitialBalanceFromIncome),
eq(contas.excludeInitialBalanceFromIncome, false), eq(financialAccounts.excludeInitialBalanceFromIncome, false),
), ),
), ),
); );
@@ -131,6 +139,6 @@ export async function fetchCategoryDetails(
currentTotal, currentTotal,
previousTotal, previousTotal,
percentageChange, percentageChange,
transactions, transactions: transactionList,
}; };
} }

View File

@@ -1,8 +1,8 @@
import { and, eq, inArray, isNull, or, sql } from "drizzle-orm"; 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 { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/shared/lib/accounts/constants";
import { db } from "@/shared/lib/db"; 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 { CATEGORY_COLORS } from "@/shared/utils/category-colors";
import { safeToNumber as toNumber } from "@/shared/utils/number"; import { safeToNumber as toNumber } from "@/shared/utils/number";
import { import {
@@ -56,14 +56,14 @@ export async function fetchAllCategories(
): Promise<CategoryOption[]> { ): Promise<CategoryOption[]> {
const result = await db const result = await db
.select({ .select({
id: categorias.id, id: categories.id,
name: categorias.name, name: categories.name,
icon: categorias.icon, icon: categories.icon,
type: categorias.type, type: categories.type,
}) })
.from(categorias) .from(categories)
.where(eq(categorias.userId, userId)) .where(eq(categories.userId, userId))
.orderBy(categorias.type, categorias.name); .orderBy(categories.type, categories.name);
return result as CategoryOption[]; return result as CategoryOption[];
} }
@@ -88,36 +88,36 @@ export async function fetchCategoryHistory(
// Fetch monthly data for ALL categories with transactions // Fetch monthly data for ALL categories with transactions
const monthlyDataQuery = (await db const monthlyDataQuery = (await db
.select({ .select({
categoryId: categorias.id, categoryId: categories.id,
categoryName: categorias.name, categoryName: categories.name,
categoryIcon: categorias.icon, categoryIcon: categories.icon,
period: lancamentos.period, period: transactions.period,
totalAmount: sql<string>`SUM(ABS(${lancamentos.amount}))`.as( totalAmount: sql<string>`SUM(ABS(${transactions.amount}))`.as(
"total_amount", "total_amount",
), ),
}) })
.from(lancamentos) .from(transactions)
.innerJoin(categorias, eq(lancamentos.categoriaId, categorias.id)) .innerJoin(categories, eq(transactions.categoryId, categories.id))
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id)) .innerJoin(payers, eq(transactions.payerId, payers.id))
.where( .where(
and( and(
eq(lancamentos.userId, userId), eq(transactions.userId, userId),
eq(categorias.userId, userId), eq(categories.userId, userId),
inArray(lancamentos.period, periods), inArray(transactions.period, periods),
eq(pagadores.role, PAGADOR_ROLE_ADMIN), eq(payers.role, PAYER_ROLE_ADMIN),
or( or(
isNull(lancamentos.note), isNull(transactions.note),
sql`${ sql`${
lancamentos.note transactions.note
} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`, } NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`,
), ),
), ),
) )
.groupBy( .groupBy(
categorias.id, categories.id,
categorias.name, categories.name,
categorias.icon, categories.icon,
lancamentos.period, transactions.period,
)) as MonthlyCategoryRow[]; )) as MonthlyCategoryRow[];
if (monthlyDataQuery.length === 0) { if (monthlyDataQuery.length === 0) {

View File

@@ -1,5 +1,5 @@
import { and, eq, inArray, sql } from "drizzle-orm"; import { and, eq, inArray, sql } from "drizzle-orm";
import { categorias, lancamentos, orcamentos } from "@/db/schema"; import { budgets, categories, transactions } from "@/db/schema";
import { import {
buildCategoryBreakdownData, buildCategoryBreakdownData,
type DashboardCategoryBreakdownData, type DashboardCategoryBreakdownData,
@@ -8,9 +8,9 @@ import {
import { import {
buildDashboardAdminFilters, buildDashboardAdminFilters,
excludeAutoInvoiceEntries, excludeAutoInvoiceEntries,
} from "@/features/dashboard/lancamento-filters"; } from "@/features/dashboard/transaction-filters";
import { db } from "@/shared/lib/db"; 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"; import { getPreviousPeriod } from "@/shared/utils/period";
export type CategoryExpenseItem = DashboardCategoryBreakdownItem; export type CategoryExpenseItem = DashboardCategoryBreakdownItem;
@@ -22,45 +22,45 @@ export async function fetchExpensesByCategory(
): Promise<ExpensesByCategoryData> { ): Promise<ExpensesByCategoryData> {
const previousPeriod = getPreviousPeriod(period); const previousPeriod = getPreviousPeriod(period);
const adminPagadorId = await getAdminPagadorId(userId); const adminPayerId = await getAdminPayerId(userId);
if (!adminPagadorId) { if (!adminPayerId) {
return { categories: [], currentTotal: 0, previousTotal: 0 }; 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([ const [rows, budgetRows] = await Promise.all([
db db
.select({ .select({
categoryId: categorias.id, categoryId: categories.id,
categoryName: categorias.name, categoryName: categories.name,
categoryIcon: categorias.icon, categoryIcon: categories.icon,
period: lancamentos.period, period: transactions.period,
total: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`, total: sql<number>`coalesce(sum(${transactions.amount}), 0)`,
}) })
.from(lancamentos) .from(transactions)
.innerJoin(categorias, eq(lancamentos.categoriaId, categorias.id)) .innerJoin(categories, eq(transactions.categoryId, categories.id))
.where( .where(
and( and(
...buildDashboardAdminFilters({ userId, adminPagadorId }), ...buildDashboardAdminFilters({ userId, adminPayerId }),
inArray(lancamentos.period, [period, previousPeriod]), inArray(transactions.period, [period, previousPeriod]),
eq(lancamentos.transactionType, "Despesa"), eq(transactions.transactionType, "Despesa"),
eq(categorias.type, "despesa"), eq(categories.type, "despesa"),
excludeAutoInvoiceEntries(), excludeAutoInvoiceEntries(),
), ),
) )
.groupBy( .groupBy(
categorias.id, categories.id,
categorias.name, categories.name,
categorias.icon, categories.icon,
lancamentos.period, transactions.period,
), ),
db db
.select({ .select({
categoriaId: orcamentos.categoriaId, categoryId: budgets.categoryId,
amount: orcamentos.amount, amount: budgets.amount,
}) })
.from(orcamentos) .from(budgets)
.where(and(eq(orcamentos.userId, userId), eq(orcamentos.period, period))), .where(and(eq(budgets.userId, userId), eq(budgets.period, period))),
]); ]);
return buildCategoryBreakdownData({ return buildCategoryBreakdownData({

View File

@@ -1,5 +1,10 @@
import { and, eq, inArray, sql } from "drizzle-orm"; 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 { import {
buildCategoryBreakdownData, buildCategoryBreakdownData,
type DashboardCategoryBreakdownData, type DashboardCategoryBreakdownData,
@@ -9,9 +14,9 @@ import {
buildDashboardAdminFilters, buildDashboardAdminFilters,
excludeAutoInvoiceEntries, excludeAutoInvoiceEntries,
excludeInitialBalanceWhenConfigured, excludeInitialBalanceWhenConfigured,
} from "@/features/dashboard/lancamento-filters"; } from "@/features/dashboard/transaction-filters";
import { db } from "@/shared/lib/db"; 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"; import { getPreviousPeriod } from "@/shared/utils/period";
export type CategoryIncomeItem = DashboardCategoryBreakdownItem; export type CategoryIncomeItem = DashboardCategoryBreakdownItem;
@@ -23,47 +28,50 @@ export async function fetchIncomeByCategory(
): Promise<IncomeByCategoryData> { ): Promise<IncomeByCategoryData> {
const previousPeriod = getPreviousPeriod(period); const previousPeriod = getPreviousPeriod(period);
const adminPagadorId = await getAdminPagadorId(userId); const adminPayerId = await getAdminPayerId(userId);
if (!adminPagadorId) { if (!adminPayerId) {
return { categories: [], currentTotal: 0, previousTotal: 0 }; 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([ const [rows, budgetRows] = await Promise.all([
db db
.select({ .select({
categoryId: categorias.id, categoryId: categories.id,
categoryName: categorias.name, categoryName: categories.name,
categoryIcon: categorias.icon, categoryIcon: categories.icon,
period: lancamentos.period, period: transactions.period,
total: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`, total: sql<number>`coalesce(sum(${transactions.amount}), 0)`,
}) })
.from(lancamentos) .from(transactions)
.innerJoin(categorias, eq(lancamentos.categoriaId, categorias.id)) .innerJoin(categories, eq(transactions.categoryId, categories.id))
.leftJoin(contas, eq(lancamentos.contaId, contas.id)) .leftJoin(
financialAccounts,
eq(transactions.accountId, financialAccounts.id),
)
.where( .where(
and( and(
...buildDashboardAdminFilters({ userId, adminPagadorId }), ...buildDashboardAdminFilters({ userId, adminPayerId }),
inArray(lancamentos.period, [period, previousPeriod]), inArray(transactions.period, [period, previousPeriod]),
eq(lancamentos.transactionType, "Receita"), eq(transactions.transactionType, "Receita"),
eq(categorias.type, "receita"), eq(categories.type, "receita"),
excludeAutoInvoiceEntries(), excludeAutoInvoiceEntries(),
excludeInitialBalanceWhenConfigured(), excludeInitialBalanceWhenConfigured(),
), ),
) )
.groupBy( .groupBy(
categorias.id, categories.id,
categorias.name, categories.name,
categorias.icon, categories.icon,
lancamentos.period, transactions.period,
), ),
db db
.select({ .select({
categoriaId: orcamentos.categoriaId, categoryId: budgets.categoryId,
amount: orcamentos.amount, amount: budgets.amount,
}) })
.from(orcamentos) .from(budgets)
.where(and(eq(orcamentos.userId, userId), eq(orcamentos.period, period))), .where(and(eq(budgets.userId, userId), eq(budgets.period, period))),
]); ]);
return buildCategoryBreakdownData({ return buildCategoryBreakdownData({

View File

@@ -38,7 +38,7 @@ import {
widgetsConfig, widgetsConfig,
} from "@/features/dashboard/widgets/widgets-config"; } from "@/features/dashboard/widgets/widgets-config";
import { NoteDialog } from "@/features/notes/components/note-dialog"; 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 type { SelectOption } from "@/features/transactions/components/types";
import { ExpandableWidgetCard } from "@/shared/components/expandable-widget-card"; import { ExpandableWidgetCard } from "@/shared/components/expandable-widget-card";
import { Button } from "@/shared/components/ui/button"; import { Button } from "@/shared/components/ui/button";
@@ -48,12 +48,12 @@ type DashboardGridEditableProps = {
period: string; period: string;
initialPreferences: WidgetPreferences | null; initialPreferences: WidgetPreferences | null;
quickActionOptions: { quickActionOptions: {
pagadorOptions: SelectOption[]; payerOptions: SelectOption[];
splitPagadorOptions: SelectOption[]; splitPayerOptions: SelectOption[];
defaultPagadorId: string | null; defaultPayerId: string | null;
contaOptions: SelectOption[]; accountOptions: SelectOption[];
cartaoOptions: SelectOption[]; cardOptions: SelectOption[];
categoriaOptions: SelectOption[]; categoryOptions: SelectOption[];
estabelecimentos: string[]; estabelecimentos: string[];
}; };
}; };
@@ -203,14 +203,14 @@ export function DashboardGridEditable({
Ações rápidas Ações rápidas
</span> </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"> <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" mode="create"
pagadorOptions={quickActionOptions.pagadorOptions} payerOptions={quickActionOptions.payerOptions}
splitPagadorOptions={quickActionOptions.splitPagadorOptions} splitPayerOptions={quickActionOptions.splitPayerOptions}
defaultPagadorId={quickActionOptions.defaultPagadorId} defaultPayerId={quickActionOptions.defaultPayerId}
contaOptions={quickActionOptions.contaOptions} accountOptions={quickActionOptions.accountOptions}
cartaoOptions={quickActionOptions.cartaoOptions} cardOptions={quickActionOptions.cardOptions}
categoriaOptions={quickActionOptions.categoriaOptions} categoryOptions={quickActionOptions.categoryOptions}
estabelecimentos={quickActionOptions.estabelecimentos} estabelecimentos={quickActionOptions.estabelecimentos}
defaultPeriod={period} defaultPeriod={period}
defaultTransactionType="Receita" defaultTransactionType="Receita"
@@ -228,14 +228,14 @@ export function DashboardGridEditable({
</Button> </Button>
} }
/> />
<LancamentoDialog <TransactionDialog
mode="create" mode="create"
pagadorOptions={quickActionOptions.pagadorOptions} payerOptions={quickActionOptions.payerOptions}
splitPagadorOptions={quickActionOptions.splitPagadorOptions} splitPayerOptions={quickActionOptions.splitPayerOptions}
defaultPagadorId={quickActionOptions.defaultPagadorId} defaultPayerId={quickActionOptions.defaultPayerId}
contaOptions={quickActionOptions.contaOptions} accountOptions={quickActionOptions.accountOptions}
cartaoOptions={quickActionOptions.cartaoOptions} cardOptions={quickActionOptions.cardOptions}
categoriaOptions={quickActionOptions.categoriaOptions} categoryOptions={quickActionOptions.categoryOptions}
estabelecimentos={quickActionOptions.estabelecimentos} estabelecimentos={quickActionOptions.estabelecimentos}
defaultPeriod={period} defaultPeriod={period}
defaultTransactionType="Despesa" defaultTransactionType="Despesa"

View File

@@ -76,7 +76,7 @@ export function InvoiceListItem({ invoice, onPay }: InvoiceListItemProps) {
{breakdown.map((share, index) => ( {breakdown.map((share, index) => (
<li <li
key={`${invoice.id}-${ key={`${invoice.id}-${
share.pagadorId ?? share.pagadorName ?? index share.payerId ?? share.pagadorName ?? index
}`} }`}
className="flex items-center gap-3" 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"> <div className="mb-2 flex items-center gap-2 text-muted-foreground">
<RiMoneyDollarCircleLine className="size-4" /> <RiMoneyDollarCircleLine className="size-4" />
<span className="text-xs font-semibold uppercase"> <span className="text-xs font-semibold uppercase">
Valor da Fatura Valor da Invoice
</span> </span>
</div> </div>
<MoneyValues <MoneyValues

View File

@@ -55,12 +55,14 @@ export function MyAccountsWidget({
> >
<div className="flex min-w-0 flex-1 items-center gap-3"> <div className="flex min-w-0 flex-1 items-center gap-3">
<div className="relative size-10 overflow-hidden"> <div className="relative size-10 overflow-hidden">
<Image {logoSrc ? (
src={logoSrc} <Image
alt={`Logo da conta ${account.name}`} src={logoSrc}
fill alt={`Logo da conta ${account.name}`}
className="object-contain rounded-full" fill
/> className="object-contain rounded-full"
/>
) : null}
</div> </div>
<div className="min-w-0"> <div className="min-w-0">

View File

@@ -21,7 +21,7 @@ import { getAvatarSrc } from "@/shared/lib/payers/utils";
import { formatPercentage } from "@/shared/utils/percentage"; import { formatPercentage } from "@/shared/utils/percentage";
type PayersWidgetProps = { type PayersWidgetProps = {
pagadores: DashboardPagador[]; payers: DashboardPagador[];
}; };
const buildInitials = (value: string) => { const buildInitials = (value: string) => {
@@ -38,10 +38,10 @@ const buildInitials = (value: string) => {
return `${firstChar}${secondChar}`.toUpperCase() || "??"; return `${firstChar}${secondChar}`.toUpperCase() || "??";
}; };
export function PayersWidget({ pagadores }: PayersWidgetProps) { export function PayersWidget({ payers }: PayersWidgetProps) {
return ( return (
<CardContent className="flex flex-col gap-4 px-0"> <CardContent className="flex flex-col gap-4 px-0">
{pagadores.length === 0 ? ( {payers.length === 0 ? (
<WidgetEmptyState <WidgetEmptyState
icon={<RiGroupLine className="size-6 text-muted-foreground" />} icon={<RiGroupLine className="size-6 text-muted-foreground" />}
title="Nenhum pagador para o período" title="Nenhum pagador para o período"
@@ -49,25 +49,25 @@ export function PayersWidget({ pagadores }: PayersWidgetProps) {
/> />
) : ( ) : (
<ul className="flex flex-col"> <ul className="flex flex-col">
{pagadores.map((pagador) => { {payers.map((payer) => {
const initials = buildInitials(pagador.name); const initials = buildInitials(payer.name);
const hasValidPercentageChange = const hasValidPercentageChange =
typeof pagador.percentageChange === "number" && typeof payer.percentageChange === "number" &&
Number.isFinite(pagador.percentageChange); Number.isFinite(payer.percentageChange);
const percentageChange = hasValidPercentageChange const percentageChange = hasValidPercentageChange
? pagador.percentageChange ? payer.percentageChange
: null; : null;
return ( return (
<li <li
key={pagador.id} key={payer.id}
className="flex items-center justify-between border-b border-dashed last:border-b-0 last:pb-0" 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"> <div className="flex min-w-0 flex-1 items-center gap-2 py-2">
<Avatar className="size-10 shrink-0"> <Avatar className="size-10 shrink-0">
<AvatarImage <AvatarImage
src={getAvatarSrc(pagador.avatarUrl)} src={getAvatarSrc(payer.avatarUrl)}
alt={`Avatar de ${pagador.name}`} alt={`Avatar de ${payer.name}`}
/> />
<AvatarFallback>{initials}</AvatarFallback> <AvatarFallback>{initials}</AvatarFallback>
</Avatar> </Avatar>
@@ -75,13 +75,11 @@ export function PayersWidget({ pagadores }: PayersWidgetProps) {
<div className="min-w-0"> <div className="min-w-0">
<Link <Link
prefetch 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" 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"> <span className="truncate font-medium">{payer.name}</span>
{pagador.name} {payer.isAdmin && (
</span>
{pagador.isAdmin && (
<RiVerifiedBadgeFill <RiVerifiedBadgeFill
className="size-4 shrink-0 text-blue-500" className="size-4 shrink-0 text-blue-500"
aria-hidden aria-hidden
@@ -93,13 +91,13 @@ export function PayersWidget({ pagadores }: PayersWidgetProps) {
/> />
</Link> </Link>
<p className="truncate text-xs text-muted-foreground"> <p className="truncate text-xs text-muted-foreground">
{pagador.email ?? "Sem email cadastrado"} {payer.email ?? "Sem email cadastrado"}
</p> </p>
</div> </div>
</div> </div>
<div className="flex shrink-0 flex-col items-end"> <div className="flex shrink-0 flex-col items-end">
<MoneyValues amount={pagador.totalExpenses} /> <MoneyValues amount={payer.totalExpenses} />
{percentageChange !== null && ( {percentageChange !== null && (
<span <span
className={`flex items-center gap-0.5 text-xs ${ 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 { and, asc, eq, gte, lte, ne, sum } from "drizzle-orm";
import { contas, lancamentos } from "@/db/schema"; import { financialAccounts, transactions } from "@/db/schema";
import { import {
buildDashboardAdminFilters, buildDashboardAdminFilters,
excludeAutoInvoiceEntries, excludeAutoInvoiceEntries,
excludeInitialBalanceWhenConfigured, excludeInitialBalanceWhenConfigured,
} from "@/features/dashboard/lancamento-filters"; } from "@/features/dashboard/transaction-filters";
import { db } from "@/shared/lib/db"; 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 { safeToNumber } from "@/shared/utils/number";
import { import {
addMonthsToPeriod, addMonthsToPeriod,
@@ -71,8 +71,8 @@ export async function fetchDashboardCardMetrics(
): Promise<DashboardCardMetrics> { ): Promise<DashboardCardMetrics> {
const previousPeriod = getPreviousPeriod(period); const previousPeriod = getPreviousPeriod(period);
const adminPagadorId = await getAdminPagadorId(userId); const adminPayerId = await getAdminPayerId(userId);
if (!adminPagadorId) { if (!adminPayerId) {
return { return {
period, period,
previousPeriod, previousPeriod,
@@ -88,24 +88,27 @@ export async function fetchDashboardCardMetrics(
const rows = await db const rows = await db
.select({ .select({
period: lancamentos.period, period: transactions.period,
transactionType: lancamentos.transactionType, transactionType: transactions.transactionType,
totalAmount: sum(lancamentos.amount).as("total"), totalAmount: sum(transactions.amount).as("total"),
}) })
.from(lancamentos) .from(transactions)
.leftJoin(contas, eq(lancamentos.contaId, contas.id)) .leftJoin(
financialAccounts,
eq(transactions.accountId, financialAccounts.id),
)
.where( .where(
and( and(
...buildDashboardAdminFilters({ userId, adminPagadorId }), ...buildDashboardAdminFilters({ userId, adminPayerId }),
gte(lancamentos.period, startPeriod), gte(transactions.period, startPeriod),
lte(lancamentos.period, period), lte(transactions.period, period),
ne(lancamentos.transactionType, TRANSFERENCIA), ne(transactions.transactionType, TRANSFERENCIA),
excludeAutoInvoiceEntries(), excludeAutoInvoiceEntries(),
excludeInitialBalanceWhenConfigured(), excludeInitialBalanceWhenConfigured(),
), ),
) )
.groupBy(lancamentos.period, lancamentos.transactionType) .groupBy(transactions.period, transactions.transactionType)
.orderBy(asc(lancamentos.period), asc(lancamentos.transactionType)); .orderBy(asc(transactions.period), asc(transactions.transactionType));
const periodTotals = new Map<string, PeriodTotals>(); const periodTotals = new Map<string, PeriodTotals>();

View File

@@ -1,11 +1,11 @@
import { and, eq, isNotNull, isNull, or, sql } from "drizzle-orm"; 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 { import {
ACCOUNT_AUTO_INVOICE_NOTE_PREFIX, ACCOUNT_AUTO_INVOICE_NOTE_PREFIX,
INITIAL_BALANCE_NOTE, INITIAL_BALANCE_NOTE,
} from "@/shared/lib/accounts/constants"; } from "@/shared/lib/accounts/constants";
import { db } from "@/shared/lib/db"; 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 { import {
buildDateOnlyStringFromPeriodDay, buildDateOnlyStringFromPeriodDay,
parseLocalDateString, parseLocalDateString,
@@ -46,7 +46,7 @@ export type InstallmentGroup = {
seriesId: string; seriesId: string;
name: string; name: string;
paymentMethod: string; paymentMethod: string;
cartaoId: string | null; cardId: string | null;
cartaoName: string | null; cartaoName: string | null;
cartaoDueDay: string | null; cartaoDueDay: string | null;
cartaoLogo: 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 // 1. Buscar todos os lançamentos parcelados não antecipados do pagador admin
const installmentRows = await db const installmentRows = await db
.select({ .select({
id: lancamentos.id, id: transactions.id,
seriesId: lancamentos.seriesId, seriesId: transactions.seriesId,
name: lancamentos.name, name: transactions.name,
amount: lancamentos.amount, amount: transactions.amount,
paymentMethod: lancamentos.paymentMethod, paymentMethod: transactions.paymentMethod,
currentInstallment: lancamentos.currentInstallment, currentInstallment: transactions.currentInstallment,
installmentCount: lancamentos.installmentCount, installmentCount: transactions.installmentCount,
dueDate: lancamentos.dueDate, dueDate: transactions.dueDate,
period: lancamentos.period, period: transactions.period,
isAnticipated: lancamentos.isAnticipated, isAnticipated: transactions.isAnticipated,
isSettled: lancamentos.isSettled, isSettled: transactions.isSettled,
purchaseDate: lancamentos.purchaseDate, purchaseDate: transactions.purchaseDate,
cartaoId: lancamentos.cartaoId, cardId: transactions.cardId,
cartaoName: cartoes.name, cartaoName: cards.name,
cartaoDueDay: cartoes.dueDay, cartaoDueDay: cards.dueDay,
cartaoLogo: cartoes.logo, cartaoLogo: cards.logo,
}) })
.from(lancamentos) .from(transactions)
.leftJoin(cartoes, eq(lancamentos.cartaoId, cartoes.id)) .leftJoin(cards, eq(transactions.cardId, cards.id))
.leftJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id)) .leftJoin(payers, eq(transactions.payerId, payers.id))
.where( .where(
and( and(
eq(lancamentos.userId, userId), eq(transactions.userId, userId),
eq(lancamentos.transactionType, "Despesa"), eq(transactions.transactionType, "Despesa"),
eq(lancamentos.condition, "Parcelado"), eq(transactions.condition, "Parcelado"),
eq(lancamentos.isAnticipated, false), eq(transactions.isAnticipated, false),
isNotNull(lancamentos.seriesId), isNotNull(transactions.seriesId),
eq(pagadores.role, PAGADOR_ROLE_ADMIN), eq(payers.role, PAYER_ROLE_ADMIN),
or( or(
isNull(lancamentos.note), isNull(transactions.note),
and( and(
sql`${lancamentos.note} != ${INITIAL_BALANCE_NOTE}`, sql`${transactions.note} != ${INITIAL_BALANCE_NOTE}`,
sql`${lancamentos.note} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`, sql`${transactions.note} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`,
), ),
), ),
), ),
) )
.orderBy(lancamentos.purchaseDate, lancamentos.currentInstallment); .orderBy(transactions.purchaseDate, transactions.currentInstallment);
// Agrupar por seriesId // Agrupar por seriesId
const seriesMap = new Map<string, InstallmentGroup>(); const seriesMap = new Map<string, InstallmentGroup>();
@@ -140,7 +140,7 @@ export async function fetchInstallmentAnalysis(
seriesId: row.seriesId, seriesId: row.seriesId,
name: row.name, name: row.name,
paymentMethod: row.paymentMethod, paymentMethod: row.paymentMethod,
cartaoId: row.cartaoId, cardId: row.cardId,
cartaoName: row.cartaoName, cartaoName: row.cartaoName,
cartaoDueDay: row.cartaoDueDay, cartaoDueDay: row.cartaoDueDay,
cartaoLogo: row.cartaoLogo, cartaoLogo: row.cartaoLogo,

View File

@@ -1,11 +1,11 @@
import { and, desc, eq, isNull, or, sql } from "drizzle-orm"; import { and, desc, eq, isNull, or, sql } from "drizzle-orm";
import { lancamentos } from "@/db/schema"; import { transactions } from "@/db/schema";
import { import {
ACCOUNT_AUTO_INVOICE_NOTE_PREFIX, ACCOUNT_AUTO_INVOICE_NOTE_PREFIX,
INITIAL_BALANCE_NOTE, INITIAL_BALANCE_NOTE,
} from "@/shared/lib/accounts/constants"; } from "@/shared/lib/accounts/constants";
import { db } from "@/shared/lib/db"; 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 { safeToNumber as toNumber } from "@/shared/utils/number";
export type InstallmentExpense = { export type InstallmentExpense = {
@@ -28,42 +28,42 @@ export async function fetchInstallmentExpenses(
userId: string, userId: string,
period: string, period: string,
): Promise<InstallmentExpensesData> { ): Promise<InstallmentExpensesData> {
const adminPagadorId = await getAdminPagadorId(userId); const adminPayerId = await getAdminPayerId(userId);
if (!adminPagadorId) { if (!adminPayerId) {
return { expenses: [] }; return { expenses: [] };
} }
const rows = await db const rows = await db
.select({ .select({
id: lancamentos.id, id: transactions.id,
name: lancamentos.name, name: transactions.name,
amount: lancamentos.amount, amount: transactions.amount,
paymentMethod: lancamentos.paymentMethod, paymentMethod: transactions.paymentMethod,
currentInstallment: lancamentos.currentInstallment, currentInstallment: transactions.currentInstallment,
installmentCount: lancamentos.installmentCount, installmentCount: transactions.installmentCount,
dueDate: lancamentos.dueDate, dueDate: transactions.dueDate,
purchaseDate: lancamentos.purchaseDate, purchaseDate: transactions.purchaseDate,
period: lancamentos.period, period: transactions.period,
}) })
.from(lancamentos) .from(transactions)
.where( .where(
and( and(
eq(lancamentos.userId, userId), eq(transactions.userId, userId),
eq(lancamentos.period, period), eq(transactions.period, period),
eq(lancamentos.transactionType, "Despesa"), eq(transactions.transactionType, "Despesa"),
eq(lancamentos.condition, "Parcelado"), eq(transactions.condition, "Parcelado"),
eq(lancamentos.isAnticipated, false), eq(transactions.isAnticipated, false),
eq(lancamentos.pagadorId, adminPagadorId), eq(transactions.payerId, adminPayerId),
or( or(
isNull(lancamentos.note), isNull(transactions.note),
and( and(
sql`${lancamentos.note} != ${INITIAL_BALANCE_NOTE}`, sql`${transactions.note} != ${INITIAL_BALANCE_NOTE}`,
sql`${lancamentos.note} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`, 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]; type InstallmentExpenseRow = (typeof rows)[number];

View File

@@ -1,11 +1,11 @@
import { and, desc, eq, isNull, or, sql } from "drizzle-orm"; import { and, desc, eq, isNull, or, sql } from "drizzle-orm";
import { lancamentos } from "@/db/schema"; import { transactions } from "@/db/schema";
import { import {
ACCOUNT_AUTO_INVOICE_NOTE_PREFIX, ACCOUNT_AUTO_INVOICE_NOTE_PREFIX,
INITIAL_BALANCE_NOTE, INITIAL_BALANCE_NOTE,
} from "@/shared/lib/accounts/constants"; } from "@/shared/lib/accounts/constants";
import { db } from "@/shared/lib/db"; 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 { safeToNumber as toNumber } from "@/shared/utils/number";
export type RecurringExpense = { export type RecurringExpense = {
@@ -24,37 +24,37 @@ export async function fetchRecurringExpenses(
userId: string, userId: string,
period: string, period: string,
): Promise<RecurringExpensesData> { ): Promise<RecurringExpensesData> {
const adminPagadorId = await getAdminPagadorId(userId); const adminPayerId = await getAdminPayerId(userId);
if (!adminPagadorId) { if (!adminPayerId) {
return { expenses: [] }; return { expenses: [] };
} }
const results = await db const results = await db
.select({ .select({
id: lancamentos.id, id: transactions.id,
name: lancamentos.name, name: transactions.name,
amount: lancamentos.amount, amount: transactions.amount,
paymentMethod: lancamentos.paymentMethod, paymentMethod: transactions.paymentMethod,
recurrenceCount: lancamentos.recurrenceCount, recurrenceCount: transactions.recurrenceCount,
}) })
.from(lancamentos) .from(transactions)
.where( .where(
and( and(
eq(lancamentos.userId, userId), eq(transactions.userId, userId),
eq(lancamentos.period, period), eq(transactions.period, period),
eq(lancamentos.transactionType, "Despesa"), eq(transactions.transactionType, "Despesa"),
eq(lancamentos.condition, "Recorrente"), eq(transactions.condition, "Recorrente"),
eq(lancamentos.pagadorId, adminPagadorId), eq(transactions.payerId, adminPayerId),
or( or(
isNull(lancamentos.note), isNull(transactions.note),
and( and(
sql`${lancamentos.note} != ${INITIAL_BALANCE_NOTE}`, sql`${transactions.note} != ${INITIAL_BALANCE_NOTE}`,
sql`${lancamentos.note} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`, 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( const expenses = results.map(
(row): RecurringExpense => ({ (row): RecurringExpense => ({

View File

@@ -1,11 +1,11 @@
import { and, asc, eq } from "drizzle-orm"; import { and, asc, eq } from "drizzle-orm";
import { cartoes, contas, lancamentos } from "@/db/schema"; import { cards, financialAccounts, transactions } from "@/db/schema";
import { import {
buildDashboardAdminPeriodFilters, buildDashboardAdminPeriodFilters,
excludeAutoGeneratedEntryNotes, excludeAutoGeneratedEntryNotes,
} from "@/features/dashboard/lancamento-filters"; } from "@/features/dashboard/transaction-filters";
import { db } from "@/shared/lib/db"; 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 { safeToNumber as toNumber } from "@/shared/utils/number";
export type TopExpense = { export type TopExpense = {
@@ -26,8 +26,8 @@ export async function fetchTopExpenses(
period: string, period: string,
cardOnly: boolean = false, cardOnly: boolean = false,
): Promise<TopExpensesData> { ): Promise<TopExpensesData> {
const adminPagadorId = await getAdminPagadorId(userId); const adminPayerId = await getAdminPayerId(userId);
if (!adminPagadorId) { if (!adminPayerId) {
return { expenses: [] }; return { expenses: [] };
} }
@@ -35,34 +35,37 @@ export async function fetchTopExpenses(
...buildDashboardAdminPeriodFilters({ ...buildDashboardAdminPeriodFilters({
userId, userId,
period, period,
adminPagadorId, adminPayerId,
}), }),
eq(lancamentos.transactionType, "Despesa"), eq(transactions.transactionType, "Despesa"),
excludeAutoGeneratedEntryNotes(), excludeAutoGeneratedEntryNotes(),
]; ];
// Se cardOnly for true, filtra apenas pagamentos com cartão // Se cardOnly for true, filtra apenas pagamentos com cartão
if (cardOnly) { 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 const results = await db
.select({ .select({
id: lancamentos.id, id: transactions.id,
name: lancamentos.name, name: transactions.name,
amount: lancamentos.amount, amount: transactions.amount,
purchaseDate: lancamentos.purchaseDate, purchaseDate: transactions.purchaseDate,
paymentMethod: lancamentos.paymentMethod, paymentMethod: transactions.paymentMethod,
cartaoId: lancamentos.cartaoId, cardId: transactions.cardId,
contaId: lancamentos.contaId, accountId: transactions.accountId,
cardLogo: cartoes.logo, cardLogo: cards.logo,
accountLogo: contas.logo, accountLogo: financialAccounts.logo,
}) })
.from(lancamentos) .from(transactions)
.leftJoin(cartoes, eq(lancamentos.cartaoId, cartoes.id)) .leftJoin(cards, eq(transactions.cardId, cards.id))
.leftJoin(contas, eq(lancamentos.contaId, contas.id)) .leftJoin(
financialAccounts,
eq(transactions.accountId, financialAccounts.id),
)
.where(and(...conditions)) .where(and(...conditions))
.orderBy(asc(lancamentos.amount)) .orderBy(asc(transactions.amount))
.limit(10); .limit(10);
const expenses = results.map( const expenses = results.map(

View File

@@ -11,7 +11,7 @@ import { fetchGoalsProgressData } from "./goals-progress-queries";
import { fetchIncomeExpenseBalance } from "./income-expense-balance-queries"; import { fetchIncomeExpenseBalance } from "./income-expense-balance-queries";
import { fetchDashboardInvoices } from "./invoices-queries"; import { fetchDashboardInvoices } from "./invoices-queries";
import { fetchDashboardNotes } from "./notes-queries"; import { fetchDashboardNotes } from "./notes-queries";
import { fetchDashboardPagadores } from "./payers-queries"; import { fetchDashboardPayers } from "./payers-queries";
import { fetchPaymentConditions } from "./payments/payment-conditions-queries"; import { fetchPaymentConditions } from "./payments/payment-conditions-queries";
import { fetchPaymentMethods } from "./payments/payment-methods-queries"; import { fetchPaymentMethods } from "./payments/payment-methods-queries";
import { fetchPaymentStatus } from "./payments/payment-status-queries"; import { fetchPaymentStatus } from "./payments/payment-status-queries";
@@ -49,7 +49,7 @@ async function fetchDashboardDataInternal(userId: string, period: string) {
fetchGoalsProgressData(userId, period), fetchGoalsProgressData(userId, period),
fetchPaymentStatus(userId, period), fetchPaymentStatus(userId, period),
fetchIncomeExpenseBalance(userId, period), fetchIncomeExpenseBalance(userId, period),
fetchDashboardPagadores(userId, period), fetchDashboardPayers(userId, period),
fetchDashboardNotes(userId), fetchDashboardNotes(userId),
fetchPaymentConditions(userId, period), fetchPaymentConditions(userId, period),
fetchPaymentMethods(userId, period), fetchPaymentMethods(userId, period),

View File

@@ -1,7 +1,7 @@
import { and, eq, ne, sql } from "drizzle-orm"; 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 { 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 { safeToNumber as toNumber } from "@/shared/utils/number";
const BUDGET_CRITICAL_THRESHOLD = 80; const BUDGET_CRITICAL_THRESHOLD = 80;
@@ -49,9 +49,9 @@ export async function fetchGoalsProgressData(
userId: string, userId: string,
period: string, period: string,
): Promise<GoalsProgressData> { ): Promise<GoalsProgressData> {
const adminPagadorId = await getAdminPagadorId(userId); const adminPayerId = await getAdminPayerId(userId);
if (!adminPagadorId) { if (!adminPayerId) {
return { return {
items: [], items: [],
categories: [], categories: [],
@@ -64,45 +64,45 @@ export async function fetchGoalsProgressData(
const [rows, categoryRows] = await Promise.all([ const [rows, categoryRows] = await Promise.all([
db db
.select({ .select({
orcamentoId: orcamentos.id, orcamentoId: budgets.id,
categoryId: categorias.id, categoryId: categories.id,
categoryName: categorias.name, categoryName: categories.name,
categoryIcon: categorias.icon, categoryIcon: categories.icon,
period: orcamentos.period, period: budgets.period,
createdAt: orcamentos.createdAt, createdAt: budgets.createdAt,
budgetAmount: orcamentos.amount, budgetAmount: budgets.amount,
spentAmount: sql<number>`COALESCE(SUM(ABS(${lancamentos.amount})), 0)`, spentAmount: sql<number>`COALESCE(SUM(ABS(${transactions.amount})), 0)`,
}) })
.from(orcamentos) .from(budgets)
.innerJoin(categorias, eq(orcamentos.categoriaId, categorias.id)) .innerJoin(categories, eq(budgets.categoryId, categories.id))
.leftJoin( .leftJoin(
lancamentos, transactions,
and( and(
eq(lancamentos.categoriaId, orcamentos.categoriaId), eq(transactions.categoryId, budgets.categoryId),
eq(lancamentos.userId, orcamentos.userId), eq(transactions.userId, budgets.userId),
eq(lancamentos.period, orcamentos.period), eq(transactions.period, budgets.period),
eq(lancamentos.pagadorId, adminPagadorId), eq(transactions.payerId, adminPayerId),
eq(lancamentos.transactionType, "Despesa"), eq(transactions.transactionType, "Despesa"),
ne(lancamentos.condition, "cancelado"), 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( .groupBy(
orcamentos.id, budgets.id,
categorias.id, categories.id,
categorias.name, categories.name,
categorias.icon, categories.icon,
orcamentos.period, budgets.period,
orcamentos.createdAt, budgets.createdAt,
orcamentos.amount, budgets.amount,
), ),
db.query.categorias.findMany({ db.query.categories.findMany({
where: and(eq(categorias.userId, userId), eq(categorias.type, "despesa")), where: and(eq(categories.userId, userId), eq(categories.type, "despesa")),
orderBy: (category, { asc }) => [asc(category.name)], orderBy: (category, { asc }) => [asc(category.name)],
}), }),
]); ]);
const categories: GoalProgressCategory[] = categoryRows.map((category) => ({ const categoryList: GoalProgressCategory[] = categoryRows.map((category) => ({
id: category.id, id: category.id,
name: category.name, name: category.name,
icon: category.icon, icon: category.icon,
@@ -139,7 +139,7 @@ export async function fetchGoalsProgressData(
return { return {
items, items,
categories, categories: categoryList,
totalBudgets: items.length, totalBudgets: items.length,
exceededCount, exceededCount,
criticalCount, criticalCount,

View File

@@ -1,12 +1,12 @@
import { and, eq, inArray, sql } from "drizzle-orm"; import { and, eq, inArray, sql } from "drizzle-orm";
import { contas, lancamentos } from "@/db/schema"; import { financialAccounts, transactions } from "@/db/schema";
import { import {
buildDashboardAdminFilters, buildDashboardAdminFilters,
excludeAutoInvoiceEntries, excludeAutoInvoiceEntries,
excludeInitialBalanceWhenConfigured, excludeInitialBalanceWhenConfigured,
} from "@/features/dashboard/lancamento-filters"; } from "@/features/dashboard/transaction-filters";
import { db } from "@/shared/lib/db"; 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 { safeToNumber as toNumber } from "@/shared/utils/number";
import { import {
buildPeriodWindow, buildPeriodWindow,
@@ -38,8 +38,8 @@ export async function fetchIncomeExpenseBalance(
userId: string, userId: string,
currentPeriod: string, currentPeriod: string,
): Promise<IncomeExpenseBalanceData> { ): Promise<IncomeExpenseBalanceData> {
const adminPagadorId = await getAdminPagadorId(userId); const adminPayerId = await getAdminPayerId(userId);
if (!adminPagadorId) { if (!adminPayerId) {
return { months: [] }; return { months: [] };
} }
@@ -48,22 +48,25 @@ export async function fetchIncomeExpenseBalance(
// Single query: GROUP BY period + transactionType instead of 12 separate queries // Single query: GROUP BY period + transactionType instead of 12 separate queries
const rows = await db const rows = await db
.select({ .select({
period: lancamentos.period, period: transactions.period,
transactionType: lancamentos.transactionType, transactionType: transactions.transactionType,
total: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`, total: sql<number>`coalesce(sum(${transactions.amount}), 0)`,
}) })
.from(lancamentos) .from(transactions)
.leftJoin(contas, eq(lancamentos.contaId, contas.id)) .leftJoin(
financialAccounts,
eq(transactions.accountId, financialAccounts.id),
)
.where( .where(
and( and(
...buildDashboardAdminFilters({ userId, adminPagadorId }), ...buildDashboardAdminFilters({ userId, adminPayerId }),
inArray(lancamentos.period, periods), inArray(transactions.period, periods),
inArray(lancamentos.transactionType, ["Receita", "Despesa"]), inArray(transactions.transactionType, ["Receita", "Despesa"]),
excludeAutoInvoiceEntries(), excludeAutoInvoiceEntries(),
excludeInitialBalanceWhenConfigured(), excludeInitialBalanceWhenConfigured(),
), ),
) )
.groupBy(lancamentos.period, lancamentos.transactionType); .groupBy(transactions.period, transactions.transactionType);
// Build lookup from query results // Build lookup from query results
const dataMap = new Map<string, { income: number; expense: number }>(); 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 { 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 { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/shared/lib/accounts/constants";
import { db } from "@/shared/lib/db"; import { db } from "@/shared/lib/db";
import { import {
@@ -28,14 +28,14 @@ type RawDashboardInvoice = {
type RawInvoiceBreakdownRow = { type RawInvoiceBreakdownRow = {
cardId: string | null; cardId: string | null;
period: string | null; period: string | null;
pagadorId: string | null; payerId: string | null;
pagadorName: string | null; pagadorName: string | null;
pagadorAvatar: string | null; pagadorAvatar: string | null;
amount: number | string | null; amount: number | string | null;
}; };
export type InvoicePagadorBreakdown = { export type InvoicePagadorBreakdown = {
pagadorId: string | null; payerId: string | null;
pagadorName: string; pagadorName: string;
pagadorAvatar: string | null; pagadorAvatar: string | null;
amount: number; amount: number;
@@ -74,15 +74,15 @@ export async function fetchDashboardInvoices(
): Promise<DashboardInvoicesSnapshot> { ): Promise<DashboardInvoicesSnapshot> {
const paymentRows = await db const paymentRows = await db
.select({ .select({
note: lancamentos.note, note: transactions.note,
purchaseDate: lancamentos.purchaseDate, purchaseDate: transactions.purchaseDate,
createdAt: lancamentos.createdAt, createdAt: transactions.createdAt,
}) })
.from(lancamentos) .from(transactions)
.where( .where(
and( and(
eq(lancamentos.userId, userId), eq(transactions.userId, userId),
ilike(lancamentos.note, `${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`), ilike(transactions.note, `${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`),
), ),
); );
@@ -117,80 +117,77 @@ export async function fetchDashboardInvoices(
} }
} }
const [rows, breakdownRows]: [ const [rows, breakdownRows] = (await Promise.all([
RawDashboardInvoice[],
RawInvoiceBreakdownRow[],
] = await Promise.all([
db db
.select({ .select({
invoiceId: faturas.id, invoiceId: invoices.id,
cardId: cartoes.id, cardId: cards.id,
cardName: cartoes.name, cardName: cards.name,
logo: cartoes.logo, logo: cards.logo,
dueDay: cartoes.dueDay, dueDay: cards.dueDay,
period: faturas.period, period: invoices.period,
paymentStatus: faturas.paymentStatus, paymentStatus: invoices.paymentStatus,
invoiceCreatedAt: faturas.createdAt, invoiceCreatedAt: invoices.createdAt,
totalAmount: sql<number | null>` 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( .leftJoin(
faturas, invoices,
and( and(
eq(faturas.cartaoId, cartoes.id), eq(invoices.cardId, cards.id),
eq(faturas.userId, userId), eq(invoices.userId, userId),
eq(faturas.period, period), eq(invoices.period, period),
), ),
) )
.leftJoin( .leftJoin(
lancamentos, transactions,
and( and(
eq(lancamentos.cartaoId, cartoes.id), eq(transactions.cardId, cards.id),
eq(lancamentos.userId, userId), eq(transactions.userId, userId),
eq(lancamentos.period, period), eq(transactions.period, period),
), ),
) )
.where(eq(cartoes.userId, userId)) .where(eq(cards.userId, userId))
.groupBy( .groupBy(
faturas.id, invoices.id,
cartoes.id, cards.id,
cartoes.name, cards.name,
cartoes.brand, cards.brand,
cartoes.status, cards.status,
cartoes.logo, cards.logo,
cartoes.dueDay, cards.dueDay,
faturas.period, invoices.period,
faturas.paymentStatus, invoices.paymentStatus,
), ),
db db
.select({ .select({
cardId: lancamentos.cartaoId, cardId: transactions.cardId,
period: lancamentos.period, period: transactions.period,
pagadorId: lancamentos.pagadorId, payerId: transactions.payerId,
pagadorName: pagadores.name, pagadorName: payers.name,
pagadorAvatar: pagadores.avatarUrl, pagadorAvatar: payers.avatarUrl,
amount: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`, amount: sql<number>`coalesce(sum(${transactions.amount}), 0)`,
}) })
.from(lancamentos) .from(transactions)
.leftJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id)) .leftJoin(payers, eq(transactions.payerId, payers.id))
.where( .where(
and( and(
eq(lancamentos.userId, userId), eq(transactions.userId, userId),
eq(lancamentos.period, period), eq(transactions.period, period),
isNotNull(lancamentos.cartaoId), isNotNull(transactions.cardId),
), ),
) )
.groupBy( .groupBy(
lancamentos.cartaoId, transactions.cardId,
lancamentos.period, transactions.period,
lancamentos.pagadorId, transactions.payerId,
pagadores.name, payers.name,
pagadores.avatarUrl, payers.avatarUrl,
), ),
]); ])) as [RawDashboardInvoice[], RawInvoiceBreakdownRow[]];
const breakdownMap = new Map<string, InvoicePagadorBreakdown[]>(); const breakdownMap = new Map<string, InvoicePagadorBreakdown[]>();
for (const row of breakdownRows) { for (const row of breakdownRows) {
@@ -205,7 +202,7 @@ export async function fetchDashboardInvoices(
const key = `${row.cardId}:${resolvedPeriod}`; const key = `${row.cardId}:${resolvedPeriod}`;
const current = breakdownMap.get(key) ?? []; const current = breakdownMap.get(key) ?? [];
current.push({ current.push({
pagadorId: row.pagadorId ?? null, payerId: row.payerId ?? null,
pagadorName: row.pagadorName?.trim() || "Sem pagador", pagadorName: row.pagadorName?.trim() || "Sem pagador",
pagadorAvatar: row.pagadorAvatar ?? null, pagadorAvatar: row.pagadorAvatar ?? null,
amount, amount,
@@ -213,7 +210,7 @@ export async function fetchDashboardInvoices(
breakdownMap.set(key, current); breakdownMap.set(key, current);
} }
const invoices: DashboardInvoice[] = []; const invoiceList: DashboardInvoice[] = [];
for (const row of rows) { for (const row of rows) {
if (!row) { if (!row) {
@@ -242,7 +239,7 @@ export async function fetchDashboardInvoices(
? (paymentMap.get(paymentKey) ?? toDateOnlyString(row.invoiceCreatedAt)) ? (paymentMap.get(paymentKey) ?? toDateOnlyString(row.invoiceCreatedAt))
: null; : null;
invoices.push({ invoiceList.push({
id: row.invoiceId ?? buildFallbackId(row.cardId, period), id: row.invoiceId ?? buildFallbackId(row.cardId, period),
cardId: row.cardId, cardId: row.cardId,
cardName: row.cardName, 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 // Ordena do maior valor para o menor
return Math.abs(b.totalAmount) - Math.abs(a.totalAmount); 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) { if (invoice.paymentStatus !== INVOICE_PAYMENT_STATUS.PENDING) {
return total; return total;
} }
@@ -273,7 +270,7 @@ export async function fetchDashboardInvoices(
}, 0); }, 0);
return { return {
invoices, invoices: invoiceList,
totalPending, totalPending,
}; };
} }

View File

@@ -7,7 +7,7 @@ export const mapDashboardNoteToNote = (note: DashboardNote): Note => ({
description: note.description, description: note.description,
type: note.type, type: note.type,
tasks: note.tasks, tasks: note.tasks,
arquivada: note.arquivada, archived: note.archived,
createdAt: note.createdAt, createdAt: note.createdAt,
}); });

View File

@@ -1,5 +1,5 @@
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { anotacoes } from "@/db/schema"; import { notes } from "@/db/schema";
import { db } from "@/shared/lib/db"; import { db } from "@/shared/lib/db";
export type DashboardTask = { export type DashboardTask = {
@@ -14,7 +14,7 @@ export type DashboardNote = {
description: string; description: string;
type: "nota" | "tarefa"; type: "nota" | "tarefa";
tasks?: DashboardTask[]; tasks?: DashboardTask[];
arquivada: boolean; archived: boolean;
createdAt: string; createdAt: string;
}; };
@@ -55,19 +55,19 @@ const parseTasks = (value: string | null): DashboardTask[] | undefined => {
export async function fetchDashboardNotes( export async function fetchDashboardNotes(
userId: string, userId: string,
): Promise<DashboardNote[]> { ): Promise<DashboardNote[]> {
const notes = await db.query.anotacoes.findMany({ const noteRows = await db.query.notes.findMany({
where: and(eq(anotacoes.userId, userId), eq(anotacoes.arquivada, false)), where: and(eq(notes.userId, userId), eq(notes.archived, false)),
orderBy: (note, { desc }) => [desc(note.createdAt)], orderBy: (note, { desc }) => [desc(note.createdAt)],
limit: 5, limit: 5,
}); });
return notes.map((note) => ({ return noteRows.map((note) => ({
id: note.id, id: note.id,
title: (note.title ?? "").trim(), title: (note.title ?? "").trim(),
description: (note.description ?? "").trim(), description: (note.description ?? "").trim(),
type: (note.type ?? "nota") as "nota" | "tarefa", type: (note.type ?? "nota") as "nota" | "tarefa",
tasks: parseTasks(note.tasks), tasks: parseTasks(note.tasks),
arquivada: note.arquivada, archived: note.archived,
createdAt: note.createdAt.toISOString(), createdAt: note.createdAt.toISOString(),
})); }));
} }

View File

@@ -2,15 +2,15 @@
import { and, eq, lt, ne, sql } from "drizzle-orm"; import { and, eq, lt, ne, sql } from "drizzle-orm";
import { import {
cartoes, budgets,
categorias, cards,
faturas, categories,
lancamentos, invoices,
orcamentos, transactions,
} from "@/db/schema"; } from "@/db/schema";
import { db } from "@/shared/lib/db"; import { db } from "@/shared/lib/db";
import { INVOICE_PAYMENT_STATUS } from "@/shared/lib/invoices"; 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 { import {
buildDateOnlyStringFromPeriodDay, buildDateOnlyStringFromPeriodDay,
getBusinessDateString, getBusinessDateString,
@@ -67,128 +67,126 @@ export async function fetchDashboardNotifications(
const today = getBusinessDateString(); const today = getBusinessDateString();
const DAYS_THRESHOLD = 5; const DAYS_THRESHOLD = 5;
const adminPagadorId = await getAdminPagadorId(userId); const adminPayerId = await getAdminPayerId(userId);
// --- Faturas atrasadas (períodos anteriores) --- // --- Faturas atrasadas (períodos anteriores) ---
const overdueInvoices = await db const overdueInvoices = await db
.select({ .select({
invoiceId: faturas.id, invoiceId: invoices.id,
cardId: cartoes.id, cardId: cards.id,
cardName: cartoes.name, cardName: cards.name,
cardLogo: cartoes.logo, cardLogo: cards.logo,
dueDay: cartoes.dueDay, dueDay: cards.dueDay,
period: faturas.period, period: invoices.period,
totalAmount: sql<number | null>` totalAmount: sql<number | null>`
COALESCE( COALESCE(
(SELECT SUM(${lancamentos.amount}) (SELECT SUM(${transactions.amount})
FROM ${lancamentos} FROM ${transactions}
WHERE ${lancamentos.cartaoId} = ${cartoes.id} WHERE ${transactions.cardId} = ${cards.id}
AND ${lancamentos.period} = ${faturas.period} AND ${transactions.period} = ${invoices.period}
AND ${lancamentos.userId} = ${faturas.userId}), AND ${transactions.userId} = ${invoices.userId}),
0 0
) )
`, `,
}) })
.from(faturas) .from(invoices)
.innerJoin(cartoes, eq(faturas.cartaoId, cartoes.id)) .innerJoin(cards, eq(invoices.cardId, cards.id))
.where( .where(
and( and(
eq(faturas.userId, userId), eq(invoices.userId, userId),
eq(faturas.paymentStatus, INVOICE_PAYMENT_STATUS.PENDING), eq(invoices.paymentStatus, INVOICE_PAYMENT_STATUS.PENDING),
lt(faturas.period, currentPeriod), lt(invoices.period, currentPeriod),
), ),
); );
// --- Faturas do período atual --- // --- Faturas do período atual ---
const currentInvoices = await db const currentInvoices = await db
.select({ .select({
invoiceId: faturas.id, invoiceId: invoices.id,
cardId: cartoes.id, cardId: cards.id,
cardName: cartoes.name, cardName: cards.name,
cardLogo: cartoes.logo, cardLogo: cards.logo,
dueDay: cartoes.dueDay, dueDay: cards.dueDay,
period: sql<string>`COALESCE(${faturas.period}, ${currentPeriod})`, period: sql<string>`COALESCE(${invoices.period}, ${currentPeriod})`,
paymentStatus: faturas.paymentStatus, paymentStatus: invoices.paymentStatus,
totalAmount: sql<number | null>` 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( .leftJoin(
faturas, invoices,
and( and(
eq(faturas.cartaoId, cartoes.id), eq(invoices.cardId, cards.id),
eq(faturas.userId, userId), eq(invoices.userId, userId),
eq(faturas.period, currentPeriod), eq(invoices.period, currentPeriod),
), ),
) )
.leftJoin( .leftJoin(
lancamentos, transactions,
and( and(
eq(lancamentos.cartaoId, cartoes.id), eq(transactions.cardId, cards.id),
eq(lancamentos.userId, userId), eq(transactions.userId, userId),
eq(lancamentos.period, currentPeriod), eq(transactions.period, currentPeriod),
), ),
) )
.where(eq(cartoes.userId, userId)) .where(eq(cards.userId, userId))
.groupBy( .groupBy(
faturas.id, invoices.id,
cartoes.id, cards.id,
cartoes.name, cards.name,
cartoes.logo, cards.logo,
cartoes.dueDay, cards.dueDay,
faturas.period, invoices.period,
faturas.paymentStatus, invoices.paymentStatus,
); );
// --- Boletos não pagos --- // --- Boletos não pagos ---
const boletosConditions = [ const boletosConditions = [
eq(lancamentos.userId, userId), eq(transactions.userId, userId),
eq(lancamentos.paymentMethod, PAYMENT_METHOD_BOLETO), eq(transactions.paymentMethod, PAYMENT_METHOD_BOLETO),
eq(lancamentos.isSettled, false), eq(transactions.isSettled, false),
]; ];
if (adminPagadorId) { if (adminPayerId) {
boletosConditions.push(eq(lancamentos.pagadorId, adminPagadorId)); boletosConditions.push(eq(transactions.payerId, adminPayerId));
} }
const boletosRows = await db const boletosRows = await db
.select({ .select({
id: lancamentos.id, id: transactions.id,
name: lancamentos.name, name: transactions.name,
amount: lancamentos.amount, amount: transactions.amount,
dueDate: lancamentos.dueDate, dueDate: transactions.dueDate,
period: lancamentos.period, period: transactions.period,
}) })
.from(lancamentos) .from(transactions)
.where(and(...boletosConditions)); .where(and(...boletosConditions));
// --- Orçamentos do período atual --- // --- Orçamentos do período atual ---
const budgetJoinConditions = [ const budgetJoinConditions = [
eq(lancamentos.categoriaId, orcamentos.categoriaId), eq(transactions.categoryId, budgets.categoryId),
eq(lancamentos.userId, orcamentos.userId), eq(transactions.userId, budgets.userId),
eq(lancamentos.period, orcamentos.period), eq(transactions.period, budgets.period),
eq(lancamentos.transactionType, "Despesa"), eq(transactions.transactionType, "Despesa"),
ne(lancamentos.condition, "cancelado"), ne(transactions.condition, "cancelado"),
]; ];
if (adminPagadorId) { if (adminPayerId) {
budgetJoinConditions.push(eq(lancamentos.pagadorId, adminPagadorId)); budgetJoinConditions.push(eq(transactions.payerId, adminPayerId));
} }
const budgetRows = await db const budgetRows = await db
.select({ .select({
orcamentoId: orcamentos.id, orcamentoId: budgets.id,
budgetAmount: orcamentos.amount, budgetAmount: budgets.amount,
categoriaName: categorias.name, categoriaName: categories.name,
spentAmount: sql<number>`COALESCE(SUM(ABS(${lancamentos.amount})), 0)`, spentAmount: sql<number>`COALESCE(SUM(ABS(${transactions.amount})), 0)`,
}) })
.from(orcamentos) .from(budgets)
.innerJoin(categorias, eq(orcamentos.categoriaId, categorias.id)) .innerJoin(categories, eq(budgets.categoryId, categories.id))
.leftJoin(lancamentos, and(...budgetJoinConditions)) .leftJoin(transactions, and(...budgetJoinConditions))
.where( .where(and(eq(budgets.userId, userId), eq(budgets.period, currentPeriod)))
and(eq(orcamentos.userId, userId), eq(orcamentos.period, currentPeriod)), .groupBy(budgets.id, budgets.amount, categories.name);
)
.groupBy(orcamentos.id, orcamentos.amount, categorias.name);
// ===================== // =====================
// Processar notificações // Processar notificações

View File

@@ -1,8 +1,8 @@
import { and, desc, eq, inArray, isNull, or, sql } from "drizzle-orm"; 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 { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/shared/lib/accounts/constants";
import { db } from "@/shared/lib/db"; 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 { calculatePercentageChange } from "@/shared/utils/math";
import { safeToNumber as toNumber } from "@/shared/utils/number"; import { safeToNumber as toNumber } from "@/shared/utils/number";
import { getPreviousPeriod } from "@/shared/utils/period"; import { getPreviousPeriod } from "@/shared/utils/period";
@@ -18,49 +18,49 @@ export type DashboardPagador = {
isAdmin: boolean; isAdmin: boolean;
}; };
export type DashboardPagadoresSnapshot = { export type DashboardPayersSnapshot = {
pagadores: DashboardPagador[]; payers: DashboardPagador[];
totalExpenses: number; totalExpenses: number;
}; };
export async function fetchDashboardPagadores( export async function fetchDashboardPayers(
userId: string, userId: string,
period: string, period: string,
): Promise<DashboardPagadoresSnapshot> { ): Promise<DashboardPayersSnapshot> {
const previousPeriod = getPreviousPeriod(period); const previousPeriod = getPreviousPeriod(period);
const rows = await db const rows = await db
.select({ .select({
id: pagadores.id, id: payers.id,
name: pagadores.name, name: payers.name,
email: pagadores.email, email: payers.email,
avatarUrl: pagadores.avatarUrl, avatarUrl: payers.avatarUrl,
role: pagadores.role, role: payers.role,
period: lancamentos.period, period: transactions.period,
totalExpenses: sql<number>`COALESCE(SUM(ABS(${lancamentos.amount})), 0)`, totalExpenses: sql<number>`COALESCE(SUM(ABS(${transactions.amount})), 0)`,
}) })
.from(lancamentos) .from(transactions)
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id)) .innerJoin(payers, eq(transactions.payerId, payers.id))
.where( .where(
and( and(
eq(lancamentos.userId, userId), eq(transactions.userId, userId),
inArray(lancamentos.period, [period, previousPeriod]), inArray(transactions.period, [period, previousPeriod]),
eq(lancamentos.transactionType, "Despesa"), eq(transactions.transactionType, "Despesa"),
or( or(
isNull(lancamentos.note), isNull(transactions.note),
sql`${lancamentos.note} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`, sql`${transactions.note} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`,
), ),
), ),
) )
.groupBy( .groupBy(
pagadores.id, payers.id,
pagadores.name, payers.name,
pagadores.email, payers.email,
pagadores.avatarUrl, payers.avatarUrl,
pagadores.role, payers.role,
lancamentos.period, transactions.period,
) )
.orderBy(desc(sql`SUM(ABS(${lancamentos.amount}))`)); .orderBy(desc(sql`SUM(ABS(${transactions.amount}))`));
const groupedPagadores = new Map< const groupedPagadores = new Map<
string, string,
@@ -81,7 +81,7 @@ export async function fetchDashboardPagadores(
name: row.name, name: row.name,
email: row.email, email: row.email,
avatarUrl: row.avatarUrl, avatarUrl: row.avatarUrl,
isAdmin: row.role === PAGADOR_ROLE_ADMIN, isAdmin: row.role === PAYER_ROLE_ADMIN,
currentExpenses: 0, currentExpenses: 0,
previousExpenses: 0, previousExpenses: 0,
}; };
@@ -96,7 +96,7 @@ export async function fetchDashboardPagadores(
groupedPagadores.set(row.id, entry); groupedPagadores.set(row.id, entry);
} }
const pagadoresList = Array.from(groupedPagadores.values()) const payerList = Array.from(groupedPagadores.values())
.filter((p) => p.currentExpenses > 0) .filter((p) => p.currentExpenses > 0)
.map((pagador) => ({ .map((pagador) => ({
id: pagador.id, id: pagador.id,
@@ -113,13 +113,13 @@ export async function fetchDashboardPagadores(
})) }))
.sort((a, b) => b.totalExpenses - a.totalExpenses); .sort((a, b) => b.totalExpenses - a.totalExpenses);
const totalExpenses = pagadoresList.reduce( const totalExpenses = payerList.reduce(
(sum, p) => sum + p.totalExpenses, (sum, p) => sum + p.totalExpenses,
0, 0,
); );
return { return {
pagadores: pagadoresList, payers: payerList,
totalExpenses, totalExpenses,
}; };
} }

View File

@@ -1,11 +1,11 @@
import { and, eq, sql } from "drizzle-orm"; import { and, eq, sql } from "drizzle-orm";
import { lancamentos } from "@/db/schema"; import { transactions } from "@/db/schema";
import { import {
buildDashboardAdminPeriodFilters, buildDashboardAdminPeriodFilters,
excludeAutoGeneratedEntryNotes, excludeAutoGeneratedEntryNotes,
} from "@/features/dashboard/lancamento-filters"; } from "@/features/dashboard/transaction-filters";
import { db } from "@/shared/lib/db"; 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 { safeToNumber as toNumber } from "@/shared/utils/number";
export type PaymentConditionSummary = { export type PaymentConditionSummary = {
@@ -23,30 +23,30 @@ export async function fetchPaymentConditions(
userId: string, userId: string,
period: string, period: string,
): Promise<PaymentConditionsData> { ): Promise<PaymentConditionsData> {
const adminPagadorId = await getAdminPagadorId(userId); const adminPayerId = await getAdminPayerId(userId);
if (!adminPagadorId) { if (!adminPayerId) {
return { conditions: [] }; return { conditions: [] };
} }
const rows = await db const rows = await db
.select({ .select({
condition: lancamentos.condition, condition: transactions.condition,
totalAmount: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`, totalAmount: sql<number>`coalesce(sum(${transactions.amount}), 0)`,
transactions: sql<number>`count(${lancamentos.id})`, transactions: sql<number>`count(${transactions.id})`,
}) })
.from(lancamentos) .from(transactions)
.where( .where(
and( and(
...buildDashboardAdminPeriodFilters({ ...buildDashboardAdminPeriodFilters({
userId, userId,
period, period,
adminPagadorId, adminPayerId,
}), }),
eq(lancamentos.transactionType, "Despesa"), eq(transactions.transactionType, "Despesa"),
excludeAutoGeneratedEntryNotes(), excludeAutoGeneratedEntryNotes(),
), ),
) )
.groupBy(lancamentos.condition); .groupBy(transactions.condition);
const summaries = rows.map((row: (typeof rows)[number]) => { const summaries = rows.map((row: (typeof rows)[number]) => {
const totalAmount = Math.abs(toNumber(row.totalAmount)); const totalAmount = Math.abs(toNumber(row.totalAmount));

View File

@@ -1,11 +1,11 @@
import { and, eq, sql } from "drizzle-orm"; import { and, eq, sql } from "drizzle-orm";
import { lancamentos } from "@/db/schema"; import { transactions } from "@/db/schema";
import { import {
buildDashboardAdminPeriodFilters, buildDashboardAdminPeriodFilters,
excludeAutoGeneratedEntryNotes, excludeAutoGeneratedEntryNotes,
} from "@/features/dashboard/lancamento-filters"; } from "@/features/dashboard/transaction-filters";
import { db } from "@/shared/lib/db"; 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 { safeToNumber as toNumber } from "@/shared/utils/number";
export type PaymentMethodSummary = { export type PaymentMethodSummary = {
@@ -23,30 +23,30 @@ export async function fetchPaymentMethods(
userId: string, userId: string,
period: string, period: string,
): Promise<PaymentMethodsData> { ): Promise<PaymentMethodsData> {
const adminPagadorId = await getAdminPagadorId(userId); const adminPayerId = await getAdminPayerId(userId);
if (!adminPagadorId) { if (!adminPayerId) {
return { methods: [] }; return { methods: [] };
} }
const rows = await db const rows = await db
.select({ .select({
paymentMethod: lancamentos.paymentMethod, paymentMethod: transactions.paymentMethod,
totalAmount: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`, totalAmount: sql<number>`coalesce(sum(${transactions.amount}), 0)`,
transactions: sql<number>`count(${lancamentos.id})`, transactions: sql<number>`count(${transactions.id})`,
}) })
.from(lancamentos) .from(transactions)
.where( .where(
and( and(
...buildDashboardAdminPeriodFilters({ ...buildDashboardAdminPeriodFilters({
userId, userId,
period, period,
adminPagadorId, adminPayerId,
}), }),
eq(lancamentos.transactionType, "Despesa"), eq(transactions.transactionType, "Despesa"),
excludeAutoGeneratedEntryNotes(), excludeAutoGeneratedEntryNotes(),
), ),
) )
.groupBy(lancamentos.paymentMethod); .groupBy(transactions.paymentMethod);
const summaries = rows.map((row: (typeof rows)[number]) => { const summaries = rows.map((row: (typeof rows)[number]) => {
const amount = Math.abs(toNumber(row.totalAmount)); const amount = Math.abs(toNumber(row.totalAmount));

View File

@@ -1,11 +1,11 @@
import { and, inArray, sql } from "drizzle-orm"; import { and, inArray, sql } from "drizzle-orm";
import { lancamentos } from "@/db/schema"; import { transactions } from "@/db/schema";
import { import {
buildDashboardAdminPeriodFilters, buildDashboardAdminPeriodFilters,
excludeAutoInvoiceEntries, excludeAutoInvoiceEntries,
} from "@/features/dashboard/lancamento-filters"; } from "@/features/dashboard/transaction-filters";
import { db } from "@/shared/lib/db"; 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 { safeToNumber as toNumber } from "@/shared/utils/number";
export type PaymentStatusCategory = { export type PaymentStatusCategory = {
@@ -29,41 +29,41 @@ export async function fetchPaymentStatus(
userId: string, userId: string,
period: string, period: string,
): Promise<PaymentStatusData> { ): Promise<PaymentStatusData> {
const adminPagadorId = await getAdminPagadorId(userId); const adminPayerId = await getAdminPayerId(userId);
if (!adminPagadorId) { if (!adminPayerId) {
return { income: emptyCategory(), expenses: emptyCategory() }; return { income: emptyCategory(), expenses: emptyCategory() };
} }
// Single query: GROUP BY transactionType instead of 2 separate queries // Single query: GROUP BY transactionType instead of 2 separate queries
const rows = await db const rows = await db
.select({ .select({
transactionType: lancamentos.transactionType, transactionType: transactions.transactionType,
confirmed: sql<number>` confirmed: sql<number>`
coalesce( 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 0
) )
`, `,
pending: sql<number>` pending: sql<number>`
coalesce( 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 0
) )
`, `,
}) })
.from(lancamentos) .from(transactions)
.where( .where(
and( and(
...buildDashboardAdminPeriodFilters({ ...buildDashboardAdminPeriodFilters({
userId, userId,
period, period,
adminPagadorId, adminPayerId,
}), }),
inArray(lancamentos.transactionType, ["Receita", "Despesa"]), inArray(transactions.transactionType, ["Receita", "Despesa"]),
excludeAutoInvoiceEntries(), excludeAutoInvoiceEntries(),
), ),
) )
.groupBy(lancamentos.transactionType); .groupBy(transactions.transactionType);
const result = { income: emptyCategory(), expenses: emptyCategory() }; const result = { income: emptyCategory(), expenses: emptyCategory() };

View File

@@ -11,10 +11,10 @@ export async function fetchUserDashboardPreferences(
): Promise<UserDashboardPreferences> { ): Promise<UserDashboardPreferences> {
const result = await db const result = await db
.select({ .select({
dashboardWidgets: schema.preferenciasUsuario.dashboardWidgets, dashboardWidgets: schema.userPreferences.dashboardWidgets,
}) })
.from(schema.preferenciasUsuario) .from(schema.userPreferences)
.where(eq(schema.preferenciasUsuario.userId, userId)) .where(eq(schema.userPreferences.userId, userId))
.limit(1); .limit(1);
return { return {

View File

@@ -1,11 +1,16 @@
import { and, desc, eq, inArray } from "drizzle-orm"; 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 { import {
buildDashboardAdminPeriodFilters, buildDashboardAdminPeriodFilters,
excludeAutoGeneratedEntryNotes, excludeAutoGeneratedEntryNotes,
} from "@/features/dashboard/lancamento-filters"; } from "@/features/dashboard/transaction-filters";
import { db } from "@/shared/lib/db"; 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 { safeToNumber as toNumber } from "@/shared/utils/number";
export type CategoryOption = { export type CategoryOption = {
@@ -45,39 +50,42 @@ export async function fetchPurchasesByCategory(
userId: string, userId: string,
period: string, period: string,
): Promise<PurchasesByCategoryData> { ): Promise<PurchasesByCategoryData> {
const adminPagadorId = await getAdminPagadorId(userId); const adminPayerId = await getAdminPayerId(userId);
if (!adminPagadorId) { if (!adminPayerId) {
return { categories: [], transactionsByCategory: {} }; return { categories: [], transactionsByCategory: {} };
} }
const transactionsRows = await db const transactionsRows = await db
.select({ .select({
id: lancamentos.id, id: transactions.id,
name: lancamentos.name, name: transactions.name,
amount: lancamentos.amount, amount: transactions.amount,
purchaseDate: lancamentos.purchaseDate, purchaseDate: transactions.purchaseDate,
categoryId: lancamentos.categoriaId, categoryId: transactions.categoryId,
categoryName: categorias.name, categoryName: categories.name,
categoryType: categorias.type, categoryType: categories.type,
cardLogo: cartoes.logo, cardLogo: cards.logo,
accountLogo: contas.logo, accountLogo: financialAccounts.logo,
}) })
.from(lancamentos) .from(transactions)
.innerJoin(categorias, eq(lancamentos.categoriaId, categorias.id)) .innerJoin(categories, eq(transactions.categoryId, categories.id))
.leftJoin(cartoes, eq(lancamentos.cartaoId, cartoes.id)) .leftJoin(cards, eq(transactions.cardId, cards.id))
.leftJoin(contas, eq(lancamentos.contaId, contas.id)) .leftJoin(
financialAccounts,
eq(transactions.accountId, financialAccounts.id),
)
.where( .where(
and( and(
...buildDashboardAdminPeriodFilters({ ...buildDashboardAdminPeriodFilters({
userId, userId,
period, period,
adminPagadorId, adminPayerId,
}), }),
inArray(categorias.type, ["despesa", "receita"]), inArray(categories.type, ["despesa", "receita"]),
excludeAutoGeneratedEntryNotes(), excludeAutoGeneratedEntryNotes(),
), ),
) )
.orderBy(desc(lancamentos.purchaseDate)); .orderBy(desc(transactions.purchaseDate));
const transactionsByCategory: Record<string, CategoryTransaction[]> = {}; const transactionsByCategory: Record<string, CategoryTransaction[]> = {};
const categoriesMap = new Map<string, CategoryOption>(); 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) // Ordena as categories: receitas primeiro, depois despesas (alfabeticamente dentro de cada tipo)
const categories = Array.from(categoriesMap.values()).sort((a, b) => { const categoryList = Array.from(categoriesMap.values()).sort((a, b) => {
// Receita vem antes de despesa // Receita vem antes de despesa
if (a.type !== b.type) { if (a.type !== b.type) {
return a.type === "receita" ? -1 : 1; return a.type === "receita" ? -1 : 1;
@@ -131,7 +139,7 @@ export async function fetchPurchasesByCategory(
}); });
return { return {
categories, categories: categoryList,
transactionsByCategory, transactionsByCategory,
}; };
} }

View File

@@ -1,8 +1,8 @@
import { and, eq, inArray } from "drizzle-orm"; import { and, eq, inArray } from "drizzle-orm";
import type { RecurringSeriesTemplate } from "@/db/schema"; 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 { 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 { safeToNumber as toNumber } from "@/shared/utils/number";
import { addMonthsToPeriod } from "@/shared/utils/period"; import { addMonthsToPeriod } from "@/shared/utils/period";
@@ -26,7 +26,7 @@ export type RecurringSeriesData = {
export async function fetchRecurringSeries( export async function fetchRecurringSeries(
userId: string, userId: string,
): Promise<RecurringSeriesData> { ): Promise<RecurringSeriesData> {
const adminPagadorId = await getAdminPagadorId(userId); const adminPayerId = await getAdminPayerId(userId);
const rows = await db const rows = await db
.select({ .select({
@@ -50,19 +50,19 @@ export async function fetchRecurringSeries(
// Fetch category names for all series in one query // Fetch category names for all series in one query
const categoryIds = rows const categoryIds = rows
.map((r) => (r.templateData as RecurringSeriesTemplate).categoriaId) .map((r) => (r.templateData as RecurringSeriesTemplate).categoryId)
.filter((id): id is string => id !== null); .filter((id): id is string => id !== null);
const categoryMap = new Map<string, { name: string; icon: string | null }>(); const categoryMap = new Map<string, { name: string; icon: string | null }>();
if (categoryIds.length > 0) { if (categoryIds.length > 0) {
const cats = await db const cats = await db
.select({ .select({
id: categorias.id, id: categories.id,
name: categorias.name, name: categories.name,
icon: categorias.icon, icon: categories.icon,
}) })
.from(categorias) .from(categories)
.where(inArray(categorias.id, categoryIds)); .where(inArray(categories.id, categoryIds));
for (const cat of cats) { for (const cat of cats) {
categoryMap.set(cat.id, { name: cat.name, icon: cat.icon }); categoryMap.set(cat.id, { name: cat.name, icon: cat.icon });
} }
@@ -71,16 +71,14 @@ export async function fetchRecurringSeries(
const series = rows const series = rows
.filter((row) => { .filter((row) => {
// If admin pagador exists, only show series belonging to admin // If admin pagador exists, only show series belonging to admin
if (!adminPagadorId) return true; if (!adminPayerId) return true;
const template = row.templateData as RecurringSeriesTemplate; const template = row.templateData as RecurringSeriesTemplate;
return ( return template.payerId === adminPayerId || template.payerId === null;
template.pagadorId === adminPagadorId || template.pagadorId === null
);
}) })
.map((row): RecurringSeriesItem => { .map((row): RecurringSeriesItem => {
const template = row.templateData as RecurringSeriesTemplate; const template = row.templateData as RecurringSeriesTemplate;
const category = template.categoriaId const category = template.categoryId
? categoryMap.get(template.categoriaId) ? categoryMap.get(template.categoryId)
: null; : null;
return { return {
id: row.id, id: row.id,

View File

@@ -1,11 +1,11 @@
import { and, eq, sql } from "drizzle-orm"; import { and, eq, sql } from "drizzle-orm";
import { cartoes, contas, lancamentos } from "@/db/schema"; import { cards, financialAccounts, transactions } from "@/db/schema";
import { import {
buildDashboardAdminPeriodFilters, buildDashboardAdminPeriodFilters,
excludeAutoGeneratedEntryNotes, excludeAutoGeneratedEntryNotes,
} from "@/features/dashboard/lancamento-filters"; } from "@/features/dashboard/transaction-filters";
import { db } from "@/shared/lib/db"; 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 { safeToNumber as toNumber } from "@/shared/utils/number";
export type TopEstablishment = { export type TopEstablishment = {
@@ -38,36 +38,41 @@ export async function fetchTopEstablishments(
userId: string, userId: string,
period: string, period: string,
): Promise<TopEstablishmentsData> { ): Promise<TopEstablishmentsData> {
const adminPagadorId = await getAdminPagadorId(userId); const adminPayerId = await getAdminPayerId(userId);
if (!adminPagadorId) { if (!adminPayerId) {
return { establishments: [] }; return { establishments: [] };
} }
const rows = await db const rows = await db
.select({ .select({
name: lancamentos.name, name: transactions.name,
totalAmount: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`, totalAmount: sql<number>`coalesce(sum(${transactions.amount}), 0)`,
occurrences: sql<number>`count(${lancamentos.id})`, occurrences: sql<number>`count(${transactions.id})`,
logo: sql<string | null>`max(coalesce(${cartoes.logo}, ${contas.logo}))`, logo: sql<
string | null
>`max(coalesce(${cards.logo}, ${financialAccounts.logo}))`,
}) })
.from(lancamentos) .from(transactions)
.leftJoin(cartoes, eq(lancamentos.cartaoId, cartoes.id)) .leftJoin(cards, eq(transactions.cardId, cards.id))
.leftJoin(contas, eq(lancamentos.contaId, contas.id)) .leftJoin(
financialAccounts,
eq(transactions.accountId, financialAccounts.id),
)
.where( .where(
and( and(
...buildDashboardAdminPeriodFilters({ ...buildDashboardAdminPeriodFilters({
userId, userId,
period, period,
adminPagadorId, adminPayerId,
}), }),
eq(lancamentos.transactionType, "Despesa"), eq(transactions.transactionType, "Despesa"),
excludeAutoGeneratedEntryNotes(), excludeAutoGeneratedEntryNotes(),
), ),
) )
.groupBy(lancamentos.name) .groupBy(transactions.name)
.orderBy( .orderBy(
sql`count(${lancamentos.id}) DESC`, sql`count(${transactions.id}) DESC`,
sql`ABS(sum(${lancamentos.amount})) DESC`, sql`ABS(sum(${transactions.amount})) DESC`,
) )
.limit(10); .limit(10);

View File

@@ -1,5 +1,5 @@
import { and, eq, ilike, isNull, ne, not, or } from "drizzle-orm"; import { and, eq, ilike, isNull, ne, not, or } from "drizzle-orm";
import { contas, lancamentos } from "@/db/schema"; import { financialAccounts, transactions } from "@/db/schema";
import { import {
ACCOUNT_AUTO_INVOICE_NOTE_PREFIX, ACCOUNT_AUTO_INVOICE_NOTE_PREFIX,
INITIAL_BALANCE_NOTE, INITIAL_BALANCE_NOTE,
@@ -7,7 +7,7 @@ import {
type DashboardAdminFiltersParams = { type DashboardAdminFiltersParams = {
userId: string; userId: string;
adminPagadorId: string; adminPayerId: string;
}; };
type DashboardAdminPeriodFiltersParams = DashboardAdminFiltersParams & { type DashboardAdminPeriodFiltersParams = DashboardAdminFiltersParams & {
@@ -16,41 +16,41 @@ type DashboardAdminPeriodFiltersParams = DashboardAdminFiltersParams & {
export const buildDashboardAdminFilters = ({ export const buildDashboardAdminFilters = ({
userId, userId,
adminPagadorId, adminPayerId,
}: DashboardAdminFiltersParams) => }: DashboardAdminFiltersParams) =>
[ [
eq(lancamentos.userId, userId), eq(transactions.userId, userId),
eq(lancamentos.pagadorId, adminPagadorId), eq(transactions.payerId, adminPayerId),
] as const; ] as const;
export const buildDashboardAdminPeriodFilters = ({ export const buildDashboardAdminPeriodFilters = ({
userId, userId,
period, period,
adminPagadorId, adminPayerId,
}: DashboardAdminPeriodFiltersParams) => }: DashboardAdminPeriodFiltersParams) =>
[ [
...buildDashboardAdminFilters({ userId, adminPagadorId }), ...buildDashboardAdminFilters({ userId, adminPayerId }),
eq(lancamentos.period, period), eq(transactions.period, period),
] as const; ] as const;
export const excludeAutoInvoiceEntries = () => export const excludeAutoInvoiceEntries = () =>
or( or(
isNull(lancamentos.note), isNull(transactions.note),
not(ilike(lancamentos.note, `${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`)), not(ilike(transactions.note, `${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`)),
); );
export const excludeAutoGeneratedEntryNotes = () => export const excludeAutoGeneratedEntryNotes = () =>
or( or(
isNull(lancamentos.note), isNull(transactions.note),
and( and(
ne(lancamentos.note, INITIAL_BALANCE_NOTE), ne(transactions.note, INITIAL_BALANCE_NOTE),
not(ilike(lancamentos.note, `${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`)), not(ilike(transactions.note, `${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`)),
), ),
); );
export const excludeInitialBalanceWhenConfigured = () => export const excludeInitialBalanceWhenConfigured = () =>
or( or(
ne(lancamentos.note, INITIAL_BALANCE_NOTE), ne(transactions.note, INITIAL_BALANCE_NOTE),
isNull(contas.excludeInitialBalanceFromIncome), isNull(financialAccounts.excludeInitialBalanceFromIncome),
eq(contas.excludeInitialBalanceFromIncome, false), eq(financialAccounts.excludeInitialBalanceFromIncome, false),
); );

View File

@@ -10,7 +10,7 @@ import {
type PaymentDialogController, type PaymentDialogController,
usePaymentDialogController, usePaymentDialogController,
} from "@/features/dashboard/use-payment-dialog-controller"; } from "@/features/dashboard/use-payment-dialog-controller";
import { toggleLancamentoSettlementAction } from "@/features/transactions/actions"; import { toggleTransactionSettlementAction } from "@/features/transactions/actions";
const EMPTY_BILLS: DashboardBill[] = []; const EMPTY_BILLS: DashboardBill[] = [];
@@ -31,7 +31,7 @@ export function useBillWidgetController(
getItemId: (bill) => bill.id, getItemId: (bill) => bill.id,
isItemConfirmed: (bill) => bill.isSettled, isItemConfirmed: (bill) => bill.isSettled,
executeConfirm: (bill) => executeConfirm: (bill) =>
toggleLancamentoSettlementAction({ toggleTransactionSettlementAction({
id: bill.id, id: bill.id,
value: true, value: true,
}), }),

View File

@@ -31,7 +31,7 @@ export function useInvoicesWidgetController(
isItemConfirmed: (invoice) => isInvoicePaid(invoice.paymentStatus), isItemConfirmed: (invoice) => isInvoicePaid(invoice.paymentStatus),
executeConfirm: (invoice) => executeConfirm: (invoice) =>
updateInvoicePaymentStatusAction({ updateInvoicePaymentStatusAction({
cartaoId: invoice.cardId, cardId: invoice.cardId,
period: invoice.period, period: invoice.period,
status: INVOICE_PAYMENT_STATUS.PAID, status: INVOICE_PAYMENT_STATUS.PAID,
}), }),

View File

@@ -18,21 +18,21 @@ export async function updateWidgetPreferences(
// Check if preferences exist // Check if preferences exist
const existing = await db const existing = await db
.select({ id: schema.preferenciasUsuario.id }) .select({ id: schema.userPreferences.id })
.from(schema.preferenciasUsuario) .from(schema.userPreferences)
.where(eq(schema.preferenciasUsuario.userId, user.id)) .where(eq(schema.userPreferences.userId, user.id))
.limit(1); .limit(1);
if (existing.length > 0) { if (existing.length > 0) {
await db await db
.update(schema.preferenciasUsuario) .update(schema.userPreferences)
.set({ .set({
dashboardWidgets: preferences, dashboardWidgets: preferences,
updatedAt: new Date(), updatedAt: new Date(),
}) })
.where(eq(schema.preferenciasUsuario.userId, user.id)); .where(eq(schema.userPreferences.userId, user.id));
} else { } else {
await db.insert(schema.preferenciasUsuario).values({ await db.insert(schema.userPreferences).values({
userId: user.id, userId: user.id,
dashboardWidgets: preferences, dashboardWidgets: preferences,
}); });
@@ -54,12 +54,12 @@ export async function resetWidgetPreferences(): Promise<{
const user = await getUser(); const user = await getUser();
await db await db
.update(schema.preferenciasUsuario) .update(schema.userPreferences)
.set({ .set({
dashboardWidgets: null, dashboardWidgets: null,
updatedAt: new Date(), updatedAt: new Date(),
}) })
.where(eq(schema.preferenciasUsuario.userId, user.id)); .where(eq(schema.userPreferences.userId, user.id));
revalidatePath("/dashboard"); revalidatePath("/dashboard");
return { success: true }; return { success: true };

View File

@@ -97,7 +97,7 @@ export const widgetsConfig: WidgetConfig[] = [
subtitle: "Despesas por pagador no período", subtitle: "Despesas por pagador no período",
icon: <RiGroupLine className="size-4" />, icon: <RiGroupLine className="size-4" />,
component: ({ data }) => ( component: ({ data }) => (
<PayersWidget pagadores={data.pagadoresSnapshot.pagadores} /> <PayersWidget payers={data.pagadoresSnapshot.payers} />
), ),
action: ( action: (
<Link <Link

View File

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

View File

@@ -1,5 +1,5 @@
import { and, eq } from "drizzle-orm"; 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 { db } from "@/shared/lib/db";
import { import {
addMonthsToPeriod, addMonthsToPeriod,
@@ -76,7 +76,7 @@ export async function generateRecurringTransactions(
const template = series.templateData; 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) => { await db.transaction(async (tx: typeof db) => {
const records = periodsToGenerate.map((period) => { const records = periodsToGenerate.map((period) => {
const purchaseDate = computePurchaseDate(period, series.dayOfMonth); const purchaseDate = computePurchaseDate(period, series.dayOfMonth);
@@ -86,10 +86,10 @@ export async function generateRecurringTransactions(
transactionType: template.transactionType, transactionType: template.transactionType,
paymentMethod: template.paymentMethod, paymentMethod: template.paymentMethod,
condition: "Recorrente" as const, condition: "Recorrente" as const,
categoriaId: template.categoriaId, categoryId: template.categoryId,
contaId: template.contaId, accountId: template.accountId,
cartaoId: template.cartaoId, cardId: template.cardId,
pagadorId: template.pagadorId, payerId: template.payerId,
note: template.note, note: template.note,
purchaseDate, purchaseDate,
period, 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 // Update lastGeneratedPeriod to the last period we generated
const lastPeriod = const lastPeriod =

View File

@@ -11,15 +11,9 @@ import {
sql, sql,
sum, sum,
} from "drizzle-orm"; } from "drizzle-orm";
import { import { cards, categories, invoices, payers, transactions } from "@/db/schema";
cartoes,
categorias,
faturas,
lancamentos,
pagadores,
} from "@/db/schema";
import { db } from "@/shared/lib/db"; 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 { formatDateOnly } from "@/shared/utils/date";
import { safeToNumber } from "@/shared/utils/number"; import { safeToNumber } from "@/shared/utils/number";
import { import {
@@ -90,7 +84,7 @@ type CardRow = {
}; };
type CardUsageRow = { type CardUsageRow = {
cartaoId: string | null; cardId: string | null;
totalAmount: unknown; totalAmount: unknown;
}; };
@@ -100,7 +94,7 @@ type MonthlyUsageRow = {
}; };
type CategoryAmountRow = { type CategoryAmountRow = {
categoriaId: string | null; categoryId: string | null;
totalAmount: unknown; totalAmount: unknown;
}; };
@@ -115,7 +109,7 @@ type TopExpenseRow = {
name: string; name: string;
amount: unknown; amount: unknown;
purchaseDate: Date | string | null; purchaseDate: Date | string | null;
categoriaId: string | null; categoryId: string | null;
}; };
type InvoiceStatusRow = { type InvoiceStatusRow = {
@@ -133,16 +127,16 @@ export async function fetchCartoesReportData(
// Fetch all active cards (not inactive) // Fetch all active cards (not inactive)
const allCards = (await db const allCards = (await db
.select({ .select({
id: cartoes.id, id: cards.id,
name: cartoes.name, name: cards.name,
brand: cartoes.brand, brand: cards.brand,
logo: cartoes.logo, logo: cards.logo,
limit: cartoes.limit, limit: cards.limit,
status: cartoes.status, status: cards.status,
}) })
.from(cartoes) .from(cards)
.where( .where(
and(eq(cartoes.userId, userId), not(ilike(cartoes.status, "inativo"))), and(eq(cards.userId, userId), not(ilike(cards.status, "inativo"))),
)) as CardRow[]; )) as CardRow[];
if (allCards.length === 0) { 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) // Fetch current period usage by card (recorrente só conta quando a data da ocorrência já passou)
const currentUsageData = (await db const currentUsageData = (await db
.select({ .select({
cartaoId: lancamentos.cartaoId, cardId: transactions.cardId,
totalAmount: sum(lancamentos.amount).as("total"), totalAmount: sum(transactions.amount).as("total"),
}) })
.from(lancamentos) .from(transactions)
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id)) .innerJoin(payers, eq(transactions.payerId, payers.id))
.where( .where(
and( and(
eq(lancamentos.userId, userId), eq(transactions.userId, userId),
eq(lancamentos.period, currentPeriod), eq(transactions.period, currentPeriod),
eq(pagadores.role, PAGADOR_ROLE_ADMIN), eq(payers.role, PAYER_ROLE_ADMIN),
eq(lancamentos.transactionType, DESPESA), eq(transactions.transactionType, DESPESA),
inArray(lancamentos.cartaoId, cardIds), inArray(transactions.cardId, cardIds),
or( or(
ne(lancamentos.condition, "Recorrente"), ne(transactions.condition, "Recorrente"),
sql`${lancamentos.purchaseDate} <= current_date`, sql`${transactions.purchaseDate} <= current_date`,
), ),
), ),
) )
.groupBy(lancamentos.cartaoId)) as CardUsageRow[]; .groupBy(transactions.cardId)) as CardUsageRow[];
// Fetch previous period usage by card // Fetch previous period usage by card
const previousUsageData = (await db const previousUsageData = (await db
.select({ .select({
cartaoId: lancamentos.cartaoId, cardId: transactions.cardId,
totalAmount: sum(lancamentos.amount).as("total"), totalAmount: sum(transactions.amount).as("total"),
}) })
.from(lancamentos) .from(transactions)
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id)) .innerJoin(payers, eq(transactions.payerId, payers.id))
.where( .where(
and( and(
eq(lancamentos.userId, userId), eq(transactions.userId, userId),
eq(lancamentos.period, previousPeriod), eq(transactions.period, previousPeriod),
eq(pagadores.role, PAGADOR_ROLE_ADMIN), eq(payers.role, PAYER_ROLE_ADMIN),
eq(lancamentos.transactionType, DESPESA), eq(transactions.transactionType, DESPESA),
inArray(lancamentos.cartaoId, cardIds), inArray(transactions.cardId, cardIds),
), ),
) )
.groupBy(lancamentos.cartaoId)) as CardUsageRow[]; .groupBy(transactions.cardId)) as CardUsageRow[];
const currentUsageMap = new Map<string, number>(); const currentUsageMap = new Map<string, number>();
for (const row of currentUsageData) { for (const row of currentUsageData) {
if (row.cartaoId) { if (row.cardId) {
currentUsageMap.set( currentUsageMap.set(row.cardId, Math.abs(safeToNumber(row.totalAmount)));
row.cartaoId,
Math.abs(safeToNumber(row.totalAmount)),
);
} }
} }
const previousUsageMap = new Map<string, number>(); const previousUsageMap = new Map<string, number>();
for (const row of previousUsageData) { for (const row of previousUsageData) {
if (row.cartaoId) { if (row.cardId) {
previousUsageMap.set( previousUsageMap.set(row.cardId, Math.abs(safeToNumber(row.totalAmount)));
row.cartaoId,
Math.abs(safeToNumber(row.totalAmount)),
);
} }
} }
// Build card summaries // Build card summaries
const cards: CardSummary[] = allCards.map((card) => { const cardSummaries: CardSummary[] = allCards.map((card) => {
const limit = safeToNumber(card.limit); const limit = safeToNumber(card.limit);
const currentUsage = currentUsageMap.get(card.id) || 0; const currentUsage = currentUsageMap.get(card.id) || 0;
const previousUsage = previousUsageMap.get(card.id) || 0; const previousUsage = previousUsageMap.get(card.id) || 0;
@@ -252,22 +240,22 @@ export async function fetchCartoesReportData(
}; };
}); });
// Sort cards by usage (descending) // Sort cardSummaries by usage (descending)
cards.sort((a, b) => b.currentUsage - a.currentUsage); cardSummaries.sort((a, b) => b.currentUsage - a.currentUsage);
// Calculate totals // Calculate totals
const totalLimit = cards.reduce((acc, c) => acc + c.limit, 0); const totalLimit = cardSummaries.reduce((acc, c) => acc + c.limit, 0);
const totalUsage = cards.reduce((acc, c) => acc + c.currentUsage, 0); const totalUsage = cardSummaries.reduce((acc, c) => acc + c.currentUsage, 0);
const totalUsagePercent = const totalUsagePercent =
totalLimit > 0 ? (totalUsage / totalLimit) * 100 : 0; totalLimit > 0 ? (totalUsage / totalLimit) * 100 : 0;
// Fetch selected card details if provided // Fetch selected card details if provided
let selectedCard: CardDetailData | null = null; let selectedCard: CardDetailData | null = null;
const targetCardId = const targetCardId =
selectedCartaoId || (cards.length > 0 ? cards[0].id : null); selectedCartaoId || (cardSummaries.length > 0 ? cardSummaries[0].id : null);
if (targetCardId) { if (targetCardId) {
const cardSummary = cards.find((c) => c.id === targetCardId); const cardSummary = cardSummaries.find((c) => c.id === targetCardId);
if (cardSummary) { if (cardSummary) {
selectedCard = await fetchCardDetail( selectedCard = await fetchCardDetail(
userId, userId,
@@ -279,7 +267,7 @@ export async function fetchCartoesReportData(
} }
return { return {
cards, cards: cardSummaries,
totalLimit, totalLimit,
totalUsage, totalUsage,
totalUsagePercent, totalUsagePercent,
@@ -301,23 +289,23 @@ async function fetchCardDetail(
// Fetch monthly usage // Fetch monthly usage
const monthlyData = (await db const monthlyData = (await db
.select({ .select({
period: lancamentos.period, period: transactions.period,
totalAmount: sum(lancamentos.amount).as("total"), totalAmount: sum(transactions.amount).as("total"),
}) })
.from(lancamentos) .from(transactions)
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id)) .innerJoin(payers, eq(transactions.payerId, payers.id))
.where( .where(
and( and(
eq(lancamentos.userId, userId), eq(transactions.userId, userId),
eq(lancamentos.cartaoId, cardId), eq(transactions.cardId, cardId),
gte(lancamentos.period, startPeriod), gte(transactions.period, startPeriod),
lte(lancamentos.period, currentPeriod), lte(transactions.period, currentPeriod),
eq(pagadores.role, PAGADOR_ROLE_ADMIN), eq(payers.role, PAYER_ROLE_ADMIN),
eq(lancamentos.transactionType, DESPESA), eq(transactions.transactionType, DESPESA),
), ),
) )
.groupBy(lancamentos.period) .groupBy(transactions.period)
.orderBy(lancamentos.period)) as MonthlyUsageRow[]; .orderBy(transactions.period)) as MonthlyUsageRow[];
const monthlyUsage = periods.map((period) => { const monthlyUsage = periods.map((period) => {
const data = monthlyData.find((d) => d.period === period); const data = monthlyData.find((d) => d.period === period);
@@ -331,37 +319,37 @@ async function fetchCardDetail(
// Fetch category breakdown for current period // Fetch category breakdown for current period
const categoryData = (await db const categoryData = (await db
.select({ .select({
categoriaId: lancamentos.categoriaId, categoryId: transactions.categoryId,
totalAmount: sum(lancamentos.amount).as("total"), totalAmount: sum(transactions.amount).as("total"),
}) })
.from(lancamentos) .from(transactions)
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id)) .innerJoin(payers, eq(transactions.payerId, payers.id))
.where( .where(
and( and(
eq(lancamentos.userId, userId), eq(transactions.userId, userId),
eq(lancamentos.cartaoId, cardId), eq(transactions.cardId, cardId),
eq(lancamentos.period, currentPeriod), eq(transactions.period, currentPeriod),
eq(pagadores.role, PAGADOR_ROLE_ADMIN), eq(payers.role, PAYER_ROLE_ADMIN),
eq(lancamentos.transactionType, DESPESA), eq(transactions.transactionType, DESPESA),
), ),
) )
.groupBy(lancamentos.categoriaId)) as CategoryAmountRow[]; .groupBy(transactions.categoryId)) as CategoryAmountRow[];
// Fetch category names // Fetch category names
const categoryIds = categoryData const categoryIds = categoryData
.map((c) => c.categoriaId) .map((c) => c.categoryId)
.filter((id): id is string => id !== null); .filter((id): id is string => id !== null);
const categoryNames = const categoryNames =
categoryIds.length > 0 categoryIds.length > 0
? ((await db ? ((await db
.select({ .select({
id: categorias.id, id: categories.id,
name: categorias.name, name: categories.name,
icon: categorias.icon, icon: categories.icon,
}) })
.from(categorias) .from(categories)
.where(inArray(categorias.id, categoryIds))) as CategoryInfoRow[]) .where(inArray(categories.id, categoryIds))) as CategoryInfoRow[])
: ([] as CategoryInfoRow[]); : ([] as CategoryInfoRow[]);
const categoryNameMap = new Map(categoryNames.map((c) => [c.id, c])); const categoryNameMap = new Map(categoryNames.map((c) => [c.id, c]));
@@ -374,11 +362,11 @@ async function fetchCardDetail(
const categoryBreakdown = categoryData const categoryBreakdown = categoryData
.map((cat) => { .map((cat) => {
const amount = Math.abs(safeToNumber(cat.totalAmount)); const amount = Math.abs(safeToNumber(cat.totalAmount));
const catInfo = cat.categoriaId const catInfo = cat.categoryId
? categoryNameMap.get(cat.categoriaId) ? categoryNameMap.get(cat.categoryId)
: null; : null;
return { return {
id: cat.categoriaId || "sem-categoria", id: cat.categoryId || "sem-categoria",
name: catInfo?.name || "Sem categoria", name: catInfo?.name || "Sem categoria",
icon: catInfo?.icon || null, icon: catInfo?.icon || null,
amount, amount,
@@ -392,29 +380,29 @@ async function fetchCardDetail(
// Fetch top expenses for current period // Fetch top expenses for current period
const topExpensesData = (await db const topExpensesData = (await db
.select({ .select({
id: lancamentos.id, id: transactions.id,
name: lancamentos.name, name: transactions.name,
amount: lancamentos.amount, amount: transactions.amount,
purchaseDate: lancamentos.purchaseDate, purchaseDate: transactions.purchaseDate,
categoriaId: lancamentos.categoriaId, categoryId: transactions.categoryId,
}) })
.from(lancamentos) .from(transactions)
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id)) .innerJoin(payers, eq(transactions.payerId, payers.id))
.where( .where(
and( and(
eq(lancamentos.userId, userId), eq(transactions.userId, userId),
eq(lancamentos.cartaoId, cardId), eq(transactions.cardId, cardId),
eq(lancamentos.period, currentPeriod), eq(transactions.period, currentPeriod),
eq(pagadores.role, PAGADOR_ROLE_ADMIN), eq(payers.role, PAYER_ROLE_ADMIN),
eq(lancamentos.transactionType, DESPESA), eq(transactions.transactionType, DESPESA),
), ),
) )
.orderBy(lancamentos.amount) .orderBy(transactions.amount)
.limit(10)) as TopExpenseRow[]; .limit(10)) as TopExpenseRow[];
const topExpenses = topExpensesData.map((expense) => { const topExpenses = topExpensesData.map((expense) => {
const catInfo = expense.categoriaId const catInfo = expense.categoryId
? categoryNameMap.get(expense.categoriaId) ? categoryNameMap.get(expense.categoryId)
: null; : null;
return { return {
id: expense.id, id: expense.id,
@@ -433,19 +421,19 @@ async function fetchCardDetail(
// Fetch invoice status for last 6 months // Fetch invoice status for last 6 months
const invoiceData = (await db const invoiceData = (await db
.select({ .select({
period: faturas.period, period: invoices.period,
status: faturas.paymentStatus, status: invoices.paymentStatus,
}) })
.from(faturas) .from(invoices)
.where( .where(
and( and(
eq(faturas.userId, userId), eq(invoices.userId, userId),
eq(faturas.cartaoId, cardId), eq(invoices.cardId, cardId),
gte(faturas.period, startPeriod), gte(invoices.period, startPeriod),
lte(faturas.period, currentPeriod), lte(invoices.period, currentPeriod),
), ),
) )
.orderBy(faturas.period)) as InvoiceStatusRow[]; .orderBy(invoices.period)) as InvoiceStatusRow[];
const invoiceStatus = periods.map((period) => { const invoiceStatus = periods.map((period) => {
const invoice = invoiceData.find((i) => i.period === 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 { 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 { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/shared/lib/accounts/constants";
import { db } from "@/shared/lib/db"; 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 { safeToNumber as toNumber } from "@/shared/utils/number";
import { formatPeriodMonthShort } from "@/shared/utils/period"; import { formatPeriodMonthShort } from "@/shared/utils/period";
import { generatePeriodRange } from "./utils"; import { generatePeriodRange } from "./utils";
@@ -35,56 +35,56 @@ export async function fetchCategoryChartData(
): Promise<CategoryChartData> { ): Promise<CategoryChartData> {
const periods = generatePeriodRange(startPeriod, endPeriod); const periods = generatePeriodRange(startPeriod, endPeriod);
const adminPagadorId = await getAdminPagadorId(userId); const adminPayerId = await getAdminPayerId(userId);
if (!adminPagadorId) { if (!adminPayerId) {
return { months: [], categories: [], chartData: [], allCategories: [] }; return { months: [], categories: [], chartData: [], allCategories: [] };
} }
const whereConditions = [ const whereConditions = [
eq(lancamentos.userId, userId), eq(transactions.userId, userId),
eq(lancamentos.pagadorId, adminPagadorId), eq(transactions.payerId, adminPayerId),
inArray(lancamentos.period, periods), inArray(transactions.period, periods),
or(eq(categorias.type, "despesa"), eq(categorias.type, "receita")), or(eq(categories.type, "despesa"), eq(categories.type, "receita")),
or( or(
isNull(lancamentos.note), isNull(transactions.note),
sql`${lancamentos.note} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`, sql`${transactions.note} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`,
), ),
]; ];
if (categoryIds && categoryIds.length > 0) { if (categoryIds && categoryIds.length > 0) {
whereConditions.push(inArray(categorias.id, categoryIds)); whereConditions.push(inArray(categories.id, categoryIds));
} }
const [rows, allCategoriesRows] = await Promise.all([ const [rows, allCategoriesRows] = await Promise.all([
db db
.select({ .select({
categoryId: categorias.id, categoryId: categories.id,
categoryName: categorias.name, categoryName: categories.name,
categoryIcon: categorias.icon, categoryIcon: categories.icon,
categoryType: categorias.type, categoryType: categories.type,
period: lancamentos.period, period: transactions.period,
total: sql<number>`coalesce(sum(abs(${lancamentos.amount})), 0)`, total: sql<number>`coalesce(sum(abs(${transactions.amount})), 0)`,
}) })
.from(lancamentos) .from(transactions)
.innerJoin(categorias, eq(lancamentos.categoriaId, categorias.id)) .innerJoin(categories, eq(transactions.categoryId, categories.id))
.where(and(...whereConditions)) .where(and(...whereConditions))
.groupBy( .groupBy(
categorias.id, categories.id,
categorias.name, categories.name,
categorias.icon, categories.icon,
categorias.type, categories.type,
lancamentos.period, transactions.period,
), ),
db db
.select({ .select({
id: categorias.id, id: categories.id,
name: categorias.name, name: categories.name,
icon: categorias.icon, icon: categories.icon,
type: categorias.type, type: categories.type,
}) })
.from(categorias) .from(categories)
.where(eq(categorias.userId, userId)) .where(eq(categories.userId, userId))
.orderBy(categorias.type, categorias.name), .orderBy(categories.type, categories.name),
]); ]);
const allCategories = allCategoriesRows.map( const allCategories = allCategoriesRows.map(
@@ -143,12 +143,12 @@ export async function fetchCategoryChartData(
formatPeriodMonthShort(period).toUpperCase(), formatPeriodMonthShort(period).toUpperCase(),
); );
const categories = Array.from(categoryMap.values()).map((cat) => ({ const categoryList = Array.from(categoryMap.values()).map((cat) => ({
id: cat.id, id: cat.id,
name: cat.name, name: cat.name,
icon: cat.icon, icon: cat.icon,
type: cat.type, 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 { 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 { ACCOUNT_AUTO_INVOICE_NOTE_PREFIX } from "@/shared/lib/accounts/constants";
import { db } from "@/shared/lib/db"; 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 { safeToNumber as toNumber } from "@/shared/utils/number";
import type { import type {
CategoryReportData, CategoryReportData,
@@ -28,47 +28,47 @@ export async function fetchCategoryReport(
// Generate all periods in the range // Generate all periods in the range
const periods = generatePeriodRange(startPeriod, endPeriod); const periods = generatePeriodRange(startPeriod, endPeriod);
const adminPagadorId = await getAdminPagadorId(userId); const adminPayerId = await getAdminPayerId(userId);
if (!adminPagadorId) { if (!adminPayerId) {
return { categories: [], periods, totals: new Map(), grandTotal: 0 }; return { categories: [], periods, totals: new Map(), grandTotal: 0 };
} }
// Build WHERE conditions // Build WHERE conditions
const whereConditions = [ const whereConditions = [
eq(lancamentos.userId, userId), eq(transactions.userId, userId),
eq(lancamentos.pagadorId, adminPagadorId), eq(transactions.payerId, adminPayerId),
inArray(lancamentos.period, periods), inArray(transactions.period, periods),
or(eq(categorias.type, "despesa"), eq(categorias.type, "receita")), or(eq(categories.type, "despesa"), eq(categories.type, "receita")),
or( or(
isNull(lancamentos.note), isNull(transactions.note),
sql`${lancamentos.note} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`, sql`${transactions.note} NOT LIKE ${`${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`}`,
), ),
]; ];
// Add optional category filter // Add optional category filter
if (categoryIds && categoryIds.length > 0) { 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 // Query to get aggregated data by category and period
const rows = await db const rows = await db
.select({ .select({
categoryId: categorias.id, categoryId: categories.id,
categoryName: categorias.name, categoryName: categories.name,
categoryIcon: categorias.icon, categoryIcon: categories.icon,
categoryType: categorias.type, categoryType: categories.type,
period: lancamentos.period, period: transactions.period,
total: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`, total: sql<number>`coalesce(sum(${transactions.amount}), 0)`,
}) })
.from(lancamentos) .from(transactions)
.innerJoin(categorias, eq(lancamentos.categoriaId, categorias.id)) .innerJoin(categories, eq(transactions.categoryId, categories.id))
.where(and(...whereConditions)) .where(and(...whereConditions))
.groupBy( .groupBy(
categorias.id, categories.id,
categorias.name, categories.name,
categorias.icon, categories.icon,
categorias.type, categories.type,
lancamentos.period, transactions.period,
); );
// Process results into CategoryReportData structure // Process results into CategoryReportData structure
@@ -171,10 +171,10 @@ export async function fetchCategoryReport(
} }
// Convert to array and sort // 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) // 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 // First by type: despesa comes before receita
if (a.type !== b.type) { if (a.type !== b.type) {
return a.type === "despesa" ? -1 : 1; return a.type === "despesa" ? -1 : 1;
@@ -185,12 +185,12 @@ export async function fetchCategoryReport(
// Calculate grand total // Calculate grand total
let grandTotal = 0; let grandTotal = 0;
for (const categoryItem of categories) { for (const categoryItem of categoryList) {
grandTotal += categoryItem.total; grandTotal += categoryItem.total;
} }
return { return {
categories, categories: categoryList,
periods, periods,
totals: periodTotalsMap, totals: periodTotalsMap,
grandTotal, grandTotal,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,13 +13,18 @@ import {
sql, sql,
sum, sum,
} from "drizzle-orm"; } from "drizzle-orm";
import { categorias, contas, lancamentos, pagadores } from "@/db/schema"; import {
categories,
financialAccounts,
payers,
transactions,
} from "@/db/schema";
import { import {
ACCOUNT_AUTO_INVOICE_NOTE_PREFIX, ACCOUNT_AUTO_INVOICE_NOTE_PREFIX,
INITIAL_BALANCE_NOTE, INITIAL_BALANCE_NOTE,
} from "@/shared/lib/accounts/constants"; } from "@/shared/lib/accounts/constants";
import { db } from "@/shared/lib/db"; 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 { safeToNumber } from "@/shared/utils/number";
import { getPreviousPeriod } from "@/shared/utils/period"; import { getPreviousPeriod } from "@/shared/utils/period";
@@ -68,7 +73,7 @@ function buildPeriodRange(currentPeriod: string, months: number): string[] {
return periods; return periods;
} }
export async function fetchTopEstabelecimentosData( export async function fetchTopEstablishmentsData(
userId: string, userId: string,
currentPeriod: string, currentPeriod: string,
periodFilter: PeriodFilter = "6", periodFilter: PeriodFilter = "6",
@@ -80,33 +85,36 @@ export async function fetchTopEstabelecimentosData(
// Fetch establishments with transaction count and total amount // Fetch establishments with transaction count and total amount
const establishmentsData = await db const establishmentsData = await db
.select({ .select({
name: lancamentos.name, name: transactions.name,
count: count().as("count"), count: count().as("count"),
totalAmount: sum(lancamentos.amount).as("total"), totalAmount: sum(transactions.amount).as("total"),
}) })
.from(lancamentos) .from(transactions)
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id)) .innerJoin(payers, eq(transactions.payerId, payers.id))
.leftJoin(contas, eq(lancamentos.contaId, contas.id)) .leftJoin(
financialAccounts,
eq(transactions.accountId, financialAccounts.id),
)
.where( .where(
and( and(
eq(lancamentos.userId, userId), eq(transactions.userId, userId),
gte(lancamentos.period, startPeriod), gte(transactions.period, startPeriod),
lte(lancamentos.period, currentPeriod), lte(transactions.period, currentPeriod),
eq(pagadores.role, PAGADOR_ROLE_ADMIN), eq(payers.role, PAYER_ROLE_ADMIN),
eq(lancamentos.transactionType, DESPESA), eq(transactions.transactionType, DESPESA),
ne(lancamentos.transactionType, TRANSFERENCIA), ne(transactions.transactionType, TRANSFERENCIA),
or( or(
isNull(lancamentos.note), isNull(transactions.note),
not(ilike(lancamentos.note, `${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`)), not(ilike(transactions.note, `${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`)),
), ),
or( or(
ne(lancamentos.note, INITIAL_BALANCE_NOTE), ne(transactions.note, INITIAL_BALANCE_NOTE),
isNull(contas.excludeInitialBalanceFromIncome), isNull(financialAccounts.excludeInitialBalanceFromIncome),
eq(contas.excludeInitialBalanceFromIncome, false), eq(financialAccounts.excludeInitialBalanceFromIncome, false),
), ),
), ),
) )
.groupBy(lancamentos.name) .groupBy(transactions.name)
.orderBy(desc(sql`count`)) .orderBy(desc(sql`count`))
.limit(50); .limit(50);
@@ -117,32 +125,32 @@ export async function fetchTopEstabelecimentosData(
const categoriesByEstablishment = await db const categoriesByEstablishment = await db
.select({ .select({
establishmentName: lancamentos.name, establishmentName: transactions.name,
categoriaId: lancamentos.categoriaId, categoryId: transactions.categoryId,
count: count().as("count"), count: count().as("count"),
}) })
.from(lancamentos) .from(transactions)
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id)) .innerJoin(payers, eq(transactions.payerId, payers.id))
.where( .where(
and( and(
eq(lancamentos.userId, userId), eq(transactions.userId, userId),
gte(lancamentos.period, startPeriod), gte(transactions.period, startPeriod),
lte(lancamentos.period, currentPeriod), lte(transactions.period, currentPeriod),
eq(pagadores.role, PAGADOR_ROLE_ADMIN), eq(payers.role, PAYER_ROLE_ADMIN),
eq(lancamentos.transactionType, DESPESA), eq(transactions.transactionType, DESPESA),
), ),
) )
.groupBy(lancamentos.name, lancamentos.categoriaId); .groupBy(transactions.name, transactions.categoryId);
// Fetch all category names // Fetch all category names
const allCategories = await db const allCategories = await db
.select({ .select({
id: categorias.id, id: categories.id,
name: categorias.name, name: categories.name,
icon: categorias.icon, icon: categories.icon,
}) })
.from(categorias) .from(categories)
.where(eq(categorias.userId, userId)); .where(eq(categories.userId, userId));
type CategoryInfo = { id: string; name: string; icon: string | null }; type CategoryInfo = { id: string; name: string; icon: string | null };
const categoryMap = new Map<string, CategoryInfo>( const categoryMap = new Map<string, CategoryInfo>(
@@ -161,11 +169,11 @@ export async function fetchTopEstabelecimentosData(
const estCategories = categoriesByEstablishment const estCategories = categoriesByEstablishment
.filter( .filter(
(c: CategoryByEstRow) => (c: CategoryByEstRow) =>
c.establishmentName === est.name && c.categoriaId, c.establishmentName === est.name && c.categoryId,
) )
.map((c: CategoryByEstRow) => ({ .map((c: CategoryByEstRow) => ({
name: name:
categoryMap.get(c.categoriaId as string)?.name || "Sem categoria", categoryMap.get(c.categoryId as string)?.name || "Sem categoria",
count: Number(c.count) || 0, count: Number(c.count) || 0,
})) }))
.sort( .sort(
@@ -189,43 +197,46 @@ export async function fetchTopEstabelecimentosData(
// Fetch top categories by spending // Fetch top categories by spending
const topCategoriesData = await db const topCategoriesData = await db
.select({ .select({
categoriaId: lancamentos.categoriaId, categoryId: transactions.categoryId,
totalAmount: sum(lancamentos.amount).as("total"), totalAmount: sum(transactions.amount).as("total"),
count: count().as("count"), count: count().as("count"),
}) })
.from(lancamentos) .from(transactions)
.innerJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id)) .innerJoin(payers, eq(transactions.payerId, payers.id))
.leftJoin(contas, eq(lancamentos.contaId, contas.id)) .leftJoin(
financialAccounts,
eq(transactions.accountId, financialAccounts.id),
)
.where( .where(
and( and(
eq(lancamentos.userId, userId), eq(transactions.userId, userId),
gte(lancamentos.period, startPeriod), gte(transactions.period, startPeriod),
lte(lancamentos.period, currentPeriod), lte(transactions.period, currentPeriod),
eq(pagadores.role, PAGADOR_ROLE_ADMIN), eq(payers.role, PAYER_ROLE_ADMIN),
eq(lancamentos.transactionType, DESPESA), eq(transactions.transactionType, DESPESA),
or( or(
isNull(lancamentos.note), isNull(transactions.note),
not(ilike(lancamentos.note, `${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`)), not(ilike(transactions.note, `${ACCOUNT_AUTO_INVOICE_NOTE_PREFIX}%`)),
), ),
or( or(
ne(lancamentos.note, INITIAL_BALANCE_NOTE), ne(transactions.note, INITIAL_BALANCE_NOTE),
isNull(contas.excludeInitialBalanceFromIncome), isNull(financialAccounts.excludeInitialBalanceFromIncome),
eq(contas.excludeInitialBalanceFromIncome, false), eq(financialAccounts.excludeInitialBalanceFromIncome, false),
), ),
), ),
) )
.groupBy(lancamentos.categoriaId) .groupBy(transactions.categoryId)
.orderBy(sql`total ASC`) .orderBy(sql`total ASC`)
.limit(10); .limit(10);
type TopCategoryRow = (typeof topCategoriesData)[0]; type TopCategoryRow = (typeof topCategoriesData)[0];
const topCategories: TopCategoryData[] = topCategoriesData const topCategories: TopCategoryData[] = topCategoriesData
.filter((c: TopCategoryRow) => c.categoriaId) .filter((c: TopCategoryRow) => c.categoryId)
.map((cat: TopCategoryRow) => { .map((cat: TopCategoryRow) => {
const catInfo = categoryMap.get(cat.categoriaId as string); const catInfo = categoryMap.get(cat.categoryId as string);
return { return {
id: cat.categoriaId as string, id: cat.categoryId as string,
name: catInfo?.name || "Sem categoria", name: catInfo?.name || "Sem categoria",
icon: catInfo?.icon || null, icon: catInfo?.icon || null,
totalAmount: Math.abs(safeToNumber(cat.totalAmount)), 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 { and, asc, desc, eq, inArray, isNull, or } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { import {
antecipacoesParcelas, categories,
categorias, installmentAnticipations,
lancamentos, payers,
pagadores, transactions,
} from "@/db/schema"; } from "@/db/schema";
import { import {
handleActionError, handleActionError,
@@ -47,8 +47,8 @@ const createAnticipationSchema = z.object({
.min(0, "Informe um desconto maior ou igual a zero.") .min(0, "Informe um desconto maior ou igual a zero.")
.optional() .optional()
.default(0), .default(0),
pagadorId: uuidSchema("Pagador").optional(), payerId: uuidSchema("Payer").optional(),
categoriaId: uuidSchema("Categoria").optional(), categoryId: uuidSchema("Category").optional(),
note: z.string().trim().optional(), note: z.string().trim().optional(),
}); });
@@ -72,16 +72,16 @@ export async function getEligibleInstallmentsAction(
const validatedSeriesId = uuidSchema("Série").parse(seriesId); const validatedSeriesId = uuidSchema("Série").parse(seriesId);
// Buscar todas as parcelas da série que estão elegíveis // 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( where: and(
eq(lancamentos.seriesId, validatedSeriesId), eq(transactions.seriesId, validatedSeriesId),
eq(lancamentos.userId, user.id), eq(transactions.userId, user.id),
eq(lancamentos.condition, "Parcelado"), eq(transactions.condition, "Parcelado"),
// Apenas parcelas não pagas e não antecipadas // Apenas parcelas não pagas e não antecipadas
or(eq(lancamentos.isSettled, false), isNull(lancamentos.isSettled)), or(eq(transactions.isSettled, false), isNull(transactions.isSettled)),
eq(lancamentos.isAnticipated, false), eq(transactions.isAnticipated, false),
), ),
orderBy: [asc(lancamentos.currentInstallment)], orderBy: [asc(transactions.currentInstallment)],
columns: { columns: {
id: true, id: true,
name: true, name: true,
@@ -92,8 +92,8 @@ export async function getEligibleInstallmentsAction(
currentInstallment: true, currentInstallment: true,
installmentCount: true, installmentCount: true,
paymentMethod: true, paymentMethod: true,
categoriaId: true, categoryId: true,
pagadorId: true, payerId: true,
}, },
}); });
@@ -107,8 +107,8 @@ export async function getEligibleInstallmentsAction(
currentInstallment: row.currentInstallment, currentInstallment: row.currentInstallment,
installmentCount: row.installmentCount, installmentCount: row.installmentCount,
paymentMethod: row.paymentMethod, paymentMethod: row.paymentMethod,
categoriaId: row.categoriaId, categoryId: row.categoryId,
pagadorId: row.pagadorId, payerId: row.payerId,
})); }));
return { return {
@@ -132,13 +132,13 @@ export async function createInstallmentAnticipationAction(
const data = createAnticipationSchema.parse(input); const data = createAnticipationSchema.parse(input);
// 1. Validar parcelas selecionadas // 1. Validar parcelas selecionadas
const installments = await db.query.lancamentos.findMany({ const installments = await db.query.transactions.findMany({
where: and( where: and(
inArray(lancamentos.id, data.installmentIds), inArray(transactions.id, data.installmentIds),
eq(lancamentos.userId, user.id), eq(transactions.userId, user.id),
eq(lancamentos.seriesId, data.seriesId), eq(transactions.seriesId, data.seriesId),
or(eq(lancamentos.isSettled, false), isNull(lancamentos.isSettled)), or(eq(transactions.isSettled, false), isNull(transactions.isSettled)),
eq(lancamentos.isAnticipated, false), eq(transactions.isAnticipated, false),
), ),
}); });
@@ -187,8 +187,8 @@ export async function createInstallmentAnticipationAction(
// 4. Criar lançamento e antecipação em transação // 4. Criar lançamento e antecipação em transação
await db.transaction(async (tx: typeof db) => { await db.transaction(async (tx: typeof db) => {
// 4.1. Criar o lançamento de antecipação (com desconto aplicado) // 4.1. Criar o lançamento de antecipação (com desconto aplicado)
const [newLancamento] = await tx const [newLancamento] = (await tx
.insert(lancamentos) .insert(transactions)
.values({ .values({
name: generateAnticipationDescription( name: generateAnticipationDescription(
firstInstallment.name, firstInstallment.name,
@@ -202,10 +202,10 @@ export async function createInstallmentAnticipationAction(
period: data.anticipationPeriod, period: data.anticipationPeriod,
dueDate: null, dueDate: null,
isSettled: false, isSettled: false,
pagadorId: data.pagadorId ?? firstInstallment.pagadorId, payerId: data.payerId ?? firstInstallment.payerId,
categoriaId: data.categoriaId ?? firstInstallment.categoriaId, categoryId: data.categoryId ?? firstInstallment.categoryId,
cartaoId: firstInstallment.cartaoId, cardId: firstInstallment.cardId,
contaId: firstInstallment.contaId, accountId: firstInstallment.accountId,
note: note:
data.note || data.note ||
generateAnticipationNote( generateAnticipationNote(
@@ -219,8 +219,8 @@ export async function createInstallmentAnticipationAction(
currentInstallment: inst.currentInstallment, currentInstallment: inst.currentInstallment,
installmentCount: inst.installmentCount, installmentCount: inst.installmentCount,
paymentMethod: inst.paymentMethod, paymentMethod: inst.paymentMethod,
categoriaId: inst.categoriaId, categoryId: inst.categoryId,
pagadorId: inst.pagadorId, payerId: inst.payerId,
})), })),
), ),
userId: user.id, userId: user.id,
@@ -234,11 +234,11 @@ export async function createInstallmentAnticipationAction(
anticipationId: null, anticipationId: null,
boletoPaymentDate: null, boletoPaymentDate: null,
}) })
.returning(); .returning()) as Array<typeof transactions.$inferSelect>;
// 4.2. Criar registro de antecipação // 4.2. Criar registro de antecipação
const [anticipation] = await tx const [anticipation] = (await tx
.insert(antecipacoesParcelas) .insert(installmentAnticipations)
.values({ .values({
seriesId: data.seriesId, seriesId: data.seriesId,
anticipationPeriod: data.anticipationPeriod, anticipationPeriod: data.anticipationPeriod,
@@ -247,26 +247,26 @@ export async function createInstallmentAnticipationAction(
totalAmount: formatDecimalForDbRequired(totalAmount), totalAmount: formatDecimalForDbRequired(totalAmount),
installmentCount: installments.length, installmentCount: installments.length,
discount: formatDecimalForDbRequired(discount), discount: formatDecimalForDbRequired(discount),
lancamentoId: newLancamento.id, transactionId: newLancamento.id,
pagadorId: data.pagadorId ?? firstInstallment.pagadorId, payerId: data.payerId ?? firstInstallment.payerId,
categoriaId: data.categoriaId ?? firstInstallment.categoriaId, categoryId: data.categoryId ?? firstInstallment.categoryId,
note: data.note || null, note: data.note || null,
userId: user.id, userId: user.id,
}) })
.returning(); .returning()) as Array<typeof installmentAnticipations.$inferSelect>;
// 4.3. Marcar parcelas como antecipadas e zerar seus valores // 4.3. Marcar parcelas como antecipadas e zerar seus valores
await tx await tx
.update(lancamentos) .update(transactions)
.set({ .set({
isAnticipated: true, isAnticipated: true,
anticipationId: anticipation.id, anticipationId: anticipation.id,
amount: "0", // Zera o valor para não contar em dobro 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 { return {
success: true, success: true,
@@ -296,40 +296,43 @@ export async function getInstallmentAnticipationsAction(
// Usar query builder ao invés de db.query para evitar problemas de tipagem // Usar query builder ao invés de db.query para evitar problemas de tipagem
const anticipations = await db const anticipations = await db
.select({ .select({
id: antecipacoesParcelas.id, id: installmentAnticipations.id,
seriesId: antecipacoesParcelas.seriesId, seriesId: installmentAnticipations.seriesId,
anticipationPeriod: antecipacoesParcelas.anticipationPeriod, anticipationPeriod: installmentAnticipations.anticipationPeriod,
anticipationDate: antecipacoesParcelas.anticipationDate, anticipationDate: installmentAnticipations.anticipationDate,
anticipatedInstallmentIds: anticipatedInstallmentIds:
antecipacoesParcelas.anticipatedInstallmentIds, installmentAnticipations.anticipatedInstallmentIds,
totalAmount: antecipacoesParcelas.totalAmount, totalAmount: installmentAnticipations.totalAmount,
installmentCount: antecipacoesParcelas.installmentCount, installmentCount: installmentAnticipations.installmentCount,
discount: antecipacoesParcelas.discount, discount: installmentAnticipations.discount,
lancamentoId: antecipacoesParcelas.lancamentoId, transactionId: installmentAnticipations.transactionId,
pagadorId: antecipacoesParcelas.pagadorId, payerId: installmentAnticipations.payerId,
categoriaId: antecipacoesParcelas.categoriaId, categoryId: installmentAnticipations.categoryId,
note: antecipacoesParcelas.note, note: installmentAnticipations.note,
userId: antecipacoesParcelas.userId, userId: installmentAnticipations.userId,
createdAt: antecipacoesParcelas.createdAt, createdAt: installmentAnticipations.createdAt,
// Joins // Joins
lancamento: lancamentos, transaction: transactions,
pagador: pagadores, payer: payers,
categoria: categorias, category: categories,
}) })
.from(antecipacoesParcelas) .from(installmentAnticipations)
.leftJoin( .leftJoin(
lancamentos, transactions,
eq(antecipacoesParcelas.lancamentoId, lancamentos.id), 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( .where(
and( and(
eq(antecipacoesParcelas.seriesId, validatedSeriesId), eq(installmentAnticipations.seriesId, validatedSeriesId),
eq(antecipacoesParcelas.userId, user.id), eq(installmentAnticipations.userId, user.id),
), ),
) )
.orderBy(desc(antecipacoesParcelas.createdAt)); .orderBy(desc(installmentAnticipations.createdAt));
return { return {
success: true, success: true,
@@ -358,32 +361,32 @@ export async function cancelInstallmentAnticipationAction(
// 1. Buscar antecipação usando query builder // 1. Buscar antecipação usando query builder
const anticipationRows = await tx const anticipationRows = await tx
.select({ .select({
id: antecipacoesParcelas.id, id: installmentAnticipations.id,
seriesId: antecipacoesParcelas.seriesId, seriesId: installmentAnticipations.seriesId,
anticipationPeriod: antecipacoesParcelas.anticipationPeriod, anticipationPeriod: installmentAnticipations.anticipationPeriod,
anticipationDate: antecipacoesParcelas.anticipationDate, anticipationDate: installmentAnticipations.anticipationDate,
anticipatedInstallmentIds: anticipatedInstallmentIds:
antecipacoesParcelas.anticipatedInstallmentIds, installmentAnticipations.anticipatedInstallmentIds,
totalAmount: antecipacoesParcelas.totalAmount, totalAmount: installmentAnticipations.totalAmount,
installmentCount: antecipacoesParcelas.installmentCount, installmentCount: installmentAnticipations.installmentCount,
discount: antecipacoesParcelas.discount, discount: installmentAnticipations.discount,
lancamentoId: antecipacoesParcelas.lancamentoId, transactionId: installmentAnticipations.transactionId,
pagadorId: antecipacoesParcelas.pagadorId, payerId: installmentAnticipations.payerId,
categoriaId: antecipacoesParcelas.categoriaId, categoryId: installmentAnticipations.categoryId,
note: antecipacoesParcelas.note, note: installmentAnticipations.note,
userId: antecipacoesParcelas.userId, userId: installmentAnticipations.userId,
createdAt: antecipacoesParcelas.createdAt, createdAt: installmentAnticipations.createdAt,
lancamento: lancamentos, transaction: transactions,
}) })
.from(antecipacoesParcelas) .from(installmentAnticipations)
.leftJoin( .leftJoin(
lancamentos, transactions,
eq(antecipacoesParcelas.lancamentoId, lancamentos.id), eq(installmentAnticipations.transactionId, transactions.id),
) )
.where( .where(
and( and(
eq(antecipacoesParcelas.id, data.anticipationId), eq(installmentAnticipations.id, data.anticipationId),
eq(antecipacoesParcelas.userId, user.id), eq(installmentAnticipations.userId, user.id),
), ),
) )
.limit(1); .limit(1);
@@ -395,7 +398,7 @@ export async function cancelInstallmentAnticipationAction(
} }
// 2. Verificar se o lançamento já foi pago // 2. Verificar se o lançamento já foi pago
if (anticipation.lancamento?.isSettled === true) { if (anticipation.transaction?.isSettled === true) {
throw new Error( throw new Error(
"Não é possível cancelar uma antecipação já paga. Remova o pagamento primeiro.", "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 // 4. Remover flag de antecipação e restaurar valores das parcelas
await tx await tx
.update(lancamentos) .update(transactions)
.set({ .set({
isAnticipated: false, isAnticipated: false,
anticipationId: null, anticipationId: null,
@@ -416,23 +419,23 @@ export async function cancelInstallmentAnticipationAction(
}) })
.where( .where(
inArray( inArray(
lancamentos.id, transactions.id,
anticipation.anticipatedInstallmentIds as string[], anticipation.anticipatedInstallmentIds as string[],
), ),
); );
// 5. Deletar lançamento de antecipação // 5. Deletar lançamento de antecipação
await tx await tx
.delete(lancamentos) .delete(transactions)
.where(eq(lancamentos.id, anticipation.lancamentoId)); .where(eq(transactions.id, anticipation.transactionId));
// 6. Deletar registro de antecipação // 6. Deletar registro de antecipação
await tx await tx
.delete(antecipacoesParcelas) .delete(installmentAnticipations)
.where(eq(antecipacoesParcelas.id, data.anticipationId)); .where(eq(installmentAnticipations.id, data.anticipationId));
}); });
revalidateForEntity("lancamentos"); revalidateForEntity("transactions");
return { return {
success: true, success: true,
@@ -455,15 +458,15 @@ export async function getAnticipationDetailsAction(
// Validar anticipationId // Validar anticipationId
const validatedId = uuidSchema("Antecipação").parse(anticipationId); const validatedId = uuidSchema("Antecipação").parse(anticipationId);
const anticipation = await db.query.antecipacoesParcelas.findFirst({ const anticipation = await db.query.installmentAnticipations.findFirst({
where: and( where: and(
eq(antecipacoesParcelas.id, validatedId), eq(installmentAnticipations.id, validatedId),
eq(antecipacoesParcelas.userId, user.id), eq(installmentAnticipations.userId, user.id),
), ),
with: { with: {
lancamento: true, transaction: true,
pagador: true, payer: true,
categoria: true, category: true,
}, },
}); });

View File

@@ -2,9 +2,9 @@ import type { SelectOption } from "@/features/transactions/components/types";
import { capitalize } from "@/shared/utils/string"; import { capitalize } from "@/shared/utils/string";
/** /**
* Group label for categorias * Group label for category options
*/ */
type CategoriaGroup = { type CategoryGroup = {
label: string; label: string;
options: SelectOption[]; options: SelectOption[];
}; };
@@ -24,15 +24,15 @@ function normalizeCategoryGroupLabel(value: string): string {
} }
/** /**
* Groups and sorts categoria options by their group property * Groups and sorts category options by their group property
* @param categoriaOptions - Array of categoria select options * @param categoryOptions - Array of category select options
* @returns Array of grouped and sorted categoria options * @returns Array of grouped and sorted category options
*/ */
export function groupAndSortCategorias( export function groupAndSortCategories(
categoriaOptions: SelectOption[], categoryOptions: SelectOption[],
): CategoriaGroup[] { ): CategoryGroup[] {
// Group categorias by their group property // Group category options by their group property
const groups = categoriaOptions.reduce<Record<string, SelectOption[]>>( const groups = categoryOptions.reduce<Record<string, SelectOption[]>>(
(acc, option) => { (acc, option) => {
const key = option.group ?? "Outros"; const key = option.group ?? "Outros";
if (!acc[key]) { 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[], allOptions: SelectOption[],
primaryPagadorId?: string, primaryPayerId?: string,
): SelectOption[] { ): 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", amount: "Valor",
condition: "Condição", condition: "Condição",
paymentMethod: "Forma de Pagamento", paymentMethod: "Forma de Pagamento",
categoriaName: "Categoria", categoriaName: "Category",
pagadorName: "Pagador", pagadorName: "Payer",
note: "Anotação", note: "Anotação",
contaCartao: "Conta/Cartão", contaCartao: "Conta/Cartão",
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,24 +1,24 @@
import type { import type {
LancamentoItem,
SelectOption, SelectOption,
TransactionItem,
} from "@/features/transactions/components/types"; } from "@/features/transactions/components/types";
export type CalendarEvent = export type CalendarEvent =
| { | {
id: string; id: string;
type: "lancamento"; type: "transaction";
date: string; date: string;
lancamento: LancamentoItem; transaction: TransactionItem;
} }
| { | {
id: string; id: string;
type: "boleto"; type: "boleto";
date: string; date: string;
lancamento: LancamentoItem; transaction: TransactionItem;
} }
| { | {
id: string; id: string;
type: "cartao"; type: "card";
date: string; date: string;
card: { card: {
id: string; id: string;
@@ -47,12 +47,12 @@ export type CalendarDay = {
}; };
export type CalendarFormOptions = { export type CalendarFormOptions = {
pagadorOptions: SelectOption[]; payerOptions: SelectOption[];
splitPagadorOptions: SelectOption[]; splitPayerOptions: SelectOption[];
defaultPagadorId: string | null; defaultPayerId: string | null;
contaOptions: SelectOption[]; accountOptions: SelectOption[];
cartaoOptions: SelectOption[]; cardOptions: SelectOption[];
categoriaOptions: SelectOption[]; categoryOptions: SelectOption[];
estabelecimentos: string[]; estabelecimentos: string[];
}; };