mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-06-09 23:06:01 +00:00
feat(transactions): edição cooperativa e visibilidade de divisões
Adiciona splitGroupId para vincular as duas shares de um lançamento dividido (schema + índice + migration 0026). Habilita: - Edição de par dividido com escolha de escopo (apenas este lado ou ambos) via novo SplitPairDialog e updateTransactionSplitPairAction - Filtro "Somente divididos" (isDivided) na tabela de lançamentos - Visibilidade de anexos para pessoas com acesso compartilhado via payerShares; upload e detach em massa expandem para shares irmãs - Cópia independente de anexos no fluxo "Importar para Minha Conta" (novo fileKey, novo userId, S3 CopyObject) com seção read-only "Anexos que serão copiados" no dialog de importação - Ícone de clipe na tabela de lançamentos da página da pessoa via EXISTS em fetchPagadorLancamentos Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/components/ui/dialog";
|
||||
import { Label } from "@/shared/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/shared/components/ui/radio-group";
|
||||
|
||||
export type SplitPairScope = "current" | "both";
|
||||
|
||||
type SplitPairDialogProps = {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onConfirm: (scope: SplitPairScope) => void;
|
||||
};
|
||||
|
||||
export function SplitPairDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
onConfirm,
|
||||
}: SplitPairDialogProps) {
|
||||
const [scope, setScope] = useState<SplitPairScope>("current");
|
||||
|
||||
const handleConfirm = () => {
|
||||
onConfirm(scope);
|
||||
onOpenChange(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Editar lançamento dividido</DialogTitle>
|
||||
<DialogDescription>
|
||||
Este lançamento está dividido com outra pessoa. Escolha o que deseja
|
||||
editar:
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<RadioGroup
|
||||
value={scope}
|
||||
onValueChange={(v) => setScope(v as SplitPairScope)}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start space-x-3">
|
||||
<RadioGroupItem
|
||||
value="current"
|
||||
id="split-current"
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<Label
|
||||
htmlFor="split-current"
|
||||
className="text-sm cursor-pointer font-medium"
|
||||
>
|
||||
Apenas este lançamento
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Aplica a alteração somente neste lado da divisão
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start space-x-3">
|
||||
<RadioGroupItem value="both" id="split-both" className="mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<Label
|
||||
htmlFor="split-both"
|
||||
className="text-sm cursor-pointer font-medium"
|
||||
>
|
||||
Atualizar os dois lançamentos
|
||||
</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Aplica nome, data, categoria e outros campos compartilhados
|
||||
nos dois lados da divisão
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="button" onClick={handleConfirm}>
|
||||
Confirmar
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -49,6 +49,26 @@ export interface TransactionDialogProps {
|
||||
pendingDetachIds: string[];
|
||||
pendingUploadFiles: File[];
|
||||
}) => void;
|
||||
onSplitEditRequest?: (data: {
|
||||
id: string;
|
||||
purchaseDate: string;
|
||||
period: string;
|
||||
name: string;
|
||||
transactionType: string;
|
||||
amount: number;
|
||||
condition: string;
|
||||
paymentMethod: string;
|
||||
categoryId: string | undefined;
|
||||
note: string;
|
||||
payerId: string | undefined;
|
||||
accountId: string | undefined;
|
||||
cardId: string | undefined;
|
||||
isSettled: boolean | null;
|
||||
dueDate: string | null;
|
||||
boletoPaymentDate: string | null;
|
||||
pendingDetachIds: string[];
|
||||
pendingUploadFiles: File[];
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export interface BaseFieldSectionProps {
|
||||
|
||||
@@ -78,6 +78,7 @@ export function TransactionDialog({
|
||||
onSuccess,
|
||||
maxSizeMb,
|
||||
onBulkEditRequest,
|
||||
onSplitEditRequest,
|
||||
}: TransactionDialogProps) {
|
||||
const [dialogOpen, setDialogOpen] = useControlledState(
|
||||
open,
|
||||
@@ -321,6 +322,10 @@ export function TransactionDialog({
|
||||
formState.boletoPaymentDate
|
||||
? formState.boletoPaymentDate
|
||||
: undefined,
|
||||
importFromTransactionId:
|
||||
mode === "create" && isImporting && transaction?.id
|
||||
? transaction.id
|
||||
: undefined,
|
||||
};
|
||||
|
||||
startTransition(async () => {
|
||||
@@ -365,6 +370,11 @@ export function TransactionDialog({
|
||||
}
|
||||
|
||||
const hasSeriesId = Boolean(transaction?.seriesId);
|
||||
const hasSplitPair = Boolean(
|
||||
transaction?.isDivided &&
|
||||
transaction?.splitGroupId &&
|
||||
!transaction?.seriesId,
|
||||
);
|
||||
|
||||
if (hasSeriesId && onBulkEditRequest) {
|
||||
// Para lançamentos em série, passa os arquivos para a página confirmar
|
||||
@@ -398,6 +408,39 @@ export function TransactionDialog({
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasSplitPair && onSplitEditRequest) {
|
||||
onSplitEditRequest({
|
||||
id: transaction?.id ?? "",
|
||||
purchaseDate: formState.purchaseDate,
|
||||
period: formState.period,
|
||||
name: formState.name.trim(),
|
||||
transactionType: formState.transactionType,
|
||||
amount: sanitizedAmount,
|
||||
condition: formState.condition,
|
||||
paymentMethod: formState.paymentMethod,
|
||||
categoryId: formState.categoryId,
|
||||
note: formState.note.trim() || "",
|
||||
payerId: formState.payerId,
|
||||
accountId: formState.accountId,
|
||||
cardId: formState.cardId,
|
||||
isSettled:
|
||||
formState.paymentMethod === "Cartão de crédito"
|
||||
? null
|
||||
: Boolean(formState.isSettled),
|
||||
dueDate:
|
||||
formState.paymentMethod === "Boleto"
|
||||
? formState.dueDate || null
|
||||
: null,
|
||||
boletoPaymentDate:
|
||||
mode === "update" && formState.paymentMethod === "Boleto"
|
||||
? formState.boletoPaymentDate || null
|
||||
: null,
|
||||
pendingDetachIds,
|
||||
pendingUploadFiles,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Atualização normal para lançamentos únicos
|
||||
const updatePayload: UpdateTransactionInput = {
|
||||
id: transaction?.id ?? "",
|
||||
@@ -609,6 +652,17 @@ export function TransactionDialog({
|
||||
formState={formState}
|
||||
onFieldChange={handleFieldChange}
|
||||
/>
|
||||
{isImportMode && transaction?.id && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-medium leading-none">
|
||||
Anexos que serão copiados
|
||||
</Label>
|
||||
<AttachmentSection
|
||||
transactionId={transaction.id}
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<AttachmentFilePicker
|
||||
files={pendingFiles}
|
||||
onAdd={(file) => setPendingFiles((prev) => [...prev, file])}
|
||||
|
||||
Reference in New Issue
Block a user