- {data.map((item) => (
-
+
+ {groups.map((group, groupIndex) => (
+
+
+ {group.label}
+
+
+ {group.items.map((item) => (
+
+ ))}
+
+
))}
);
@@ -90,6 +150,7 @@ export function TransactionsMobileList({
type TransactionMobileCardProps = Omit
& {
item: TransactionItem;
+ showDate?: boolean;
};
function TransactionMobileCard({
@@ -108,6 +169,7 @@ function TransactionMobileCard({
onConvertToRecurring,
isSettlementLoading,
showActions = true,
+ showDate = false,
}: TransactionMobileCardProps) {
const installmentBadge =
item.currentInstallment && item.installmentCount
@@ -156,10 +218,12 @@ function TransactionMobileCard({
{item.name}
-
-
- {formatDate(item.purchaseDate)}
-
+ {showDate ? (
+
+
+ {formatDate(item.purchaseDate)}
+
+ ) : null}
{dueDateLabel ? (
{dueDateLabel}
diff --git a/src/features/transactions/components/table/transactions-table.tsx b/src/features/transactions/components/table/transactions-table.tsx
index 4b78aa2..f7de06c 100644
--- a/src/features/transactions/components/table/transactions-table.tsx
+++ b/src/features/transactions/components/table/transactions-table.tsx
@@ -9,13 +9,14 @@ import {
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
+ type Row,
type RowSelectionState,
type SortingState,
useReactTable,
type VisibilityState,
} from "@tanstack/react-table";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
-import { type ReactNode, useMemo, useState } from "react";
+import { Fragment, type ReactNode, useMemo, useState } from "react";
import type {
TransactionsExportContext,
TransactionsPaginationState,
@@ -37,6 +38,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/shared/components/ui/tooltip";
+import { formatDateGroupLabel } from "@/shared/utils/date";
import { cn } from "@/shared/utils/ui";
import { TransactionsExport } from "../transactions-export";
import type {
@@ -79,6 +81,7 @@ type TransactionsTableProps = {
isSettlementLoading?: (id: string) => boolean;
showActions?: boolean;
showFilters?: boolean;
+ groupTransactionsByDate?: boolean;
};
export function TransactionsTable({
@@ -110,6 +113,7 @@ export function TransactionsTable({
isSettlementLoading,
showActions = true,
showFilters = true,
+ groupTransactionsByDate = true,
}: TransactionsTableProps) {
const router = useRouter();
const pathname = usePathname();
@@ -145,12 +149,14 @@ export function TransactionsTable({
onViewAnticipationHistory,
isSettlementLoading: isSettlementLoading ?? (() => false),
showActions,
+ showDateGroups: groupTransactionsByDate,
columnOrder: columnOrderPreference,
}),
[
currentUserId,
noteAsColumn,
columnOrderPreference,
+ groupTransactionsByDate,
onEdit,
onCopy,
onImport,
@@ -191,6 +197,24 @@ export function TransactionsTable({
const rowModel = table.getRowModel();
const hasRows = rowModel.rows.length > 0;
+ const groupedRows = rowModel.rows.reduce<
+ Array<{ date: string; label: string; rows: Row[] }>
+ >((acc, row) => {
+ const date = row.original.purchaseDate?.slice(0, 10) ?? "";
+ const existingGroup = acc.find((group) => group.date === date);
+ if (existingGroup) {
+ existingGroup.rows.push(row);
+ return acc;
+ }
+
+ acc.push({
+ date,
+ label: formatDateGroupLabel(row.original.purchaseDate),
+ rows: [row],
+ });
+ return acc;
+ }, []);
+ const visibleColumnCount = table.getVisibleLeafColumns().length;
const totalRows = isServerPaginated
? (serverPagination?.totalItems ?? 0)
: table.getCoreRowModel().rows.length;
@@ -275,6 +299,25 @@ export function TransactionsTable({
const showTopControls =
Boolean(createSlot) || Boolean(onMassAdd) || showFilters;
+ const renderTransactionRow = (row: Row) => (
+
+ {row.getVisibleCells().map((cell) => (
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ ))}
+
+ );
return (
@@ -366,7 +409,7 @@ export function TransactionsTable({
) : null}
-
+
{hasRows ? (
<>
false)}
showActions={showActions}
+ showDateGroups={groupTransactionsByDate}
/>
@@ -407,28 +451,23 @@ export function TransactionsTable({
))}
- {rowModel.rows.map((row) => (
-
- {row.getVisibleCells().map((cell) => (
-
- {flexRender(
- cell.column.columnDef.cell,
- cell.getContext(),
- )}
-
- ))}
-
- ))}
+ {groupTransactionsByDate
+ ? groupedRows.map((group, groupIndex) => (
+
+
+
+ {group.label}
+
+
+ {group.rows.map(renderTransactionRow)}
+
+ ))
+ : rowModel.rows.map(renderTransactionRow)}
diff --git a/src/shared/utils/date.ts b/src/shared/utils/date.ts
index ff27cf1..5615396 100644
--- a/src/shared/utils/date.ts
+++ b/src/shared/utils/date.ts
@@ -331,6 +331,45 @@ export function formatDate(value: string | Date | null | undefined): string {
.replace(" de", "");
}
+/**
+ * Formats a date-only value as a compact group label.
+ * @example
+ * formatDateGroupLabel("2026-06-26") // "SEX, 26 JUN 2026"
+ */
+export function formatDateGroupLabel(
+ value: string | Date | null | undefined,
+): string {
+ const dateString = toDateOnlyString(value);
+ if (!dateString) {
+ return "—";
+ }
+
+ const parsed = parseUtcDateString(dateString);
+ if (!parsed) {
+ return "—";
+ }
+
+ const parts = new Intl.DateTimeFormat("pt-BR", {
+ weekday: "short",
+ day: "2-digit",
+ month: "short",
+ year: "numeric",
+ timeZone: "UTC",
+ }).formatToParts(parsed);
+ const weekday = parts.find((part) => part.type === "weekday")?.value;
+ const day = parts.find((part) => part.type === "day")?.value;
+ const month = parts.find((part) => part.type === "month")?.value;
+ const year = parts.find((part) => part.type === "year")?.value;
+
+ if (!weekday || !day || !month || !year) {
+ return "—";
+ }
+
+ return `${weekday.replace(".", "").toUpperCase()}, ${day} ${month
+ .replace(".", "")
+ .toUpperCase()} ${year}`;
+}
+
/**
* Formats a date-only value (YYYY-MM-DD) using UTC to preserve the civil day
*/