feat: melhora os dialogs e detalhes de lançamentos

This commit is contained in:
Felipe Coutinho
2026-03-16 01:14:40 +00:00
parent 69df314db7
commit f4e7108119
7 changed files with 350 additions and 437 deletions

View File

@@ -7,17 +7,15 @@ import {
formatPeriod,
} from "@/features/transactions/formatting-helpers";
import { TransactionTypeBadge } from "@/shared/components/transaction-type-badge";
import { Badge } from "@/shared/components/ui/badge";
import { Button } from "@/shared/components/ui/button";
import {
CardContent,
CardDescription,
CardHeader,
} from "@/shared/components/ui/card";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/shared/components/ui/dialog";
import { Separator } from "@/shared/components/ui/separator";
@@ -30,12 +28,14 @@ interface TransactionDetailsDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
transaction: TransactionItem | null;
onEdit?: (transaction: TransactionItem) => void;
}
export function TransactionDetailsDialog({
open,
onOpenChange,
transaction,
onEdit,
}: TransactionDetailsDialogProps) {
if (!transaction) return null;
@@ -54,26 +54,26 @@ export function TransactionDetailsDialog({
? valorParcela * (totalParcelas - parcelaAtual)
: 0;
const isBoleto = transaction.paymentMethod === "Boleto";
const handleEdit = () => {
onOpenChange(false);
onEdit?.(transaction);
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="p-0 sm:max-w-xl sm:border-0 sm:p-2">
<div className="gap-2 space-y-4 py-4">
<CardHeader className="flex flex-row items-start border-b sm:border-b-0">
<div>
<DialogTitle className="group flex items-center gap-2 text-lg">
#{transaction.id}
</DialogTitle>
<CardDescription>
<DialogContent className="sm:max-w-xl">
<DialogHeader>
<DialogTitle>{transaction.name}</DialogTitle>
<DialogDescription>
{formatDate(transaction.purchaseDate)}
</CardDescription>
</div>
</CardHeader>
</DialogDescription>
</DialogHeader>
<CardContent className="text-sm">
<div className="max-h-[60vh] overflow-y-auto text-sm">
<div className="grid gap-3">
<ul className="grid gap-3">
<DetailRow label="Descrição" value={transaction.name} />
<DetailRow
label="Período"
value={formatPeriod(transaction.period)}
@@ -85,9 +85,7 @@ export function TransactionDetailsDialog({
</span>
<span className="flex items-center gap-1.5">
{getPaymentMethodIcon(transaction.paymentMethod)}
<span className="capitalize">
{transaction.paymentMethod}
</span>
<span>{transaction.paymentMethod}</span>
</span>
</li>
@@ -102,9 +100,7 @@ export function TransactionDetailsDialog({
/>
<li className="flex items-center justify-between">
<span className="text-muted-foreground">
Tipo de Transação
</span>
<span className="text-muted-foreground">Tipo de Transação</span>
<TransactionTypeBadge
kind={
transaction.categoriaName === "Saldo inicial"
@@ -121,22 +117,46 @@ export function TransactionDetailsDialog({
<li className="flex items-center justify-between">
<span className="text-muted-foreground">Responsável</span>
<span className="flex items-center gap-2 capitalize">
<span>{transaction.pagadorName}</span>
</span>
</li>
<li className="flex items-center justify-between">
<span className="text-muted-foreground">Status</span>
<Badge
variant="secondary"
className={
transaction.isSettled
? "text-success bg-success/10"
: "text-muted-foreground"
}
>
{transaction.isSettled ? "Pago" : "Pendente"}
</Badge>
</li>
{isBoleto && transaction.dueDate && (
<DetailRow
label="Status"
value={transaction.isSettled ? "Pago" : "Pendente"}
label="Vencimento"
value={formatDate(transaction.dueDate)}
/>
)}
{transaction.isDivided && (
<li className="flex items-center justify-between">
<span className="text-muted-foreground">Divisão</span>
<Badge variant="outline">Dividido</Badge>
</li>
)}
{transaction.note && (
<DetailRow label="Notas" value={transaction.note} />
<li className="flex flex-col gap-1">
<span className="text-muted-foreground">Notas</span>
<span className="text-foreground">{transaction.note}</span>
</li>
)}
</ul>
<ul className="mb-6 grid gap-3">
<ul className="mb-2 grid gap-3">
{isInstallment && (
<li className="mt-4">
<InstallmentTimeline
@@ -179,14 +199,18 @@ export function TransactionDetailsDialog({
</li>
</ul>
</div>
</div>
<DialogFooter>
{onEdit && !transaction.readonly && (
<Button variant="outline" onClick={handleEdit}>
Editar
</Button>
)}
<DialogClose asChild>
<Button type="button">Entendi</Button>
<Button type="button">Fechar</Button>
</DialogClose>
</DialogFooter>
</CardContent>
</div>
</DialogContent>
</Dialog>
);
@@ -201,7 +225,7 @@ function DetailRow({ label, value }: DetailRowProps) {
return (
<li className="flex items-center justify-between">
<span className="text-muted-foreground">{label}</span>
<span className="capitalize">{value}</span>
<span>{value}</span>
</li>
);
}

View File

@@ -16,14 +16,14 @@ export function BasicFieldsSection({
return (
<div className="space-y-3">
<div className="space-y-1">
<Label htmlFor="name">Estabelecimento</Label>
<Label htmlFor="name">Descrição</Label>
<EstabelecimentoInput
id="name"
value={formState.name}
onChange={(value) => onFieldChange("name", value)}
estabelecimentos={estabelecimentos}
placeholder="Ex.: Restaurante do Zé"
maxLength={20}
maxLength={60}
required
/>
</div>

View File

@@ -44,7 +44,7 @@ export function PayerSection({
>
<SelectTrigger
id="payer"
className={formState.isSplit ? "w-[55%]" : "w-full"}
className={formState.isSplit ? "min-w-0 flex-1" : "w-full"}
>
<SelectValue placeholder="Selecione">
{formState.payerId &&

View File

@@ -44,7 +44,7 @@ function InlinePeriodPicker({
<PopoverTrigger asChild>
<button
type="button"
className="text-xs text-primary underline-offset-2 hover:underline cursor-pointer lowercase"
className="cursor-pointer text-xs text-primary underline-offset-2 hover:underline lowercase"
>
{displayPeriod(period)}
</button>
@@ -82,7 +82,6 @@ export function PaymentMethodSection({
"Transferência bancária",
].includes(formState.paymentMethod);
// Filtrar contas apenas do tipo "Pré-Pago | VR/VA" quando forma de pagamento for "Pré-Pago | VR/VA"
const filteredContaOptions =
formState.paymentMethod === "Pré-Pago | VR/VA"
? accountOptions.filter(
@@ -90,14 +89,15 @@ export function PaymentMethodSection({
)
: accountOptions;
const hasSecondaryColumn = isCartaoSelected || showContaSelect;
return (
<>
{!isUpdateMode ? (
<div className="flex w-full flex-col gap-2 md:flex-row">
{!isUpdateMode ? (
<div
className={cn(
"space-y-1 w-full",
isCartaoSelected || showContaSelect ? "md:w-1/2" : "md:w-full",
"w-full space-y-1",
hasSecondaryColumn ? "md:w-1/2" : "md:w-full",
)}
>
<Label htmlFor="paymentMethod">Forma de pagamento</Label>
@@ -113,9 +113,7 @@ export function PaymentMethodSection({
>
<SelectValue placeholder="Selecione" className="w-full">
{formState.paymentMethod && (
<PaymentMethodSelectContent
label={formState.paymentMethod}
/>
<PaymentMethodSelectContent label={formState.paymentMethod} />
)}
</SelectValue>
</SelectTrigger>
@@ -128,9 +126,15 @@ export function PaymentMethodSection({
</SelectContent>
</Select>
</div>
) : null}
{isCartaoSelected ? (
<div className="space-y-1 w-full md:w-1/2">
<div
className={cn(
"w-full space-y-1",
!isUpdateMode ? "md:w-1/2" : "md:w-full",
)}
>
<Label htmlFor="cartao">Cartão</Label>
<Select
value={formState.cardId}
@@ -190,7 +194,7 @@ export function PaymentMethodSection({
{!isCartaoSelected && showContaSelect ? (
<div
className={cn(
"space-y-1 w-full",
"w-full space-y-1",
!isUpdateMode ? "md:w-1/2" : "md:w-full",
)}
>
@@ -239,121 +243,5 @@ export function PaymentMethodSection({
</div>
) : null}
</div>
) : null}
{isUpdateMode ? (
<div className="flex w-full flex-col gap-2 md:flex-row">
{isCartaoSelected ? (
<div
className={cn(
"space-y-1 w-full",
!isUpdateMode ? "md:w-1/2" : "md:w-full",
)}
>
<Label htmlFor="cartaoUpdate">Cartão</Label>
<Select
value={formState.cardId}
onValueChange={(value) => onFieldChange("cardId", value)}
>
<SelectTrigger id="cartaoUpdate" className="w-full">
<SelectValue placeholder="Selecione">
{formState.cardId &&
(() => {
const selectedOption = cardOptions.find(
(opt) => opt.value === formState.cardId,
);
return selectedOption ? (
<AccountCardSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={true}
/>
) : null;
})()}
</SelectValue>
</SelectTrigger>
<SelectContent>
{cardOptions.length === 0 ? (
<div className="px-2 py-6 text-center">
<p className="text-sm text-muted-foreground">
Nenhum cartão cadastrado
</p>
</div>
) : (
cardOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<AccountCardSelectContent
label={option.label}
logo={option.logo}
isCartao={true}
/>
</SelectItem>
))
)}
</SelectContent>
</Select>
{formState.cardId ? (
<InlinePeriodPicker
period={formState.period}
onPeriodChange={(value) => onFieldChange("period", value)}
/>
) : null}
</div>
) : null}
{!isCartaoSelected && showContaSelect ? (
<div
className={cn(
"space-y-1 w-full",
!isUpdateMode ? "md:w-1/2" : "md:w-full",
)}
>
<Label htmlFor="contaUpdate">Conta</Label>
<Select
value={formState.accountId}
onValueChange={(value) => onFieldChange("accountId", value)}
>
<SelectTrigger id="contaUpdate" className="w-full">
<SelectValue placeholder="Selecione">
{formState.accountId &&
(() => {
const selectedOption = filteredContaOptions.find(
(opt) => opt.value === formState.accountId,
);
return selectedOption ? (
<AccountCardSelectContent
label={selectedOption.label}
logo={selectedOption.logo}
isCartao={false}
/>
) : null;
})()}
</SelectValue>
</SelectTrigger>
<SelectContent>
{filteredContaOptions.length === 0 ? (
<div className="px-2 py-6 text-center">
<p className="text-sm text-muted-foreground">
Nenhuma conta cadastrada
</p>
</div>
) : (
filteredContaOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
<AccountCardSelectContent
label={option.label}
logo={option.logo}
isCartao={false}
/>
</SelectItem>
))
)}
</SelectContent>
</Select>
</div>
) : null}
</div>
) : null}
</>
);
}

View File

@@ -21,7 +21,7 @@ export function SplitAndSettlementSection({
<div>
<p className="text-sm text-foreground">Dividir lançamento</p>
<p className="text-xs text-muted-foreground">
Selecione para atribuir parte do valor a outro pagador.
Atribuir parte do valor a outro pagador.
</p>
</div>
<Checkbox
@@ -40,7 +40,7 @@ export function SplitAndSettlementSection({
<div>
<p className="text-sm text-foreground">Marcar como pago</p>
<p className="text-xs text-muted-foreground">
Indica que o lançamento foi pago ou recebido.
Indica que o valor foi pago.
</p>
</div>
<Checkbox

View File

@@ -418,10 +418,11 @@ export function TransactionDialog({
</DialogHeader>
<form
className="space-y-3 -mx-6 max-h-[80vh] overflow-y-auto px-6 pb-1"
className="flex flex-col gap-0"
onSubmit={handleSubmit}
noValidate
>
<div className="space-y-3 -mx-6 max-h-[70vh] overflow-y-auto px-6 pb-1">
<BasicFieldsSection
formState={formState}
onFieldChange={handleFieldChange}
@@ -439,13 +440,11 @@ export function TransactionDialog({
}
/>
{!isUpdateMode ? (
<SplitAndSettlementSection
formState={formState}
onFieldChange={handleFieldChange}
showSettledToggle={showSettledToggle}
/>
) : null}
<PayerSection
formState={formState}
@@ -473,37 +472,38 @@ export function TransactionDialog({
/>
) : null}
<Collapsible
defaultOpen={
formState.condition !== "À vista" || formState.note.length > 0
}
>
{isUpdateMode ? (
<NoteSection
formState={formState}
onFieldChange={handleFieldChange}
/>
) : (
<Collapsible defaultOpen={formState.condition !== "À vista"}>
<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">
<RiArrowDropDownLine className="text-primary size-4 transition-transform duration-200" />
Condições e anotações
</CollapsibleTrigger>
<CollapsibleContent className="space-y-3 pt-3">
{!isUpdateMode ? (
<ConditionSection
formState={formState}
onFieldChange={handleFieldChange}
showInstallments={showInstallments}
showRecurrence={showRecurrence}
/>
) : null}
<NoteSection
formState={formState}
onFieldChange={handleFieldChange}
/>
</CollapsibleContent>
</Collapsible>
)}
</div>
{errorMessage ? (
<p className="text-sm text-destructive">{errorMessage}</p>
<p className="mt-3 text-sm text-destructive">{errorMessage}</p>
) : null}
<DialogFooter>
<DialogFooter className="mt-4">
<Button
type="button"
variant="outline"

View File

@@ -506,6 +506,7 @@ export function TransactionsPage({
}
}}
transaction={detailsOpen ? selectedTransaction : null}
onEdit={handleEdit}
/>
<ConfirmActionDialog