mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 19:01:47 +00:00
refactor: atualiza transacoes dashboard e relatorios
This commit is contained in:
@@ -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,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -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 -")
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 ${
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|
||||||
|
|||||||
@@ -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 => ({
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 }>();
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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() };
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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),
|
||||||
);
|
);
|
||||||
@@ -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,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>(
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user