mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 19:01:47 +00:00
refactor: pagina transações e modulariza ações
This commit is contained in:
@@ -11,7 +11,10 @@ import {
|
||||
updateTransactionBulkAction,
|
||||
} from "@/features/transactions/actions";
|
||||
import { ConfirmActionDialog } from "@/shared/components/confirm-action-dialog";
|
||||
|
||||
import type {
|
||||
TransactionsExportContext,
|
||||
TransactionsPaginationState,
|
||||
} from "../../export-types";
|
||||
import { AnticipateInstallmentsDialog } from "../dialogs/anticipate-installments-dialog/anticipate-installments-dialog";
|
||||
import { AnticipationHistoryDialog } from "../dialogs/anticipate-installments-dialog/anticipation-history-dialog";
|
||||
import {
|
||||
@@ -54,6 +57,8 @@ interface TransactionsPageProps {
|
||||
defaultPaymentMethod?: string | null;
|
||||
lockCardSelection?: boolean;
|
||||
lockPaymentMethod?: boolean;
|
||||
pagination?: TransactionsPaginationState;
|
||||
exportContext?: TransactionsExportContext;
|
||||
// Opções específicas para o dialog de importação (quando visualizando dados de outro usuário)
|
||||
importPayerOptions?: SelectOption[];
|
||||
importSplitPayerOptions?: SelectOption[];
|
||||
@@ -84,6 +89,8 @@ export function TransactionsPage({
|
||||
defaultPaymentMethod,
|
||||
lockCardSelection,
|
||||
lockPaymentMethod,
|
||||
pagination,
|
||||
exportContext,
|
||||
importPayerOptions,
|
||||
importSplitPayerOptions,
|
||||
importDefaultPayerId,
|
||||
@@ -393,6 +400,8 @@ export function TransactionsPage({
|
||||
categoryFilterOptions={categoryFilterOptions}
|
||||
accountCardFilterOptions={accountCardFilterOptions}
|
||||
selectedPeriod={selectedPeriod}
|
||||
pagination={pagination}
|
||||
exportContext={exportContext}
|
||||
onCreate={allowCreate ? handleCreate : undefined}
|
||||
onMassAdd={allowCreate ? handleMassAdd : undefined}
|
||||
onEdit={handleEdit}
|
||||
|
||||
@@ -162,10 +162,13 @@ export function TransactionsFilters({
|
||||
nextParams.delete(key);
|
||||
}
|
||||
|
||||
nextParams.delete("page");
|
||||
|
||||
startTransition(() => {
|
||||
router.replace(`${pathname}?${nextParams.toString()}`, {
|
||||
scroll: false,
|
||||
});
|
||||
const target = nextParams.toString()
|
||||
? `${pathname}?${nextParams.toString()}`
|
||||
: pathname;
|
||||
router.replace(target, { scroll: false });
|
||||
});
|
||||
},
|
||||
[searchParams, pathname, router],
|
||||
@@ -193,10 +196,14 @@ export function TransactionsFilters({
|
||||
|
||||
const handleReset = () => {
|
||||
const periodValue = searchParams.get("periodo");
|
||||
const pageSizeValue = searchParams.get("pageSize");
|
||||
const nextParams = new URLSearchParams();
|
||||
if (periodValue) {
|
||||
nextParams.set("periodo", periodValue);
|
||||
}
|
||||
if (pageSizeValue) {
|
||||
nextParams.set("pageSize", pageSizeValue);
|
||||
}
|
||||
setSearchValue("");
|
||||
setCategoryOpen(false);
|
||||
startTransition(() => {
|
||||
|
||||
@@ -34,8 +34,13 @@ import {
|
||||
} from "@tanstack/react-table";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useMemo, useState } from "react";
|
||||
import { DEFAULT_LANCAMENTOS_COLUMN_ORDER } from "@/features/transactions/column-order";
|
||||
import type {
|
||||
TransactionsExportContext,
|
||||
TransactionsPaginationState,
|
||||
} from "@/features/transactions/export-types";
|
||||
import { EmptyState } from "@/shared/components/empty-state";
|
||||
import {
|
||||
CategoryIconBadge,
|
||||
@@ -289,7 +294,7 @@ const buildColumns = ({
|
||||
<TooltipContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="max-w-xs whitespace-pre-line text-sm"
|
||||
className="max-w-xs whitespace-pre-line"
|
||||
>
|
||||
{note}
|
||||
</TooltipContent>
|
||||
@@ -743,6 +748,8 @@ type LancamentosTableProps = {
|
||||
categoryFilterOptions?: TransactionFilterOption[];
|
||||
accountCardFilterOptions?: AccountCardFilterOption[];
|
||||
selectedPeriod?: string;
|
||||
pagination?: TransactionsPaginationState;
|
||||
exportContext?: TransactionsExportContext;
|
||||
onCreate?: (type: "Despesa" | "Receita") => void;
|
||||
onMassAdd?: () => void;
|
||||
onEdit?: (item: TransactionItem) => void;
|
||||
@@ -769,6 +776,8 @@ export function TransactionsTable({
|
||||
categoryFilterOptions = [],
|
||||
accountCardFilterOptions = [],
|
||||
selectedPeriod,
|
||||
pagination: serverPagination,
|
||||
exportContext,
|
||||
onCreate,
|
||||
onMassAdd,
|
||||
onEdit,
|
||||
@@ -785,6 +794,9 @@ export function TransactionsTable({
|
||||
showActions = true,
|
||||
showFilters = true,
|
||||
}: LancamentosTableProps) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const [sorting, setSorting] = useState<SortingState>([
|
||||
{ id: "purchaseDate", desc: true },
|
||||
]);
|
||||
@@ -796,6 +808,7 @@ export function TransactionsTable({
|
||||
pageSize: 30,
|
||||
});
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const isServerPaginated = Boolean(serverPagination);
|
||||
|
||||
const columns = useMemo(() => {
|
||||
const built = buildColumns({
|
||||
@@ -835,30 +848,53 @@ export function TransactionsTable({
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
state: {
|
||||
sorting,
|
||||
columnVisibility,
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
state: isServerPaginated
|
||||
? {
|
||||
sorting,
|
||||
columnVisibility,
|
||||
rowSelection,
|
||||
}
|
||||
: {
|
||||
sorting,
|
||||
columnVisibility,
|
||||
pagination,
|
||||
rowSelection,
|
||||
},
|
||||
onSortingChange: setSorting,
|
||||
onPaginationChange: setPagination,
|
||||
onPaginationChange: isServerPaginated ? undefined : setPagination,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getPaginationRowModel: isServerPaginated
|
||||
? undefined
|
||||
: getPaginationRowModel(),
|
||||
manualPagination: isServerPaginated,
|
||||
pageCount: serverPagination?.totalPages,
|
||||
enableRowSelection: true,
|
||||
});
|
||||
|
||||
const rowModel = table.getRowModel();
|
||||
const hasRows = rowModel.rows.length > 0;
|
||||
const totalRows = table.getCoreRowModel().rows.length;
|
||||
const totalRows = isServerPaginated
|
||||
? (serverPagination?.totalItems ?? 0)
|
||||
: table.getCoreRowModel().rows.length;
|
||||
const selectedRows = table.getFilteredSelectedRowModel().rows;
|
||||
const selectedCount = selectedRows.length;
|
||||
const selectedTotal = selectedRows.reduce(
|
||||
(total, row) => total + (row.original.amount ?? 0),
|
||||
0,
|
||||
);
|
||||
const currentPage = isServerPaginated
|
||||
? (serverPagination?.page ?? 1)
|
||||
: table.getState().pagination.pageIndex + 1;
|
||||
const currentPageSize = isServerPaginated
|
||||
? (serverPagination?.pageSize ?? pagination.pageSize)
|
||||
: pagination.pageSize;
|
||||
const totalPages = isServerPaginated
|
||||
? Math.max(serverPagination?.totalPages ?? 1, 1)
|
||||
: Math.max(table.getPageCount(), 1);
|
||||
const canPreviousPage = currentPage > 1;
|
||||
const canNextPage = currentPage < totalPages;
|
||||
|
||||
// Check if there's any data from other users
|
||||
const hasOtherUserData = data.some((item) => item.userId !== currentUserId);
|
||||
@@ -882,6 +918,28 @@ export function TransactionsTable({
|
||||
const showTopControls =
|
||||
Boolean(onCreate) || Boolean(onMassAdd) || showFilters;
|
||||
|
||||
const navigateToPage = (nextPage: number, nextPageSize = currentPageSize) => {
|
||||
const nextParams = new URLSearchParams(searchParams.toString());
|
||||
|
||||
if (nextPage <= 1) {
|
||||
nextParams.delete("page");
|
||||
} else {
|
||||
nextParams.set("page", nextPage.toString());
|
||||
}
|
||||
|
||||
if (nextPageSize === 30) {
|
||||
nextParams.delete("pageSize");
|
||||
} else {
|
||||
nextParams.set("pageSize", nextPageSize.toString());
|
||||
}
|
||||
|
||||
const target = nextParams.toString()
|
||||
? `${pathname}?${nextParams.toString()}`
|
||||
: pathname;
|
||||
router.replace(target, { scroll: false });
|
||||
setRowSelection({});
|
||||
};
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
{showTopControls ? (
|
||||
@@ -943,6 +1001,7 @@ export function TransactionsTable({
|
||||
<TransactionsExport
|
||||
lancamentos={data}
|
||||
period={selectedPeriod}
|
||||
exportContext={exportContext}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
@@ -1073,9 +1132,15 @@ export function TransactionsTable({
|
||||
{totalRows} lançamentos
|
||||
</span>
|
||||
<Select
|
||||
value={pagination.pageSize.toString()}
|
||||
value={currentPageSize.toString()}
|
||||
onValueChange={(value) => {
|
||||
table.setPageSize(Number(value));
|
||||
const nextPageSize = Number(value);
|
||||
if (isServerPaginated) {
|
||||
navigateToPage(1, nextPageSize);
|
||||
return;
|
||||
}
|
||||
|
||||
table.setPageSize(nextPageSize);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-max">
|
||||
@@ -1094,15 +1159,18 @@ export function TransactionsTable({
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Página {table.getState().pagination.pageIndex + 1} de{" "}
|
||||
{Math.max(table.getPageCount(), 1)}
|
||||
Página {currentPage} de {totalPages}
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
onClick={() => table.setPageIndex(0)}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
onClick={() =>
|
||||
isServerPaginated
|
||||
? navigateToPage(1)
|
||||
: table.setPageIndex(0)
|
||||
}
|
||||
disabled={!canPreviousPage}
|
||||
aria-label="Primeira página"
|
||||
>
|
||||
<RiArrowLeftDoubleLine className="size-4" />
|
||||
@@ -1110,8 +1178,12 @@ export function TransactionsTable({
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
onClick={() =>
|
||||
isServerPaginated
|
||||
? navigateToPage(currentPage - 1)
|
||||
: table.previousPage()
|
||||
}
|
||||
disabled={!canPreviousPage}
|
||||
aria-label="Página anterior"
|
||||
>
|
||||
<RiArrowLeftSLine className="size-4" />
|
||||
@@ -1119,8 +1191,12 @@ export function TransactionsTable({
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
onClick={() =>
|
||||
isServerPaginated
|
||||
? navigateToPage(currentPage + 1)
|
||||
: table.nextPage()
|
||||
}
|
||||
disabled={!canNextPage}
|
||||
aria-label="Próxima página"
|
||||
>
|
||||
<RiArrowRightSLine className="size-4" />
|
||||
@@ -1129,9 +1205,11 @@ export function TransactionsTable({
|
||||
variant="outline"
|
||||
size="icon-sm"
|
||||
onClick={() =>
|
||||
table.setPageIndex(table.getPageCount() - 1)
|
||||
isServerPaginated
|
||||
? navigateToPage(totalPages)
|
||||
: table.setPageIndex(table.getPageCount() - 1)
|
||||
}
|
||||
disabled={!table.getCanNextPage()}
|
||||
disabled={!canNextPage}
|
||||
aria-label="Última página"
|
||||
>
|
||||
<RiArrowRightDoubleLine className="size-4" />
|
||||
|
||||
@@ -6,11 +6,10 @@ import {
|
||||
RiFilePdfLine,
|
||||
RiFileTextLine,
|
||||
} from "@remixicon/react";
|
||||
import jsPDF from "jspdf";
|
||||
import autoTable from "jspdf-autotable";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import * as XLSX from "xlsx";
|
||||
import { exportTransactionsDataAction } from "@/features/transactions/actions";
|
||||
import type { TransactionsExportContext } from "@/features/transactions/export-types";
|
||||
import { formatCurrency } from "@/features/transactions/formatting-helpers";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
import {
|
||||
@@ -30,11 +29,24 @@ import type { TransactionItem } from "./types";
|
||||
interface LancamentosExportProps {
|
||||
lancamentos: TransactionItem[];
|
||||
period: string;
|
||||
exportContext?: TransactionsExportContext;
|
||||
}
|
||||
|
||||
const loadXlsx = () => import("xlsx");
|
||||
|
||||
const loadPdfDeps = async () => {
|
||||
const [{ default: jsPDF }, { default: autoTable }] = await Promise.all([
|
||||
import("jspdf"),
|
||||
import("jspdf-autotable"),
|
||||
]);
|
||||
|
||||
return { jsPDF, autoTable };
|
||||
};
|
||||
|
||||
export function TransactionsExport({
|
||||
lancamentos,
|
||||
period,
|
||||
exportContext,
|
||||
}: LancamentosExportProps) {
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
|
||||
@@ -69,9 +81,24 @@ export function TransactionsExport({
|
||||
return `${transaction.name} (${transaction.currentInstallment ?? 1}/${transaction.installmentCount})`;
|
||||
};
|
||||
|
||||
const exportToCSV = () => {
|
||||
const loadTransactions = async () => {
|
||||
if (!exportContext) {
|
||||
return lancamentos;
|
||||
}
|
||||
|
||||
const result = await exportTransactionsDataAction(exportContext);
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
return result.data?.transactions ?? [];
|
||||
};
|
||||
|
||||
const exportToCSV = async () => {
|
||||
try {
|
||||
setIsExporting(true);
|
||||
const transactions = await loadTransactions();
|
||||
|
||||
const headers = [
|
||||
"Data",
|
||||
@@ -86,7 +113,7 @@ export function TransactionsExport({
|
||||
];
|
||||
const rows: string[][] = [];
|
||||
|
||||
lancamentos.forEach((lancamento) => {
|
||||
transactions.forEach((lancamento) => {
|
||||
const row = [
|
||||
formatDate(lancamento.purchaseDate),
|
||||
getNameWithInstallment(lancamento),
|
||||
@@ -127,9 +154,11 @@ export function TransactionsExport({
|
||||
}
|
||||
};
|
||||
|
||||
const exportToExcel = () => {
|
||||
const exportToExcel = async () => {
|
||||
try {
|
||||
setIsExporting(true);
|
||||
const transactions = await loadTransactions();
|
||||
const XLSX = await loadXlsx();
|
||||
|
||||
const headers = [
|
||||
"Data",
|
||||
@@ -144,7 +173,7 @@ export function TransactionsExport({
|
||||
];
|
||||
const rows: (string | number)[][] = [];
|
||||
|
||||
lancamentos.forEach((lancamento) => {
|
||||
transactions.forEach((lancamento) => {
|
||||
const row = [
|
||||
formatDate(lancamento.purchaseDate),
|
||||
getNameWithInstallment(lancamento),
|
||||
@@ -189,6 +218,8 @@ export function TransactionsExport({
|
||||
const exportToPDF = async () => {
|
||||
try {
|
||||
setIsExporting(true);
|
||||
const transactions = await loadTransactions();
|
||||
const { jsPDF, autoTable } = await loadPdfDeps();
|
||||
|
||||
const doc = new jsPDF({ orientation: "landscape" });
|
||||
const primaryColor = getPrimaryPdfColor();
|
||||
@@ -245,7 +276,7 @@ export function TransactionsExport({
|
||||
],
|
||||
];
|
||||
|
||||
const body = lancamentos.map((lancamento) => [
|
||||
const body = transactions.map((lancamento) => [
|
||||
formatDate(lancamento.purchaseDate),
|
||||
getNameWithInstallment(lancamento),
|
||||
lancamento.transactionType,
|
||||
@@ -285,7 +316,7 @@ export function TransactionsExport({
|
||||
},
|
||||
didParseCell: (cellData) => {
|
||||
if (cellData.section === "body" && cellData.column.index === 5) {
|
||||
const lancamento = lancamentos[cellData.row.index];
|
||||
const lancamento = transactions[cellData.row.index];
|
||||
if (lancamento) {
|
||||
if (lancamento.transactionType === "Despesa") {
|
||||
cellData.cell.styles.textColor = [220, 38, 38];
|
||||
|
||||
Reference in New Issue
Block a user