feat(lancamentos): aprimora parcelamentos e protecoes

This commit is contained in:
Felipe Coutinho
2026-05-21 13:47:14 +00:00
parent b6659ef66e
commit 4e8f9cc5fa
16 changed files with 275 additions and 66 deletions

View File

@@ -1,7 +1,13 @@
"use client";
import { useState } from "react";
import { TRANSACTION_CONDITIONS } from "@/features/transactions/lib/constants";
import { Label } from "@/shared/components/ui/label";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/shared/components/ui/popover";
import {
Select,
SelectContent,
@@ -14,6 +20,61 @@ import { cn } from "@/shared/utils/ui";
import { ConditionSelectContent } from "../../select-items";
import type { ConditionSectionProps } from "./transaction-dialog-types";
function InlineStartInstallmentPicker({
value,
options,
onChange,
}: {
value: string;
options: number[];
onChange: (value: string) => void;
}) {
const [open, setOpen] = useState(false);
const selected = Number(value || "1");
const selectedLabel =
!Number.isNaN(selected) && selected > 0
? `${selected}ª parcela`
: "1ª parcela";
const disabled = options.length === 0;
return (
<div className="ml-1">
<span className="text-xs text-muted-foreground">Começar em </span>
<Popover modal open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
type="button"
className="cursor-pointer text-xs text-primary underline-offset-2 hover:underline disabled:cursor-not-allowed disabled:text-muted-foreground disabled:hover:no-underline"
disabled={disabled}
>
{selectedLabel}
</button>
</PopoverTrigger>
<PopoverContent className="w-40 p-1" align="start">
<div className="max-h-56 overflow-y-auto">
{options.map((option) => (
<button
key={option}
type="button"
className={cn(
"flex w-full items-center rounded-sm px-2 py-1.5 text-left text-sm hover:bg-accent hover:text-accent-foreground",
option === selected && "font-medium text-primary",
)}
onClick={() => {
onChange(String(option));
setOpen(false);
}}
>
{option}ª parcela
</button>
))}
</div>
</PopoverContent>
</Popover>
</div>
);
}
export function ConditionSection({
formState,
onFieldChange,
@@ -37,11 +98,17 @@ export function ConditionSection({
const installmentSummary =
showInstallments &&
formState.installmentCount &&
amount &&
!Number.isNaN(installmentCount) &&
installmentCount > 0
? getInstallmentLabel(installmentCount)
: null;
const startInstallmentOptions =
showInstallments &&
formState.installmentCount &&
!Number.isNaN(installmentCount) &&
installmentCount > 0
? Array.from({ length: installmentCount }, (_, index) => index + 1)
: [];
return (
<div className="flex w-full flex-col gap-2 md:flex-row">
@@ -96,6 +163,11 @@ export function ConditionSection({
})}
</SelectContent>
</Select>
<InlineStartInstallmentPicker
value={formState.startInstallment}
options={startInstallmentOptions}
onChange={(value) => onFieldChange("startInstallment", value)}
/>
</div>
) : null}

View File

@@ -17,6 +17,7 @@ export interface TransactionDialogProps {
estabelecimentos: string[];
transaction?: TransactionItem;
defaultPeriod?: string;
defaultAccountId?: string | null;
defaultCardId?: string | null;
defaultPaymentMethod?: string | null;
defaultPurchaseDate?: string | null;

View File

@@ -65,6 +65,7 @@ export function TransactionDialog({
estabelecimentos,
transaction,
defaultPeriod,
defaultAccountId,
defaultCardId,
defaultPaymentMethod,
defaultPurchaseDate,
@@ -88,6 +89,7 @@ export function TransactionDialog({
const [formState, setFormState] = useState<FormState>(() =>
buildTransactionInitialState(transaction, defaultPayerId, defaultPeriod, {
defaultAccountId,
defaultCardId,
defaultPaymentMethod,
defaultPurchaseDate,
@@ -112,6 +114,7 @@ export function TransactionDialog({
defaultPayerId,
defaultPeriod,
{
defaultAccountId,
defaultCardId,
defaultPaymentMethod,
defaultPurchaseDate,
@@ -151,6 +154,7 @@ export function TransactionDialog({
transaction,
defaultPayerId,
defaultPeriod,
defaultAccountId,
defaultCardId,
defaultPaymentMethod,
defaultPurchaseDate,
@@ -327,6 +331,12 @@ export function TransactionDialog({
formState.condition === "Parcelado" && formState.installmentCount
? Number(formState.installmentCount)
: undefined,
startInstallment:
mode === "create" &&
formState.condition === "Parcelado" &&
formState.startInstallment
? Number(formState.startInstallment)
: undefined,
recurrenceCount:
formState.condition === "Recorrente" && formState.recurrenceCount
? Number(formState.recurrenceCount)

View File

@@ -63,6 +63,7 @@ interface TransactionsPageProps {
categoryFilterOptions: TransactionFilterOption[];
accountCardFilterOptions: AccountCardFilterOption[];
selectedPeriod: string;
defaultAccountId?: string | null;
estabelecimentos: string[];
allowCreate?: boolean;
noteAsColumn?: boolean;
@@ -96,6 +97,7 @@ export function TransactionsPage({
categoryFilterOptions,
accountCardFilterOptions,
selectedPeriod,
defaultAccountId,
estabelecimentos,
allowCreate = true,
noteAsColumn = false,
@@ -562,6 +564,7 @@ export function TransactionsPage({
categoryOptions={categoryOptions}
estabelecimentos={estabelecimentos}
defaultPeriod={selectedPeriod}
defaultAccountId={defaultAccountId}
defaultCardId={defaultCardId}
defaultPaymentMethod={defaultPaymentMethod}
lockCardSelection={lockCardSelection}
@@ -585,6 +588,7 @@ export function TransactionsPage({
categoryOptions={categoryOptions}
estabelecimentos={estabelecimentos}
defaultPeriod={selectedPeriod}
defaultAccountId={defaultAccountId}
defaultCardId={defaultCardId}
defaultPaymentMethod={defaultPaymentMethod}
lockCardSelection={lockCardSelection}
@@ -648,6 +652,7 @@ export function TransactionsPage({
estabelecimentos={estabelecimentos}
transaction={transactionToCopy ?? undefined}
defaultPeriod={selectedPeriod}
defaultAccountId={defaultAccountId}
maxSizeMb={attachmentMaxSizeMb}
/>
@@ -669,6 +674,7 @@ export function TransactionsPage({
estabelecimentos={estabelecimentos}
transaction={transactionToImport ?? undefined}
defaultPeriod={selectedPeriod}
defaultAccountId={defaultAccountId}
isImporting={true}
maxSizeMb={attachmentMaxSizeMb}
/>
@@ -697,6 +703,7 @@ export function TransactionsPage({
estabelecimentos={estabelecimentos}
transaction={selectedTransaction ?? undefined}
defaultPeriod={selectedPeriod}
defaultAccountId={defaultAccountId}
onBulkEditRequest={handleBulkEditRequest}
onSplitEditRequest={handleSplitEditRequest}
maxSizeMb={attachmentMaxSizeMb}

View File

@@ -1,6 +1,6 @@
"use client";
import { RiCalendarCheckLine, RiCloseLine, RiEyeLine } from "@remixicon/react";
import { RiCalendarCheckLine } from "@remixicon/react";
import { format } from "date-fns";
import { ptBR } from "date-fns/locale";
import { useTransition } from "react";
@@ -164,16 +164,14 @@ export function AnticipationCard({
onClick={handleViewLancamento}
disabled={isPending}
>
<RiEyeLine className="mr-2 size-4" />
Ver Lançamento
Cancelar
</Button>
{canCancel && (
<ConfirmActionDialog
trigger={
<Button variant="destructive" size="sm" disabled={isPending}>
<RiCloseLine className="mr-2 size-4" />
Cancelar Antecipação
Desfazer Antecipação
</Button>
}
title="Cancelar antecipação?"

View File

@@ -426,7 +426,7 @@ function buildColumns({
const initial = displayName.charAt(0).toUpperCase() || "?";
const content = (
<>
<Avatar className="size-7">
<Avatar className="size-8">
<AvatarImage src={avatarSrc} alt={`Avatar de ${label}`} />
<AvatarFallback className="text-xs font-medium uppercase">
{initial}
@@ -477,15 +477,21 @@ function buildColumns({
const content = (
<span className="inline-flex items-center gap-2">
{logoSrc && (
<Image
src={logoSrc}
alt={`Logo de ${label}`}
width={30}
height={30}
className="rounded-full"
/>
<Avatar className="size-8">
<AvatarImage src={logoSrc} alt={`Logo de ${label}`} />
<AvatarFallback className="text-xs font-medium uppercase">
{label}
</AvatarFallback>
</Avatar>
)}
<span className="truncate">{label}</span>
<span
className={cn(
"truncate underline-offset-2",
isOwnData && href && "group-hover:underline",
)}
>
{label}
</span>
</span>
);
@@ -503,7 +509,7 @@ function buildColumns({
return (
<Tooltip>
<TooltipTrigger asChild>
<Link href={href} className="hover:underline">
<Link href={href} className="group">
{content}
</Link>
</TooltipTrigger>
@@ -654,14 +660,14 @@ function buildColumns({
Editar
</DropdownMenuItem>
)}
{row.original.categoriaName !== "Pagamentos" &&
{!row.original.readonly &&
row.original.userId === currentUserId && (
<DropdownMenuItem onSelect={() => handleCopy(row.original)}>
<RiFileCopyLine className="size-4" />
Copiar
</DropdownMenuItem>
)}
{row.original.categoriaName !== "Pagamentos" &&
{!row.original.readonly &&
row.original.userId !== currentUserId && (
<DropdownMenuItem onSelect={() => handleImport(row.original)}>
<RiFileCopyLine className="size-4" />

View File

@@ -174,7 +174,7 @@ export function TransactionsTable({
: getPaginationRowModel(),
manualPagination: isServerPaginated,
pageCount: serverPagination?.totalPages,
enableRowSelection: true,
enableRowSelection: (row) => !row.original.readonly,
});
const rowModel = table.getRowModel();