feat(finance): refina fluxos de transacoes e pagadores

This commit is contained in:
Felipe Coutinho
2026-03-09 17:13:44 +00:00
parent 69da27276c
commit ada1377640
58 changed files with 1288 additions and 1559 deletions

View File

@@ -2,7 +2,7 @@
import { format } from "date-fns";
import { ptBR } from "date-fns/locale";
import MoneyValues from "@/components/money-values";
import MoneyValues from "@/components/shared/money-values";
import { Badge } from "@/components/ui/badge";
import { Checkbox } from "@/components/ui/checkbox";
import {
@@ -15,6 +15,7 @@ import {
} from "@/components/ui/table";
import type { EligibleInstallment } from "@/lib/installments/anticipation-types";
import { formatCurrentInstallment } from "@/lib/installments/utils";
import { formatShortPeriodLabel } from "@/lib/utils/period";
import { cn } from "@/lib/utils/ui";
interface InstallmentSelectionTableProps {
@@ -43,12 +44,6 @@ export function InstallmentSelectionTable({
}
};
const formatPeriod = (period: string) => {
const [year, month] = period.split("-");
const date = new Date(Number(year), Number(month) - 1);
return format(date, "MMM/yyyy", { locale: ptBR });
};
const formatDate = (date: Date | null) => {
if (!date) return "—";
return format(date, "dd/MM/yyyy", { locale: ptBR });
@@ -116,7 +111,7 @@ export function InstallmentSelectionTable({
</Badge>
</TableCell>
<TableCell className="font-medium">
{formatPeriod(inst.period)}
{formatShortPeriodLabel(inst.period)}
</TableCell>
<TableCell className="text-muted-foreground">
{formatDate(inst.dueDate)}

View File

@@ -1,6 +1,6 @@
"use client";
import { useCallback, useMemo, useState, useTransition } from "react";
import { useMemo, useState, useTransition } from "react";
import { toast } from "sonner";
import { createLancamentoAction } from "@/app/(dashboard)/lancamentos/actions";
import { Button } from "@/components/ui/button";
@@ -58,20 +58,18 @@ export function BulkImportDialog({
const [contaId, setContaId] = useState<string | undefined>(undefined);
const [cartaoId, setCartaoId] = useState<string | undefined>(undefined);
const [isPending, startTransition] = useTransition();
type CreateLancamentoInput = Parameters<typeof createLancamentoAction>[0];
// Reset form when dialog opens/closes
const handleOpenChange = useCallback(
(newOpen: boolean) => {
if (!newOpen) {
setPagadorId(defaultPagadorId ?? undefined);
setCategoriaId(undefined);
setContaId(undefined);
setCartaoId(undefined);
}
onOpenChange(newOpen);
},
[onOpenChange, defaultPagadorId],
);
const handleOpenChange = (newOpen: boolean) => {
if (!newOpen) {
setPagadorId(defaultPagadorId ?? undefined);
setCategoriaId(undefined);
setContaId(undefined);
setCartaoId(undefined);
}
onOpenChange(newOpen);
};
const categoriaGroups = useMemo(() => {
// Get unique transaction types from items
@@ -88,111 +86,100 @@ export function BulkImportDialog({
return groupAndSortCategorias(filtered);
}, [categoriaOptions, items]);
const handleSubmit = useCallback(
async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!pagadorId) {
toast.error("Selecione o pagador.");
return;
}
if (!pagadorId) {
toast.error("Selecione o pagador.");
return;
}
if (!categoriaId) {
toast.error("Selecione a categoria.");
return;
}
if (!categoriaId) {
toast.error("Selecione a categoria.");
return;
}
startTransition(async () => {
let successCount = 0;
let errorCount = 0;
startTransition(async () => {
let successCount = 0;
let errorCount = 0;
for (const item of items) {
const sanitizedAmount = Math.abs(item.amount);
for (const item of items) {
const sanitizedAmount = Math.abs(item.amount);
// Determine payment method based on original item
const isCredit = item.paymentMethod === "Cartão de crédito";
// Determine payment method based on original item
const isCredit = item.paymentMethod === "Cartão de crédito";
// Validate payment method fields
if (isCredit && !cartaoId) {
toast.error("Selecione um cartão de crédito.");
return;
}
if (!isCredit && !contaId) {
toast.error("Selecione uma conta.");
return;
}
const payload = {
purchaseDate: item.purchaseDate,
period: item.period,
name: item.name,
transactionType: item.transactionType as
| "Despesa"
| "Receita"
| "Transferência",
amount: sanitizedAmount,
condition: item.condition as "À vista" | "Parcelado" | "Recorrente",
paymentMethod: item.paymentMethod as
| "Cartão de crédito"
| "Cartão de débito"
| "Pix"
| "Dinheiro"
| "Boleto"
| "Pré-Pago | VR/VA"
| "Transferência bancária",
pagadorId,
secondaryPagadorId: undefined,
isSplit: false,
contaId: isCredit ? undefined : contaId,
cartaoId: isCredit ? cartaoId : undefined,
categoriaId,
note: item.note || undefined,
isSettled: isCredit ? null : Boolean(item.isSettled),
installmentCount:
item.condition === "Parcelado" && item.installmentCount
? Number(item.installmentCount)
: undefined,
recurrenceCount:
item.condition === "Recorrente" && item.recurrenceCount
? Number(item.recurrenceCount)
: undefined,
dueDate:
item.paymentMethod === "Boleto" && item.dueDate
? item.dueDate
: undefined,
};
const result = await createLancamentoAction(payload);
if (result.success) {
successCount++;
} else {
errorCount++;
console.error(`Failed to import ${item.name}:`, result.error);
}
// Validate payment method fields
if (isCredit && !cartaoId) {
toast.error("Selecione um cartão de crédito.");
return;
}
if (errorCount === 0) {
toast.success(
`${successCount} ${
successCount === 1
? "lançamento importado"
: "lançamentos importados"
} com sucesso!`,
);
handleOpenChange(false);
} else if (successCount > 0) {
toast.warning(
`${successCount} importados, ${errorCount} falharam. Verifique o console para detalhes.`,
);
if (!isCredit && !contaId) {
toast.error("Selecione uma conta.");
return;
}
const payload: CreateLancamentoInput = {
purchaseDate: item.purchaseDate,
period: item.period,
name: item.name,
transactionType:
item.transactionType as CreateLancamentoInput["transactionType"],
amount: sanitizedAmount,
condition: item.condition as CreateLancamentoInput["condition"],
paymentMethod:
item.paymentMethod as CreateLancamentoInput["paymentMethod"],
pagadorId: pagadorId ?? null,
secondaryPagadorId: undefined,
isSplit: false,
contaId: isCredit ? null : (contaId ?? null),
cartaoId: isCredit ? (cartaoId ?? null) : null,
categoriaId: categoriaId ?? null,
note: item.note ?? null,
isSettled: isCredit ? null : Boolean(item.isSettled),
installmentCount:
item.condition === "Parcelado" && item.installmentCount
? Number(item.installmentCount)
: undefined,
recurrenceCount:
item.condition === "Recorrente" && item.recurrenceCount
? Number(item.recurrenceCount)
: undefined,
dueDate:
item.paymentMethod === "Boleto" && item.dueDate
? item.dueDate
: undefined,
};
const result = await createLancamentoAction(payload);
if (result.success) {
successCount++;
} else {
toast.error("Falha ao importar lançamentos. Verifique o console.");
errorCount++;
console.error(`Failed to import ${item.name}:`, result.error);
}
});
},
[items, pagadorId, categoriaId, contaId, cartaoId, handleOpenChange],
);
}
if (errorCount === 0) {
toast.success(
`${successCount} ${
successCount === 1
? "lançamento importado"
: "lançamentos importados"
} com sucesso!`,
);
handleOpenChange(false);
} else if (successCount > 0) {
toast.warning(
`${successCount} importados, ${errorCount} falharam. Verifique o console para detalhes.`,
);
} else {
toast.error("Falha ao importar lançamentos. Verifique o console.");
}
});
};
const itemCount = items.length;
const hasCredit = items.some(

View File

@@ -1,6 +1,5 @@
"use client";
import { useCallback, useMemo } from "react";
import { Label } from "@/components/ui/label";
import {
Select,
@@ -10,73 +9,48 @@ import {
SelectValue,
} from "@/components/ui/select";
import { LANCAMENTO_CONDITIONS } from "@/lib/lancamentos/constants";
import { formatCurrency } from "@/lib/utils/currency";
import { cn } from "@/lib/utils/ui";
import { ConditionSelectContent } from "../../select-items";
import type { ConditionSectionProps } from "./lancamento-dialog-types";
function formatCurrency(value: number): string {
return value.toLocaleString("pt-BR", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
}
export function ConditionSection({
formState,
onFieldChange,
showInstallments,
showRecurrence,
}: ConditionSectionProps) {
const amount = useMemo(() => {
const value = Number(formState.amount);
return Number.isNaN(value) || value <= 0 ? null : value;
}, [formState.amount]);
const parsedAmount = Number(formState.amount);
const amount =
Number.isNaN(parsedAmount) || parsedAmount <= 0 ? null : parsedAmount;
const getInstallmentLabel = useCallback(
(count: number) => {
if (amount) {
const installmentValue = amount / count;
return `${count}x de R$ ${formatCurrency(installmentValue)}`;
}
return `${count}x`;
},
[amount],
);
const getInstallmentLabel = (count: number) => {
if (amount) {
const installmentValue = amount / count;
return `${count}x de R$ ${formatCurrency(installmentValue)}`;
}
const _getRecurrenceLabel = (count: number) => {
return `${count} meses`;
return `${count}x`;
};
const installmentSummary = useMemo(() => {
if (!showInstallments || !formState.installmentCount || !amount) {
return null;
}
const installmentCount = Number(formState.installmentCount);
const installmentSummary =
showInstallments &&
formState.installmentCount &&
amount &&
!Number.isNaN(installmentCount) &&
installmentCount > 0
? getInstallmentLabel(installmentCount)
: null;
const count = Number(formState.installmentCount);
if (Number.isNaN(count) || count <= 0) {
return null;
}
return getInstallmentLabel(count);
}, [
showInstallments,
formState.installmentCount,
amount,
getInstallmentLabel,
]);
const recurrenceSummary = useMemo(() => {
if (!showRecurrence || !formState.recurrenceCount) {
return null;
}
const count = Number(formState.recurrenceCount);
if (Number.isNaN(count) || count <= 0) {
return null;
}
return `Por ${count} meses`;
}, [showRecurrence, formState.recurrenceCount]);
const recurrenceCount = Number(formState.recurrenceCount);
const recurrenceSummary =
showRecurrence &&
formState.recurrenceCount &&
!Number.isNaN(recurrenceCount) &&
recurrenceCount > 0
? `Por ${recurrenceCount} meses`
: null;
return (
<div className="flex w-full flex-col gap-2 md:flex-row">

View File

@@ -1,6 +1,5 @@
"use client";
import { useCallback } from "react";
import { CurrencyInput } from "@/components/ui/currency-input";
import { Label } from "@/components/ui/label";
import {
@@ -20,25 +19,19 @@ export function PagadorSection({
secondaryPagadorOptions,
totalAmount,
}: PagadorSectionProps) {
const handlePrimaryAmountChange = useCallback(
(value: string) => {
onFieldChange("primarySplitAmount", value);
const numericValue = Number.parseFloat(value) || 0;
const remaining = Math.max(0, totalAmount - numericValue);
onFieldChange("secondarySplitAmount", remaining.toFixed(2));
},
[totalAmount, onFieldChange],
);
const handlePrimaryAmountChange = (value: string) => {
onFieldChange("primarySplitAmount", value);
const numericValue = Number.parseFloat(value) || 0;
const remaining = Math.max(0, totalAmount - numericValue);
onFieldChange("secondarySplitAmount", remaining.toFixed(2));
};
const handleSecondaryAmountChange = useCallback(
(value: string) => {
onFieldChange("secondarySplitAmount", value);
const numericValue = Number.parseFloat(value) || 0;
const remaining = Math.max(0, totalAmount - numericValue);
onFieldChange("primarySplitAmount", remaining.toFixed(2));
},
[totalAmount, onFieldChange],
);
const handleSecondaryAmountChange = (value: string) => {
onFieldChange("secondarySplitAmount", value);
const numericValue = Number.parseFloat(value) || 0;
const remaining = Math.max(0, totalAmount - numericValue);
onFieldChange("primarySplitAmount", remaining.toFixed(2));
};
return (
<div className="flex w-full flex-col gap-2 md:flex-row">

View File

@@ -16,7 +16,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { LANCAMENTO_PAYMENT_METHODS } from "@/lib/lancamentos/constants";
import { displayPeriod } from "@/lib/utils/period";
import { dateToPeriod, displayPeriod, periodToDate } from "@/lib/utils/period";
import { cn } from "@/lib/utils/ui";
import {
ContaCartaoSelectContent,
@@ -24,17 +24,6 @@ import {
} from "../../select-items";
import type { PaymentMethodSectionProps } from "./lancamento-dialog-types";
function periodToDate(period: string): Date {
const [year, month] = period.split("-").map(Number);
return new Date(year, month - 1, 1);
}
function dateToPeriod(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
return `${year}-${month}`;
}
function InlinePeriodPicker({
period,
onPeriodChange,

View File

@@ -33,9 +33,12 @@ import {
import { Separator } from "@/components/ui/separator";
import { Spinner } from "@/components/ui/spinner";
import { groupAndSortCategorias } from "@/lib/lancamentos/categoria-helpers";
import { LANCAMENTO_PAYMENT_METHODS } from "@/lib/lancamentos/constants";
import {
LANCAMENTO_PAYMENT_METHODS,
type LANCAMENTO_TRANSACTION_TYPES,
} from "@/lib/lancamentos/constants";
import { getTodayDateString } from "@/lib/utils/date";
import { displayPeriod } from "@/lib/utils/period";
import { dateToPeriod, displayPeriod, periodToDate } from "@/lib/utils/period";
import {
CategoriaSelectContent,
ContaCartaoSelectContent,
@@ -50,17 +53,8 @@ import type { SelectOption } from "../types";
const MASS_ADD_PAYMENT_METHODS = LANCAMENTO_PAYMENT_METHODS.filter(
(m) => m !== "Boleto",
);
function periodToDate(period: string): Date {
const [year, month] = period.split("-").map(Number);
return new Date(year, month - 1, 1);
}
function dateToPeriod(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
return `${year}-${month}`;
}
type MassAddTransactionType = (typeof LANCAMENTO_TRANSACTION_TYPES)[number];
type MassAddPaymentMethod = (typeof LANCAMENTO_PAYMENT_METHODS)[number];
function InlinePeriodPicker({
period,
@@ -111,23 +105,9 @@ interface MassAddDialogProps {
defaultCartaoId?: string | null;
}
export interface MassAddFormData {
fixedFields: {
transactionType?: string;
paymentMethod?: string;
condition?: string;
period?: string;
contaId?: string;
cartaoId?: string;
};
transactions: Array<{
purchaseDate: string;
name: string;
amount: string;
categoriaId?: string;
pagadorId?: string;
}>;
}
export type MassAddFormData = Parameters<
typeof import("@/app/(dashboard)/lancamentos/actions").createMassLancamentosAction
>[0];
interface TransactionRow {
id: string;
@@ -154,8 +134,9 @@ export function MassAddDialog({
const [loading, setLoading] = useState(false);
// Fixed fields state (sempre ativos, sem checkboxes)
const [transactionType, setTransactionType] = useState<string>("Despesa");
const [paymentMethod, setPaymentMethod] = useState<string>(
const [transactionType, setTransactionType] =
useState<MassAddTransactionType>("Despesa");
const [paymentMethod, setPaymentMethod] = useState<MassAddPaymentMethod>(
LANCAMENTO_PAYMENT_METHODS[0],
);
const [period, setPeriod] = useState<string>(selectedPeriod);
@@ -257,7 +238,7 @@ export function MassAddDialog({
transactions: transactions.map((t) => ({
purchaseDate: t.purchaseDate,
name: t.name.trim(),
amount: t.amount.trim(),
amount: Number(t.amount.trim()),
categoriaId: t.categoriaId,
pagadorId: t.pagadorId,
})),
@@ -312,7 +293,9 @@ export function MassAddDialog({
<Label htmlFor="transaction-type">Tipo de Transação</Label>
<Select
value={transactionType}
onValueChange={setTransactionType}
onValueChange={(value) =>
setTransactionType(value as MassAddTransactionType)
}
>
<SelectTrigger id="transaction-type" className="w-full">
<SelectValue>
@@ -338,7 +321,7 @@ export function MassAddDialog({
<Select
value={paymentMethod}
onValueChange={(value) => {
setPaymentMethod(value);
setPaymentMethod(value as MassAddPaymentMethod);
// Reset conta/cartao when changing payment method
if (value === "Cartão de crédito") {
setContaId(undefined);

View File

@@ -19,10 +19,12 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { formatCurrency } from "@/lib/lancamentos/formatting-helpers";
import { formatDateOnly, formatDateTime } from "@/lib/utils/date";
import {
getPrimaryPdfColor,
loadExportLogoDataUrl,
} from "@/lib/utils/export-branding";
import { displayPeriod } from "@/lib/utils/period";
import type { LancamentoItem } from "./types";
interface LancamentosExportProps {
@@ -41,12 +43,13 @@ export function LancamentosExport({
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString("pt-BR", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
return (
formatDateOnly(dateString, {
day: "2-digit",
month: "2-digit",
year: "numeric",
}) ?? dateString
);
};
const getContaCartaoName = (lancamento: LancamentoItem) => {
@@ -190,8 +193,8 @@ export function LancamentosExport({
const doc = new jsPDF({ orientation: "landscape" });
const primaryColor = getPrimaryPdfColor();
const [smallLogoDataUrl, textLogoDataUrl] = await Promise.all([
loadExportLogoDataUrl("/logo_small.png"),
loadExportLogoDataUrl("/logo_text.png"),
loadExportLogoDataUrl("/imagens/logo_small.png"),
loadExportLogoDataUrl("/imagens/logo_text.png"),
]);
let brandingEndX = 14;
@@ -212,28 +215,15 @@ export function LancamentosExport({
doc.text("Lançamentos", titleX, 15);
doc.setFontSize(10);
const periodParts = period.split("-");
const monthNames = [
"Janeiro",
"Fevereiro",
"Março",
"Abril",
"Maio",
"Junho",
"Julho",
"Agosto",
"Setembro",
"Outubro",
"Novembro",
"Dezembro",
];
const formattedPeriod =
periodParts.length === 2
? `${monthNames[Number.parseInt(periodParts[1], 10) - 1]}/${periodParts[0]}`
: period;
doc.text(`Período: ${formattedPeriod}`, titleX, 22);
doc.text(`Período: ${displayPeriod(period)}`, titleX, 22);
doc.text(
`Gerado em: ${new Date().toLocaleDateString("pt-BR")}`,
`Gerado em: ${
formatDateTime(new Date(), {
day: "2-digit",
month: "2-digit",
year: "numeric",
}) ?? "—"
}`,
titleX,
27,
);

View File

@@ -1,6 +1,6 @@
"use client";
import { useCallback, useState } from "react";
import { useState } from "react";
import { toast } from "sonner";
import {
createMassLancamentosAction,
@@ -10,7 +10,7 @@ import {
toggleLancamentoSettlementAction,
updateLancamentoBulkAction,
} from "@/app/(dashboard)/lancamentos/actions";
import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
import { ConfirmActionDialog } from "@/components/shared/confirm-action-dialog";
import { AnticipateInstallmentsDialog } from "../dialogs/anticipate-installments-dialog/anticipate-installments-dialog";
import { AnticipationHistoryDialog } from "../dialogs/anticipate-installments-dialog/anticipation-history-dialog";
@@ -139,7 +139,7 @@ export function LancamentosPage({
LancamentoItem[]
>([]);
const handleToggleSettlement = useCallback(async (item: LancamentoItem) => {
const handleToggleSettlement = async (item: LancamentoItem) => {
if (item.paymentMethod === "Cartão de crédito") {
toast.info(
"Pagamentos com cartão são conciliados automaticamente. Ajuste pelo cartão.",
@@ -182,9 +182,9 @@ export function LancamentosPage({
} finally {
setSettlementLoadingId(null);
}
}, []);
};
const handleDelete = useCallback(async () => {
const handleDelete = async () => {
if (!lancamentoToDelete) {
return;
}
@@ -200,91 +200,82 @@ export function LancamentosPage({
toast.success(result.message);
setDeleteOpen(false);
}, [lancamentoToDelete]);
};
const handleBulkDelete = useCallback(
async (scope: BulkActionScope) => {
if (!pendingDeleteData) {
return;
}
const handleBulkDelete = async (scope: BulkActionScope) => {
if (!pendingDeleteData) {
return;
}
const result = await deleteLancamentoBulkAction({
id: pendingDeleteData.id,
scope,
});
const result = await deleteLancamentoBulkAction({
id: pendingDeleteData.id,
scope,
});
if (!result.success) {
toast.error(result.error);
throw new Error(result.error);
}
if (!result.success) {
toast.error(result.error);
throw new Error(result.error);
}
toast.success(result.message);
setBulkDeleteOpen(false);
setPendingDeleteData(null);
},
[pendingDeleteData],
);
toast.success(result.message);
setBulkDeleteOpen(false);
setPendingDeleteData(null);
};
const handleBulkEditRequest = useCallback(
(data: {
id: string;
name: string;
categoriaId: string | undefined;
note: string;
pagadorId: string | undefined;
contaId: string | undefined;
cartaoId: string | undefined;
amount: number;
dueDate: string | null;
boletoPaymentDate: string | null;
}) => {
if (!selectedLancamento) {
return;
}
const handleBulkEditRequest = (data: {
id: string;
name: string;
categoriaId: string | undefined;
note: string;
pagadorId: string | undefined;
contaId: string | undefined;
cartaoId: string | undefined;
amount: number;
dueDate: string | null;
boletoPaymentDate: string | null;
}) => {
if (!selectedLancamento) {
return;
}
setPendingEditData({
...data,
lancamento: selectedLancamento,
});
setEditOpen(false);
setBulkEditOpen(true);
},
[selectedLancamento],
);
setPendingEditData({
...data,
lancamento: selectedLancamento,
});
setEditOpen(false);
setBulkEditOpen(true);
};
const handleBulkEdit = useCallback(
async (scope: BulkActionScope) => {
if (!pendingEditData) {
return;
}
const handleBulkEdit = async (scope: BulkActionScope) => {
if (!pendingEditData) {
return;
}
const result = await updateLancamentoBulkAction({
id: pendingEditData.id,
scope,
name: pendingEditData.name,
categoriaId: pendingEditData.categoriaId,
note: pendingEditData.note,
pagadorId: pendingEditData.pagadorId,
contaId: pendingEditData.contaId,
cartaoId: pendingEditData.cartaoId,
amount: pendingEditData.amount,
dueDate: pendingEditData.dueDate,
boletoPaymentDate: pendingEditData.boletoPaymentDate,
});
const result = await updateLancamentoBulkAction({
id: pendingEditData.id,
scope,
name: pendingEditData.name,
categoriaId: pendingEditData.categoriaId,
note: pendingEditData.note,
pagadorId: pendingEditData.pagadorId,
contaId: pendingEditData.contaId,
cartaoId: pendingEditData.cartaoId,
amount: pendingEditData.amount,
dueDate: pendingEditData.dueDate,
boletoPaymentDate: pendingEditData.boletoPaymentDate,
});
if (!result.success) {
toast.error(result.error);
throw new Error(result.error);
}
if (!result.success) {
toast.error(result.error);
throw new Error(result.error);
}
toast.success(result.message);
setBulkEditOpen(false);
setPendingEditData(null);
},
[pendingEditData],
);
toast.success(result.message);
setBulkEditOpen(false);
setPendingEditData(null);
};
const handleMassAddSubmit = useCallback(async (data: MassAddFormData) => {
const handleMassAddSubmit = async (data: MassAddFormData) => {
const result = await createMassLancamentosAction(data);
if (!result.success) {
@@ -293,9 +284,9 @@ export function LancamentosPage({
}
toast.success(result.message);
}, []);
};
const handleMultipleBulkDelete = useCallback((items: LancamentoItem[]) => {
const handleMultipleBulkDelete = (items: LancamentoItem[]) => {
// Se todos os selecionados são da mesma série (parcelado/recorrente), abrir dialog de escopo
const withSeries = items.filter((i) => i.seriesId);
const sameSeries =
@@ -309,9 +300,9 @@ export function LancamentosPage({
}
setPendingMultipleDeleteData(items);
setMultipleBulkDeleteOpen(true);
}, []);
};
const confirmMultipleBulkDelete = useCallback(async () => {
const confirmMultipleBulkDelete = async () => {
if (pendingMultipleDeleteData.length === 0) {
return;
}
@@ -327,42 +318,42 @@ export function LancamentosPage({
toast.success(result.message);
setMultipleBulkDeleteOpen(false);
setPendingMultipleDeleteData([]);
}, [pendingMultipleDeleteData]);
};
const [transactionTypeForCreate, setTransactionTypeForCreate] = useState<
"Despesa" | "Receita" | null
>(null);
const handleCreate = useCallback((type: "Despesa" | "Receita") => {
const handleCreate = (type: "Despesa" | "Receita") => {
setTransactionTypeForCreate(type);
setCreateOpen(true);
}, []);
};
const handleMassAdd = useCallback(() => {
const handleMassAdd = () => {
setMassAddOpen(true);
}, []);
};
const handleEdit = useCallback((item: LancamentoItem) => {
const handleEdit = (item: LancamentoItem) => {
setSelectedLancamento(item);
setEditOpen(true);
}, []);
};
const handleCopy = useCallback((item: LancamentoItem) => {
const handleCopy = (item: LancamentoItem) => {
setLancamentoToCopy(item);
setCopyOpen(true);
}, []);
};
const handleImport = useCallback((item: LancamentoItem) => {
const handleImport = (item: LancamentoItem) => {
setLancamentoToImport(item);
setImportOpen(true);
}, []);
};
const handleBulkImport = useCallback((items: LancamentoItem[]) => {
const handleBulkImport = (items: LancamentoItem[]) => {
setLancamentosToImport(items);
setBulkImportOpen(true);
}, []);
};
const handleConfirmDelete = useCallback((item: LancamentoItem) => {
const handleConfirmDelete = (item: LancamentoItem) => {
if (item.seriesId) {
setPendingDeleteData(item);
setBulkDeleteOpen(true);
@@ -370,22 +361,22 @@ export function LancamentosPage({
setLancamentoToDelete(item);
setDeleteOpen(true);
}
}, []);
};
const handleViewDetails = useCallback((item: LancamentoItem) => {
const handleViewDetails = (item: LancamentoItem) => {
setSelectedLancamento(item);
setDetailsOpen(true);
}, []);
};
const handleAnticipate = useCallback((item: LancamentoItem) => {
const handleAnticipate = (item: LancamentoItem) => {
setSelectedForAnticipation(item);
setAnticipateOpen(true);
}, []);
};
const handleViewAnticipationHistory = useCallback((item: LancamentoItem) => {
const handleViewAnticipationHistory = (item: LancamentoItem) => {
setSelectedForAnticipation(item);
setAnticipationHistoryOpen(true);
}, []);
};
return (
<>

View File

@@ -3,8 +3,9 @@
import { RiBankCard2Line, RiBankLine } from "@remixicon/react";
import Image from "next/image";
import { CategoryIcon } from "@/components/categorias/category-icon";
import DotIcon from "@/components/dot-icon";
import StatusDot from "@/components/shared/status-dot";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { resolveLogoSrc } from "@/lib/logo";
import { getAvatarSrc } from "@/lib/pagadores/utils";
import { getConditionIcon, getPaymentMethodIcon } from "@/lib/utils/icons";
@@ -56,7 +57,7 @@ export function TransactionTypeSelectContent({ label }: { label: string }) {
return (
<span className="flex items-center gap-2">
<DotIcon color={colorMap[label]} />
<StatusDot color={colorMap[label]} />
<span>{label}</span>
</span>
);
@@ -89,15 +90,6 @@ export function ContaCartaoSelectContent({
logo,
isCartao,
}: SelectItemContentProps & { isCartao?: boolean }) {
const resolveLogoSrc = (logoPath: string | null) => {
if (!logoPath) {
return null;
}
const fileName = logoPath.split("/").filter(Boolean).pop() ?? logoPath;
return `/logos/${fileName}`;
};
const logoSrc = resolveLogoSrc(logo);
const Icon = isCartao ? RiBankCard2Line : RiBankLine;

View File

@@ -6,8 +6,8 @@ import { ptBR } from "date-fns/locale";
import { useTransition } from "react";
import { toast } from "sonner";
import { cancelInstallmentAnticipationAction } from "@/app/(dashboard)/lancamentos/anticipation-actions";
import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
import MoneyValues from "@/components/money-values";
import { ConfirmActionDialog } from "@/components/shared/confirm-action-dialog";
import MoneyValues from "@/components/shared/money-values";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {

View File

@@ -31,9 +31,9 @@ import Image from "next/image";
import Link from "next/link";
import { useMemo, useState } from "react";
import { CategoryIcon } from "@/components/categorias/category-icon";
import MoneyValues from "@/components/money-values";
import { EmptyState } from "@/components/shared/empty-state";
import { TypeBadge } from "@/components/type-badge";
import MoneyValues from "@/components/shared/money-values";
import { TypeBadge } from "@/components/shared/type-badge";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@@ -69,6 +69,7 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { DEFAULT_LANCAMENTOS_COLUMN_ORDER } from "@/lib/lancamentos/column-order";
import { resolveLogoSrc } from "@/lib/logo";
import { getAvatarSrc } from "@/lib/pagadores/utils";
import { formatDate } from "@/lib/utils/date";
import { getConditionIcon, getPaymentMethodIcon } from "@/lib/utils/icons";
@@ -82,15 +83,6 @@ import type {
} from "../types";
import { LancamentosFilters } from "./lancamentos-filters";
const resolveLogoSrc = (logo: string | null) => {
if (!logo) {
return null;
}
const fileName = logo.split("/").filter(Boolean).pop() ?? logo;
return `/logos/${fileName}`;
};
type BuildColumnsArgs = {
currentUserId: string;
noteAsColumn: boolean;
@@ -386,7 +378,7 @@ const buildColumns = ({
cell: ({ row }) => {
const { pagadorId, pagadorName, pagadorAvatar } = row.original;
const label = pagadorName.trim() || "Sem pagador";
const label = pagadorName?.trim() || "Sem pagador";
const displayName = label.split(/\s+/)[0] ?? label;
const avatarSrc = getAvatarSrc(pagadorAvatar);
const initial = displayName.charAt(0).toUpperCase() || "?";