diff --git a/src/features/inbox/components/inbox-bulk-actions.tsx b/src/features/inbox/components/inbox-bulk-actions.tsx new file mode 100644 index 0000000..b035674 --- /dev/null +++ b/src/features/inbox/components/inbox-bulk-actions.tsx @@ -0,0 +1,153 @@ +import { RiDeleteBinLine } from "@remixicon/react"; +import Image from "next/image"; +import { Button } from "@/shared/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/shared/components/ui/select"; +import { resolveLogoSrc } from "@/shared/lib/logo"; +import type { InboxItem, InboxStatus } from "./types"; + +const DEFAULT_INBOX_APP_LOGO = "/avatars/default_icon.png"; + +function findMatchingLogo( + sourceAppName: string | null, + appLogoMap: Record, +): string | null { + if (!sourceAppName) return null; + const appName = sourceAppName.toLowerCase(); + if (appLogoMap[appName]) return resolveLogoSrc(appLogoMap[appName]); + for (const [name, logo] of Object.entries(appLogoMap)) { + if (name.includes(appName) || appName.includes(name)) { + return resolveLogoSrc(logo); + } + } + return null; +} + +type InboxBulkActionsProps = { + status: InboxStatus; + items: InboxItem[]; + activeApp: string | null; + appFilterOptions: string[]; + selectedIds: string[]; + allSelected: boolean; + appLogoMap: Record; + onAppChange: (app: string) => void; + onToggleSelectAll: () => void; + onSelectionBulkRequest: (status: InboxStatus) => void; + onBulkDeleteRequest: (status: "processed" | "discarded") => void; +}; + +export function InboxBulkActions({ + status, + items, + activeApp, + appFilterOptions, + selectedIds, + allSelected, + appLogoMap, + onAppChange, + onToggleSelectAll, + onSelectionBulkRequest, + onBulkDeleteRequest, +}: InboxBulkActionsProps) { + const getAppLogo = (appName: string | null) => + findMatchingLogo(appName, appLogoMap) ?? DEFAULT_INBOX_APP_LOGO; + + const appFilter = + appFilterOptions.length > 0 ? ( + + ) : null; + + return ( +
+ {appFilter} + {items.length > 0 ? ( +
+ + {selectedIds.length > 0 && ( + + )} + {(status === "processed" || status === "discarded") && ( + + )} +
+ ) : null} +
+ ); +} diff --git a/src/features/inbox/components/inbox-card.tsx b/src/features/inbox/components/inbox-card.tsx index b0fe609..8de995b 100644 --- a/src/features/inbox/components/inbox-card.tsx +++ b/src/features/inbox/components/inbox-card.tsx @@ -117,13 +117,15 @@ export const InboxCard = memo(function InboxCard({ className="shrink-0" /> )} - +
+ +
{item.sourceAppName || item.sourceApp} diff --git a/src/features/inbox/components/inbox-items-list.tsx b/src/features/inbox/components/inbox-items-list.tsx new file mode 100644 index 0000000..4e02e3f --- /dev/null +++ b/src/features/inbox/components/inbox-items-list.tsx @@ -0,0 +1,133 @@ +import { RiAtLine, RiCalendarEventLine } from "@remixicon/react"; +import { format } from "date-fns"; +import { ptBR } from "date-fns/locale"; +import { EmptyState } from "@/shared/components/empty-state"; +import { Card } from "@/shared/components/ui/card"; +import { InboxCard } from "./inbox-card"; +import type { InboxItem } from "./types"; + +// O Companion envia hora local de Brasília com 'Z' literal (não converte para UTC). +// Por isso, o timestamp armazenado no DB já tem a data correta de Brasília como componente UTC. +// Basta fatiar o ISO string sem nenhum ajuste para obter a data de Brasília do item. +function getItemDateKey(date: Date): string { + return date.toISOString().slice(0, 10); +} + +// Para "hoje" e "ontem", precisamos da data real de Brasília (UTC-3). +function getBrasiliaDateKey(date: Date): string { + const BRASILIA_OFFSET_MS = 3 * 60 * 60 * 1000; + return new Date(date.getTime() - BRASILIA_OFFSET_MS) + .toISOString() + .slice(0, 10); +} + +function getGroupLabel(dateKey: string): string { + const now = new Date(); + const todayKey = getBrasiliaDateKey(now); + const yesterdayKey = getBrasiliaDateKey( + new Date(now.getTime() - 24 * 60 * 60 * 1000), + ); + if (dateKey === todayKey) return "Hoje"; + if (dateKey === yesterdayKey) return "Ontem"; + const [year, month, day] = dateKey.split("-").map(Number); + return format(new Date(year, (month ?? 1) - 1, day), "d 'de' MMMM", { + locale: ptBR, + }); +} + +function groupItemsByDay( + items: InboxItem[], +): { label: string; items: InboxItem[] }[] { + const groups = new Map(); + for (const item of items) { + const key = getItemDateKey(new Date(item.notificationTimestamp)); + const group = groups.get(key); + if (group) { + group.push(item); + } else { + groups.set(key, [item]); + } + } + const sortedKeys = [...groups.keys()].sort((a, b) => b.localeCompare(a)); + return sortedKeys.map((key) => ({ + label: getGroupLabel(key), + items: groups.get(key) ?? [], + })); +} + +type InboxItemsListProps = { + items: InboxItem[]; + readonly?: boolean; + activeApp: string | null; + appLogoMap: Record; + selectedIds: string[]; + onProcess?: (item: InboxItem) => void; + onDiscard?: (item: InboxItem) => void; + onViewDetails?: (item: InboxItem) => void; + onDelete?: (item: InboxItem) => void; + onRestoreToPending?: (item: InboxItem) => void; + onSelectToggle: (id: string) => void; +}; + +export function InboxItemsList({ + items, + readonly, + activeApp, + appLogoMap, + selectedIds, + onProcess, + onDiscard, + onViewDetails, + onDelete, + onRestoreToPending, + onSelectToggle, +}: InboxItemsListProps) { + if (items.length === 0) { + const message = activeApp + ? "Nenhuma notificação deste app" + : readonly + ? "Nenhuma notificação nesta aba" + : "Nenhum pré-lançamento pendente"; + return ( + + } + title={message} + description="As notificações capturadas pelo app OpenMonetis Companion aparecerão aqui. Saiba mais em Ajustes > Companion." + /> + + ); + } + + const groups = groupItemsByDay(items); + + return ( +
+ {groups.map((group) => ( +
+
+ +

{group.label}

+
+
+ {group.items.map((item) => ( + + ))} +
+
+ ))} +
+ ); +} diff --git a/src/features/inbox/components/inbox-page.tsx b/src/features/inbox/components/inbox-page.tsx index 31ac0ac..46b93b9 100644 --- a/src/features/inbox/components/inbox-page.tsx +++ b/src/features/inbox/components/inbox-page.tsx @@ -1,17 +1,5 @@ "use client"; -import { - RiArrowLeftDoubleLine, - RiArrowLeftSLine, - RiArrowRightDoubleLine, - RiArrowRightSLine, - RiAtLine, - RiCalendarEventLine, - RiDeleteBinLine, -} from "@remixicon/react"; -import { format } from "date-fns"; -import { ptBR } from "date-fns/locale"; -import Image from "next/image"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { useCallback, @@ -30,31 +18,15 @@ import { markInboxAsProcessedAction, restoreDiscardedInboxItemAction, } from "@/features/inbox/actions"; -import { - INBOX_DEFAULT_PAGE_SIZE, - INBOX_PAGE_SIZE_OPTIONS, -} from "@/features/inbox/page-helpers"; +import { INBOX_DEFAULT_PAGE_SIZE } from "@/features/inbox/page-helpers"; import { TransactionDialog } from "@/features/transactions/components/dialogs/transaction-dialog/transaction-dialog"; import { ConfirmActionDialog } from "@/shared/components/confirm-action-dialog"; -import { EmptyState } from "@/shared/components/empty-state"; -import { Button } from "@/shared/components/ui/button"; -import { Card } from "@/shared/components/ui/card"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/shared/components/ui/select"; -import { - Tabs, - TabsContent, - TabsList, - TabsTrigger, -} from "@/shared/components/ui/tabs"; -import { resolveLogoSrc } from "@/shared/lib/logo"; -import { InboxCard } from "./inbox-card"; +import { Tabs, TabsContent } from "@/shared/components/ui/tabs"; +import { InboxBulkActions } from "./inbox-bulk-actions"; import { InboxDetailsDialog } from "./inbox-details-dialog"; +import { InboxItemsList } from "./inbox-items-list"; +import { InboxPagination } from "./inbox-pagination"; +import { InboxTabs } from "./inbox-tabs"; import type { InboxItem, InboxPaginationState, @@ -63,76 +35,6 @@ import type { SelectOption, } from "./types"; -const DEFAULT_INBOX_APP_LOGO = "/avatars/default_icon.png"; - -// O Companion envia hora local de Brasília com 'Z' literal (não converte para UTC). -// Por isso, o timestamp armazenado no DB já tem a data correta de Brasília como componente UTC. -// Basta fatiar o ISO string sem nenhum ajuste para obter a data de Brasília do item. -function getItemDateKey(date: Date): string { - return date.toISOString().slice(0, 10); -} - -// Para "hoje" e "ontem", precisamos da data real de Brasília (UTC-3). -function getBrasiliaDateKey(date: Date): string { - const BRASILIA_OFFSET_MS = 3 * 60 * 60 * 1000; - return new Date(date.getTime() - BRASILIA_OFFSET_MS) - .toISOString() - .slice(0, 10); -} - -function getGroupLabel(dateKey: string): string { - const now = new Date(); - const todayKey = getBrasiliaDateKey(now); - const yesterdayKey = getBrasiliaDateKey( - new Date(now.getTime() - 24 * 60 * 60 * 1000), - ); - if (dateKey === todayKey) return "Hoje"; - if (dateKey === yesterdayKey) return "Ontem"; - const [year, month, day] = dateKey.split("-").map(Number); - return format(new Date(year, month - 1, day), "d 'de' MMMM", { - locale: ptBR, - }); -} - -function groupItemsByDay( - items: InboxItem[], -): { label: string; items: InboxItem[] }[] { - const groups = new Map(); - for (const item of items) { - const key = getItemDateKey(new Date(item.notificationTimestamp)); - const group = groups.get(key); - if (group) { - group.push(item); - } else { - groups.set(key, [item]); - } - } - const sortedKeys = [...groups.keys()].sort((a, b) => b.localeCompare(a)); - return sortedKeys.map((key) => ({ - label: getGroupLabel(key), - items: groups.get(key) ?? [], - })); -} - -function findMatchingLogo( - sourceAppName: string | null, - appLogoMap: Record, -): string | null { - if (!sourceAppName) return null; - - const appName = sourceAppName.toLowerCase(); - - if (appLogoMap[appName]) return resolveLogoSrc(appLogoMap[appName]); - - for (const [name, logo] of Object.entries(appLogoMap)) { - if (name.includes(appName) || appName.includes(name)) { - return resolveLogoSrc(logo); - } - } - - return null; -} - interface InboxPageProps { activeStatus: InboxStatus; activeApp: string | null; @@ -197,24 +99,14 @@ export function InboxPage({ useState("pending"); const normalizedSourceApps = useMemo(() => { - if (!Array.isArray(sourceApps)) { - return []; - } - + if (!Array.isArray(sourceApps)) return []; const uniqueApps = new Set(); for (const app of sourceApps) { - if (typeof app !== "string") { - continue; - } - + if (typeof app !== "string") continue; const trimmedApp = app.trim(); - if (!trimmedApp) { - continue; - } - + if (!trimmedApp) continue; uniqueApps.add(trimmedApp); } - return [...uniqueApps].sort((left, right) => left.localeCompare(right, "pt-BR"), ); @@ -225,28 +117,19 @@ export function InboxPage({ ? [activeApp, ...normalizedSourceApps] : normalizedSourceApps; - const getAppLogo = (appName: string | null) => - findMatchingLogo(appName, appLogoMap) ?? DEFAULT_INBOX_APP_LOGO; - const handleProcessOpenChange = (open: boolean) => { setProcessOpen(open); - if (!open) { - setItemToProcess(null); - } + if (!open) setItemToProcess(null); }; const handleDetailsOpenChange = (open: boolean) => { setDetailsOpen(open); - if (!open) { - setItemDetails(null); - } + if (!open) setItemDetails(null); }; const handleDiscardOpenChange = (open: boolean) => { setDiscardOpen(open); - if (!open) { - setItemToDiscard(null); - } + if (!open) setItemToDiscard(null); }; const handleProcessRequest = useCallback((item: InboxItem) => { @@ -266,25 +149,20 @@ export function InboxPage({ const handleDiscardConfirm = async () => { if (!itemToDiscard) return; - const result = await discardInboxItemAction({ inboxItemId: itemToDiscard.id, }); - if (result.success) { toast.success(result.message); return; } - toast.error(result.error); throw new Error(result.error); }; const handleDeleteOpenChange = (open: boolean) => { setDeleteOpen(open); - if (!open) { - setItemToDelete(null); - } + if (!open) setItemToDelete(null); }; const handleDeleteRequest = useCallback((item: InboxItem) => { @@ -294,25 +172,20 @@ export function InboxPage({ const handleDeleteConfirm = async () => { if (!itemToDelete) return; - const result = await deleteInboxItemAction({ inboxItemId: itemToDelete.id, }); - if (result.success) { toast.success(result.message); return; } - toast.error(result.error); throw new Error(result.error); }; const handleRestoreOpenChange = (open: boolean) => { setRestoreOpen(open); - if (!open) { - setItemToRestore(null); - } + if (!open) setItemToRestore(null); }; const handleRestoreRequest = useCallback((item: InboxItem) => { @@ -322,16 +195,13 @@ export function InboxPage({ const handleRestoreToPendingConfirm = async () => { if (!itemToRestore) return; - const result = await restoreDiscardedInboxItemAction({ inboxItemId: itemToRestore.id, }); - if (result.success) { toast.success(result.message); return; } - toast.error(result.error); throw new Error(result.error); }; @@ -365,25 +235,21 @@ export function InboxPage({ nextPageSize: number, ) => { const nextParams = new URLSearchParams(searchParams.toString()); - if (nextStatus === "pending") { nextParams.delete("status"); } else { nextParams.set("status", nextStatus); } - if (nextPage <= 1) { nextParams.delete("page"); } else { nextParams.set("page", nextPage.toString()); } - if (nextPageSize === INBOX_DEFAULT_PAGE_SIZE) { nextParams.delete("pageSize"); } else { nextParams.set("pageSize", nextPageSize.toString()); } - startTransition(() => { const target = nextParams.toString() ? `${pathname}?${nextParams.toString()}` @@ -431,10 +297,7 @@ export function InboxPage({ }; const handleSelectionBulkRequest = (status: InboxStatus) => { - if (selectedIds.length === 0) { - return; - } - + if (selectedIds.length === 0) return; setSelectionBulkStatus(status); setSelectionBulkOpen(true); }; @@ -465,10 +328,6 @@ export function InboxPage({ } }; - const handleBulkDeleteOpenChange = (open: boolean) => { - setBulkDeleteOpen(open); - }; - const handleBulkDeleteRequest = (status: "processed" | "discarded") => { setBulkDeleteStatus(status); setBulkDeleteOpen(true); @@ -478,23 +337,19 @@ export function InboxPage({ const result = await bulkDeleteInboxItemsAction({ status: bulkDeleteStatus, }); - if (result.success) { toast.success(result.message); return; } - toast.error(result.error); throw new Error(result.error); }; const handleLancamentoSuccess = async () => { if (!itemToProcess) return; - const result = await markInboxAsProcessedAction({ inboxItemId: itemToProcess.id, }); - if (result.success) { toast.success("Notificação processada!"); } else { @@ -502,10 +357,6 @@ export function InboxPage({ } }; - const canPreviousPage = pagination.page > 1; - const canNextPage = pagination.page < pagination.totalPages; - - // Prepare default values from inbox item const getDateString = ( date: Date | string | null | undefined, ): string | null => { @@ -516,140 +367,29 @@ export function InboxPage({ const defaultPurchaseDate = getDateString(itemToProcess?.notificationTimestamp) ?? null; - const defaultName = itemToProcess?.parsedName ? itemToProcess.parsedName .toLowerCase() .replace(/\b\w/g, (char) => char.toUpperCase()) : null; - const defaultAmount = itemToProcess?.parsedAmount ? String(Math.abs(Number(itemToProcess.parsedAmount))) : null; - // Match sourceAppName with a cartão to pre-fill card select const matchedCartaoId = useMemo(() => { const appName = itemToProcess?.sourceAppName?.toLowerCase(); if (!appName) return null; - for (const option of cardOptions) { const label = option.label.toLowerCase(); - if (label.includes(appName) || appName.includes(label)) { + if (label.includes(appName) || appName.includes(label)) return option.value; - } } return null; }, [itemToProcess?.sourceAppName, cardOptions]); - const renderEmptyState = (message: string) => ( - - } - title={message} - description="As notificações capturadas pelo app OpenMonetis Companion aparecerão aqui. Saiba mais em Ajustes > Companion." - /> - - ); - - const renderGroupedGrid = (list: InboxItem[], readonly?: boolean) => { - if (list.length === 0) { - if (activeApp) { - return renderEmptyState("Nenhuma notificação deste app"); - } - return renderEmptyState( - readonly - ? "Nenhuma notificação nesta aba" - : "Nenhum pré-lançamento pendente", - ); - } - - const groups = groupItemsByDay(list); - - return ( -
- {groups.map((group) => ( -
-
- -

{group.label}

-
-
- {group.items.map((item) => ( - - ))} -
-
- ))} -
- ); - }; - - const renderAppFilter = () => { - if (appFilterOptions.length === 0) { - return null; - } - - return ( - - ); - }; + const showTabActions = (status: InboxStatus) => + activeStatus === status && + (appFilterOptions.length > 0 || items.length > 0); return ( <> @@ -658,229 +398,106 @@ export function InboxPage({ onValueChange={handleTabChange} className="w-full" > - - - Pendentes - ({counts.pending}) - - - Processados - ({counts.processed}) - - - Descartados - ({counts.discarded}) - - + - {activeStatus === "pending" && - (appFilterOptions.length > 0 || items.length > 0) && ( -
- {renderAppFilter()} - {items.length > 0 ? ( -
- - {selectedIds.length > 0 && ( - - )} -
- ) : null} -
- )} - {activeStatus === "pending" ? renderGroupedGrid(items, false) : null} + {showTabActions("pending") && ( + + )} + {activeStatus === "pending" && ( + + )}
+ - {activeStatus === "processed" && - (appFilterOptions.length > 0 || items.length > 0) && ( -
- {renderAppFilter()} - {items.length > 0 ? ( -
- - {selectedIds.length > 0 && ( - - )} - -
- ) : null} -
- )} - {activeStatus === "processed" ? renderGroupedGrid(items, true) : null} + {showTabActions("processed") && ( + + )} + {activeStatus === "processed" && ( + + )}
+ - {activeStatus === "discarded" && - (appFilterOptions.length > 0 || items.length > 0) && ( -
- {renderAppFilter()} - {items.length > 0 ? ( -
- - {selectedIds.length > 0 && ( - - )} - -
- ) : null} -
- )} - {activeStatus === "discarded" ? renderGroupedGrid(items, true) : null} + {showTabActions("discarded") && ( + + )} + {activeStatus === "discarded" && ( + + )}
- {pagination.totalItems > 0 ? ( -
-
- - {pagination.totalItems} notificações - - -
-
- - Página {pagination.page} de {pagination.totalPages} - -
- - - - -
-
-
- ) : null} + void; +}; + +export function InboxPagination({ + pagination, + activeStatus, + isPending, + onNavigate, +}: InboxPaginationProps) { + if (pagination.totalItems === 0) return null; + + const canPreviousPage = pagination.page > 1; + const canNextPage = pagination.page < pagination.totalPages; + + return ( +
+
+ + {pagination.totalItems} notificações + + +
+
+ + Página {pagination.page} de {pagination.totalPages} + +
+ + + + +
+
+
+ ); +} + +// Re-export para facilitar uso externo +export { INBOX_DEFAULT_PAGE_SIZE }; diff --git a/src/features/inbox/components/inbox-tabs.tsx b/src/features/inbox/components/inbox-tabs.tsx new file mode 100644 index 0000000..10b364b --- /dev/null +++ b/src/features/inbox/components/inbox-tabs.tsx @@ -0,0 +1,40 @@ +import { TabsList, TabsTrigger } from "@/shared/components/ui/tabs"; +import type { InboxStatus, InboxStatusCounts } from "./types"; + +type InboxTabsProps = { + counts: InboxStatusCounts; + isPending: boolean; +}; + +export function InboxTabs({ counts, isPending }: InboxTabsProps) { + return ( + + + Pendentes + ({counts.pending}) + + + Processados + ({counts.processed}) + + + Descartados + ({counts.discarded}) + + + ); +} + +export type { InboxStatus, InboxStatusCounts }; diff --git a/src/features/transactions/components/table/transactions-bulk-bar.tsx b/src/features/transactions/components/table/transactions-bulk-bar.tsx new file mode 100644 index 0000000..35369e6 --- /dev/null +++ b/src/features/transactions/components/table/transactions-bulk-bar.tsx @@ -0,0 +1,59 @@ +import { RiDeleteBin5Line, RiFileCopyLine } from "@remixicon/react"; +import MoneyValues from "@/shared/components/money-values"; +import { Button } from "@/shared/components/ui/button"; + +type TransactionsBulkBarProps = { + selectedCount: number; + selectedTotal: number; + mode: "delete" | "import"; + onAction: () => void; +}; + +export function TransactionsBulkBar({ + selectedCount, + selectedTotal, + mode, + onAction, +}: TransactionsBulkBarProps) { + return ( +
+
+ + {selectedCount}{" "} + {selectedCount === 1 ? "item selecionado" : "itens selecionados"} + + + - + + + Total:{" "} + + +
+ {mode === "delete" ? ( + + ) : ( + + )} +
+ ); +} diff --git a/src/features/transactions/components/table/transactions-columns.tsx b/src/features/transactions/components/table/transactions-columns.tsx new file mode 100644 index 0000000..4f498d1 --- /dev/null +++ b/src/features/transactions/components/table/transactions-columns.tsx @@ -0,0 +1,719 @@ +import { + RiAttachment2, + RiBankCard2Line, + RiChat1Line, + RiCheckboxBlankCircleLine, + RiCheckboxCircleFill, + RiCheckLine, + RiDeleteBin5Line, + RiFileCopyLine, + RiFileList2Line, + RiGroupLine, + RiHistoryLine, + RiMoreFill, + RiPencilLine, + RiTimeLine, +} from "@remixicon/react"; +import type { ColumnDef } from "@tanstack/react-table"; +import Image from "next/image"; +import Link from "next/link"; +import { DEFAULT_LANCAMENTOS_COLUMN_ORDER } from "@/features/transactions/column-order"; +import { + CategoryIconBadge, + EstablishmentLogo, +} from "@/shared/components/entity-avatar"; +import MoneyValues from "@/shared/components/money-values"; +import { TransactionTypeBadge } from "@/shared/components/transaction-type-badge"; +import { + Avatar, + AvatarFallback, + AvatarImage, +} from "@/shared/components/ui/avatar"; +import { Badge } from "@/shared/components/ui/badge"; +import { Button } from "@/shared/components/ui/button"; +import { Checkbox } from "@/shared/components/ui/checkbox"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/shared/components/ui/dropdown-menu"; +import { Spinner } from "@/shared/components/ui/spinner"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/shared/components/ui/tooltip"; +import { resolveLogoSrc } from "@/shared/lib/logo"; +import { getAvatarSrc } from "@/shared/lib/payers/utils"; +import { formatDate } from "@/shared/utils/date"; +import { getConditionIcon, getPaymentMethodIcon } from "@/shared/utils/icons"; +import { cn } from "@/shared/utils/ui"; +import type { TransactionItem } from "../types"; + +export type BuildColumnsArgs = { + currentUserId: string; + noteAsColumn: boolean; + onEdit?: (item: TransactionItem) => void; + onCopy?: (item: TransactionItem) => void; + onImport?: (item: TransactionItem) => void; + onConfirmDelete?: (item: TransactionItem) => void; + onViewDetails?: (item: TransactionItem) => void; + onToggleSettlement?: (item: TransactionItem) => void; + onAnticipate?: (item: TransactionItem) => void; + onViewAnticipationHistory?: (item: TransactionItem) => void; + isSettlementLoading: (id: string) => boolean; + showActions: boolean; + columnOrder?: string[] | null; +}; + +function getPaymentMethodTableLabel(method: string) { + if (method === "Transferência bancária") return "Transf. bancária"; + return method; +} + +const FIXED_START_IDS = ["select", "purchaseDate"]; +const FIXED_END_IDS = ["actions"]; + +function getColumnId(col: ColumnDef): string { + const c = col as { id?: string; accessorKey?: string }; + return c.id ?? c.accessorKey ?? ""; +} + +function reorderColumnsByPreference( + columns: ColumnDef[], + orderPreference: string[] | null | undefined, +): ColumnDef[] { + if (!orderPreference || orderPreference.length === 0) return columns; + + const order = orderPreference; + const fixedStart: ColumnDef[] = []; + const reorderable: ColumnDef[] = []; + const fixedEnd: ColumnDef[] = []; + + for (const col of columns) { + const id = getColumnId(col as ColumnDef); + if (FIXED_START_IDS.includes(id)) fixedStart.push(col); + else if (FIXED_END_IDS.includes(id)) fixedEnd.push(col); + else reorderable.push(col); + } + + const sorted = [...reorderable].sort((a, b) => { + const idA = getColumnId(a as ColumnDef); + const idB = getColumnId(b as ColumnDef); + const indexA = order.indexOf(idA); + const indexB = order.indexOf(idB); + if (indexA === -1 && indexB === -1) return 0; + if (indexA === -1) return 1; + if (indexB === -1) return -1; + return indexA - indexB; + }); + + return [...fixedStart, ...sorted, ...fixedEnd]; +} + +function buildColumns({ + currentUserId, + noteAsColumn, + onEdit, + onCopy, + onImport, + onConfirmDelete, + onViewDetails, + onToggleSettlement, + onAnticipate, + onViewAnticipationHistory, + isSettlementLoading, + showActions, +}: BuildColumnsArgs): ColumnDef[] { + const noop = () => undefined; + const handleEdit = onEdit ?? noop; + const handleCopy = onCopy ?? noop; + const handleImport = onImport ?? noop; + const handleConfirmDelete = onConfirmDelete ?? noop; + const handleViewDetails = onViewDetails ?? noop; + const handleToggleSettlement = onToggleSettlement ?? noop; + const handleAnticipate = onAnticipate ?? noop; + const handleViewAnticipationHistory = onViewAnticipationHistory ?? noop; + + const columns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Selecionar todos" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Selecionar linha" + /> + ), + enableSorting: false, + enableHiding: false, + }, + { + id: "purchaseDate", + accessorKey: "purchaseDate", + header: () => null, + cell: () => null, + }, + { + accessorKey: "name", + header: "Estabelecimento", + cell: ({ row }) => { + const { + name, + purchaseDate, + installmentCount, + currentInstallment, + paymentMethod, + dueDate, + note, + isDivided, + isAnticipated, + hasAttachments, + } = row.original; + + const installmentBadge = + currentInstallment && installmentCount + ? `${currentInstallment} de ${installmentCount}` + : null; + + const isBoleto = paymentMethod === "Boleto" && dueDate; + const dueDateLabel = + isBoleto && dueDate ? `venc. ${formatDate(dueDate)}` : null; + const hasNote = Boolean(note?.trim().length); + const isLastInstallment = + currentInstallment === installmentCount && + installmentCount && + installmentCount > 1; + + return ( + + + + + {formatDate(purchaseDate)} + {dueDateLabel ? ( + {dueDateLabel} + ) : null} + + + + + + {name} + + + + {name} + + + + {isDivided && ( + + + + + + Dividido entre pagadores + + + + + Dividido entre pagadores + + + )} + + {isLastInstallment ? ( + + + + Última parcela + Última parcela + + + Última parcela! + + ) : null} + + {installmentBadge ? ( + + {installmentBadge} + + ) : null} + + {isAnticipated && ( + + + + + Parcela antecipada + + + + Parcela antecipada + + + )} + + {!noteAsColumn && hasNote ? ( + + + + + Ver anotação + + + + {note} + + + ) : null} + + {hasAttachments ? ( + + + + + Possui anexos + + + Possui anexos + + ) : null} + + + + ); + }, + }, + { + accessorKey: "transactionType", + header: "Transação", + cell: ({ row }) => { + const type = + row.original.categoriaName === "Saldo inicial" + ? "Saldo inicial" + : row.original.transactionType; + return ( + + ); + }, + }, + { + accessorKey: "amount", + header: "Valor", + cell: ({ row }) => { + const isReceita = row.original.transactionType === "Receita"; + const isTransfer = row.original.transactionType === "Transferência"; + return ( + + ); + }, + }, + { + accessorKey: "condition", + header: "Condição", + cell: ({ row }) => { + const condition = row.original.condition; + const icon = getConditionIcon(condition); + return ( + + {icon} + {condition} + + ); + }, + }, + { + accessorKey: "paymentMethod", + header: "Forma de Pagamento", + cell: ({ row }) => { + const method = row.original.paymentMethod; + const icon = getPaymentMethodIcon(method); + return ( + + {icon} + {getPaymentMethodTableLabel(method)} + + ); + }, + }, + { + accessorKey: "categoriaName", + header: "Categoria", + cell: ({ row }) => { + const { categoriaName, categoriaIcon } = row.original; + if (!categoriaName) { + return ; + } + return ( + + + {categoriaName} + + ); + }, + }, + { + accessorKey: "pagadorName", + header: "Pagador", + cell: ({ row }) => { + const { payerId, pagadorName, pagadorAvatar } = row.original; + const label = pagadorName?.trim() || "Sem pagador"; + const displayName = label.split(/\s+/)[0] ?? label; + const avatarSrc = getAvatarSrc(pagadorAvatar); + const initial = displayName.charAt(0).toUpperCase() || "?"; + const content = ( + <> + + + + {initial} + + + {displayName} + + ); + if (!payerId) { + return ( + {content} + ); + } + return ( + + {content} + + ); + }, + }, + { + id: "contaCartao", + header: "Conta/Cartão", + cell: ({ row }) => { + const { + cartaoName, + contaName, + cartaoLogo, + contaLogo, + cardId, + accountId, + userId, + } = row.original; + const isCartao = Boolean(cartaoName); + const label = cartaoName ?? contaName; + const logoSrc = resolveLogoSrc(cartaoLogo ?? contaLogo); + const href = cardId + ? `/cards/${cardId}/invoice` + : accountId + ? `/accounts/${accountId}/statement` + : null; + const isOwnData = userId === currentUserId; + + const content = ( + + {logoSrc && ( + {`Logo + )} + {label} + + ); + + if (!isOwnData || !href) { + return ( + + {content} + + {isCartao ? "Cartão" : "Conta"}: {label} + + + ); + } + + return ( + + + + {logoSrc && ( + {`Logo + )} + {label} + + + + {isCartao ? "Cartão" : "Conta"}: {label} + + + ); + }, + }, + ]; + + if (noteAsColumn) { + const accountCardIndex = columns.findIndex((c) => c.id === "contaCartao"); + const noteColumn: ColumnDef = { + accessorKey: "note", + header: "Anotação", + cell: ({ row }) => { + const note = row.original.note; + if (!note?.trim()) + return ; + return ( + + {note} + + ); + }, + }; + columns.splice(accountCardIndex, 0, noteColumn); + } + + if (showActions) { + columns.push({ + id: "actions", + header: "Ações", + enableSorting: false, + cell: ({ row }) => ( +
+ {(() => { + const paymentMethod = row.original.paymentMethod; + const showSettlementButton = [ + "Pix", + "Boleto", + "Cartão de crédito", + "Dinheiro", + "Cartão de débito", + "Transferência bancária", + "Pré-Pago | VR/VA", + ].includes(paymentMethod); + + if (!showSettlementButton) return null; + + const canToggleSettlement = + paymentMethod === "Pix" || + paymentMethod === "Boleto" || + paymentMethod === "Dinheiro" || + paymentMethod === "Cartão de débito" || + paymentMethod === "Transferência bancária" || + paymentMethod === "Pré-Pago | VR/VA"; + + if (!canToggleSettlement) + return ( + + + + ); + + const readOnly = row.original.readonly; + const loading = isSettlementLoading(row.original.id); + const settled = Boolean(row.original.isSettled); + + return ( + + + + + + {settled ? "Desfazer pagamento" : "Marcar como pago"} + + + ); + })()} + + + + + + + handleViewDetails(row.original)} + > + + Detalhes + + {row.original.userId === currentUserId && ( + handleEdit(row.original)} + disabled={row.original.readonly} + > + + Editar + + )} + {row.original.categoriaName !== "Pagamentos" && + row.original.userId === currentUserId && ( + handleCopy(row.original)}> + + Copiar + + )} + {row.original.categoriaName !== "Pagamentos" && + row.original.userId !== currentUserId && ( + handleImport(row.original)}> + + Importar para Minha Conta + + )} + {row.original.userId === currentUserId && ( + handleConfirmDelete(row.original)} + disabled={row.original.readonly} + > + + Remover + + )} + + {/* Opções de Antecipação */} + {row.original.userId === currentUserId && + row.original.condition === "Parcelado" && + row.original.seriesId && ( + <> + + + {!row.original.isAnticipated && onAnticipate && ( + handleAnticipate(row.original)} + > + + Antecipar Parcelas + + )} + + {onViewAnticipationHistory && ( + + handleViewAnticipationHistory(row.original) + } + > + + Histórico de Antecipações + + )} + + {row.original.isAnticipated && ( + + + Parcela Antecipada + + )} + + )} + + +
+ ), + }); + } + + return columns; +} + +export function getTransactionColumns( + args: BuildColumnsArgs, +): ColumnDef[] { + const built = buildColumns(args); + const order = args.columnOrder?.length + ? args.columnOrder + : DEFAULT_LANCAMENTOS_COLUMN_ORDER; + return reorderColumnsByPreference(built, order); +} diff --git a/src/features/transactions/components/table/transactions-pagination.tsx b/src/features/transactions/components/table/transactions-pagination.tsx new file mode 100644 index 0000000..f650970 --- /dev/null +++ b/src/features/transactions/components/table/transactions-pagination.tsx @@ -0,0 +1,106 @@ +import { + RiArrowLeftDoubleLine, + RiArrowLeftSLine, + RiArrowRightDoubleLine, + RiArrowRightSLine, +} from "@remixicon/react"; +import { Button } from "@/shared/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/shared/components/ui/select"; + +type TransactionsPaginationProps = { + totalRows: number; + currentPage: number; + currentPageSize: number; + totalPages: number; + canPreviousPage: boolean; + canNextPage: boolean; + onPageChange: (page: number) => void; + onPageSizeChange: (size: number) => void; +}; + +export function TransactionsPagination({ + totalRows, + currentPage, + currentPageSize, + totalPages, + canPreviousPage, + canNextPage, + onPageChange, + onPageSizeChange, +}: TransactionsPaginationProps) { + return ( +
+
+ + {totalRows} lançamentos + + +
+
+ + Página {currentPage} de {totalPages} + +
+ + + + +
+
+
+ ); +} diff --git a/src/features/transactions/components/table/transactions-table.tsx b/src/features/transactions/components/table/transactions-table.tsx index f8e68cc..37bb81e 100644 --- a/src/features/transactions/components/table/transactions-table.tsx +++ b/src/features/transactions/components/table/transactions-table.tsx @@ -1,30 +1,11 @@ "use client"; import { RiAddFill, - RiArrowLeftDoubleLine, RiArrowLeftRightLine, - RiArrowLeftSLine, - RiArrowRightDoubleLine, - RiArrowRightSLine, - RiAttachment2, - RiBankCard2Line, - RiChat1Line, - RiCheckboxBlankCircleLine, - RiCheckboxCircleFill, - RiCheckLine, - RiDeleteBin5Line, - RiFileCopyLine, RiFileExcel2Line, - RiFileList2Line, RiFlashlightFill, - RiGroupLine, - RiHistoryLine, - RiMoreFill, - RiPencilLine, - RiTimeLine, } from "@remixicon/react"; import { - type ColumnDef, flexRender, getCoreRowModel, getPaginationRowModel, @@ -34,46 +15,15 @@ import { useReactTable, type VisibilityState, } 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, - EstablishmentLogo, -} from "@/shared/components/entity-avatar"; -import MoneyValues from "@/shared/components/money-values"; -import { TransactionTypeBadge } from "@/shared/components/transaction-type-badge"; -import { - Avatar, - AvatarFallback, - AvatarImage, -} from "@/shared/components/ui/avatar"; -import { Badge } from "@/shared/components/ui/badge"; import { Button } from "@/shared/components/ui/button"; import { Card, CardContent } from "@/shared/components/ui/card"; -import { Checkbox } from "@/shared/components/ui/checkbox"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/shared/components/ui/dropdown-menu"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/shared/components/ui/select"; -import { Spinner } from "@/shared/components/ui/spinner"; import { Table, TableBody, @@ -88,10 +38,6 @@ import { TooltipProvider, TooltipTrigger, } from "@/shared/components/ui/tooltip"; -import { resolveLogoSrc } from "@/shared/lib/logo"; -import { getAvatarSrc } from "@/shared/lib/payers/utils"; -import { formatDate } from "@/shared/utils/date"; -import { getConditionIcon, getPaymentMethodIcon } from "@/shared/utils/icons"; import { cn } from "@/shared/utils/ui"; import { TransactionsExport } from "../transactions-export"; import type { @@ -99,676 +45,10 @@ import type { TransactionFilterOption, TransactionItem, } from "../types"; +import { TransactionsBulkBar } from "./transactions-bulk-bar"; +import { getTransactionColumns } from "./transactions-columns"; import { TransactionsFilters } from "./transactions-filters"; - -type BuildColumnsArgs = { - currentUserId: string; - noteAsColumn: boolean; - onEdit?: (item: TransactionItem) => void; - onCopy?: (item: TransactionItem) => void; - onImport?: (item: TransactionItem) => void; - onConfirmDelete?: (item: TransactionItem) => void; - onViewDetails?: (item: TransactionItem) => void; - onToggleSettlement?: (item: TransactionItem) => void; - onAnticipate?: (item: TransactionItem) => void; - onViewAnticipationHistory?: (item: TransactionItem) => void; - isSettlementLoading: (id: string) => boolean; - showActions: boolean; -}; - -function getPaymentMethodTableLabel(method: string) { - if (method === "Transferência bancária") { - return "Transf. bancária"; - } - - return method; -} - -const buildColumns = ({ - currentUserId, - noteAsColumn, - onEdit, - onCopy, - onImport, - onConfirmDelete, - onViewDetails, - onToggleSettlement, - onAnticipate, - onViewAnticipationHistory, - isSettlementLoading, - showActions, -}: BuildColumnsArgs): ColumnDef[] => { - const noop = () => undefined; - const handleEdit = onEdit ?? noop; - const handleCopy = onCopy ?? noop; - const handleImport = onImport ?? noop; - const handleConfirmDelete = onConfirmDelete ?? noop; - const handleViewDetails = onViewDetails ?? noop; - const handleToggleSettlement = onToggleSettlement ?? noop; - const handleAnticipate = onAnticipate ?? noop; - const handleViewAnticipationHistory = onViewAnticipationHistory ?? noop; - - const columns: ColumnDef[] = [ - { - id: "select", - header: ({ table }) => ( - table.toggleAllPageRowsSelected(!!value)} - aria-label="Selecionar todos" - /> - ), - cell: ({ row }) => ( - row.toggleSelected(!!value)} - aria-label="Selecionar linha" - /> - ), - enableSorting: false, - enableHiding: false, - }, - { - id: "purchaseDate", - accessorKey: "purchaseDate", - header: () => null, - cell: () => null, - }, - { - accessorKey: "name", - header: "Estabelecimento", - cell: ({ row }) => { - const { - name, - purchaseDate, - installmentCount, - currentInstallment, - paymentMethod, - dueDate, - note, - isDivided, - isAnticipated, - hasAttachments, - } = row.original; - - const installmentBadge = - currentInstallment && installmentCount - ? `${currentInstallment} de ${installmentCount}` - : null; - - const isBoleto = paymentMethod === "Boleto" && dueDate; - const dueDateLabel = - isBoleto && dueDate ? `venc. ${formatDate(dueDate)}` : null; - const hasNote = Boolean(note?.trim().length); - const isLastInstallment = - currentInstallment === installmentCount && - installmentCount && - installmentCount > 1; - - return ( - - - - - - {formatDate(purchaseDate)} - - {dueDateLabel ? ( - {dueDateLabel} - ) : null} - - - - - - {name} - - - - {name} - - - - {isDivided && ( - - - - - - Dividido entre pagadores - - - - - Dividido entre pagadores - - - )} - - {isLastInstallment ? ( - - - - Última parcela - Última parcela - - - Última parcela! - - ) : null} - - {installmentBadge ? ( - - {installmentBadge} - - ) : null} - - {isAnticipated && ( - - - - - Parcela antecipada - - - - Parcela antecipada - - - )} - - {!noteAsColumn && hasNote ? ( - - - - - Ver anotação - - - - {note} - - - ) : null} - - {hasAttachments ? ( - - - - - Possui anexos - - - Possui anexos - - ) : null} - - - - ); - }, - }, - { - accessorKey: "transactionType", - header: "Transação", - cell: ({ row }) => { - const type = - row.original.categoriaName === "Saldo inicial" - ? "Saldo inicial" - : row.original.transactionType; - - return ( - - ); - }, - }, - { - accessorKey: "amount", - header: "Valor", - cell: ({ row }) => { - const isReceita = row.original.transactionType === "Receita"; - const isTransfer = row.original.transactionType === "Transferência"; - - return ( - - ); - }, - }, - { - accessorKey: "condition", - header: "Condição", - cell: ({ row }) => { - const condition = row.original.condition; - const icon = getConditionIcon(condition); - return ( - - {icon} - {condition} - - ); - }, - }, - { - accessorKey: "paymentMethod", - header: "Forma de Pagamento", - cell: ({ row }) => { - const method = row.original.paymentMethod; - const icon = getPaymentMethodIcon(method); - return ( - - {icon} - {getPaymentMethodTableLabel(method)} - - ); - }, - }, - { - accessorKey: "categoriaName", - header: "Categoria", - cell: ({ row }) => { - const { categoriaName, categoriaIcon } = row.original; - - if (!categoriaName) { - return ; - } - - return ( - - - {categoriaName} - - ); - }, - }, - { - accessorKey: "pagadorName", - header: "Pagador", - cell: ({ row }) => { - const { payerId, pagadorName, pagadorAvatar } = row.original; - - const label = pagadorName?.trim() || "Sem pagador"; - const displayName = label.split(/\s+/)[0] ?? label; - const avatarSrc = getAvatarSrc(pagadorAvatar); - const initial = displayName.charAt(0).toUpperCase() || "?"; - const content = ( - <> - - - - {initial} - - - {displayName} - - ); - - if (!payerId) { - return ( - {content} - ); - } - - return ( - - {content} - - ); - }, - }, - { - id: "contaCartao", - header: "Conta/Cartão", - cell: ({ row }) => { - const { - cartaoName, - contaName, - cartaoLogo, - contaLogo, - cardId, - accountId, - userId, - } = row.original; - const isCartao = Boolean(cartaoName); - const label = cartaoName ?? contaName; - const logoSrc = resolveLogoSrc(cartaoLogo ?? contaLogo); - const href = cardId - ? `/cards/${cardId}/invoice` - : accountId - ? `/accounts/${accountId}/statement` - : null; - const isOwnData = userId === currentUserId; - - const content = ( - - {logoSrc && ( - {`Logo - )} - {label} - - ); - - if (!isOwnData || !href) { - return ( - - {content} - - {isCartao ? "Cartão" : "Conta"}: {label} - - - ); - } - - return ( - - - - {logoSrc && ( - {`Logo - )} - {label} - - - - {isCartao ? "Cartão" : "Conta"}: {label} - - - ); - }, - }, - ]; - - if (noteAsColumn) { - const accountCardIndex = columns.findIndex((c) => c.id === "contaCartao"); - const noteColumn: ColumnDef = { - accessorKey: "note", - header: "Anotação", - cell: ({ row }) => { - const note = row.original.note; - if (!note?.trim()) - return ; - return ( - - {note} - - ); - }, - }; - columns.splice(accountCardIndex, 0, noteColumn); - } - - if (showActions) { - columns.push({ - id: "actions", - header: "Ações", - enableSorting: false, - cell: ({ row }) => ( -
- {(() => { - const paymentMethod = row.original.paymentMethod; - const showSettlementButton = [ - "Pix", - "Boleto", - "Cartão de crédito", - "Dinheiro", - "Cartão de débito", - "Transferência bancária", - "Pré-Pago | VR/VA", - ].includes(paymentMethod); - - if (!showSettlementButton) { - return null; - } - - const canToggleSettlement = - paymentMethod === "Pix" || - paymentMethod === "Boleto" || - paymentMethod === "Dinheiro" || - paymentMethod === "Cartão de débito" || - paymentMethod === "Transferência bancária" || - paymentMethod === "Pré-Pago | VR/VA"; - - if (!canToggleSettlement) - return ( - - - - ); - - const readOnly = row.original.readonly; - const loading = isSettlementLoading(row.original.id); - const settled = Boolean(row.original.isSettled); - - return ( - - - - - - {settled ? "Desfazer pagamento" : "Marcar como pago"} - - - ); - })()} - - - - - - - handleViewDetails(row.original)} - > - - Detalhes - - {row.original.userId === currentUserId && ( - handleEdit(row.original)} - disabled={row.original.readonly} - > - - Editar - - )} - {row.original.categoriaName !== "Pagamentos" && - row.original.userId === currentUserId && ( - handleCopy(row.original)}> - - Copiar - - )} - {row.original.categoriaName !== "Pagamentos" && - row.original.userId !== currentUserId && ( - handleImport(row.original)}> - - Importar para Minha Conta - - )} - {row.original.userId === currentUserId && ( - handleConfirmDelete(row.original)} - disabled={row.original.readonly} - > - - Remover - - )} - - {/* Opções de Antecipação */} - {row.original.userId === currentUserId && - row.original.condition === "Parcelado" && - row.original.seriesId && ( - <> - - - {!row.original.isAnticipated && onAnticipate && ( - handleAnticipate(row.original)} - > - - Antecipar Parcelas - - )} - - {onViewAnticipationHistory && ( - - handleViewAnticipationHistory(row.original) - } - > - - Histórico de Antecipações - - )} - - {row.original.isAnticipated && ( - - - Parcela Antecipada - - )} - - )} - - -
- ), - }); - } - - return columns; -}; - -const FIXED_START_IDS = ["select", "purchaseDate"]; -const FIXED_END_IDS = ["actions"]; - -function getColumnId(col: ColumnDef): string { - const c = col as { id?: string; accessorKey?: string }; - return c.id ?? c.accessorKey ?? ""; -} - -function reorderColumnsByPreference( - columns: ColumnDef[], - orderPreference: string[] | null | undefined, -): ColumnDef[] { - if (!orderPreference || orderPreference.length === 0) return columns; - - const order = orderPreference; - const fixedStart: ColumnDef[] = []; - const reorderable: ColumnDef[] = []; - const fixedEnd: ColumnDef[] = []; - - for (const col of columns) { - const id = getColumnId(col as ColumnDef); - if (FIXED_START_IDS.includes(id)) fixedStart.push(col); - else if (FIXED_END_IDS.includes(id)) fixedEnd.push(col); - else reorderable.push(col); - } - - const sorted = [...reorderable].sort((a, b) => { - const idA = getColumnId(a as ColumnDef); - const idB = getColumnId(b as ColumnDef); - const indexA = order.indexOf(idA); - const indexB = order.indexOf(idB); - if (indexA === -1 && indexB === -1) return 0; - if (indexA === -1) return 1; - if (indexB === -1) return -1; - return indexA - indexB; - }); - - return [...fixedStart, ...sorted, ...fixedEnd]; -} +import { TransactionsPagination } from "./transactions-pagination"; type LancamentosTableProps = { data: TransactionItem[]; @@ -841,10 +121,27 @@ export function TransactionsTable({ const [rowSelection, setRowSelection] = useState({}); const isServerPaginated = Boolean(serverPagination); - const columns = useMemo(() => { - const built = buildColumns({ + const columns = useMemo( + () => + getTransactionColumns({ + currentUserId, + noteAsColumn, + onEdit, + onCopy, + onImport, + onConfirmDelete, + onViewDetails, + onToggleSettlement, + onAnticipate, + onViewAnticipationHistory, + isSettlementLoading: isSettlementLoading ?? (() => false), + showActions, + columnOrder: columnOrderPreference, + }), + [ currentUserId, noteAsColumn, + columnOrderPreference, onEdit, onCopy, onImport, @@ -853,44 +150,17 @@ export function TransactionsTable({ onToggleSettlement, onAnticipate, onViewAnticipationHistory, - isSettlementLoading: isSettlementLoading ?? (() => false), + isSettlementLoading, showActions, - }); - const order = columnOrderPreference?.length - ? columnOrderPreference - : DEFAULT_LANCAMENTOS_COLUMN_ORDER; - return reorderColumnsByPreference(built, order); - }, [ - currentUserId, - noteAsColumn, - columnOrderPreference, - onEdit, - onCopy, - onImport, - onConfirmDelete, - onViewDetails, - onToggleSettlement, - onAnticipate, - onViewAnticipationHistory, - isSettlementLoading, - showActions, - ]); + ], + ); const table = useReactTable({ data, columns, state: isServerPaginated - ? { - sorting, - columnVisibility, - rowSelection, - } - : { - sorting, - columnVisibility, - pagination, - rowSelection, - }, + ? { sorting, columnVisibility, rowSelection } + : { sorting, columnVisibility, pagination, rowSelection }, onSortingChange: setSorting, onPaginationChange: isServerPaginated ? undefined : setPagination, onRowSelectionChange: setRowSelection, @@ -927,43 +197,34 @@ export function TransactionsTable({ 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); const handleBulkDelete = () => { if (onBulkDelete && selectedCount > 0) { - const selectedItems = selectedRows.map((row) => row.original); - onBulkDelete(selectedItems); + onBulkDelete(selectedRows.map((row) => row.original)); setRowSelection({}); } }; const handleBulkImport = () => { if (onBulkImport && selectedCount > 0) { - const selectedItems = selectedRows.map((row) => row.original); - onBulkImport(selectedItems); + onBulkImport(selectedRows.map((row) => row.original)); setRowSelection({}); } }; - 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; @@ -971,6 +232,25 @@ export function TransactionsTable({ setRowSelection({}); }; + const handlePageChange = (nextPage: number) => { + if (isServerPaginated) { + navigateToPage(nextPage); + } else { + table.setPageIndex(nextPage - 1); + } + }; + + const handlePageSizeChange = (size: number) => { + if (isServerPaginated) { + navigateToPage(1, size); + } else { + table.setPageSize(size); + } + }; + + const showTopControls = + Boolean(onCreate) || Boolean(onMassAdd) || showFilters; + return ( {showTopControls ? ( @@ -1060,65 +340,23 @@ export function TransactionsTable({ {selectedCount > 0 && onBulkDelete && selectedRows.every((row) => row.original.userId === currentUserId) ? ( -
-
- - {selectedCount}{" "} - {selectedCount === 1 ? "item selecionado" : "itens selecionados"} - - - - - - - Total:{" "} - - -
- -
+ ) : null} {selectedCount > 0 && onBulkImport && selectedRows.some((row) => row.original.userId !== currentUserId) ? ( -
-
- - {selectedCount}{" "} - {selectedCount === 1 ? "item selecionado" : "itens selecionados"} - - - - - - - Total:{" "} - - -
- -
+ ) : null} @@ -1173,97 +411,16 @@ export function TransactionsTable({ -
-
- - {totalRows} lançamentos - - -
-
- - Página {currentPage} de {totalPages} - -
- - - - -
-
-
+ ) : (