refactor: alinha features financeiras ao novo naming

This commit is contained in:
Felipe Coutinho
2026-03-14 12:50:55 +00:00
parent ef918a3667
commit 67ad4b9d02
51 changed files with 876 additions and 898 deletions

View File

@@ -9,20 +9,20 @@ import {
fetchAccountSummary, fetchAccountSummary,
} from "@/features/accounts/statement-queries"; } from "@/features/accounts/statement-queries";
import { fetchUserPreferences } from "@/features/settings/queries"; import { fetchUserPreferences } from "@/features/settings/queries";
import { LancamentosPage as LancamentosSection } from "@/features/transactions/components/page/transactions-page"; import { TransactionsPage as LancamentosSection } 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,
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 { Button } from "@/shared/components/ui/button"; import { Button } from "@/shared/components/ui/button";
@@ -41,7 +41,7 @@ const capitalize = (value: string) =>
value.length > 0 ? value[0]?.toUpperCase().concat(value.slice(1)) : value; value.length > 0 ? value[0]?.toUpperCase().concat(value.slice(1)) : value;
export default async function Page({ params, searchParams }: PageProps) { export default async function Page({ params, searchParams }: PageProps) {
const { accountId: contaId } = await params; const { accountId } = await params;
const userId = await getUserId(); const userId = await getUserId();
const resolvedSearchParams = searchParams ? await searchParams : undefined; const resolvedSearchParams = searchParams ? await searchParams : undefined;
@@ -52,9 +52,9 @@ export default async function Page({ params, searchParams }: PageProps) {
year, year,
} = parsePeriodParam(periodoParamRaw); } = parsePeriodParam(periodoParamRaw);
const searchFilters = extractLancamentoSearchFilters(resolvedSearchParams); const searchFilters = extractTransactionSearchFilters(resolvedSearchParams);
const account = await fetchAccountData(userId, contaId); const account = await fetchAccountData(userId, accountId);
if (!account) { if (!account) {
notFound(); notFound();
@@ -67,16 +67,16 @@ export default async function Page({ params, searchParams }: PageProps) {
estabelecimentos, estabelecimentos,
userPreferences, userPreferences,
] = await Promise.all([ ] = await Promise.all([
fetchLancamentoFilterSources(userId), fetchTransactionFilterSources(userId),
loadLogoOptions(), loadLogoOptions(),
fetchAccountSummary(userId, contaId, selectedPeriod), fetchAccountSummary(userId, accountId, selectedPeriod),
fetchRecentEstablishments(userId), fetchRecentEstablishments(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,
@@ -84,9 +84,9 @@ export default async function Page({ params, searchParams }: PageProps) {
accountId: account.id, accountId: account.id,
}); });
const lancamentoRows = await fetchAccountLancamentos(filters); const transactionRows = await fetchAccountLancamentos(filters);
const lancamentosData = mapLancamentosData(lancamentoRows); const transactionData = mapTransactionsData(transactionRows);
const { openingBalance, currentBalance, totalIncomes, totalExpenses } = const { openingBalance, currentBalance, totalIncomes, totalExpenses } =
accountSummary; accountSummary;
@@ -105,18 +105,18 @@ export default async function Page({ params, searchParams }: PageProps) {
}; };
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,
limitContaId: account.id, limitContaId: account.id,
}); });
@@ -157,21 +157,21 @@ export default async function Page({ params, searchParams }: PageProps) {
<section className="flex flex-col gap-4"> <section className="flex flex-col gap-4">
<LancamentosSection <LancamentosSection
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}
allowCreate={false} allowCreate={false}
noteAsColumn={userPreferences?.extratoNoteAsColumn ?? false} noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
columnOrder={userPreferences?.lancamentosColumnOrder ?? null} columnOrder={userPreferences?.transactionsColumnOrder ?? null}
/> />
</section> </section>
</main> </main>

View File

