mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-10 03:11:46 +00:00
feat(lançamentos): escopo "period" na ação em lote e correção do fluxo de anexos em séries
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -33,7 +33,7 @@ const presignSchema = z.object({
|
||||
|
||||
const confirmSchema = z.object({
|
||||
uploadToken: z.string().min(1),
|
||||
applyToSeries: z.boolean().default(false),
|
||||
scope: z.enum(["current", "period", "future", "all"]).default("current"),
|
||||
});
|
||||
|
||||
const detachSchema = z.object({
|
||||
@@ -183,7 +183,7 @@ export async function getPresignedUploadUrlAction(input: {
|
||||
|
||||
export async function confirmAttachmentUploadAction(input: {
|
||||
uploadToken: string;
|
||||
applyToSeries?: boolean;
|
||||
scope?: "current" | "period" | "future" | "all";
|
||||
}): Promise<ActionResult> {
|
||||
try {
|
||||
const user = await getUser();
|
||||
@@ -195,7 +195,11 @@ export async function confirmAttachmentUploadAction(input: {
|
||||
}
|
||||
|
||||
const [transaction] = await db
|
||||
.select({ id: transactions.id, seriesId: transactions.seriesId })
|
||||
.select({
|
||||
id: transactions.id,
|
||||
seriesId: transactions.seriesId,
|
||||
period: transactions.period,
|
||||
})
|
||||
.from(transactions)
|
||||
.where(
|
||||
and(
|
||||
@@ -253,9 +257,9 @@ export async function confirmAttachmentUploadAction(input: {
|
||||
|
||||
let transactionIds: string[] = [uploadPayload.transactionId];
|
||||
|
||||
if (data.applyToSeries && transaction.seriesId) {
|
||||
if (data.scope !== "current" && transaction.seriesId) {
|
||||
const seriesRows = await db
|
||||
.select({ id: transactions.id })
|
||||
.select({ id: transactions.id, period: transactions.period })
|
||||
.from(transactions)
|
||||
.where(
|
||||
and(
|
||||
@@ -263,7 +267,18 @@ export async function confirmAttachmentUploadAction(input: {
|
||||
eq(transactions.userId, user.id),
|
||||
),
|
||||
);
|
||||
transactionIds = seriesRows.map((t) => t.id);
|
||||
|
||||
if (data.scope === "period") {
|
||||
transactionIds = seriesRows
|
||||
.filter((r) => r.period === transaction.period)
|
||||
.map((r) => r.id);
|
||||
} else if (data.scope === "future") {
|
||||
transactionIds = seriesRows
|
||||
.filter((r) => (r.period ?? "") >= (transaction.period ?? ""))
|
||||
.map((r) => r.id);
|
||||
} else {
|
||||
transactionIds = seriesRows.map((r) => r.id);
|
||||
}
|
||||
}
|
||||
|
||||
await db.insert(transactionAttachments).values(
|
||||
@@ -407,6 +422,110 @@ export async function fetchTransactionAttachmentsAction(
|
||||
);
|
||||
}
|
||||
|
||||
const detachBulkSchema = z.object({
|
||||
attachmentId: z.string().uuid(),
|
||||
transactionId: z.string().uuid(),
|
||||
scope: z.enum(["current", "period", "future", "all"]),
|
||||
});
|
||||
|
||||
export async function detachAttachmentBulkAction(input: {
|
||||
attachmentId: string;
|
||||
transactionId: string;
|
||||
scope: "current" | "period" | "future" | "all";
|
||||
}): Promise<ActionResult> {
|
||||
try {
|
||||
const user = await getUser();
|
||||
const data = detachBulkSchema.parse(input);
|
||||
|
||||
const [baseTransaction] = await db
|
||||
.select({
|
||||
id: transactions.id,
|
||||
seriesId: transactions.seriesId,
|
||||
period: transactions.period,
|
||||
})
|
||||
.from(transactions)
|
||||
.where(
|
||||
and(
|
||||
eq(transactions.id, data.transactionId),
|
||||
eq(transactions.userId, user.id),
|
||||
),
|
||||
);
|
||||
|
||||
if (!baseTransaction) {
|
||||
return { success: false, error: "Lançamento não encontrado." };
|
||||
}
|
||||
|
||||
const [attachment] = await db
|
||||
.select({ id: attachments.id, fileKey: attachments.fileKey })
|
||||
.from(attachments)
|
||||
.where(
|
||||
and(
|
||||
eq(attachments.id, data.attachmentId),
|
||||
eq(attachments.userId, user.id),
|
||||
),
|
||||
);
|
||||
|
||||
if (!attachment) {
|
||||
return { success: false, error: "Anexo não encontrado." };
|
||||
}
|
||||
|
||||
let targetTransactionIds: string[];
|
||||
|
||||
if (data.scope === "current" || !baseTransaction.seriesId) {
|
||||
targetTransactionIds = [data.transactionId];
|
||||
} else {
|
||||
const seriesRows = await db
|
||||
.select({ id: transactions.id, period: transactions.period })
|
||||
.from(transactions)
|
||||
.where(
|
||||
and(
|
||||
eq(transactions.seriesId, baseTransaction.seriesId),
|
||||
eq(transactions.userId, user.id),
|
||||
),
|
||||
);
|
||||
|
||||
if (data.scope === "period") {
|
||||
targetTransactionIds = seriesRows
|
||||
.filter((r) => r.period === baseTransaction.period)
|
||||
.map((r) => r.id);
|
||||
} else if (data.scope === "future") {
|
||||
targetTransactionIds = seriesRows
|
||||
.filter((r) => (r.period ?? "") >= (baseTransaction.period ?? ""))
|
||||
.map((r) => r.id);
|
||||
} else {
|
||||
targetTransactionIds = seriesRows.map((r) => r.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (targetTransactionIds.length > 0) {
|
||||
await db
|
||||
.delete(transactionAttachments)
|
||||
.where(
|
||||
and(
|
||||
inArray(transactionAttachments.transactionId, targetTransactionIds),
|
||||
eq(transactionAttachments.attachmentId, data.attachmentId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const [remaining] = await db
|
||||
.select({ total: count() })
|
||||
.from(transactionAttachments)
|
||||
.where(eq(transactionAttachments.attachmentId, data.attachmentId));
|
||||
|
||||
if (!remaining || remaining.total === 0) {
|
||||
await deleteS3Object(attachment.fileKey);
|
||||
await db.delete(attachments).where(eq(attachments.id, data.attachmentId));
|
||||
}
|
||||
|
||||
revalidateForEntity("transactions", user.id);
|
||||
|
||||
return { success: true, message: "Anexo removido com sucesso." };
|
||||
} catch (error) {
|
||||
return handleActionError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/** Limpa anexos órfãos do S3 após deletar transações. Chame APÓS o delete. */
|
||||
export async function cleanupAttachmentsAfterTransactionDelete(
|
||||
attachmentData: Array<{ id: string; fileKey: string }>,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { and, asc, eq, inArray, sql } from "drizzle-orm";
|
||||
import { and, asc, eq, inArray, isNull, sql } from "drizzle-orm";
|
||||
import { transactions } from "@/db/schema";
|
||||
import {
|
||||
PAYMENT_METHODS,
|
||||
@@ -80,6 +80,24 @@ export async function deleteTransactionBulkAction(
|
||||
return { success: true, message: "Lançamento removido com sucesso." };
|
||||
}
|
||||
|
||||
if (data.scope === "period") {
|
||||
await db
|
||||
.delete(transactions)
|
||||
.where(
|
||||
and(
|
||||
eq(transactions.seriesId, existing.seriesId),
|
||||
eq(transactions.userId, user.id),
|
||||
eq(transactions.period, existing.period ?? ""),
|
||||
),
|
||||
);
|
||||
|
||||
revalidate(user.id);
|
||||
return {
|
||||
success: true,
|
||||
message: "Todos os lançamentos do período foram removidos.",
|
||||
};
|
||||
}
|
||||
|
||||
if (data.scope === "future") {
|
||||
await db
|
||||
.delete(transactions)
|
||||
@@ -147,6 +165,7 @@ export async function updateTransactionBulkAction(
|
||||
condition: true,
|
||||
transactionType: true,
|
||||
purchaseDate: true,
|
||||
payerId: true,
|
||||
},
|
||||
where: and(
|
||||
eq(transactions.id, data.id),
|
||||
@@ -169,7 +188,8 @@ export async function updateTransactionBulkAction(
|
||||
name: data.name,
|
||||
categoryId: data.categoryId ?? null,
|
||||
note: data.note ?? null,
|
||||
payerId: data.payerId ?? null,
|
||||
// "period" atualiza todos os pagadores do mês — preserva o payerId de cada linha
|
||||
...(data.scope !== "period" && { payerId: data.payerId ?? null }),
|
||||
accountId: data.accountId ?? null,
|
||||
cardId: data.cardId ?? null,
|
||||
...(data.isSettled !== undefined && { isSettled: data.isSettled }),
|
||||
@@ -309,6 +329,42 @@ export async function updateTransactionBulkAction(
|
||||
return { success: true, message: "Lançamento atualizado com sucesso." };
|
||||
}
|
||||
|
||||
if (data.scope === "period") {
|
||||
if (!existing.period) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Período do lançamento não encontrado.",
|
||||
};
|
||||
}
|
||||
|
||||
const periodLancamentos = await db.query.transactions.findMany({
|
||||
columns: { id: true, purchaseDate: true },
|
||||
where: and(
|
||||
eq(transactions.seriesId, existing.seriesId),
|
||||
eq(transactions.userId, user.id),
|
||||
eq(transactions.period, existing.period),
|
||||
),
|
||||
orderBy: asc(transactions.purchaseDate),
|
||||
});
|
||||
|
||||
await applyUpdates(
|
||||
periodLancamentos.map((item: (typeof periodLancamentos)[number]) => ({
|
||||
id: item.id,
|
||||
purchaseDate: item.purchaseDate ?? null,
|
||||
})),
|
||||
);
|
||||
|
||||
revalidate(user.id);
|
||||
return {
|
||||
success: true,
|
||||
message: "Todos os lançamentos do período foram atualizados.",
|
||||
};
|
||||
}
|
||||
|
||||
const payerIdFilter = existing.payerId
|
||||
? eq(transactions.payerId, existing.payerId)
|
||||
: isNull(transactions.payerId);
|
||||
|
||||
if (data.scope === "future") {
|
||||
const futureLancamentos = await db.query.transactions.findMany({
|
||||
columns: {
|
||||
@@ -319,6 +375,7 @@ export async function updateTransactionBulkAction(
|
||||
eq(transactions.seriesId, existing.seriesId),
|
||||
eq(transactions.userId, user.id),
|
||||
sql`${transactions.period} >= ${existing.period}`,
|
||||
payerIdFilter,
|
||||
),
|
||||
orderBy: asc(transactions.purchaseDate),
|
||||
});
|
||||
@@ -346,6 +403,7 @@ export async function updateTransactionBulkAction(
|
||||
where: and(
|
||||
eq(transactions.seriesId, existing.seriesId),
|
||||
eq(transactions.userId, user.id),
|
||||
payerIdFilter,
|
||||
),
|
||||
orderBy: asc(transactions.purchaseDate),
|
||||
});
|
||||
|
||||
@@ -664,7 +664,7 @@ export const buildLancamentoRecords = ({
|
||||
|
||||
export const deleteBulkSchema = z.object({
|
||||
id: uuidSchema("Lançamento"),
|
||||
scope: z.enum(["current", "future", "all"], {
|
||||
scope: z.enum(["current", "period", "future", "all"], {
|
||||
message: "Escopo de ação inválido.",
|
||||
}),
|
||||
});
|
||||
@@ -673,7 +673,7 @@ export type DeleteBulkInput = z.infer<typeof deleteBulkSchema>;
|
||||
|
||||
export const updateBulkSchema = z.object({
|
||||
id: uuidSchema("Lançamento"),
|
||||
scope: z.enum(["current", "future", "all"], {
|
||||
scope: z.enum(["current", "period", "future", "all"], {
|
||||
message: "Escopo de ação inválido.",
|
||||
}),
|
||||
name: z
|
||||
|
||||
Reference in New Issue
Block a user