mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 19:01:47 +00:00
feat(finance): refina fluxos de transacoes e pagadores
This commit is contained in:
@@ -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)}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() || "?";
|
||||
|
||||
Reference in New Issue
Block a user