mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 11:01:45 +00:00
refactor: alinha features financeiras ao novo naming
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 }}>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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={
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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:";
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user