chore: snapshot sidebar layout antes de experimentar topbar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Felipe Coutinho
2026-02-22 19:57:42 +00:00
parent 82713d667f
commit f16140cb44
6 changed files with 703 additions and 444 deletions

View File

@@ -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<string[]> {
try {

View File

@@ -72,7 +72,7 @@ export default async function DashboardLayout({
<SiteHeader notificationsSnapshot={notificationsSnapshot} />
<div className="flex flex-1 flex-col pt-12 md:pt-0">
<div className="@container/main flex flex-1 flex-col gap-2">
<div className="flex flex-col gap-4 py-4 md:gap-6">
<div className="flex flex-col gap-4 py-4 md:gap-6 w-full max-w-8xl mx-auto">
{children}
</div>
</div>

View File

@@ -4,6 +4,8 @@
@theme {
--spacing-custom-height-1: 30rem;
--spacing-8xl: 88rem; /* 1408px */
--spacing-9xl: 96rem; /* 1536px */
}
:root {

View File

@@ -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 (
<AlertDialog
open
onOpenChange={(open) => {
if (!open) onCancel();
}}
>
<AlertDialogContent className="sm:max-w-md">
<AlertDialogHeader>
<AlertDialogTitle>Fatura indisponível</AlertDialogTitle>
<AlertDialogDescription>
{buildWarningMessage(warning)} Deseja registrá-lo em{" "}
<span className="font-medium text-foreground">
{formatPeriodDisplay(warning.nextPeriod)}
</span>
?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter className="flex-col gap-2 sm:flex-col">
<AlertDialogAction onClick={() => onConfirm(warning.nextPeriod)}>
Mover para {formatPeriodDisplay(warning.nextPeriod)}
</AlertDialogAction>
<AlertDialogCancel onClick={onCancel}>
Manter em {formatPeriodDisplay(warning.currentPeriod)}
</AlertDialogCancel>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}

View File

@@ -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<string | null>(null);
const [faturaWarning, setFaturaWarning] = useState<FaturaWarning | null>(
null,
);
const lastCheckedRef = useRef<string | null>(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 (
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
{trigger ? <DialogTrigger asChild>{trigger}</DialogTrigger> : null}
<DialogContent className="sm:max-w-xl p-6 sm:px-8">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
{trigger ? <DialogTrigger asChild>{trigger}</DialogTrigger> : null}
<DialogContent className="sm:max-w-xl p-6 sm:px-8">
<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"
onSubmit={handleSubmit}
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
<form
className="space-y-2 -mx-6 max-h-[80vh] overflow-y-auto px-6 pb-1"
onSubmit={handleSubmit}
noValidate
>
<BasicFieldsSection
formState={formState}
onFieldChange={handleFieldChange}
showSettledToggle={showSettledToggle}
estabelecimentos={estabelecimentos}
/>
) : 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
<CategorySection
formState={formState}
onFieldChange={handleFieldChange}
showPaymentDate={showPaymentDate}
categoriaOptions={categoriaOptions}
categoriaGroups={categoriaGroups}
isUpdateMode={isUpdateMode}
hideTransactionType={
Boolean(isNewWithType) && !forceShowTransactionType
}
/>
) : null}
{!isUpdateMode ? (
<ConditionSection
{!isUpdateMode ? (
<SplitAndSettlementSection
formState={formState}
onFieldChange={handleFieldChange}
showSettledToggle={showSettledToggle}
/>
) : null}
<PagadorSection
formState={formState}
onFieldChange={handleFieldChange}
showInstallments={showInstallments}
showRecurrence={showRecurrence}
pagadorOptions={pagadorOptions}
secondaryPagadorOptions={secondaryPagadorOptions}
totalAmount={totalAmount}
/>
) : null}
<NoteSection
formState={formState}
onFieldChange={handleFieldChange}
/>
<PaymentMethodSection
formState={formState}
onFieldChange={handleFieldChange}
contaOptions={contaOptions}
cartaoOptions={cartaoOptions}
isUpdateMode={isUpdateMode}
disablePaymentMethod={disablePaymentMethod}
disableCartaoSelect={disableCartaoSelect}
/>
{errorMessage ? (
<p className="text-sm text-destructive">{errorMessage}</p>
) : null}
{showDueDate ? (
<BoletoFieldsSection
formState={formState}
onFieldChange={handleFieldChange}
showPaymentDate={showPaymentDate}
/>
) : 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>
{!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)}
/>
</>
);
}

View File

@@ -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<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[]>([
{
@@ -238,381 +276,402 @@ export function MassAddDialog({
};
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{" "}
<span className="font-bold">sempre à vista</span>.
</DialogDescription>
</DialogHeader>
<>
<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{" "}
<span className="font-bold">sempre à vista</span>.
</DialogDescription>
</DialogHeader>
<div className="space-y-6">
{/* Fixed Fields Section */}
<div className="space-y-4">
<h3 className="text-sm font-semibold">Valores Padrão</h3>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
{/* Transaction Type */}
<div className="space-y-2">
<Label htmlFor="transaction-type">Tipo de Transação</Label>
<Select
value={transactionType}
onValueChange={setTransactionType}
>
<SelectTrigger id="transaction-type" className="w-full">
<SelectValue>
{transactionType && (
<TransactionTypeSelectContent label={transactionType} />
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="Despesa">
<TransactionTypeSelectContent label="Despesa" />
</SelectItem>
<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") {
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
value={period}
onChange={setPeriod}
className="w-full truncate"
/>
</div>
{/* Conta/Cartao */}
<div className="space-y-2">
<Label htmlFor="conta-cartao">
{isLockedToCartao ? "Cartão" : "Conta/Cartão"}
</Label>
<Select
value={contaCartaoId}
onValueChange={setContaCartaoId}
disabled={isLockedToCartao}
>
<SelectTrigger id="conta-cartao" className="w-full">
<SelectValue placeholder="Selecione">
{contaCartaoId &&
(() => {
if (isCartaoSelected) {
const selectedOption = cartaoOptions.find(
(opt) => opt.value === cartaoId,
);
return selectedOption ? (
<ContaCartaoSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={true}
/>
) : null;
} else {
const selectedOption = contaOptions.find(
(opt) => opt.value === contaId,
);
return selectedOption ? (
<ContaCartaoSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={false}
/>
) : null;
}
})()}
</SelectValue>
</SelectTrigger>
<SelectContent>
{cartaoOptions.length > 0 && (
<SelectGroup>
{!isLockedToCartao && (
<SelectLabel>Cartões</SelectLabel>
<div className="space-y-6">
{/* Fixed Fields Section */}
<div className="space-y-4">
<h3 className="text-sm font-semibold">Valores Padrão</h3>
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
{/* Transaction Type */}
<div className="space-y-2">
<Label htmlFor="transaction-type">Tipo de Transação</Label>
<Select
value={transactionType}
onValueChange={setTransactionType}
>
<SelectTrigger id="transaction-type" className="w-full">
<SelectValue>
{transactionType && (
<TransactionTypeSelectContent
label={transactionType}
/>
)}
{cartaoOptions
.filter(
(option) =>
!isLockedToCartao ||
option.value === defaultCartaoId,
)
.map((option) => (
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="Despesa">
<TransactionTypeSelectContent label="Despesa" />
</SelectItem>
<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") {
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
value={period}
onChange={setPeriod}
className="w-full truncate"
/>
</div>
{/* Conta/Cartao */}
<div className="space-y-2">
<Label htmlFor="conta-cartao">
{isLockedToCartao ? "Cartão" : "Conta/Cartão"}
</Label>
<Select
value={contaCartaoId}
onValueChange={setContaCartaoId}
disabled={isLockedToCartao}
>
<SelectTrigger id="conta-cartao" className="w-full">
<SelectValue placeholder="Selecione">
{contaCartaoId &&
(() => {
if (isCartaoSelected) {
const selectedOption = cartaoOptions.find(
(opt) => opt.value === cartaoId,
);
return selectedOption ? (
<ContaCartaoSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={true}
/>
) : null;
} else {
const selectedOption = contaOptions.find(
(opt) => opt.value === contaId,
);
return selectedOption ? (
<ContaCartaoSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={false}
/>
) : null;
}
})()}
</SelectValue>
</SelectTrigger>
<SelectContent>
{cartaoOptions.length > 0 && (
<SelectGroup>
{!isLockedToCartao && (
<SelectLabel>Cartões</SelectLabel>
)}
{cartaoOptions
.filter(
(option) =>
!isLockedToCartao ||
option.value === defaultCartaoId,
)
.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
key={option.value}
value={`cartao:${option.value}`}
value={`conta:${option.value}`}
>
<ContaCartaoSelectContent
label={option.label}
logo={option.logo}
isCartao={true}
isCartao={false}
/>
</SelectItem>
))}
</SelectGroup>
)}
{!isLockedToCartao && contaOptions.length > 0 && (
<SelectGroup>
<SelectLabel>Contas</SelectLabel>
{contaOptions.map((option) => (
<SelectItem
key={option.value}
value={`conta:${option.value}`}
</SelectGroup>
)}
</SelectContent>
</Select>
</div>
</div>
</div>
<Separator />
{/* 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"
>
<ContaCartaoSelectContent
label={option.label}
logo={option.logo}
isCartao={false}
/>
</SelectItem>
))}
</SelectGroup>
)}
</SelectContent>
</Select>
<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>
<Separator />
<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>
{/* 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">
{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>
<FaturaWarningDialog
warning={faturaWarning}
onConfirm={(nextPeriod) => {
setPeriod(nextPeriod);
setFaturaWarning(null);
}}
onCancel={() => setFaturaWarning(null)}
/>
</>
);
}