mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-07-09 11:26:00 +00:00
feat(lançamentos): ações de converter transações existentes em parceladas e recorrentes
This commit is contained in:
committed by
Felipe Coutinho
parent
558197e870
commit
4b5cdf81b8
@@ -4,6 +4,8 @@ import { RiAddFill } from "@remixicon/react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
convertTransactionToInstallmentAction,
|
||||
convertTransactionToRecurringAction,
|
||||
createMassTransactionsAction,
|
||||
deleteMultipleTransactionsAction,
|
||||
deleteTransactionAction,
|
||||
@@ -18,8 +20,19 @@ import {
|
||||
detachAttachmentBulkAction,
|
||||
getPresignedUploadUrlAction,
|
||||
} from "@/features/transactions/actions/attachments";
|
||||
import { detectInstallmentFromName } from "@/features/transactions/lib/installment-detection";
|
||||
import { ConfirmActionDialog } from "@/shared/components/confirm-action-dialog";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/components/ui/dialog";
|
||||
import { Input } from "@/shared/components/ui/input";
|
||||
import { Label } from "@/shared/components/ui/label";
|
||||
import type {
|
||||
TransactionsExportContext,
|
||||
TransactionsPaginationState,
|
||||
@@ -190,6 +203,14 @@ export function TransactionsPage({
|
||||
const [refundOpen, setRefundOpen] = useState(false);
|
||||
const [transactionToRefund, setTransactionToRefund] =
|
||||
useState<TransactionItem | null>(null);
|
||||
const [convertInstallmentOpen, setConvertInstallmentOpen] = useState(false);
|
||||
const [transactionToConvert, setTransactionToConvert] =
|
||||
useState<TransactionItem | null>(null);
|
||||
const [convertRecurringOpen, setConvertRecurringOpen] = useState(false);
|
||||
const [transactionToConvertRecurring, setTransactionToConvertRecurring] =
|
||||
useState<TransactionItem | null>(null);
|
||||
const [recurrenceCount, setRecurrenceCount] = useState("12");
|
||||
const [recurrencePending, setRecurrencePending] = useState(false);
|
||||
|
||||
const handleToggleSettlement = async (item: TransactionItem) => {
|
||||
if (item.paymentMethod === "Cartão de crédito") {
|
||||
@@ -542,6 +563,71 @@ export function TransactionsPage({
|
||||
setRefundOpen(true);
|
||||
};
|
||||
|
||||
const handleConvertToInstallment = (item: TransactionItem) => {
|
||||
setTransactionToConvert(item);
|
||||
setConvertInstallmentOpen(true);
|
||||
};
|
||||
|
||||
const confirmConvertToInstallment = async () => {
|
||||
if (!transactionToConvert) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await convertTransactionToInstallmentAction({
|
||||
id: transactionToConvert.id,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
toast.error(result.error);
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
toast.success(result.message);
|
||||
setConvertInstallmentOpen(false);
|
||||
setTransactionToConvert(null);
|
||||
};
|
||||
|
||||
const conversionPreview = transactionToConvert
|
||||
? detectInstallmentFromName(transactionToConvert.name)
|
||||
: null;
|
||||
|
||||
const handleConvertToRecurring = (item: TransactionItem) => {
|
||||
setTransactionToConvertRecurring(item);
|
||||
setRecurrenceCount("12");
|
||||
setConvertRecurringOpen(true);
|
||||
};
|
||||
|
||||
const confirmConvertToRecurring = async () => {
|
||||
if (!transactionToConvertRecurring) {
|
||||
return;
|
||||
}
|
||||
|
||||
const count = Number(recurrenceCount);
|
||||
if (!Number.isInteger(count) || count < 2 || count > 60) {
|
||||
toast.error("Informe uma recorrência entre 2 e 60 meses.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setRecurrencePending(true);
|
||||
const result = await convertTransactionToRecurringAction({
|
||||
id: transactionToConvertRecurring.id,
|
||||
recurrenceCount: count,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
toast.error(result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success(result.message);
|
||||
setConvertRecurringOpen(false);
|
||||
setTransactionToConvertRecurring(null);
|
||||
} finally {
|
||||
setRecurrencePending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAnticipate = (item: TransactionItem) => {
|
||||
setSelectedForAnticipation(item);
|
||||
setAnticipateOpen(true);
|
||||
@@ -628,6 +714,8 @@ export function TransactionsPage({
|
||||
onBulkImport={handleBulkImport}
|
||||
onViewDetails={handleViewDetails}
|
||||
onRefund={handleRefund}
|
||||
onConvertToInstallment={handleConvertToInstallment}
|
||||
onConvertToRecurring={handleConvertToRecurring}
|
||||
onToggleSettlement={handleToggleSettlement}
|
||||
onAnticipate={handleAnticipate}
|
||||
onViewAnticipationHistory={handleViewAnticipationHistory}
|
||||
@@ -749,6 +837,79 @@ export function TransactionsPage({
|
||||
disabled={!transactionToDelete}
|
||||
/>
|
||||
|
||||
<ConfirmActionDialog
|
||||
open={convertInstallmentOpen && !!transactionToConvert}
|
||||
onOpenChange={(open) => {
|
||||
setConvertInstallmentOpen(open);
|
||||
if (!open) {
|
||||
setTransactionToConvert(null);
|
||||
}
|
||||
}}
|
||||
title="Converter em parcelamento?"
|
||||
description={
|
||||
conversionPreview
|
||||
? `Este lançamento será convertido para "${conversionPreview.name}" como parcela ${conversionPreview.currentInstallment} de ${conversionPreview.installmentCount}. As próximas parcelas serão criadas automaticamente.`
|
||||
: "Este lançamento será convertido em uma série parcelada."
|
||||
}
|
||||
confirmLabel="Converter"
|
||||
pendingLabel="Convertendo..."
|
||||
onConfirm={confirmConvertToInstallment}
|
||||
disabled={!transactionToConvert}
|
||||
/>
|
||||
|
||||
<Dialog
|
||||
open={convertRecurringOpen && !!transactionToConvertRecurring}
|
||||
onOpenChange={(open) => {
|
||||
setConvertRecurringOpen(open);
|
||||
if (!open) {
|
||||
setTransactionToConvertRecurring(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Converter em recorrente?</DialogTitle>
|
||||
<DialogDescription>
|
||||
O lançamento atual será mantido como a primeira recorrência e os
|
||||
próximos meses serão criados automaticamente.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="recurrenceCount">Repetir por</Label>
|
||||
<Input
|
||||
id="recurrenceCount"
|
||||
type="number"
|
||||
min={2}
|
||||
max={60}
|
||||
value={recurrenceCount}
|
||||
onChange={(event) => setRecurrenceCount(event.target.value)}
|
||||
/>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Use o total de meses da série, incluindo este lançamento.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setConvertRecurringOpen(false)}
|
||||
disabled={recurrencePending}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={confirmConvertToRecurring}
|
||||
disabled={recurrencePending}
|
||||
>
|
||||
{recurrencePending ? "Convertendo..." : "Converter"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<BulkActionDialog
|
||||
open={bulkDeleteOpen && !!pendingDeleteData}
|
||||
onOpenChange={setBulkDeleteOpen}
|
||||
|
||||
@@ -20,6 +20,9 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/shared/components/ui/dropdown-menu";
|
||||
import { REFUND_NOTE_PREFIX } from "@/shared/lib/accounts/constants";
|
||||
import { CREDIT_CARD_PAYMENT_METHOD } from "@/features/transactions/lib/constants";
|
||||
import { detectInstallmentFromName } from "@/features/transactions/lib/installment-detection";
|
||||
import { getConditionIcon } from "@/shared/utils/icons";
|
||||
import type { TransactionItem } from "../types";
|
||||
|
||||
type TransactionActionsMenuProps = {
|
||||
@@ -33,6 +36,8 @@ type TransactionActionsMenuProps = {
|
||||
onRefund?: (item: TransactionItem) => void;
|
||||
onAnticipate?: (item: TransactionItem) => void;
|
||||
onViewAnticipationHistory?: (item: TransactionItem) => void;
|
||||
onConvertToInstallment?: (item: TransactionItem) => void;
|
||||
onConvertToRecurring?: (item: TransactionItem) => void;
|
||||
};
|
||||
|
||||
export function TransactionActionsMenu({
|
||||
@@ -46,6 +51,8 @@ export function TransactionActionsMenu({
|
||||
onRefund,
|
||||
onAnticipate,
|
||||
onViewAnticipationHistory,
|
||||
onConvertToInstallment,
|
||||
onConvertToRecurring,
|
||||
}: TransactionActionsMenuProps) {
|
||||
const isOwnData = item.userId === currentUserId;
|
||||
const canRefund =
|
||||
@@ -55,9 +62,29 @@ export function TransactionActionsMenu({
|
||||
!item.splitGroupId &&
|
||||
!item.readonly &&
|
||||
!item.note?.startsWith(REFUND_NOTE_PREFIX);
|
||||
|
||||
const showInstallmentActions =
|
||||
isOwnData && item.condition === "Parcelado" && item.seriesId;
|
||||
|
||||
const detectedInstallment = detectInstallmentFromName(item.name);
|
||||
const canConvertToInstallment =
|
||||
isOwnData &&
|
||||
item.paymentMethod === CREDIT_CARD_PAYMENT_METHOD &&
|
||||
item.condition === "À vista" &&
|
||||
!item.splitGroupId &&
|
||||
!item.isDivided &&
|
||||
!item.readonly &&
|
||||
Boolean(detectedInstallment) &&
|
||||
Boolean(onConvertToInstallment);
|
||||
|
||||
const canConvertToRecurring =
|
||||
isOwnData &&
|
||||
item.condition === "À vista" &&
|
||||
!item.splitGroupId &&
|
||||
!item.isDivided &&
|
||||
!item.readonly &&
|
||||
Boolean(onConvertToRecurring);
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -112,6 +139,24 @@ export function TransactionActionsMenu({
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
|
||||
{canConvertToInstallment ? (
|
||||
<DropdownMenuItem
|
||||
onSelect={() => onConvertToInstallment?.(item)}
|
||||
>
|
||||
{getConditionIcon("Parcelado")}
|
||||
Converter em Parcelamento
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
|
||||
{canConvertToRecurring ? (
|
||||
<DropdownMenuItem
|
||||
onSelect={() => onConvertToRecurring?.(item)}
|
||||
>
|
||||
{getConditionIcon("Recorrente")}
|
||||
Converter em Recorrente
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
|
||||
{isOwnData ? (
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
|
||||
@@ -47,6 +47,8 @@ type BuildColumnsArgs = {
|
||||
onToggleSettlement?: (item: TransactionItem) => void;
|
||||
onAnticipate?: (item: TransactionItem) => void;
|
||||
onViewAnticipationHistory?: (item: TransactionItem) => void;
|
||||
onConvertToInstallment?: (item: TransactionItem) => void;
|
||||
onConvertToRecurring?: (item: TransactionItem) => void;
|
||||
isSettlementLoading: (id: string) => boolean;
|
||||
showActions: boolean;
|
||||
columnOrder?: string[] | null;
|
||||
@@ -109,6 +111,8 @@ function buildColumns({
|
||||
onToggleSettlement,
|
||||
onAnticipate,
|
||||
onViewAnticipationHistory,
|
||||
onConvertToInstallment,
|
||||
onConvertToRecurring,
|
||||
isSettlementLoading,
|
||||
showActions,
|
||||
}: BuildColumnsArgs): ColumnDef<TransactionItem>[] {
|
||||
@@ -122,6 +126,8 @@ function buildColumns({
|
||||
const handleToggleSettlement = onToggleSettlement ?? noop;
|
||||
const handleAnticipate = onAnticipate ?? noop;
|
||||
const handleViewAnticipationHistory = onViewAnticipationHistory ?? noop;
|
||||
const handleConvertToInstallment = onConvertToInstallment ?? noop;
|
||||
const handleConvertToRecurring = onConvertToRecurring ?? noop;
|
||||
|
||||
const columns: ColumnDef<TransactionItem>[] = [
|
||||
{
|
||||
@@ -545,6 +551,8 @@ function buildColumns({
|
||||
onRefund={handleRefund}
|
||||
onAnticipate={handleAnticipate}
|
||||
onViewAnticipationHistory={handleViewAnticipationHistory}
|
||||
onConvertToInstallment={onConvertToInstallment ? handleConvertToInstallment : undefined}
|
||||
onConvertToRecurring={onConvertToRecurring ? handleConvertToRecurring : undefined}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
|
||||
@@ -39,6 +39,8 @@ type TransactionsMobileListProps = {
|
||||
onToggleSettlement?: (item: TransactionItem) => void;
|
||||
onAnticipate?: (item: TransactionItem) => void;
|
||||
onViewAnticipationHistory?: (item: TransactionItem) => void;
|
||||
onConvertToInstallment?: (item: TransactionItem) => void;
|
||||
onConvertToRecurring?: (item: TransactionItem) => void;
|
||||
isSettlementLoading: (id: string) => boolean;
|
||||
showActions?: boolean;
|
||||
};
|
||||
@@ -55,6 +57,8 @@ export function TransactionsMobileList({
|
||||
onToggleSettlement,
|
||||
onAnticipate,
|
||||
onViewAnticipationHistory,
|
||||
onConvertToInstallment,
|
||||
onConvertToRecurring,
|
||||
isSettlementLoading,
|
||||
showActions = true,
|
||||
}: TransactionsMobileListProps) {
|
||||
@@ -74,6 +78,8 @@ export function TransactionsMobileList({
|
||||
onToggleSettlement={onToggleSettlement}
|
||||
onAnticipate={onAnticipate}
|
||||
onViewAnticipationHistory={onViewAnticipationHistory}
|
||||
onConvertToInstallment={onConvertToInstallment}
|
||||
onConvertToRecurring={onConvertToRecurring}
|
||||
isSettlementLoading={isSettlementLoading}
|
||||
showActions={showActions}
|
||||
/>
|
||||
@@ -98,6 +104,8 @@ function TransactionMobileCard({
|
||||
onToggleSettlement,
|
||||
onAnticipate,
|
||||
onViewAnticipationHistory,
|
||||
onConvertToInstallment,
|
||||
onConvertToRecurring,
|
||||
isSettlementLoading,
|
||||
showActions = true,
|
||||
}: TransactionMobileCardProps) {
|
||||
@@ -261,6 +269,8 @@ function TransactionMobileCard({
|
||||
onRefund={onRefund}
|
||||
onAnticipate={onAnticipate}
|
||||
onViewAnticipationHistory={onViewAnticipationHistory}
|
||||
onConvertToInstallment={onConvertToInstallment}
|
||||
onConvertToRecurring={onConvertToRecurring}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -71,6 +71,8 @@ type TransactionsTableProps = {
|
||||
onBulkImport?: (items: TransactionItem[]) => void;
|
||||
onViewDetails?: (item: TransactionItem) => void;
|
||||
onRefund?: (item: TransactionItem) => void;
|
||||
onConvertToInstallment?: (item: TransactionItem) => void;
|
||||
onConvertToRecurring?: (item: TransactionItem) => void;
|
||||
onToggleSettlement?: (item: TransactionItem) => void;
|
||||
onAnticipate?: (item: TransactionItem) => void;
|
||||
onViewAnticipationHistory?: (item: TransactionItem) => void;
|
||||
@@ -100,6 +102,8 @@ export function TransactionsTable({
|
||||
onBulkImport,
|
||||
onViewDetails,
|
||||
onRefund,
|
||||
onConvertToInstallment,
|
||||
onConvertToRecurring,
|
||||
onToggleSettlement,
|
||||
onAnticipate,
|
||||
onViewAnticipationHistory,
|
||||
@@ -134,6 +138,8 @@ export function TransactionsTable({
|
||||
onConfirmDelete,
|
||||
onViewDetails,
|
||||
onRefund,
|
||||
onConvertToInstallment,
|
||||
onConvertToRecurring,
|
||||
onToggleSettlement,
|
||||
onAnticipate,
|
||||
onViewAnticipationHistory,
|
||||
@@ -151,6 +157,8 @@ export function TransactionsTable({
|
||||
onConfirmDelete,
|
||||
onViewDetails,
|
||||
onRefund,
|
||||
onConvertToInstallment,
|
||||
onConvertToRecurring,
|
||||
onToggleSettlement,
|
||||
onAnticipate,
|
||||
onViewAnticipationHistory,
|
||||
|
||||
Reference in New Issue
Block a user