forked from git.gladyson/openmonetis
feat: adição de novos ícones SVG e configuração do ambiente
- Adicionados ícones SVG para ChatGPT, Claude, Gemini e OpenRouter - Implementados ícones para modos claro e escuro do ChatGPT - Criado script de inicialização para PostgreSQL com extensão pgcrypto - Adicionado script de configuração de ambiente que faz backup do .env - Configurado tsconfig.json para TypeScript com opções de compilação
This commit is contained in:
503
components/lancamentos/page/lancamentos-page.tsx
Normal file
503
components/lancamentos/page/lancamentos-page.tsx
Normal file
@@ -0,0 +1,503 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
createMassLancamentosAction,
|
||||
deleteLancamentoAction,
|
||||
deleteLancamentoBulkAction,
|
||||
deleteMultipleLancamentosAction,
|
||||
toggleLancamentoSettlementAction,
|
||||
updateLancamentoBulkAction,
|
||||
} from "@/app/(dashboard)/lancamentos/actions";
|
||||
import { ConfirmActionDialog } from "@/components/confirm-action-dialog";
|
||||
import { useCallback, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { AnticipateInstallmentsDialog } from "../dialogs/anticipate-installments-dialog/anticipate-installments-dialog";
|
||||
import { AnticipationHistoryDialog } from "../dialogs/anticipate-installments-dialog/anticipation-history-dialog";
|
||||
import { BulkActionDialog, type BulkActionScope } from "../dialogs/bulk-action-dialog";
|
||||
import { LancamentoDetailsDialog } from "../dialogs/lancamento-details-dialog";
|
||||
import { LancamentoDialog } from "../dialogs/lancamento-dialog/lancamento-dialog";
|
||||
import { LancamentosTable } from "../table/lancamentos-table";
|
||||
import { MassAddDialog, type MassAddFormData } from "../dialogs/mass-add-dialog";
|
||||
import type {
|
||||
ContaCartaoFilterOption,
|
||||
LancamentoFilterOption,
|
||||
LancamentoItem,
|
||||
SelectOption,
|
||||
} from "../types";
|
||||
|
||||
interface LancamentosPageProps {
|
||||
lancamentos: LancamentoItem[];
|
||||
pagadorOptions: SelectOption[];
|
||||
splitPagadorOptions: SelectOption[];
|
||||
defaultPagadorId: string | null;
|
||||
contaOptions: SelectOption[];
|
||||
cartaoOptions: SelectOption[];
|
||||
categoriaOptions: SelectOption[];
|
||||
pagadorFilterOptions: LancamentoFilterOption[];
|
||||
categoriaFilterOptions: LancamentoFilterOption[];
|
||||
contaCartaoFilterOptions: ContaCartaoFilterOption[];
|
||||
selectedPeriod: string;
|
||||
estabelecimentos: string[];
|
||||
allowCreate?: boolean;
|
||||
defaultCartaoId?: string | null;
|
||||
defaultPaymentMethod?: string | null;
|
||||
lockCartaoSelection?: boolean;
|
||||
lockPaymentMethod?: boolean;
|
||||
}
|
||||
|
||||
export function LancamentosPage({
|
||||
lancamentos,
|
||||
pagadorOptions,
|
||||
splitPagadorOptions,
|
||||
defaultPagadorId,
|
||||
contaOptions,
|
||||
cartaoOptions,
|
||||
categoriaOptions,
|
||||
pagadorFilterOptions,
|
||||
categoriaFilterOptions,
|
||||
contaCartaoFilterOptions,
|
||||
selectedPeriod,
|
||||
estabelecimentos,
|
||||
allowCreate = true,
|
||||
defaultCartaoId,
|
||||
defaultPaymentMethod,
|
||||
lockCartaoSelection,
|
||||
lockPaymentMethod,
|
||||
}: LancamentosPageProps) {
|
||||
const [selectedLancamento, setSelectedLancamento] =
|
||||
useState<LancamentoItem | null>(null);
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [massAddOpen, setMassAddOpen] = useState(false);
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
const [lancamentoToDelete, setLancamentoToDelete] =
|
||||
useState<LancamentoItem | null>(null);
|
||||
const [detailsOpen, setDetailsOpen] = useState(false);
|
||||
const [settlementLoadingId, setSettlementLoadingId] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
const [bulkEditOpen, setBulkEditOpen] = useState(false);
|
||||
const [bulkDeleteOpen, setBulkDeleteOpen] = useState(false);
|
||||
const [pendingEditData, setPendingEditData] = useState<{
|
||||
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;
|
||||
lancamento: LancamentoItem;
|
||||
} | null>(null);
|
||||
const [pendingDeleteData, setPendingDeleteData] =
|
||||
useState<LancamentoItem | null>(null);
|
||||
const [multipleBulkDeleteOpen, setMultipleBulkDeleteOpen] = useState(false);
|
||||
const [pendingMultipleDeleteData, setPendingMultipleDeleteData] = useState<
|
||||
LancamentoItem[]
|
||||
>([]);
|
||||
const [anticipateOpen, setAnticipateOpen] = useState(false);
|
||||
const [anticipationHistoryOpen, setAnticipationHistoryOpen] = useState(false);
|
||||
const [selectedForAnticipation, setSelectedForAnticipation] =
|
||||
useState<LancamentoItem | null>(null);
|
||||
|
||||
const handleToggleSettlement = useCallback(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."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const supportedMethods = ["Pix", "Boleto", "Dinheiro", "Cartão de débito"];
|
||||
if (!supportedMethods.includes(item.paymentMethod)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextValue = !Boolean(item.isSettled);
|
||||
|
||||
try {
|
||||
setSettlementLoadingId(item.id);
|
||||
const result = await toggleLancamentoSettlementAction({
|
||||
id: item.id,
|
||||
value: nextValue,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
toast.success(result.message);
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Não foi possível atualizar o pagamento.";
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setSettlementLoadingId(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleDelete = useCallback(async () => {
|
||||
if (!lancamentoToDelete) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await deleteLancamentoAction({
|
||||
id: lancamentoToDelete.id,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
toast.error(result.error);
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
toast.success(result.message);
|
||||
setDeleteOpen(false);
|
||||
}, [lancamentoToDelete]);
|
||||
|
||||
const handleBulkDelete = useCallback(
|
||||
async (scope: BulkActionScope) => {
|
||||
if (!pendingDeleteData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await deleteLancamentoBulkAction({
|
||||
id: pendingDeleteData.id,
|
||||
scope,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
toast.error(result.error);
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
toast.success(result.message);
|
||||
setBulkDeleteOpen(false);
|
||||
setPendingDeleteData(null);
|
||||
},
|
||||
[pendingDeleteData]
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
setPendingEditData({
|
||||
...data,
|
||||
lancamento: selectedLancamento,
|
||||
});
|
||||
setEditOpen(false);
|
||||
setBulkEditOpen(true);
|
||||
},
|
||||
[selectedLancamento]
|
||||
);
|
||||
|
||||
const handleBulkEdit = useCallback(
|
||||
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,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
toast.error(result.error);
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
toast.success(result.message);
|
||||
setBulkEditOpen(false);
|
||||
setPendingEditData(null);
|
||||
},
|
||||
[pendingEditData]
|
||||
);
|
||||
|
||||
const handleMassAddSubmit = useCallback(async (data: MassAddFormData) => {
|
||||
const result = await createMassLancamentosAction(data);
|
||||
|
||||
if (!result.success) {
|
||||
toast.error(result.error);
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
toast.success(result.message);
|
||||
}, []);
|
||||
|
||||
const handleMultipleBulkDelete = useCallback((items: LancamentoItem[]) => {
|
||||
setPendingMultipleDeleteData(items);
|
||||
setMultipleBulkDeleteOpen(true);
|
||||
}, []);
|
||||
|
||||
const confirmMultipleBulkDelete = useCallback(async () => {
|
||||
if (pendingMultipleDeleteData.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ids = pendingMultipleDeleteData.map((item) => item.id);
|
||||
const result = await deleteMultipleLancamentosAction({ ids });
|
||||
|
||||
if (!result.success) {
|
||||
toast.error(result.error);
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
toast.success(result.message);
|
||||
setMultipleBulkDeleteOpen(false);
|
||||
setPendingMultipleDeleteData([]);
|
||||
}, [pendingMultipleDeleteData]);
|
||||
|
||||
const handleCreate = useCallback(() => {
|
||||
setCreateOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleMassAdd = useCallback(() => {
|
||||
setMassAddOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleEdit = useCallback((item: LancamentoItem) => {
|
||||
setSelectedLancamento(item);
|
||||
setEditOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleConfirmDelete = useCallback((item: LancamentoItem) => {
|
||||
if (item.seriesId) {
|
||||
setPendingDeleteData(item);
|
||||
setBulkDeleteOpen(true);
|
||||
} else {
|
||||
setLancamentoToDelete(item);
|
||||
setDeleteOpen(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleViewDetails = useCallback((item: LancamentoItem) => {
|
||||
setSelectedLancamento(item);
|
||||
setDetailsOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleAnticipate = useCallback((item: LancamentoItem) => {
|
||||
setSelectedForAnticipation(item);
|
||||
setAnticipateOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleViewAnticipationHistory = useCallback((item: LancamentoItem) => {
|
||||
setSelectedForAnticipation(item);
|
||||
setAnticipationHistoryOpen(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LancamentosTable
|
||||
data={lancamentos}
|
||||
pagadorFilterOptions={pagadorFilterOptions}
|
||||
categoriaFilterOptions={categoriaFilterOptions}
|
||||
contaCartaoFilterOptions={contaCartaoFilterOptions}
|
||||
onCreate={allowCreate ? handleCreate : undefined}
|
||||
onMassAdd={allowCreate ? handleMassAdd : undefined}
|
||||
onEdit={handleEdit}
|
||||
onConfirmDelete={handleConfirmDelete}
|
||||
onBulkDelete={handleMultipleBulkDelete}
|
||||
onViewDetails={handleViewDetails}
|
||||
onToggleSettlement={handleToggleSettlement}
|
||||
onAnticipate={handleAnticipate}
|
||||
onViewAnticipationHistory={handleViewAnticipationHistory}
|
||||
isSettlementLoading={(id) => settlementLoadingId === id}
|
||||
/>
|
||||
|
||||
{allowCreate ? (
|
||||
<LancamentoDialog
|
||||
mode="create"
|
||||
open={createOpen}
|
||||
onOpenChange={setCreateOpen}
|
||||
pagadorOptions={pagadorOptions}
|
||||
splitPagadorOptions={splitPagadorOptions}
|
||||
defaultPagadorId={defaultPagadorId}
|
||||
contaOptions={contaOptions}
|
||||
cartaoOptions={cartaoOptions}
|
||||
categoriaOptions={categoriaOptions}
|
||||
estabelecimentos={estabelecimentos}
|
||||
defaultPeriod={selectedPeriod}
|
||||
defaultCartaoId={defaultCartaoId}
|
||||
defaultPaymentMethod={defaultPaymentMethod}
|
||||
lockCartaoSelection={lockCartaoSelection}
|
||||
lockPaymentMethod={lockPaymentMethod}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<LancamentoDialog
|
||||
mode="update"
|
||||
open={editOpen && !!selectedLancamento}
|
||||
onOpenChange={setEditOpen}
|
||||
pagadorOptions={pagadorOptions}
|
||||
splitPagadorOptions={splitPagadorOptions}
|
||||
defaultPagadorId={defaultPagadorId}
|
||||
contaOptions={contaOptions}
|
||||
cartaoOptions={cartaoOptions}
|
||||
categoriaOptions={categoriaOptions}
|
||||
estabelecimentos={estabelecimentos}
|
||||
lancamento={selectedLancamento ?? undefined}
|
||||
defaultPeriod={selectedPeriod}
|
||||
onBulkEditRequest={handleBulkEditRequest}
|
||||
/>
|
||||
|
||||
<LancamentoDetailsDialog
|
||||
open={detailsOpen && !!selectedLancamento}
|
||||
onOpenChange={(open) => {
|
||||
setDetailsOpen(open);
|
||||
if (!open) {
|
||||
setSelectedLancamento(null);
|
||||
}
|
||||
}}
|
||||
lancamento={detailsOpen ? selectedLancamento : null}
|
||||
/>
|
||||
|
||||
<ConfirmActionDialog
|
||||
open={deleteOpen && !!lancamentoToDelete}
|
||||
onOpenChange={setDeleteOpen}
|
||||
title={
|
||||
lancamentoToDelete
|
||||
? `Remover lançamento "${lancamentoToDelete.name}"?`
|
||||
: "Remover lançamento?"
|
||||
}
|
||||
description="Essa ação é irreversível e removerá o lançamento de forma permanente."
|
||||
confirmLabel="Remover"
|
||||
pendingLabel="Removendo..."
|
||||
confirmVariant="destructive"
|
||||
onConfirm={handleDelete}
|
||||
disabled={!lancamentoToDelete}
|
||||
/>
|
||||
|
||||
<BulkActionDialog
|
||||
open={bulkDeleteOpen && !!pendingDeleteData}
|
||||
onOpenChange={setBulkDeleteOpen}
|
||||
actionType="delete"
|
||||
seriesType={
|
||||
pendingDeleteData?.condition === "Parcelado"
|
||||
? "installment"
|
||||
: "recurring"
|
||||
}
|
||||
currentNumber={pendingDeleteData?.currentInstallment ?? undefined}
|
||||
totalCount={
|
||||
pendingDeleteData?.installmentCount ??
|
||||
pendingDeleteData?.recurrenceCount ??
|
||||
undefined
|
||||
}
|
||||
onConfirm={handleBulkDelete}
|
||||
/>
|
||||
|
||||
<BulkActionDialog
|
||||
open={bulkEditOpen && !!pendingEditData}
|
||||
onOpenChange={setBulkEditOpen}
|
||||
actionType="edit"
|
||||
seriesType={
|
||||
pendingEditData?.lancamento.condition === "Parcelado"
|
||||
? "installment"
|
||||
: "recurring"
|
||||
}
|
||||
currentNumber={
|
||||
pendingEditData?.lancamento.currentInstallment ?? undefined
|
||||
}
|
||||
totalCount={
|
||||
pendingEditData?.lancamento.installmentCount ??
|
||||
pendingEditData?.lancamento.recurrenceCount ??
|
||||
undefined
|
||||
}
|
||||
onConfirm={handleBulkEdit}
|
||||
/>
|
||||
|
||||
{allowCreate ? (
|
||||
<MassAddDialog
|
||||
open={massAddOpen}
|
||||
onOpenChange={setMassAddOpen}
|
||||
onSubmit={handleMassAddSubmit}
|
||||
pagadorOptions={pagadorOptions}
|
||||
contaOptions={contaOptions}
|
||||
cartaoOptions={cartaoOptions}
|
||||
categoriaOptions={categoriaOptions}
|
||||
estabelecimentos={estabelecimentos}
|
||||
selectedPeriod={selectedPeriod}
|
||||
defaultPagadorId={defaultPagadorId}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<ConfirmActionDialog
|
||||
open={multipleBulkDeleteOpen && pendingMultipleDeleteData.length > 0}
|
||||
onOpenChange={setMultipleBulkDeleteOpen}
|
||||
title={`Remover ${pendingMultipleDeleteData.length} ${
|
||||
pendingMultipleDeleteData.length === 1 ? "lançamento" : "lançamentos"
|
||||
}?`}
|
||||
description="Essa ação é irreversível e removerá os lançamentos selecionados de forma permanente."
|
||||
confirmLabel="Remover"
|
||||
pendingLabel="Removendo..."
|
||||
confirmVariant="destructive"
|
||||
onConfirm={confirmMultipleBulkDelete}
|
||||
disabled={pendingMultipleDeleteData.length === 0}
|
||||
/>
|
||||
|
||||
{/* Dialogs de Antecipação */}
|
||||
{selectedForAnticipation && (
|
||||
<AnticipateInstallmentsDialog
|
||||
open={anticipateOpen}
|
||||
onOpenChange={setAnticipateOpen}
|
||||
seriesId={selectedForAnticipation.seriesId!}
|
||||
lancamentoName={selectedForAnticipation.name}
|
||||
categorias={categoriaOptions.map((c) => ({
|
||||
id: c.value,
|
||||
name: c.label,
|
||||
icon: c.icon ?? null,
|
||||
}))}
|
||||
pagadores={pagadorOptions.map((p) => ({
|
||||
id: p.value,
|
||||
name: p.label,
|
||||
}))}
|
||||
defaultPeriod={selectedPeriod}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedForAnticipation && (
|
||||
<AnticipationHistoryDialog
|
||||
open={anticipationHistoryOpen}
|
||||
onOpenChange={setAnticipationHistoryOpen}
|
||||
seriesId={selectedForAnticipation.seriesId!}
|
||||
lancamentoName={selectedForAnticipation.name}
|
||||
onViewLancamento={(lancamentoId) => {
|
||||
const lancamento = lancamentos.find((l) => l.id === lancamentoId);
|
||||
if (lancamento) {
|
||||
setSelectedLancamento(lancamento);
|
||||
setDetailsOpen(true);
|
||||
setAnticipationHistoryOpen(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user