@@ -24,7 +24,7 @@ export default function OrcamentosLoading() {
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: 6 }).map((_, i) => ( {Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="rounded-2xl border p-6 space-y-4"> <div key={i} className="rounded-2xl border p-6 space-y-4">
{/* Categoria com ícone */} {/* Category com ícone */}
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Skeleton className="size-10 rounded-2xl bg-foreground/10" /> <Skeleton className="size-10 rounded-2xl bg-foreground/10" />
<div className="flex-1 space-y-2"> <div className="flex-1 space-y-2">

View File

@@ -1,29 +1,29 @@
import { RiPencilLine } from "@remixicon/react"; import { RiPencilLine } from "@remixicon/react";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import type { Conta } from "@/db/schema"; import type { FinancialAccount } from "@/db/schema";
import { CardDialog } from "@/features/cards/components/card-dialog"; import { CardDialog } from "@/features/cards/components/card-dialog";
import type { Card } from "@/features/cards/components/types"; import type { Card } from "@/features/cards/components/types";
import { InvoiceSummaryCard } from "@/features/invoices/components/invoice-summary-card"; import { InvoiceSummaryCard } from "@/features/invoices/components/invoice-summary-card";
import { import {
fetchCardData, fetchCardData,
fetchCardLancamentos, fetchCardTransactions,
fetchInvoiceData, fetchInvoiceData,
} from "@/features/invoices/queries"; } from "@/features/invoices/queries";
import { fetchUserPreferences } from "@/features/settings/queries"; import { fetchUserPreferences } from "@/features/settings/queries";
import { LancamentosPage as LancamentosSection } from "@/features/transactions/components/page/transactions-page"; import { TransactionsPage as LancamentosSection } from "@/features/transactions/components/page/transactions-page";
import { import {
buildLancamentoWhere,
buildOptionSets, buildOptionSets,
buildSluggedFilters, buildSluggedFilters,
buildSlugMaps, buildSlugMaps,
extractLancamentoSearchFilters, buildTransactionWhere,
extractTransactionSearchFilters,
getSingleParam, getSingleParam,
mapLancamentosData, mapTransactionsData,
type ResolvedSearchParams, type ResolvedSearchParams,
} 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 { Button } from "@/shared/components/ui/button"; import { Button } from "@/shared/components/ui/button";
@@ -39,7 +39,7 @@ type PageProps = {
}; };
export default async function Page({ params, searchParams }: PageProps) { export default async function Page({ params, searchParams }: PageProps) {
const { cardId: cartaoId } = await params; const { cardId } = await params;
const userId = await getUserId(); const userId = await getUserId();
const resolvedSearchParams = searchParams ? await searchParams : undefined; const resolvedSearchParams = searchParams ? await searchParams : undefined;
@@ -50,9 +50,9 @@ export default async function Page({ params, searchParams }: PageProps) {
year, year,
} = parsePeriodParam(periodoParamRaw); } = parsePeriodParam(periodoParamRaw);
const searchFilters = extractLancamentoSearchFilters(resolvedSearchParams); const searchFilters = extractTransactionSearchFilters(resolvedSearchParams);
const card = await fetchCardData(userId, cartaoId); const card = await fetchCardData(userId, cardId);
if (!card) { if (!card) {
notFound(); notFound();
@@ -65,16 +65,16 @@ export default async function Page({ params, searchParams }: PageProps) {
estabelecimentos, estabelecimentos,
userPreferences, userPreferences,
] = await Promise.all([ ] = await Promise.all([
fetchLancamentoFilterSources(userId), fetchTransactionFilterSources(userId),
loadLogoOptions(), loadLogoOptions(),
fetchInvoiceData(userId, cartaoId, selectedPeriod), fetchInvoiceData(userId, cardId, selectedPeriod),
fetchRecentEstablishments(userId), fetchRecentEstablishments(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,
@@ -82,35 +82,39 @@ export default async function Page({ params, searchParams }: PageProps) {
cardId: card.id, cardId: card.id,
}); });
const lancamentoRows = await fetchCardLancamentos(filters); const transactionRows = await fetchCardTransactions(filters);
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,
limitCartaoId: card.id, limitCartaoId: card.id,
}); });
const accountOptions = filterSources.contaRows.map((conta: Conta) => ({ const cardDialogAccounts = filterSources.accountRows.map(
id: conta.id, (financialAccount: FinancialAccount) => ({
name: conta.name ?? "Conta", id: financialAccount.id,
logo: conta.logo ?? null, name: financialAccount.name ?? "FinancialAccount",
})); logo: financialAccount.logo ?? null,
}),
);
const contaName = const accountName =
filterSources.contaRows.find((conta: Conta) => conta.id === card.contaId) filterSources.accountRows.find(
?.name ?? "Conta"; (financialAccount: FinancialAccount) =>
financialAccount.id === card.accountId,
)?.name ?? "FinancialAccount";
const cardDialogData: Card = { const cardDialogData: Card = {
id: card.id, id: card.id,
@@ -125,9 +129,9 @@ export default async function Page({ params, searchParams }: PageProps) {
card.limit !== null && card.limit !== undefined card.limit !== null && card.limit !== undefined
? Number(card.limit) ? Number(card.limit)
: null, : null,
contaId: card.contaId, accountId: card.accountId,
contaName, accountName,
limitInUse: null, limitInUse: 0,
limitAvailable: null, limitAvailable: null,
}; };
@@ -145,7 +149,7 @@ export default async function Page({ params, searchParams }: PageProps) {
<section className="flex flex-col gap-4"> <section className="flex flex-col gap-4">
<InvoiceSummaryCard <InvoiceSummaryCard
cartaoId={card.id} cardId={card.id}
period={selectedPeriod} period={selectedPeriod}
cardName={card.name} cardName={card.name}
cardBrand={card.brand ?? null} cardBrand={card.brand ?? null}
@@ -163,7 +167,7 @@ export default async function Page({ params, searchParams }: PageProps) {
mode="update" mode="update"
card={cardDialogData} card={cardDialogData}
logoOptions={logoOptions} logoOptions={logoOptions}
accounts={accountOptions} accounts={cardDialogAccounts}
trigger={ trigger={
<Button <Button
type="button" type="button"
@@ -183,24 +187,24 @@ export default async function Page({ params, searchParams }: PageProps) {
<section className="flex flex-col gap-4"> <section className="flex flex-col gap-4">
<LancamentosSection <LancamentosSection
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}
allowCreate allowCreate
noteAsColumn={userPreferences?.extratoNoteAsColumn ?? false} noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
columnOrder={userPreferences?.lancamentosColumnOrder ?? null} columnOrder={userPreferences?.transactionsColumnOrder ?? null}
defaultCartaoId={card.id} defaultCardId={card.id}
defaultPaymentMethod="Cartão de crédito" defaultPaymentMethod="Cartão de crédito"
lockCartaoSelection lockCardSelection
lockPaymentMethod lockPaymentMethod
/> />
</section> </section>

View File

@@ -2,14 +2,14 @@ import { notFound } from "next/navigation";
import { CategoryDetailHeader } from "@/features/categories/components/category-detail-header"; import { CategoryDetailHeader } from "@/features/categories/components/category-detail-header";
import { fetchCategoryDetails } from "@/features/dashboard/categories/category-details-queries"; import { fetchCategoryDetails } from "@/features/dashboard/categories/category-details-queries";
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 {
buildOptionSets, buildOptionSets,
buildSluggedFilters, buildSluggedFilters,
} 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 { getUserId } from "@/shared/lib/auth/server"; import { getUserId } from "@/shared/lib/auth/server";
@@ -42,7 +42,7 @@ export default async function Page({ params, searchParams }: PageProps) {
const [detail, filterSources, estabelecimentos, userPreferences] = const [detail, filterSources, estabelecimentos, userPreferences] =
await Promise.all([ await Promise.all([
fetchCategoryDetails(userId, categoryId, selectedPeriod), fetchCategoryDetails(userId, categoryId, selectedPeriod),
fetchLancamentoFilterSources(userId), fetchTransactionFilterSources(userId),
fetchRecentEstablishments(userId), fetchRecentEstablishments(userId),
fetchUserPreferences(userId), fetchUserPreferences(userId),
]); ]);
@@ -53,18 +53,18 @@ export default async function Page({ params, searchParams }: PageProps) {
const sluggedFilters = buildSluggedFilters(filterSources); const sluggedFilters = buildSluggedFilters(filterSources);
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,
}); });
const currentPeriodLabel = displayPeriod(detail.period); const currentPeriodLabel = displayPeriod(detail.period);
@@ -82,23 +82,23 @@ export default async function Page({ params, searchParams }: PageProps) {
percentageChange={detail.percentageChange} percentageChange={detail.percentageChange}
transactionCount={detail.transactions.length} transactionCount={detail.transactions.length}
/> />
<LancamentosPage <TransactionsPage
currentUserId={userId} currentUserId={userId}
lancamentos={detail.transactions} transactions={detail.transactions}
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={detail.period} selectedPeriod={detail.period}
estabelecimentos={estabelecimentos} estabelecimentos={estabelecimentos}
allowCreate={true} allowCreate={true}
noteAsColumn={userPreferences?.extratoNoteAsColumn ?? false} noteAsColumn={userPreferences?.statementNoteAsColumn ?? false}
columnOrder={userPreferences?.lancamentosColumnOrder ?? null} columnOrder={userPreferences?.transactionsColumnOrder ?? null}
/> />
</main> </main>
); );

View File

@@ -24,12 +24,12 @@ export default async function Page() {
pendingItems={pendingItems} pendingItems={pendingItems}
processedItems={processedItems} processedItems={processedItems}
discardedItems={discardedItems} discardedItems={discardedItems}
pagadorOptions={dialogData.pagadorOptions} payerOptions={dialogData.payerOptions}
splitPagadorOptions={dialogData.splitPagadorOptions} splitPayerOptions={dialogData.splitPayerOptions}
defaultPagadorId={dialogData.defaultPagadorId} defaultPayerId={dialogData.defaultPayerId}
contaOptions={dialogData.contaOptions} accountOptions={dialogData.accountOptions}
cartaoOptions={dialogData.cartaoOptions} cardOptions={dialogData.cardOptions}
categoriaOptions={dialogData.categoriaOptions} categoryOptions={dialogData.categoryOptions}
estabelecimentos={dialogData.estabelecimentos} estabelecimentos={dialogData.estabelecimentos}
appLogoMap={appLogoMap} appLogoMap={appLogoMap}
/> />

View File

@@ -9,7 +9,7 @@ import { PreferencesForm } from "@/features/settings/components/preferences-form
import { UpdateEmailForm } from "@/features/settings/components/update-email-form"; import { UpdateEmailForm } from "@/features/settings/components/update-email-form";
import { UpdateNameForm } from "@/features/settings/components/update-name-form"; import { UpdateNameForm } from "@/features/settings/components/update-name-form";
import { UpdatePasswordForm } from "@/features/settings/components/update-password-form"; import { UpdatePasswordForm } from "@/features/settings/components/update-password-form";
import { fetchAjustesPageData } from "@/features/settings/queries"; import { fetchSettingsPageData } from "@/features/settings/queries";
import { Card } from "@/shared/components/ui/card"; import { Card } from "@/shared/components/ui/card";
import { import {
Tabs, Tabs,
@@ -32,7 +32,7 @@ export default async function Page() {
const userEmail = session.user.email || ""; const userEmail = session.user.email || "";
const { authProvider, userPreferences, userApiTokens } = const { authProvider, userPreferences, userApiTokens } =
await fetchAjustesPageData(session.user.id); await fetchSettingsPageData(session.user.id);
return ( return (
<div className="w-full"> <div className="w-full">
@@ -71,11 +71,11 @@ export default async function Page() {
</p> </p>
</div> </div>
<PreferencesForm <PreferencesForm
extratoNoteAsColumn={ statementNoteAsColumn={
userPreferences?.extratoNoteAsColumn ?? false userPreferences?.statementNoteAsColumn ?? false
} }
lancamentosColumnOrder={ transactionsColumnOrder={
userPreferences?.lancamentosColumnOrder ?? null userPreferences?.transactionsColumnOrder ?? null
} }
/> />
</div> </div>

View File

@@ -2,7 +2,12 @@
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { categorias, contas, lancamentos, pagadores } from "@/db/schema"; import {
categories,
financialAccounts,
payers,
transactions,
} from "@/db/schema";
import { import {
INITIAL_BALANCE_CATEGORY_NAME, INITIAL_BALANCE_CATEGORY_NAME,
INITIAL_BALANCE_CONDITION, INITIAL_BALANCE_CONDITION,
@@ -17,7 +22,7 @@ import {
} from "@/shared/lib/actions/helpers"; } from "@/shared/lib/actions/helpers";
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 { noteSchema, uuidSchema } from "@/shared/lib/schemas/common"; import { noteSchema, uuidSchema } from "@/shared/lib/schemas/common";
import { import {
TRANSFER_CATEGORY_NAME, TRANSFER_CATEGORY_NAME,
@@ -67,10 +72,10 @@ const accountBaseSchema = z.object({
const createAccountSchema = accountBaseSchema; const createAccountSchema = accountBaseSchema;
const updateAccountSchema = accountBaseSchema.extend({ const updateAccountSchema = accountBaseSchema.extend({
id: uuidSchema("Conta"), id: uuidSchema("FinancialAccount"),
}); });
const deleteAccountSchema = z.object({ const deleteAccountSchema = z.object({
id: uuidSchema("Conta"), id: uuidSchema("FinancialAccount"),
}); });
type AccountCreateInput = z.infer<typeof createAccountSchema>; type AccountCreateInput = z.infer<typeof createAccountSchema>;
@@ -91,7 +96,7 @@ export async function createAccountAction(
await db.transaction(async (tx: typeof db) => { await db.transaction(async (tx: typeof db) => {
const [createdAccount] = await tx const [createdAccount] = await tx
.insert(contas) .insert(financialAccounts)
.values({ .values({
name: data.name, name: data.name,
accountType: data.accountType, accountType: data.accountType,
@@ -103,7 +108,7 @@ export async function createAccountAction(
excludeInitialBalanceFromIncome: data.excludeInitialBalanceFromIncome, excludeInitialBalanceFromIncome: data.excludeInitialBalanceFromIncome,
userId: user.id, userId: user.id,
}) })
.returning({ id: contas.id, name: contas.name }); .returning({ id: financialAccounts.id, name: financialAccounts.name });
if (!createdAccount) { if (!createdAccount) {
throw new Error("Não foi possível criar a conta."); throw new Error("Não foi possível criar a conta.");
@@ -114,37 +119,37 @@ export async function createAccountAction(
} }
const [category, adminPagador] = await Promise.all([ const [category, adminPagador] = await Promise.all([
tx.query.categorias.findFirst({ tx.query.categories.findFirst({
columns: { id: true }, columns: { id: true },
where: and( where: and(
eq(categorias.userId, user.id), eq(categories.userId, user.id),
eq(categorias.name, INITIAL_BALANCE_CATEGORY_NAME), eq(categories.name, INITIAL_BALANCE_CATEGORY_NAME),
), ),
}), }),
tx.query.pagadores.findFirst({ tx.query.payers.findFirst({
columns: { id: true }, columns: { id: true },
where: and( where: and(
eq(pagadores.userId, user.id), eq(payers.userId, user.id),
eq(pagadores.role, PAGADOR_ROLE_ADMIN), eq(payers.role, PAYER_ROLE_ADMIN),
), ),
}), }),
]); ]);
if (!category) { if (!category) {
throw new Error( throw new Error(
'Categoria "Saldo inicial" não encontrada. Crie-a antes de definir um saldo inicial.', 'Category "Saldo inicial" não encontrada. Crie-a antes de definir um saldo inicial.',
); );
} }
if (!adminPagador) { if (!adminPagador) {
throw new Error( throw new Error(
"Pagador com papel administrador não encontrado. Crie um pagador admin antes de definir um saldo inicial.", "Payer com papel administrador não encontrado. Crie um pagador admin antes de definir um saldo inicial.",
); );
} }
const { date, period } = getTodayInfo(); const { date, period } = getTodayInfo();
await tx.insert(lancamentos).values({ await tx.insert(transactions).values({
condition: INITIAL_BALANCE_CONDITION, condition: INITIAL_BALANCE_CONDITION,
name: `Saldo inicial - ${createdAccount.name}`, name: `Saldo inicial - ${createdAccount.name}`,
paymentMethod: INITIAL_BALANCE_PAYMENT_METHOD, paymentMethod: INITIAL_BALANCE_PAYMENT_METHOD,
@@ -155,17 +160,17 @@ export async function createAccountAction(
period, period,
isSettled: true, isSettled: true,
userId: user.id, userId: user.id,
contaId: createdAccount.id, accountId: createdAccount.id,
categoriaId: category.id, categoryId: category.id,
pagadorId: adminPagador.id, payerId: adminPagador.id,
}); });
}); });
revalidateForEntity("contas"); revalidateForEntity("accounts");
return { return {
success: true, success: true,
message: "Conta criada com sucesso.", message: "FinancialAccount criada com sucesso.",
}; };
} catch (error) { } catch (error) {
return handleActionError(error); return handleActionError(error);
@@ -182,7 +187,7 @@ export async function updateAccountAction(
const logoFile = normalizeFilePath(data.logo); const logoFile = normalizeFilePath(data.logo);
const [updated] = await db const [updated] = await db
.update(contas) .update(financialAccounts)
.set({ .set({
name: data.name, name: data.name,
accountType: data.accountType, accountType: data.accountType,
@@ -193,21 +198,26 @@ export async function updateAccountAction(
excludeFromBalance: data.excludeFromBalance, excludeFromBalance: data.excludeFromBalance,
excludeInitialBalanceFromIncome: data.excludeInitialBalanceFromIncome, excludeInitialBalanceFromIncome: data.excludeInitialBalanceFromIncome,
}) })
.where(and(eq(contas.id, data.id), eq(contas.userId, user.id))) .where(
and(
eq(financialAccounts.id, data.id),
eq(financialAccounts.userId, user.id),
),
)
.returning(); .returning();
if (!updated) { if (!updated) {
return { return {
success: false, success: false,
error: "Conta não encontrada.", error: "FinancialAccount não encontrada.",
}; };
} }
revalidateForEntity("contas"); revalidateForEntity("accounts");
return { return {
success: true, success: true,
message: "Conta atualizada com sucesso.", message: "FinancialAccount atualizada com sucesso.",
}; };
} catch (error) { } catch (error) {
return handleActionError(error); return handleActionError(error);
@@ -222,22 +232,27 @@ export async function deleteAccountAction(
const data = deleteAccountSchema.parse(input); const data = deleteAccountSchema.parse(input);
const [deleted] = await db const [deleted] = await db
.delete(contas) .delete(financialAccounts)
.where(and(eq(contas.id, data.id), eq(contas.userId, user.id))) .where(
.returning({ id: contas.id }); and(
eq(financialAccounts.id, data.id),
eq(financialAccounts.userId, user.id),
),
)
.returning({ id: financialAccounts.id });
if (!deleted) { if (!deleted) {
return { return {
success: false, success: false,
error: "Conta não encontrada.", error: "FinancialAccount não encontrada.",
}; };
} }
revalidateForEntity("contas"); revalidateForEntity("accounts");
return { return {
success: true, success: true,
message: "Conta removida com sucesso.", message: "FinancialAccount removida com sucesso.",
}; };
} catch (error) { } catch (error) {
return handleActionError(error); return handleActionError(error);
@@ -246,8 +261,8 @@ export async function deleteAccountAction(
// Transfer between accounts // Transfer between accounts
const transferSchema = z.object({ const transferSchema = z.object({
fromAccountId: uuidSchema("Conta de origem"), fromAccountId: uuidSchema("FinancialAccount de origem"),
toAccountId: uuidSchema("Conta de destino"), toAccountId: uuidSchema("FinancialAccount de destino"),
amount: z amount: z
.string() .string()
.trim() .trim()
@@ -265,7 +280,7 @@ const transferSchema = z.object({
.min(1, "Informe o período."), .min(1, "Informe o período."),
}); });
type TransferInput = z.infer<typeof transferSchema>; type TransferInput = z.input<typeof transferSchema>;
export async function transferBetweenAccountsAction( export async function transferBetweenAccountsAction(
input: TransferInput, input: TransferInput,
@@ -288,64 +303,64 @@ export async function transferBetweenAccountsAction(
await db.transaction(async (tx: typeof db) => { await db.transaction(async (tx: typeof db) => {
// Verify both accounts exist and belong to the user // Verify both accounts exist and belong to the user
const [fromAccount, toAccount] = await Promise.all([ const [fromAccount, toAccount] = await Promise.all([
tx.query.contas.findFirst({ tx.query.financialAccounts.findFirst({
columns: { id: true, name: true }, columns: { id: true, name: true },
where: and( where: and(
eq(contas.id, data.fromAccountId), eq(financialAccounts.id, data.fromAccountId),
eq(contas.userId, user.id), eq(financialAccounts.userId, user.id),
), ),
}), }),
tx.query.contas.findFirst({ tx.query.financialAccounts.findFirst({
columns: { id: true, name: true }, columns: { id: true, name: true },
where: and( where: and(
eq(contas.id, data.toAccountId), eq(financialAccounts.id, data.toAccountId),
eq(contas.userId, user.id), eq(financialAccounts.userId, user.id),
), ),
}), }),
]); ]);
if (!fromAccount) { if (!fromAccount) {
throw new Error("Conta de origem não encontrada."); throw new Error("FinancialAccount de origem não encontrada.");
} }
if (!toAccount) { if (!toAccount) {
throw new Error("Conta de destino não encontrada."); throw new Error("FinancialAccount de destino não encontrada.");
} }
// Get the transfer category // Get the transfer category
const transferCategory = await tx.query.categorias.findFirst({ const transferCategory = await tx.query.categories.findFirst({
columns: { id: true }, columns: { id: true },
where: and( where: and(
eq(categorias.userId, user.id), eq(categories.userId, user.id),
eq(categorias.name, TRANSFER_CATEGORY_NAME), eq(categories.name, TRANSFER_CATEGORY_NAME),
), ),
}); });
if (!transferCategory) { if (!transferCategory) {
throw new Error( throw new Error(
`Categoria "${TRANSFER_CATEGORY_NAME}" não encontrada. Por favor, crie esta categoria antes de fazer transferências.`, `Category "${TRANSFER_CATEGORY_NAME}" não encontrada. Por favor, crie esta categoria antes de fazer transferências.`,
); );
} }
// Get the admin payer // Get the admin payer
const adminPagador = await tx.query.pagadores.findFirst({ const adminPagador = await tx.query.payers.findFirst({
columns: { id: true }, columns: { id: true },
where: and( where: and(
eq(pagadores.userId, user.id), eq(payers.userId, user.id),
eq(pagadores.role, PAGADOR_ROLE_ADMIN), eq(payers.role, PAYER_ROLE_ADMIN),
), ),
}); });
if (!adminPagador) { if (!adminPagador) {
throw new Error( throw new Error(
"Pagador administrador não encontrado. Por favor, crie um pagador admin.", "Payer administrador não encontrado. Por favor, crie um pagador admin.",
); );
} }
const transferNote = `de ${fromAccount.name} -> ${toAccount.name}`; const transferNote = `de ${fromAccount.name} -> ${toAccount.name}`;
// Create outgoing transaction (transfer from source account) // Create outgoing transaction (transfer from source account)
await tx.insert(lancamentos).values({ await tx.insert(transactions).values({
condition: TRANSFER_CONDITION, condition: TRANSFER_CONDITION,
name: TRANSFER_ESTABLISHMENT_SAIDA, name: TRANSFER_ESTABLISHMENT_SAIDA,
paymentMethod: TRANSFER_PAYMENT_METHOD, paymentMethod: TRANSFER_PAYMENT_METHOD,
@@ -356,14 +371,14 @@ export async function transferBetweenAccountsAction(
period: data.period, period: data.period,
isSettled: true, isSettled: true,
userId: user.id, userId: user.id,
contaId: fromAccount.id, accountId: fromAccount.id,
categoriaId: transferCategory.id, categoryId: transferCategory.id,
pagadorId: adminPagador.id, payerId: adminPagador.id,
transferId, transferId,
}); });
// Create incoming transaction (transfer to destination account) // Create incoming transaction (transfer to destination account)
await tx.insert(lancamentos).values({ await tx.insert(transactions).values({
condition: TRANSFER_CONDITION, condition: TRANSFER_CONDITION,
name: TRANSFER_ESTABLISHMENT_ENTRADA, name: TRANSFER_ESTABLISHMENT_ENTRADA,
paymentMethod: TRANSFER_PAYMENT_METHOD, paymentMethod: TRANSFER_PAYMENT_METHOD,
@@ -374,15 +389,15 @@ export async function transferBetweenAccountsAction(
period: data.period, period: data.period,
isSettled: true, isSettled: true,
userId: user.id, userId: user.id,
contaId: toAccount.id, accountId: toAccount.id,
categoriaId: transferCategory.id, categoryId: transferCategory.id,
pagadorId: adminPagador.id, payerId: adminPagador.id,
transferId, transferId,
}); });
}); });
revalidateForEntity("contas"); revalidateForEntity("accounts");
revalidateForEntity("lancamentos"); revalidateForEntity("transactions");
return { return {
success: true, success: true,

View File

@@ -33,10 +33,10 @@ import { AccountFormFields } from "./account-form-fields";
import type { Account, AccountFormValues } from "./types"; import type { Account, AccountFormValues } from "./types";
const DEFAULT_ACCOUNT_TYPES = [ const DEFAULT_ACCOUNT_TYPES = [
"Conta Corrente", "FinancialAccount Corrente",
"Conta Poupança", "FinancialAccount Poupança",
"Carteira Digital", "Carteira Digital",
"Conta Investimento", "FinancialAccount Investimento",
"Pré-Pago | VR/VA", "Pré-Pago | VR/VA",
] as const; ] as const;
@@ -167,7 +167,7 @@ export function AccountDialog({
const accountId = account?.id; const accountId = account?.id;
if (mode === "update" && !accountId) { if (mode === "update" && !accountId) {
const message = "Conta inválida."; const message = "FinancialAccount inválida.";
setErrorMessage(message); setErrorMessage(message);
toast.error(message); toast.error(message);
return; return;

View File

@@ -110,10 +110,7 @@ export function AccountFormFields({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Checkbox <Checkbox
id="exclude-from-balance" id="exclude-from-balance"
checked={ checked={Boolean(values.excludeFromBalance)}
values.excludeFromBalance === true ||
values.excludeFromBalance === "true"
}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
onChange("excludeFromBalance", checked ? "true" : "false") onChange("excludeFromBalance", checked ? "true" : "false")
} }
@@ -130,10 +127,7 @@ export function AccountFormFields({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Checkbox <Checkbox
id="exclude-initial-balance-from-income" id="exclude-initial-balance-from-income"
checked={ checked={Boolean(values.excludeInitialBalanceFromIncome)}
values.excludeInitialBalanceFromIncome === true ||
values.excludeInitialBalanceFromIncome === "true"
}
onCheckedChange={(checked) => onCheckedChange={(checked) =>
onChange( onChange(
"excludeInitialBalanceFromIncome", "excludeInitialBalanceFromIncome",

View File

@@ -142,7 +142,7 @@ export function AccountStatementCard({
/> />
</div> </div>
{/* Informações da Conta */} {/* Informações da FinancialAccount */}
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 pt-2 border-t border-border/60 border-dashed"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 pt-2 border-t border-border/60 border-dashed">
<DetailItem <DetailItem
label="Tipo da conta" label="Tipo da conta"

View File

@@ -115,9 +115,7 @@ export function AccountsPage({
<EmptyState <EmptyState
media={<RiBankLine className="size-6 text-primary" />} media={<RiBankLine className="size-6 text-primary" />}
title={ title={
isArchived isArchived ? "Nenhuma conta archived" : "Nenhuma conta cadastrada"
? "Nenhuma conta arquivada"
: "Nenhuma conta cadastrada"
} }
description={ description={
isArchived isArchived

View File

@@ -4,7 +4,7 @@ import { useState, useTransition } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { transferBetweenAccountsAction } from "@/features/accounts/actions"; import { transferBetweenAccountsAction } from "@/features/accounts/actions";
import type { AccountData } from "@/features/accounts/queries"; import type { AccountData } from "@/features/accounts/queries";
import { ContaCartaoSelectContent } from "@/features/transactions/components/select-items"; import { AccountCardSelectContent } from "@/features/transactions/components/select-items";
import { PeriodPicker } from "@/shared/components/period-picker"; import { PeriodPicker } from "@/shared/components/period-picker";
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";
@@ -157,12 +157,12 @@ export function TransferDialog({
</div> </div>
<div className="flex flex-col gap-2 sm:col-span-2"> <div className="flex flex-col gap-2 sm:col-span-2">
<Label htmlFor="from-account">Conta de origem</Label> <Label htmlFor="from-account">FinancialAccount de origem</Label>
<Select value={fromAccountId} disabled> <Select value={fromAccountId} disabled>
<SelectTrigger id="from-account" className="w-full"> <SelectTrigger id="from-account" className="w-full">
<SelectValue> <SelectValue>
{fromAccount && ( {fromAccount && (
<ContaCartaoSelectContent <AccountCardSelectContent
label={fromAccount.name} label={fromAccount.name}
logo={fromAccount.logo} logo={fromAccount.logo}
isCartao={false} isCartao={false}
@@ -173,7 +173,7 @@ export function TransferDialog({
<SelectContent> <SelectContent>
{fromAccount && ( {fromAccount && (
<SelectItem value={fromAccount.id}> <SelectItem value={fromAccount.id}>
<ContaCartaoSelectContent <AccountCardSelectContent
label={fromAccount.name} label={fromAccount.name}
logo={fromAccount.logo} logo={fromAccount.logo}
isCartao={false} isCartao={false}
@@ -185,7 +185,7 @@ export function TransferDialog({
</div> </div>
<div className="flex flex-col gap-2 sm:col-span-2"> <div className="flex flex-col gap-2 sm:col-span-2">
<Label htmlFor="to-account">Conta de destino</Label> <Label htmlFor="to-account">FinancialAccount de destino</Label>
{availableAccounts.length === 0 ? ( {availableAccounts.length === 0 ? (
<div className="rounded-md border border-border bg-muted p-3 text-sm text-muted-foreground"> <div className="rounded-md border border-border bg-muted p-3 text-sm text-muted-foreground">
É necessário ter mais de uma conta cadastrada para realizar É necessário ter mais de uma conta cadastrada para realizar
@@ -201,7 +201,7 @@ export function TransferDialog({
(acc) => acc.id === toAccountId, (acc) => acc.id === toAccountId,
); );
return selectedAccount ? ( return selectedAccount ? (
<ContaCartaoSelectContent <AccountCardSelectContent
label={selectedAccount.name} label={selectedAccount.name}
logo={selectedAccount.logo} logo={selectedAccount.logo}
isCartao={false} isCartao={false}
@@ -213,7 +213,7 @@ export function TransferDialog({
<SelectContent className="w-full"> <SelectContent className="w-full">
{availableAccounts.map((account) => ( {availableAccounts.map((account) => (
<SelectItem key={account.id} value={account.id}> <SelectItem key={account.id} value={account.id}>
<ContaCartaoSelectContent <AccountCardSelectContent
label={account.name} label={account.name}
logo={account.logo} logo={account.logo}
isCartao={false} isCartao={false}

View File

@@ -1,9 +1,9 @@
import { and, eq, ilike, not, sql } from "drizzle-orm"; import { and, eq, ilike, not, 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 { loadLogoOptions } from "@/shared/lib/logo/options"; import { loadLogoOptions } from "@/shared/lib/logo/options";
import { PAGADOR_ROLE_ADMIN } from "@/shared/lib/payers/constants"; import { PAYER_ROLE_ADMIN } from "@/shared/lib/payers/constants";
export type AccountData = { export type AccountData = {
id: string; id: string;
@@ -20,58 +20,59 @@ export type AccountData = {
export async function fetchAccountsForUser( export async function fetchAccountsForUser(
userId: string, userId: string,
): Promise<{ accounts: AccountData[]; logoOptions: LogoOption[] }> { ): Promise<{ accounts: AccountData[]; logoOptions: string[] }> {
const [accountRows, logoOptions] = await Promise.all([ const [accountRows, logoOptions] = await Promise.all([
db 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,
note: contas.note, note: financialAccounts.note,
logo: contas.logo, logo: financialAccounts.logo,
initialBalance: contas.initialBalance, initialBalance: financialAccounts.initialBalance,
excludeFromBalance: contas.excludeFromBalance, excludeFromBalance: financialAccounts.excludeFromBalance,
excludeInitialBalanceFromIncome: contas.excludeInitialBalanceFromIncome, excludeInitialBalanceFromIncome:
financialAccounts.excludeInitialBalanceFromIncome,
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),
not(ilike(contas.status, "inativa")), not(ilike(financialAccounts.status, "inativa")),
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.note, financialAccounts.note,
contas.logo, financialAccounts.logo,
contas.initialBalance, financialAccounts.initialBalance,
contas.excludeFromBalance, financialAccounts.excludeFromBalance,
contas.excludeInitialBalanceFromIncome, financialAccounts.excludeInitialBalanceFromIncome,
), ),
loadLogoOptions(), loadLogoOptions(),
]); ]);
@@ -94,60 +95,61 @@ export async function fetchAccountsForUser(
return { accounts, logoOptions }; return { accounts, logoOptions };
} }
export async function fetchInativosForUser( export async function fetchInactiveForUser(
userId: string, userId: string,
): Promise<{ accounts: AccountData[]; logoOptions: LogoOption[] }> { ): Promise<{ accounts: AccountData[]; logoOptions: string[] }> {
const [accountRows, logoOptions] = await Promise.all([ const [accountRows, logoOptions] = await Promise.all([
db 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,
note: contas.note, note: financialAccounts.note,
logo: contas.logo, logo: financialAccounts.logo,
initialBalance: contas.initialBalance, initialBalance: financialAccounts.initialBalance,
excludeFromBalance: contas.excludeFromBalance, excludeFromBalance: financialAccounts.excludeFromBalance,
excludeInitialBalanceFromIncome: contas.excludeInitialBalanceFromIncome, excludeInitialBalanceFromIncome:
financialAccounts.excludeInitialBalanceFromIncome,
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),
ilike(contas.status, "inativa"), ilike(financialAccounts.status, "inativa"),
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.note, financialAccounts.note,
contas.logo, financialAccounts.logo,
contas.initialBalance, financialAccounts.initialBalance,
contas.excludeFromBalance, financialAccounts.excludeFromBalance,
contas.excludeInitialBalanceFromIncome, financialAccounts.excludeInitialBalanceFromIncome,
), ),
loadLogoOptions(), loadLogoOptions(),
]); ]);
@@ -173,11 +175,11 @@ export async function fetchInativosForUser(
export async function fetchAllAccountsForUser(userId: string): Promise<{ export async function fetchAllAccountsForUser(userId: string): Promise<{
activeAccounts: AccountData[]; activeAccounts: AccountData[];
archivedAccounts: AccountData[]; archivedAccounts: AccountData[];
logoOptions: LogoOption[]; logoOptions: string[];
}> { }> {
const [activeData, archivedData] = await Promise.all([ const [activeData, archivedData] = await Promise.all([
fetchAccountsForUser(userId), fetchAccountsForUser(userId),
fetchInativosForUser(userId), fetchInactiveForUser(userId),
]); ]);
return { return {

View File

@@ -1,8 +1,8 @@
import { and, desc, eq, lt, type SQL, sql } from "drizzle-orm"; import { and, desc, eq, lt, type SQL, 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";
export type AccountSummaryData = { export type AccountSummaryData = {
openingBalance: number; openingBalance: number;
@@ -11,8 +11,8 @@ export type AccountSummaryData = {
totalExpenses: number; totalExpenses: number;
}; };
export async function fetchAccountData(userId: string, contaId: string) { export async function fetchAccountData(userId: string, accountId: string) {
const account = await db.query.contas.findFirst({ const account = await db.query.financialAccounts.findFirst({
columns: { columns: {
id: true, id: true,
name: true, name: true,
@@ -22,7 +22,10 @@ export async function fetchAccountData(userId: string, contaId: string) {
logo: true, logo: true,
note: true, note: true,
}, },
where: and(eq(contas.id, contaId), eq(contas.userId, userId)), where: and(
eq(financialAccounts.id, accountId),
eq(financialAccounts.userId, userId),
),
}); });
return account; return account;
@@ -30,7 +33,7 @@ export async function fetchAccountData(userId: string, contaId: string) {
export async function fetchAccountSummary( export async function fetchAccountSummary(
userId: string, userId: string,
contaId: string, accountId: string,
selectedPeriod: string, selectedPeriod: string,
): Promise<AccountSummaryData> { ): Promise<AccountSummaryData> {
const [periodSummary] = await db const [periodSummary] = await db
@@ -39,8 +42,8 @@ export async function fetchAccountSummary(
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
@@ -50,8 +53,8 @@ export async function fetchAccountSummary(
coalesce( coalesce(
sum( sum(
case case
when ${lancamentos.note} = ${INITIAL_BALANCE_NOTE} then 0 when ${transactions.note} = ${INITIAL_BALANCE_NOTE} then 0
when ${lancamentos.transactionType} = 'Receita' then ${lancamentos.amount} when ${transactions.transactionType} = 'Receita' then ${transactions.amount}
else 0 else 0
end end
), ),
@@ -62,8 +65,8 @@ export async function fetchAccountSummary(
coalesce( coalesce(
sum( sum(
case case
when ${lancamentos.note} = ${INITIAL_BALANCE_NOTE} then 0 when ${transactions.note} = ${INITIAL_BALANCE_NOTE} then 0
when ${lancamentos.transactionType} = 'Despesa' then ${lancamentos.amount} when ${transactions.transactionType} = 'Despesa' then ${transactions.amount}
else 0 else 0
end end
), ),
@@ -71,15 +74,15 @@ export async function fetchAccountSummary(
) )
`, `,
}) })
.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.contaId, contaId), eq(transactions.accountId, accountId),
eq(lancamentos.period, selectedPeriod), eq(transactions.period, selectedPeriod),
eq(lancamentos.isSettled, true), eq(transactions.isSettled, true),
eq(pagadores.role, PAGADOR_ROLE_ADMIN), eq(payers.role, PAYER_ROLE_ADMIN),
), ),
); );
@@ -89,27 +92,27 @@ export async function fetchAccountSummary(
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(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.contaId, contaId), eq(transactions.accountId, accountId),
lt(lancamentos.period, selectedPeriod), lt(transactions.period, selectedPeriod),
eq(lancamentos.isSettled, true), eq(transactions.isSettled, true),
eq(pagadores.role, PAGADOR_ROLE_ADMIN), eq(payers.role, PAYER_ROLE_ADMIN),
), ),
); );
const account = await fetchAccountData(userId, contaId); const account = await fetchAccountData(userId, accountId);
if (!account) { if (!account) {
throw new Error("Account not found"); throw new Error("Account not found");
} }
@@ -135,17 +138,17 @@ export async function fetchAccountLancamentos(
settledOnly = true, settledOnly = true,
) { ) {
const allFilters = settledOnly const allFilters = settledOnly
? [...filters, eq(lancamentos.isSettled, true)] ? [...filters, eq(transactions.isSettled, true)]
: filters; : filters;
return db.query.lancamentos.findMany({ return db.query.transactions.findMany({
where: and(...allFilters), where: and(...allFilters),
with: { with: {
pagador: true, payer: true,
conta: true, financialAccount: true,
cartao: true, card: true,
categoria: true, category: true,
}, },
orderBy: desc(lancamentos.purchaseDate), orderBy: desc(transactions.purchaseDate),
}); });
} }

View File

@@ -111,7 +111,7 @@ export function SignupForm({ className, ...props }: DivProps) {
}, },
onSuccess: () => { onSuccess: () => {
setLoadingEmail(false); setLoadingEmail(false);
toast.success("Conta criada com sucesso!"); toast.success("FinancialAccount criada com sucesso!");
router.replace("/dashboard"); router.replace("/dashboard");
}, },
onError: (ctx) => { onError: (ctx) => {

View File

@@ -2,7 +2,7 @@
import { and, eq, ne } from "drizzle-orm"; import { and, eq, ne } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { categorias, orcamentos } from "@/db/schema"; import { budgets, categories } from "@/db/schema";
import { import {
handleActionError, handleActionError,
revalidateForEntity, revalidateForEntity,
@@ -18,7 +18,7 @@ import {
import { getPreviousPeriod } from "@/shared/utils/period"; import { getPreviousPeriod } from "@/shared/utils/period";
const budgetBaseSchema = z.object({ const budgetBaseSchema = z.object({
categoriaId: uuidSchema("Categoria"), categoryId: uuidSchema("Category"),
period: periodSchema, period: periodSchema,
amount: z amount: z
.string({ message: "Informe o valor limite." }) .string({ message: "Informe o valor limite." })
@@ -48,21 +48,21 @@ type BudgetCreateInput = z.input<typeof createBudgetSchema>;
type BudgetUpdateInput = z.input<typeof updateBudgetSchema>; type BudgetUpdateInput = z.input<typeof updateBudgetSchema>;
type BudgetDeleteInput = z.input<typeof deleteBudgetSchema>; type BudgetDeleteInput = z.input<typeof deleteBudgetSchema>;
type BudgetCopyRow = { type BudgetCopyRow = {
categoriaId: string | null; categoryId: string | null;
amount: unknown; amount: unknown;
}; };
const ensureCategory = async (userId: string, categoriaId: string) => { const ensureCategory = async (userId: string, categoryId: string) => {
const category = await db.query.categorias.findFirst({ const category = await db.query.categories.findFirst({
columns: { columns: {
id: true, id: true,
type: true, type: true,
}, },
where: and(eq(categorias.id, categoriaId), eq(categorias.userId, userId)), where: and(eq(categories.id, categoryId), eq(categories.userId, userId)),
}); });
if (!category) { if (!category) {
throw new Error("Categoria não encontrada."); throw new Error("Category não encontrada.");
} }
if (category.type !== "despesa") { if (category.type !== "despesa") {
@@ -77,15 +77,15 @@ export async function createBudgetAction(
const user = await getUser(); const user = await getUser();
const data = createBudgetSchema.parse(input); const data = createBudgetSchema.parse(input);
await ensureCategory(user.id, data.categoriaId); await ensureCategory(user.id, data.categoryId);
const duplicateConditions = [ const duplicateConditions = [
eq(orcamentos.userId, user.id), eq(budgets.userId, user.id),
eq(orcamentos.period, data.period), eq(budgets.period, data.period),
eq(orcamentos.categoriaId, data.categoriaId), eq(budgets.categoryId, data.categoryId),
] as const; ] as const;
const duplicate = await db.query.orcamentos.findFirst({ const duplicate = await db.query.budgets.findFirst({
columns: { id: true }, columns: { id: true },
where: and(...duplicateConditions), where: and(...duplicateConditions),
}); });
@@ -98,14 +98,14 @@ export async function createBudgetAction(
}; };
} }
await db.insert(orcamentos).values({ await db.insert(budgets).values({
amount: formatDecimalForDbRequired(data.amount), amount: formatDecimalForDbRequired(data.amount),
period: data.period, period: data.period,
userId: user.id, userId: user.id,
categoriaId: data.categoriaId, categoryId: data.categoryId,
}); });
revalidateForEntity("orcamentos"); revalidateForEntity("budgets");
return { success: true, message: "Orçamento criado com sucesso." }; return { success: true, message: "Orçamento criado com sucesso." };
} catch (error) { } catch (error) {
@@ -120,16 +120,16 @@ export async function updateBudgetAction(
const user = await getUser(); const user = await getUser();
const data = updateBudgetSchema.parse(input); const data = updateBudgetSchema.parse(input);
await ensureCategory(user.id, data.categoriaId); await ensureCategory(user.id, data.categoryId);
const duplicateConditions = [ const duplicateConditions = [
eq(orcamentos.userId, user.id), eq(budgets.userId, user.id),
eq(orcamentos.period, data.period), eq(budgets.period, data.period),
eq(orcamentos.categoriaId, data.categoriaId), eq(budgets.categoryId, data.categoryId),
ne(orcamentos.id, data.id), ne(budgets.id, data.id),
] as const; ] as const;
const duplicate = await db.query.orcamentos.findFirst({ const duplicate = await db.query.budgets.findFirst({
columns: { id: true }, columns: { id: true },
where: and(...duplicateConditions), where: and(...duplicateConditions),
}); });
@@ -143,14 +143,14 @@ export async function updateBudgetAction(
} }
const [updated] = await db const [updated] = await db
.update(orcamentos) .update(budgets)
.set({ .set({
amount: formatDecimalForDbRequired(data.amount), amount: formatDecimalForDbRequired(data.amount),
period: data.period, period: data.period,
categoriaId: data.categoriaId, categoryId: data.categoryId,
}) })
.where(and(eq(orcamentos.id, data.id), eq(orcamentos.userId, user.id))) .where(and(eq(budgets.id, data.id), eq(budgets.userId, user.id)))
.returning({ id: orcamentos.id }); .returning({ id: budgets.id });
if (!updated) { if (!updated) {
return { return {
@@ -159,7 +159,7 @@ export async function updateBudgetAction(
}; };
} }
revalidateForEntity("orcamentos"); revalidateForEntity("budgets");
return { success: true, message: "Orçamento atualizado com sucesso." }; return { success: true, message: "Orçamento atualizado com sucesso." };
} catch (error) { } catch (error) {
@@ -175,9 +175,9 @@ export async function deleteBudgetAction(
const data = deleteBudgetSchema.parse(input); const data = deleteBudgetSchema.parse(input);
const [deleted] = await db const [deleted] = await db
.delete(orcamentos) .delete(budgets)
.where(and(eq(orcamentos.id, data.id), eq(orcamentos.userId, user.id))) .where(and(eq(budgets.id, data.id), eq(budgets.userId, user.id)))
.returning({ id: orcamentos.id }); .returning({ id: budgets.id });
if (!deleted) { if (!deleted) {
return { return {
@@ -186,7 +186,7 @@ export async function deleteBudgetAction(
}; };
} }
revalidateForEntity("orcamentos"); revalidateForEntity("budgets");
return { success: true, message: "Orçamento removido com sucesso." }; return { success: true, message: "Orçamento removido com sucesso." };
} catch (error) { } catch (error) {
@@ -211,10 +211,10 @@ export async function duplicatePreviousMonthBudgetsAction(
const previousPeriod = getPreviousPeriod(data.period); const previousPeriod = getPreviousPeriod(data.period);
// Buscar orçamentos do mês anterior // Buscar orçamentos do mês anterior
const previousBudgets = (await db.query.orcamentos.findMany({ const previousBudgets = (await db.query.budgets.findMany({
where: and( where: and(
eq(orcamentos.userId, user.id), eq(budgets.userId, user.id),
eq(orcamentos.period, previousPeriod), eq(budgets.period, previousPeriod),
), ),
})) as BudgetCopyRow[]; })) as BudgetCopyRow[];
@@ -226,41 +226,38 @@ export async function duplicatePreviousMonthBudgetsAction(
} }
// Buscar orçamentos existentes do mês atual // Buscar orçamentos existentes do mês atual
const currentBudgets = (await db.query.orcamentos.findMany({ const currentBudgets = (await db.query.budgets.findMany({
where: and( where: and(eq(budgets.userId, user.id), eq(budgets.period, data.period)),
eq(orcamentos.userId, user.id),
eq(orcamentos.period, data.period),
),
})) as BudgetCopyRow[]; })) as BudgetCopyRow[];
// Filtrar para evitar duplicatas // Filtrar para evitar duplicatas
const existingCategoryIds = new Set( const existingCategoryIds = new Set(
currentBudgets.map((b) => b.categoriaId), currentBudgets.map((b) => b.categoryId),
); );
const budgetsToCopy = previousBudgets.filter( const budgetsToCopy = previousBudgets.filter(
(b) => b.categoriaId && !existingCategoryIds.has(b.categoriaId), (b) => b.categoryId && !existingCategoryIds.has(b.categoryId),
); );
if (budgetsToCopy.length === 0) { if (budgetsToCopy.length === 0) {
return { return {
success: false, success: false,
error: error:
"Todas as categorias do mês anterior já possuem orçamento neste mês.", "Todas as categories do mês anterior já possuem orçamento neste mês.",
}; };
} }
// Inserir novos orçamentos // Inserir novos orçamentos
await db.insert(orcamentos).values( await db.insert(budgets).values(
budgetsToCopy.map((b) => ({ budgetsToCopy.map((b) => ({
amount: b.amount, amount: b.amount as string,
period: data.period, period: data.period,
userId: user.id, userId: user.id,
categoriaId: b.categoriaId as string, categoryId: b.categoryId as string,
})), })),
); );
revalidateForEntity("orcamentos"); revalidateForEntity("budgets");
return { return {
success: true, success: true,

View File

@@ -30,7 +30,7 @@ const buildUsagePercent = (spent: number, limit: number) => {
}; };
const formatCategoryName = (budget: Budget) => const formatCategoryName = (budget: Budget) =>
budget.category?.name ?? "Categoria removida"; budget.category?.name ?? "Category removida";
export function BudgetCard({ export function BudgetCard({
budget, budget,

View File

@@ -50,7 +50,7 @@ const buildInitialValues = ({
budget?: Budget; budget?: Budget;
defaultPeriod: string; defaultPeriod: string;
}): BudgetFormValues => ({ }): BudgetFormValues => ({
categoriaId: budget?.category?.id ?? "", categoryId: budget?.category?.id ?? "",
period: budget?.period ?? defaultPeriod, period: budget?.period ?? defaultPeriod,
amount: budget ? (Math.round(budget.amount * 100) / 100).toFixed(2) : "", amount: budget ? (Math.round(budget.amount * 100) / 100).toFixed(2) : "",
}); });
@@ -113,7 +113,7 @@ export function BudgetDialog({
return; return;
} }
if (formState.categoriaId.length === 0) { if (formState.categoryId.length === 0) {
const message = "Selecione uma categoria."; const message = "Selecione uma categoria.";
setErrorMessage(message); setErrorMessage(message);
toast.error(message); toast.error(message);
@@ -135,7 +135,7 @@ export function BudgetDialog({
} }
const payload = { const payload = {
categoriaId: formState.categoriaId, categoryId: formState.categoryId,
period: formState.period, period: formState.period,
amount: formState.amount, amount: formState.amount,
}; };
@@ -207,10 +207,10 @@ export function BudgetDialog({
) : ( ) : (
<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="budget-category">Categoria</Label> <Label htmlFor="budget-category">Category</Label>
<Select <Select
value={formState.categoriaId} value={formState.categoryId}
onValueChange={(value) => updateField("categoriaId", value)} onValueChange={(value) => updateField("categoryId", value)}
> >
<SelectTrigger id="budget-category" className="w-full"> <SelectTrigger id="budget-category" className="w-full">
<SelectValue placeholder="Selecione uma categoria" /> <SelectValue placeholder="Selecione uma categoria" />

View File

@@ -14,7 +14,7 @@ export type Budget = {
}; };
export type BudgetFormValues = { export type BudgetFormValues = {
categoriaId: string; categoryId: string;
period: string; period: string;
amount: string; amount: string;
}; };

View File

@@ -1,14 +1,14 @@
import { and, asc, eq, inArray, isNull, or, sql, sum } from "drizzle-orm"; import { and, asc, eq, inArray, isNull, or, sql, sum } from "drizzle-orm";
import { import {
categorias, type Budget,
lancamentos, budgets,
type Orcamento, categories,
orcamentos, payers,
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 { 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";
const toNumber = (value: string | number | null | undefined) => { const toNumber = (value: string | number | null | undefined) => {
if (typeof value === "number") return value; if (typeof value === "number") return value;
@@ -46,28 +46,28 @@ export async function fetchBudgetsForUser(
categoriesOptions: CategoryOption[]; categoriesOptions: CategoryOption[];
}> { }> {
const [budgetRows, categoryRows] = await Promise.all([ const [budgetRows, categoryRows] = await Promise.all([
db.query.orcamentos.findMany({ db.query.budgets.findMany({
where: and( where: and(
eq(orcamentos.userId, userId), eq(budgets.userId, userId),
eq(orcamentos.period, selectedPeriod), eq(budgets.period, selectedPeriod),
), ),
with: { with: {
categoria: true, category: true,
}, },
}), }),
db.query.categorias.findMany({ db.query.categories.findMany({
columns: { columns: {
id: true, id: true,
name: true, name: true,
icon: true, icon: true,
}, },
where: and(eq(categorias.userId, userId), eq(categorias.type, "despesa")), where: and(eq(categories.userId, userId), eq(categories.type, "despesa")),
orderBy: asc(categorias.name), orderBy: asc(categories.name),
}), }),
]); ]);
const categoryIds = budgetRows const categoryIds = budgetRows
.map((budget: Orcamento) => budget.categoriaId) .map((budget) => budget.categoryId)
.filter((id: string | null): id is string => Boolean(id)); .filter((id: string | null): id is string => Boolean(id));
let totalsByCategory = new Map<string, number>(); let totalsByCategory = new Map<string, number>();
@@ -75,50 +75,48 @@ export async function fetchBudgetsForUser(
if (categoryIds.length > 0) { if (categoryIds.length > 0) {
const totals = await db const totals = await db
.select({ .select({
categoriaId: lancamentos.categoriaId, categoryId: transactions.categoryId,
totalAmount: sum(lancamentos.amount).as("totalAmount"), totalAmount: sum(transactions.amount).as("totalAmount"),
}) })
.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, selectedPeriod), eq(transactions.period, selectedPeriod),
eq(lancamentos.transactionType, "Despesa"), eq(transactions.transactionType, "Despesa"),
eq(pagadores.role, PAGADOR_ROLE_ADMIN), eq(payers.role, PAYER_ROLE_ADMIN),
inArray(lancamentos.categoriaId, categoryIds), inArray(transactions.categoryId, categoryIds),
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(lancamentos.categoriaId); .groupBy(transactions.categoryId);
totalsByCategory = new Map( totalsByCategory = new Map(
totals.map( totals.map(
(row: { categoriaId: string | null; totalAmount: string | null }) => [ (row: { categoryId: string | null; totalAmount: string | null }) => [
row.categoriaId ?? "", row.categoryId ?? "",
Math.abs(toNumber(row.totalAmount)), Math.abs(toNumber(row.totalAmount)),
], ],
), ),
); );
} }
const budgets = budgetRows const budgetList = budgetRows
.map((budget: Orcamento) => ({ .map((budget) => ({
id: budget.id, id: budget.id,
amount: toNumber(budget.amount), amount: toNumber(budget.amount),
spent: totalsByCategory.get(budget.categoriaId ?? "") ?? 0, spent: totalsByCategory.get(budget.categoryId ?? "") ?? 0,
period: budget.period, period: budget.period,
createdAt: budget.createdAt.toISOString(), createdAt: budget.createdAt.toISOString(),
category: budget.categoria category: (() => {
? { type Cat = { id: string; name: string; icon: string | null };
id: budget.categoria.id, const cat = budget.category as Cat | null | undefined;
name: budget.categoria.name, return cat ? { id: cat.id, name: cat.name, icon: cat.icon } : null;
icon: budget.categoria.icon, })(),
}
: null,
})) }))
.sort((a, b) => .sort((a, b) =>
(a.category?.name ?? "").localeCompare(b.category?.name ?? "", "pt-BR", { (a.category?.name ?? "").localeCompare(b.category?.name ?? "", "pt-BR", {
@@ -132,5 +130,5 @@ export async function fetchBudgetsForUser(
icon: category.icon, icon: category.icon,
})); }));
return { budgets, categoriesOptions }; return { budgets: budgetList, categoriesOptions };
} }

View File

@@ -2,7 +2,7 @@
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { cartoes, contas } from "@/db/schema"; import { cards, financialAccounts } from "@/db/schema";
import { import {
type ActionResult, type ActionResult,
handleActionError, handleActionError,
@@ -40,7 +40,7 @@ const cardBaseSchema = z.object({
.string({ message: "Selecione um logo." }) .string({ message: "Selecione um logo." })
.trim() .trim()
.min(1, "Selecione um logo."), .min(1, "Selecione um logo."),
contaId: uuidSchema("Conta"), accountId: uuidSchema("FinancialAccount"),
}); });
const createCardSchema = cardBaseSchema; const createCardSchema = cardBaseSchema;
@@ -55,14 +55,17 @@ type CardCreateInput = z.infer<typeof createCardSchema>;
type CardUpdateInput = z.infer<typeof updateCardSchema>; type CardUpdateInput = z.infer<typeof updateCardSchema>;
type CardDeleteInput = z.infer<typeof deleteCardSchema>; type CardDeleteInput = z.infer<typeof deleteCardSchema>;
async function assertAccountOwnership(userId: string, contaId: string) { async function assertAccountOwnership(userId: string, accountId: string) {
const account = await db.query.contas.findFirst({ const account = await db.query.financialAccounts.findFirst({
columns: { id: true }, columns: { id: true },
where: and(eq(contas.id, contaId), eq(contas.userId, userId)), where: and(
eq(financialAccounts.id, accountId),
eq(financialAccounts.userId, userId),
),
}); });
if (!account) { if (!account) {
throw new Error("Conta vinculada não encontrada."); throw new Error("FinancialAccount vinculada não encontrada.");
} }
} }
@@ -73,11 +76,11 @@ export async function createCardAction(
const user = await getUser(); const user = await getUser();
const data = createCardSchema.parse(input); const data = createCardSchema.parse(input);
await assertAccountOwnership(user.id, data.contaId); await assertAccountOwnership(user.id, data.accountId);
const logoFile = normalizeFilePath(data.logo); const logoFile = normalizeFilePath(data.logo);
await db.insert(cartoes).values({ await db.insert(cards).values({
name: data.name, name: data.name,
brand: data.brand, brand: data.brand,
status: data.status, status: data.status,
@@ -86,11 +89,11 @@ export async function createCardAction(
note: data.note ?? null, note: data.note ?? null,
limit: formatDecimalForDb(data.limit), limit: formatDecimalForDb(data.limit),
logo: logoFile, logo: logoFile,
contaId: data.contaId, accountId: data.accountId,
userId: user.id, userId: user.id,
}); });
revalidateForEntity("cartoes"); revalidateForEntity("cards");
return { success: true, message: "Cartão criado com sucesso." }; return { success: true, message: "Cartão criado com sucesso." };
} catch (error) { } catch (error) {
@@ -105,12 +108,12 @@ export async function updateCardAction(
const user = await getUser(); const user = await getUser();
const data = updateCardSchema.parse(input); const data = updateCardSchema.parse(input);
await assertAccountOwnership(user.id, data.contaId); await assertAccountOwnership(user.id, data.accountId);
const logoFile = normalizeFilePath(data.logo); const logoFile = normalizeFilePath(data.logo);
const [updated] = await db const [updated] = await db
.update(cartoes) .update(cards)
.set({ .set({
name: data.name, name: data.name,
brand: data.brand, brand: data.brand,
@@ -120,9 +123,9 @@ export async function updateCardAction(
note: data.note ?? null, note: data.note ?? null,
limit: formatDecimalForDb(data.limit), limit: formatDecimalForDb(data.limit),
logo: logoFile, logo: logoFile,
contaId: data.contaId, accountId: data.accountId,
}) })
.where(and(eq(cartoes.id, data.id), eq(cartoes.userId, user.id))) .where(and(eq(cards.id, data.id), eq(cards.userId, user.id)))
.returning(); .returning();
if (!updated) { if (!updated) {
@@ -132,7 +135,7 @@ export async function updateCardAction(
}; };
} }
revalidateForEntity("cartoes"); revalidateForEntity("cards");
return { success: true, message: "Cartão atualizado com sucesso." }; return { success: true, message: "Cartão atualizado com sucesso." };
} catch (error) { } catch (error) {
@@ -148,9 +151,9 @@ export async function deleteCardAction(
const data = deleteCardSchema.parse(input); const data = deleteCardSchema.parse(input);
const [deleted] = await db const [deleted] = await db
.delete(cartoes) .delete(cards)
.where(and(eq(cartoes.id, data.id), eq(cartoes.userId, user.id))) .where(and(eq(cards.id, data.id), eq(cards.userId, user.id)))
.returning({ id: cartoes.id }); .returning({ id: cards.id });
if (!deleted) { if (!deleted) {
return { return {
@@ -159,7 +162,7 @@ export async function deleteCardAction(
}; };
} }
revalidateForEntity("cartoes"); revalidateForEntity("cards");
return { success: true, message: "Cartão removido com sucesso." }; return { success: true, message: "Cartão removido com sucesso." };
} catch (error) { } catch (error) {

View File

@@ -70,7 +70,7 @@ const buildInitialValues = ({
limit: formatLimitInput(card?.limit ?? null), limit: formatLimitInput(card?.limit ?? null),
note: card?.note ?? "", note: card?.note ?? "",
logo: selectedLogo, logo: selectedLogo,
contaId: card?.contaId ?? accounts[0]?.id ?? "", accountId: card?.accountId ?? accounts[0]?.id ?? "",
}; };
}; };
@@ -146,7 +146,7 @@ export function CardDialog({
return; return;
} }
if (!formState.contaId) { if (!formState.accountId) {
const message = "Selecione a conta vinculada."; const message = "Selecione a conta vinculada.";
setErrorMessage(message); setErrorMessage(message);
toast.error(message); toast.error(message);
@@ -163,7 +163,7 @@ export function CardDialog({
limit: rawLimit ? Number(rawLimit) : null, limit: rawLimit ? Number(rawLimit) : null,
note: formState.note.trim() || null, note: formState.note.trim() || null,
logo: formState.logo, logo: formState.logo,
contaId: formState.contaId, accountId: formState.accountId,
}; };
if (!payload.logo) { if (!payload.logo) {

View File

@@ -160,10 +160,10 @@ export function CardFormFields({
</div> </div>
<div className="flex flex-col gap-2 sm:col-span-2"> <div className="flex flex-col gap-2 sm:col-span-2">
<Label htmlFor="card-account">Conta vinculada</Label> <Label htmlFor="card-account">FinancialAccount vinculada</Label>
<Select <Select
value={values.contaId} value={values.accountId}
onValueChange={(value) => onChange("contaId", value)} onValueChange={(value) => onChange("accountId", value)}
disabled={accountOptions.length === 0} disabled={accountOptions.length === 0}
> >
<SelectTrigger id="card-account" className="w-full"> <SelectTrigger id="card-account" className="w-full">
@@ -174,10 +174,10 @@ export function CardFormFields({
: "Selecione a conta" : "Selecione a conta"
} }
> >
{values.contaId && {values.accountId &&
(() => { (() => {
const selectedAccount = accountOptions.find( const selectedAccount = accountOptions.find(
(acc) => acc.id === values.contaId, (acc) => acc.id === values.accountId,
); );
return selectedAccount ? ( return selectedAccount ? (
<AccountSelectContent <AccountSelectContent

View File

@@ -33,7 +33,7 @@ interface CardItemProps {
limit: number | null; limit: number | null;
limitInUse?: number | null; limitInUse?: number | null;
limitAvailable?: number | null; limitAvailable?: number | null;
contaName: string; accountName: string;
logo?: string | null; logo?: string | null;
note?: string | null; note?: string | null;
onEdit?: () => void; onEdit?: () => void;
@@ -52,14 +52,14 @@ export function CardItem({
limit, limit,
limitInUse, limitInUse,
limitAvailable, limitAvailable,
contaName: _contaName, accountName: _accountName,
logo, logo,
note, note,
onEdit, onEdit,
onInvoice, onInvoice,
onRemove, onRemove,
}: CardItemProps) { }: CardItemProps) {
void _contaName; void _accountName;
const limitTotal = limit ?? null; const limitTotal = limit ?? null;
const used = const used =

View File

@@ -142,7 +142,7 @@ export function CardsPage({
limit={card.limit} limit={card.limit}
limitInUse={card.limitInUse ?? null} limitInUse={card.limitInUse ?? null}
limitAvailable={card.limitAvailable ?? card.limit ?? null} limitAvailable={card.limitAvailable ?? card.limit ?? null}
contaName={card.contaName} accountName={card.accountName}
logo={card.logo} logo={card.logo}
note={card.note} note={card.note}
onEdit={() => handleEdit(card)} onEdit={() => handleEdit(card)}

View File

@@ -8,10 +8,10 @@ export type Card = {
note: string | null; note: string | null;
logo: string | null; logo: string | null;
limit: number | null; limit: number | null;
contaId: string; accountId: string;
contaName: string; accountName: string;
limitInUse?: number | null; limitInUse: number;
limitAvailable?: number | null; limitAvailable: number | null;
}; };
export type CardFormValues = { export type CardFormValues = {
@@ -23,5 +23,5 @@ export type CardFormValues = {
limit: string; limit: string;
note: string; note: string;
logo: string; logo: string;
contaId: string; accountId: string;
}; };

View File

@@ -1,22 +1,22 @@
import { and, eq, ilike, isNull, ne, not, or, sql } from "drizzle-orm"; import { and, eq, ilike, isNull, ne, not, or, sql } from "drizzle-orm";
import { cartoes, contas, lancamentos } from "@/db/schema"; import { cards, financialAccounts, transactions } from "@/db/schema";
import { db } from "@/shared/lib/db"; import { db } from "@/shared/lib/db";
import { loadLogoOptions } from "@/shared/lib/logo/options"; import { loadLogoOptions } from "@/shared/lib/logo/options";
export type CardData = { export type CardData = {
id: string; id: string;
name: string; name: string;
brand: string | null; brand: string;
status: string | null; status: string;
closingDay: number; closingDay: string;
dueDay: number; dueDay: string;
note: string | null; note: string | null;
logo: string | null; logo: string | null;
limit: number | null; limit: number | null;
limitInUse: number; limitInUse: number;
limitAvailable: number | null; limitAvailable: number | null;
contaId: string; accountId: string;
contaName: string; accountName: string;
}; };
export type AccountSimple = { export type AccountSimple = {
@@ -28,20 +28,14 @@ export type AccountSimple = {
export async function fetchCardsForUser(userId: string): Promise<{ export async function fetchCardsForUser(userId: string): Promise<{
cards: CardData[]; cards: CardData[];
accounts: AccountSimple[]; accounts: AccountSimple[];
logoOptions: LogoOption[]; logoOptions: string[];
}> { }> {
const [cardRows, accountRows, logoOptions, usageRows] = await Promise.all([ const [cardRows, accountRows, logoOptions, usageRows] = await Promise.all([
db.query.cartoes.findMany({ db.query.cards.findMany({
orderBy: ( orderBy: (table, { desc }) => [desc(table.name)],
card: typeof cartoes.$inferSelect, where: and(eq(cards.userId, userId), not(ilike(cards.status, "inativo"))),
{ desc }: { desc: (field: unknown) => unknown },
) => [desc(card.name)],
where: and(
eq(cartoes.userId, userId),
not(ilike(cartoes.status, "inativo")),
),
with: { with: {
conta: { financialAccount: {
columns: { columns: {
id: true, id: true,
name: true, name: true,
@@ -49,12 +43,9 @@ export async function fetchCardsForUser(userId: string): Promise<{
}, },
}, },
}), }),
db.query.contas.findMany({ db.query.financialAccounts.findMany({
orderBy: ( orderBy: (table, { desc }) => [desc(table.name)],
account: typeof contas.$inferSelect, where: eq(financialAccounts.userId, userId),
{ desc }: { desc: (field: unknown) => unknown },
) => [desc(account.name)],
where: eq(contas.userId, userId),
columns: { columns: {
id: true, id: true,
name: true, name: true,
@@ -64,37 +55,35 @@ export async function fetchCardsForUser(userId: string): Promise<{
loadLogoOptions(), loadLogoOptions(),
db db
.select({ .select({
cartaoId: lancamentos.cartaoId, cardId: transactions.cardId,
total: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`, total: sql<number>`coalesce(sum(${transactions.amount}), 0)`,
}) })
.from(lancamentos) .from(transactions)
.where( .where(
and( and(
eq(lancamentos.userId, userId), eq(transactions.userId, userId),
or(isNull(lancamentos.isSettled), eq(lancamentos.isSettled, false)), or(isNull(transactions.isSettled), eq(transactions.isSettled, false)),
// Recorrente no cartão: só consome limite quando a data da ocorrência já passou // Recorrente no cartão: só consome limite quando a data da ocorrência já passou
or( or(
ne(lancamentos.condition, "Recorrente"), ne(transactions.condition, "Recorrente"),
sql`${lancamentos.purchaseDate} <= current_date`, sql`${transactions.purchaseDate} <= current_date`,
), ),
), ),
) )
.groupBy(lancamentos.cartaoId), .groupBy(transactions.cardId),
]); ]);
const usageMap = new Map<string, number>(); const usageMap = new Map<string, number>();
usageRows.forEach( usageRows.forEach((row: { cardId: string | null; total: number | null }) => {
(row: { cartaoId: string | null; total: number | null }) => { if (!row.cardId) return;
if (!row.cartaoId) return; usageMap.set(row.cardId, Number(row.total ?? 0));
usageMap.set(row.cartaoId, Number(row.total ?? 0)); });
},
);
const cards = cardRows.map((card) => ({ const cardList = cardRows.map((card) => ({
id: card.id, id: card.id,
name: card.name, name: card.name,
brand: card.brand, brand: card.brand ?? "",
status: card.status, status: card.status ?? "",
closingDay: card.closingDay, closingDay: card.closingDay,
dueDay: card.dueDay, dueDay: card.dueDay,
note: card.note, note: card.note,
@@ -112,8 +101,10 @@ export async function fetchCardsForUser(userId: string): Promise<{
const inUse = total < 0 ? Math.abs(total) : 0; const inUse = total < 0 ? Math.abs(total) : 0;
return Math.max(Number(card.limit) - inUse, 0); return Math.max(Number(card.limit) - inUse, 0);
})(), })(),
contaId: card.contaId, accountId: card.accountId,
contaName: card.conta?.name ?? "Conta não encontrada", accountName:
(card.financialAccount as { name?: string } | null)?.name ??
"Conta não encontrada",
})); }));
const accounts = accountRows.map((account) => ({ const accounts = accountRows.map((account) => ({
@@ -122,23 +113,20 @@ export async function fetchCardsForUser(userId: string): Promise<{
logo: account.logo, logo: account.logo,
})); }));
return { cards, accounts, logoOptions }; return { cards: cardList, accounts, logoOptions };
} }
export async function fetchInativosForUser(userId: string): Promise<{ export async function fetchInactiveForUser(userId: string): Promise<{
cards: CardData[]; cards: CardData[];
accounts: AccountSimple[]; accounts: AccountSimple[];
logoOptions: LogoOption[]; logoOptions: string[];
}> { }> {
const [cardRows, accountRows, logoOptions, usageRows] = await Promise.all([ const [cardRows, accountRows, logoOptions, usageRows] = await Promise.all([
db.query.cartoes.findMany({ db.query.cards.findMany({
orderBy: ( orderBy: (table, { desc }) => [desc(table.name)],
card: typeof cartoes.$inferSelect, where: and(eq(cards.userId, userId), ilike(cards.status, "inativo")),
{ desc }: { desc: (field: unknown) => unknown },
) => [desc(card.name)],
where: and(eq(cartoes.userId, userId), ilike(cartoes.status, "inativo")),
with: { with: {
conta: { financialAccount: {
columns: { columns: {
id: true, id: true,
name: true, name: true,
@@ -146,12 +134,9 @@ export async function fetchInativosForUser(userId: string): Promise<{
}, },
}, },
}), }),
db.query.contas.findMany({ db.query.financialAccounts.findMany({
orderBy: ( orderBy: (table, { desc }) => [desc(table.name)],
account: typeof contas.$inferSelect, where: eq(financialAccounts.userId, userId),
{ desc }: { desc: (field: unknown) => unknown },
) => [desc(account.name)],
where: eq(contas.userId, userId),
columns: { columns: {
id: true, id: true,
name: true, name: true,
@@ -161,37 +146,35 @@ export async function fetchInativosForUser(userId: string): Promise<{
loadLogoOptions(), loadLogoOptions(),
db db
.select({ .select({
cartaoId: lancamentos.cartaoId, cardId: transactions.cardId,
total: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`, total: sql<number>`coalesce(sum(${transactions.amount}), 0)`,
}) })
.from(lancamentos) .from(transactions)
.where( .where(
and( and(
eq(lancamentos.userId, userId), eq(transactions.userId, userId),
or(isNull(lancamentos.isSettled), eq(lancamentos.isSettled, false)), or(isNull(transactions.isSettled), eq(transactions.isSettled, false)),
// Recorrente no cartão: só consome limite quando a data da ocorrência já passou // Recorrente no cartão: só consome limite quando a data da ocorrência já passou
or( or(
ne(lancamentos.condition, "Recorrente"), ne(transactions.condition, "Recorrente"),
sql`${lancamentos.purchaseDate} <= current_date`, sql`${transactions.purchaseDate} <= current_date`,
), ),
), ),
) )
.groupBy(lancamentos.cartaoId), .groupBy(transactions.cardId),
]); ]);
const usageMap = new Map<string, number>(); const usageMap = new Map<string, number>();
usageRows.forEach( usageRows.forEach((row: { cardId: string | null; total: number | null }) => {
(row: { cartaoId: string | null; total: number | null }) => { if (!row.cardId) return;
if (!row.cartaoId) return; usageMap.set(row.cardId, Number(row.total ?? 0));
usageMap.set(row.cartaoId, Number(row.total ?? 0)); });
},
);
const cards = cardRows.map((card) => ({ const cardList = cardRows.map((card) => ({
id: card.id, id: card.id,
name: card.name, name: card.name,
brand: card.brand, brand: card.brand ?? "",
status: card.status, status: card.status ?? "",
closingDay: card.closingDay, closingDay: card.closingDay,
dueDay: card.dueDay, dueDay: card.dueDay,
note: card.note, note: card.note,
@@ -209,8 +192,10 @@ export async function fetchInativosForUser(userId: string): Promise<{
const inUse = total < 0 ? Math.abs(total) : 0; const inUse = total < 0 ? Math.abs(total) : 0;
return Math.max(Number(card.limit) - inUse, 0); return Math.max(Number(card.limit) - inUse, 0);
})(), })(),
contaId: card.contaId, accountId: card.accountId,
contaName: card.conta?.name ?? "Conta não encontrada", accountName:
(card.financialAccount as { name?: string } | null)?.name ??
"Conta não encontrada",
})); }));
const accounts = accountRows.map((account) => ({ const accounts = accountRows.map((account) => ({
@@ -219,18 +204,18 @@ export async function fetchInativosForUser(userId: string): Promise<{
logo: account.logo, logo: account.logo,
})); }));
return { cards, accounts, logoOptions }; return { cards: cardList, accounts, logoOptions };
} }
export async function fetchAllCardsForUser(userId: string): Promise<{ export async function fetchAllCardsForUser(userId: string): Promise<{
activeCards: CardData[]; activeCards: CardData[];
archivedCards: CardData[]; archivedCards: CardData[];
accounts: AccountSimple[]; accounts: AccountSimple[];
logoOptions: LogoOption[]; logoOptions: string[];
}> { }> {
const [activeData, archivedData] = await Promise.all([ const [activeData, archivedData] = await Promise.all([
fetchCardsForUser(userId), fetchCardsForUser(userId),
fetchInativosForUser(userId), fetchInactiveForUser(userId),
]); ]);
return { return {

View File

@@ -2,7 +2,7 @@
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { categorias } from "@/db/schema"; import { categories } from "@/db/schema";
import { import {
type ActionResult, type ActionResult,
handleActionError, handleActionError,
@@ -32,10 +32,10 @@ const categoryBaseSchema = z.object({
const createCategorySchema = categoryBaseSchema; const createCategorySchema = categoryBaseSchema;
const updateCategorySchema = categoryBaseSchema.extend({ const updateCategorySchema = categoryBaseSchema.extend({
id: uuidSchema("Categoria"), id: uuidSchema("Category"),
}); });
const deleteCategorySchema = z.object({ const deleteCategorySchema = z.object({
id: uuidSchema("Categoria"), id: uuidSchema("Category"),
}); });
type CategoryCreateInput = z.infer<typeof createCategorySchema>; type CategoryCreateInput = z.infer<typeof createCategorySchema>;
@@ -49,16 +49,16 @@ export async function createCategoryAction(
const user = await getUser(); const user = await getUser();
const data = createCategorySchema.parse(input); const data = createCategorySchema.parse(input);
await db.insert(categorias).values({ await db.insert(categories).values({
name: data.name, name: data.name,
type: data.type, type: data.type,
icon: data.icon, icon: data.icon,
userId: user.id, userId: user.id,
}); });
revalidateForEntity("categorias"); revalidateForEntity("categories");
return { success: true, message: "Categoria criada com sucesso." }; return { success: true, message: "Category criada com sucesso." };
} catch (error) { } catch (error) {
return handleActionError(error); return handleActionError(error);
} }
@@ -72,19 +72,19 @@ export async function updateCategoryAction(
const data = updateCategorySchema.parse(input); const data = updateCategorySchema.parse(input);
// Buscar categoria antes de atualizar para verificar restrições // Buscar categoria antes de atualizar para verificar restrições
const categoria = await db.query.categorias.findFirst({ const categoria = await db.query.categories.findFirst({
columns: { id: true, name: true }, columns: { id: true, name: true },
where: and(eq(categorias.id, data.id), eq(categorias.userId, user.id)), where: and(eq(categories.id, data.id), eq(categories.userId, user.id)),
}); });
if (!categoria) { if (!categoria) {
return { return {
success: false, success: false,
error: "Categoria não encontrada.", error: "Category não encontrada.",
}; };
} }
// Bloquear edição das categorias protegidas // Bloquear edição das categories protegidas
const categoriasProtegidas = [ const categoriasProtegidas = [
"Transferência interna", "Transferência interna",
"Saldo inicial", "Saldo inicial",
@@ -98,25 +98,25 @@ export async function updateCategoryAction(
} }
const [updated] = await db const [updated] = await db
.update(categorias) .update(categories)
.set({ .set({
name: data.name, name: data.name,
type: data.type, type: data.type,
icon: data.icon, icon: data.icon,
}) })
.where(and(eq(categorias.id, data.id), eq(categorias.userId, user.id))) .where(and(eq(categories.id, data.id), eq(categories.userId, user.id)))
.returning(); .returning();
if (!updated) { if (!updated) {
return { return {
success: false, success: false,
error: "Categoria não encontrada.", error: "Category não encontrada.",
}; };
} }
revalidateForEntity("categorias"); revalidateForEntity("categories");
return { success: true, message: "Categoria atualizada com sucesso." }; return { success: true, message: "Category atualizada com sucesso." };
} catch (error) { } catch (error) {
return handleActionError(error); return handleActionError(error);
} }
@@ -130,19 +130,19 @@ export async function deleteCategoryAction(
const data = deleteCategorySchema.parse(input); const data = deleteCategorySchema.parse(input);
// Buscar categoria antes de deletar para verificar restrições // Buscar categoria antes de deletar para verificar restrições
const categoria = await db.query.categorias.findFirst({ const categoria = await db.query.categories.findFirst({
columns: { id: true, name: true }, columns: { id: true, name: true },
where: and(eq(categorias.id, data.id), eq(categorias.userId, user.id)), where: and(eq(categories.id, data.id), eq(categories.userId, user.id)),
}); });
if (!categoria) { if (!categoria) {
return { return {
success: false, success: false,
error: "Categoria não encontrada.", error: "Category não encontrada.",
}; };
} }
// Bloquear remoção das categorias protegidas // Bloquear remoção das categories protegidas
const categoriasProtegidas = [ const categoriasProtegidas = [
"Transferência interna", "Transferência interna",
"Saldo inicial", "Saldo inicial",
@@ -156,20 +156,20 @@ export async function deleteCategoryAction(
} }
const [deleted] = await db const [deleted] = await db
.delete(categorias) .delete(categories)
.where(and(eq(categorias.id, data.id), eq(categorias.userId, user.id))) .where(and(eq(categories.id, data.id), eq(categories.userId, user.id)))
.returning({ id: categorias.id }); .returning({ id: categories.id });
if (!deleted) { if (!deleted) {
return { return {
success: false, success: false,
error: "Categoria não encontrada.", error: "Category não encontrada.",
}; };
} }
revalidateForEntity("categorias"); revalidateForEntity("categories");
return { success: true, message: "Categoria removida com sucesso." }; return { success: true, message: "Category removida com sucesso." };
} catch (error) { } catch (error) {
return handleActionError(error); return handleActionError(error);
} }

View File

@@ -103,7 +103,7 @@ export function CategoryDialog({
setErrorMessage(null); setErrorMessage(null);
if (mode === "update" && !category?.id) { if (mode === "update" && !category?.id) {
const message = "Categoria inválida."; const message = "Category inválida.";
setErrorMessage(message); setErrorMessage(message);
toast.error(message); toast.error(message);
return; return;

View File

@@ -64,6 +64,7 @@ export function CategoryIconBadge({
style={{ backgroundColor: bgColor }} style={{ backgroundColor: bgColor }}
> >
{IconComponent ? ( {IconComponent ? (
// @ts-expect-error icon accepts style but type is too narrow
<IconComponent className={variant.icon} style={{ color }} /> <IconComponent className={variant.icon} style={{ color }} />
) : ( ) : (
<span className={cn("uppercase", variant.text)} style={{ color }}> <span className={cn("uppercase", variant.text)} style={{ color }}>

View File

@@ -19,7 +19,7 @@ export function CategoryIcon({ name, className }: CategoryIconProps) {
if (!IconComponent) { if (!IconComponent) {
return ( return (
<span className={cn("text-xs text-muted-foreground", className)}> <span className={cn("text-xs text-muted-foreground", className)}>
{name ?? "Categoria"} {name ?? "Category"}
</span> </span>
); );
} }

View File

@@ -1,5 +1,5 @@
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { type Categoria, categorias } from "@/db/schema"; import { type Category, categories } from "@/db/schema";
import type { CategoryType } from "@/features/categories/components/types"; import type { CategoryType } from "@/features/categories/components/types";
import { db } from "@/shared/lib/db"; import { db } from "@/shared/lib/db";
@@ -13,11 +13,11 @@ export type CategoryData = {
export async function fetchCategoriesForUser( export async function fetchCategoriesForUser(
userId: string, userId: string,
): Promise<CategoryData[]> { ): Promise<CategoryData[]> {
const categoryRows = await db.query.categorias.findMany({ const categoryRows = await db.query.categories.findMany({
where: eq(categorias.userId, userId), where: eq(categories.userId, userId),
}); });
return categoryRows.map((category: Categoria) => ({ return categoryRows.map((category: Category) => ({
id: category.id, id: category.id,
name: category.name, name: category.name,
type: category.type as CategoryType, type: category.type as CategoryType,

View File

@@ -2,7 +2,7 @@
import { and, eq, inArray } from "drizzle-orm"; import { and, eq, inArray } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { preLancamentos } from "@/db/schema"; import { inboxItems } from "@/db/schema";
import { import {
handleActionError, handleActionError,
revalidateForEntity, revalidateForEntity,
@@ -52,12 +52,12 @@ export async function markInboxAsProcessedAction(
// Verificar se item existe e pertence ao usuário // Verificar se item existe e pertence ao usuário
const [item] = await db const [item] = await db
.select() .select()
.from(preLancamentos) .from(inboxItems)
.where( .where(
and( and(
eq(preLancamentos.id, data.inboxItemId), eq(inboxItems.id, data.inboxItemId),
eq(preLancamentos.userId, user.id), eq(inboxItems.userId, user.id),
eq(preLancamentos.status, "pending"), eq(inboxItems.status, "pending"),
), ),
) )
.limit(1); .limit(1);
@@ -68,7 +68,7 @@ export async function markInboxAsProcessedAction(
// Marcar item como processado // Marcar item como processado
await db await db
.update(preLancamentos) .update(inboxItems)
.set({ .set({
status: "processed", status: "processed",
processedAt: new Date(), processedAt: new Date(),
@@ -76,8 +76,8 @@ export async function markInboxAsProcessedAction(
}) })
.where( .where(
and( and(
eq(preLancamentos.id, data.inboxItemId), eq(inboxItems.id, data.inboxItemId),
eq(preLancamentos.userId, user.id), eq(inboxItems.userId, user.id),
), ),
); );
@@ -99,12 +99,12 @@ export async function discardInboxItemAction(
// Verificar se item existe e pertence ao usuário // Verificar se item existe e pertence ao usuário
const [item] = await db const [item] = await db
.select() .select()
.from(preLancamentos) .from(inboxItems)
.where( .where(
and( and(
eq(preLancamentos.id, data.inboxItemId), eq(inboxItems.id, data.inboxItemId),
eq(preLancamentos.userId, user.id), eq(inboxItems.userId, user.id),
eq(preLancamentos.status, "pending"), eq(inboxItems.status, "pending"),
), ),
) )
.limit(1); .limit(1);
@@ -115,7 +115,7 @@ export async function discardInboxItemAction(
// Marcar item como descartado // Marcar item como descartado
await db await db
.update(preLancamentos) .update(inboxItems)
.set({ .set({
status: "discarded", status: "discarded",
discardedAt: new Date(), discardedAt: new Date(),
@@ -123,8 +123,8 @@ export async function discardInboxItemAction(
}) })
.where( .where(
and( and(
eq(preLancamentos.id, data.inboxItemId), eq(inboxItems.id, data.inboxItemId),
eq(preLancamentos.userId, user.id), eq(inboxItems.userId, user.id),
), ),
); );
@@ -145,7 +145,7 @@ export async function bulkDiscardInboxItemsAction(
// Marcar todos os itens como descartados // Marcar todos os itens como descartados
await db await db
.update(preLancamentos) .update(inboxItems)
.set({ .set({
status: "discarded", status: "discarded",
discardedAt: new Date(), discardedAt: new Date(),
@@ -153,9 +153,9 @@ export async function bulkDiscardInboxItemsAction(
}) })
.where( .where(
and( and(
inArray(preLancamentos.id, data.inboxItemIds), inArray(inboxItems.id, data.inboxItemIds),
eq(preLancamentos.userId, user.id), eq(inboxItems.userId, user.id),
eq(preLancamentos.status, "pending"), eq(inboxItems.status, "pending"),
), ),
); );
@@ -178,13 +178,13 @@ export async function restoreDiscardedInboxItemAction(
const data = restoreDiscardedInboxSchema.parse(input); const data = restoreDiscardedInboxSchema.parse(input);
const [item] = await db const [item] = await db
.select({ id: preLancamentos.id }) .select({ id: inboxItems.id })
.from(preLancamentos) .from(inboxItems)
.where( .where(
and( and(
eq(preLancamentos.id, data.inboxItemId), eq(inboxItems.id, data.inboxItemId),
eq(preLancamentos.userId, user.id), eq(inboxItems.userId, user.id),
eq(preLancamentos.status, "discarded"), eq(inboxItems.status, "discarded"),
), ),
) )
.limit(1); .limit(1);
@@ -197,7 +197,7 @@ export async function restoreDiscardedInboxItemAction(
} }
await db await db
.update(preLancamentos) .update(inboxItems)
.set({ .set({
status: "pending", status: "pending",
discardedAt: null, discardedAt: null,
@@ -205,8 +205,8 @@ export async function restoreDiscardedInboxItemAction(
}) })
.where( .where(
and( and(
eq(preLancamentos.id, data.inboxItemId), eq(inboxItems.id, data.inboxItemId),
eq(preLancamentos.userId, user.id), eq(inboxItems.userId, user.id),
), ),
); );
@@ -226,12 +226,12 @@ export async function deleteInboxItemAction(
const data = deleteInboxSchema.parse(input); const data = deleteInboxSchema.parse(input);
const [item] = await db const [item] = await db
.select({ status: preLancamentos.status }) .select({ status: inboxItems.status })
.from(preLancamentos) .from(inboxItems)
.where( .where(
and( and(
eq(preLancamentos.id, data.inboxItemId), eq(inboxItems.id, data.inboxItemId),
eq(preLancamentos.userId, user.id), eq(inboxItems.userId, user.id),
), ),
) )
.limit(1); .limit(1);
@@ -248,11 +248,11 @@ export async function deleteInboxItemAction(
} }
await db await db
.delete(preLancamentos) .delete(inboxItems)
.where( .where(
and( and(
eq(preLancamentos.id, data.inboxItemId), eq(inboxItems.id, data.inboxItemId),
eq(preLancamentos.userId, user.id), eq(inboxItems.userId, user.id),
), ),
); );
@@ -272,14 +272,11 @@ export async function bulkDeleteInboxItemsAction(
const data = bulkDeleteInboxSchema.parse(input); const data = bulkDeleteInboxSchema.parse(input);
const result = await db const result = await db
.delete(preLancamentos) .delete(inboxItems)
.where( .where(
and( and(eq(inboxItems.userId, user.id), eq(inboxItems.status, data.status)),
eq(preLancamentos.userId, user.id),
eq(preLancamentos.status, data.status),
),
) )
.returning({ id: preLancamentos.id }); .returning({ id: inboxItems.id });
revalidateInbox(); revalidateInbox();

View File

@@ -10,7 +10,7 @@ import {
markInboxAsProcessedAction, markInboxAsProcessedAction,
restoreDiscardedInboxItemAction, restoreDiscardedInboxItemAction,
} from "@/features/inbox/actions"; } from "@/features/inbox/actions";
import { LancamentoDialog } from "@/features/transactions/components/dialogs/transaction-dialog/transaction-dialog"; import { TransactionDialog } from "@/features/transactions/components/dialogs/transaction-dialog/transaction-dialog";
import { ConfirmActionDialog } from "@/shared/components/confirm-action-dialog"; import { ConfirmActionDialog } from "@/shared/components/confirm-action-dialog";
import { EmptyState } from "@/shared/components/empty-state"; import { EmptyState } from "@/shared/components/empty-state";
import { Button } from "@/shared/components/ui/button"; import { Button } from "@/shared/components/ui/button";
@@ -29,12 +29,12 @@ interface InboxPageProps {
pendingItems: InboxItem[]; pendingItems: InboxItem[];
processedItems: InboxItem[]; processedItems: InboxItem[];
discardedItems: InboxItem[]; discardedItems: InboxItem[];
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[];
appLogoMap: Record<string, string>; appLogoMap: Record<string, string>;
} }
@@ -43,12 +43,12 @@ export function InboxPage({
pendingItems, pendingItems,
processedItems, processedItems,
discardedItems, discardedItems,
pagadorOptions, payerOptions,
splitPagadorOptions, splitPayerOptions,
defaultPagadorId, defaultPayerId,
contaOptions, accountOptions,
cartaoOptions, cardOptions,
categoriaOptions, categoryOptions,
estabelecimentos, estabelecimentos,
appLogoMap, appLogoMap,
}: InboxPageProps) { }: InboxPageProps) {
@@ -272,14 +272,14 @@ export function InboxPage({
const appName = itemToProcess?.sourceAppName?.toLowerCase(); const appName = itemToProcess?.sourceAppName?.toLowerCase();
if (!appName) return null; if (!appName) return null;
for (const option of cartaoOptions) { for (const option of cardOptions) {
const label = option.label.toLowerCase(); const label = option.label.toLowerCase();
if (label.includes(appName) || appName.includes(label)) { if (label.includes(appName) || appName.includes(label)) {
return option.value; return option.value;
} }
} }
return null; return null;
}, [itemToProcess?.sourceAppName, cartaoOptions]); }, [itemToProcess?.sourceAppName, cardOptions]);
const renderEmptyState = (message: string) => ( const renderEmptyState = (message: string) => (
<Card className="flex min-h-[50vh] w-full items-center justify-center py-12"> <Card className="flex min-h-[50vh] w-full items-center justify-center py-12">
@@ -378,21 +378,21 @@ export function InboxPage({
</TabsContent> </TabsContent>
</Tabs> </Tabs>
<LancamentoDialog <TransactionDialog
mode="create" mode="create"
open={processOpen} open={processOpen}
onOpenChange={handleProcessOpenChange} onOpenChange={handleProcessOpenChange}
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}
defaultPurchaseDate={defaultPurchaseDate} defaultPurchaseDate={defaultPurchaseDate}
defaultName={defaultName} defaultName={defaultName}
defaultAmount={defaultAmount} defaultAmount={defaultAmount}
defaultCartaoId={matchedCartaoId} defaultCardId={matchedCartaoId}
defaultPaymentMethod={matchedCartaoId ? "Cartão de crédito" : null} defaultPaymentMethod={matchedCartaoId ? "Cartão de crédito" : null}
forceShowTransactionType forceShowTransactionType
onSuccess={handleLancamentoSuccess} onSuccess={handleLancamentoSuccess}

View File

@@ -10,7 +10,7 @@ export interface InboxItem {
parsedName: string | null; parsedName: string | null;
parsedAmount: string | null; parsedAmount: string | null;
status: string; status: string;
lancamentoId: string | null; transactionId: string | null;
processedAt: Date | null; processedAt: Date | null;
discardedAt: Date | null; discardedAt: Date | null;
createdAt: Date; createdAt: Date;

View File

@@ -1,5 +1,5 @@
import { and, desc, eq } from "drizzle-orm"; import { and, desc, eq } from "drizzle-orm";
import { cartoes, categorias, contas, preLancamentos } from "@/db/schema"; import { cards, categories, financialAccounts, inboxItems } from "@/db/schema";
import type { import type {
InboxItem, InboxItem,
SelectOption, SelectOption,
@@ -9,8 +9,8 @@ import {
buildSluggedFilters, buildSluggedFilters,
} 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";
@@ -20,11 +20,9 @@ export async function fetchInboxItems(
): Promise<InboxItem[]> { ): Promise<InboxItem[]> {
const items = await db const items = await db
.select() .select()
.from(preLancamentos) .from(inboxItems)
.where( .where(and(eq(inboxItems.userId, userId), eq(inboxItems.status, status)))
and(eq(preLancamentos.userId, userId), eq(preLancamentos.status, status)), .orderBy(desc(inboxItems.createdAt));
)
.orderBy(desc(preLancamentos.createdAt));
return items; return items;
} }
@@ -35,54 +33,57 @@ export async function fetchInboxItemById(
): Promise<InboxItem | null> { ): Promise<InboxItem | null> {
const [item] = await db const [item] = await db
.select() .select()
.from(preLancamentos) .from(inboxItems)
.where( .where(and(eq(inboxItems.id, itemId), eq(inboxItems.userId, userId)))
and(eq(preLancamentos.id, itemId), eq(preLancamentos.userId, userId)),
)
.limit(1); .limit(1);
return item ?? null; return item ?? null;
} }
export async function fetchCategoriasForSelect( export async function fetchCategoriesForSelect(
userId: string, userId: string,
type?: string, type?: string,
): Promise<SelectOption[]> { ): Promise<SelectOption[]> {
const query = db const rows = await db
.select({ id: categorias.id, name: categorias.name }) .select({ id: categories.id, name: categories.name })
.from(categorias) .from(categories)
.where( .where(
type type
? and(eq(categorias.userId, userId), eq(categorias.type, type)) ? and(eq(categories.userId, userId), eq(categories.type, type))
: eq(categorias.userId, userId), : eq(categories.userId, userId),
) )
.orderBy(categorias.name); .orderBy(categories.name);
return query; return rows.map((row) => ({ value: row.id, label: row.name }));
} }
export async function fetchContasForSelect( export async function fetchAccountsForSelect(
userId: string, userId: string,
): Promise<SelectOption[]> { ): Promise<SelectOption[]> {
const items = await db const rows = await db
.select({ id: contas.id, name: contas.name }) .select({ id: financialAccounts.id, name: financialAccounts.name })
.from(contas) .from(financialAccounts)
.where(and(eq(contas.userId, userId), eq(contas.status, "ativo"))) .where(
.orderBy(contas.name); and(
eq(financialAccounts.userId, userId),
eq(financialAccounts.status, "ativo"),
),
)
.orderBy(financialAccounts.name);
return items; return rows.map((row) => ({ value: row.id, label: row.name }));
} }
export async function fetchCartoesForSelect( export async function fetchCardsForSelect(
userId: string, userId: string,
): Promise<(SelectOption & { lastDigits?: string })[]> { ): Promise<(SelectOption & { lastDigits?: string })[]> {
const items = await db const rows = await db
.select({ id: cartoes.id, name: cartoes.name }) .select({ id: cards.id, name: cards.name })
.from(cartoes) .from(cards)
.where(and(eq(cartoes.userId, userId), eq(cartoes.status, "ativo"))) .where(and(eq(cards.userId, userId), eq(cards.status, "ativo")))
.orderBy(cartoes.name); .orderBy(cards.name);
return items; return rows.map((row) => ({ value: row.id, label: row.name }));
} }
export async function fetchAppLogoMap( export async function fetchAppLogoMap(
@@ -90,13 +91,13 @@ export async function fetchAppLogoMap(
): Promise<Record<string, string>> { ): Promise<Record<string, string>> {
const [userCartoes, userContas] = await Promise.all([ const [userCartoes, userContas] = await Promise.all([
db db
.select({ name: cartoes.name, logo: cartoes.logo }) .select({ name: cards.name, logo: cards.logo })
.from(cartoes) .from(cards)
.where(eq(cartoes.userId, userId)), .where(eq(cards.userId, userId)),
db db
.select({ name: contas.name, logo: contas.logo }) .select({ name: financialAccounts.name, logo: financialAccounts.logo })
.from(contas) .from(financialAccounts)
.where(eq(contas.userId, userId)), .where(eq(financialAccounts.userId, userId)),
]); ]);
const logoMap: Record<string, string> = {}; const logoMap: Record<string, string> = {};
@@ -112,54 +113,51 @@ export async function fetchAppLogoMap(
export async function fetchPendingInboxCount(userId: string): Promise<number> { export async function fetchPendingInboxCount(userId: string): Promise<number> {
const items = await db const items = await db
.select({ id: preLancamentos.id }) .select({ id: inboxItems.id })
.from(preLancamentos) .from(inboxItems)
.where( .where(
and( and(eq(inboxItems.userId, userId), eq(inboxItems.status, "pending")),
eq(preLancamentos.userId, userId),
eq(preLancamentos.status, "pending"),
),
); );
return items.length; return items.length;
} }
/** /**
* Fetch all data needed for the LancamentoDialog in inbox context * Fetch all data needed for the TransactionDialog in inbox context
*/ */
export async function fetchInboxDialogData(userId: string): Promise<{ export async function fetchInboxDialogData(userId: string): Promise<{
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[];
}> { }> {
const filterSources = await fetchLancamentoFilterSources(userId); const filterSources = await fetchTransactionFilterSources(userId);
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,
}); });
const estabelecimentos = await fetchRecentEstablishments(userId); const estabelecimentos = await fetchRecentEstablishments(userId);
return { return {
pagadorOptions, payerOptions,
splitPagadorOptions, splitPayerOptions,
defaultPagadorId, defaultPayerId,
contaOptions, accountOptions,
cartaoOptions, cardOptions,
categoriaOptions, categoryOptions,
estabelecimentos, estabelecimentos,
}; };
} }

View File

@@ -2,13 +2,7 @@
import { and, eq, sql } from "drizzle-orm"; import { and, eq, sql } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { import { cards, categories, invoices, payers, transactions } from "@/db/schema";
cartoes,
categorias,
faturas,
lancamentos,
pagadores,
} from "@/db/schema";
import { buildInvoicePaymentNote } from "@/shared/lib/accounts/constants"; import { buildInvoicePaymentNote } from "@/shared/lib/accounts/constants";
import { revalidateForEntity } from "@/shared/lib/actions/helpers"; import { revalidateForEntity } from "@/shared/lib/actions/helpers";
import { getUser } from "@/shared/lib/auth/server"; import { getUser } from "@/shared/lib/auth/server";
@@ -19,7 +13,7 @@ import {
type InvoicePaymentStatus, type InvoicePaymentStatus,
PERIOD_FORMAT_REGEX, PERIOD_FORMAT_REGEX,
} from "@/shared/lib/invoices"; } from "@/shared/lib/invoices";
import { PAGADOR_ROLE_ADMIN } from "@/shared/lib/payers/constants"; import { PAYER_ROLE_ADMIN } from "@/shared/lib/payers/constants";
import { import {
getBusinessTodayDate, getBusinessTodayDate,
parseLocalDateString, parseLocalDateString,
@@ -29,7 +23,7 @@ const isValidPaymentDate = (value: string) =>
!Number.isNaN(parseLocalDateString(value).getTime()); !Number.isNaN(parseLocalDateString(value).getTime());
const updateInvoicePaymentStatusSchema = z.object({ const updateInvoicePaymentStatusSchema = z.object({
cartaoId: z.string({ message: "Cartão inválido." }).uuid("Cartão inválido."), cardId: z.string({ message: "Cartão inválido." }).uuid("Cartão inválido."),
period: z period: z
.string({ message: "Período inválido." }) .string({ message: "Período inválido." })
.regex(PERIOD_FORMAT_REGEX, "Período inválido."), .regex(PERIOD_FORMAT_REGEX, "Período inválido."),
@@ -53,7 +47,7 @@ type ActionResult =
| { success: false; error: string }; | { success: false; error: string };
const successMessageByStatus: Record<InvoicePaymentStatus, string> = { const successMessageByStatus: Record<InvoicePaymentStatus, string> = {
[INVOICE_PAYMENT_STATUS.PAID]: "Fatura marcada como paga.", [INVOICE_PAYMENT_STATUS.PAID]: "Invoice marcada como paga.",
[INVOICE_PAYMENT_STATUS.PENDING]: "Pagamento da fatura foi revertido.", [INVOICE_PAYMENT_STATUS.PENDING]: "Pagamento da fatura foi revertido.",
}; };
@@ -68,36 +62,36 @@ export async function updateInvoicePaymentStatusAction(
const data = updateInvoicePaymentStatusSchema.parse(input); const data = updateInvoicePaymentStatusSchema.parse(input);
await db.transaction(async (tx: typeof db) => { await db.transaction(async (tx: typeof db) => {
const card = await tx.query.cartoes.findFirst({ const card = await tx.query.cards.findFirst({
columns: { id: true, contaId: true, name: true }, columns: { id: true, accountId: true, name: true },
where: and(eq(cartoes.id, data.cartaoId), eq(cartoes.userId, user.id)), where: and(eq(cards.id, data.cardId), eq(cards.userId, user.id)),
}); });
if (!card) { if (!card) {
throw new Error("Cartão não encontrado."); throw new Error("Cartão não encontrado.");
} }
const existingInvoice = await tx.query.faturas.findFirst({ const existingInvoice = await tx.query.invoices.findFirst({
columns: { columns: {
id: true, id: true,
}, },
where: and( where: and(
eq(faturas.cartaoId, data.cartaoId), eq(invoices.cardId, data.cardId),
eq(faturas.userId, user.id), eq(invoices.userId, user.id),
eq(faturas.period, data.period), eq(invoices.period, data.period),
), ),
}); });
if (existingInvoice) { if (existingInvoice) {
await tx await tx
.update(faturas) .update(invoices)
.set({ .set({
paymentStatus: data.status, paymentStatus: data.status,
}) })
.where(eq(faturas.id, existingInvoice.id)); .where(eq(invoices.id, existingInvoice.id));
} else { } else {
await tx.insert(faturas).values({ await tx.insert(invoices).values({
cartaoId: data.cartaoId, cardId: data.cardId,
period: data.period, period: data.period,
paymentStatus: data.status, paymentStatus: data.status,
userId: user.id, userId: user.id,
@@ -107,13 +101,13 @@ export async function updateInvoicePaymentStatusAction(
const shouldMarkAsPaid = data.status === INVOICE_PAYMENT_STATUS.PAID; const shouldMarkAsPaid = data.status === INVOICE_PAYMENT_STATUS.PAID;
await tx await tx
.update(lancamentos) .update(transactions)
.set({ isSettled: shouldMarkAsPaid }) .set({ isSettled: shouldMarkAsPaid })
.where( .where(
and( and(
eq(lancamentos.userId, user.id), eq(transactions.userId, user.id),
eq(lancamentos.cartaoId, card.id), eq(transactions.cardId, card.id),
eq(lancamentos.period, data.period), eq(transactions.period, data.period),
), ),
); );
@@ -124,39 +118,39 @@ export async function updateInvoicePaymentStatusAction(
.select({ .select({
total: sql<number>` total: sql<number>`
coalesce( coalesce(
sum(${lancamentos.amount}), sum(${transactions.amount}),
0 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, user.id), eq(transactions.userId, user.id),
eq(lancamentos.cartaoId, card.id), eq(transactions.cardId, card.id),
eq(lancamentos.period, data.period), eq(transactions.period, data.period),
eq(pagadores.role, PAGADOR_ROLE_ADMIN), eq(payers.role, PAYER_ROLE_ADMIN),
), ),
); );
const adminShare = Number(adminShareRow?.total ?? 0); const adminShare = Number(adminShareRow?.total ?? 0);
const adminPayableAmount = Math.abs(Math.min(adminShare, 0)); const adminPayableAmount = Math.abs(Math.min(adminShare, 0));
if (adminPayableAmount > 0 && card.contaId) { if (adminPayableAmount > 0 && card.accountId) {
const adminPagador = await tx.query.pagadores.findFirst({ const adminPagador = await tx.query.payers.findFirst({
columns: { id: true }, columns: { id: true },
where: and( where: and(
eq(pagadores.userId, user.id), eq(payers.userId, user.id),
eq(pagadores.role, PAGADOR_ROLE_ADMIN), eq(payers.role, PAYER_ROLE_ADMIN),
), ),
}); });
const paymentCategory = await tx.query.categorias.findFirst({ const paymentCategory = await tx.query.categories.findFirst({
columns: { id: true }, columns: { id: true },
where: and( where: and(
eq(categorias.userId, user.id), eq(categories.userId, user.id),
eq(categorias.name, "Pagamentos"), eq(categories.name, "Pagamentos"),
), ),
}); });
@@ -178,42 +172,42 @@ export async function updateInvoicePaymentStatusAction(
period: data.period, period: data.period,
isSettled: true, isSettled: true,
userId: user.id, userId: user.id,
contaId: card.contaId, accountId: card.accountId,
categoriaId: paymentCategory?.id ?? null, categoryId: paymentCategory?.id ?? null,
pagadorId: adminPagador.id, payerId: adminPagador.id,
}; };
const existingPayment = await tx.query.lancamentos.findFirst({ const existingPayment = await tx.query.transactions.findFirst({
columns: { id: true }, columns: { id: true },
where: and( where: and(
eq(lancamentos.userId, user.id), eq(transactions.userId, user.id),
eq(lancamentos.note, invoiceNote), eq(transactions.note, invoiceNote),
), ),
}); });
if (existingPayment) { if (existingPayment) {
await tx await tx
.update(lancamentos) .update(transactions)
.set(payload) .set(payload)
.where(eq(lancamentos.id, existingPayment.id)); .where(eq(transactions.id, existingPayment.id));
} else { } else {
await tx.insert(lancamentos).values(payload); await tx.insert(transactions).values(payload);
} }
} }
} }
} else { } else {
await tx await tx
.delete(lancamentos) .delete(transactions)
.where( .where(
and( and(
eq(lancamentos.userId, user.id), eq(transactions.userId, user.id),
eq(lancamentos.note, invoiceNote), eq(transactions.note, invoiceNote),
), ),
); );
} }
}); });
revalidateForEntity("cartoes"); revalidateForEntity("cards");
return { success: true, message: successMessageByStatus[data.status] }; return { success: true, message: successMessageByStatus[data.status] };
} catch (error) { } catch (error) {
@@ -232,7 +226,7 @@ export async function updateInvoicePaymentStatusAction(
} }
const updatePaymentDateSchema = z.object({ const updatePaymentDateSchema = z.object({
cartaoId: z.string({ message: "Cartão inválido." }).uuid("Cartão inválido."), cardId: z.string({ message: "Cartão inválido." }).uuid("Cartão inválido."),
period: z period: z
.string({ message: "Período inválido." }) .string({ message: "Período inválido." })
.regex(PERIOD_FORMAT_REGEX, "Período inválido."), .regex(PERIOD_FORMAT_REGEX, "Período inválido."),
@@ -253,9 +247,9 @@ export async function updatePaymentDateAction(
const data = updatePaymentDateSchema.parse(input); const data = updatePaymentDateSchema.parse(input);
await db.transaction(async (tx: typeof db) => { await db.transaction(async (tx: typeof db) => {
const card = await tx.query.cartoes.findFirst({ const card = await tx.query.cards.findFirst({
columns: { id: true }, columns: { id: true },
where: and(eq(cartoes.id, data.cartaoId), eq(cartoes.userId, user.id)), where: and(eq(cards.id, data.cardId), eq(cards.userId, user.id)),
}); });
if (!card) { if (!card) {
@@ -264,11 +258,11 @@ export async function updatePaymentDateAction(
const invoiceNote = buildInvoicePaymentNote(card.id, data.period); const invoiceNote = buildInvoicePaymentNote(card.id, data.period);
const existingPayment = await tx.query.lancamentos.findFirst({ const existingPayment = await tx.query.transactions.findFirst({
columns: { id: true }, columns: { id: true },
where: and( where: and(
eq(lancamentos.userId, user.id), eq(transactions.userId, user.id),
eq(lancamentos.note, invoiceNote), eq(transactions.note, invoiceNote),
), ),
}); });
@@ -277,14 +271,14 @@ export async function updatePaymentDateAction(
} }
await tx await tx
.update(lancamentos) .update(transactions)
.set({ .set({
purchaseDate: parseLocalDateString(data.paymentDate), purchaseDate: parseLocalDateString(data.paymentDate),
}) })
.where(eq(lancamentos.id, existingPayment.id)); .where(eq(transactions.id, existingPayment.id));
}); });
revalidateForEntity("cartoes"); revalidateForEntity("cards");
return { success: true, message: "Data de pagamento atualizada." }; return { success: true, message: "Data de pagamento atualizada." };
} catch (error) { } catch (error) {

View File

@@ -33,7 +33,7 @@ import { cn } from "@/shared/utils/ui";
import { EditPaymentDateDialog } from "./edit-payment-date-dialog"; import { EditPaymentDateDialog } from "./edit-payment-date-dialog";
type InvoiceSummaryCardProps = { type InvoiceSummaryCardProps = {
cartaoId: string; cardId: string;
period: string; period: string;
cardName: string; cardName: string;
cardBrand: string | null; cardBrand: string | null;
@@ -74,7 +74,7 @@ const getCardStatusDotColor = (status: string | null) => {
}; };
export function InvoiceSummaryCard({ export function InvoiceSummaryCard({
cartaoId, cardId,
period, period,
cardName, cardName,
cardBrand, cardBrand,
@@ -113,7 +113,7 @@ export function InvoiceSummaryCard({
const handleAction = () => { const handleAction = () => {
startTransition(async () => { startTransition(async () => {
const result = await updateInvoicePaymentStatusAction({ const result = await updateInvoicePaymentStatusAction({
cartaoId, cardId,
period, period,
status: targetStatus, status: targetStatus,
paymentDate: paymentDate:
@@ -136,7 +136,7 @@ export function InvoiceSummaryCard({
setPaymentDate(newDate); setPaymentDate(newDate);
startTransition(async () => { startTransition(async () => {
const result = await updatePaymentDateAction({ const result = await updatePaymentDateAction({
cartaoId, cardId,
period, period,
paymentDate: newDate.toISOString().split("T")[0] ?? "", paymentDate: newDate.toISOString().split("T")[0] ?? "",
}); });
@@ -177,7 +177,7 @@ export function InvoiceSummaryCard({
{cardName} {cardName}
</CardTitle> </CardTitle>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Fatura de {periodLabel} Invoice de {periodLabel}
</p> </p>
</div> </div>
{actions ? <div className="shrink-0">{actions}</div> : null} {actions ? <div className="shrink-0">{actions}</div> : null}

View File

@@ -1,5 +1,5 @@
import { and, desc, eq, type SQL, sum } from "drizzle-orm"; import { and, desc, eq, type SQL, sum } from "drizzle-orm";
import { cartoes, faturas, lancamentos } from "@/db/schema"; import { cards, invoices, transactions } from "@/db/schema";
import { buildInvoicePaymentNote } from "@/shared/lib/accounts/constants"; import { buildInvoicePaymentNote } from "@/shared/lib/accounts/constants";
import { db } from "@/shared/lib/db"; import { db } from "@/shared/lib/db";
import { import {
@@ -18,8 +18,8 @@ const toNumber = (value: string | number | null | undefined) => {
return Number.isNaN(parsed) ? 0 : parsed; return Number.isNaN(parsed) ? 0 : parsed;
}; };
export async function fetchCardData(userId: string, cartaoId: string) { export async function fetchCardData(userId: string, cardId: string) {
const card = await db.query.cartoes.findFirst({ const card = await db.query.cards.findFirst({
columns: { columns: {
id: true, id: true,
name: true, name: true,
@@ -30,9 +30,9 @@ export async function fetchCardData(userId: string, cartaoId: string) {
limit: true, limit: true,
status: true, status: true,
note: true, note: true,
contaId: true, accountId: true,
}, },
where: and(eq(cartoes.id, cartaoId), eq(cartoes.userId, userId)), where: and(eq(cards.id, cardId), eq(cards.userId, userId)),
}); });
return card; return card;
@@ -40,7 +40,7 @@ export async function fetchCardData(userId: string, cartaoId: string) {
export async function fetchInvoiceData( export async function fetchInvoiceData(
userId: string, userId: string,
cartaoId: string, cardId: string,
selectedPeriod: string, selectedPeriod: string,
): Promise<{ ): Promise<{
totalAmount: number; totalAmount: number;
@@ -48,26 +48,26 @@ export async function fetchInvoiceData(
paymentDate: Date | null; paymentDate: Date | null;
}> { }> {
const [invoiceRow, totalRow] = await Promise.all([ const [invoiceRow, totalRow] = await Promise.all([
db.query.faturas.findFirst({ db.query.invoices.findFirst({
columns: { columns: {
id: true, id: true,
period: true, period: true,
paymentStatus: true, paymentStatus: true,
}, },
where: and( where: and(
eq(faturas.cartaoId, cartaoId), eq(invoices.cardId, cardId),
eq(faturas.userId, userId), eq(invoices.userId, userId),
eq(faturas.period, selectedPeriod), eq(invoices.period, selectedPeriod),
), ),
}), }),
db db
.select({ totalAmount: sum(lancamentos.amount) }) .select({ totalAmount: sum(transactions.amount) })
.from(lancamentos) .from(transactions)
.where( .where(
and( and(
eq(lancamentos.userId, userId), eq(transactions.userId, userId),
eq(lancamentos.cartaoId, cartaoId), eq(transactions.cardId, cardId),
eq(lancamentos.period, selectedPeriod), eq(transactions.period, selectedPeriod),
), ),
), ),
]); ]);
@@ -85,14 +85,14 @@ export async function fetchInvoiceData(
// Buscar data do pagamento se a fatura estiver paga // Buscar data do pagamento se a fatura estiver paga
let paymentDate: Date | null = null; let paymentDate: Date | null = null;
if (invoiceStatus === INVOICE_PAYMENT_STATUS.PAID) { if (invoiceStatus === INVOICE_PAYMENT_STATUS.PAID) {
const invoiceNote = buildInvoicePaymentNote(cartaoId, selectedPeriod); const invoiceNote = buildInvoicePaymentNote(cardId, selectedPeriod);
const paymentLancamento = await db.query.lancamentos.findFirst({ const paymentLancamento = await db.query.transactions.findFirst({
columns: { columns: {
purchaseDate: true, purchaseDate: true,
}, },
where: and( where: and(
eq(lancamentos.userId, userId), eq(transactions.userId, userId),
eq(lancamentos.note, invoiceNote), eq(transactions.note, invoiceNote),
), ),
}); });
paymentDate = paymentLancamento?.purchaseDate paymentDate = paymentLancamento?.purchaseDate
@@ -103,15 +103,15 @@ export async function fetchInvoiceData(
return { totalAmount, invoiceStatus, paymentDate }; return { totalAmount, invoiceStatus, paymentDate };
} }
export async function fetchCardLancamentos(filters: SQL[]) { export async function fetchCardTransactions(filters: SQL[]) {
return db.query.lancamentos.findMany({ return db.query.transactions.findMany({
where: and(...filters), where: and(...filters),
with: { with: {
pagador: true, payer: true,
conta: true, financialAccount: true,
cartao: true, card: true,
categoria: true, category: true,
}, },
orderBy: desc(lancamentos.purchaseDate), orderBy: desc(transactions.purchaseDate),
}); });
} }

View File

@@ -2,7 +2,7 @@
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { anotacoes } from "@/db/schema"; import { notes } from "@/db/schema";
import { import {
handleActionError, handleActionError,
revalidateForEntity, revalidateForEntity,
@@ -64,8 +64,8 @@ const deleteNoteSchema = z.object({
id: uuidSchema("Anotação"), id: uuidSchema("Anotação"),
}); });
type NoteCreateInput = z.infer<typeof createNoteSchema>; type NoteCreateInput = z.input<typeof createNoteSchema>;
type NoteUpdateInput = z.infer<typeof updateNoteSchema>; type NoteUpdateInput = z.input<typeof updateNoteSchema>;
type NoteDeleteInput = z.infer<typeof deleteNoteSchema>; type NoteDeleteInput = z.infer<typeof deleteNoteSchema>;
export async function createNoteAction( export async function createNoteAction(
@@ -75,7 +75,7 @@ export async function createNoteAction(
const user = await getUser(); const user = await getUser();
const data = createNoteSchema.parse(input); const data = createNoteSchema.parse(input);
await db.insert(anotacoes).values({ await db.insert(notes).values({
title: data.title, title: data.title,
description: data.description, description: data.description,
type: data.type, type: data.type,
@@ -84,7 +84,7 @@ export async function createNoteAction(
userId: user.id, userId: user.id,
}); });
revalidateForEntity("anotacoes"); revalidateForEntity("notes");
return { success: true, message: "Anotação criada com sucesso." }; return { success: true, message: "Anotação criada com sucesso." };
} catch (error) { } catch (error) {
@@ -100,7 +100,7 @@ export async function updateNoteAction(
const data = updateNoteSchema.parse(input); const data = updateNoteSchema.parse(input);
const [updated] = await db const [updated] = await db
.update(anotacoes) .update(notes)
.set({ .set({
title: data.title, title: data.title,
description: data.description, description: data.description,
@@ -110,8 +110,8 @@ export async function updateNoteAction(
? JSON.stringify(data.tasks) ? JSON.stringify(data.tasks)
: null, : null,
}) })
.where(and(eq(anotacoes.id, data.id), eq(anotacoes.userId, user.id))) .where(and(eq(notes.id, data.id), eq(notes.userId, user.id)))
.returning({ id: anotacoes.id }); .returning({ id: notes.id });
if (!updated) { if (!updated) {
return { return {
@@ -120,7 +120,7 @@ export async function updateNoteAction(
}; };
} }
revalidateForEntity("anotacoes"); revalidateForEntity("notes");
return { success: true, message: "Anotação atualizada com sucesso." }; return { success: true, message: "Anotação atualizada com sucesso." };
} catch (error) { } catch (error) {
@@ -136,9 +136,9 @@ export async function deleteNoteAction(
const data = deleteNoteSchema.parse(input); const data = deleteNoteSchema.parse(input);
const [deleted] = await db const [deleted] = await db
.delete(anotacoes) .delete(notes)
.where(and(eq(anotacoes.id, data.id), eq(anotacoes.userId, user.id))) .where(and(eq(notes.id, data.id), eq(notes.userId, user.id)))
.returning({ id: anotacoes.id }); .returning({ id: notes.id });
if (!deleted) { if (!deleted) {
return { return {
@@ -147,7 +147,7 @@ export async function deleteNoteAction(
}; };
} }
revalidateForEntity("anotacoes"); revalidateForEntity("notes");
return { success: true, message: "Anotação removida com sucesso." }; return { success: true, message: "Anotação removida com sucesso." };
} catch (error) { } catch (error) {
@@ -157,12 +157,12 @@ export async function deleteNoteAction(
const arquivarNoteSchema = z.object({ const arquivarNoteSchema = z.object({
id: uuidSchema("Anotação"), id: uuidSchema("Anotação"),
arquivada: z.boolean(), archived: z.boolean(),
}); });
type NoteArquivarInput = z.infer<typeof arquivarNoteSchema>; type NoteArquivarInput = z.infer<typeof arquivarNoteSchema>;
export async function arquivarAnotacaoAction( export async function archiveNoteAction(
input: NoteArquivarInput, input: NoteArquivarInput,
): Promise<ActionResult> { ): Promise<ActionResult> {
try { try {
@@ -170,12 +170,12 @@ export async function arquivarAnotacaoAction(
const data = arquivarNoteSchema.parse(input); const data = arquivarNoteSchema.parse(input);
const [updated] = await db const [updated] = await db
.update(anotacoes) .update(notes)
.set({ .set({
arquivada: data.arquivada, archived: data.archived,
}) })
.where(and(eq(anotacoes.id, data.id), eq(anotacoes.userId, user.id))) .where(and(eq(notes.id, data.id), eq(notes.userId, user.id)))
.returning({ id: anotacoes.id }); .returning({ id: notes.id });
if (!updated) { if (!updated) {
return { return {
@@ -184,12 +184,12 @@ export async function arquivarAnotacaoAction(
}; };
} }
revalidateForEntity("anotacoes"); revalidateForEntity("notes");
return { return {
success: true, success: true,
message: data.arquivada message: data.archived
? "Anotação arquivada com sucesso." ? "Anotação archived com sucesso."
: "Anotação desarquivada com sucesso.", : "Anotação desarquivada com sucesso.",
}; };
} catch (error) { } catch (error) {

View File

@@ -215,7 +215,7 @@ export function NoteDialog({
setDialogOpen(false); setDialogOpen(false);
return; return;
} }
setErrorMessage(result.error); setErrorMessage(result.error ?? null);
toast.error(result.error); toast.error(result.error);
titleRef.current?.focus(); titleRef.current?.focus();
}); });

View File

@@ -3,10 +3,7 @@
import { RiAddCircleLine, RiTodoLine } from "@remixicon/react"; import { RiAddCircleLine, RiTodoLine } from "@remixicon/react";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { import { archiveNoteAction, deleteNoteAction } from "@/features/notes/actions";
arquivarAnotacaoAction,
deleteNoteAction,
} from "@/features/notes/actions";
import { ConfirmActionDialog } from "@/shared/components/confirm-action-dialog"; import { ConfirmActionDialog } from "@/shared/components/confirm-action-dialog";
import { EmptyState } from "@/shared/components/empty-state"; import { EmptyState } from "@/shared/components/empty-state";
import { Button } from "@/shared/components/ui/button"; import { Button } from "@/shared/components/ui/button";
@@ -115,9 +112,9 @@ export function NotesPage({ notes, archivedNotes }: NotesPageProps) {
return; return;
} }
const result = await arquivarAnotacaoAction({ const result = await archiveNoteAction({
id: noteToArquivar.id, id: noteToArquivar.id,
arquivada: !isArquivadas, archived: !isArquivadas,
}); });
if (result.success) { if (result.success) {
@@ -171,7 +168,7 @@ export function NotesPage({ notes, archivedNotes }: NotesPageProps) {
media={<RiTodoLine className="size-6 text-primary" />} media={<RiTodoLine className="size-6 text-primary" />}
title={ title={
isArchived isArchived
? "Nenhuma anotação arquivada" ? "Nenhuma anotação archived"
: "Nenhuma anotação registrada" : "Nenhuma anotação registrada"
} }
description={ description={

View File

@@ -12,7 +12,7 @@ export interface Note {
description: string; description: string;
type: NoteType; type: NoteType;
tasks?: Task[]; tasks?: Task[];
arquivada: boolean; archived: boolean;
createdAt: string; createdAt: string;
} }

View File

@@ -1,5 +1,5 @@
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
import { type Anotacao, anotacoes } from "@/db/schema"; import { type Note, notes } from "@/db/schema";
import { db } from "@/shared/lib/db"; import { db } from "@/shared/lib/db";
export type Task = { export type Task = {
@@ -14,20 +14,17 @@ export type NoteData = {
description: string; description: string;
type: "nota" | "tarefa"; type: "nota" | "tarefa";
tasks?: Task[]; tasks?: Task[];
arquivada: boolean; archived: boolean;
createdAt: string; createdAt: string;
}; };
export async function fetchNotesForUser(userId: string): Promise<NoteData[]> { export async function fetchNotesForUser(userId: string): Promise<NoteData[]> {
const noteRows = 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: ( orderBy: (table, { desc }) => [desc(table.createdAt)],
note: typeof anotacoes.$inferSelect,
{ desc }: { desc: (field: unknown) => unknown },
) => [desc(note.createdAt)],
}); });
return noteRows.map((note: Anotacao) => { return noteRows.map((note: Note) => {
let tasks: Task[] | undefined; let tasks: Task[] | undefined;
// Parse tasks if they exist // Parse tasks if they exist
@@ -46,7 +43,7 @@ export async function fetchNotesForUser(userId: string): Promise<NoteData[]> {
description: (note.description ?? "").trim(), description: (note.description ?? "").trim(),
type: (note.type ?? "nota") as "nota" | "tarefa", type: (note.type ?? "nota") as "nota" | "tarefa",
tasks, tasks,
arquivada: note.arquivada, archived: note.archived,
createdAt: note.createdAt.toISOString(), createdAt: note.createdAt.toISOString(),
}; };
}); });
@@ -57,24 +54,21 @@ export async function fetchAllNotesForUser(
): Promise<{ activeNotes: NoteData[]; archivedNotes: NoteData[] }> { ): Promise<{ activeNotes: NoteData[]; archivedNotes: NoteData[] }> {
const [activeNotes, archivedNotes] = await Promise.all([ const [activeNotes, archivedNotes] = await Promise.all([
fetchNotesForUser(userId), fetchNotesForUser(userId),
fetchArquivadasForUser(userId), fetchArchivedForUser(userId),
]); ]);
return { activeNotes, archivedNotes }; return { activeNotes, archivedNotes };
} }
export async function fetchArquivadasForUser( export async function fetchArchivedForUser(
userId: string, userId: string,
): Promise<NoteData[]> { ): Promise<NoteData[]> {
const noteRows = await db.query.anotacoes.findMany({ const noteRows = await db.query.notes.findMany({
where: and(eq(anotacoes.userId, userId), eq(anotacoes.arquivada, true)), where: and(eq(notes.userId, userId), eq(notes.archived, true)),
orderBy: ( orderBy: (table, { desc }) => [desc(table.createdAt)],
note: typeof anotacoes.$inferSelect,
{ desc }: { desc: (field: unknown) => unknown },
) => [desc(note.createdAt)],
}); });
return noteRows.map((note: Anotacao) => { return noteRows.map((note: Note) => {
let tasks: Task[] | undefined; let tasks: Task[] | undefined;
// Parse tasks if they exist // Parse tasks if they exist
@@ -93,7 +87,7 @@ export async function fetchArquivadasForUser(
description: (note.description ?? "").trim(), description: (note.description ?? "").trim(),
type: (note.type ?? "nota") as "nota" | "tarefa", type: (note.type ?? "nota") as "nota" | "tarefa",
tasks, tasks,
arquivada: note.arquivada, archived: note.archived,
createdAt: note.createdAt.toISOString(), createdAt: note.createdAt.toISOString(),
}; };
}); });

View File

@@ -6,10 +6,10 @@ import { and, eq, isNull, ne } from "drizzle-orm";
import { revalidatePath } from "next/cache"; import { revalidatePath } from "next/cache";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { z } from "zod"; import { z } from "zod";
import { account, pagadores, tokensApi } from "@/db/schema"; import { account, apiTokens, payers } from "@/db/schema";
import { auth } from "@/shared/lib/auth/config"; import { auth } from "@/shared/lib/auth/config";
import { db, schema } from "@/shared/lib/db"; import { db, schema } from "@/shared/lib/db";
import { PAGADOR_ROLE_ADMIN } from "@/shared/lib/payers/constants"; import { PAYER_ROLE_ADMIN } from "@/shared/lib/payers/constants";
type ActionResponse<T = void> = { type ActionResponse<T = void> = {
success: boolean; success: boolean;
@@ -47,14 +47,12 @@ const updateEmailSchema = z
}); });
const deleteAccountSchema = z.object({ const deleteAccountSchema = z.object({
confirmation: z.literal("DELETAR", { confirmation: z.literal("DELETAR"),
errorMap: () => ({ message: 'Você deve digitar "DELETAR" para confirmar' }),
}),
}); });
const updatePreferencesSchema = z.object({ const updatePreferencesSchema = z.object({
extratoNoteAsColumn: z.boolean(), statementNoteAsColumn: z.boolean(),
lancamentosColumnOrder: z.array(z.string()).nullable(), transactionsColumnOrder: z.array(z.string()).nullable(),
}); });
// Actions // Actions
@@ -85,12 +83,12 @@ export async function updateNameAction(
// Sincronizar nome com o pagador admin // Sincronizar nome com o pagador admin
await db await db
.update(pagadores) .update(payers)
.set({ name: fullName }) .set({ name: fullName })
.where( .where(
and( and(
eq(pagadores.userId, session.user.id), eq(payers.userId, session.user.id),
eq(pagadores.role, PAGADOR_ROLE_ADMIN), eq(payers.role, PAYER_ROLE_ADMIN),
), ),
); );
@@ -147,7 +145,7 @@ export async function updatePasswordAction(
return { return {
success: false, success: false,
error: error:
"Não é possível alterar senha para contas autenticadas via Google", "Não é possível alterar senha para financialAccounts autenticadas via Google",
}; };
} }
@@ -170,8 +168,8 @@ export async function updatePasswordAction(
// Verificar se o erro é de senha incorreta // Verificar se o erro é de senha incorreta
if ( if (
authError?.message?.includes("password") || (authError as Error)?.message?.includes("password") ||
authError?.message?.includes("incorrect") (authError as Error)?.message?.includes("incorrect")
) { ) {
return { return {
success: false, success: false,
@@ -253,7 +251,7 @@ export async function updateEmailAction(
if (!storedHash) { if (!storedHash) {
return { return {
success: false, success: false,
error: "Conta de credencial não encontrada.", error: "FinancialAccount de credencial não encontrada.",
}; };
} }
@@ -350,7 +348,7 @@ export async function deleteAccountAction(
return { return {
success: true, success: true,
message: "Conta deletada com sucesso", message: "FinancialAccount deletada com sucesso",
}; };
} catch (error) { } catch (error) {
if (error instanceof z.ZodError) { if (error instanceof z.ZodError) {
@@ -360,7 +358,7 @@ export async function deleteAccountAction(
}; };
} }
console.error("Erro ao deletar conta:", error); console.error("Erro ao deletar financialAccount:", error);
return { return {
success: false, success: false,
error: "Erro ao deletar conta. Tente novamente.", error: "Erro ao deletar conta. Tente novamente.",
@@ -388,8 +386,8 @@ export async function updatePreferencesAction(
// Check if preferences exist, if not create them // Check if preferences exist, if not create them
const existingResult = await db const existingResult = await db
.select() .select()
.from(schema.preferenciasUsuario) .from(schema.userPreferences)
.where(eq(schema.preferenciasUsuario.userId, session.user.id)) .where(eq(schema.userPreferences.userId, session.user.id))
.limit(1); .limit(1);
const existing = existingResult[0] || null; const existing = existingResult[0] || null;
@@ -397,19 +395,19 @@ export async function updatePreferencesAction(
if (existing) { if (existing) {
// Update existing preferences // Update existing preferences
await db await db
.update(schema.preferenciasUsuario) .update(schema.userPreferences)
.set({ .set({
extratoNoteAsColumn: validated.extratoNoteAsColumn, statementNoteAsColumn: validated.statementNoteAsColumn,
lancamentosColumnOrder: validated.lancamentosColumnOrder, transactionsColumnOrder: validated.transactionsColumnOrder,
updatedAt: new Date(), updatedAt: new Date(),
}) })
.where(eq(schema.preferenciasUsuario.userId, session.user.id)); .where(eq(schema.userPreferences.userId, session.user.id));
} else { } else {
// Create new preferences // Create new preferences
await db.insert(schema.preferenciasUsuario).values({ await db.insert(schema.userPreferences).values({
userId: session.user.id, userId: session.user.id,
extratoNoteAsColumn: validated.extratoNoteAsColumn, statementNoteAsColumn: validated.statementNoteAsColumn,
lancamentosColumnOrder: validated.lancamentosColumnOrder, transactionsColumnOrder: validated.transactionsColumnOrder,
}); });
} }
@@ -480,7 +478,7 @@ export async function createApiTokenAction(
// Save to database // Save to database
const [newToken] = await db const [newToken] = await db
.insert(tokensApi) .insert(apiTokens)
.values({ .values({
userId: session.user.id, userId: session.user.id,
name: validated.name, name: validated.name,
@@ -488,7 +486,7 @@ export async function createApiTokenAction(
tokenPrefix, tokenPrefix,
expiresAt: null, // No expiration for now expiresAt: null, // No expiration for now
}) })
.returning({ id: tokensApi.id }); .returning({ id: apiTokens.id });
revalidatePath("/settings"); revalidatePath("/settings");
@@ -536,12 +534,12 @@ export async function revokeApiTokenAction(
// Find token and verify ownership // Find token and verify ownership
const [existingToken] = await db const [existingToken] = await db
.select() .select()
.from(tokensApi) .from(apiTokens)
.where( .where(
and( and(
eq(tokensApi.id, validated.tokenId), eq(apiTokens.id, validated.tokenId),
eq(tokensApi.userId, session.user.id), eq(apiTokens.userId, session.user.id),
isNull(tokensApi.revokedAt), isNull(apiTokens.revokedAt),
), ),
) )
.limit(1); .limit(1);
@@ -555,11 +553,11 @@ export async function revokeApiTokenAction(
// Revoke token // Revoke token
await db await db
.update(tokensApi) .update(apiTokens)
.set({ .set({
revokedAt: new Date(), revokedAt: new Date(),
}) })
.where(eq(tokensApi.id, validated.tokenId)); .where(eq(apiTokens.id, validated.tokenId));
revalidatePath("/settings"); revalidatePath("/settings");

View File

@@ -25,7 +25,7 @@ export function DeleteAccountForm() {
const handleDelete = () => { const handleDelete = () => {
startTransition(async () => { startTransition(async () => {
const result = await deleteAccountAction({ const result = await deleteAccountAction({
confirmation, confirmation: confirmation as "DELETAR",
}); });
if (result.success) { if (result.success) {

View File

@@ -79,8 +79,8 @@ export function PasskeysForm() {
setPasskeys( setPasskeys(
(data ?? []).map((p) => ({ (data ?? []).map((p) => ({
id: p.id, id: p.id,
name: p.name, name: p.name ?? null,
deviceType: p.deviceType, deviceType: p.deviceType as string,
createdAt: p.createdAt ? new Date(p.createdAt) : null, createdAt: p.createdAt ? new Date(p.createdAt) : null,
})), })),
); );

View File

@@ -30,8 +30,8 @@ import { Label } from "@/shared/components/ui/label";
import { Switch } from "@/shared/components/ui/switch"; import { Switch } from "@/shared/components/ui/switch";
interface PreferencesFormProps { interface PreferencesFormProps {
extratoNoteAsColumn: boolean; statementNoteAsColumn: boolean;
lancamentosColumnOrder: string[] | null; transactionsColumnOrder: string[] | null;
} }
function SortableColumnItem({ id }: { id: string }) { function SortableColumnItem({ id }: { id: string }) {
@@ -72,12 +72,12 @@ function SortableColumnItem({ id }: { id: string }) {
} }
export function PreferencesForm({ export function PreferencesForm({
extratoNoteAsColumn: initialExtratoNoteAsColumn, statementNoteAsColumn: initialExtratoNoteAsColumn,
lancamentosColumnOrder: initialColumnOrder, transactionsColumnOrder: initialColumnOrder,
}: PreferencesFormProps) { }: PreferencesFormProps) {
const router = useRouter(); const router = useRouter();
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const [extratoNoteAsColumn, setExtratoNoteAsColumn] = useState( const [statementNoteAsColumn, setExtratoNoteAsColumn] = useState(
initialExtratoNoteAsColumn, initialExtratoNoteAsColumn,
); );
const [columnOrder, setColumnOrder] = useState<string[]>( const [columnOrder, setColumnOrder] = useState<string[]>(
@@ -107,8 +107,8 @@ export function PreferencesForm({
startTransition(async () => { startTransition(async () => {
const result = await updatePreferencesAction({ const result = await updatePreferencesAction({
extratoNoteAsColumn, statementNoteAsColumn,
lancamentosColumnOrder: columnOrder, transactionsColumnOrder: columnOrder,
}); });
if (result.success) { if (result.success) {
@@ -145,7 +145,7 @@ export function PreferencesForm({
</div> </div>
<Switch <Switch
id="extrato-note-column" id="extrato-note-column"
checked={extratoNoteAsColumn} checked={statementNoteAsColumn}
onCheckedChange={setExtratoNoteAsColumn} onCheckedChange={setExtratoNoteAsColumn}
disabled={isPending} disabled={isPending}
/> />

View File

@@ -1,10 +1,10 @@
import { desc, eq } from "drizzle-orm"; import { desc, eq } from "drizzle-orm";
import { tokensApi } from "@/db/schema"; import { apiTokens } from "@/db/schema";
import { db, schema } from "@/shared/lib/db"; import { db, schema } from "@/shared/lib/db";
export interface UserPreferences { export interface UserPreferences {
extratoNoteAsColumn: boolean; statementNoteAsColumn: boolean;
lancamentosColumnOrder: string[] | null; transactionsColumnOrder: string[] | null;
} }
export interface ApiToken { export interface ApiToken {
@@ -30,11 +30,11 @@ export async function fetchUserPreferences(
): Promise<UserPreferences | null> { ): Promise<UserPreferences | null> {
const result = await db const result = await db
.select({ .select({
extratoNoteAsColumn: schema.preferenciasUsuario.extratoNoteAsColumn, statementNoteAsColumn: schema.userPreferences.statementNoteAsColumn,
lancamentosColumnOrder: schema.preferenciasUsuario.lancamentosColumnOrder, transactionsColumnOrder: schema.userPreferences.transactionsColumnOrder,
}) })
.from(schema.preferenciasUsuario) .from(schema.userPreferences)
.where(eq(schema.preferenciasUsuario.userId, userId)) .where(eq(schema.userPreferences.userId, userId))
.limit(1); .limit(1);
if (!result[0]) return null; if (!result[0]) return null;
@@ -45,21 +45,21 @@ export async function fetchUserPreferences(
export async function fetchApiTokens(userId: string): Promise<ApiToken[]> { export async function fetchApiTokens(userId: string): Promise<ApiToken[]> {
return db return db
.select({ .select({
id: tokensApi.id, id: apiTokens.id,
name: tokensApi.name, name: apiTokens.name,
tokenPrefix: tokensApi.tokenPrefix, tokenPrefix: apiTokens.tokenPrefix,
lastUsedAt: tokensApi.lastUsedAt, lastUsedAt: apiTokens.lastUsedAt,
lastUsedIp: tokensApi.lastUsedIp, lastUsedIp: apiTokens.lastUsedIp,
createdAt: tokensApi.createdAt, createdAt: apiTokens.createdAt,
expiresAt: tokensApi.expiresAt, expiresAt: apiTokens.expiresAt,
revokedAt: tokensApi.revokedAt, revokedAt: apiTokens.revokedAt,
}) })
.from(tokensApi) .from(apiTokens)
.where(eq(tokensApi.userId, userId)) .where(eq(apiTokens.userId, userId))
.orderBy(desc(tokensApi.createdAt)); .orderBy(desc(apiTokens.createdAt));
} }
export async function fetchAjustesPageData(userId: string) { export async function fetchSettingsPageData(userId: string) {
const [authProvider, userPreferences, userApiTokens] = await Promise.all([ const [authProvider, userPreferences, userApiTokens] = await Promise.all([
fetchAuthProvider(userId), fetchAuthProvider(userId),
fetchUserPreferences(userId), fetchUserPreferences(userId),

View File

@@ -1,19 +1,19 @@
import { import {
LANCAMENTO_CONDITIONS, PAYMENT_METHODS,
LANCAMENTO_PAYMENT_METHODS, TRANSACTION_CONDITIONS,
LANCAMENTO_TRANSACTION_TYPES, TRANSACTION_TYPES,
} from "@/features/transactions/constants"; } from "@/features/transactions/constants";
export const INITIAL_BALANCE_CATEGORY_NAME = "Saldo inicial"; export const INITIAL_BALANCE_CATEGORY_NAME = "Saldo inicial";
export const INITIAL_BALANCE_NOTE = "saldo inicial"; export const INITIAL_BALANCE_NOTE = "saldo inicial";
export const INITIAL_BALANCE_CONDITION = export const INITIAL_BALANCE_CONDITION =
LANCAMENTO_CONDITIONS.find((condition) => condition === "À vista") ?? TRANSACTION_CONDITIONS.find((condition) => condition === "À vista") ??
"À vista"; "À vista";
export const INITIAL_BALANCE_PAYMENT_METHOD = export const INITIAL_BALANCE_PAYMENT_METHOD =
LANCAMENTO_PAYMENT_METHODS.find((method) => method === "Pix") ?? "Pix"; PAYMENT_METHODS.find((method) => method === "Pix") ?? "Pix";
export const INITIAL_BALANCE_TRANSACTION_TYPE = export const INITIAL_BALANCE_TRANSACTION_TYPE =
LANCAMENTO_TRANSACTION_TYPES.find((type) => type === "Receita") ?? "Receita"; TRANSACTION_TYPES.find((type) => type === "Receita") ?? "Receita";
export const ACCOUNT_AUTO_INVOICE_NOTE_PREFIX = "AUTO_FATURA:"; export const ACCOUNT_AUTO_INVOICE_NOTE_PREFIX = "AUTO_FATURA:";

View File

@@ -1,5 +1,5 @@
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { categorias } from "@/db/schema"; import { categories } from "@/db/schema";
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";
@@ -42,7 +42,7 @@ export const DEFAULT_CATEGORIES: DefaultCategory[] = [
{ name: "Outras receitas", type: "receita", icon: "RiMore2Line" }, { name: "Outras receitas", type: "receita", icon: "RiMore2Line" },
{ name: "Saldo inicial", type: "receita", icon: "RiWallet2Line" }, { name: "Saldo inicial", type: "receita", icon: "RiWallet2Line" },
// Categoria especial para transferências entre contas // Category especial para transferências entre financialAccounts
{ {
name: "Transferência interna", name: "Transferência interna",
type: "receita", type: "receita",
@@ -59,9 +59,9 @@ export async function seedDefaultCategoriesForUser(userId: string | undefined) {
return; return;
} }
const existing = await db.query.categorias.findFirst({ const existing = await db.query.categories.findFirst({
columns: { id: true }, columns: { id: true },
where: eq(categorias.userId, userId), where: eq(categories.userId, userId),
}); });
if (existing) { if (existing) {
@@ -72,7 +72,7 @@ export async function seedDefaultCategoriesForUser(userId: string | undefined) {
return; return;
} }
await db.insert(categorias).values( await db.insert(categories).values(
DEFAULT_CATEGORIES.map((category) => ({ DEFAULT_CATEGORIES.map((category) => ({
name: category.name, name: category.name,
type: category.type, type: category.type,