feat(lancamentos): separar botões Nova Receita e Nova Despesa

- Substituir botão único "Novo lançamento" por dois botões separados
- Adicionar ícones coloridos (verde para Receita, vermelho para Despesa)
- Adicionar suporte a defaultTransactionType no dialog
- Atualizar título e descrição do dialog conforme tipo selecionado
- Ocultar campo de tipo de transação quando tipo é pré-definido

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Felipe Coutinho
2026-01-20 13:43:00 +00:00
parent 9b08a8e020
commit 478bd0c267
5 changed files with 126 additions and 65 deletions

View File

@@ -24,10 +24,13 @@ export function CategorySection({
categoriaOptions,
categoriaGroups,
isUpdateMode,
hideTransactionType = false,
}: CategorySectionProps) {
const showTransactionTypeField = !isUpdateMode && !hideTransactionType;
return (
<div className="flex w-full flex-col gap-2 md:flex-row">
{!isUpdateMode ? (
{showTransactionTypeField ? (
<div className="w-full space-y-1 md:w-1/2">
<Label htmlFor="transactionType">Tipo de transação</Label>
<Select
@@ -45,7 +48,7 @@ export function CategorySection({
</SelectTrigger>
<SelectContent>
{LANCAMENTO_TRANSACTION_TYPES.filter(
(type) => type !== "Transferência"
(type) => type !== "Transferência",
).map((type) => (
<SelectItem key={type} value={type}>
<TransactionTypeSelectContent label={type} />
@@ -59,7 +62,7 @@ export function CategorySection({
<div
className={cn(
"space-y-1 w-full",
!isUpdateMode ? "md:w-1/2" : "md:w-full"
showTransactionTypeField ? "md:w-1/2" : "md:w-full",
)}
>
<Label htmlFor="categoria">Categoria</Label>
@@ -72,7 +75,7 @@ export function CategorySection({
{formState.categoriaId &&
(() => {
const selectedOption = categoriaOptions.find(
(opt) => opt.value === formState.categoriaId
(opt) => opt.value === formState.categoriaId,
);
return selectedOption ? (
<CategoriaSelectContent

View File

@@ -23,6 +23,7 @@ export interface LancamentoDialogProps {
lockCartaoSelection?: boolean;
lockPaymentMethod?: boolean;
isImporting?: boolean;
defaultTransactionType?: "Despesa" | "Receita";
onBulkEditRequest?: (data: {
id: string;
name: string;
@@ -41,7 +42,7 @@ export interface BaseFieldSectionProps {
formState: FormState;
onFieldChange: <Key extends keyof FormState>(
key: Key,
value: FormState[Key]
value: FormState[Key],
) => void;
}
@@ -56,6 +57,7 @@ export interface CategorySectionProps extends BaseFieldSectionProps {
options: SelectOption[];
}>;
isUpdateMode: boolean;
hideTransactionType?: boolean;
}
export interface SplitAndSettlementSectionProps extends BaseFieldSectionProps {

View File

@@ -63,12 +63,13 @@ export function LancamentoDialog({
lockCartaoSelection,
lockPaymentMethod,
isImporting = false,
defaultTransactionType,
onBulkEditRequest,
}: LancamentoDialogProps) {
const [dialogOpen, setDialogOpen] = useControlledState(
open,
false,
onOpenChange
onOpenChange,
);
const [formState, setFormState] = useState<FormState>(() =>
@@ -76,8 +77,9 @@ export function LancamentoDialog({
defaultCartaoId,
defaultPaymentMethod,
defaultPurchaseDate,
defaultTransactionType,
isImporting,
})
}),
);
const [periodDirty, setPeriodDirty] = useState(false);
const [isPending, startTransition] = useTransition();
@@ -94,9 +96,10 @@ export function LancamentoDialog({
defaultCartaoId,
defaultPaymentMethod,
defaultPurchaseDate,
defaultTransactionType,
isImporting,
}
)
},
),
);
setErrorMessage(null);
setPeriodDirty(false);
@@ -109,6 +112,7 @@ export function LancamentoDialog({
defaultCartaoId,
defaultPaymentMethod,
defaultPurchaseDate,
defaultTransactionType,
isImporting,
]);
@@ -116,18 +120,17 @@ export function LancamentoDialog({
const secondaryPagadorOptions = useMemo(
() => filterSecondaryPagadorOptions(splitPagadorOptions, primaryPagador),
[splitPagadorOptions, primaryPagador]
[splitPagadorOptions, primaryPagador],
);
const categoriaGroups = useMemo(() => {
const filtered = categoriaOptions.filter(
(option) =>
option.group?.toLowerCase() === formState.transactionType.toLowerCase()
option.group?.toLowerCase() === formState.transactionType.toLowerCase(),
);
return groupAndSortCategorias(filtered);
}, [categoriaOptions, formState.transactionType]);
const handleFieldChange = useCallback(
<Key extends keyof FormState>(key: Key, value: FormState[Key]) => {
if (key === "period") {
@@ -139,7 +142,7 @@ export function LancamentoDialog({
key,
value,
prev,
periodDirty
periodDirty,
);
return {
@@ -149,7 +152,7 @@ export function LancamentoDialog({
};
});
},
[periodDirty]
[periodDirty],
);
const handleSubmit = useCallback(
@@ -302,16 +305,24 @@ export function LancamentoDialog({
lancamento?.seriesId,
setDialogOpen,
onBulkEditRequest,
]
],
);
const isCopyMode = mode === "create" && Boolean(lancamento) && !isImporting;
const isImportMode = mode === "create" && Boolean(lancamento) && isImporting;
const title = mode === "create"
const isNewWithType =
mode === "create" && !lancamento && defaultTransactionType;
const title =
mode === "create"
? isImportMode
? "Importar para Minha Conta"
: isCopyMode
? "Copiar lançamento"
: isNewWithType
? defaultTransactionType === "Despesa"
? "Nova Despesa"
: "Nova Receita"
: "Novo lançamento"
: "Editar lançamento";
const description =
@@ -320,6 +331,8 @@ export function LancamentoDialog({
? "Importando lançamento de outro usuário. Ajuste a categoria, pagador e cartão/conta antes de salvar."
: isCopyMode
? "Os dados do lançamento foram copiados. Revise e ajuste conforme necessário antes de salvar."
: isNewWithType
? `Informe os dados abaixo para registrar ${defaultTransactionType === "Despesa" ? "uma nova despesa" : "uma nova receita"}.`
: "Informe os dados abaixo para registrar um novo lançamento."
: "Atualize as informações do lançamento selecionado.";
const submitLabel = mode === "create" ? "Salvar lançamento" : "Atualizar";
@@ -358,6 +371,7 @@ export function LancamentoDialog({
categoriaOptions={categoriaOptions}
categoriaGroups={categoriaGroups}
isUpdateMode={isUpdateMode}
hideTransactionType={Boolean(isNewWithType)}
/>
{!isUpdateMode ? (

View File

@@ -73,13 +73,13 @@ import {
import Image from "next/image";
import Link from "next/link";
import { useMemo, useState } from "react";
import { LancamentosExport } from "../lancamentos-export";
import { EstabelecimentoLogo } from "../shared/estabelecimento-logo";
import type {
ContaCartaoFilterOption,
LancamentoFilterOption,
LancamentoItem,
} from "../types";
import { LancamentosExport } from "../lancamentos-export";
import { LancamentosFilters } from "./lancamentos-filters";
const resolveLogoSrc = (logo: string | null) => {
@@ -325,7 +325,7 @@ const buildColumns = ({
isReceita
? "text-green-600 dark:text-green-400"
: "text-foreground",
isTransfer && "text-blue-700 dark:text-blue-500"
isTransfer && "text-blue-700 dark:text-blue-500",
)}
/>
);
@@ -468,7 +468,7 @@ const buildColumns = ({
href={href ?? "#"}
className={cn(
"flex items-center gap-2",
href ? "underline " : "pointer-events-none"
href ? "underline " : "pointer-events-none",
)}
aria-disabled={!href}
>
@@ -553,13 +553,15 @@ const buildColumns = ({
Editar
</DropdownMenuItem>
)}
{row.original.categoriaName !== "Pagamentos" && row.original.userId === currentUserId && (
{row.original.categoriaName !== "Pagamentos" &&
row.original.userId === currentUserId && (
<DropdownMenuItem onSelect={() => handleCopy(row.original)}>
<RiFileCopyLine className="size-4" />
Copiar
</DropdownMenuItem>
)}
{row.original.categoriaName !== "Pagamentos" && row.original.userId !== currentUserId && (
{row.original.categoriaName !== "Pagamentos" &&
row.original.userId !== currentUserId && (
<DropdownMenuItem onSelect={() => handleImport(row.original)}>
<RiFileCopyLine className="size-4" />
Importar para Minha Conta
@@ -628,7 +630,7 @@ type LancamentosTableProps = {
categoriaFilterOptions?: LancamentoFilterOption[];
contaCartaoFilterOptions?: ContaCartaoFilterOption[];
selectedPeriod?: string;
onCreate?: () => void;
onCreate?: (type: "Despesa" | "Receita") => void;
onMassAdd?: () => void;
onEdit?: (item: LancamentoItem) => void;
onCopy?: (item: LancamentoItem) => void;
@@ -704,7 +706,7 @@ export function LancamentosTable({
onViewAnticipationHistory,
isSettlementLoading,
showActions,
]
],
);
const table = useReactTable({
@@ -731,7 +733,7 @@ export function LancamentosTable({
const selectedCount = selectedRows.length;
const selectedTotal = selectedRows.reduce(
(total, row) => total + (row.original.amount ?? 0),
0
0,
);
// Check if there's any data from other users
@@ -763,10 +765,24 @@ export function LancamentosTable({
{onCreate || onMassAdd ? (
<div className="flex gap-2">
{onCreate ? (
<Button onClick={onCreate} className="w-full sm:w-auto">
<RiAddCircleLine className="size-4" />
Novo lançamento
<>
<Button
onClick={() => onCreate("Receita")}
variant="outline"
className="w-full sm:w-auto"
>
<RiAddCircleLine className="size-4 text-green-500" />
Nova Receita
</Button>
<Button
onClick={() => onCreate("Despesa")}
variant="outline"
className="w-full sm:w-auto"
>
<RiAddCircleLine className="size-4 text-red-500" />
Nova Despesa
</Button>
</>
) : null}
{onMassAdd ? (
<Tooltip>
@@ -813,7 +829,9 @@ export function LancamentosTable({
</div>
) : null}
{selectedCount > 0 && onBulkDelete && selectedRows.every(row => row.original.userId === currentUserId) ? (
{selectedCount > 0 &&
onBulkDelete &&
selectedRows.every((row) => row.original.userId === currentUserId) ? (
<div className="flex flex-wrap items-center gap-3 rounded-lg border bg-muted/50 px-4 py-2">
<div className="flex flex-col text-sm text-muted-foreground sm:flex-row sm:items-center sm:gap-2">
<span>
@@ -843,7 +861,9 @@ export function LancamentosTable({
</div>
) : null}
{selectedCount > 0 && onBulkImport && selectedRows.some(row => row.original.userId !== currentUserId) ? (
{selectedCount > 0 &&
onBulkImport &&
selectedRows.some((row) => row.original.userId !== currentUserId) ? (
<div className="flex flex-wrap items-center gap-3 rounded-lg border bg-muted/50 px-4 py-2">
<div className="flex flex-col text-sm text-muted-foreground sm:flex-row sm:items-center sm:gap-2">
<span>
@@ -891,7 +911,7 @@ export function LancamentosTable({
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
header.getContext(),
)}
</TableHead>
))}
@@ -905,7 +925,7 @@ export function LancamentosTable({
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
cell.getContext(),
)}
</TableCell>
))}

View File

@@ -3,7 +3,11 @@
*/
import type { LancamentoItem } from "@/components/lancamentos/types";
import { LANCAMENTO_CONDITIONS, LANCAMENTO_PAYMENT_METHODS, LANCAMENTO_TRANSACTION_TYPES } from "./constants";
import {
LANCAMENTO_CONDITIONS,
LANCAMENTO_PAYMENT_METHODS,
LANCAMENTO_TRANSACTION_TYPES,
} from "./constants";
import { derivePeriodFromDate } from "@/lib/utils/period";
import { getTodayDateString } from "@/lib/utils/date";
@@ -39,6 +43,7 @@ export type LancamentoFormOverrides = {
defaultCartaoId?: string | null;
defaultPaymentMethod?: string | null;
defaultPurchaseDate?: string | null;
defaultTransactionType?: "Despesa" | "Receita";
isImporting?: boolean;
};
@@ -49,11 +54,11 @@ export function buildLancamentoInitialState(
lancamento?: LancamentoItem,
defaultPagadorId?: string | null,
preferredPeriod?: string,
overrides?: LancamentoFormOverrides
overrides?: LancamentoFormOverrides,
): LancamentoFormState {
const purchaseDate = lancamento?.purchaseDate
? lancamento.purchaseDate.slice(0, 10)
: overrides?.defaultPurchaseDate ?? "";
: (overrides?.defaultPurchaseDate ?? "");
const paymentMethod =
lancamento?.paymentMethod ??
@@ -84,7 +89,11 @@ export function buildLancamentoInitialState(
let baseAmount = Math.abs(lancamento.amount);
// Se está importando e é parcelado, usar o valor total (parcela * quantidade)
if (isImporting && lancamento.condition === "Parcelado" && lancamento.installmentCount) {
if (
isImporting &&
lancamento.condition === "Parcelado" &&
lancamento.installmentCount
) {
baseAmount = baseAmount * lancamento.installmentCount;
}
@@ -99,7 +108,9 @@ export function buildLancamentoInitialState(
: fallbackPeriod,
name: lancamento?.name ?? "",
transactionType:
lancamento?.transactionType ?? LANCAMENTO_TRANSACTION_TYPES[0],
lancamento?.transactionType ??
overrides?.defaultTransactionType ??
LANCAMENTO_TRANSACTION_TYPES[0],
amount: amountValue,
condition: lancamento?.condition ?? LANCAMENTO_CONDITIONS[0],
paymentMethod,
@@ -109,12 +120,18 @@ export function buildLancamentoInitialState(
contaId:
paymentMethod === "Cartão de crédito"
? undefined
: isImporting ? undefined : (lancamento?.contaId ?? undefined),
: isImporting
? undefined
: (lancamento?.contaId ?? undefined),
cartaoId:
paymentMethod === "Cartão de crédito"
? isImporting ? (overrides?.defaultCartaoId ?? undefined) : (lancamento?.cartaoId ?? overrides?.defaultCartaoId ?? undefined)
? isImporting
? (overrides?.defaultCartaoId ?? undefined)
: (lancamento?.cartaoId ?? overrides?.defaultCartaoId ?? undefined)
: undefined,
categoriaId: isImporting ? undefined : (lancamento?.categoriaId ?? undefined),
categoriaId: isImporting
? undefined
: (lancamento?.categoriaId ?? undefined),
installmentCount: lancamento?.installmentCount
? String(lancamento.installmentCount)
: "",
@@ -127,7 +144,7 @@ export function buildLancamentoInitialState(
isSettled:
paymentMethod === "Cartão de crédito"
? null
: lancamento?.isSettled ?? true,
: (lancamento?.isSettled ?? true),
};
}
@@ -139,7 +156,7 @@ export function applyFieldDependencies(
key: keyof LancamentoFormState,
value: LancamentoFormState[keyof LancamentoFormState],
currentState: LancamentoFormState,
_periodDirty: boolean
_periodDirty: boolean,
): Partial<LancamentoFormState> {
const updates: Partial<LancamentoFormState> = {};
@@ -174,11 +191,15 @@ export function applyFieldDependencies(
if (value !== "Boleto") {
updates.dueDate = "";
updates.boletoPaymentDate = "";
} else if (currentState.isSettled || (updates.isSettled !== null && updates.isSettled !== undefined)) {
} else if (
currentState.isSettled ||
(updates.isSettled !== null && updates.isSettled !== undefined)
) {
// Set today's date for boleto payment if settled
const settled = updates.isSettled ?? currentState.isSettled;
if (settled) {
updates.boletoPaymentDate = currentState.boletoPaymentDate || getTodayDateString();
updates.boletoPaymentDate =
currentState.boletoPaymentDate || getTodayDateString();
}
}
}
@@ -199,7 +220,8 @@ export function applyFieldDependencies(
// When isSettled changes and payment method is Boleto
if (key === "isSettled" && currentState.paymentMethod === "Boleto") {
if (value === true) {
updates.boletoPaymentDate = currentState.boletoPaymentDate || getTodayDateString();
updates.boletoPaymentDate =
currentState.boletoPaymentDate || getTodayDateString();
} else if (value === false) {
updates.boletoPaymentDate = "";
}