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