From 3f1fd55a7b97403a9792c73eaaf4877b050de24c Mon Sep 17 00:00:00 2001
From: Neil <55785687+carkom@users.noreply.github.com>
Date: Mon, 20 May 2024 17:23:59 +0100
Subject: [PATCH] Custom Reports: Table Totals Callback (#2768)

* Table Totals Callback

* notes

* Update packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableRow.tsx

Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>

---------

Co-authored-by: Matiss Janis Aboltins <matiss@mja.lv>
---
 .../src/components/reports/ChooseGraph.tsx    |  67 +---
 .../graphs/tableGraph/RenderTableRow.tsx      |   9 -
 .../reports/graphs/tableGraph/ReportTable.tsx | 133 +++++--
 .../graphs/tableGraph/ReportTableHeader.tsx   |  10 +-
 .../graphs/tableGraph/ReportTableList.tsx     |  12 -
 .../graphs/tableGraph/ReportTableRow.tsx      | 359 +++++++++---------
 .../graphs/tableGraph/ReportTableTotals.tsx   | 307 ++++-----------
 .../spreadsheets/custom-spreadsheet.ts        |   2 +-
 .../reports/spreadsheets/recalculate.ts       |   2 +-
 .../loot-core/src/types/models/reports.d.ts   |   3 +-
 upcoming-release-notes/2768.md                |   6 +
 11 files changed, 391 insertions(+), 519 deletions(-)
 create mode 100644 upcoming-release-notes/2768.md

diff --git a/packages/desktop-client/src/components/reports/ChooseGraph.tsx b/packages/desktop-client/src/components/reports/ChooseGraph.tsx
index 6d2e557e1..b195b7eec 100644
--- a/packages/desktop-client/src/components/reports/ChooseGraph.tsx
+++ b/packages/desktop-client/src/components/reports/ChooseGraph.tsx
@@ -5,7 +5,6 @@ import { type RuleConditionEntity } from 'loot-core/types/models/rule';
 
 import { type CSSProperties } from '../../style';
 import { styles } from '../../style/styles';
-import { View } from '../common/View';
 
 import { AreaGraph } from './graphs/AreaGraph';
 import { BarGraph } from './graphs/BarGraph';
@@ -14,8 +13,6 @@ import { DonutGraph } from './graphs/DonutGraph';
 import { LineGraph } from './graphs/LineGraph';
 import { StackedBarGraph } from './graphs/StackedBarGraph';
 import { ReportTable } from './graphs/tableGraph/ReportTable';
-import { ReportTableHeader } from './graphs/tableGraph/ReportTableHeader';
-import { ReportTableTotals } from './graphs/tableGraph/ReportTableTotals';
 import { ReportOptions } from './ReportOptions';
 
 type ChooseGraphProps = {
@@ -170,51 +167,25 @@ export function ChooseGraph({
   }
   if (graphType === 'TableGraph') {
     return (
-      <View>
-        <ReportTableHeader
-          headerScrollRef={headerScrollRef}
-          handleScroll={handleScroll}
-          data={data.intervalData}
-          groupBy={groupBy}
-          interval={interval}
-          balanceType={balanceType}
-          compact={compact}
-          style={rowStyle}
-          compactStyle={compactStyle}
-          mode={mode}
-        />
-        <ReportTable
-          saveScrollWidth={saveScrollWidth}
-          listScrollRef={listScrollRef}
-          handleScroll={handleScroll}
-          balanceTypeOp={balanceTypeOp}
-          groupBy={groupBy}
-          data={data}
-          filters={filters}
-          mode={mode}
-          intervalsCount={intervalsCount}
-          compact={compact}
-          style={rowStyle}
-          compactStyle={compactStyle}
-          showHiddenCategories={showHiddenCategories}
-          showOffBudget={showOffBudget}
-        />
-        <ReportTableTotals
-          totalScrollRef={totalScrollRef}
-          handleScroll={handleScroll}
-          data={data}
-          mode={mode}
-          balanceTypeOp={balanceTypeOp}
-          intervalsCount={intervalsCount}
-          compact={compact}
-          style={rowStyle}
-          compactStyle={compactStyle}
-          groupBy={groupBy}
-          filters={filters}
-          showHiddenCategories={showHiddenCategories}
-          showOffBudget={showOffBudget}
-        />
-      </View>
+      <ReportTable
+        saveScrollWidth={saveScrollWidth}
+        headerScrollRef={headerScrollRef}
+        listScrollRef={listScrollRef}
+        totalScrollRef={totalScrollRef}
+        handleScroll={handleScroll}
+        balanceTypeOp={balanceTypeOp}
+        groupBy={groupBy}
+        data={data}
+        filters={filters}
+        mode={mode}
+        intervalsCount={intervalsCount}
+        interval={interval}
+        compact={compact}
+        style={rowStyle}
+        compactStyle={compactStyle}
+        showHiddenCategories={showHiddenCategories}
+        showOffBudget={showOffBudget}
+      />
     );
   }
   return null;
diff --git a/packages/desktop-client/src/components/reports/graphs/tableGraph/RenderTableRow.tsx b/packages/desktop-client/src/components/reports/graphs/tableGraph/RenderTableRow.tsx
index aad73c342..3b1b92c5c 100644
--- a/packages/desktop-client/src/components/reports/graphs/tableGraph/RenderTableRow.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/tableGraph/RenderTableRow.tsx
@@ -10,25 +10,19 @@ import { type renderRowProps } from './ReportTable';
 type RenderTableRowProps = {
   index: number;
   parent_index?: number;
-  compact: boolean;
   renderRow: (arg: renderRowProps) => ReactNode;
-  intervalsCount: number;
   mode: string;
   metadata: GroupedEntity[];
   style?: CSSProperties;
-  compactStyle?: CSSProperties;
 };
 
 export function RenderTableRow({
   index,
   parent_index,
-  compact,
   renderRow,
-  intervalsCount,
   mode,
   metadata,
   style,
-  compactStyle,
 }: RenderTableRowProps) {
   const child = metadata[index];
   const parent =
@@ -45,10 +39,7 @@ export function RenderTableRow({
       {renderRow({
         item,
         mode,
-        intervalsCount,
-        compact,
         style,
-        compactStyle,
       })}
     </View>
   );
diff --git a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx
index c45be017e..78882fc98 100644
--- a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTable.tsx
@@ -16,12 +16,16 @@ import { type CSSProperties } from '../../../../style';
 import { Block } from '../../../common/Block';
 import { View } from '../../../common/View';
 
+import { ReportTableHeader } from './ReportTableHeader';
 import { ReportTableList } from './ReportTableList';
 import { ReportTableRow } from './ReportTableRow';
+import { ReportTableTotals } from './ReportTableTotals';
 
 type ReportTableProps = {
   saveScrollWidth: (value: number) => void;
+  headerScrollRef: RefObject<HTMLDivElement>;
   listScrollRef: RefObject<HTMLDivElement>;
+  totalScrollRef: RefObject<HTMLDivElement>;
   handleScroll: UIEventHandler<HTMLDivElement>;
   groupBy: string;
   balanceTypeOp: 'totalDebts' | 'totalTotals' | 'totalAssets';
@@ -29,6 +33,7 @@ type ReportTableProps = {
   filters?: RuleConditionEntity[];
   mode: string;
   intervalsCount: number;
+  interval: string;
   compact: boolean;
   style?: CSSProperties;
   compactStyle?: CSSProperties;
@@ -36,18 +41,25 @@ type ReportTableProps = {
   showOffBudget?: boolean;
 };
 
+export type renderTotalsProps = {
+  metadata: GroupedEntity;
+  mode: string;
+  totalsStyle: CSSProperties;
+  testStyle: CSSProperties;
+  scrollWidthTotals: number;
+};
+
 export type renderRowProps = {
   item: GroupedEntity;
   mode: string;
-  intervalsCount: number;
-  compact: boolean;
   style?: CSSProperties;
-  compactStyle?: CSSProperties;
 };
 
 export function ReportTable({
   saveScrollWidth,
+  headerScrollRef,
   listScrollRef,
+  totalScrollRef,
   handleScroll,
   groupBy,
   balanceTypeOp,
@@ -55,6 +67,7 @@ export function ReportTable({
   filters,
   mode,
   intervalsCount,
+  interval,
   compact,
   style,
   compactStyle,
@@ -69,18 +82,37 @@ export function ReportTable({
     }
   });
 
-  const renderRow = useCallback(
+  const renderRow = useCallback(({ item, mode, style }: renderRowProps) => {
+    return (
+      <ReportTableRow
+        item={item}
+        balanceTypeOp={balanceTypeOp}
+        groupBy={groupBy}
+        mode={mode}
+        filters={filters}
+        startDate={data.startDate}
+        endDate={data.endDate}
+        intervalsCount={intervalsCount}
+        compact={compact}
+        style={style}
+        compactStyle={compactStyle}
+        showHiddenCategories={showHiddenCategories}
+        showOffBudget={showOffBudget}
+      />
+    );
+  }, []);
+
+  const renderTotals = useCallback(
     ({
-      item,
+      metadata,
       mode,
-      intervalsCount,
-      compact,
-      style,
-      compactStyle,
-    }: renderRowProps) => {
+      totalsStyle,
+      testStyle,
+      scrollWidthTotals,
+    }: renderTotalsProps) => {
       return (
         <ReportTableRow
-          item={item}
+          item={metadata}
           balanceTypeOp={balanceTypeOp}
           groupBy={groupBy}
           mode={mode}
@@ -89,10 +121,14 @@ export function ReportTable({
           endDate={data.endDate}
           intervalsCount={intervalsCount}
           compact={compact}
-          style={style}
+          style={totalsStyle}
           compactStyle={compactStyle}
           showHiddenCategories={showHiddenCategories}
           showOffBudget={showOffBudget}
+          totalStyle={testStyle}
+          totalScrollRef={totalScrollRef}
+          handleScroll={handleScroll}
+          height={32 + scrollWidthTotals}
         />
       );
     },
@@ -100,39 +136,58 @@ export function ReportTable({
   );
 
   return (
-    <View
-      style={{
-        flex: 1,
-        flexDirection: 'row',
-        outline: 'none',
-        '& .animated .animated-row': { transition: '.25s transform' },
-      }}
-      tabIndex={1}
-    >
-      <Block
-        innerRef={listScrollRef}
-        onScroll={handleScroll}
-        id="list"
+    <View>
+      <ReportTableHeader
+        headerScrollRef={headerScrollRef}
+        handleScroll={handleScroll}
+        data={data.intervalData}
+        groupBy={groupBy}
+        interval={interval}
+        balanceTypeOp={balanceTypeOp}
+        compact={compact}
+        style={style}
+        compactStyle={compactStyle}
+        mode={mode}
+      />
+      <View
         style={{
-          overflowY: 'auto',
-          scrollbarWidth: 'none',
-          '::-webkit-scrollbar': { display: 'none' },
           flex: 1,
+          flexDirection: 'row',
           outline: 'none',
           '& .animated .animated-row': { transition: '.25s transform' },
         }}
+        tabIndex={1}
       >
-        <ReportTableList
-          data={data}
-          intervalsCount={intervalsCount}
-          mode={mode}
-          groupBy={groupBy}
-          renderRow={renderRow}
-          compact={compact}
-          style={style}
-          compactStyle={compactStyle}
-        />
-      </Block>
+        <Block
+          innerRef={listScrollRef}
+          onScroll={handleScroll}
+          id="list"
+          style={{
+            overflowY: 'auto',
+            scrollbarWidth: 'none',
+            '::-webkit-scrollbar': { display: 'none' },
+            flex: 1,
+            outline: 'none',
+            '& .animated .animated-row': { transition: '.25s transform' },
+          }}
+        >
+          <ReportTableList
+            data={data}
+            mode={mode}
+            groupBy={groupBy}
+            renderRow={renderRow}
+            style={style}
+          />
+        </Block>
+      </View>
+      <ReportTableTotals
+        data={data}
+        mode={mode}
+        totalScrollRef={totalScrollRef}
+        compact={compact}
+        style={style}
+        renderTotals={renderTotals}
+      />
     </View>
   );
 }
diff --git a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableHeader.tsx b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableHeader.tsx
index 4543fda45..2dea557de 100644
--- a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableHeader.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableHeader.tsx
@@ -12,12 +12,12 @@ type ReportTableHeaderProps = {
   groupBy: string;
   interval: string;
   data: IntervalEntity[];
-  balanceType: string;
+  balanceTypeOp: 'totalDebts' | 'totalTotals' | 'totalAssets';
   headerScrollRef: RefObject<HTMLDivElement>;
   handleScroll: UIEventHandler<HTMLDivElement>;
   compact: boolean;
-  style: CSSProperties;
-  compactStyle: CSSProperties;
+  style?: CSSProperties;
+  compactStyle?: CSSProperties;
   mode: string;
 };
 
@@ -25,7 +25,7 @@ export function ReportTableHeader({
   groupBy,
   interval,
   data,
-  balanceType,
+  balanceTypeOp,
   headerScrollRef,
   handleScroll,
   compact,
@@ -84,7 +84,7 @@ export function ReportTableHeader({
                 />
               );
             })
-          : balanceType === 'Net' && (
+          : balanceTypeOp === 'totalTotals' && (
               <>
                 <Cell
                   style={{
diff --git a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableList.tsx b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableList.tsx
index f2076605a..f3ba6bad8 100644
--- a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableList.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableList.tsx
@@ -15,23 +15,17 @@ import { type renderRowProps } from './ReportTable';
 type ReportTableListProps = {
   data: DataEntity;
   mode: string;
-  intervalsCount: number;
   groupBy: string;
   renderRow: (arg: renderRowProps) => ReactNode;
-  compact: boolean;
   style?: CSSProperties;
-  compactStyle?: CSSProperties;
 };
 
 export function ReportTableList({
   data,
-  intervalsCount,
   mode,
   groupBy,
   renderRow,
-  compact,
   style,
-  compactStyle,
 }: ReportTableListProps) {
   const metadata: GroupedEntity[] | undefined =
     groupBy === 'Category'
@@ -60,9 +54,7 @@ export function ReportTableList({
               <View key={index}>
                 <RenderTableRow
                   index={index}
-                  compact={compact}
                   renderRow={renderRow}
-                  intervalsCount={intervalsCount}
                   mode={mode}
                   metadata={metadata}
                   style={{
@@ -73,7 +65,6 @@ export function ReportTableList({
                     }),
                     ...style,
                   }}
-                  compactStyle={compactStyle}
                 />
                 {item.categories && (
                   <>
@@ -84,14 +75,11 @@ export function ReportTableList({
                             <RenderTableRow
                               key={category.id}
                               index={i}
-                              compact={compact}
                               renderRow={renderRow}
-                              intervalsCount={intervalsCount}
                               mode={mode}
                               metadata={metadata}
                               parent_index={index}
                               style={style}
-                              compactStyle={compactStyle}
                             />
                           );
                         },
diff --git a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableRow.tsx b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableRow.tsx
index d759a90d7..cf3de8cf1 100644
--- a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableRow.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableRow.tsx
@@ -1,4 +1,4 @@
-import React, { memo } from 'react';
+import React, { memo, type RefObject, type UIEventHandler } from 'react';
 
 import {
   amountToCurrency,
@@ -13,6 +13,7 @@ import { useCategories } from '../../../../hooks/useCategories';
 import { useNavigate } from '../../../../hooks/useNavigate';
 import { useResponsive } from '../../../../ResponsiveProvider';
 import { type CSSProperties, theme } from '../../../../style';
+import { View } from '../../../common/View';
 import { Row, Cell } from '../../../table';
 import { showActivity } from '../showActivity';
 
@@ -28,8 +29,12 @@ type ReportTableRowProps = {
   compact: boolean;
   style?: CSSProperties;
   compactStyle?: CSSProperties;
+  totalStyle?: CSSProperties;
   showHiddenCategories?: boolean;
   showOffBudget?: boolean;
+  totalScrollRef?: RefObject<HTMLDivElement>;
+  handleScroll?: UIEventHandler<HTMLDivElement>;
+  height?: number;
 };
 
 export const ReportTableRow = memo(
@@ -45,8 +50,12 @@ export const ReportTableRow = memo(
     compact,
     style,
     compactStyle,
+    totalStyle,
     showHiddenCategories = false,
     showOffBudget = false,
+    totalScrollRef,
+    handleScroll,
+    height,
   }: ReportTableRowProps) => {
     const average = amountToInteger(item[balanceTypeOp]) / intervalsCount;
     const groupByItem = groupBy === 'Interval' ? 'date' : 'name';
@@ -77,6 +86,7 @@ export const ReportTableRow = memo(
     return (
       <Row
         key={item.id}
+        height={height}
         collapsed={true}
         style={{
           color: theme.tableText,
@@ -84,176 +94,187 @@ export const ReportTableRow = memo(
           ...style,
         }}
       >
-        <Cell
-          value={item[groupByItem]}
-          title={item[groupByItem] ?? undefined}
+        <View
+          innerRef={totalScrollRef}
+          onScroll={handleScroll}
+          id={totalScrollRef ? 'total' : item.id}
           style={{
-            width: compact ? 80 : 125,
-            flexShrink: 0,
+            flexDirection: 'row',
+            flex: 1,
+            ...totalStyle,
           }}
-          valueStyle={compactStyle}
-        />
-        {item.intervalData && mode === 'time'
-          ? item.intervalData.map(intervalItem => {
-              return (
-                <Cell
-                  key={amountToCurrency(intervalItem[balanceTypeOp])}
-                  style={{
-                    minWidth: compact ? 50 : 85,
-                  }}
-                  linkStyle={hoverUnderline}
-                  valueStyle={compactStyle}
-                  value={amountToCurrency(intervalItem[balanceTypeOp])}
-                  title={
-                    Math.abs(intervalItem[balanceTypeOp]) > 100000
-                      ? amountToCurrency(intervalItem[balanceTypeOp])
-                      : undefined
-                  }
-                  onClick={() =>
-                    !isNarrowWidth &&
-                    !['Group', 'Interval'].includes(groupBy) &&
-                    !categories.grouped.map(g => g.id).includes(item.id) &&
-                    showActivity({
-                      navigate,
-                      categories,
-                      accounts,
-                      balanceTypeOp,
-                      filters,
-                      showHiddenCategories,
-                      showOffBudget,
-                      type: 'time',
-                      startDate: intervalItem.dateLookup || '',
-                      field: groupBy.toLowerCase(),
-                      id: item.id,
-                    })
-                  }
-                  width="flex"
-                  privacyFilter
-                />
-              );
-            })
-          : balanceTypeOp === 'totalTotals' && (
-              <>
-                <Cell
-                  value={amountToCurrency(item.totalAssets)}
-                  title={
-                    Math.abs(item.totalAssets) > 100000
-                      ? amountToCurrency(item.totalAssets)
-                      : undefined
-                  }
-                  width="flex"
-                  privacyFilter
-                  style={{
-                    minWidth: compact ? 50 : 85,
-                  }}
-                  linkStyle={hoverUnderline}
-                  valueStyle={compactStyle}
-                  onClick={() =>
-                    !isNarrowWidth &&
-                    !['Group', 'Interval'].includes(groupBy) &&
-                    !categories.grouped.map(g => g.id).includes(item.id) &&
-                    showActivity({
-                      navigate,
-                      categories,
-                      accounts,
-                      balanceTypeOp,
-                      filters,
-                      showHiddenCategories,
-                      showOffBudget,
-                      type: 'assets',
-                      startDate,
-                      endDate,
-                      field: groupBy.toLowerCase(),
-                      id: item.id,
-                    })
-                  }
-                />
-                <Cell
-                  value={amountToCurrency(item.totalDebts)}
-                  title={
-                    Math.abs(item.totalDebts) > 100000
-                      ? amountToCurrency(item.totalDebts)
-                      : undefined
-                  }
-                  width="flex"
-                  privacyFilter
-                  style={{
-                    minWidth: compact ? 50 : 85,
-                  }}
-                  linkStyle={hoverUnderline}
-                  valueStyle={compactStyle}
-                  onClick={() =>
-                    !isNarrowWidth &&
-                    !['Group', 'Interval'].includes(groupBy) &&
-                    !categories.grouped.map(g => g.id).includes(item.id) &&
-                    showActivity({
-                      navigate,
-                      categories,
-                      accounts,
-                      balanceTypeOp,
-                      filters,
-                      showHiddenCategories,
-                      showOffBudget,
-                      type: 'debts',
-                      startDate,
-                      endDate,
-                      field: groupBy.toLowerCase(),
-                      id: item.id,
-                    })
-                  }
-                />
-              </>
-            )}
-        <Cell
-          value={amountToCurrency(item[balanceTypeOp])}
-          title={
-            Math.abs(item[balanceTypeOp]) > 100000
-              ? amountToCurrency(item[balanceTypeOp])
-              : undefined
-          }
-          style={{
-            fontWeight: 600,
-            minWidth: compact ? 50 : 85,
-          }}
-          linkStyle={hoverUnderline}
-          valueStyle={compactStyle}
-          onClick={() =>
-            !isNarrowWidth &&
-            !['Group', 'Interval'].includes(groupBy) &&
-            !categories.grouped.map(g => g.id).includes(item.id) &&
-            showActivity({
-              navigate,
-              categories,
-              accounts,
-              balanceTypeOp,
-              filters,
-              showHiddenCategories,
-              showOffBudget,
-              type: 'totals',
-              startDate,
-              endDate,
-              field: groupBy.toLowerCase(),
-              id: item.id,
-            })
-          }
-          width="flex"
-          privacyFilter
-        />
-        <Cell
-          value={integerToCurrency(Math.round(average))}
-          title={
-            Math.abs(Math.round(average / 100)) > 100000
-              ? integerToCurrency(Math.round(average))
-              : undefined
-          }
-          style={{
-            fontWeight: 600,
-            minWidth: compact ? 50 : 85,
-          }}
-          valueStyle={compactStyle}
-          width="flex"
-          privacyFilter
-        />
+        >
+          <Cell
+            value={item[groupByItem]}
+            title={item[groupByItem]}
+            style={{
+              width: compact ? 80 : 125,
+              flexShrink: 0,
+            }}
+            valueStyle={compactStyle}
+          />
+          {item.intervalData && mode === 'time'
+            ? item.intervalData.map((intervalItem, index) => {
+                return (
+                  <Cell
+                    key={index}
+                    style={{
+                      minWidth: compact ? 50 : 85,
+                    }}
+                    linkStyle={hoverUnderline}
+                    valueStyle={compactStyle}
+                    value={amountToCurrency(intervalItem[balanceTypeOp])}
+                    title={
+                      Math.abs(intervalItem[balanceTypeOp]) > 100000
+                        ? amountToCurrency(intervalItem[balanceTypeOp])
+                        : undefined
+                    }
+                    onClick={() =>
+                      !isNarrowWidth &&
+                      !['Group', 'Interval'].includes(groupBy) &&
+                      !categories.grouped.map(g => g.id).includes(item.id) &&
+                      showActivity({
+                        navigate,
+                        categories,
+                        accounts,
+                        balanceTypeOp,
+                        filters,
+                        showHiddenCategories,
+                        showOffBudget,
+                        type: 'time',
+                        startDate: intervalItem.intervalStartDate || '',
+                        field: groupBy.toLowerCase(),
+                        id: item.id,
+                      })
+                    }
+                    width="flex"
+                    privacyFilter
+                  />
+                );
+              })
+            : balanceTypeOp === 'totalTotals' && (
+                <>
+                  <Cell
+                    value={amountToCurrency(item.totalAssets)}
+                    title={
+                      Math.abs(item.totalAssets) > 100000
+                        ? amountToCurrency(item.totalAssets)
+                        : undefined
+                    }
+                    width="flex"
+                    privacyFilter
+                    style={{
+                      minWidth: compact ? 50 : 85,
+                    }}
+                    linkStyle={hoverUnderline}
+                    valueStyle={compactStyle}
+                    onClick={() =>
+                      !isNarrowWidth &&
+                      !['Group', 'Interval'].includes(groupBy) &&
+                      !categories.grouped.map(g => g.id).includes(item.id) &&
+                      showActivity({
+                        navigate,
+                        categories,
+                        accounts,
+                        balanceTypeOp,
+                        filters,
+                        showHiddenCategories,
+                        showOffBudget,
+                        type: 'assets',
+                        startDate,
+                        endDate,
+                        field: groupBy.toLowerCase(),
+                        id: item.id,
+                      })
+                    }
+                  />
+                  <Cell
+                    value={amountToCurrency(item.totalDebts)}
+                    title={
+                      Math.abs(item.totalDebts) > 100000
+                        ? amountToCurrency(item.totalDebts)
+                        : undefined
+                    }
+                    width="flex"
+                    privacyFilter
+                    style={{
+                      minWidth: compact ? 50 : 85,
+                    }}
+                    linkStyle={hoverUnderline}
+                    valueStyle={compactStyle}
+                    onClick={() =>
+                      !isNarrowWidth &&
+                      !['Group', 'Interval'].includes(groupBy) &&
+                      !categories.grouped.map(g => g.id).includes(item.id) &&
+                      showActivity({
+                        navigate,
+                        categories,
+                        accounts,
+                        balanceTypeOp,
+                        filters,
+                        showHiddenCategories,
+                        showOffBudget,
+                        type: 'debts',
+                        startDate,
+                        endDate,
+                        field: groupBy.toLowerCase(),
+                        id: item.id,
+                      })
+                    }
+                  />
+                </>
+              )}
+          <Cell
+            value={amountToCurrency(item[balanceTypeOp])}
+            title={
+              Math.abs(item[balanceTypeOp]) > 100000
+                ? amountToCurrency(item[balanceTypeOp])
+                : undefined
+            }
+            style={{
+              fontWeight: 600,
+              minWidth: compact ? 50 : 85,
+            }}
+            linkStyle={hoverUnderline}
+            valueStyle={compactStyle}
+            onClick={() =>
+              !isNarrowWidth &&
+              !['Group', 'Interval'].includes(groupBy) &&
+              !categories.grouped.map(g => g.id).includes(item.id) &&
+              showActivity({
+                navigate,
+                categories,
+                accounts,
+                balanceTypeOp,
+                filters,
+                showHiddenCategories,
+                showOffBudget,
+                type: 'totals',
+                startDate,
+                endDate,
+                field: groupBy.toLowerCase(),
+                id: item.id,
+              })
+            }
+            width="flex"
+            privacyFilter
+          />
+          <Cell
+            value={integerToCurrency(Math.round(average))}
+            title={
+              Math.abs(Math.round(average / 100)) > 100000
+                ? integerToCurrency(Math.round(average))
+                : undefined
+            }
+            style={{
+              fontWeight: 600,
+              minWidth: compact ? 50 : 85,
+            }}
+            valueStyle={compactStyle}
+            width="flex"
+            privacyFilter
+          />
+        </View>
       </Row>
     );
   },
diff --git a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx
index f9f01a6df..8690fc709 100644
--- a/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx
+++ b/packages/desktop-client/src/components/reports/graphs/tableGraph/ReportTableTotals.tsx
@@ -1,59 +1,67 @@
 import React, {
-  type UIEventHandler,
+  type ReactNode,
   useLayoutEffect,
   useState,
   type RefObject,
 } from 'react';
 
 import {
-  amountToCurrency,
-  amountToInteger,
-  integerToCurrency,
-} from 'loot-core/src/shared/util';
-import { type DataEntity } from 'loot-core/src/types/models/reports';
-import { type RuleConditionEntity } from 'loot-core/types/models/rule';
+  type GroupedEntity,
+  type DataEntity,
+} from 'loot-core/src/types/models/reports';
 
-import { useAccounts } from '../../../../hooks/useAccounts';
-import { useCategories } from '../../../../hooks/useCategories';
-import { useNavigate } from '../../../../hooks/useNavigate';
-import { useResponsive } from '../../../../ResponsiveProvider';
 import { theme } from '../../../../style';
 import { styles } from '../../../../style/styles';
 import { type CSSProperties } from '../../../../style/types';
 import { View } from '../../../common/View';
-import { Row, Cell } from '../../../table';
-import { showActivity } from '../showActivity';
+
+import { type renderTotalsProps } from './ReportTable';
+
+type RenderTotalsRowProps = {
+  metadata: GroupedEntity;
+  mode: string;
+  totalsStyle: CSSProperties;
+  testStyle: CSSProperties;
+  scrollWidthTotals: number;
+  renderTotals: (arg: renderTotalsProps) => ReactNode;
+};
+function RenderTotalsRow({
+  metadata,
+  mode,
+  totalsStyle,
+  testStyle,
+  scrollWidthTotals,
+  renderTotals,
+}: RenderTotalsRowProps) {
+  return (
+    <View>
+      {renderTotals({
+        metadata,
+        mode,
+        totalsStyle,
+        testStyle,
+        scrollWidthTotals,
+      })}
+    </View>
+  );
+}
 
 type ReportTableTotalsProps = {
   data: DataEntity;
-  balanceTypeOp: 'totalAssets' | 'totalDebts' | 'totalTotals';
   mode: string;
-  intervalsCount: number;
   totalScrollRef: RefObject<HTMLDivElement>;
-  handleScroll: UIEventHandler<HTMLDivElement>;
   compact: boolean;
   style?: CSSProperties;
-  compactStyle?: CSSProperties;
-  groupBy: string;
-  filters?: RuleConditionEntity[];
-  showHiddenCategories?: boolean;
-  showOffBudget?: boolean;
+  renderTotals: (arg: renderTotalsProps) => ReactNode;
 };
 
 export function ReportTableTotals({
   data,
-  balanceTypeOp,
   mode,
-  intervalsCount,
   totalScrollRef,
-  handleScroll,
   compact,
   style,
-  compactStyle,
-  groupBy,
-  filters = [],
-  showHiddenCategories = false,
-  showOffBudget = false,
+  renderTotals,
 }: ReportTableTotalsProps) {
   const [scrollWidthTotals, setScrollWidthTotals] = useState(0);
 
@@ -70,214 +78,47 @@ export function ReportTableTotals({
       setScrollWidthTotals(parent > 0 && child > 0 ? parent - child : 0);
     }
   });
-  const average = amountToInteger(data[balanceTypeOp]) / intervalsCount;
 
-  const navigate = useNavigate();
-  const { isNarrowWidth } = useResponsive();
-  const categories = useCategories();
-  const accounts = useAccounts();
+  const metadata: GroupedEntity = {
+    id: '',
+    name: 'Totals',
+    intervalData: data.intervalData,
+    totalAssets: data.totalAssets,
+    totalDebts: data.totalDebts,
+    totalTotals: data.totalTotals,
+  };
 
-  const pointer =
-    !isNarrowWidth && !['Group', 'Interval'].includes(groupBy)
-      ? 'pointer'
-      : 'inherit';
+  const totalsStyle: CSSProperties = {
+    borderTopWidth: 1,
+    borderColor: theme.tableBorder,
+    justifyContent: 'center',
+    color: theme.tableHeaderText,
+    backgroundColor: theme.tableHeaderBackground,
+    fontWeight: 600,
+    ...style,
+  };
 
-  const hoverUnderline =
-    !isNarrowWidth && !['Group', 'Interval'].includes(groupBy)
-      ? {
-          cursor: pointer,
-          ':hover': { textDecoration: 'underline' },
-          flexGrow: 0,
-        }
-      : {};
+  const testStyle: CSSProperties = {
+    overflowX: 'auto',
+    scrollbarWidth: compact ? 'none' : 'inherit',
+    ...styles.horizontalScrollbar,
+    '::-webkit-scrollbar': {
+      backgroundColor: theme.tableBackground,
+      height: 12,
+      dispaly: compact && 'none',
+    },
+    flexDirection: 'row',
+    flex: 1,
+  };
 
   return (
-    <Row
-      collapsed={true}
-      height={32 + scrollWidthTotals}
-      style={{
-        borderTopWidth: 1,
-        borderColor: theme.tableBorder,
-        justifyContent: 'center',
-        color: theme.tableHeaderText,
-        backgroundColor: theme.tableHeaderBackground,
-        fontWeight: 600,
-        ...style,
-      }}
-    >
-      <View
-        innerRef={totalScrollRef}
-        onScroll={handleScroll}
-        id="total"
-        style={{
-          overflowX: 'auto',
-          scrollbarWidth: compact ? 'none' : 'inherit',
-          ...styles.horizontalScrollbar,
-          '::-webkit-scrollbar': {
-            backgroundColor: theme.tableBackground,
-            height: 12,
-            dispaly: compact && 'none',
-          },
-          flexDirection: 'row',
-          flex: 1,
-        }}
-      >
-        <Cell
-          style={{
-            width: compact ? 80 : 125,
-            flexShrink: 0,
-          }}
-          valueStyle={compactStyle}
-          value="Totals"
-        />
-        {mode === 'time'
-          ? data.intervalData.map(item => {
-              return (
-                <Cell
-                  style={{
-                    minWidth: compact ? 50 : 85,
-                  }}
-                  linkStyle={hoverUnderline}
-                  valueStyle={compactStyle}
-                  key={amountToCurrency(item[balanceTypeOp])}
-                  value={amountToCurrency(item[balanceTypeOp])}
-                  title={
-                    Math.abs(item[balanceTypeOp]) > 100000
-                      ? amountToCurrency(item[balanceTypeOp])
-                      : undefined
-                  }
-                  onClick={() =>
-                    !isNarrowWidth &&
-                    !['Group', 'Interval'].includes(groupBy) &&
-                    showActivity({
-                      navigate,
-                      categories,
-                      accounts,
-                      balanceTypeOp,
-                      filters,
-                      showHiddenCategories,
-                      showOffBudget,
-                      type: 'time',
-                      startDate: item.dateStart || '',
-                    })
-                  }
-                  width="flex"
-                  privacyFilter
-                />
-              );
-            })
-          : balanceTypeOp === 'totalTotals' && (
-              <>
-                <Cell
-                  style={{
-                    minWidth: compact ? 50 : 85,
-                  }}
-                  linkStyle={hoverUnderline}
-                  valueStyle={compactStyle}
-                  value={amountToCurrency(data.totalAssets)}
-                  title={
-                    Math.abs(data.totalAssets) > 100000
-                      ? amountToCurrency(data.totalAssets)
-                      : undefined
-                  }
-                  onClick={() =>
-                    !isNarrowWidth &&
-                    !['Group', 'Interval'].includes(groupBy) &&
-                    showActivity({
-                      navigate,
-                      categories,
-                      accounts,
-                      balanceTypeOp,
-                      filters,
-                      showHiddenCategories,
-                      showOffBudget,
-                      type: 'assets',
-                      startDate: data.startDate || '',
-                      endDate: data.endDate,
-                    })
-                  }
-                  width="flex"
-                  privacyFilter
-                />
-                <Cell
-                  style={{
-                    minWidth: compact ? 50 : 85,
-                  }}
-                  linkStyle={hoverUnderline}
-                  valueStyle={compactStyle}
-                  value={amountToCurrency(data.totalDebts)}
-                  title={
-                    Math.abs(data.totalDebts) > 100000
-                      ? amountToCurrency(data.totalDebts)
-                      : undefined
-                  }
-                  onClick={() =>
-                    !isNarrowWidth &&
-                    !['Group', 'Interval'].includes(groupBy) &&
-                    showActivity({
-                      navigate,
-                      categories,
-                      accounts,
-                      balanceTypeOp,
-                      filters,
-                      showHiddenCategories,
-                      showOffBudget,
-                      type: 'debts',
-                      startDate: data.startDate || '',
-                      endDate: data.endDate,
-                    })
-                  }
-                  width="flex"
-                  privacyFilter
-                />
-              </>
-            )}
-        <Cell
-          style={{
-            minWidth: compact ? 50 : 85,
-          }}
-          linkStyle={hoverUnderline}
-          valueStyle={compactStyle}
-          value={amountToCurrency(data[balanceTypeOp])}
-          title={
-            Math.abs(data[balanceTypeOp]) > 100000
-              ? amountToCurrency(data[balanceTypeOp])
-              : undefined
-          }
-          onClick={() =>
-            !isNarrowWidth &&
-            !['Group', 'Interval'].includes(groupBy) &&
-            showActivity({
-              navigate,
-              categories,
-              accounts,
-              balanceTypeOp,
-              filters,
-              showHiddenCategories,
-              showOffBudget,
-              type: 'totals',
-              startDate: data.startDate || '',
-              endDate: data.endDate,
-            })
-          }
-          width="flex"
-          privacyFilter
-        />
-        <Cell
-          style={{
-            minWidth: compact ? 50 : 85,
-          }}
-          valueStyle={compactStyle}
-          value={integerToCurrency(Math.round(average))}
-          title={
-            Math.abs(Math.round(average / 100)) > 100000
-              ? integerToCurrency(Math.round(average))
-              : undefined
-          }
-          width="flex"
-          privacyFilter
-        />
-      </View>
-    </Row>
+    <RenderTotalsRow
+      metadata={metadata}
+      mode={mode}
+      totalsStyle={totalsStyle}
+      testStyle={testStyle}
+      scrollWidthTotals={scrollWidthTotals}
+      renderTotals={renderTotals}
+    />
   );
 }
diff --git a/packages/desktop-client/src/components/reports/spreadsheets/custom-spreadsheet.ts b/packages/desktop-client/src/components/reports/spreadsheets/custom-spreadsheet.ts
index ad734c270..ab8b3a0e1 100644
--- a/packages/desktop-client/src/components/reports/spreadsheets/custom-spreadsheet.ts
+++ b/packages/desktop-client/src/components/reports/spreadsheets/custom-spreadsheet.ts
@@ -214,7 +214,7 @@ export function createCustomSpreadsheet({
             ReportOptions.intervalFormat.get(interval) || '',
           ),
           ...stacked,
-          dateStart: intervalItem,
+          intervalStartDate: intervalItem,
           totalDebts: integerToAmount(perIntervalDebts),
           totalAssets: integerToAmount(perIntervalAssets),
           totalTotals: integerToAmount(perIntervalDebts + perIntervalAssets),
diff --git a/packages/desktop-client/src/components/reports/spreadsheets/recalculate.ts b/packages/desktop-client/src/components/reports/spreadsheets/recalculate.ts
index a7262dde8..a01332e4b 100644
--- a/packages/desktop-client/src/components/reports/spreadsheets/recalculate.ts
+++ b/packages/desktop-client/src/components/reports/spreadsheets/recalculate.ts
@@ -77,7 +77,7 @@ export function recalculate({
         totalDebts: integerToAmount(intervalDebts),
         totalTotals: integerToAmount(intervalAssets + intervalDebts),
         change,
-        dateLookup: intervalItem,
+        intervalStartDate: intervalItem,
       });
 
       return arr;
diff --git a/packages/loot-core/src/types/models/reports.d.ts b/packages/loot-core/src/types/models/reports.d.ts
index 563de0b1f..ca893d5f1 100644
--- a/packages/loot-core/src/types/models/reports.d.ts
+++ b/packages/loot-core/src/types/models/reports.d.ts
@@ -77,9 +77,8 @@ type LegendEntity = {
 
 export type IntervalEntity = {
   date?: string;
-  dateStart?: string;
   change?: number;
-  dateLookup?: string;
+  intervalStartDate?: string;
   totalAssets: number;
   totalDebts: number;
   totalTotals: number;
diff --git a/upcoming-release-notes/2768.md b/upcoming-release-notes/2768.md
new file mode 100644
index 000000000..7ec924000
--- /dev/null
+++ b/upcoming-release-notes/2768.md
@@ -0,0 +1,6 @@
+---
+category: Bugfix
+authors: [carkom]
+---
+
+Creating a callback for the table totals to fix a bug that created duplicate columns while rendering.
-- 
GitLab