- Replace ESLint with Biome for linting and formatting - Configure Biome with tabs, double quotes, and organized imports - Move all SQL/Drizzle queries from page.tsx files to data.ts files - Create new data.ts files for: ajustes, dashboard, relatorios/categorias - Update existing data.ts files: extrato, fatura (add lancamentos queries) - Remove all drizzle-orm imports from page.tsx files - Update README.md with new tooling info Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
169 lines
4.0 KiB
TypeScript
169 lines
4.0 KiB
TypeScript
"use server";
|
|
|
|
import { and, eq } from "drizzle-orm";
|
|
import { z } from "zod";
|
|
import { cartoes, contas } from "@/db/schema";
|
|
import {
|
|
type ActionResult,
|
|
handleActionError,
|
|
revalidateForEntity,
|
|
} from "@/lib/actions/helpers";
|
|
import { getUser } from "@/lib/auth/server";
|
|
import { db } from "@/lib/db";
|
|
import {
|
|
dayOfMonthSchema,
|
|
noteSchema,
|
|
optionalDecimalSchema,
|
|
uuidSchema,
|
|
} from "@/lib/schemas/common";
|
|
import { formatDecimalForDb } from "@/lib/utils/currency";
|
|
import { normalizeFilePath } from "@/lib/utils/string";
|
|
|
|
const cardBaseSchema = z.object({
|
|
name: z
|
|
.string({ message: "Informe o nome do cartão." })
|
|
.trim()
|
|
.min(1, "Informe o nome do cartão."),
|
|
brand: z
|
|
.string({ message: "Informe a bandeira." })
|
|
.trim()
|
|
.min(1, "Informe a bandeira."),
|
|
status: z
|
|
.string({ message: "Informe o status do cartão." })
|
|
.trim()
|
|
.min(1, "Informe o status do cartão."),
|
|
closingDay: dayOfMonthSchema,
|
|
dueDay: dayOfMonthSchema,
|
|
note: noteSchema,
|
|
limit: optionalDecimalSchema,
|
|
logo: z
|
|
.string({ message: "Selecione um logo." })
|
|
.trim()
|
|
.min(1, "Selecione um logo."),
|
|
contaId: uuidSchema("Conta"),
|
|
});
|
|
|
|
const createCardSchema = cardBaseSchema;
|
|
const updateCardSchema = cardBaseSchema.extend({
|
|
id: uuidSchema("Cartão"),
|
|
});
|
|
const deleteCardSchema = z.object({
|
|
id: uuidSchema("Cartão"),
|
|
});
|
|
|
|
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({
|
|
columns: { id: true },
|
|
where: and(eq(contas.id, contaId), eq(contas.userId, userId)),
|
|
});
|
|
|
|
if (!account) {
|
|
throw new Error("Conta vinculada não encontrada.");
|
|
}
|
|
}
|
|
|
|
export async function createCardAction(
|
|
input: CardCreateInput,
|
|
): Promise<ActionResult> {
|
|
try {
|
|
const user = await getUser();
|
|
const data = createCardSchema.parse(input);
|
|
|
|
await assertAccountOwnership(user.id, data.contaId);
|
|
|
|
const logoFile = normalizeFilePath(data.logo);
|
|
|
|
await db.insert(cartoes).values({
|
|
name: data.name,
|
|
brand: data.brand,
|
|
status: data.status,
|
|
closingDay: data.closingDay,
|
|
dueDay: data.dueDay,
|
|
note: data.note ?? null,
|
|
limit: formatDecimalForDb(data.limit),
|
|
logo: logoFile,
|
|
contaId: data.contaId,
|
|
userId: user.id,
|
|
});
|
|
|
|
revalidateForEntity("cartoes");
|
|
|
|
return { success: true, message: "Cartão criado com sucesso." };
|
|
} catch (error) {
|
|
return handleActionError(error);
|
|
}
|
|
}
|
|
|
|
export async function updateCardAction(
|
|
input: CardUpdateInput,
|
|
): Promise<ActionResult> {
|
|
try {
|
|
const user = await getUser();
|
|
const data = updateCardSchema.parse(input);
|
|
|
|
await assertAccountOwnership(user.id, data.contaId);
|
|
|
|
const logoFile = normalizeFilePath(data.logo);
|
|
|
|
const [updated] = await db
|
|
.update(cartoes)
|
|
.set({
|
|
name: data.name,
|
|
brand: data.brand,
|
|
status: data.status,
|
|
closingDay: data.closingDay,
|
|
dueDay: data.dueDay,
|
|
note: data.note ?? null,
|
|
limit: formatDecimalForDb(data.limit),
|
|
logo: logoFile,
|
|
contaId: data.contaId,
|
|
})
|
|
.where(and(eq(cartoes.id, data.id), eq(cartoes.userId, user.id)))
|
|
.returning();
|
|
|
|
if (!updated) {
|
|
return {
|
|
success: false,
|
|
error: "Cartão não encontrado.",
|
|
};
|
|
}
|
|
|
|
revalidateForEntity("cartoes");
|
|
|
|
return { success: true, message: "Cartão atualizado com sucesso." };
|
|
} catch (error) {
|
|
return handleActionError(error);
|
|
}
|
|
}
|
|
|
|
export async function deleteCardAction(
|
|
input: CardDeleteInput,
|
|
): Promise<ActionResult> {
|
|
try {
|
|
const user = await getUser();
|
|
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 });
|
|
|
|
if (!deleted) {
|
|
return {
|
|
success: false,
|
|
error: "Cartão não encontrado.",
|
|
};
|
|
}
|
|
|
|
revalidateForEntity("cartoes");
|
|
|
|
return { success: true, message: "Cartão removido com sucesso." };
|
|
} catch (error) {
|
|
return handleActionError(error);
|
|
}
|
|
}
|