refactor: alinha features financeiras ao novo naming

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

View File

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

View File

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

View File

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

View File

@@ -142,7 +142,7 @@ export function AccountStatementCard({
/>
</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">
<DetailItem
label="Tipo da conta"

View File

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

View File

@@ -4,7 +4,7 @@ import { useState, useTransition } from "react";
import { toast } from "sonner";
import { transferBetweenAccountsAction } from "@/features/accounts/actions";
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 { Button } from "@/shared/components/ui/button";
import { CurrencyInput } from "@/shared/components/ui/currency-input";
@@ -157,12 +157,12 @@ export function TransferDialog({
</div>
<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>
<SelectTrigger id="from-account" className="w-full">
<SelectValue>
{fromAccount && (
<ContaCartaoSelectContent
<AccountCardSelectContent
label={fromAccount.name}
logo={fromAccount.logo}
isCartao={false}
@@ -173,7 +173,7 @@ export function TransferDialog({
<SelectContent>
{fromAccount && (
<SelectItem value={fromAccount.id}>
<ContaCartaoSelectContent
<AccountCardSelectContent
label={fromAccount.name}
logo={fromAccount.logo}
isCartao={false}
@@ -185,7 +185,7 @@ export function TransferDialog({
</div>
<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 ? (
<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
@@ -201,7 +201,7 @@ export function TransferDialog({
(acc) => acc.id === toAccountId,
);
return selectedAccount ? (
<ContaCartaoSelectContent
<AccountCardSelectContent
label={selectedAccount.name}
logo={selectedAccount.logo}
isCartao={false}
@@ -213,7 +213,7 @@ export function TransferDialog({
<SelectContent className="w-full">
{availableAccounts.map((account) => (
<SelectItem key={account.id} value={account.id}>
<ContaCartaoSelectContent
<AccountCardSelectContent
label={account.name}
logo={account.logo}
isCartao={false}

View File

@@ -1,9 +1,9 @@
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 { db } from "@/shared/lib/db";
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 = {
id: string;
@@ -20,58 +20,59 @@ export type AccountData = {
export async function fetchAccountsForUser(
userId: string,
): Promise<{ accounts: AccountData[]; logoOptions: LogoOption[] }> {
): Promise<{ accounts: AccountData[]; logoOptions: string[] }> {
const [accountRows, logoOptions] = await Promise.all([
db
.select({
id: contas.id,
name: contas.name,
accountType: contas.accountType,
status: contas.status,
note: contas.note,
logo: contas.logo,
initialBalance: contas.initialBalance,
excludeFromBalance: contas.excludeFromBalance,
excludeInitialBalanceFromIncome: contas.excludeInitialBalanceFromIncome,
id: financialAccounts.id,
name: financialAccounts.name,
accountType: financialAccounts.accountType,
status: financialAccounts.status,
note: financialAccounts.note,
logo: financialAccounts.logo,
initialBalance: financialAccounts.initialBalance,
excludeFromBalance: financialAccounts.excludeFromBalance,
excludeInitialBalanceFromIncome:
financialAccounts.excludeInitialBalanceFromIncome,
balanceMovements: sql<number>`
coalesce(
sum(
case
when ${lancamentos.note} = ${INITIAL_BALANCE_NOTE} then 0
else ${lancamentos.amount}
when ${transactions.note} = ${INITIAL_BALANCE_NOTE} then 0
else ${transactions.amount}
end
),
0
)
`,
})
.from(contas)
.from(financialAccounts)
.leftJoin(
lancamentos,
transactions,
and(
eq(lancamentos.contaId, contas.id),
eq(lancamentos.userId, userId),
eq(lancamentos.isSettled, true),
eq(transactions.accountId, financialAccounts.id),
eq(transactions.userId, userId),
eq(transactions.isSettled, true),
),
)
.leftJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
.leftJoin(payers, eq(transactions.payerId, payers.id))
.where(
and(
eq(contas.userId, userId),
not(ilike(contas.status, "inativa")),
sql`(${lancamentos.id} IS NULL OR ${pagadores.role} = ${PAGADOR_ROLE_ADMIN})`,
eq(financialAccounts.userId, userId),
not(ilike(financialAccounts.status, "inativa")),
sql`(${transactions.id} IS NULL OR ${payers.role} = ${PAYER_ROLE_ADMIN})`,
),
)
.groupBy(
contas.id,
contas.name,
contas.accountType,
contas.status,
contas.note,
contas.logo,
contas.initialBalance,
contas.excludeFromBalance,
contas.excludeInitialBalanceFromIncome,
financialAccounts.id,
financialAccounts.name,
financialAccounts.accountType,
financialAccounts.status,
financialAccounts.note,
financialAccounts.logo,
financialAccounts.initialBalance,
financialAccounts.excludeFromBalance,
financialAccounts.excludeInitialBalanceFromIncome,
),
loadLogoOptions(),
]);
@@ -94,60 +95,61 @@ export async function fetchAccountsForUser(
return { accounts, logoOptions };
}
export async function fetchInativosForUser(
export async function fetchInactiveForUser(
userId: string,
): Promise<{ accounts: AccountData[]; logoOptions: LogoOption[] }> {
): Promise<{ accounts: AccountData[]; logoOptions: string[] }> {
const [accountRows, logoOptions] = await Promise.all([
db
.select({
id: contas.id,
name: contas.name,
accountType: contas.accountType,
status: contas.status,
note: contas.note,
logo: contas.logo,
initialBalance: contas.initialBalance,
excludeFromBalance: contas.excludeFromBalance,
excludeInitialBalanceFromIncome: contas.excludeInitialBalanceFromIncome,
id: financialAccounts.id,
name: financialAccounts.name,
accountType: financialAccounts.accountType,
status: financialAccounts.status,
note: financialAccounts.note,
logo: financialAccounts.logo,
initialBalance: financialAccounts.initialBalance,
excludeFromBalance: financialAccounts.excludeFromBalance,
excludeInitialBalanceFromIncome:
financialAccounts.excludeInitialBalanceFromIncome,
balanceMovements: sql<number>`
coalesce(
sum(
case
when ${lancamentos.note} = ${INITIAL_BALANCE_NOTE} then 0
else ${lancamentos.amount}
when ${transactions.note} = ${INITIAL_BALANCE_NOTE} then 0
else ${transactions.amount}
end
),
0
)
`,
})
.from(contas)
.from(financialAccounts)
.leftJoin(
lancamentos,
transactions,
and(
eq(lancamentos.contaId, contas.id),
eq(lancamentos.userId, userId),
eq(lancamentos.isSettled, true),
eq(transactions.accountId, financialAccounts.id),
eq(transactions.userId, userId),
eq(transactions.isSettled, true),
),
)
.leftJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
.leftJoin(payers, eq(transactions.payerId, payers.id))
.where(
and(
eq(contas.userId, userId),
ilike(contas.status, "inativa"),
sql`(${lancamentos.id} IS NULL OR ${pagadores.role} = ${PAGADOR_ROLE_ADMIN})`,
eq(financialAccounts.userId, userId),
ilike(financialAccounts.status, "inativa"),
sql`(${transactions.id} IS NULL OR ${payers.role} = ${PAYER_ROLE_ADMIN})`,
),
)
.groupBy(
contas.id,
contas.name,
contas.accountType,
contas.status,
contas.note,
contas.logo,
contas.initialBalance,
contas.excludeFromBalance,
contas.excludeInitialBalanceFromIncome,
financialAccounts.id,
financialAccounts.name,
financialAccounts.accountType,
financialAccounts.status,
financialAccounts.note,
financialAccounts.logo,
financialAccounts.initialBalance,
financialAccounts.excludeFromBalance,
financialAccounts.excludeInitialBalanceFromIncome,
),
loadLogoOptions(),
]);
@@ -173,11 +175,11 @@ export async function fetchInativosForUser(
export async function fetchAllAccountsForUser(userId: string): Promise<{
activeAccounts: AccountData[];
archivedAccounts: AccountData[];
logoOptions: LogoOption[];
logoOptions: string[];
}> {
const [activeData, archivedData] = await Promise.all([
fetchAccountsForUser(userId),
fetchInativosForUser(userId),
fetchInactiveForUser(userId),
]);
return {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -160,10 +160,10 @@ export function CardFormFields({
</div>
<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
value={values.contaId}
onValueChange={(value) => onChange("contaId", value)}
value={values.accountId}
onValueChange={(value) => onChange("accountId", value)}
disabled={accountOptions.length === 0}
>
<SelectTrigger id="card-account" className="w-full">
@@ -174,10 +174,10 @@ export function CardFormFields({
: "Selecione a conta"
}
>
{values.contaId &&
{values.accountId &&
(() => {
const selectedAccount = accountOptions.find(
(acc) => acc.id === values.contaId,
(acc) => acc.id === values.accountId,
);
return selectedAccount ? (
<AccountSelectContent

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
import { and, eq } from "drizzle-orm";
import { z } from "zod";
import { categorias } from "@/db/schema";
import { categories } from "@/db/schema";
import {
type ActionResult,
handleActionError,
@@ -32,10 +32,10 @@ const categoryBaseSchema = z.object({
const createCategorySchema = categoryBaseSchema;
const updateCategorySchema = categoryBaseSchema.extend({
id: uuidSchema("Categoria"),
id: uuidSchema("Category"),
});
const deleteCategorySchema = z.object({
id: uuidSchema("Categoria"),
id: uuidSchema("Category"),
});
type CategoryCreateInput = z.infer<typeof createCategorySchema>;
@@ -49,16 +49,16 @@ export async function createCategoryAction(
const user = await getUser();
const data = createCategorySchema.parse(input);
await db.insert(categorias).values({
await db.insert(categories).values({
name: data.name,
type: data.type,
icon: data.icon,
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) {
return handleActionError(error);
}
@@ -72,19 +72,19 @@ export async function updateCategoryAction(
const data = updateCategorySchema.parse(input);
// 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 },
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) {
return {
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 = [
"Transferência interna",
"Saldo inicial",
@@ -98,25 +98,25 @@ export async function updateCategoryAction(
}
const [updated] = await db
.update(categorias)
.update(categories)
.set({
name: data.name,
type: data.type,
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();
if (!updated) {
return {
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) {
return handleActionError(error);
}
@@ -130,19 +130,19 @@ export async function deleteCategoryAction(
const data = deleteCategorySchema.parse(input);
// 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 },
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) {
return {
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 = [
"Transferência interna",
"Saldo inicial",
@@ -156,20 +156,20 @@ export async function deleteCategoryAction(
}
const [deleted] = await db
.delete(categorias)
.where(and(eq(categorias.id, data.id), eq(categorias.userId, user.id)))
.returning({ id: categorias.id });
.delete(categories)
.where(and(eq(categories.id, data.id), eq(categories.userId, user.id)))
.returning({ id: categories.id });
if (!deleted) {
return {
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) {
return handleActionError(error);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
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 {
InboxItem,
SelectOption,
@@ -9,8 +9,8 @@ import {
buildSluggedFilters,
} from "@/features/transactions/page-helpers";
import {
fetchLancamentoFilterSources,
fetchRecentEstablishments,
fetchTransactionFilterSources,
} from "@/features/transactions/queries";
import { db } from "@/shared/lib/db";
@@ -20,11 +20,9 @@ export async function fetchInboxItems(
): Promise<InboxItem[]> {
const items = await db
.select()
.from(preLancamentos)
.where(
and(eq(preLancamentos.userId, userId), eq(preLancamentos.status, status)),
)
.orderBy(desc(preLancamentos.createdAt));
.from(inboxItems)
.where(and(eq(inboxItems.userId, userId), eq(inboxItems.status, status)))
.orderBy(desc(inboxItems.createdAt));
return items;
}
@@ -35,54 +33,57 @@ export async function fetchInboxItemById(
): Promise<InboxItem | null> {
const [item] = await db
.select()
.from(preLancamentos)
.where(
and(eq(preLancamentos.id, itemId), eq(preLancamentos.userId, userId)),
)
.from(inboxItems)
.where(and(eq(inboxItems.id, itemId), eq(inboxItems.userId, userId)))
.limit(1);
return item ?? null;
}
export async function fetchCategoriasForSelect(
export async function fetchCategoriesForSelect(
userId: string,
type?: string,
): Promise<SelectOption[]> {
const query = db
.select({ id: categorias.id, name: categorias.name })
.from(categorias)
const rows = await db
.select({ id: categories.id, name: categories.name })
.from(categories)
.where(
type
? and(eq(categorias.userId, userId), eq(categorias.type, type))
: eq(categorias.userId, userId),
? and(eq(categories.userId, userId), eq(categories.type, type))
: 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,
): Promise<SelectOption[]> {
const items = await db
.select({ id: contas.id, name: contas.name })
.from(contas)
.where(and(eq(contas.userId, userId), eq(contas.status, "ativo")))
.orderBy(contas.name);
const rows = await db
.select({ id: financialAccounts.id, name: financialAccounts.name })
.from(financialAccounts)
.where(
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,
): Promise<(SelectOption & { lastDigits?: string })[]> {
const items = await db
.select({ id: cartoes.id, name: cartoes.name })
.from(cartoes)
.where(and(eq(cartoes.userId, userId), eq(cartoes.status, "ativo")))
.orderBy(cartoes.name);
const rows = await db
.select({ id: cards.id, name: cards.name })
.from(cards)
.where(and(eq(cards.userId, userId), eq(cards.status, "ativo")))
.orderBy(cards.name);
return items;
return rows.map((row) => ({ value: row.id, label: row.name }));
}
export async function fetchAppLogoMap(
@@ -90,13 +91,13 @@ export async function fetchAppLogoMap(
): Promise<Record<string, string>> {
const [userCartoes, userContas] = await Promise.all([
db
.select({ name: cartoes.name, logo: cartoes.logo })
.from(cartoes)
.where(eq(cartoes.userId, userId)),
.select({ name: cards.name, logo: cards.logo })
.from(cards)
.where(eq(cards.userId, userId)),
db
.select({ name: contas.name, logo: contas.logo })
.from(contas)
.where(eq(contas.userId, userId)),
.select({ name: financialAccounts.name, logo: financialAccounts.logo })
.from(financialAccounts)
.where(eq(financialAccounts.userId, userId)),
]);
const logoMap: Record<string, string> = {};
@@ -112,54 +113,51 @@ export async function fetchAppLogoMap(
export async function fetchPendingInboxCount(userId: string): Promise<number> {
const items = await db
.select({ id: preLancamentos.id })
.from(preLancamentos)
.select({ id: inboxItems.id })
.from(inboxItems)
.where(
and(
eq(preLancamentos.userId, userId),
eq(preLancamentos.status, "pending"),
),
and(eq(inboxItems.userId, userId), eq(inboxItems.status, "pending")),
);
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<{
pagadorOptions: SelectOption[];
splitPagadorOptions: SelectOption[];
defaultPagadorId: string | null;
contaOptions: SelectOption[];
cartaoOptions: SelectOption[];
categoriaOptions: SelectOption[];
payerOptions: SelectOption[];
splitPayerOptions: SelectOption[];
defaultPayerId: string | null;
accountOptions: SelectOption[];
cardOptions: SelectOption[];
categoryOptions: SelectOption[];
estabelecimentos: string[];
}> {
const filterSources = await fetchLancamentoFilterSources(userId);
const filterSources = await fetchTransactionFilterSources(userId);
const sluggedFilters = buildSluggedFilters(filterSources);
const {
pagadorOptions,
splitPagadorOptions,
defaultPagadorId,
contaOptions,
cartaoOptions,
categoriaOptions,
payerOptions,
splitPayerOptions,
defaultPayerId,
accountOptions,
cardOptions,
categoryOptions,
} = buildOptionSets({
...sluggedFilters,
pagadorRows: filterSources.pagadorRows,
payerRows: filterSources.payerRows,
});
const estabelecimentos = await fetchRecentEstablishments(userId);
return {
pagadorOptions,
splitPagadorOptions,
defaultPagadorId,
contaOptions,
cartaoOptions,
categoriaOptions,
payerOptions,
splitPayerOptions,
defaultPayerId,
accountOptions,
cardOptions,
categoryOptions,
estabelecimentos,
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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