forked from git.gladyson/openmonetis
refactor: migrate from ESLint to Biome and extract SQL queries to data.ts
- 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>
This commit is contained in:
@@ -1,119 +1,117 @@
|
||||
"use server";
|
||||
|
||||
import {
|
||||
cartoes,
|
||||
categorias,
|
||||
faturas,
|
||||
lancamentos,
|
||||
pagadores,
|
||||
} from "@/db/schema";
|
||||
import { buildInvoicePaymentNote } from "@/lib/accounts/constants";
|
||||
import { db } from "@/lib/db";
|
||||
import { getUser } from "@/lib/auth/server";
|
||||
import {
|
||||
INVOICE_PAYMENT_STATUS,
|
||||
INVOICE_STATUS_VALUES,
|
||||
PERIOD_FORMAT_REGEX,
|
||||
type InvoicePaymentStatus,
|
||||
} from "@/lib/faturas";
|
||||
import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
|
||||
import { parseLocalDateString } from "@/lib/utils/date";
|
||||
import { and, eq, sql } from "drizzle-orm";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
cartoes,
|
||||
categorias,
|
||||
faturas,
|
||||
lancamentos,
|
||||
pagadores,
|
||||
} from "@/db/schema";
|
||||
import { buildInvoicePaymentNote } from "@/lib/accounts/constants";
|
||||
import { getUser } from "@/lib/auth/server";
|
||||
import { db } from "@/lib/db";
|
||||
import {
|
||||
INVOICE_PAYMENT_STATUS,
|
||||
INVOICE_STATUS_VALUES,
|
||||
type InvoicePaymentStatus,
|
||||
PERIOD_FORMAT_REGEX,
|
||||
} from "@/lib/faturas";
|
||||
import { PAGADOR_ROLE_ADMIN } from "@/lib/pagadores/constants";
|
||||
import { parseLocalDateString } from "@/lib/utils/date";
|
||||
|
||||
const updateInvoicePaymentStatusSchema = z.object({
|
||||
cartaoId: 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."),
|
||||
status: z.enum(
|
||||
INVOICE_STATUS_VALUES as [InvoicePaymentStatus, ...InvoicePaymentStatus[]]
|
||||
),
|
||||
paymentDate: z.string().optional(),
|
||||
cartaoId: 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."),
|
||||
status: z.enum(
|
||||
INVOICE_STATUS_VALUES as [InvoicePaymentStatus, ...InvoicePaymentStatus[]],
|
||||
),
|
||||
paymentDate: z.string().optional(),
|
||||
});
|
||||
|
||||
type UpdateInvoicePaymentStatusInput = z.infer<
|
||||
typeof updateInvoicePaymentStatusSchema
|
||||
typeof updateInvoicePaymentStatusSchema
|
||||
>;
|
||||
|
||||
type ActionResult =
|
||||
| { success: true; message: string }
|
||||
| { success: false; error: string };
|
||||
| { success: true; message: string }
|
||||
| { success: false; error: string };
|
||||
|
||||
const successMessageByStatus: Record<InvoicePaymentStatus, string> = {
|
||||
[INVOICE_PAYMENT_STATUS.PAID]: "Fatura marcada como paga.",
|
||||
[INVOICE_PAYMENT_STATUS.PENDING]: "Pagamento da fatura foi revertido.",
|
||||
[INVOICE_PAYMENT_STATUS.PAID]: "Fatura marcada como paga.",
|
||||
[INVOICE_PAYMENT_STATUS.PENDING]: "Pagamento da fatura foi revertido.",
|
||||
};
|
||||
|
||||
const formatDecimal = (value: number) =>
|
||||
(Math.round(value * 100) / 100).toFixed(2);
|
||||
(Math.round(value * 100) / 100).toFixed(2);
|
||||
|
||||
export async function updateInvoicePaymentStatusAction(
|
||||
input: UpdateInvoicePaymentStatusInput
|
||||
input: UpdateInvoicePaymentStatusInput,
|
||||
): Promise<ActionResult> {
|
||||
try {
|
||||
const user = await getUser();
|
||||
const data = updateInvoicePaymentStatusSchema.parse(input);
|
||||
try {
|
||||
const user = await getUser();
|
||||
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)),
|
||||
});
|
||||
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)),
|
||||
});
|
||||
|
||||
if (!card) {
|
||||
throw new Error("Cartão não encontrado.");
|
||||
}
|
||||
if (!card) {
|
||||
throw new Error("Cartão não encontrado.");
|
||||
}
|
||||
|
||||
const existingInvoice = await tx.query.faturas.findFirst({
|
||||
columns: {
|
||||
id: true,
|
||||
},
|
||||
where: and(
|
||||
eq(faturas.cartaoId, data.cartaoId),
|
||||
eq(faturas.userId, user.id),
|
||||
eq(faturas.period, data.period)
|
||||
),
|
||||
});
|
||||
const existingInvoice = await tx.query.faturas.findFirst({
|
||||
columns: {
|
||||
id: true,
|
||||
},
|
||||
where: and(
|
||||
eq(faturas.cartaoId, data.cartaoId),
|
||||
eq(faturas.userId, user.id),
|
||||
eq(faturas.period, data.period),
|
||||
),
|
||||
});
|
||||
|
||||
if (existingInvoice) {
|
||||
await tx
|
||||
.update(faturas)
|
||||
.set({
|
||||
paymentStatus: data.status,
|
||||
})
|
||||
.where(eq(faturas.id, existingInvoice.id));
|
||||
} else {
|
||||
await tx.insert(faturas).values({
|
||||
cartaoId: data.cartaoId,
|
||||
period: data.period,
|
||||
paymentStatus: data.status,
|
||||
userId: user.id,
|
||||
});
|
||||
}
|
||||
if (existingInvoice) {
|
||||
await tx
|
||||
.update(faturas)
|
||||
.set({
|
||||
paymentStatus: data.status,
|
||||
})
|
||||
.where(eq(faturas.id, existingInvoice.id));
|
||||
} else {
|
||||
await tx.insert(faturas).values({
|
||||
cartaoId: data.cartaoId,
|
||||
period: data.period,
|
||||
paymentStatus: data.status,
|
||||
userId: user.id,
|
||||
});
|
||||
}
|
||||
|
||||
const shouldMarkAsPaid = data.status === INVOICE_PAYMENT_STATUS.PAID;
|
||||
const shouldMarkAsPaid = data.status === INVOICE_PAYMENT_STATUS.PAID;
|
||||
|
||||
await tx
|
||||
.update(lancamentos)
|
||||
.set({ isSettled: shouldMarkAsPaid })
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, user.id),
|
||||
eq(lancamentos.cartaoId, card.id),
|
||||
eq(lancamentos.period, data.period)
|
||||
)
|
||||
);
|
||||
await tx
|
||||
.update(lancamentos)
|
||||
.set({ isSettled: shouldMarkAsPaid })
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, user.id),
|
||||
eq(lancamentos.cartaoId, card.id),
|
||||
eq(lancamentos.period, data.period),
|
||||
),
|
||||
);
|
||||
|
||||
const invoiceNote = buildInvoicePaymentNote(card.id, data.period);
|
||||
const invoiceNote = buildInvoicePaymentNote(card.id, data.period);
|
||||
|
||||
if (shouldMarkAsPaid) {
|
||||
const [adminShareRow] = await tx
|
||||
.select({
|
||||
total: sql<number>`
|
||||
if (shouldMarkAsPaid) {
|
||||
const [adminShareRow] = await tx
|
||||
.select({
|
||||
total: sql<number>`
|
||||
coalesce(
|
||||
sum(
|
||||
case
|
||||
@@ -124,177 +122,175 @@ export async function updateInvoicePaymentStatusAction(
|
||||
0
|
||||
)
|
||||
`,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.leftJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, user.id),
|
||||
eq(lancamentos.cartaoId, card.id),
|
||||
eq(lancamentos.period, data.period),
|
||||
eq(pagadores.role, PAGADOR_ROLE_ADMIN)
|
||||
)
|
||||
);
|
||||
})
|
||||
.from(lancamentos)
|
||||
.leftJoin(pagadores, eq(lancamentos.pagadorId, pagadores.id))
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, user.id),
|
||||
eq(lancamentos.cartaoId, card.id),
|
||||
eq(lancamentos.period, data.period),
|
||||
eq(pagadores.role, PAGADOR_ROLE_ADMIN),
|
||||
),
|
||||
);
|
||||
|
||||
const adminShare = Math.abs(Number(adminShareRow?.total ?? 0));
|
||||
const adminShare = Math.abs(Number(adminShareRow?.total ?? 0));
|
||||
|
||||
if (adminShare > 0 && card.contaId) {
|
||||
const adminPagador = await tx.query.pagadores.findFirst({
|
||||
columns: { id: true },
|
||||
where: and(
|
||||
eq(pagadores.userId, user.id),
|
||||
eq(pagadores.role, PAGADOR_ROLE_ADMIN)
|
||||
),
|
||||
});
|
||||
if (adminShare > 0 && card.contaId) {
|
||||
const adminPagador = await tx.query.pagadores.findFirst({
|
||||
columns: { id: true },
|
||||
where: and(
|
||||
eq(pagadores.userId, user.id),
|
||||
eq(pagadores.role, PAGADOR_ROLE_ADMIN),
|
||||
),
|
||||
});
|
||||
|
||||
const paymentCategory = await tx.query.categorias.findFirst({
|
||||
columns: { id: true },
|
||||
where: and(
|
||||
eq(categorias.userId, user.id),
|
||||
eq(categorias.name, "Pagamentos")
|
||||
),
|
||||
});
|
||||
const paymentCategory = await tx.query.categorias.findFirst({
|
||||
columns: { id: true },
|
||||
where: and(
|
||||
eq(categorias.userId, user.id),
|
||||
eq(categorias.name, "Pagamentos"),
|
||||
),
|
||||
});
|
||||
|
||||
if (adminPagador) {
|
||||
// Usar a data customizada ou a data atual como data de pagamento
|
||||
const invoiceDate = data.paymentDate
|
||||
? parseLocalDateString(data.paymentDate)
|
||||
: new Date();
|
||||
if (adminPagador) {
|
||||
// Usar a data customizada ou a data atual como data de pagamento
|
||||
const invoiceDate = data.paymentDate
|
||||
? parseLocalDateString(data.paymentDate)
|
||||
: new Date();
|
||||
|
||||
const amount = `-${formatDecimal(adminShare)}`;
|
||||
const payload = {
|
||||
condition: "À vista",
|
||||
name: `Pagamento fatura - ${card.name}`,
|
||||
paymentMethod: "Pix",
|
||||
note: invoiceNote,
|
||||
amount,
|
||||
purchaseDate: invoiceDate,
|
||||
transactionType: "Despesa" as const,
|
||||
period: data.period,
|
||||
isSettled: true,
|
||||
userId: user.id,
|
||||
contaId: card.contaId,
|
||||
categoriaId: paymentCategory?.id ?? null,
|
||||
pagadorId: adminPagador.id,
|
||||
};
|
||||
const amount = `-${formatDecimal(adminShare)}`;
|
||||
const payload = {
|
||||
condition: "À vista",
|
||||
name: `Pagamento fatura - ${card.name}`,
|
||||
paymentMethod: "Pix",
|
||||
note: invoiceNote,
|
||||
amount,
|
||||
purchaseDate: invoiceDate,
|
||||
transactionType: "Despesa" as const,
|
||||
period: data.period,
|
||||
isSettled: true,
|
||||
userId: user.id,
|
||||
contaId: card.contaId,
|
||||
categoriaId: paymentCategory?.id ?? null,
|
||||
pagadorId: adminPagador.id,
|
||||
};
|
||||
|
||||
const existingPayment = await tx.query.lancamentos.findFirst({
|
||||
columns: { id: true },
|
||||
where: and(
|
||||
eq(lancamentos.userId, user.id),
|
||||
eq(lancamentos.note, invoiceNote)
|
||||
),
|
||||
});
|
||||
const existingPayment = await tx.query.lancamentos.findFirst({
|
||||
columns: { id: true },
|
||||
where: and(
|
||||
eq(lancamentos.userId, user.id),
|
||||
eq(lancamentos.note, invoiceNote),
|
||||
),
|
||||
});
|
||||
|
||||
if (existingPayment) {
|
||||
await tx
|
||||
.update(lancamentos)
|
||||
.set(payload)
|
||||
.where(eq(lancamentos.id, existingPayment.id));
|
||||
} else {
|
||||
await tx.insert(lancamentos).values(payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await tx
|
||||
.delete(lancamentos)
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, user.id),
|
||||
eq(lancamentos.note, invoiceNote)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
if (existingPayment) {
|
||||
await tx
|
||||
.update(lancamentos)
|
||||
.set(payload)
|
||||
.where(eq(lancamentos.id, existingPayment.id));
|
||||
} else {
|
||||
await tx.insert(lancamentos).values(payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await tx
|
||||
.delete(lancamentos)
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, user.id),
|
||||
eq(lancamentos.note, invoiceNote),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
revalidatePath(`/cartoes/${data.cartaoId}/fatura`);
|
||||
revalidatePath("/cartoes");
|
||||
revalidatePath("/contas");
|
||||
revalidatePath(`/cartoes/${data.cartaoId}/fatura`);
|
||||
revalidatePath("/cartoes");
|
||||
revalidatePath("/contas");
|
||||
|
||||
return { success: true, message: successMessageByStatus[data.status] };
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.issues[0]?.message ?? "Dados inválidos.",
|
||||
};
|
||||
}
|
||||
return { success: true, message: successMessageByStatus[data.status] };
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.issues[0]?.message ?? "Dados inválidos.",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro inesperado.",
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro inesperado.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const updatePaymentDateSchema = z.object({
|
||||
cartaoId: 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."),
|
||||
paymentDate: z.string({ message: "Data de pagamento inválida." }),
|
||||
cartaoId: 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."),
|
||||
paymentDate: z.string({ message: "Data de pagamento inválida." }),
|
||||
});
|
||||
|
||||
type UpdatePaymentDateInput = z.infer<typeof updatePaymentDateSchema>;
|
||||
|
||||
export async function updatePaymentDateAction(
|
||||
input: UpdatePaymentDateInput
|
||||
input: UpdatePaymentDateInput,
|
||||
): Promise<ActionResult> {
|
||||
try {
|
||||
const user = await getUser();
|
||||
const data = updatePaymentDateSchema.parse(input);
|
||||
try {
|
||||
const user = await getUser();
|
||||
const data = updatePaymentDateSchema.parse(input);
|
||||
|
||||
await db.transaction(async (tx: typeof db) => {
|
||||
const card = await tx.query.cartoes.findFirst({
|
||||
columns: { id: true },
|
||||
where: and(eq(cartoes.id, data.cartaoId), eq(cartoes.userId, user.id)),
|
||||
});
|
||||
await db.transaction(async (tx: typeof db) => {
|
||||
const card = await tx.query.cartoes.findFirst({
|
||||
columns: { id: true },
|
||||
where: and(eq(cartoes.id, data.cartaoId), eq(cartoes.userId, user.id)),
|
||||
});
|
||||
|
||||
if (!card) {
|
||||
throw new Error("Cartão não encontrado.");
|
||||
}
|
||||
if (!card) {
|
||||
throw new Error("Cartão não encontrado.");
|
||||
}
|
||||
|
||||
const invoiceNote = buildInvoicePaymentNote(card.id, data.period);
|
||||
const invoiceNote = buildInvoicePaymentNote(card.id, data.period);
|
||||
|
||||
const existingPayment = await tx.query.lancamentos.findFirst({
|
||||
columns: { id: true },
|
||||
where: and(
|
||||
eq(lancamentos.userId, user.id),
|
||||
eq(lancamentos.note, invoiceNote)
|
||||
),
|
||||
});
|
||||
const existingPayment = await tx.query.lancamentos.findFirst({
|
||||
columns: { id: true },
|
||||
where: and(
|
||||
eq(lancamentos.userId, user.id),
|
||||
eq(lancamentos.note, invoiceNote),
|
||||
),
|
||||
});
|
||||
|
||||
if (!existingPayment) {
|
||||
throw new Error("Pagamento não encontrado.");
|
||||
}
|
||||
if (!existingPayment) {
|
||||
throw new Error("Pagamento não encontrado.");
|
||||
}
|
||||
|
||||
await tx
|
||||
.update(lancamentos)
|
||||
.set({
|
||||
purchaseDate: parseLocalDateString(data.paymentDate),
|
||||
})
|
||||
.where(eq(lancamentos.id, existingPayment.id));
|
||||
});
|
||||
await tx
|
||||
.update(lancamentos)
|
||||
.set({
|
||||
purchaseDate: parseLocalDateString(data.paymentDate),
|
||||
})
|
||||
.where(eq(lancamentos.id, existingPayment.id));
|
||||
});
|
||||
|
||||
revalidatePath(`/cartoes/${data.cartaoId}/fatura`);
|
||||
revalidatePath("/cartoes");
|
||||
revalidatePath("/contas");
|
||||
revalidatePath(`/cartoes/${data.cartaoId}/fatura`);
|
||||
revalidatePath("/cartoes");
|
||||
revalidatePath("/contas");
|
||||
|
||||
return { success: true, message: "Data de pagamento atualizada." };
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.issues[0]?.message ?? "Dados inválidos.",
|
||||
};
|
||||
}
|
||||
return { success: true, message: "Data de pagamento atualizada." };
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.issues[0]?.message ?? "Dados inválidos.",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro inesperado.",
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Erro inesperado.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,104 +1,117 @@
|
||||
import { and, desc, eq, type SQL, sum } from "drizzle-orm";
|
||||
import { cartoes, faturas, lancamentos } from "@/db/schema";
|
||||
import { buildInvoicePaymentNote } from "@/lib/accounts/constants";
|
||||
import { db } from "@/lib/db";
|
||||
import {
|
||||
INVOICE_PAYMENT_STATUS,
|
||||
type InvoicePaymentStatus,
|
||||
INVOICE_PAYMENT_STATUS,
|
||||
type InvoicePaymentStatus,
|
||||
} from "@/lib/faturas";
|
||||
import { and, eq, sum } from "drizzle-orm";
|
||||
|
||||
const toNumber = (value: string | number | null | undefined) => {
|
||||
if (typeof value === "number") {
|
||||
return value;
|
||||
}
|
||||
if (value === null || value === undefined) {
|
||||
return 0;
|
||||
}
|
||||
const parsed = Number(value);
|
||||
return Number.isNaN(parsed) ? 0 : parsed;
|
||||
if (typeof value === "number") {
|
||||
return value;
|
||||
}
|
||||
if (value === null || value === undefined) {
|
||||
return 0;
|
||||
}
|
||||
const parsed = Number(value);
|
||||
return Number.isNaN(parsed) ? 0 : parsed;
|
||||
};
|
||||
|
||||
export async function fetchCardData(userId: string, cartaoId: string) {
|
||||
const card = await db.query.cartoes.findFirst({
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
brand: true,
|
||||
closingDay: true,
|
||||
dueDay: true,
|
||||
logo: true,
|
||||
limit: true,
|
||||
status: true,
|
||||
note: true,
|
||||
contaId: true,
|
||||
},
|
||||
where: and(eq(cartoes.id, cartaoId), eq(cartoes.userId, userId)),
|
||||
});
|
||||
const card = await db.query.cartoes.findFirst({
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
brand: true,
|
||||
closingDay: true,
|
||||
dueDay: true,
|
||||
logo: true,
|
||||
limit: true,
|
||||
status: true,
|
||||
note: true,
|
||||
contaId: true,
|
||||
},
|
||||
where: and(eq(cartoes.id, cartaoId), eq(cartoes.userId, userId)),
|
||||
});
|
||||
|
||||
return card;
|
||||
return card;
|
||||
}
|
||||
|
||||
export async function fetchInvoiceData(
|
||||
userId: string,
|
||||
cartaoId: string,
|
||||
selectedPeriod: string
|
||||
userId: string,
|
||||
cartaoId: string,
|
||||
selectedPeriod: string,
|
||||
): Promise<{
|
||||
totalAmount: number;
|
||||
invoiceStatus: InvoicePaymentStatus;
|
||||
paymentDate: Date | null;
|
||||
totalAmount: number;
|
||||
invoiceStatus: InvoicePaymentStatus;
|
||||
paymentDate: Date | null;
|
||||
}> {
|
||||
const [invoiceRow, totalRow] = await Promise.all([
|
||||
db.query.faturas.findFirst({
|
||||
columns: {
|
||||
id: true,
|
||||
period: true,
|
||||
paymentStatus: true,
|
||||
},
|
||||
where: and(
|
||||
eq(faturas.cartaoId, cartaoId),
|
||||
eq(faturas.userId, userId),
|
||||
eq(faturas.period, selectedPeriod)
|
||||
),
|
||||
}),
|
||||
db
|
||||
.select({ totalAmount: sum(lancamentos.amount) })
|
||||
.from(lancamentos)
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, userId),
|
||||
eq(lancamentos.cartaoId, cartaoId),
|
||||
eq(lancamentos.period, selectedPeriod)
|
||||
)
|
||||
),
|
||||
]);
|
||||
const [invoiceRow, totalRow] = await Promise.all([
|
||||
db.query.faturas.findFirst({
|
||||
columns: {
|
||||
id: true,
|
||||
period: true,
|
||||
paymentStatus: true,
|
||||
},
|
||||
where: and(
|
||||
eq(faturas.cartaoId, cartaoId),
|
||||
eq(faturas.userId, userId),
|
||||
eq(faturas.period, selectedPeriod),
|
||||
),
|
||||
}),
|
||||
db
|
||||
.select({ totalAmount: sum(lancamentos.amount) })
|
||||
.from(lancamentos)
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, userId),
|
||||
eq(lancamentos.cartaoId, cartaoId),
|
||||
eq(lancamentos.period, selectedPeriod),
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
const totalAmount = toNumber(totalRow[0]?.totalAmount);
|
||||
const isInvoiceStatus = (
|
||||
value: string | null | undefined
|
||||
): value is InvoicePaymentStatus =>
|
||||
!!value && ["pendente", "pago"].includes(value);
|
||||
const totalAmount = toNumber(totalRow[0]?.totalAmount);
|
||||
const isInvoiceStatus = (
|
||||
value: string | null | undefined,
|
||||
): value is InvoicePaymentStatus =>
|
||||
!!value && ["pendente", "pago"].includes(value);
|
||||
|
||||
const invoiceStatus = isInvoiceStatus(invoiceRow?.paymentStatus)
|
||||
? invoiceRow?.paymentStatus
|
||||
: INVOICE_PAYMENT_STATUS.PENDING;
|
||||
const invoiceStatus = isInvoiceStatus(invoiceRow?.paymentStatus)
|
||||
? invoiceRow?.paymentStatus
|
||||
: INVOICE_PAYMENT_STATUS.PENDING;
|
||||
|
||||
// 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({
|
||||
columns: {
|
||||
purchaseDate: true,
|
||||
},
|
||||
where: and(
|
||||
eq(lancamentos.userId, userId),
|
||||
eq(lancamentos.note, invoiceNote)
|
||||
),
|
||||
});
|
||||
paymentDate = paymentLancamento?.purchaseDate
|
||||
? new Date(paymentLancamento.purchaseDate)
|
||||
: null;
|
||||
}
|
||||
// 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({
|
||||
columns: {
|
||||
purchaseDate: true,
|
||||
},
|
||||
where: and(
|
||||
eq(lancamentos.userId, userId),
|
||||
eq(lancamentos.note, invoiceNote),
|
||||
),
|
||||
});
|
||||
paymentDate = paymentLancamento?.purchaseDate
|
||||
? new Date(paymentLancamento.purchaseDate)
|
||||
: null;
|
||||
}
|
||||
|
||||
return { totalAmount, invoiceStatus, paymentDate };
|
||||
return { totalAmount, invoiceStatus, paymentDate };
|
||||
}
|
||||
|
||||
export async function fetchCardLancamentos(filters: SQL[]) {
|
||||
return db.query.lancamentos.findMany({
|
||||
where: and(...filters),
|
||||
with: {
|
||||
pagador: true,
|
||||
conta: true,
|
||||
cartao: true,
|
||||
categoria: true,
|
||||
},
|
||||
orderBy: desc(lancamentos.purchaseDate),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
FilterSkeleton,
|
||||
InvoiceSummaryCardSkeleton,
|
||||
TransactionsTableSkeleton,
|
||||
FilterSkeleton,
|
||||
InvoiceSummaryCardSkeleton,
|
||||
TransactionsTableSkeleton,
|
||||
} from "@/components/skeletons";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
@@ -10,32 +10,32 @@ import { Skeleton } from "@/components/ui/skeleton";
|
||||
* Layout: MonthPicker + InvoiceSummaryCard + Filtros + Tabela de lançamentos
|
||||
*/
|
||||
export default function FaturaLoading() {
|
||||
return (
|
||||
<main className="flex flex-col gap-6">
|
||||
{/* Month Picker placeholder */}
|
||||
<div className="h-[60px] animate-pulse rounded-2xl bg-foreground/10" />
|
||||
return (
|
||||
<main className="flex flex-col gap-6">
|
||||
{/* Month Picker placeholder */}
|
||||
<div className="h-[60px] animate-pulse rounded-2xl bg-foreground/10" />
|
||||
|
||||
{/* Invoice Summary Card */}
|
||||
<section className="flex flex-col gap-4">
|
||||
<InvoiceSummaryCardSkeleton />
|
||||
</section>
|
||||
{/* Invoice Summary Card */}
|
||||
<section className="flex flex-col gap-4">
|
||||
<InvoiceSummaryCardSkeleton />
|
||||
</section>
|
||||
|
||||
{/* Seção de lançamentos */}
|
||||
<section className="flex flex-col gap-4">
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-8 w-48 rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-10 w-40 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
{/* Seção de lançamentos */}
|
||||
<section className="flex flex-col gap-4">
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-8 w-48 rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-10 w-40 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
|
||||
{/* Filtros */}
|
||||
<FilterSkeleton />
|
||||
{/* Filtros */}
|
||||
<FilterSkeleton />
|
||||
|
||||
{/* Tabela */}
|
||||
<TransactionsTableSkeleton />
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
{/* Tabela */}
|
||||
<TransactionsTableSkeleton />
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { RiPencilLine } from "@remixicon/react";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getRecentEstablishmentsAction } from "@/app/(dashboard)/lancamentos/actions";
|
||||
import { CardDialog } from "@/components/cartoes/card-dialog";
|
||||
import type { Card } from "@/components/cartoes/types";
|
||||
@@ -5,204 +7,187 @@ import { InvoiceSummaryCard } from "@/components/faturas/invoice-summary-card";
|
||||
import { LancamentosPage as LancamentosSection } from "@/components/lancamentos/page/lancamentos-page";
|
||||
import MonthNavigation from "@/components/month-picker/month-navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { lancamentos, type Conta } from "@/db/schema";
|
||||
import { db } from "@/lib/db";
|
||||
import type { Conta } from "@/db/schema";
|
||||
import { getUserId } from "@/lib/auth/server";
|
||||
import {
|
||||
buildLancamentoWhere,
|
||||
buildOptionSets,
|
||||
buildSluggedFilters,
|
||||
buildSlugMaps,
|
||||
extractLancamentoSearchFilters,
|
||||
fetchLancamentoFilterSources,
|
||||
getSingleParam,
|
||||
mapLancamentosData,
|
||||
type ResolvedSearchParams,
|
||||
buildLancamentoWhere,
|
||||
buildOptionSets,
|
||||
buildSluggedFilters,
|
||||
buildSlugMaps,
|
||||
extractLancamentoSearchFilters,
|
||||
fetchLancamentoFilterSources,
|
||||
getSingleParam,
|
||||
mapLancamentosData,
|
||||
type ResolvedSearchParams,
|
||||
} from "@/lib/lancamentos/page-helpers";
|
||||
import { loadLogoOptions } from "@/lib/logo/options";
|
||||
import { parsePeriodParam } from "@/lib/utils/period";
|
||||
import { RiPencilLine } from "@remixicon/react";
|
||||
import { and, desc } from "drizzle-orm";
|
||||
import { notFound } from "next/navigation";
|
||||
import { fetchCardData, fetchInvoiceData } from "./data";
|
||||
import { fetchCardData, fetchCardLancamentos, fetchInvoiceData } from "./data";
|
||||
|
||||
type PageSearchParams = Promise<ResolvedSearchParams>;
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{ cartaoId: string }>;
|
||||
searchParams?: PageSearchParams;
|
||||
params: Promise<{ cartaoId: string }>;
|
||||
searchParams?: PageSearchParams;
|
||||
};
|
||||
|
||||
export default async function Page({ params, searchParams }: PageProps) {
|
||||
const { cartaoId } = await params;
|
||||
const userId = await getUserId();
|
||||
const resolvedSearchParams = searchParams ? await searchParams : undefined;
|
||||
const { cartaoId } = await params;
|
||||
const userId = await getUserId();
|
||||
const resolvedSearchParams = searchParams ? await searchParams : undefined;
|
||||
|
||||
const periodoParamRaw = getSingleParam(resolvedSearchParams, "periodo");
|
||||
const {
|
||||
period: selectedPeriod,
|
||||
monthName,
|
||||
year,
|
||||
} = parsePeriodParam(periodoParamRaw);
|
||||
const periodoParamRaw = getSingleParam(resolvedSearchParams, "periodo");
|
||||
const {
|
||||
period: selectedPeriod,
|
||||
monthName,
|
||||
year,
|
||||
} = parsePeriodParam(periodoParamRaw);
|
||||
|
||||
const searchFilters = extractLancamentoSearchFilters(resolvedSearchParams);
|
||||
const searchFilters = extractLancamentoSearchFilters(resolvedSearchParams);
|
||||
|
||||
const card = await fetchCardData(userId, cartaoId);
|
||||
const card = await fetchCardData(userId, cartaoId);
|
||||
|
||||
if (!card) {
|
||||
notFound();
|
||||
}
|
||||
if (!card) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const [
|
||||
filterSources,
|
||||
logoOptions,
|
||||
invoiceData,
|
||||
estabelecimentos,
|
||||
] = await Promise.all([
|
||||
fetchLancamentoFilterSources(userId),
|
||||
loadLogoOptions(),
|
||||
fetchInvoiceData(userId, cartaoId, selectedPeriod),
|
||||
getRecentEstablishmentsAction(),
|
||||
]);
|
||||
const sluggedFilters = buildSluggedFilters(filterSources);
|
||||
const slugMaps = buildSlugMaps(sluggedFilters);
|
||||
const [filterSources, logoOptions, invoiceData, estabelecimentos] =
|
||||
await Promise.all([
|
||||
fetchLancamentoFilterSources(userId),
|
||||
loadLogoOptions(),
|
||||
fetchInvoiceData(userId, cartaoId, selectedPeriod),
|
||||
getRecentEstablishmentsAction(),
|
||||
]);
|
||||
const sluggedFilters = buildSluggedFilters(filterSources);
|
||||
const slugMaps = buildSlugMaps(sluggedFilters);
|
||||
|
||||
const filters = buildLancamentoWhere({
|
||||
userId,
|
||||
period: selectedPeriod,
|
||||
filters: searchFilters,
|
||||
slugMaps,
|
||||
cardId: card.id,
|
||||
});
|
||||
const filters = buildLancamentoWhere({
|
||||
userId,
|
||||
period: selectedPeriod,
|
||||
filters: searchFilters,
|
||||
slugMaps,
|
||||
cardId: card.id,
|
||||
});
|
||||
|
||||
const lancamentoRows = await db.query.lancamentos.findMany({
|
||||
where: and(...filters),
|
||||
with: {
|
||||
pagador: true,
|
||||
conta: true,
|
||||
cartao: true,
|
||||
categoria: true,
|
||||
},
|
||||
orderBy: desc(lancamentos.purchaseDate),
|
||||
});
|
||||
const lancamentoRows = await fetchCardLancamentos(filters);
|
||||
|
||||
const lancamentosData = mapLancamentosData(lancamentoRows);
|
||||
const lancamentosData = mapLancamentosData(lancamentoRows);
|
||||
|
||||
const {
|
||||
pagadorOptions,
|
||||
splitPagadorOptions,
|
||||
defaultPagadorId,
|
||||
contaOptions,
|
||||
cartaoOptions,
|
||||
categoriaOptions,
|
||||
pagadorFilterOptions,
|
||||
categoriaFilterOptions,
|
||||
contaCartaoFilterOptions,
|
||||
} = buildOptionSets({
|
||||
...sluggedFilters,
|
||||
pagadorRows: filterSources.pagadorRows,
|
||||
limitCartaoId: card.id,
|
||||
});
|
||||
const {
|
||||
pagadorOptions,
|
||||
splitPagadorOptions,
|
||||
defaultPagadorId,
|
||||
contaOptions,
|
||||
cartaoOptions,
|
||||
categoriaOptions,
|
||||
pagadorFilterOptions,
|
||||
categoriaFilterOptions,
|
||||
contaCartaoFilterOptions,
|
||||
} = buildOptionSets({
|
||||
...sluggedFilters,
|
||||
pagadorRows: filterSources.pagadorRows,
|
||||
limitCartaoId: card.id,
|
||||
});
|
||||
|
||||
const accountOptions = filterSources.contaRows.map((conta: Conta) => ({
|
||||
id: conta.id,
|
||||
name: conta.name ?? "Conta",
|
||||
}));
|
||||
const accountOptions = filterSources.contaRows.map((conta: Conta) => ({
|
||||
id: conta.id,
|
||||
name: conta.name ?? "Conta",
|
||||
}));
|
||||
|
||||
const contaName =
|
||||
filterSources.contaRows.find((conta: Conta) => conta.id === card.contaId)
|
||||
?.name ?? "Conta";
|
||||
const contaName =
|
||||
filterSources.contaRows.find((conta: Conta) => conta.id === card.contaId)
|
||||
?.name ?? "Conta";
|
||||
|
||||
const cardDialogData: Card = {
|
||||
id: card.id,
|
||||
name: card.name,
|
||||
brand: card.brand ?? "",
|
||||
status: card.status ?? "",
|
||||
closingDay: card.closingDay,
|
||||
dueDay: card.dueDay,
|
||||
note: card.note ?? null,
|
||||
logo: card.logo,
|
||||
limit:
|
||||
card.limit !== null && card.limit !== undefined
|
||||
? Number(card.limit)
|
||||
: null,
|
||||
contaId: card.contaId,
|
||||
contaName,
|
||||
limitInUse: null,
|
||||
limitAvailable: null,
|
||||
};
|
||||
const cardDialogData: Card = {
|
||||
id: card.id,
|
||||
name: card.name,
|
||||
brand: card.brand ?? "",
|
||||
status: card.status ?? "",
|
||||
closingDay: card.closingDay,
|
||||
dueDay: card.dueDay,
|
||||
note: card.note ?? null,
|
||||
logo: card.logo,
|
||||
limit:
|
||||
card.limit !== null && card.limit !== undefined
|
||||
? Number(card.limit)
|
||||
: null,
|
||||
contaId: card.contaId,
|
||||
contaName,
|
||||
limitInUse: null,
|
||||
limitAvailable: null,
|
||||
};
|
||||
|
||||
const { totalAmount, invoiceStatus, paymentDate } = invoiceData;
|
||||
const limitAmount =
|
||||
card.limit !== null && card.limit !== undefined ? Number(card.limit) : null;
|
||||
const { totalAmount, invoiceStatus, paymentDate } = invoiceData;
|
||||
const limitAmount =
|
||||
card.limit !== null && card.limit !== undefined ? Number(card.limit) : null;
|
||||
|
||||
const periodLabel = `${monthName.charAt(0).toUpperCase()}${monthName.slice(
|
||||
1
|
||||
)} de ${year}`;
|
||||
const periodLabel = `${monthName.charAt(0).toUpperCase()}${monthName.slice(
|
||||
1,
|
||||
)} de ${year}`;
|
||||
|
||||
return (
|
||||
<main className="flex flex-col gap-6">
|
||||
<MonthNavigation />
|
||||
return (
|
||||
<main className="flex flex-col gap-6">
|
||||
<MonthNavigation />
|
||||
|
||||
<section className="flex flex-col gap-4">
|
||||
<InvoiceSummaryCard
|
||||
cartaoId={card.id}
|
||||
period={selectedPeriod}
|
||||
cardName={card.name}
|
||||
cardBrand={card.brand ?? null}
|
||||
cardStatus={card.status ?? null}
|
||||
closingDay={card.closingDay}
|
||||
dueDay={card.dueDay}
|
||||
periodLabel={periodLabel}
|
||||
totalAmount={totalAmount}
|
||||
limitAmount={limitAmount}
|
||||
invoiceStatus={invoiceStatus}
|
||||
paymentDate={paymentDate}
|
||||
logo={card.logo}
|
||||
actions={
|
||||
<CardDialog
|
||||
mode="update"
|
||||
card={cardDialogData}
|
||||
logoOptions={logoOptions}
|
||||
accounts={accountOptions}
|
||||
trigger={
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
aria-label="Editar cartão"
|
||||
>
|
||||
<RiPencilLine className="size-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</section>
|
||||
<section className="flex flex-col gap-4">
|
||||
<InvoiceSummaryCard
|
||||
cartaoId={card.id}
|
||||
period={selectedPeriod}
|
||||
cardName={card.name}
|
||||
cardBrand={card.brand ?? null}
|
||||
cardStatus={card.status ?? null}
|
||||
closingDay={card.closingDay}
|
||||
dueDay={card.dueDay}
|
||||
periodLabel={periodLabel}
|
||||
totalAmount={totalAmount}
|
||||
limitAmount={limitAmount}
|
||||
invoiceStatus={invoiceStatus}
|
||||
paymentDate={paymentDate}
|
||||
logo={card.logo}
|
||||
actions={
|
||||
<CardDialog
|
||||
mode="update"
|
||||
card={cardDialogData}
|
||||
logoOptions={logoOptions}
|
||||
accounts={accountOptions}
|
||||
trigger={
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
aria-label="Editar cartão"
|
||||
>
|
||||
<RiPencilLine className="size-4" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="flex flex-col gap-4">
|
||||
<LancamentosSection
|
||||
currentUserId={userId}
|
||||
lancamentos={lancamentosData}
|
||||
pagadorOptions={pagadorOptions}
|
||||
splitPagadorOptions={splitPagadorOptions}
|
||||
defaultPagadorId={defaultPagadorId}
|
||||
contaOptions={contaOptions}
|
||||
cartaoOptions={cartaoOptions}
|
||||
categoriaOptions={categoriaOptions}
|
||||
pagadorFilterOptions={pagadorFilterOptions}
|
||||
categoriaFilterOptions={categoriaFilterOptions}
|
||||
contaCartaoFilterOptions={contaCartaoFilterOptions}
|
||||
selectedPeriod={selectedPeriod}
|
||||
estabelecimentos={estabelecimentos}
|
||||
allowCreate
|
||||
defaultCartaoId={card.id}
|
||||
defaultPaymentMethod="Cartão de crédito"
|
||||
lockCartaoSelection
|
||||
lockPaymentMethod
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
<section className="flex flex-col gap-4">
|
||||
<LancamentosSection
|
||||
currentUserId={userId}
|
||||
lancamentos={lancamentosData}
|
||||
pagadorOptions={pagadorOptions}
|
||||
splitPagadorOptions={splitPagadorOptions}
|
||||
defaultPagadorId={defaultPagadorId}
|
||||
contaOptions={contaOptions}
|
||||
cartaoOptions={cartaoOptions}
|
||||
categoriaOptions={categoriaOptions}
|
||||
pagadorFilterOptions={pagadorFilterOptions}
|
||||
categoriaFilterOptions={categoriaFilterOptions}
|
||||
contaCartaoFilterOptions={contaCartaoFilterOptions}
|
||||
selectedPeriod={selectedPeriod}
|
||||
estabelecimentos={estabelecimentos}
|
||||
allowCreate
|
||||
defaultCartaoId={card.id}
|
||||
defaultPaymentMethod="Cartão de crédito"
|
||||
lockCartaoSelection
|
||||
lockPaymentMethod
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,51 +1,54 @@
|
||||
"use server";
|
||||
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { cartoes, contas } from "@/db/schema";
|
||||
import { type ActionResult, handleActionError } from "@/lib/actions/helpers";
|
||||
import { revalidateForEntity } from "@/lib/actions/helpers";
|
||||
import {
|
||||
dayOfMonthSchema,
|
||||
noteSchema,
|
||||
optionalDecimalSchema,
|
||||
uuidSchema,
|
||||
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";
|
||||
import { db } from "@/lib/db";
|
||||
import { getUser } from "@/lib/auth/server";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
|
||||
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"),
|
||||
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"),
|
||||
id: uuidSchema("Cartão"),
|
||||
});
|
||||
const deleteCardSchema = z.object({
|
||||
id: uuidSchema("Cartão"),
|
||||
id: uuidSchema("Cartão"),
|
||||
});
|
||||
|
||||
type CardCreateInput = z.infer<typeof createCardSchema>;
|
||||
@@ -53,113 +56,113 @@ 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)),
|
||||
});
|
||||
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.");
|
||||
}
|
||||
if (!account) {
|
||||
throw new Error("Conta vinculada não encontrada.");
|
||||
}
|
||||
}
|
||||
|
||||
export async function createCardAction(
|
||||
input: CardCreateInput
|
||||
input: CardCreateInput,
|
||||
): Promise<ActionResult> {
|
||||
try {
|
||||
const user = await getUser();
|
||||
const data = createCardSchema.parse(input);
|
||||
try {
|
||||
const user = await getUser();
|
||||
const data = createCardSchema.parse(input);
|
||||
|
||||
await assertAccountOwnership(user.id, data.contaId);
|
||||
await assertAccountOwnership(user.id, data.contaId);
|
||||
|
||||
const logoFile = normalizeFilePath(data.logo);
|
||||
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,
|
||||
});
|
||||
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");
|
||||
revalidateForEntity("cartoes");
|
||||
|
||||
return { success: true, message: "Cartão criado com sucesso." };
|
||||
} catch (error) {
|
||||
return handleActionError(error);
|
||||
}
|
||||
return { success: true, message: "Cartão criado com sucesso." };
|
||||
} catch (error) {
|
||||
return handleActionError(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateCardAction(
|
||||
input: CardUpdateInput
|
||||
input: CardUpdateInput,
|
||||
): Promise<ActionResult> {
|
||||
try {
|
||||
const user = await getUser();
|
||||
const data = updateCardSchema.parse(input);
|
||||
try {
|
||||
const user = await getUser();
|
||||
const data = updateCardSchema.parse(input);
|
||||
|
||||
await assertAccountOwnership(user.id, data.contaId);
|
||||
await assertAccountOwnership(user.id, data.contaId);
|
||||
|
||||
const logoFile = normalizeFilePath(data.logo);
|
||||
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();
|
||||
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.",
|
||||
};
|
||||
}
|
||||
if (!updated) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Cartão não encontrado.",
|
||||
};
|
||||
}
|
||||
|
||||
revalidateForEntity("cartoes");
|
||||
revalidateForEntity("cartoes");
|
||||
|
||||
return { success: true, message: "Cartão atualizado com sucesso." };
|
||||
} catch (error) {
|
||||
return handleActionError(error);
|
||||
}
|
||||
return { success: true, message: "Cartão atualizado com sucesso." };
|
||||
} catch (error) {
|
||||
return handleActionError(error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteCardAction(
|
||||
input: CardDeleteInput
|
||||
input: CardDeleteInput,
|
||||
): Promise<ActionResult> {
|
||||
try {
|
||||
const user = await getUser();
|
||||
const data = deleteCardSchema.parse(input);
|
||||
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 });
|
||||
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.",
|
||||
};
|
||||
}
|
||||
if (!deleted) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Cartão não encontrado.",
|
||||
};
|
||||
}
|
||||
|
||||
revalidateForEntity("cartoes");
|
||||
revalidateForEntity("cartoes");
|
||||
|
||||
return { success: true, message: "Cartão removido com sucesso." };
|
||||
} catch (error) {
|
||||
return handleActionError(error);
|
||||
}
|
||||
return { success: true, message: "Cartão removido com sucesso." };
|
||||
} catch (error) {
|
||||
return handleActionError(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,191 +4,210 @@ import { loadLogoOptions } from "@/lib/logo/options";
|
||||
import { and, eq, ilike, isNull, not, or, sql } from "drizzle-orm";
|
||||
|
||||
export type CardData = {
|
||||
id: string;
|
||||
name: string;
|
||||
brand: string | null;
|
||||
status: string | null;
|
||||
closingDay: number;
|
||||
dueDay: number;
|
||||
note: string | null;
|
||||
logo: string | null;
|
||||
limit: number | null;
|
||||
limitInUse: number;
|
||||
limitAvailable: number | null;
|
||||
contaId: string;
|
||||
contaName: string;
|
||||
id: string;
|
||||
name: string;
|
||||
brand: string | null;
|
||||
status: string | null;
|
||||
closingDay: number;
|
||||
dueDay: number;
|
||||
note: string | null;
|
||||
logo: string | null;
|
||||
limit: number | null;
|
||||
limitInUse: number;
|
||||
limitAvailable: number | null;
|
||||
contaId: string;
|
||||
contaName: string;
|
||||
};
|
||||
|
||||
export type AccountSimple = {
|
||||
id: string;
|
||||
name: string;
|
||||
logo: string | null;
|
||||
id: string;
|
||||
name: string;
|
||||
logo: string | null;
|
||||
};
|
||||
|
||||
export async function fetchCardsForUser(userId: string): Promise<{
|
||||
cards: CardData[];
|
||||
accounts: AccountSimple[];
|
||||
logoOptions: LogoOption[];
|
||||
cards: CardData[];
|
||||
accounts: AccountSimple[];
|
||||
logoOptions: LogoOption[];
|
||||
}> {
|
||||
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"))),
|
||||
with: {
|
||||
conta: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
db.query.contas.findMany({
|
||||
orderBy: (account: typeof contas.$inferSelect, { desc }: { desc: (field: unknown) => unknown }) => [desc(account.name)],
|
||||
where: eq(contas.userId, userId),
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
logo: true,
|
||||
},
|
||||
}),
|
||||
loadLogoOptions(),
|
||||
db
|
||||
.select({
|
||||
cartaoId: lancamentos.cartaoId,
|
||||
total: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, userId),
|
||||
or(isNull(lancamentos.isSettled), eq(lancamentos.isSettled, false))
|
||||
)
|
||||
)
|
||||
.groupBy(lancamentos.cartaoId),
|
||||
]);
|
||||
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")),
|
||||
),
|
||||
with: {
|
||||
conta: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
db.query.contas.findMany({
|
||||
orderBy: (
|
||||
account: typeof contas.$inferSelect,
|
||||
{ desc }: { desc: (field: unknown) => unknown },
|
||||
) => [desc(account.name)],
|
||||
where: eq(contas.userId, userId),
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
logo: true,
|
||||
},
|
||||
}),
|
||||
loadLogoOptions(),
|
||||
db
|
||||
.select({
|
||||
cartaoId: lancamentos.cartaoId,
|
||||
total: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, userId),
|
||||
or(isNull(lancamentos.isSettled), eq(lancamentos.isSettled, false)),
|
||||
),
|
||||
)
|
||||
.groupBy(lancamentos.cartaoId),
|
||||
]);
|
||||
|
||||
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));
|
||||
});
|
||||
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));
|
||||
},
|
||||
);
|
||||
|
||||
const cards = cardRows.map((card) => ({
|
||||
id: card.id,
|
||||
name: card.name,
|
||||
brand: card.brand,
|
||||
status: card.status,
|
||||
closingDay: card.closingDay,
|
||||
dueDay: card.dueDay,
|
||||
note: card.note,
|
||||
logo: card.logo,
|
||||
limit: card.limit ? Number(card.limit) : null,
|
||||
limitInUse: (() => {
|
||||
const total = usageMap.get(card.id) ?? 0;
|
||||
return total < 0 ? Math.abs(total) : 0;
|
||||
})(),
|
||||
limitAvailable: (() => {
|
||||
if (!card.limit) {
|
||||
return null;
|
||||
}
|
||||
const total = usageMap.get(card.id) ?? 0;
|
||||
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",
|
||||
}));
|
||||
const cards = cardRows.map((card) => ({
|
||||
id: card.id,
|
||||
name: card.name,
|
||||
brand: card.brand,
|
||||
status: card.status,
|
||||
closingDay: card.closingDay,
|
||||
dueDay: card.dueDay,
|
||||
note: card.note,
|
||||
logo: card.logo,
|
||||
limit: card.limit ? Number(card.limit) : null,
|
||||
limitInUse: (() => {
|
||||
const total = usageMap.get(card.id) ?? 0;
|
||||
return total < 0 ? Math.abs(total) : 0;
|
||||
})(),
|
||||
limitAvailable: (() => {
|
||||
if (!card.limit) {
|
||||
return null;
|
||||
}
|
||||
const total = usageMap.get(card.id) ?? 0;
|
||||
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",
|
||||
}));
|
||||
|
||||
const accounts = accountRows.map((account) => ({
|
||||
id: account.id,
|
||||
name: account.name,
|
||||
logo: account.logo,
|
||||
}));
|
||||
const accounts = accountRows.map((account) => ({
|
||||
id: account.id,
|
||||
name: account.name,
|
||||
logo: account.logo,
|
||||
}));
|
||||
|
||||
return { cards, accounts, logoOptions };
|
||||
return { cards, accounts, logoOptions };
|
||||
}
|
||||
|
||||
export async function fetchInativosForUser(userId: string): Promise<{
|
||||
cards: CardData[];
|
||||
accounts: AccountSimple[];
|
||||
logoOptions: LogoOption[];
|
||||
cards: CardData[];
|
||||
accounts: AccountSimple[];
|
||||
logoOptions: LogoOption[];
|
||||
}> {
|
||||
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")),
|
||||
with: {
|
||||
conta: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
db.query.contas.findMany({
|
||||
orderBy: (account: typeof contas.$inferSelect, { desc }: { desc: (field: unknown) => unknown }) => [desc(account.name)],
|
||||
where: eq(contas.userId, userId),
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
logo: true,
|
||||
},
|
||||
}),
|
||||
loadLogoOptions(),
|
||||
db
|
||||
.select({
|
||||
cartaoId: lancamentos.cartaoId,
|
||||
total: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, userId),
|
||||
or(isNull(lancamentos.isSettled), eq(lancamentos.isSettled, false))
|
||||
)
|
||||
)
|
||||
.groupBy(lancamentos.cartaoId),
|
||||
]);
|
||||
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")),
|
||||
with: {
|
||||
conta: {
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
db.query.contas.findMany({
|
||||
orderBy: (
|
||||
account: typeof contas.$inferSelect,
|
||||
{ desc }: { desc: (field: unknown) => unknown },
|
||||
) => [desc(account.name)],
|
||||
where: eq(contas.userId, userId),
|
||||
columns: {
|
||||
id: true,
|
||||
name: true,
|
||||
logo: true,
|
||||
},
|
||||
}),
|
||||
loadLogoOptions(),
|
||||
db
|
||||
.select({
|
||||
cartaoId: lancamentos.cartaoId,
|
||||
total: sql<number>`coalesce(sum(${lancamentos.amount}), 0)`,
|
||||
})
|
||||
.from(lancamentos)
|
||||
.where(
|
||||
and(
|
||||
eq(lancamentos.userId, userId),
|
||||
or(isNull(lancamentos.isSettled), eq(lancamentos.isSettled, false)),
|
||||
),
|
||||
)
|
||||
.groupBy(lancamentos.cartaoId),
|
||||
]);
|
||||
|
||||
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));
|
||||
});
|
||||
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));
|
||||
},
|
||||
);
|
||||
|
||||
const cards = cardRows.map((card) => ({
|
||||
id: card.id,
|
||||
name: card.name,
|
||||
brand: card.brand,
|
||||
status: card.status,
|
||||
closingDay: card.closingDay,
|
||||
dueDay: card.dueDay,
|
||||
note: card.note,
|
||||
logo: card.logo,
|
||||
limit: card.limit ? Number(card.limit) : null,
|
||||
limitInUse: (() => {
|
||||
const total = usageMap.get(card.id) ?? 0;
|
||||
return total < 0 ? Math.abs(total) : 0;
|
||||
})(),
|
||||
limitAvailable: (() => {
|
||||
if (!card.limit) {
|
||||
return null;
|
||||
}
|
||||
const total = usageMap.get(card.id) ?? 0;
|
||||
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",
|
||||
}));
|
||||
const cards = cardRows.map((card) => ({
|
||||
id: card.id,
|
||||
name: card.name,
|
||||
brand: card.brand,
|
||||
status: card.status,
|
||||
closingDay: card.closingDay,
|
||||
dueDay: card.dueDay,
|
||||
note: card.note,
|
||||
logo: card.logo,
|
||||
limit: card.limit ? Number(card.limit) : null,
|
||||
limitInUse: (() => {
|
||||
const total = usageMap.get(card.id) ?? 0;
|
||||
return total < 0 ? Math.abs(total) : 0;
|
||||
})(),
|
||||
limitAvailable: (() => {
|
||||
if (!card.limit) {
|
||||
return null;
|
||||
}
|
||||
const total = usageMap.get(card.id) ?? 0;
|
||||
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",
|
||||
}));
|
||||
|
||||
const accounts = accountRows.map((account) => ({
|
||||
id: account.id,
|
||||
name: account.name,
|
||||
logo: account.logo,
|
||||
}));
|
||||
const accounts = accountRows.map((account) => ({
|
||||
id: account.id,
|
||||
name: account.name,
|
||||
logo: account.logo,
|
||||
}));
|
||||
|
||||
return { cards, accounts, logoOptions };
|
||||
return { cards, accounts, logoOptions };
|
||||
}
|
||||
|
||||
@@ -3,17 +3,17 @@ import { getUserId } from "@/lib/auth/server";
|
||||
import { fetchInativosForUser } from "../data";
|
||||
|
||||
export default async function InativosPage() {
|
||||
const userId = await getUserId();
|
||||
const { cards, accounts, logoOptions } = await fetchInativosForUser(userId);
|
||||
const userId = await getUserId();
|
||||
const { cards, accounts, logoOptions } = await fetchInativosForUser(userId);
|
||||
|
||||
return (
|
||||
<main className="flex flex-col items-start gap-6">
|
||||
<CardsPage
|
||||
cards={cards}
|
||||
accounts={accounts}
|
||||
logoOptions={logoOptions}
|
||||
isInativos={true}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
return (
|
||||
<main className="flex flex-col items-start gap-6">
|
||||
<CardsPage
|
||||
cards={cards}
|
||||
accounts={accounts}
|
||||
logoOptions={logoOptions}
|
||||
isInativos={true}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import PageDescription from "@/components/page-description";
|
||||
import { RiBankCard2Line } from "@remixicon/react";
|
||||
import PageDescription from "@/components/page-description";
|
||||
|
||||
export const metadata = {
|
||||
title: "Cartões | Opensheets",
|
||||
title: "Cartões | Opensheets",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section className="space-y-6 px-6">
|
||||
<PageDescription
|
||||
icon={<RiBankCard2Line />}
|
||||
title="Cartões"
|
||||
subtitle="Acompanhe todas os cartões do mês selecionado incluindo faturas, limites
|
||||
return (
|
||||
<section className="space-y-6 px-6">
|
||||
<PageDescription
|
||||
icon={<RiBankCard2Line />}
|
||||
title="Cartões"
|
||||
subtitle="Acompanhe todas os cartões do mês selecionado incluindo faturas, limites
|
||||
e transações previstas. Use o seletor abaixo para navegar pelos meses e
|
||||
visualizar as movimentações correspondentes."
|
||||
/>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
/>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,33 +1,30 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
/**
|
||||
* Loading state para a página de cartões
|
||||
*/
|
||||
export default function CartoesLoading() {
|
||||
return (
|
||||
<main className="flex flex-col gap-6">
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-8 w-32 rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-10 w-40 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
return (
|
||||
<main className="flex flex-col gap-6">
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-8 w-32 rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-10 w-40 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
|
||||
{/* Grid de cartões */}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="rounded-2xl border p-6 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="size-10 rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-8 w-16 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
<Skeleton className="h-6 w-32 rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-4 w-full rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-4 w-24 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
{/* Grid de cartões */}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="rounded-2xl border p-6 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="size-10 rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-8 w-16 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
<Skeleton className="h-6 w-32 rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-4 w-full rounded-2xl bg-foreground/10" />
|
||||
<Skeleton className="h-4 w-24 rounded-2xl bg-foreground/10" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ import { getUserId } from "@/lib/auth/server";
|
||||
import { fetchCardsForUser } from "./data";
|
||||
|
||||
export default async function Page() {
|
||||
const userId = await getUserId();
|
||||
const { cards, accounts, logoOptions } = await fetchCardsForUser(userId);
|
||||
const userId = await getUserId();
|
||||
const { cards, accounts, logoOptions } = await fetchCardsForUser(userId);
|
||||
|
||||
return (
|
||||
<main className="flex flex-col items-start gap-6">
|
||||
<CardsPage cards={cards} accounts={accounts} logoOptions={logoOptions} />
|
||||
</main>
|
||||
);
|
||||
return (
|
||||
<main className="flex flex-col items-start gap-6">
|
||||
<CardsPage cards={cards} accounts={accounts} logoOptions={logoOptions} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user