refactor: simplificar lancamento-dialog e mass-add-dialog

Substitui FaturaWarningDialog por deriveCreditCardPeriod() que calcula
o período da fatura automaticamente a partir da data de compra +
dia de fechamento/vencimento do cartão.

lancamento-dialog: remove periodDirty state, adiciona seção colapsável
"Condições e anotações", propaga closingDay/dueDay via cardInfo.

mass-add-dialog: unifica contaId/cartaoId em contaCartaoId com
parsing por prefixo, period picker apenas para cartão de crédito.

basic-fields-section: remove PeriodPicker (período agora auto-derivado),
move Estabelecimento para topo.

payment-method-section: adiciona InlinePeriodPicker como link
"Fatura de [mês]" com popover MonthPicker.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Felipe Coutinho
2026-02-26 17:23:23 +00:00
parent 96118d85e4
commit ac2ea63dbd
10 changed files with 693 additions and 677 deletions

View File

@@ -7,7 +7,6 @@ import {
cartoes,
categorias,
contas,
faturas,
lancamentos,
pagadores,
} from "@/db/schema";
@@ -33,7 +32,6 @@ import {
import { noteSchema, uuidSchema } from "@/lib/schemas/common";
import { formatDecimalForDbRequired } from "@/lib/utils/currency";
import { getTodayDate, parseLocalDateString } from "@/lib/utils/date";
import { getNextPeriod } from "@/lib/utils/period";
// ============================================================================
// Authorization Validation Functions
@@ -1641,59 +1639,6 @@ export async function deleteMultipleLancamentosAction(
}
}
// Check fatura payment status and card closing day for the given period
export async function checkFaturaStatusAction(
cartaoId: string,
period: string,
): Promise<{
shouldSuggestNext: boolean;
isPaid: boolean;
isAfterClosing: boolean;
closingDay: string | null;
cardName: string;
nextPeriod: string;
} | null> {
try {
const user = await getUser();
const cartao = await db.query.cartoes.findFirst({
where: and(eq(cartoes.id, cartaoId), eq(cartoes.userId, user.id)),
columns: { id: true, name: true, closingDay: true },
});
if (!cartao) return null;
const fatura = await db.query.faturas.findFirst({
where: and(
eq(faturas.cartaoId, cartaoId),
eq(faturas.userId, user.id),
eq(faturas.period, period),
),
columns: { paymentStatus: true },
});
const isPaid = fatura?.paymentStatus === "pago";
const today = new Date();
const currentPeriod = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}`;
const closingDayNum = Number.parseInt(cartao.closingDay ?? "", 10);
const isAfterClosing =
period === currentPeriod &&
!Number.isNaN(closingDayNum) &&
today.getDate() > closingDayNum;
return {
shouldSuggestNext: isPaid || isAfterClosing,
isPaid,
isAfterClosing,
closingDay: cartao.closingDay,
cardName: cartao.name,
nextPeriod: getNextPeriod(period),
};
} catch {
return null;
}
}
// Get unique establishment names from the last 3 months
export async function getRecentEstablishmentsAction(): Promise<string[]> {
try {

View File

@@ -2,7 +2,6 @@
import { RiCalculatorLine } from "@remixicon/react";
import { CalculatorDialogButton } from "@/components/calculadora/calculator-dialog";
import { PeriodPicker } from "@/components/period-picker";
import { CurrencyInput } from "@/components/ui/currency-input";
import { DatePicker } from "@/components/ui/date-picker";
import { Label } from "@/components/ui/label";
@@ -15,43 +14,32 @@ export function BasicFieldsSection({
estabelecimentos,
}: Omit<BasicFieldsSectionProps, "monthOptions">) {
return (
<>
<div className="flex w-full flex-col gap-2 md:flex-row">
<div className="w-1/2 space-y-1">
<Label htmlFor="purchaseDate">Data da transação</Label>
<DatePicker
id="purchaseDate"
value={formState.purchaseDate}
onChange={(value) => onFieldChange("purchaseDate", value)}
placeholder="Data da transação"
required
/>
</div>
<div className="w-1/2 space-y-1">
<Label htmlFor="period">Período</Label>
<PeriodPicker
value={formState.period}
onChange={(value) => onFieldChange("period", value)}
className="w-full"
/>
</div>
</div>
<div className="flex w-full flex-col gap-2 md:flex-row">
<div className="w-1/2 space-y-1">
<div className="space-y-3">
<div className="space-y-1">
<Label htmlFor="name">Estabelecimento</Label>
<EstabelecimentoInput
id="name"
value={formState.name}
onChange={(value) => onFieldChange("name", value)}
estabelecimentos={estabelecimentos}
placeholder="Ex.: Padaria"
placeholder="Ex.: Restaurante do Zé"
maxLength={20}
required
/>
</div>
<div className="flex w-full flex-col gap-2 md:flex-row">
<div className="w-1/2 space-y-1">
<Label htmlFor="purchaseDate">Data</Label>
<DatePicker
id="purchaseDate"
value={formState.purchaseDate}
onChange={(value) => onFieldChange("purchaseDate", value)}
placeholder="Data"
required
/>
</div>
<div className="w-1/2 space-y-1">
<Label htmlFor="amount">Valor</Label>
<div className="relative">
@@ -74,6 +62,6 @@ export function BasicFieldsSection({
</div>
</div>
</div>
</>
</div>
);
}

View File

@@ -14,7 +14,7 @@ export function BoletoFieldsSection({
<div className="flex w-full flex-col gap-2 md:flex-row">
<div
className={cn(
"space-y-2 w-full",
"space-y-1 w-full",
showPaymentDate ? "md:w-1/2" : "md:w-full",
)}
>

View File

@@ -1,6 +1,6 @@
"use client";
import { useMemo } from "react";
import { useCallback, useMemo } from "react";
import { Label } from "@/components/ui/label";
import {
Select,
@@ -32,13 +32,16 @@ export function ConditionSection({
return Number.isNaN(value) || value <= 0 ? null : value;
}, [formState.amount]);
const getInstallmentLabel = (count: number) => {
const getInstallmentLabel = useCallback(
(count: number) => {
if (amount) {
const installmentValue = amount / count;
return `${count}x de R$ ${formatCurrency(installmentValue)}`;
}
return `${count}x`;
};
},
[amount],
);
const _getRecurrenceLabel = (count: number) => {
return `${count} meses`;

View File

@@ -1,19 +1,23 @@
"use client";
import { RiAddLine } from "@remixicon/react";
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
useTransition,
} from "react";
import { toast } from "sonner";
import {
checkFaturaStatusAction,
createLancamentoAction,
updateLancamentoAction,
} from "@/app/(dashboard)/lancamentos/actions";
import { Button } from "@/components/ui/button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
Dialog,
DialogContent,
@@ -32,10 +36,6 @@ import {
applyFieldDependencies,
buildLancamentoInitialState,
} from "@/lib/lancamentos/form-helpers";
import {
type FaturaWarning,
FaturaWarningDialog,
} from "../fatura-warning-dialog";
import { BasicFieldsSection } from "./basic-fields-section";
import { BoletoFieldsSection } from "./boleto-fields-section";
import { CategorySection } from "./category-section";
@@ -93,13 +93,8 @@ export function LancamentoDialog({
isImporting,
}),
);
const [periodDirty, setPeriodDirty] = useState(false);
const [isPending, startTransition] = useTransition();
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [faturaWarning, setFaturaWarning] = useState<FaturaWarning | null>(
null,
);
const lastCheckedRef = useRef<string | null>(null);
useEffect(() => {
if (dialogOpen) {
@@ -120,11 +115,6 @@ export function LancamentoDialog({
),
);
setErrorMessage(null);
setPeriodDirty(false);
setFaturaWarning(null);
lastCheckedRef.current = null;
} else {
setFaturaWarning(null);
}
}, [
dialogOpen,
@@ -140,40 +130,6 @@ export function LancamentoDialog({
isImporting,
]);
useEffect(() => {
if (mode !== "create") return;
if (!dialogOpen) return;
if (formState.paymentMethod !== "Cartão de crédito") return;
if (!formState.cartaoId) return;
const checkKey = `${formState.cartaoId}:${formState.period}`;
if (checkKey === lastCheckedRef.current) return;
lastCheckedRef.current = checkKey;
checkFaturaStatusAction(formState.cartaoId, formState.period).then(
(result) => {
if (result?.shouldSuggestNext) {
setFaturaWarning({
nextPeriod: result.nextPeriod,
cardName: result.cardName,
isPaid: result.isPaid,
isAfterClosing: result.isAfterClosing,
closingDay: result.closingDay,
currentPeriod: formState.period,
});
} else {
setFaturaWarning(null);
}
},
);
}, [
mode,
dialogOpen,
formState.paymentMethod,
formState.cartaoId,
formState.period,
]);
const primaryPagador = formState.pagadorId;
const secondaryPagadorOptions = useMemo(
@@ -194,19 +150,27 @@ export function LancamentoDialog({
return Number.isNaN(parsed) ? 0 : Math.abs(parsed);
}, [formState.amount]);
const getCardInfo = useCallback(
(cartaoId: string | undefined) => {
if (!cartaoId) return null;
const card = cartaoOptions.find((opt) => opt.value === cartaoId);
if (!card) return null;
return {
closingDay: card.closingDay ?? null,
dueDay: card.dueDay ?? null,
};
},
[cartaoOptions],
);
const handleFieldChange = useCallback(
<Key extends keyof FormState>(key: Key, value: FormState[Key]) => {
if (key === "period") {
setPeriodDirty(true);
}
setFormState((prev) => {
const dependencies = applyFieldDependencies(
key,
value,
prev,
periodDirty,
);
const effectiveCartaoId =
key === "cartaoId" ? (value as string) : prev.cartaoId;
const cardInfo = getCardInfo(effectiveCartaoId);
const dependencies = applyFieldDependencies(key, value, prev, cardInfo);
return {
...prev,
@@ -215,7 +179,7 @@ export function LancamentoDialog({
};
});
},
[periodDirty],
[getCardInfo],
);
const handleSubmit = useCallback(
@@ -440,17 +404,16 @@ export function LancamentoDialog({
const disableCartaoSelect = Boolean(lockCartaoSelection && mode === "create");
return (
<>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
{trigger ? <DialogTrigger asChild>{trigger}</DialogTrigger> : null}
<DialogContent className="sm:max-w-xl p-6 sm:px-8">
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<form
className="space-y-2 -mx-6 max-h-[80vh] overflow-y-auto px-6 pb-1"
className="space-y-3 -mx-6 max-h-[80vh] overflow-y-auto px-6 pb-1"
onSubmit={handleSubmit}
noValidate
>
@@ -505,6 +468,16 @@ export function LancamentoDialog({
/>
) : null}
<Collapsible
defaultOpen={
formState.condition !== "À vista" || formState.note.length > 0
}
>
<CollapsibleTrigger className="flex w-full items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer [&[data-state=open]>svg]:rotate-180 mt-4">
<RiAddLine className="text-primary size-4 transition-transform duration-200" />
Condições e anotações
</CollapsibleTrigger>
<CollapsibleContent className="space-y-3 pt-3">
{!isUpdateMode ? (
<ConditionSection
formState={formState}
@@ -518,12 +491,14 @@ export function LancamentoDialog({
formState={formState}
onFieldChange={handleFieldChange}
/>
</CollapsibleContent>
</Collapsible>
{errorMessage ? (
<p className="text-sm text-destructive">{errorMessage}</p>
) : null}
<DialogFooter className="gap-3">
<DialogFooter>
<Button
type="button"
variant="outline"
@@ -539,15 +514,5 @@ export function LancamentoDialog({
</form>
</DialogContent>
</Dialog>
<FaturaWarningDialog
warning={faturaWarning}
onConfirm={(nextPeriod) => {
handleFieldChange("period", nextPeriod);
setFaturaWarning(null);
}}
onCancel={() => setFaturaWarning(null)}
/>
</>
);
}

View File

@@ -1,6 +1,13 @@
"use client";
import { useState } from "react";
import { Label } from "@/components/ui/label";
import { MonthPicker } from "@/components/ui/monthpicker";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Select,
SelectContent,
@@ -9,6 +16,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { LANCAMENTO_PAYMENT_METHODS } from "@/lib/lancamentos/constants";
import { displayPeriod } from "@/lib/utils/period";
import { cn } from "@/lib/utils/ui";
import {
ContaCartaoSelectContent,
@@ -16,6 +24,52 @@ import {
} from "../../select-items";
import type { PaymentMethodSectionProps } from "./lancamento-dialog-types";
function periodToDate(period: string): Date {
const [year, month] = period.split("-").map(Number);
return new Date(year, month - 1, 1);
}
function dateToPeriod(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
return `${year}-${month}`;
}
function InlinePeriodPicker({
period,
onPeriodChange,
}: {
period: string;
onPeriodChange: (value: string) => void;
}) {
const [open, setOpen] = useState(false);
return (
<div className="ml-1">
<span className="text-xs text-muted-foreground">Fatura de </span>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
type="button"
className="text-xs text-primary underline-offset-2 hover:underline cursor-pointer lowercase"
>
{displayPeriod(period)}
</button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<MonthPicker
selectedMonth={periodToDate(period)}
onMonthSelect={(date) => {
onPeriodChange(dateToPeriod(date));
setOpen(false);
}}
/>
</PopoverContent>
</Popover>
</div>
);
}
export function PaymentMethodSection({
formState,
onFieldChange,
@@ -46,7 +100,7 @@ export function PaymentMethodSection({
return (
<>
{!isUpdateMode ? (
<div className="flex w-full flex-col gap-2 md:flex-row mt-3">
<div className="flex w-full flex-col gap-2 md:flex-row">
<div
className={cn(
"space-y-1 w-full",
@@ -131,6 +185,12 @@ export function PaymentMethodSection({
)}
</SelectContent>
</Select>
{formState.cartaoId ? (
<InlinePeriodPicker
period={formState.period}
onPeriodChange={(value) => onFieldChange("period", value)}
/>
) : null}
</div>
) : null}
@@ -239,6 +299,12 @@ export function PaymentMethodSection({
)}
</SelectContent>
</Select>
{formState.cartaoId ? (
<InlinePeriodPicker
period={formState.period}
onPeriodChange={(value) => onFieldChange("period", value)}
/>
) : null}
</div>
) : null}

View File

@@ -10,7 +10,7 @@ export function SplitAndSettlementSection({
showSettledToggle,
}: SplitAndSettlementSectionProps) {
return (
<div className="flex w-full flex-col gap-2 py-2 md:flex-row">
<div className="flex w-full flex-col gap-2 md:flex-row">
<div
className={cn(
"space-y-1",

View File

@@ -1,9 +1,8 @@
"use client";
import { RiAddLine, RiDeleteBinLine } from "@remixicon/react";
import { useEffect, useMemo, useRef, useState } from "react";
import { useMemo, useState } from "react";
import { toast } from "sonner";
import { checkFaturaStatusAction } from "@/app/(dashboard)/lancamentos/actions";
import { PeriodPicker } from "@/components/period-picker";
import { Button } from "@/components/ui/button";
import { CurrencyInput } from "@/components/ui/currency-input";
@@ -40,10 +39,6 @@ import {
TransactionTypeSelectContent,
} from "../select-items";
import { EstabelecimentoInput } from "../shared/estabelecimento-input";
import {
type FaturaWarning,
FaturaWarningDialog,
} from "./fatura-warning-dialog";
interface MassAddDialogProps {
open: boolean;
@@ -124,39 +119,6 @@ export function MassAddDialog({
? contaCartaoId.replace("cartao:", "")
: undefined;
const [faturaWarning, setFaturaWarning] = useState<FaturaWarning | null>(
null,
);
const lastCheckedRef = useRef<string | null>(null);
useEffect(() => {
if (!open) {
setFaturaWarning(null);
lastCheckedRef.current = null;
return;
}
if (!isCartaoSelected || !cartaoId) return;
const checkKey = `${cartaoId}:${period}`;
if (checkKey === lastCheckedRef.current) return;
lastCheckedRef.current = checkKey;
checkFaturaStatusAction(cartaoId, period).then((result) => {
if (result?.shouldSuggestNext) {
setFaturaWarning({
nextPeriod: result.nextPeriod,
cardName: result.cardName,
isPaid: result.isPaid,
isAfterClosing: result.isAfterClosing,
closingDay: result.closingDay,
currentPeriod: period,
});
} else {
setFaturaWarning(null);
}
});
}, [open, isCartaoSelected, cartaoId, period]);
// Transaction rows
const [transactions, setTransactions] = useState<TransactionRow[]>([
{
@@ -275,15 +237,17 @@ export function MassAddDialog({
}
};
// Show period picker only for credit card
const showPeriodPicker = isCartaoSelected;
return (
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-3xl max-h-[90vh] overflow-y-auto p-6 sm:px-8">
<DialogHeader>
<DialogTitle>Adicionar múltiplos lançamentos</DialogTitle>
<DialogDescription>
Configure os valores padrão e adicione várias transações de uma
vez. Todos os lançamentos adicionados aqui são{" "}
Configure os valores padrão e adicione várias transações de uma vez.
Todos os lançamentos adicionados aqui são{" "}
<span className="font-bold">sempre à vista</span>.
</DialogDescription>
</DialogHeader>
@@ -303,9 +267,7 @@ export function MassAddDialog({
<SelectTrigger id="transaction-type" className="w-full">
<SelectValue>
{transactionType && (
<TransactionTypeSelectContent
label={transactionType}
/>
<TransactionTypeSelectContent label={transactionType} />
)}
</SelectValue>
</SelectTrigger>
@@ -329,9 +291,9 @@ export function MassAddDialog({
setPaymentMethod(value);
// Reset conta/cartao when changing payment method
if (value === "Cartão de crédito") {
setContaId(undefined);
setContaCartaoId(undefined);
} else {
setCartaoId(undefined);
setContaCartaoId(undefined);
}
}}
>
@@ -352,15 +314,17 @@ export function MassAddDialog({
</Select>
</div>
{/* Period */}
{/* Period - only for credit card */}
{showPeriodPicker ? (
<div className="space-y-2">
<Label htmlFor="period">Período</Label>
<Label htmlFor="period">Fatura</Label>
<PeriodPicker
value={period}
onChange={setPeriod}
className="w-full truncate"
/>
</div>
) : null}
{/* Conta/Cartao */}
<div className="space-y-2">
@@ -533,11 +497,7 @@ export function MassAddDialog({
<Select
value={transaction.pagadorId}
onValueChange={(value) =>
updateTransaction(
transaction.id,
"pagadorId",
value,
)
updateTransaction(transaction.id, "pagadorId", value)
}
>
<SelectTrigger
@@ -548,8 +508,7 @@ export function MassAddDialog({
{transaction.pagadorId &&
(() => {
const selectedOption = pagadorOptions.find(
(opt) =>
opt.value === transaction.pagadorId,
(opt) => opt.value === transaction.pagadorId,
);
return selectedOption ? (
<PagadorSelectContent
@@ -562,10 +521,7 @@ export function MassAddDialog({
</SelectTrigger>
<SelectContent>
{pagadorOptions.map((option) => (
<SelectItem
key={option.value}
value={option.value}
>
<SelectItem key={option.value} value={option.value}>
<PagadorSelectContent
label={option.label}
avatarUrl={option.avatarUrl}
@@ -663,15 +619,5 @@ export function MassAddDialog({
</DialogFooter>
</DialogContent>
</Dialog>
<FaturaWarningDialog
warning={faturaWarning}
onConfirm={(nextPeriod) => {
setPeriod(nextPeriod);
setFaturaWarning(null);
}}
onCancel={() => setFaturaWarning(null)}
/>
</>
);
}

View File

@@ -46,6 +46,8 @@ export type SelectOption = {
logo?: string | null;
icon?: string | null;
accountType?: string | null;
closingDay?: string | null;
dueDay?: string | null;
};
export type LancamentoFilterOption = {

View File

@@ -1,12 +1,64 @@
import type { LancamentoItem } from "@/components/lancamentos/types";
import { getTodayDateString } from "@/lib/utils/date";
import { derivePeriodFromDate } from "@/lib/utils/period";
import { derivePeriodFromDate, getNextPeriod } from "@/lib/utils/period";
import {
LANCAMENTO_CONDITIONS,
LANCAMENTO_PAYMENT_METHODS,
LANCAMENTO_TRANSACTION_TYPES,
} from "./constants";
/**
* Derives the fatura period for a credit card purchase based on closing day
* and due day. The period represents the month the fatura is due (vencimento).
*
* Steps:
* 1. If purchase day > closing day → the purchase missed this month's closing,
* so it enters the NEXT month's billing cycle (+1 month from purchase).
* 2. Then, if dueDay < closingDay, the due date falls in the month AFTER the
* closing month (e.g., closes 22nd, due 1st → closes Mar/22, due Apr/1),
* so we add another +1 month.
*
* @example
* // Card closes day 22, due day 1 (dueDay < closingDay → +1 extra)
* deriveCreditCardPeriod("2026-02-25", "22", "1") // "2026-04" (missed Feb closing → Mar cycle → due Apr)
* deriveCreditCardPeriod("2026-02-15", "22", "1") // "2026-03" (in Feb cycle → due Mar)
*
* // Card closes day 5, due day 15 (dueDay >= closingDay → no extra)
* deriveCreditCardPeriod("2026-02-10", "5", "15") // "2026-03" (missed Feb closing → Mar cycle → due Mar)
* deriveCreditCardPeriod("2026-02-03", "5", "15") // "2026-02" (in Feb cycle → due Feb)
*/
export function deriveCreditCardPeriod(
purchaseDate: string,
closingDay: string | null | undefined,
dueDay?: string | null | undefined,
): string {
const basePeriod = derivePeriodFromDate(purchaseDate);
if (!closingDay) return basePeriod;
const closingDayNum = Number.parseInt(closingDay, 10);
if (Number.isNaN(closingDayNum)) return basePeriod;
const dayPart = purchaseDate.split("-")[2];
const purchaseDayNum = Number.parseInt(dayPart ?? "1", 10);
// Start with the purchase month as the billing cycle
let period = basePeriod;
// If purchase is after closing day, it enters the next billing cycle
if (purchaseDayNum > closingDayNum) {
period = getNextPeriod(period);
}
// If due day < closing day, the due date falls in the month after closing
// (e.g., closes 22nd, due 1st → closing in March means due in April)
const dueDayNum = Number.parseInt(dueDay ?? "", 10);
if (!Number.isNaN(dueDayNum) && dueDayNum < closingDayNum) {
period = getNextPeriod(period);
}
return period;
}
/**
* Split type for dividing transactions between payers
*/
@@ -198,16 +250,44 @@ export function applyFieldDependencies(
key: keyof LancamentoFormState,
value: LancamentoFormState[keyof LancamentoFormState],
currentState: LancamentoFormState,
_periodDirty: boolean,
cardInfo?: { closingDay: string | null; dueDay: string | null } | null,
): Partial<LancamentoFormState> {
const updates: Partial<LancamentoFormState> = {};
// Removed automatic period update when purchase date changes
// if (key === "purchaseDate" && typeof value === "string") {
// if (!periodDirty) {
// updates.period = derivePeriodFromDate(value);
// }
// }
// Auto-derive period from purchaseDate
if (key === "purchaseDate" && typeof value === "string" && value) {
const method = currentState.paymentMethod;
if (method === "Cartão de crédito") {
updates.period = deriveCreditCardPeriod(
value,
cardInfo?.closingDay,
cardInfo?.dueDay,
);
} else if (method !== "Boleto") {
updates.period = derivePeriodFromDate(value);
}
}
// Auto-derive period from dueDate when payment method is boleto
if (key === "dueDate" && typeof value === "string" && value) {
if (currentState.paymentMethod === "Boleto") {
updates.period = derivePeriodFromDate(value);
}
}
// Auto-derive period when cartaoId changes (credit card selected)
if (
key === "cartaoId" &&
currentState.paymentMethod === "Cartão de crédito"
) {
if (typeof value === "string" && value && currentState.purchaseDate) {
updates.period = deriveCreditCardPeriod(
currentState.purchaseDate,
cardInfo?.closingDay,
cardInfo?.dueDay,
);
}
}
// When condition changes, clear irrelevant fields
if (key === "condition" && typeof value === "string") {
@@ -229,6 +309,27 @@ export function applyFieldDependencies(
updates.isSettled = currentState.isSettled ?? true;
}
// Re-derive period based on new payment method
if (value === "Cartão de crédito") {
if (
currentState.purchaseDate &&
currentState.cartaoId &&
cardInfo?.closingDay
) {
updates.period = deriveCreditCardPeriod(
currentState.purchaseDate,
cardInfo.closingDay,
cardInfo.dueDay,
);
} else if (currentState.purchaseDate) {
updates.period = derivePeriodFromDate(currentState.purchaseDate);
}
} else if (value === "Boleto" && currentState.dueDate) {
updates.period = derivePeriodFromDate(currentState.dueDate);
} else if (currentState.purchaseDate) {
updates.period = derivePeriodFromDate(currentState.purchaseDate);
}
// Clear boleto-specific fields if not boleto
if (value !== "Boleto") {
updates.dueDate = "";