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:
@@ -7,7 +7,6 @@ import {
|
|||||||
cartoes,
|
cartoes,
|
||||||
categorias,
|
categorias,
|
||||||
contas,
|
contas,
|
||||||
faturas,
|
|
||||||
lancamentos,
|
lancamentos,
|
||||||
pagadores,
|
pagadores,
|
||||||
} from "@/db/schema";
|
} from "@/db/schema";
|
||||||
@@ -33,7 +32,6 @@ import {
|
|||||||
import { noteSchema, uuidSchema } from "@/lib/schemas/common";
|
import { noteSchema, uuidSchema } from "@/lib/schemas/common";
|
||||||
import { formatDecimalForDbRequired } from "@/lib/utils/currency";
|
import { formatDecimalForDbRequired } from "@/lib/utils/currency";
|
||||||
import { getTodayDate, parseLocalDateString } from "@/lib/utils/date";
|
import { getTodayDate, parseLocalDateString } from "@/lib/utils/date";
|
||||||
import { getNextPeriod } from "@/lib/utils/period";
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Authorization Validation Functions
|
// 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
|
// Get unique establishment names from the last 3 months
|
||||||
export async function getRecentEstablishmentsAction(): Promise<string[]> {
|
export async function getRecentEstablishmentsAction(): Promise<string[]> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { RiCalculatorLine } from "@remixicon/react";
|
import { RiCalculatorLine } from "@remixicon/react";
|
||||||
import { CalculatorDialogButton } from "@/components/calculadora/calculator-dialog";
|
import { CalculatorDialogButton } from "@/components/calculadora/calculator-dialog";
|
||||||
import { PeriodPicker } from "@/components/period-picker";
|
|
||||||
import { CurrencyInput } from "@/components/ui/currency-input";
|
import { CurrencyInput } from "@/components/ui/currency-input";
|
||||||
import { DatePicker } from "@/components/ui/date-picker";
|
import { DatePicker } from "@/components/ui/date-picker";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
@@ -15,39 +14,28 @@ export function BasicFieldsSection({
|
|||||||
estabelecimentos,
|
estabelecimentos,
|
||||||
}: Omit<BasicFieldsSectionProps, "monthOptions">) {
|
}: Omit<BasicFieldsSectionProps, "monthOptions">) {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="space-y-3">
|
||||||
<div className="flex w-full flex-col gap-2 md:flex-row">
|
<div className="space-y-1">
|
||||||
<div className="w-1/2 space-y-1">
|
<Label htmlFor="name">Estabelecimento</Label>
|
||||||
<Label htmlFor="purchaseDate">Data da transação</Label>
|
<EstabelecimentoInput
|
||||||
<DatePicker
|
id="name"
|
||||||
id="purchaseDate"
|
value={formState.name}
|
||||||
value={formState.purchaseDate}
|
onChange={(value) => onFieldChange("name", value)}
|
||||||
onChange={(value) => onFieldChange("purchaseDate", value)}
|
estabelecimentos={estabelecimentos}
|
||||||
placeholder="Data da transação"
|
placeholder="Ex.: Restaurante do Zé"
|
||||||
required
|
maxLength={20}
|
||||||
/>
|
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>
|
||||||
|
|
||||||
<div className="flex w-full flex-col gap-2 md:flex-row">
|
<div className="flex w-full flex-col gap-2 md:flex-row">
|
||||||
<div className="w-1/2 space-y-1">
|
<div className="w-1/2 space-y-1">
|
||||||
<Label htmlFor="name">Estabelecimento</Label>
|
<Label htmlFor="purchaseDate">Data</Label>
|
||||||
<EstabelecimentoInput
|
<DatePicker
|
||||||
id="name"
|
id="purchaseDate"
|
||||||
value={formState.name}
|
value={formState.purchaseDate}
|
||||||
onChange={(value) => onFieldChange("name", value)}
|
onChange={(value) => onFieldChange("purchaseDate", value)}
|
||||||
estabelecimentos={estabelecimentos}
|
placeholder="Data"
|
||||||
placeholder="Ex.: Padaria"
|
|
||||||
maxLength={20}
|
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -74,6 +62,6 @@ export function BasicFieldsSection({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export function BoletoFieldsSection({
|
|||||||
<div className="flex w-full flex-col gap-2 md:flex-row">
|
<div className="flex w-full flex-col gap-2 md:flex-row">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"space-y-2 w-full",
|
"space-y-1 w-full",
|
||||||
showPaymentDate ? "md:w-1/2" : "md:w-full",
|
showPaymentDate ? "md:w-1/2" : "md:w-full",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useMemo } from "react";
|
import { useCallback, useMemo } from "react";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -32,13 +32,16 @@ export function ConditionSection({
|
|||||||
return Number.isNaN(value) || value <= 0 ? null : value;
|
return Number.isNaN(value) || value <= 0 ? null : value;
|
||||||
}, [formState.amount]);
|
}, [formState.amount]);
|
||||||
|
|
||||||
const getInstallmentLabel = (count: number) => {
|
const getInstallmentLabel = useCallback(
|
||||||
if (amount) {
|
(count: number) => {
|
||||||
const installmentValue = amount / count;
|
if (amount) {
|
||||||
return `${count}x de R$ ${formatCurrency(installmentValue)}`;
|
const installmentValue = amount / count;
|
||||||
}
|
return `${count}x de R$ ${formatCurrency(installmentValue)}`;
|
||||||
return `${count}x`;
|
}
|
||||||
};
|
return `${count}x`;
|
||||||
|
},
|
||||||
|
[amount],
|
||||||
|
);
|
||||||
|
|
||||||
const _getRecurrenceLabel = (count: number) => {
|
const _getRecurrenceLabel = (count: number) => {
|
||||||
return `${count} meses`;
|
return `${count} meses`;
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
import { RiAddLine } from "@remixicon/react";
|
||||||
import {
|
import {
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
|
||||||
useState,
|
useState,
|
||||||
useTransition,
|
useTransition,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import {
|
import {
|
||||||
checkFaturaStatusAction,
|
|
||||||
createLancamentoAction,
|
createLancamentoAction,
|
||||||
updateLancamentoAction,
|
updateLancamentoAction,
|
||||||
} from "@/app/(dashboard)/lancamentos/actions";
|
} from "@/app/(dashboard)/lancamentos/actions";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from "@/components/ui/collapsible";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -32,10 +36,6 @@ import {
|
|||||||
applyFieldDependencies,
|
applyFieldDependencies,
|
||||||
buildLancamentoInitialState,
|
buildLancamentoInitialState,
|
||||||
} from "@/lib/lancamentos/form-helpers";
|
} from "@/lib/lancamentos/form-helpers";
|
||||||
import {
|
|
||||||
type FaturaWarning,
|
|
||||||
FaturaWarningDialog,
|
|
||||||
} from "../fatura-warning-dialog";
|
|
||||||
import { BasicFieldsSection } from "./basic-fields-section";
|
import { BasicFieldsSection } from "./basic-fields-section";
|
||||||
import { BoletoFieldsSection } from "./boleto-fields-section";
|
import { BoletoFieldsSection } from "./boleto-fields-section";
|
||||||
import { CategorySection } from "./category-section";
|
import { CategorySection } from "./category-section";
|
||||||
@@ -93,13 +93,8 @@ export function LancamentoDialog({
|
|||||||
isImporting,
|
isImporting,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const [periodDirty, setPeriodDirty] = useState(false);
|
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
const [faturaWarning, setFaturaWarning] = useState<FaturaWarning | null>(
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
const lastCheckedRef = useRef<string | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dialogOpen) {
|
if (dialogOpen) {
|
||||||
@@ -120,11 +115,6 @@ export function LancamentoDialog({
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
setPeriodDirty(false);
|
|
||||||
setFaturaWarning(null);
|
|
||||||
lastCheckedRef.current = null;
|
|
||||||
} else {
|
|
||||||
setFaturaWarning(null);
|
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
dialogOpen,
|
dialogOpen,
|
||||||
@@ -140,40 +130,6 @@ export function LancamentoDialog({
|
|||||||
isImporting,
|
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 primaryPagador = formState.pagadorId;
|
||||||
|
|
||||||
const secondaryPagadorOptions = useMemo(
|
const secondaryPagadorOptions = useMemo(
|
||||||
@@ -194,19 +150,27 @@ export function LancamentoDialog({
|
|||||||
return Number.isNaN(parsed) ? 0 : Math.abs(parsed);
|
return Number.isNaN(parsed) ? 0 : Math.abs(parsed);
|
||||||
}, [formState.amount]);
|
}, [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(
|
const handleFieldChange = useCallback(
|
||||||
<Key extends keyof FormState>(key: Key, value: FormState[Key]) => {
|
<Key extends keyof FormState>(key: Key, value: FormState[Key]) => {
|
||||||
if (key === "period") {
|
|
||||||
setPeriodDirty(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
setFormState((prev) => {
|
setFormState((prev) => {
|
||||||
const dependencies = applyFieldDependencies(
|
const effectiveCartaoId =
|
||||||
key,
|
key === "cartaoId" ? (value as string) : prev.cartaoId;
|
||||||
value,
|
const cardInfo = getCardInfo(effectiveCartaoId);
|
||||||
prev,
|
|
||||||
periodDirty,
|
const dependencies = applyFieldDependencies(key, value, prev, cardInfo);
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
@@ -215,7 +179,7 @@ export function LancamentoDialog({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[periodDirty],
|
[getCardInfo],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
@@ -440,114 +404,115 @@ export function LancamentoDialog({
|
|||||||
const disableCartaoSelect = Boolean(lockCartaoSelection && mode === "create");
|
const disableCartaoSelect = Boolean(lockCartaoSelection && mode === "create");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
{trigger ? <DialogTrigger asChild>{trigger}</DialogTrigger> : null}
|
||||||
{trigger ? <DialogTrigger asChild>{trigger}</DialogTrigger> : null}
|
<DialogContent>
|
||||||
<DialogContent className="sm:max-w-xl p-6 sm:px-8">
|
<DialogHeader>
|
||||||
<DialogHeader>
|
<DialogTitle>{title}</DialogTitle>
|
||||||
<DialogTitle>{title}</DialogTitle>
|
<DialogDescription>{description}</DialogDescription>
|
||||||
<DialogDescription>{description}</DialogDescription>
|
</DialogHeader>
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<form
|
<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}
|
onSubmit={handleSubmit}
|
||||||
noValidate
|
noValidate
|
||||||
|
>
|
||||||
|
<BasicFieldsSection
|
||||||
|
formState={formState}
|
||||||
|
onFieldChange={handleFieldChange}
|
||||||
|
estabelecimentos={estabelecimentos}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CategorySection
|
||||||
|
formState={formState}
|
||||||
|
onFieldChange={handleFieldChange}
|
||||||
|
categoriaOptions={categoriaOptions}
|
||||||
|
categoriaGroups={categoriaGroups}
|
||||||
|
isUpdateMode={isUpdateMode}
|
||||||
|
hideTransactionType={
|
||||||
|
Boolean(isNewWithType) && !forceShowTransactionType
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!isUpdateMode ? (
|
||||||
|
<SplitAndSettlementSection
|
||||||
|
formState={formState}
|
||||||
|
onFieldChange={handleFieldChange}
|
||||||
|
showSettledToggle={showSettledToggle}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<PagadorSection
|
||||||
|
formState={formState}
|
||||||
|
onFieldChange={handleFieldChange}
|
||||||
|
pagadorOptions={pagadorOptions}
|
||||||
|
secondaryPagadorOptions={secondaryPagadorOptions}
|
||||||
|
totalAmount={totalAmount}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PaymentMethodSection
|
||||||
|
formState={formState}
|
||||||
|
onFieldChange={handleFieldChange}
|
||||||
|
contaOptions={contaOptions}
|
||||||
|
cartaoOptions={cartaoOptions}
|
||||||
|
isUpdateMode={isUpdateMode}
|
||||||
|
disablePaymentMethod={disablePaymentMethod}
|
||||||
|
disableCartaoSelect={disableCartaoSelect}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{showDueDate ? (
|
||||||
|
<BoletoFieldsSection
|
||||||
|
formState={formState}
|
||||||
|
onFieldChange={handleFieldChange}
|
||||||
|
showPaymentDate={showPaymentDate}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Collapsible
|
||||||
|
defaultOpen={
|
||||||
|
formState.condition !== "À vista" || formState.note.length > 0
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<BasicFieldsSection
|
<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">
|
||||||
formState={formState}
|
<RiAddLine className="text-primary size-4 transition-transform duration-200" />
|
||||||
onFieldChange={handleFieldChange}
|
Condições e anotações
|
||||||
estabelecimentos={estabelecimentos}
|
</CollapsibleTrigger>
|
||||||
/>
|
<CollapsibleContent className="space-y-3 pt-3">
|
||||||
|
{!isUpdateMode ? (
|
||||||
|
<ConditionSection
|
||||||
|
formState={formState}
|
||||||
|
onFieldChange={handleFieldChange}
|
||||||
|
showInstallments={showInstallments}
|
||||||
|
showRecurrence={showRecurrence}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<CategorySection
|
<NoteSection
|
||||||
formState={formState}
|
|
||||||
onFieldChange={handleFieldChange}
|
|
||||||
categoriaOptions={categoriaOptions}
|
|
||||||
categoriaGroups={categoriaGroups}
|
|
||||||
isUpdateMode={isUpdateMode}
|
|
||||||
hideTransactionType={
|
|
||||||
Boolean(isNewWithType) && !forceShowTransactionType
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{!isUpdateMode ? (
|
|
||||||
<SplitAndSettlementSection
|
|
||||||
formState={formState}
|
formState={formState}
|
||||||
onFieldChange={handleFieldChange}
|
onFieldChange={handleFieldChange}
|
||||||
showSettledToggle={showSettledToggle}
|
|
||||||
/>
|
/>
|
||||||
) : null}
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
|
||||||
<PagadorSection
|
{errorMessage ? (
|
||||||
formState={formState}
|
<p className="text-sm text-destructive">{errorMessage}</p>
|
||||||
onFieldChange={handleFieldChange}
|
) : null}
|
||||||
pagadorOptions={pagadorOptions}
|
|
||||||
secondaryPagadorOptions={secondaryPagadorOptions}
|
|
||||||
totalAmount={totalAmount}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PaymentMethodSection
|
<DialogFooter>
|
||||||
formState={formState}
|
<Button
|
||||||
onFieldChange={handleFieldChange}
|
type="button"
|
||||||
contaOptions={contaOptions}
|
variant="outline"
|
||||||
cartaoOptions={cartaoOptions}
|
onClick={() => setDialogOpen(false)}
|
||||||
isUpdateMode={isUpdateMode}
|
disabled={isPending}
|
||||||
disablePaymentMethod={disablePaymentMethod}
|
>
|
||||||
disableCartaoSelect={disableCartaoSelect}
|
Cancelar
|
||||||
/>
|
</Button>
|
||||||
|
<Button type="submit" disabled={isPending}>
|
||||||
{showDueDate ? (
|
{isPending ? "Salvando..." : submitLabel}
|
||||||
<BoletoFieldsSection
|
</Button>
|
||||||
formState={formState}
|
</DialogFooter>
|
||||||
onFieldChange={handleFieldChange}
|
</form>
|
||||||
showPaymentDate={showPaymentDate}
|
</DialogContent>
|
||||||
/>
|
</Dialog>
|
||||||
) : null}
|
|
||||||
|
|
||||||
{!isUpdateMode ? (
|
|
||||||
<ConditionSection
|
|
||||||
formState={formState}
|
|
||||||
onFieldChange={handleFieldChange}
|
|
||||||
showInstallments={showInstallments}
|
|
||||||
showRecurrence={showRecurrence}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<NoteSection
|
|
||||||
formState={formState}
|
|
||||||
onFieldChange={handleFieldChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{errorMessage ? (
|
|
||||||
<p className="text-sm text-destructive">{errorMessage}</p>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<DialogFooter className="gap-3">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => setDialogOpen(false)}
|
|
||||||
disabled={isPending}
|
|
||||||
>
|
|
||||||
Cancelar
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={isPending}>
|
|
||||||
{isPending ? "Salvando..." : submitLabel}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<FaturaWarningDialog
|
|
||||||
warning={faturaWarning}
|
|
||||||
onConfirm={(nextPeriod) => {
|
|
||||||
handleFieldChange("period", nextPeriod);
|
|
||||||
setFaturaWarning(null);
|
|
||||||
}}
|
|
||||||
onCancel={() => setFaturaWarning(null)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { MonthPicker } from "@/components/ui/monthpicker";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -9,6 +16,7 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { LANCAMENTO_PAYMENT_METHODS } from "@/lib/lancamentos/constants";
|
import { LANCAMENTO_PAYMENT_METHODS } from "@/lib/lancamentos/constants";
|
||||||
|
import { displayPeriod } from "@/lib/utils/period";
|
||||||
import { cn } from "@/lib/utils/ui";
|
import { cn } from "@/lib/utils/ui";
|
||||||
import {
|
import {
|
||||||
ContaCartaoSelectContent,
|
ContaCartaoSelectContent,
|
||||||
@@ -16,6 +24,52 @@ import {
|
|||||||
} from "../../select-items";
|
} from "../../select-items";
|
||||||
import type { PaymentMethodSectionProps } from "./lancamento-dialog-types";
|
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({
|
export function PaymentMethodSection({
|
||||||
formState,
|
formState,
|
||||||
onFieldChange,
|
onFieldChange,
|
||||||
@@ -46,7 +100,7 @@ export function PaymentMethodSection({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!isUpdateMode ? (
|
{!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
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"space-y-1 w-full",
|
"space-y-1 w-full",
|
||||||
@@ -131,6 +185,12 @@ export function PaymentMethodSection({
|
|||||||
)}
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
{formState.cartaoId ? (
|
||||||
|
<InlinePeriodPicker
|
||||||
|
period={formState.period}
|
||||||
|
onPeriodChange={(value) => onFieldChange("period", value)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -239,6 +299,12 @@ export function PaymentMethodSection({
|
|||||||
)}
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
{formState.cartaoId ? (
|
||||||
|
<InlinePeriodPicker
|
||||||
|
period={formState.period}
|
||||||
|
onPeriodChange={(value) => onFieldChange("period", value)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export function SplitAndSettlementSection({
|
|||||||
showSettledToggle,
|
showSettledToggle,
|
||||||
}: SplitAndSettlementSectionProps) {
|
}: SplitAndSettlementSectionProps) {
|
||||||
return (
|
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
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"space-y-1",
|
"space-y-1",
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { RiAddLine, RiDeleteBinLine } from "@remixicon/react";
|
import { RiAddLine, RiDeleteBinLine } from "@remixicon/react";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { checkFaturaStatusAction } from "@/app/(dashboard)/lancamentos/actions";
|
|
||||||
import { PeriodPicker } from "@/components/period-picker";
|
import { PeriodPicker } from "@/components/period-picker";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { CurrencyInput } from "@/components/ui/currency-input";
|
import { CurrencyInput } from "@/components/ui/currency-input";
|
||||||
@@ -40,10 +39,6 @@ import {
|
|||||||
TransactionTypeSelectContent,
|
TransactionTypeSelectContent,
|
||||||
} from "../select-items";
|
} from "../select-items";
|
||||||
import { EstabelecimentoInput } from "../shared/estabelecimento-input";
|
import { EstabelecimentoInput } from "../shared/estabelecimento-input";
|
||||||
import {
|
|
||||||
type FaturaWarning,
|
|
||||||
FaturaWarningDialog,
|
|
||||||
} from "./fatura-warning-dialog";
|
|
||||||
|
|
||||||
interface MassAddDialogProps {
|
interface MassAddDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -124,39 +119,6 @@ export function MassAddDialog({
|
|||||||
? contaCartaoId.replace("cartao:", "")
|
? contaCartaoId.replace("cartao:", "")
|
||||||
: undefined;
|
: 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
|
// Transaction rows
|
||||||
const [transactions, setTransactions] = useState<TransactionRow[]>([
|
const [transactions, setTransactions] = useState<TransactionRow[]>([
|
||||||
{
|
{
|
||||||
@@ -275,403 +237,387 @@ export function MassAddDialog({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Show period picker only for credit card
|
||||||
|
const showPeriodPicker = isCartaoSelected;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<DialogContent className="sm:max-w-3xl max-h-[90vh] overflow-y-auto p-6 sm:px-8">
|
||||||
<DialogContent className="sm:max-w-3xl max-h-[90vh] overflow-y-auto p-6 sm:px-8">
|
<DialogHeader>
|
||||||
<DialogHeader>
|
<DialogTitle>Adicionar múltiplos lançamentos</DialogTitle>
|
||||||
<DialogTitle>Adicionar múltiplos lançamentos</DialogTitle>
|
<DialogDescription>
|
||||||
<DialogDescription>
|
Configure os valores padrão e adicione várias transações de uma vez.
|
||||||
Configure os valores padrão e adicione várias transações de uma
|
Todos os lançamentos adicionados aqui são{" "}
|
||||||
vez. Todos os lançamentos adicionados aqui são{" "}
|
<span className="font-bold">sempre à vista</span>.
|
||||||
<span className="font-bold">sempre à vista</span>.
|
</DialogDescription>
|
||||||
</DialogDescription>
|
</DialogHeader>
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Fixed Fields Section */}
|
{/* Fixed Fields Section */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h3 className="text-sm font-semibold">Valores Padrão</h3>
|
<h3 className="text-sm font-semibold">Valores Padrão</h3>
|
||||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
{/* Transaction Type */}
|
{/* Transaction Type */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="transaction-type">Tipo de Transação</Label>
|
<Label htmlFor="transaction-type">Tipo de Transação</Label>
|
||||||
<Select
|
<Select
|
||||||
value={transactionType}
|
value={transactionType}
|
||||||
onValueChange={setTransactionType}
|
onValueChange={setTransactionType}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="transaction-type" className="w-full">
|
<SelectTrigger id="transaction-type" className="w-full">
|
||||||
<SelectValue>
|
<SelectValue>
|
||||||
{transactionType && (
|
{transactionType && (
|
||||||
<TransactionTypeSelectContent
|
<TransactionTypeSelectContent label={transactionType} />
|
||||||
label={transactionType}
|
)}
|
||||||
/>
|
</SelectValue>
|
||||||
)}
|
</SelectTrigger>
|
||||||
</SelectValue>
|
<SelectContent>
|
||||||
</SelectTrigger>
|
<SelectItem value="Despesa">
|
||||||
<SelectContent>
|
<TransactionTypeSelectContent label="Despesa" />
|
||||||
<SelectItem value="Despesa">
|
</SelectItem>
|
||||||
<TransactionTypeSelectContent label="Despesa" />
|
<SelectItem value="Receita">
|
||||||
|
<TransactionTypeSelectContent label="Receita" />
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Payment Method */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="payment-method">Forma de Pagamento</Label>
|
||||||
|
<Select
|
||||||
|
value={paymentMethod}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setPaymentMethod(value);
|
||||||
|
// Reset conta/cartao when changing payment method
|
||||||
|
if (value === "Cartão de crédito") {
|
||||||
|
setContaCartaoId(undefined);
|
||||||
|
} else {
|
||||||
|
setContaCartaoId(undefined);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="payment-method" className="w-full">
|
||||||
|
<SelectValue>
|
||||||
|
{paymentMethod && (
|
||||||
|
<PaymentMethodSelectContent label={paymentMethod} />
|
||||||
|
)}
|
||||||
|
</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{LANCAMENTO_PAYMENT_METHODS.map((method) => (
|
||||||
|
<SelectItem key={method} value={method}>
|
||||||
|
<PaymentMethodSelectContent label={method} />
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="Receita">
|
))}
|
||||||
<TransactionTypeSelectContent label="Receita" />
|
</SelectContent>
|
||||||
</SelectItem>
|
</Select>
|
||||||
</SelectContent>
|
</div>
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Payment Method */}
|
{/* Period - only for credit card */}
|
||||||
|
{showPeriodPicker ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="payment-method">Forma de Pagamento</Label>
|
<Label htmlFor="period">Fatura</Label>
|
||||||
<Select
|
|
||||||
value={paymentMethod}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
setPaymentMethod(value);
|
|
||||||
// Reset conta/cartao when changing payment method
|
|
||||||
if (value === "Cartão de crédito") {
|
|
||||||
setContaId(undefined);
|
|
||||||
} else {
|
|
||||||
setCartaoId(undefined);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="payment-method" className="w-full">
|
|
||||||
<SelectValue>
|
|
||||||
{paymentMethod && (
|
|
||||||
<PaymentMethodSelectContent label={paymentMethod} />
|
|
||||||
)}
|
|
||||||
</SelectValue>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{LANCAMENTO_PAYMENT_METHODS.map((method) => (
|
|
||||||
<SelectItem key={method} value={method}>
|
|
||||||
<PaymentMethodSelectContent label={method} />
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Period */}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="period">Período</Label>
|
|
||||||
<PeriodPicker
|
<PeriodPicker
|
||||||
value={period}
|
value={period}
|
||||||
onChange={setPeriod}
|
onChange={setPeriod}
|
||||||
className="w-full truncate"
|
className="w-full truncate"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{/* Conta/Cartao */}
|
{/* Conta/Cartao */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="conta-cartao">
|
<Label htmlFor="conta-cartao">
|
||||||
{isLockedToCartao ? "Cartão" : "Conta/Cartão"}
|
{isLockedToCartao ? "Cartão" : "Conta/Cartão"}
|
||||||
</Label>
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
value={contaCartaoId}
|
value={contaCartaoId}
|
||||||
onValueChange={setContaCartaoId}
|
onValueChange={setContaCartaoId}
|
||||||
disabled={isLockedToCartao}
|
disabled={isLockedToCartao}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="conta-cartao" className="w-full">
|
<SelectTrigger id="conta-cartao" className="w-full">
|
||||||
<SelectValue placeholder="Selecione">
|
<SelectValue placeholder="Selecione">
|
||||||
{contaCartaoId &&
|
{contaCartaoId &&
|
||||||
(() => {
|
(() => {
|
||||||
if (isCartaoSelected) {
|
if (isCartaoSelected) {
|
||||||
const selectedOption = cartaoOptions.find(
|
const selectedOption = cartaoOptions.find(
|
||||||
(opt) => opt.value === cartaoId,
|
(opt) => opt.value === cartaoId,
|
||||||
);
|
);
|
||||||
return selectedOption ? (
|
return selectedOption ? (
|
||||||
<ContaCartaoSelectContent
|
<ContaCartaoSelectContent
|
||||||
label={selectedOption.label}
|
label={selectedOption.label}
|
||||||
logo={selectedOption.logo}
|
logo={selectedOption.logo}
|
||||||
isCartao={true}
|
isCartao={true}
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
} else {
|
} else {
|
||||||
const selectedOption = contaOptions.find(
|
const selectedOption = contaOptions.find(
|
||||||
(opt) => opt.value === contaId,
|
(opt) => opt.value === contaId,
|
||||||
);
|
);
|
||||||
return selectedOption ? (
|
return selectedOption ? (
|
||||||
<ContaCartaoSelectContent
|
<ContaCartaoSelectContent
|
||||||
label={selectedOption.label}
|
label={selectedOption.label}
|
||||||
logo={selectedOption.logo}
|
logo={selectedOption.logo}
|
||||||
isCartao={false}
|
isCartao={false}
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
}
|
}
|
||||||
})()}
|
})()}
|
||||||
</SelectValue>
|
</SelectValue>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{cartaoOptions.length > 0 && (
|
{cartaoOptions.length > 0 && (
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
{!isLockedToCartao && (
|
{!isLockedToCartao && (
|
||||||
<SelectLabel>Cartões</SelectLabel>
|
<SelectLabel>Cartões</SelectLabel>
|
||||||
)}
|
)}
|
||||||
{cartaoOptions
|
{cartaoOptions
|
||||||
.filter(
|
.filter(
|
||||||
(option) =>
|
(option) =>
|
||||||
!isLockedToCartao ||
|
!isLockedToCartao ||
|
||||||
option.value === defaultCartaoId,
|
option.value === defaultCartaoId,
|
||||||
)
|
)
|
||||||
.map((option) => (
|
.map((option) => (
|
||||||
<SelectItem
|
|
||||||
key={option.value}
|
|
||||||
value={`cartao:${option.value}`}
|
|
||||||
>
|
|
||||||
<ContaCartaoSelectContent
|
|
||||||
label={option.label}
|
|
||||||
logo={option.logo}
|
|
||||||
isCartao={true}
|
|
||||||
/>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectGroup>
|
|
||||||
)}
|
|
||||||
{!isLockedToCartao && contaOptions.length > 0 && (
|
|
||||||
<SelectGroup>
|
|
||||||
<SelectLabel>Contas</SelectLabel>
|
|
||||||
{contaOptions.map((option) => (
|
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={option.value}
|
key={option.value}
|
||||||
value={`conta:${option.value}`}
|
value={`cartao:${option.value}`}
|
||||||
>
|
>
|
||||||
<ContaCartaoSelectContent
|
<ContaCartaoSelectContent
|
||||||
label={option.label}
|
label={option.label}
|
||||||
logo={option.logo}
|
logo={option.logo}
|
||||||
isCartao={false}
|
isCartao={true}
|
||||||
/>
|
/>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
)}
|
)}
|
||||||
</SelectContent>
|
{!isLockedToCartao && contaOptions.length > 0 && (
|
||||||
</Select>
|
<SelectGroup>
|
||||||
</div>
|
<SelectLabel>Contas</SelectLabel>
|
||||||
</div>
|
{contaOptions.map((option) => (
|
||||||
</div>
|
<SelectItem
|
||||||
|
key={option.value}
|
||||||
<Separator />
|
value={`conta:${option.value}`}
|
||||||
|
|
||||||
{/* Transactions Section */}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-sm font-semibold">Lançamentos</h3>
|
|
||||||
|
|
||||||
<div className="space-y-3">
|
|
||||||
{transactions.map((transaction, index) => (
|
|
||||||
<div
|
|
||||||
key={transaction.id}
|
|
||||||
className="grid gap-2 border-b pb-3 border-dashed last:border-0"
|
|
||||||
>
|
|
||||||
<div className="flex gap-2 w-full">
|
|
||||||
<div className="w-full">
|
|
||||||
<Label
|
|
||||||
htmlFor={`date-${transaction.id}`}
|
|
||||||
className="sr-only"
|
|
||||||
>
|
|
||||||
Data {index + 1}
|
|
||||||
</Label>
|
|
||||||
<DatePicker
|
|
||||||
id={`date-${transaction.id}`}
|
|
||||||
value={transaction.purchaseDate}
|
|
||||||
onChange={(value) =>
|
|
||||||
updateTransaction(
|
|
||||||
transaction.id,
|
|
||||||
"purchaseDate",
|
|
||||||
value,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
placeholder="Data"
|
|
||||||
className="w-32 truncate"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="w-full">
|
|
||||||
<Label
|
|
||||||
htmlFor={`name-${transaction.id}`}
|
|
||||||
className="sr-only"
|
|
||||||
>
|
|
||||||
Estabelecimento {index + 1}
|
|
||||||
</Label>
|
|
||||||
<EstabelecimentoInput
|
|
||||||
id={`name-${transaction.id}`}
|
|
||||||
placeholder="Local"
|
|
||||||
value={transaction.name}
|
|
||||||
onChange={(value) =>
|
|
||||||
updateTransaction(transaction.id, "name", value)
|
|
||||||
}
|
|
||||||
estabelecimentos={estabelecimentos}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full">
|
|
||||||
<Label
|
|
||||||
htmlFor={`amount-${transaction.id}`}
|
|
||||||
className="sr-only"
|
|
||||||
>
|
|
||||||
Valor {index + 1}
|
|
||||||
</Label>
|
|
||||||
<CurrencyInput
|
|
||||||
id={`amount-${transaction.id}`}
|
|
||||||
placeholder="R$ 0,00"
|
|
||||||
value={transaction.amount}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
updateTransaction(transaction.id, "amount", value)
|
|
||||||
}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full">
|
|
||||||
<Label
|
|
||||||
htmlFor={`pagador-${transaction.id}`}
|
|
||||||
className="sr-only"
|
|
||||||
>
|
|
||||||
Pagador {index + 1}
|
|
||||||
</Label>
|
|
||||||
<Select
|
|
||||||
value={transaction.pagadorId}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
updateTransaction(
|
|
||||||
transaction.id,
|
|
||||||
"pagadorId",
|
|
||||||
value,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger
|
|
||||||
id={`pagador-${transaction.id}`}
|
|
||||||
className="w-32 truncate"
|
|
||||||
>
|
>
|
||||||
<SelectValue placeholder="Pagador">
|
<ContaCartaoSelectContent
|
||||||
{transaction.pagadorId &&
|
label={option.label}
|
||||||
(() => {
|
logo={option.logo}
|
||||||
const selectedOption = pagadorOptions.find(
|
isCartao={false}
|
||||||
(opt) =>
|
/>
|
||||||
opt.value === transaction.pagadorId,
|
</SelectItem>
|
||||||
);
|
))}
|
||||||
return selectedOption ? (
|
</SelectGroup>
|
||||||
<PagadorSelectContent
|
)}
|
||||||
label={selectedOption.label}
|
</SelectContent>
|
||||||
avatarUrl={selectedOption.avatarUrl}
|
</Select>
|
||||||
/>
|
|
||||||
) : null;
|
|
||||||
})()}
|
|
||||||
</SelectValue>
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{pagadorOptions.map((option) => (
|
|
||||||
<SelectItem
|
|
||||||
key={option.value}
|
|
||||||
value={option.value}
|
|
||||||
>
|
|
||||||
<PagadorSelectContent
|
|
||||||
label={option.label}
|
|
||||||
avatarUrl={option.avatarUrl}
|
|
||||||
/>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full">
|
|
||||||
<Label
|
|
||||||
htmlFor={`categoria-${transaction.id}`}
|
|
||||||
className="sr-only"
|
|
||||||
>
|
|
||||||
Categoria {index + 1}
|
|
||||||
</Label>
|
|
||||||
<Select
|
|
||||||
value={transaction.categoriaId}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
updateTransaction(
|
|
||||||
transaction.id,
|
|
||||||
"categoriaId",
|
|
||||||
value,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger
|
|
||||||
id={`categoria-${transaction.id}`}
|
|
||||||
className="w-32 truncate"
|
|
||||||
>
|
|
||||||
<SelectValue placeholder="Categoria" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{groupedCategorias.map((group) => (
|
|
||||||
<SelectGroup key={group.label}>
|
|
||||||
<SelectLabel>{group.label}</SelectLabel>
|
|
||||||
{group.options.map((option) => (
|
|
||||||
<SelectItem
|
|
||||||
key={option.value}
|
|
||||||
value={option.value}
|
|
||||||
>
|
|
||||||
<CategoriaSelectContent
|
|
||||||
label={option.label}
|
|
||||||
icon={option.icon}
|
|
||||||
/>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectGroup>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="size-7 shrink-0"
|
|
||||||
onClick={addTransaction}
|
|
||||||
>
|
|
||||||
<RiAddLine className="size-3.5" />
|
|
||||||
<span className="sr-only">Adicionar transação</span>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="size-7 shrink-0"
|
|
||||||
onClick={() => removeTransaction(transaction.id)}
|
|
||||||
disabled={transactions.length === 1}
|
|
||||||
>
|
|
||||||
<RiDeleteBinLine className="size-3.5" />
|
|
||||||
<span className="sr-only">Remover transação</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<Separator />
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => onOpenChange(false)}
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
Cancelar
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleSubmit} disabled={loading}>
|
|
||||||
{loading && <Spinner className="size-4" />}
|
|
||||||
Criar {transactions.length}{" "}
|
|
||||||
{transactions.length === 1 ? "lançamento" : "lançamentos"}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<FaturaWarningDialog
|
{/* Transactions Section */}
|
||||||
warning={faturaWarning}
|
<div className="space-y-4">
|
||||||
onConfirm={(nextPeriod) => {
|
<h3 className="text-sm font-semibold">Lançamentos</h3>
|
||||||
setPeriod(nextPeriod);
|
|
||||||
setFaturaWarning(null);
|
<div className="space-y-3">
|
||||||
}}
|
{transactions.map((transaction, index) => (
|
||||||
onCancel={() => setFaturaWarning(null)}
|
<div
|
||||||
/>
|
key={transaction.id}
|
||||||
</>
|
className="grid gap-2 border-b pb-3 border-dashed last:border-0"
|
||||||
|
>
|
||||||
|
<div className="flex gap-2 w-full">
|
||||||
|
<div className="w-full">
|
||||||
|
<Label
|
||||||
|
htmlFor={`date-${transaction.id}`}
|
||||||
|
className="sr-only"
|
||||||
|
>
|
||||||
|
Data {index + 1}
|
||||||
|
</Label>
|
||||||
|
<DatePicker
|
||||||
|
id={`date-${transaction.id}`}
|
||||||
|
value={transaction.purchaseDate}
|
||||||
|
onChange={(value) =>
|
||||||
|
updateTransaction(
|
||||||
|
transaction.id,
|
||||||
|
"purchaseDate",
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
placeholder="Data"
|
||||||
|
className="w-32 truncate"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-full">
|
||||||
|
<Label
|
||||||
|
htmlFor={`name-${transaction.id}`}
|
||||||
|
className="sr-only"
|
||||||
|
>
|
||||||
|
Estabelecimento {index + 1}
|
||||||
|
</Label>
|
||||||
|
<EstabelecimentoInput
|
||||||
|
id={`name-${transaction.id}`}
|
||||||
|
placeholder="Local"
|
||||||
|
value={transaction.name}
|
||||||
|
onChange={(value) =>
|
||||||
|
updateTransaction(transaction.id, "name", value)
|
||||||
|
}
|
||||||
|
estabelecimentos={estabelecimentos}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full">
|
||||||
|
<Label
|
||||||
|
htmlFor={`amount-${transaction.id}`}
|
||||||
|
className="sr-only"
|
||||||
|
>
|
||||||
|
Valor {index + 1}
|
||||||
|
</Label>
|
||||||
|
<CurrencyInput
|
||||||
|
id={`amount-${transaction.id}`}
|
||||||
|
placeholder="R$ 0,00"
|
||||||
|
value={transaction.amount}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateTransaction(transaction.id, "amount", value)
|
||||||
|
}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full">
|
||||||
|
<Label
|
||||||
|
htmlFor={`pagador-${transaction.id}`}
|
||||||
|
className="sr-only"
|
||||||
|
>
|
||||||
|
Pagador {index + 1}
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={transaction.pagadorId}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateTransaction(transaction.id, "pagadorId", value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id={`pagador-${transaction.id}`}
|
||||||
|
className="w-32 truncate"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Pagador">
|
||||||
|
{transaction.pagadorId &&
|
||||||
|
(() => {
|
||||||
|
const selectedOption = pagadorOptions.find(
|
||||||
|
(opt) => opt.value === transaction.pagadorId,
|
||||||
|
);
|
||||||
|
return selectedOption ? (
|
||||||
|
<PagadorSelectContent
|
||||||
|
label={selectedOption.label}
|
||||||
|
avatarUrl={selectedOption.avatarUrl}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
})()}
|
||||||
|
</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{pagadorOptions.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={option.value}>
|
||||||
|
<PagadorSelectContent
|
||||||
|
label={option.label}
|
||||||
|
avatarUrl={option.avatarUrl}
|
||||||
|
/>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full">
|
||||||
|
<Label
|
||||||
|
htmlFor={`categoria-${transaction.id}`}
|
||||||
|
className="sr-only"
|
||||||
|
>
|
||||||
|
Categoria {index + 1}
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={transaction.categoriaId}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
updateTransaction(
|
||||||
|
transaction.id,
|
||||||
|
"categoriaId",
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id={`categoria-${transaction.id}`}
|
||||||
|
className="w-32 truncate"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Categoria" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{groupedCategorias.map((group) => (
|
||||||
|
<SelectGroup key={group.label}>
|
||||||
|
<SelectLabel>{group.label}</SelectLabel>
|
||||||
|
{group.options.map((option) => (
|
||||||
|
<SelectItem
|
||||||
|
key={option.value}
|
||||||
|
value={option.value}
|
||||||
|
>
|
||||||
|
<CategoriaSelectContent
|
||||||
|
label={option.label}
|
||||||
|
icon={option.icon}
|
||||||
|
/>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-7 shrink-0"
|
||||||
|
onClick={addTransaction}
|
||||||
|
>
|
||||||
|
<RiAddLine className="size-3.5" />
|
||||||
|
<span className="sr-only">Adicionar transação</span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="size-7 shrink-0"
|
||||||
|
onClick={() => removeTransaction(transaction.id)}
|
||||||
|
disabled={transactions.length === 1}
|
||||||
|
>
|
||||||
|
<RiDeleteBinLine className="size-3.5" />
|
||||||
|
<span className="sr-only">Remover transação</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSubmit} disabled={loading}>
|
||||||
|
{loading && <Spinner className="size-4" />}
|
||||||
|
Criar {transactions.length}{" "}
|
||||||
|
{transactions.length === 1 ? "lançamento" : "lançamentos"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ export type SelectOption = {
|
|||||||
logo?: string | null;
|
logo?: string | null;
|
||||||
icon?: string | null;
|
icon?: string | null;
|
||||||
accountType?: string | null;
|
accountType?: string | null;
|
||||||
|
closingDay?: string | null;
|
||||||
|
dueDay?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LancamentoFilterOption = {
|
export type LancamentoFilterOption = {
|
||||||
|
|||||||
@@ -1,12 +1,64 @@
|
|||||||
import type { LancamentoItem } from "@/components/lancamentos/types";
|
import type { LancamentoItem } from "@/components/lancamentos/types";
|
||||||
import { getTodayDateString } from "@/lib/utils/date";
|
import { getTodayDateString } from "@/lib/utils/date";
|
||||||
import { derivePeriodFromDate } from "@/lib/utils/period";
|
import { derivePeriodFromDate, getNextPeriod } from "@/lib/utils/period";
|
||||||
import {
|
import {
|
||||||
LANCAMENTO_CONDITIONS,
|
LANCAMENTO_CONDITIONS,
|
||||||
LANCAMENTO_PAYMENT_METHODS,
|
LANCAMENTO_PAYMENT_METHODS,
|
||||||
LANCAMENTO_TRANSACTION_TYPES,
|
LANCAMENTO_TRANSACTION_TYPES,
|
||||||
} from "./constants";
|
} 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
|
* Split type for dividing transactions between payers
|
||||||
*/
|
*/
|
||||||
@@ -198,16 +250,44 @@ export function applyFieldDependencies(
|
|||||||
key: keyof LancamentoFormState,
|
key: keyof LancamentoFormState,
|
||||||
value: LancamentoFormState[keyof LancamentoFormState],
|
value: LancamentoFormState[keyof LancamentoFormState],
|
||||||
currentState: LancamentoFormState,
|
currentState: LancamentoFormState,
|
||||||
_periodDirty: boolean,
|
cardInfo?: { closingDay: string | null; dueDay: string | null } | null,
|
||||||
): Partial<LancamentoFormState> {
|
): Partial<LancamentoFormState> {
|
||||||
const updates: Partial<LancamentoFormState> = {};
|
const updates: Partial<LancamentoFormState> = {};
|
||||||
|
|
||||||
// Removed automatic period update when purchase date changes
|
// Auto-derive period from purchaseDate
|
||||||
// if (key === "purchaseDate" && typeof value === "string") {
|
if (key === "purchaseDate" && typeof value === "string" && value) {
|
||||||
// if (!periodDirty) {
|
const method = currentState.paymentMethod;
|
||||||
// updates.period = derivePeriodFromDate(value);
|
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
|
// When condition changes, clear irrelevant fields
|
||||||
if (key === "condition" && typeof value === "string") {
|
if (key === "condition" && typeof value === "string") {
|
||||||
@@ -229,6 +309,27 @@ export function applyFieldDependencies(
|
|||||||
updates.isSettled = currentState.isSettled ?? true;
|
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
|
// Clear boleto-specific fields if not boleto
|
||||||
if (value !== "Boleto") {
|
if (value !== "Boleto") {
|
||||||
updates.dueDate = "";
|
updates.dueDate = "";
|
||||||
|
|||||||
Reference in New Issue
Block a user