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,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 ? (
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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 = "";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user