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:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,25 +305,35 @@ 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"
|
||||
? isImportMode
|
||||
? "Importar para Minha Conta"
|
||||
: isCopyMode
|
||||
? "Copiar lançamento"
|
||||
: "Novo lançamento"
|
||||
: "Editar lançamento";
|
||||
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 =
|
||||
mode === "create"
|
||||
? isImportMode
|
||||
? "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."
|
||||
: "Informe os dados abaixo para registrar um novo lançamento."
|
||||
? "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 ? (
|
||||
|
||||
@@ -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",
|
||||
)}
|
||||
/>
|
||||
);
|
||||
@@ -432,8 +432,8 @@ const buildColumns = ({
|
||||
const href = cartaoId
|
||||
? `/cartoes/${cartaoId}/fatura`
|
||||
: contaId
|
||||
? `/contas/${contaId}/extrato`
|
||||
: null;
|
||||
? `/contas/${contaId}/extrato`
|
||||
: null;
|
||||
const Icon = cartaoId ? RiBankCard2Line : contaId ? RiBankLine : null;
|
||||
const isOwnData = userId === currentUserId;
|
||||
|
||||
@@ -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,18 +553,20 @@ const buildColumns = ({
|
||||
Editar
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{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 && (
|
||||
<DropdownMenuItem onSelect={() => handleImport(row.original)}>
|
||||
<RiFileCopyLine className="size-4" />
|
||||
Importar para Minha Conta
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{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 && (
|
||||
<DropdownMenuItem onSelect={() => handleImport(row.original)}>
|
||||
<RiFileCopyLine className="size-4" />
|
||||
Importar para Minha Conta
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{row.original.userId === currentUserId && (
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
@@ -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>
|
||||
<>
|
||||
<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>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user