From f16140cb443a92a15ffcd85b9de927826fb6053b Mon Sep 17 00:00:00 2001 From: Felipe Coutinho Date: Sun, 22 Feb 2026 19:57:42 +0000 Subject: [PATCH] chore: snapshot sidebar layout antes de experimentar topbar Co-Authored-By: Claude Sonnet 4.6 --- app/(dashboard)/lancamentos/actions.ts | 55 ++ app/(dashboard)/layout.tsx | 2 +- app/globals.css | 2 + .../dialogs/fatura-warning-dialog.tsx | 84 ++ .../lancamento-dialog/lancamento-dialog.tsx | 225 +++-- .../lancamentos/dialogs/mass-add-dialog.tsx | 779 ++++++++++-------- 6 files changed, 703 insertions(+), 444 deletions(-) create mode 100644 components/lancamentos/dialogs/fatura-warning-dialog.tsx diff --git a/app/(dashboard)/lancamentos/actions.ts b/app/(dashboard)/lancamentos/actions.ts index b6364a8..4cec77f 100644 --- a/app/(dashboard)/lancamentos/actions.ts +++ b/app/(dashboard)/lancamentos/actions.ts @@ -7,6 +7,7 @@ import { cartoes, categorias, contas, + faturas, lancamentos, pagadores, } from "@/db/schema"; @@ -32,6 +33,7 @@ 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 @@ -1639,6 +1641,59 @@ 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 { try { diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx index 7df5fb6..2df5167 100644 --- a/app/(dashboard)/layout.tsx +++ b/app/(dashboard)/layout.tsx @@ -72,7 +72,7 @@ export default async function DashboardLayout({
-
+
{children}
diff --git a/app/globals.css b/app/globals.css index 099fe53..371c538 100644 --- a/app/globals.css +++ b/app/globals.css @@ -4,6 +4,8 @@ @theme { --spacing-custom-height-1: 30rem; + --spacing-8xl: 88rem; /* 1408px */ + --spacing-9xl: 96rem; /* 1536px */ } :root { diff --git a/components/lancamentos/dialogs/fatura-warning-dialog.tsx b/components/lancamentos/dialogs/fatura-warning-dialog.tsx new file mode 100644 index 0000000..266ed2b --- /dev/null +++ b/components/lancamentos/dialogs/fatura-warning-dialog.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { MONTH_NAMES } from "@/lib/utils/period"; + +export type FaturaWarning = { + nextPeriod: string; + cardName: string; + isPaid: boolean; + isAfterClosing: boolean; + closingDay: string | null; + currentPeriod: string; +}; + +export function formatPeriodDisplay(period: string): string { + const [yearStr, monthStr] = period.split("-"); + const monthIndex = Number.parseInt(monthStr ?? "1", 10) - 1; + const monthName = MONTH_NAMES[monthIndex] ?? monthStr; + return `${monthName}/${yearStr}`; +} + +function buildWarningMessage(warning: FaturaWarning): string { + const currentDisplay = formatPeriodDisplay(warning.currentPeriod); + if (warning.isPaid && warning.isAfterClosing) { + return `A fatura do ${warning.cardName} em ${currentDisplay} já está paga e fechou no dia ${warning.closingDay}.`; + } + if (warning.isPaid) { + return `A fatura do ${warning.cardName} em ${currentDisplay} já está paga.`; + } + return `A fatura do ${warning.cardName} fechou no dia ${warning.closingDay}.`; +} + +interface FaturaWarningDialogProps { + warning: FaturaWarning | null; + onConfirm: (nextPeriod: string) => void; + onCancel: () => void; +} + +export function FaturaWarningDialog({ + warning, + onConfirm, + onCancel, +}: FaturaWarningDialogProps) { + if (!warning) return null; + + return ( + { + if (!open) onCancel(); + }} + > + + + Fatura indisponível + + {buildWarningMessage(warning)} Deseja registrá-lo em{" "} + + {formatPeriodDisplay(warning.nextPeriod)} + + ? + + + + onConfirm(warning.nextPeriod)}> + Mover para {formatPeriodDisplay(warning.nextPeriod)} + + + Manter em {formatPeriodDisplay(warning.currentPeriod)} + + + + + ); +} diff --git a/components/lancamentos/dialogs/lancamento-dialog/lancamento-dialog.tsx b/components/lancamentos/dialogs/lancamento-dialog/lancamento-dialog.tsx index 4da25b9..16861dc 100644 --- a/components/lancamentos/dialogs/lancamento-dialog/lancamento-dialog.tsx +++ b/components/lancamentos/dialogs/lancamento-dialog/lancamento-dialog.tsx @@ -3,11 +3,13 @@ import { useCallback, useEffect, useMemo, + useRef, useState, useTransition, } from "react"; import { toast } from "sonner"; import { + checkFaturaStatusAction, createLancamentoAction, updateLancamentoAction, } from "@/app/(dashboard)/lancamentos/actions"; @@ -30,6 +32,10 @@ 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"; @@ -90,6 +96,10 @@ export function LancamentoDialog({ const [periodDirty, setPeriodDirty] = useState(false); const [isPending, startTransition] = useTransition(); const [errorMessage, setErrorMessage] = useState(null); + const [faturaWarning, setFaturaWarning] = useState( + null, + ); + const lastCheckedRef = useRef(null); useEffect(() => { if (dialogOpen) { @@ -111,6 +121,10 @@ export function LancamentoDialog({ ); setErrorMessage(null); setPeriodDirty(false); + setFaturaWarning(null); + lastCheckedRef.current = null; + } else { + setFaturaWarning(null); } }, [ dialogOpen, @@ -126,6 +140,40 @@ 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( @@ -392,103 +440,114 @@ export function LancamentoDialog({ const disableCartaoSelect = Boolean(lockCartaoSelection && mode === "create"); return ( - - {trigger ? {trigger} : null} - - - {title} - {description} - + <> + + {trigger ? {trigger} : null} + + + {title} + {description} + -
- - - - - {!isUpdateMode ? ( - + - ) : null} - - - - - {showDueDate ? ( - - ) : null} - {!isUpdateMode ? ( - + ) : null} + + - ) : null} - + - {errorMessage ? ( -

{errorMessage}

- ) : null} + {showDueDate ? ( + + ) : null} - - - - - -
-
+ {!isUpdateMode ? ( + + ) : null} + + + + {errorMessage ? ( +

{errorMessage}

+ ) : null} + + + + + + +
+
+ + { + handleFieldChange("period", nextPeriod); + setFaturaWarning(null); + }} + onCancel={() => setFaturaWarning(null)} + /> + ); } diff --git a/components/lancamentos/dialogs/mass-add-dialog.tsx b/components/lancamentos/dialogs/mass-add-dialog.tsx index 34e4658..ee6c64c 100644 --- a/components/lancamentos/dialogs/mass-add-dialog.tsx +++ b/components/lancamentos/dialogs/mass-add-dialog.tsx @@ -1,8 +1,9 @@ "use client"; import { RiAddLine, RiDeleteBinLine } from "@remixicon/react"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, 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"; @@ -39,6 +40,10 @@ import { TransactionTypeSelectContent, } from "../select-items"; import { EstabelecimentoInput } from "../shared/estabelecimento-input"; +import { + type FaturaWarning, + FaturaWarningDialog, +} from "./fatura-warning-dialog"; interface MassAddDialogProps { open: boolean; @@ -119,6 +124,39 @@ export function MassAddDialog({ ? contaCartaoId.replace("cartao:", "") : undefined; + const [faturaWarning, setFaturaWarning] = useState( + null, + ); + const lastCheckedRef = useRef(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([ { @@ -238,381 +276,402 @@ export function MassAddDialog({ }; return ( - - - - Adicionar múltiplos lançamentos - - Configure os valores padrão e adicione várias transações de uma vez. - Todos os lançamentos adicionados aqui são{" "} - sempre à vista. - - + <> + + + + Adicionar múltiplos lançamentos + + Configure os valores padrão e adicione várias transações de uma + vez. Todos os lançamentos adicionados aqui são{" "} + sempre à vista. + + -
- {/* Fixed Fields Section */} -
-

