mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-06-09 23:06:01 +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:
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { RiErrorWarningLine } from "@remixicon/react";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
import {
|
||||
@@ -13,7 +14,7 @@ import {
|
||||
import { Label } from "@/shared/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/shared/components/ui/radio-group";
|
||||
|
||||
export type BulkActionScope = "current" | "future" | "all";
|
||||
export type BulkActionScope = "current" | "period" | "future" | "all";
|
||||
|
||||
type BulkActionDialogProps = {
|
||||
open: boolean;
|
||||
@@ -108,6 +109,30 @@ export function BulkActionDialog({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3">
|
||||
<RadioGroupItem value="period" id="period" className="mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<Label
|
||||
htmlFor="period"
|
||||
className="text-sm cursor-pointer font-medium"
|
||||
>
|
||||
Todos os pagadores deste período
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Aplica a todos os lançamentos deste mesmo mês na série
|
||||
</p>
|
||||
{scope === "period" && actionType === "edit" && (
|
||||
<div className="mt-1.5 flex items-start gap-1.5 rounded-md bg-amber-50 px-2 py-1.5 text-amber-800 dark:bg-amber-950/40 dark:text-amber-300">
|
||||
<RiErrorWarningLine className="mt-0.5 size-3.5 shrink-0" />
|
||||
<p className="text-xs">
|
||||
Atenção: os valores individuais de cada pagador serão
|
||||
substituídos pelos valores deste lançamento.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3">
|
||||
<RadioGroupItem value="future" id="future" className="mt-0.5" />
|
||||
<div className="flex-1">
|
||||
|
||||
@@ -223,7 +223,6 @@ export function TransactionDetailsDialog({
|
||||
<div className="min-w-0">
|
||||
<AttachmentSection
|
||||
transactionId={transaction.id}
|
||||
seriesId={transaction.seriesId}
|
||||
readonly
|
||||
onLoaded={setAttachmentCount}
|
||||
/>
|
||||
|
||||
@@ -30,6 +30,8 @@ export interface TransactionDialogProps {
|
||||
forceShowTransactionType?: boolean;
|
||||
/** Called after successful create/update. Receives the action result. */
|
||||
onSuccess?: () => void;
|
||||
/** Max attachment file size in MB for this user */
|
||||
maxSizeMb?: number;
|
||||
onBulkEditRequest?: (data: {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -42,6 +44,8 @@ export interface TransactionDialogProps {
|
||||
dueDate: string | null;
|
||||
boletoPaymentDate: string | null;
|
||||
isSettled: boolean | null;
|
||||
pendingDetachIds: string[];
|
||||
pendingUploadFiles: File[];
|
||||
}) => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "@/features/transactions/actions";
|
||||
import {
|
||||
confirmAttachmentUploadAction,
|
||||
detachTransactionAttachmentAction,
|
||||
getPresignedUploadUrlAction,
|
||||
} from "@/features/transactions/actions/attachments";
|
||||
import {
|
||||
@@ -72,10 +73,11 @@ export function TransactionDialog({
|
||||
defaultAmount,
|
||||
lockCardSelection,
|
||||
lockPaymentMethod,
|
||||
isImporting = false,
|
||||
isImporting,
|
||||
defaultTransactionType,
|
||||
forceShowTransactionType = false,
|
||||
forceShowTransactionType,
|
||||
onSuccess,
|
||||
maxSizeMb,
|
||||
onBulkEditRequest,
|
||||
}: TransactionDialogProps) {
|
||||
const [dialogOpen, setDialogOpen] = useControlledState(
|
||||
@@ -98,6 +100,8 @@ export function TransactionDialog({
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [pendingFile, setPendingFile] = useState<File | null>(null);
|
||||
const [pendingDetachIds, setPendingDetachIds] = useState<string[]>([]);
|
||||
const [pendingUploadFiles, setPendingUploadFiles] = useState<File[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (dialogOpen) {
|
||||
@@ -136,6 +140,8 @@ export function TransactionDialog({
|
||||
setFormState(initial);
|
||||
setErrorMessage(null);
|
||||
setPendingFile(null);
|
||||
setPendingDetachIds([]);
|
||||
setPendingUploadFiles([]);
|
||||
}
|
||||
}, [
|
||||
dialogOpen,
|
||||
@@ -342,7 +348,7 @@ export function TransactionDialog({
|
||||
});
|
||||
await confirmAttachmentUploadAction({
|
||||
uploadToken: presign.uploadToken,
|
||||
applyToSeries: isNewSeries,
|
||||
scope: isNewSeries ? "all" : "current",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -357,11 +363,11 @@ export function TransactionDialog({
|
||||
return;
|
||||
}
|
||||
|
||||
// Update mode
|
||||
const hasSeriesId = Boolean(transaction?.seriesId);
|
||||
|
||||
if (hasSeriesId && onBulkEditRequest) {
|
||||
// Para lançamentos em série, abre o diálogo de bulk action
|
||||
// Para lançamentos em série, passa os arquivos para a página confirmar
|
||||
// o upload após o escopo ser escolhido (sem upload antecipado ao S3)
|
||||
onBulkEditRequest({
|
||||
id: transaction?.id ?? "",
|
||||
name: formState.name.trim(),
|
||||
@@ -383,11 +389,13 @@ export function TransactionDialog({
|
||||
formState.paymentMethod === "Cartão de crédito"
|
||||
? null
|
||||
: Boolean(formState.isSettled),
|
||||
pendingDetachIds,
|
||||
pendingUploadFiles,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Atualização normal para lançamentos únicos ou todos os campos
|
||||
// Atualização normal para lançamentos únicos
|
||||
const updatePayload: UpdateTransactionInput = {
|
||||
id: transaction?.id ?? "",
|
||||
...payload,
|
||||
@@ -396,6 +404,31 @@ export function TransactionDialog({
|
||||
const result = await updateTransactionAction(updatePayload);
|
||||
|
||||
if (result.success) {
|
||||
for (const attachmentId of pendingDetachIds) {
|
||||
await detachTransactionAttachmentAction({
|
||||
attachmentId,
|
||||
transactionId: transaction?.id ?? "",
|
||||
});
|
||||
}
|
||||
for (const file of pendingUploadFiles) {
|
||||
const presign = await getPresignedUploadUrlAction({
|
||||
fileName: file.name,
|
||||
mimeType: file.type,
|
||||
fileSize: file.size,
|
||||
transactionId: transaction?.id ?? "",
|
||||
});
|
||||
if (presign.success) {
|
||||
await fetch(presign.presignedUrl, {
|
||||
method: "PUT",
|
||||
body: file,
|
||||
headers: { "Content-Type": file.type },
|
||||
});
|
||||
await confirmAttachmentUploadAction({
|
||||
uploadToken: presign.uploadToken,
|
||||
scope: "current",
|
||||
});
|
||||
}
|
||||
}
|
||||
toast.success(result.message);
|
||||
onSuccess?.();
|
||||
setDialogOpen(false);
|
||||
@@ -521,7 +554,40 @@ export function TransactionDialog({
|
||||
</Label>
|
||||
<AttachmentSection
|
||||
transactionId={transaction?.id ?? ""}
|
||||
seriesId={transaction?.seriesId ?? null}
|
||||
maxSizeMb={maxSizeMb}
|
||||
pendingDetachIds={
|
||||
transaction?.seriesId ? pendingDetachIds : undefined
|
||||
}
|
||||
onPendingDetach={
|
||||
transaction?.seriesId
|
||||
? (id) => setPendingDetachIds((prev) => [...prev, id])
|
||||
: undefined
|
||||
}
|
||||
onUndoPendingDetach={
|
||||
transaction?.seriesId
|
||||
? (id) =>
|
||||
setPendingDetachIds((prev) =>
|
||||
prev.filter((x) => x !== id),
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
pendingUploadFiles={
|
||||
transaction?.seriesId ? pendingUploadFiles : undefined
|
||||
}
|
||||
onPendingUpload={
|
||||
transaction?.seriesId
|
||||
? (file) =>
|
||||
setPendingUploadFiles((prev) => [...prev, file])
|
||||
: undefined
|
||||
}
|
||||
onCancelPendingUpload={
|
||||
transaction?.seriesId
|
||||
? (file) =>
|
||||
setPendingUploadFiles((prev) =>
|
||||
prev.filter((f) => f !== file),
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
@@ -548,6 +614,7 @@ export function TransactionDialog({
|
||||
<AttachmentFilePicker
|
||||
file={pendingFile}
|
||||
onChange={setPendingFile}
|
||||
maxSizeMb={maxSizeMb}
|
||||
/>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
Reference in New Issue
Block a user