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

View File

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

View File

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

View File

@@ -44,7 +44,7 @@ function InlinePeriodPicker({
<PopoverTrigger asChild> <PopoverTrigger asChild>
<button <button
type="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)} {displayPeriod(period)}
</button> </button>
@@ -82,7 +82,6 @@ export function PaymentMethodSection({
"Transferência bancária", "Transferência bancária",
].includes(formState.paymentMethod); ].includes(formState.paymentMethod);
// Filtrar contas apenas do tipo "Pré-Pago | VR/VA" quando forma de pagamento for "Pré-Pago | VR/VA"
const filteredContaOptions = const filteredContaOptions =
formState.paymentMethod === "Pré-Pago | VR/VA" formState.paymentMethod === "Pré-Pago | VR/VA"
? accountOptions.filter( ? accountOptions.filter(
@@ -90,14 +89,15 @@ export function PaymentMethodSection({
) )
: accountOptions; : accountOptions;
const hasSecondaryColumn = isCartaoSelected || showContaSelect;
return ( return (
<>
{!isUpdateMode ? (
<div className="flex w-full flex-col gap-2 md:flex-row"> <div className="flex w-full flex-col gap-2 md:flex-row">
{!isUpdateMode ? (
<div <div
className={cn( className={cn(
"space-y-1 w-full", "w-full space-y-1",
isCartaoSelected || showContaSelect ? "md:w-1/2" : "md:w-full", hasSecondaryColumn ? "md:w-1/2" : "md:w-full",
)} )}
> >
<Label htmlFor="paymentMethod">Forma de pagamento</Label> <Label htmlFor="paymentMethod">Forma de pagamento</Label>
@@ -113,9 +113,7 @@ export function PaymentMethodSection({
> >
<SelectValue placeholder="Selecione" className="w-full"> <SelectValue placeholder="Selecione" className="w-full">
{formState.paymentMethod && ( {formState.paymentMethod && (
<PaymentMethodSelectContent <PaymentMethodSelectContent label={formState.paymentMethod} />
label={formState.paymentMethod}
/>
)} )}
</SelectValue> </SelectValue>
</SelectTrigger> </SelectTrigger>
@@ -128,9 +126,15 @@ export function PaymentMethodSection({
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
) : null}
{isCartaoSelected ? ( {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> <Label htmlFor="cartao">Cartão</Label>
<Select <Select
value={formState.cardId} value={formState.cardId}
@@ -190,7 +194,7 @@ export function PaymentMethodSection({
{!isCartaoSelected && showContaSelect ? ( {!isCartaoSelected && showContaSelect ? (
<div <div
className={cn( className={cn(
"space-y-1 w-full", "w-full space-y-1",
!isUpdateMode ? "md:w-1/2" : "md:w-full", !isUpdateMode ? "md:w-1/2" : "md:w-full",
)} )}
> >
@@ -239,121 +243,5 @@ export function PaymentMethodSection({
</div> </div>
) : null} ) : null}
</div> </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> <div>
<p className="text-sm text-foreground">Dividir lançamento</p> <p className="text-sm text-foreground">Dividir lançamento</p>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Selecione para atribuir parte do valor a outro pagador. Atribuir parte do valor a outro pagador.
</p> </p>
</div> </div>
<Checkbox <Checkbox
@@ -40,7 +40,7 @@ export function SplitAndSettlementSection({
<div> <div>
<p className="text-sm text-foreground">Marcar como pago</p> <p className="text-sm text-foreground">Marcar como pago</p>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Indica que o lançamento foi pago ou recebido. Indica que o valor foi pago.
</p> </p>
</div> </div>
<Checkbox <Checkbox

View File

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

View File

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