Valores Padrão

-
- {/* Transaction Type */} -
- - -
- - {/* Payment Method */} -
- - -
- - {/* Period */} -
- - -
- - {/* Conta/Cartao */} -
- - + + + {transactionType && ( + )} - {cartaoOptions - .filter( - (option) => - !isLockedToCartao || - option.value === defaultCartaoId, - ) - .map((option) => ( + + + + + + + + + + + +
+ + {/* Payment Method */} +
+ + +
+ + {/* Period */} +
+ + +
+ + {/* Conta/Cartao */} +
+ + +
+
+
+ + + + {/* Transactions Section */} +
+

Lançamentos

+ +
+ {transactions.map((transaction, index) => ( +
+
+
+ + + updateTransaction( + transaction.id, + "purchaseDate", + value, + ) + } + placeholder="Data" + className="w-32 truncate" + required + /> +
+
+ + + updateTransaction(transaction.id, "name", value) + } + estabelecimentos={estabelecimentos} + required + /> +
+ +
+ + + updateTransaction(transaction.id, "amount", value) + } + required + /> +
+ +
+ + + + {transaction.pagadorId && + (() => { + const selectedOption = pagadorOptions.find( + (opt) => + opt.value === transaction.pagadorId, + ); + return selectedOption ? ( + + ) : null; + })()} + + + + {pagadorOptions.map((option) => ( + + + + ))} + + +
+ +
+ + +
+ + +
+
+ ))}
- + + + + +
+
- {/* Transactions Section */} -
-

Lançamentos

- -
- {transactions.map((transaction, index) => ( -
-
-
- - - updateTransaction( - transaction.id, - "purchaseDate", - value, - ) - } - placeholder="Data" - className="w-32 truncate" - required - /> -
-
- - - updateTransaction(transaction.id, "name", value) - } - estabelecimentos={estabelecimentos} - required - /> -
- -
- - - updateTransaction(transaction.id, "amount", value) - } - required - /> -
- -
- - -
- -
- - -
- - -
-
- ))} -
-
-
- - - - - - - + { + setPeriod(nextPeriod); + setFaturaWarning(null); + }} + onCancel={() => setFaturaWarning(null)} + /> + ); }