Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calendar Report #3828

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open

Calendar Report #3828

wants to merge 11 commits into from

Conversation

lelemm
Copy link
Contributor

@lelemm lelemm commented Nov 12, 2024

A new report type where data shows in a calendar format`:

Actual.-.Google.Chrome.2024-11-12.13-38-51.mp4

@actual-github-bot actual-github-bot bot changed the title Calendar Report [WIP] Calendar Report Nov 12, 2024
Copy link

netlify bot commented Nov 12, 2024

Deploy Preview for actualbudget ready!

Name Link
🔨 Latest commit 050391c
🔍 Latest deploy log https://app.netlify.com/sites/actualbudget/deploys/6734c939689c920008d5eddd
😎 Deploy Preview https://deploy-preview-3828.demo.actualbudget.org
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

Copy link
Contributor

github-actions bot commented Nov 12, 2024

Bundle Stats — desktop-client

Hey there, this message comes from a GitHub action that helps you and reviewers to understand how these changes affect the size of this project's bundle.

As this PR is updated, I'll keep you updated on how the bundle size is impacted.

Total

Files count Total bundle size % Changed
9 5.35 MB → 5.42 MB (+62.26 kB) +1.14%
Changeset
File Δ Size
src/components/reports/reports/Calendar.tsx 🆕 +25 kB 0 B → 25 kB
src/components/reports/reports/CalendarCard.tsx 🆕 +14.76 kB 0 B → 14.76 kB
src/components/reports/graphs/CalendarGraph.tsx 🆕 +8.84 kB 0 B → 8.84 kB
src/components/reports/spreadsheets/calendar-spreadsheet.ts 🆕 +5.74 kB 0 B → 5.74 kB
node_modules/date-fns/esm/differenceInDays/index.js 🆕 +3.04 kB 0 B → 3.04 kB
node_modules/date-fns/esm/isSameMonth/index.js 🆕 +1.02 kB 0 B → 1.02 kB
node_modules/date-fns/esm/getDaysInMonth/index.js 🆕 +796 B 0 B → 796 B
node_modules/date-fns/esm/getDate/index.js 🆕 +576 B 0 B → 576 B
node_modules/clsx/dist/clsx.js 🆕 +509 B 0 B → 509 B
src/icons/v1/ArrowThickDown.tsx 🆕 +345 B 0 B → 345 B
src/icons/v1/ArrowThickUp.tsx 🆕 +344 B 0 B → 344 B
node_modules/clsx/dist/clsx.js?commonjs-module 🆕 +27 B 0 B → 27 B
src/components/reports/ReportRouter.tsx 📈 +322 B (+20.65%) 1.52 kB → 1.84 kB
src/components/reports/Overview.tsx 📈 +608 B (+3.91%) 15.2 kB → 15.79 kB
src/components/transactions/TransactionList.jsx 📈 +98 B (+1.87%) 5.12 kB → 5.22 kB
src/components/transactions/TransactionsTable.jsx 📈 +562 B (+0.82%) 67.1 kB → 67.64 kB
src/components/reports/reportRanges.ts 📈 +31 B (+0.75%) 4.05 kB → 4.08 kB
src/style/themes/development.ts 📈 +42 B (+0.53%) 7.72 kB → 7.76 kB
src/style/themes/dark.ts 📈 +42 B (+0.53%) 7.75 kB → 7.79 kB
src/style/themes/light.ts 📈 +42 B (+0.52%) 7.81 kB → 7.86 kB
src/style/themes/midnight.ts 📈 +40 B (+0.52%) 7.45 kB → 7.49 kB
node_modules/lodash/throttle.js 📈 +2 B (+0.07%) 2.69 kB → 2.69 kB
node_modules/react-grid-layout/build/ReactGridLayout.js 📉 -1 B (-0.00%) 24.96 kB → 24.96 kB
node_modules/react-grid-layout/build/GridItem.js 📉 -1 B (-0.00%) 21.49 kB → 21.49 kB
node_modules/react-grid-layout/build/components/WidthProvider.js 📉 -1 B (-0.02%) 5.22 kB → 5.22 kB
node_modules/clsx/dist/clsx.mjs 🔥 -368 B (-100%) 368 B → 0 B
node_modules/clsx/dist/clsx.mjs?commonjs-proxy 🔥 -64 B (-100%) 64 B → 0 B
View detailed bundle breakdown

Added

Asset File Size % Changed
static/js/TransactionList.js 0 B → 102.55 kB (+102.55 kB) -

Removed

Asset File Size % Changed
static/js/AppliedFilters.js 21.3 kB → 0 B (-21.3 kB) -100%

Bigger

Asset File Size % Changed
static/js/ReportRouter.js 1.49 MB → 1.55 MB (+61.29 kB) +4.01%
static/js/index.js 3.37 MB → 3.37 MB (+334 B) +0.01%

Smaller

Asset File Size % Changed
static/js/wide.js 242.64 kB → 162.04 kB (-80.6 kB) -33.22%

Unchanged

Asset File Size % Changed
static/js/workbox-window.prod.es5.js 5.69 kB 0%
static/js/resize-observer.js 18.37 kB 0%
static/js/indexeddb-main-thread-worker-e59fee74.js 13.5 kB 0%
static/js/BackgroundImage.js 122.29 kB 0%
static/js/narrow.js 82.76 kB 0%

Copy link
Contributor

github-actions bot commented Nov 12, 2024

Bundle Stats — loot-core

Hey there, this message comes from a GitHub action that helps you and reviewers to understand how these changes affect the size of this project's bundle.

As this PR is updated, I'll keep you updated on how the bundle size is impacted.

Total

Files count Total bundle size % Changed
1 1.32 MB 0%

Changeset

No files were changed

View detailed bundle breakdown

Added

No assets were added

Removed

No assets were removed

Bigger

No assets were bigger

Smaller

No assets were smaller

Unchanged

Asset File Size % Changed
kcab.worker.js 1.32 MB 0%

@lelemm lelemm changed the title [WIP] Calendar Report Calendar Report Nov 12, 2024
Copy link
Contributor

coderabbitai bot commented Nov 12, 2024

Walkthrough

The pull request introduces several enhancements to the reporting functionality within the desktop client application. Key modifications include the addition of a new CalendarCard component, which is integrated into the Overview component and allows users to visualize calendar data. A new hook, useSyncedPref, retrieves user preferences for the first day of the week, which is passed to the CalendarCard. The ReportRouter component is updated to include new routes for the calendar view, enabling direct navigation to calendar-related pages. A new CalendarGraph component is introduced to visualize income and expense data over a week. Additionally, a Calendar component is created to manage the calendar interface, while the CalendarCard component displays a report card with financial data. The calendar-spreadsheet module is added to generate reports based on income and expenses. Lastly, the TransactionList and TransactionsTable components are updated to support new props for transaction selection and splitting. Overall, these changes enhance the dashboard's functionality and user interaction with calendar and transaction data.

Possibly related PRs

Suggested labels

sparkles: Merged

Suggested reviewers

  • joel-jeremy
  • MikesGlitch

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

🧹 Outside diff range and nitpick comments (22)
packages/loot-core/src/types/models/dashboard.d.ts (1)

93-101: Consider moving type definition before its usage

While the CalendarWidget type definition is well-structured and follows the established patterns, it's currently defined after its usage in the SpecializedWidget union type. Consider moving this type definition to be grouped with other widget types (before line 68) to maintain better code organization and prevent potential TypeScript resolution issues.

- // Current position (after SpecializedWidget)
- export type CalendarWidget = AbstractWidget<
-   'calendar-card',
-   {
-     name?: string;
-     conditions?: RuleConditionEntity[];
-     conditionsOp?: 'and' | 'or';
-     timeFrame?: TimeFrame;
-   } | null
- >;

+ // Suggested position (grouped with other widget types, before SpecializedWidget)
+ export type CalendarWidget = AbstractWidget<
+   'calendar-card',
+   {
+     name?: string;
+     conditions?: RuleConditionEntity[];
+     conditionsOp?: 'and' | 'or';
+     timeFrame?: TimeFrame;
+   } | null
+ >;

  type SpecializedWidget =
    | NetWorthWidget
    | CashFlowWidget
    | SpendingWidget
    | MarkdownWidget
    | CalendarWidget;
packages/desktop-client/src/components/reports/reportRanges.ts (2)

164-167: Document the special case behavior.

The special case where offset === 1 results in start equaling end should be documented to prevent confusion.

Add a comment explaining this behavior:

 export function getLatestRange(offset: number) {
   const end = monthUtils.currentMonth();
+  // When offset is 1, start and end will be the same month
   let start = end;
   if (offset !== 1) {
     start = monthUtils.subMonths(end, offset);
   }
   return [start, end, 'sliding-window'] as const;
 }

164-167: Consider handling negative offsets.

The function should validate the offset parameter to handle edge cases.

Consider adding validation:

 export function getLatestRange(offset: number) {
+  if (offset <= 0) {
+    throw new Error('Offset must be a positive number');
+  }
   const end = monthUtils.currentMonth();
   let start = end;
   if (offset !== 1) {
     start = monthUtils.subMonths(end, offset);
   }
   return [start, end, 'sliding-window'] as const;
 }
packages/desktop-client/src/components/transactions/TransactionList.jsx (1)

91-92: Document prop types for better maintainability.

Consider adding PropTypes or TypeScript type definitions for the new props to improve code maintainability and prevent potential runtime issues.

+ import PropTypes from 'prop-types';

export function TransactionList({
  // ... other props
  showSelection = true,
  allowSplitTransaction = true,
}) {
  // ... component implementation
}

+ TransactionList.propTypes = {
+   // ... other prop types
+   showSelection: PropTypes.bool,
+   allowSplitTransaction: PropTypes.bool,
+ };
packages/desktop-client/src/components/reports/Overview.tsx (1)

562-570: Consider enhancing robustness and user experience

Good implementation, but consider these improvements:

  1. Add unit tests for the calendar widget integration
  2. Implement a loading state for the calendar data to improve user experience
packages/desktop-client/src/components/transactions/TransactionsTable.jsx (2)

885-886: Consider adding PropTypes and default values for the new props.

The new props showSelection and allowSplitTransaction are propagated correctly through the component hierarchy, but they lack type definitions and default values. This could lead to runtime issues if the props are not provided.

Consider adding:

+TransactionTable.propTypes = {
+  showSelection: PropTypes.bool,
+  allowSplitTransaction: PropTypes.bool,
+  // ... other prop types
+};
+
+TransactionTable.defaultProps = {
+  showSelection: true,
+  allowSplitTransaction: true,
+  // ... other default props
+};

Also applies to: 1790-1791, 1994-1995, 2615-2616


209-234: Enhance keyboard navigation accessibility for selection cells.

While the selection UI is implemented correctly, it could benefit from improved keyboard navigation feedback.

Consider adding:

 <SelectCell
   exposed={true}
   focused={false}
   selected={hasSelected}
   width={20}
   style={{
     borderTopWidth: 0,
     borderBottomWidth: 0,
+    outline: 'none',
+    '&:focus-visible': {
+      boxShadow: `0 0 0 2px ${theme.focusColor}`,
+    },
   }}
   onSelect={e =>
     dispatchSelected({
       type: 'select-all',
       isRangeSelect: e.shiftKey,
     })
   }
+  aria-label="Select all transactions"
 />
packages/desktop-client/src/components/reports/spreadsheets/calendar-spreadsheet.ts (4)

130-136: Simplify total value calculations by initializing to zero

Currently, totalExpenseValue and totalIncomeValue are set to null if there are no values, which requires additional null checks later. Initializing them to zero simplifies the logic and avoids unnecessary null checks.

Apply this diff to initialize totals to zero:

     const totalExpenseValue = expenseValues.length
       ? expenseValues.reduce((acc, val) => acc + val, 0)
-      : null;
+      : 0;

     const totalIncomeValue = incomeValues.length
       ? incomeValues.reduce((acc, val) => acc + val, 0)
-      : null;
+      : 0;

Then, you can simplify the getBarLength function by removing the null checks:

     const getBarLength = (value: number) => {
-      if (value < 0 && totalExpenseValue !== null && totalExpenseValue !== 0) {
+      if (value < 0 && totalExpenseValue !== 0) {
         return (Math.abs(value) / totalExpenseValue) * 100;
-      } else if (
-        value > 0 &&
-        totalIncomeValue !== null &&
-        totalIncomeValue !== 0
-      ) {
+      } else if (value > 0 && totalIncomeValue !== 0) {
         return (value / totalIncomeValue) * 100;
       } else {
         return 0;
       }
     };

93-102: Improve clarity by renaming the 'recalculate' function

The function name recalculate might not clearly indicate its purpose. Consider renaming it to better reflect its functionality, such as generateCalendarData or computeCalendarMetrics, to enhance code readability and maintainability.


37-67: Combine income and expense queries to reduce database calls

Currently, two separate queries are executed to fetch income and expense data. Consider combining them into a single query to reduce database calls and improve performance.

You can modify the root query to group both positive and negative amounts and then separate them in the processing logic:

     const data = await runQuery(
       makeRootQuery()
-        .filter({
-          $and: { amount: { $lt: 0 } },
-        }),
+        // No additional filter here
     );

     // Process the data into incomeData and expenseData
+    const incomeData = data.data.filter(item => item.amount > 0);
+    const expenseData = data.data.filter(item => item.amount < 0);

This approach reduces the number of queries from two to one.


106-113: Define a shared type for income and expense data items

The types for incomeData and expenseData are identical. Consider defining a shared type to eliminate duplication and improve code maintainability.

Apply this diff to define a shared type:

+  type TransactionDataItem = {
+    date: string;
+    amount: number;
+  };

   function recalculate(
-    incomeData: Array<{
-      date: string;
-      amount: number;
-    }>,
-    expenseData: Array<{
-      date: string;
-      amount: number;
-    }>,
+    incomeData: TransactionDataItem[],
+    expenseData: TransactionDataItem[],
     months: Date[],
     start: string,
     firstDayOfWeekIdx?: SyncedPrefs['firstDayOfWeekIdx'],
   ) {
packages/desktop-client/src/components/reports/graphs/CalendarGraph.tsx (3)

34-43: Remove unused commented-out code for cleaner codebase

The commented-out code in lines 34-43 appears to be obsolete or no longer needed. Removing unnecessary code can improve readability and maintainability.

Apply this diff to remove the commented code:

 type CalendarGraphProps = {
   data: {
     date: Date;
     incomeValue: number;
     expenseValue: number;
     incomeSize: number;
     expenseSize: number;
   }[];
   start: Date;
   firstDayOfWeekIdx?: SyncedPrefs['firstDayOfWeekIdx'];
   onDayClick: (date: Date) => void;
-  // onFilter: (
-  //   conditionsOrSavedFilter:
-  //     | null
-  //     | {
-  //         conditions: RuleConditionEntity[];
-  //         conditionsOp: 'and' | 'or';
-  //         id: RuleConditionEntity[];
-  //       }
-  //     | RuleConditionEntity,
-  // ) => void;
 };

49-49: Clean up commented-out parameter in function declaration

Line 49 contains a commented-out parameter //onFilter,. If this parameter is no longer required, consider removing it to tidy up the code.

Apply this diff:

 export function CalendarGraph({
   data,
   start,
   firstDayOfWeekIdx,
-  //onFilter,
   onDayClick,
 }: CalendarGraphProps) {

207-211: Eliminate unnecessary state in DayButton component

The currentFontSize state mirrors the fontSize prop without additional computation. You can simplify the component by using fontSize directly.

Apply this diff to remove the redundant state:

 function DayButton({ day, onPress, fontSize, resizeRef }: DayButtonProps) {
-  const [currentFontSize, setCurrentFontSize] = useState(fontSize);
-
-  useEffect(() => {
-    setCurrentFontSize(fontSize);
-  }, [fontSize]);

   return (
     <Button
       ref={resizeRef}
       /* ... */
     >
       {/* ... */}
       <span
         style={{
-          fontSize: `${currentFontSize}px`,
+          fontSize: `${fontSize}px`,
           fontWeight: 500,
           position: 'relative',
         }}
       >
         {getDate(day.date)}
       </span>
     </Button>
   );
 }
packages/desktop-client/src/components/reports/reports/CalendarCard.tsx (1)

487-517: Avoid assignments within expressions for clearer code

Using assignments within expressions, such as in the ref callbacks, can make the code harder to read and maintain. Consider separating the assignment from the expression by using dedicated functions for each ref assignment.

Apply this diff to refactor the ref assignments:

- ref={rel => (monthFormatSizeContainers.current[0] = rel)}
+ const setMonthFormatSizeRef0 = (rel: HTMLSpanElement | null) => {
+   monthFormatSizeContainers.current[0] = rel;
+ };
+ // ...
+ ref={setMonthFormatSizeRef0}

Repeat this pattern for the other refs at lines 494, 501, 508, and 515:

// For index 1
- ref={rel => (monthFormatSizeContainers.current[1] = rel)}
+ const setMonthFormatSizeRef1 = (rel: HTMLSpanElement | null) => {
+   monthFormatSizeContainers.current[1] = rel;
+ };
+ ref={setMonthFormatSizeRef1}

// For index 2
- ref={rel => (monthFormatSizeContainers.current[2] = rel)}
+ const setMonthFormatSizeRef2 = (rel: HTMLSpanElement | null) => {
+   monthFormatSizeContainers.current[2] = rel;
+ };
+ ref={setMonthFormatSizeRef2}

// For index 3
- ref={rel => (monthFormatSizeContainers.current[3] = rel)}
+ const setMonthFormatSizeRef3 = (rel: HTMLSpanElement | null) => {
+   monthFormatSizeContainers.current[3] = rel;
+ };
+ ref={setMonthFormatSizeRef3}

// For index 4
- ref={rel => (monthFormatSizeContainers.current[4] = rel)}
+ const setMonthFormatSizeRef4 = (rel: HTMLSpanElement | null) => {
+   monthFormatSizeContainers.current[4] = rel;
+ };
+ ref={setMonthFormatSizeRef4}
🧰 Tools
🪛 Biome

[error] 487-487: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


[error] 494-494: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


[error] 501-501: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


[error] 508-508: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


[error] 515-515: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

packages/desktop-client/src/components/reports/reports/Calendar.tsx (3)

278-278: Use Consistent Default Widget Name

In the onSaveWidgetName function, the default widget name is set to 'Net Worth'. Since this component represents a calendar report, consider using 'Calendar' as the default name for consistency.

Apply this diff to update the default name:

- const name = newName || t('Net Worth');
+ const name = newName || t('Calendar');

765-788: Avoid Rendering Empty Strings in Conditional Rendering

Currently, when totalIncome or totalExpense is falsy, an empty string '' is rendered. This can lead to unnecessary empty nodes in the DOM.

Apply this diff to render null instead:

{totalIncome ? (
  // Existing rendering logic
) : (
- ''
+ null
)}

{totalExpense ? (
  // Existing rendering logic
) : (
- ''
+ null
)}

184-210: Ensure Consistency in Error Logging

In the added error handling, errors are logged using console.error. For consistency and better debugging, consider using a centralized logging mechanism if available.

If your application uses a logging service or utility, replace console.error with that method.

packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx (4)

Line range hint 32-34: Consolidate duplicate mocks for '../../hooks/useFeatureFlag'

There are two vi.mock statements for '../../hooks/useFeatureFlag', which might lead to unexpected behavior or conflicts in the tests. It's advisable to combine them into a single mock to maintain clarity and avoid potential issues.

Consider merging them as follows:

-vi.mock('../../hooks/useFeatureFlag', () => ({
-  default: vi.fn().mockReturnValue(false),
-}));
-vi.mock('../../hooks/useFeatureFlag', () => ({
-  useFeatureFlag: () => false,
+vi.mock('../../hooks/useFeatureFlag', () => ({
+  default: vi.fn().mockReturnValue(false),
+  useFeatureFlag: () => false,
}));

Also applies to: 38-40


Line range hint 279-282: Address the TODO: Fix flakiness and re-enable the test

There's a commented-out section with a TODO note to fix flakiness and re-enable the test for navigating to the top of the transaction list. Resolving this will ensure that all keybindings are thoroughly tested, improving the robustness of the navigation functionality.

Would you like assistance in diagnosing the cause of the flakiness and implementing a fix? I can help by providing suggestions or code changes to resolve the issue.


Line range hint 359-384: Re-enable the skipped test: 'dropdown invalid value resets correctly'

The test 'dropdown invalid value resets correctly' is currently skipped using test.skip. It's important to address any underlying issues causing the test to fail so that input validation for invalid values is thoroughly tested.

Can I assist in debugging this test to identify and fix the issues? Re-enabling it will help ensure that invalid inputs are handled correctly.


171-172: Add test cases for 'showSelection' and 'allowSplitTransaction' props

To ensure the new props showSelection={true} and allowSplitTransaction={true} function as intended, consider adding specific test cases that verify their behavior. This will help confirm that selection visibility and split transaction capabilities are working correctly in the TransactionTable.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 9e47801 and ad6ac02.

⛔ Files ignored due to path filters (1)
  • upcoming-release-notes/3828.md is excluded by !**/*.md
📒 Files selected for processing (11)
  • packages/desktop-client/src/components/reports/Overview.tsx (5 hunks)
  • packages/desktop-client/src/components/reports/ReportRouter.tsx (2 hunks)
  • packages/desktop-client/src/components/reports/graphs/CalendarGraph.tsx (1 hunks)
  • packages/desktop-client/src/components/reports/reportRanges.ts (1 hunks)
  • packages/desktop-client/src/components/reports/reports/Calendar.tsx (1 hunks)
  • packages/desktop-client/src/components/reports/reports/CalendarCard.tsx (1 hunks)
  • packages/desktop-client/src/components/reports/spreadsheets/calendar-spreadsheet.ts (1 hunks)
  • packages/desktop-client/src/components/transactions/TransactionList.jsx (2 hunks)
  • packages/desktop-client/src/components/transactions/TransactionsTable.jsx (10 hunks)
  • packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx (1 hunks)
  • packages/loot-core/src/types/models/dashboard.d.ts (2 hunks)
🧰 Additional context used
🪛 Biome
packages/desktop-client/src/components/reports/reports/CalendarCard.tsx

[error] 487-487: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


[error] 494-494: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


[error] 501-501: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


[error] 508-508: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


[error] 515-515: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

🔇 Additional comments (13)
packages/desktop-client/src/components/reports/ReportRouter.tsx (2)

5-5: LGTM! Import follows established pattern

The Calendar import follows the consistent pattern used for other report components.


23-24: LGTM! Routes follow established pattern

The new Calendar routes maintain consistency with other report routes by:

  • Following the same base path + :id parameter pattern
  • Using the same component structure

Let's verify the Calendar component implements the expected route parameter handling:

packages/loot-core/src/types/models/dashboard.d.ts (1)

68-69: LGTM: Clean addition to SpecializedWidget union type

The CalendarWidget is properly integrated into the SpecializedWidget union type, following the established pattern.

packages/desktop-client/src/components/reports/reportRanges.ts (1)

164-167: Verify the impact of modified date range logic.

The change introduces a special case where start equals end when offset is 1. While this might be intentional for the new calendar feature, we should verify it doesn't affect existing reports.

Let's check for existing usages:

packages/desktop-client/src/components/transactions/TransactionList.jsx (1)

256-257: LGTM!

The props are correctly passed down to the TransactionTable component, maintaining a clean pass-through implementation.

packages/desktop-client/src/components/reports/Overview.tsx (3)

25-25: LGTM: Clean implementation of user preference sync

The implementation properly handles the first day of week preference with a sensible default value.

Also applies to: 56-57


38-38: LGTM: Consistent import organization

The CalendarCard import follows the established pattern for report components.


562-570: Verify CalendarCard component interface

The integration looks good, but please ensure:

  1. The CalendarCard component has proper TypeScript interfaces for all props
  2. The component is documented with JSDoc comments
#!/bin/bash
# Search for CalendarCard component definition and its interface
ast-grep --pattern 'interface $name {
  $$$
  widgetId: $_
  isEditing: $_
  meta: $_
  firstDayOfWeekIdx: $_
  $$$
}'

# Search for JSDoc comments
rg -B 2 "export (type|interface) .*CalendarCard.*|export function CalendarCard"
packages/desktop-client/src/components/reports/spreadsheets/calendar-spreadsheet.ts (1)

159-162: ⚠️ Potential issue

Review the calculation logic for 'totalDays'

The calculation for totalDays may not correctly account for the number of days to display, especially when the month doesn't start on the first day of the week. d.differenceInDays(firstDay, beginDay) can return a negative value if beginDay is before firstDay, which is expected, but adding d.getDaysInMonth(firstDay) may not produce the intended result. Consider revising the logic to ensure that totalDays accurately represents the days needed to fill the calendar grid.

Please verify the correctness of the totalDays calculation. You can test this with different months and firstDayOfWeekIdx values to ensure the calendar displays properly.

packages/desktop-client/src/components/reports/graphs/CalendarGraph.tsx (1)

183-183: Verify the logic for assigning resizeRef based on index

Assigning resizeRef to DayButton only when index === 15 may not work consistently with varying data lengths. Ensure that this logic is intentional and functions correctly for all data sets.

Consider applying resizeRef to a consistent element, such as the first day of the month, or refactoring the approach to dynamically select an appropriate DayButton.

packages/desktop-client/src/components/reports/reports/Calendar.tsx (3)

356-360: Include Dependencies in useEffect Dependency Array

The useEffect hook uses refContainer.current, but refContainer is not included in the dependency array. Although refs generally don't need to be in dependencies, if refContainer.current can change, consider adding it to ensure the effect runs as expected.

Please verify if refContainer.current can change over time. If so, include it in the dependencies:

useEffect(() => {
  if (refContainer.current) {
    setTotalHeight(refContainer.current.clientHeight - 115);
  }
- }, [query]);
+ }, [query, refContainer.current]);

399-415: Ensure Proper Handling of Negative Offset Values

In the useDrag hook, the condition oy < 0 triggers cancel() and opens the transactions. Ensure that negative offset values are correctly handled and that this logic matches the intended user interaction.

Please test the drag behavior to confirm that upward swipes open the transaction list as intended, and that the gestures function correctly on different devices.


121-123: Confirm Default Value for firstDayOfWeekIdx

The default value for firstDayOfWeekIdx is set to '0' if _firstDayOfWeekIdx is undefined. Ensure that this default aligns with the expected first day of the week in your application (e.g., Sunday).

Consider parsing the value to an integer:

- const firstDayOfWeekIdx = _firstDayOfWeekIdx || '0';
+ const firstDayOfWeekIdx = parseInt(_firstDayOfWeekIdx ?? '0', 10);

@lelemm lelemm changed the title Calendar Report [WIP] Calendar Report Nov 12, 2024
@lelemm lelemm changed the title [WIP] Calendar Report Calendar Report Nov 12, 2024
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Outside diff range and nitpick comments (12)
packages/desktop-client/src/components/reports/spreadsheets/calendar-spreadsheet.ts (2)

18-36: Consider extracting the callback type definition.

The setData callback type is complex and could be reused elsewhere. Consider extracting it into a separate type definition.

+type CalendarSetDataType = {
+  calendarData: {
+    start: Date;
+    end: Date;
+    data: CalendarDataType[];
+    totalExpense: number;
+    totalIncome: number;
+  }[];
+};

 export function calendarSpreadsheet(
   start: string,
   end: string,
   conditions: RuleConditionEntity[] = [],
   conditionsOp: 'and' | 'or' = 'and',
   firstDayOfWeekIdx?: SyncedPrefs['firstDayOfWeekIdx'],
 ) {
   return async (
     spreadsheet: ReturnType<typeof useSpreadsheet>,
-    setData: (data: {
-      calendarData: {
-        start: Date;
-        end: Date;
-        data: CalendarDataType[];
-        totalExpense: number;
-        totalIncome: number;
-      }[];
-    }) => void,
+    setData: (data: CalendarSetDataType) => void,
   ) => {

103-114: Simplify month range calculation using date-fns.

The getOneDatePerMonth function could be simplified using eachMonthOfInterval from date-fns.

-    const getOneDatePerMonth = (start: Date, end: Date) => {
-      const months = [];
-      let currentDate = d.startOfMonth(start);
-
-      while (!d.isSameMonth(currentDate, end)) {
-        months.push(currentDate);
-        currentDate = d.addMonths(currentDate, 1);
-      }
-      months.push(end);
-
-      return months;
-    };
+    const getOneDatePerMonth = (start: Date, end: Date) => {
+      return d.eachMonthOfInterval({ start, end });
+    };
packages/desktop-client/src/components/reports/graphs/CalendarGraph.tsx (4)

34-43: Remove or implement the commented out onFilter prop

The commented out onFilter prop and its type definition suggest incomplete functionality. Either implement this feature or remove the commented code to maintain clean and maintainable code.


78-81: Extract grid layout constants

Consider extracting the grid layout values into named constants for better maintainability and reusability.

+const CALENDAR_GRID = {
+  COLUMNS: 7,
+  GAP: 2,
+} as const;
+
 <View
   style={{
     color: theme.pageTextSubdued,
     display: 'grid',
-    gridTemplateColumns: 'repeat(7, 1fr)',
+    gridTemplateColumns: `repeat(${CALENDAR_GRID.COLUMNS}, 1fr)`,
     gridAutoRows: '1fr',
-    gap: 2,
+    gap: CALENDAR_GRID.GAP,
   }}
 >

119-176: Extract tooltip content into a separate component

The tooltip content is complex and could benefit from being extracted into a separate component for better maintainability and reusability.

Consider creating a DayTooltip component to encapsulate this logic:

type DayTooltipProps = {
  day: {
    date: Date;
    incomeValue: number;
    expenseValue: number;
    incomeSize: number;
    expenseSize: number;
  };
};

function DayTooltip({ day }: DayTooltipProps) {
  const { t } = useTranslation();
  return (
    <View>
      <View style={{ marginBottom: 10 }}>
        <strong>
          {t('Day:') + ' '}
          {format(day.date, 'dd')}
        </strong>
      </View>
      {/* ... rest of the tooltip content ... */}
    </View>
  );
}

232-282: Reduce style duplication in bar components

The bar styles for income and expense are very similar. Consider extracting common styles into a shared object or utility function.

const getBarStyles = (side: 'left' | 'right', color: string, size: number) => ({
  position: 'absolute',
  [side]: 0,
  bottom: 0,
  opacity: 0.9,
  height: `${Math.ceil(size)}%`,
  backgroundColor: color,
  width: '50%',
  transition: 'height 0.5s ease-out',
} as const);

// Usage:
<View
  className="bar positive-bar"
  style={getBarStyles('left', chartTheme.colors.blue, day.incomeSize)}
/>
<View
  className="bar"
  style={getBarStyles('right', chartTheme.colors.red, day.expenseSize)}
/>
packages/desktop-client/src/components/reports/reports/CalendarCard.tsx (3)

44-51: Consider enhancing type safety for meta prop

The meta prop's type could be more strictly defined to prevent potential runtime errors.

Consider this improvement:

type CalendarProps = {
  widgetId: string;
  isEditing?: boolean;
- meta?: CalendarWidget['meta'];
+ meta?: {
+   name?: string;
+   timeFrame?: string;
+   conditions?: unknown;
+   conditionsOp?: string;
+ };
  onMetaChange: (newMeta: CalendarWidget['meta']) => void;
  onRemove: () => void;
  firstDayOfWeekIdx?: SyncedPrefs['firstDayOfWeekIdx'];
};

97-111: Optimize total calculations with single reduce

The current implementation uses two separate reduce operations to calculate totals. This can be optimized to a single pass.

Consider this optimization:

  const { totalIncome, totalExpense } = useMemo(() => {
    if (!data) {
      return { totalIncome: 0, totalExpense: 0 };
    }
-   return {
-     totalIncome: data.calendarData.reduce(
-       (prev, cur) => prev + cur.totalIncome,
-       0,
-     ),
-     totalExpense: data.calendarData.reduce(
-       (prev, cur) => prev + cur.totalExpense,
-       0,
-     ),
-   };
+   return data.calendarData.reduce(
+     (acc, cur) => ({
+       totalIncome: acc.totalIncome + cur.totalIncome,
+       totalExpense: acc.totalExpense + cur.totalExpense,
+     }),
+     { totalIncome: 0, totalExpense: 0 },
+   );
  }, [data]);

431-473: Enhance accessibility for financial indicators

While basic aria-labels are present, the financial information could be more accessible.

Consider these improvements:

  <View
    style={{
      color: chartTheme.colors.blue,
      flexDirection: 'row',
      fontSize: '10px',
      marginRight: 10,
    }}
-   aria-label="Income"
+   aria-label={`Income ${calendar.totalIncome !== 0 ? amountToCurrency(calendar.totalIncome) : 'none'}`}
+   role="text"
  >
    {/* ... */}
  </View>
  <View
    style={{
      color: chartTheme.colors.red,
      flexDirection: 'row',
      fontSize: '10px',
    }}
-   aria-label="Expenses"
+   aria-label={`Expenses ${calendar.totalExpense !== 0 ? amountToCurrency(calendar.totalExpense) : 'none'}`}
+   role="text"
  >
packages/desktop-client/src/components/reports/reports/Calendar.tsx (3)

93-131: Consider consolidating related state management

The component manages several related pieces of state (start, end, mode) separately. Consider using useReducer to manage these related states together, which would make the state transitions more predictable and easier to maintain.

-  const [start, setStart] = useState(initialStart);
-  const [end, setEnd] = useState(initialEnd);
-  const [mode, setMode] = useState(initialMode);
+  const [dateRange, dispatch] = useReducer(dateRangeReducer, {
+    start: initialStart,
+    end: initialEnd,
+    mode: initialMode
+  });

702-830: Consider extracting styles for better maintainability

The component contains multiple inline styles that could be extracted into a separate styles object or styled-components for better organization and reuse.

+const calendarStyles = {
+  container: {
+    minWidth: '300px',
+    maxWidth: '300px',
+    padding: 10,
+    borderRadius: 4,
+    backgroundColor: theme.tableBackground,
+  },
+  header: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    flexWrap: 'wrap',
+    marginBottom: 16,
+  },
+  // ... other styles
+};

 function CalendarWithHeader({ ... }) {
   return (
     <View
-      style={{
-        minWidth: '300px',
-        maxWidth: '300px',
-        padding: 10,
-        borderRadius: 4,
-        backgroundColor: theme.tableBackground,
-      }}
+      style={calendarStyles.container}
     >

913-932: Simplify field mapping with object literal

The switch statement could be replaced with an object literal for better maintainability and performance.

+const FIELD_MAPPINGS = {
+  account: 'account.name',
+  payee: 'payee.name',
+  category: 'category.name',
+  payment: 'amount',
+  deposit: 'amount',
+};

 function getField(field?: string) {
-  if (!field) {
-    return 'date';
-  }
-
-  switch (field) {
-    case 'account':
-      return 'account.name';
-    case 'payee':
-      return 'payee.name';
-    case 'category':
-      return 'category.name';
-    case 'payment':
-      return 'amount';
-    case 'deposit':
-      return 'amount';
-    default:
-      return field;
-  }
+  if (!field) return 'date';
+  return FIELD_MAPPINGS[field] || field;
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between ad6ac02 and 2e89ef2.

📒 Files selected for processing (4)
  • packages/desktop-client/src/components/reports/graphs/CalendarGraph.tsx (1 hunks)
  • packages/desktop-client/src/components/reports/reports/Calendar.tsx (1 hunks)
  • packages/desktop-client/src/components/reports/reports/CalendarCard.tsx (1 hunks)
  • packages/desktop-client/src/components/reports/spreadsheets/calendar-spreadsheet.ts (1 hunks)
🧰 Additional context used
📓 Learnings (1)
packages/desktop-client/src/components/reports/spreadsheets/calendar-spreadsheet.ts (1)
Learnt from: lelemm
PR: actualbudget/actual#3828
File: packages/desktop-client/src/components/reports/spreadsheets/calendar-spreadsheet.ts:154-157
Timestamp: 2024-11-12T16:44:22.913Z
Learning: In `packages/desktop-client/src/components/reports/spreadsheets/calendar-spreadsheet.ts`, the variable `firstDayOfWeekIdx` comes from a system parameter and is guaranteed to be a number between 0 and 6, so additional validation is not necessary when using it in `startOfWeek`.
🪛 Biome
packages/desktop-client/src/components/reports/graphs/CalendarGraph.tsx

[error] 56-56: isNaN is unsafe. It attempts a type coercion. Use Number.isNaN instead.

See the MDN documentation for more details.
Unsafe fix: Use Number.isNaN instead.

(lint/suspicious/noGlobalIsNan)

packages/desktop-client/src/components/reports/reports/CalendarCard.tsx

[error] 490-490: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


[error] 497-497: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


[error] 504-504: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


[error] 511-511: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


[error] 518-518: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

🔇 Additional comments (6)
packages/desktop-client/src/components/reports/spreadsheets/calendar-spreadsheet.ts (3)

1-17: LGTM! Well-structured imports and type definitions.

The imports are properly organized and the CalendarDataType interface is well-defined with clear type annotations.


79-101: LGTM! Robust error handling for data fetching.

The code properly handles potential errors during data fetching and provides appropriate fallbacks.


187-190: LGTM! Proper handling of firstDayOfWeekIdx.

Based on the learnings from previous reviews, firstDayOfWeekIdx is guaranteed to be a number between 0 and 6, so the current implementation is correct.

packages/desktop-client/src/components/reports/graphs/CalendarGraph.tsx (1)

1-294: Overall implementation looks good!

The calendar graph implementation effectively visualizes financial data with good separation of concerns, proper type definitions, and responsive design. The suggested improvements are mostly about code organization and reducing duplication.

🧰 Tools
🪛 Biome

[error] 56-56: isNaN is unsafe. It attempts a type coercion. Use Number.isNaN instead.

See the MDN documentation for more details.
Unsafe fix: Use Number.isNaN instead.

(lint/suspicious/noGlobalIsNan)

packages/desktop-client/src/components/reports/reports/CalendarCard.tsx (1)

1-525: Overall implementation looks good!

The calendar card implementation is well-structured with proper separation of concerns, good use of hooks, and responsive design considerations. The suggested improvements are non-blocking and can be addressed in follow-up iterations.

🧰 Tools
🪛 Biome

[error] 490-490: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


[error] 497-497: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


[error] 504-504: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


[error] 511-511: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)


[error] 518-518: The assignment should not be in an expression.

The use of assignments in expressions is confusing.
Expressions are often considered as side-effect free.

(lint/suspicious/noAssignInExpressions)

packages/desktop-client/src/components/reports/reports/Calendar.tsx (1)

73-86: LGTM! Clean component structure with proper loading handling.

The Calendar component effectively manages the loading state and cleanly delegates the main functionality to CalendarInner.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Outside diff range and nitpick comments (20)
packages/desktop-client/src/components/reports/spreadsheets/calendar-spreadsheet.ts (2)

206-211: Simplify firstDayOfWeekIdx validation

The current validation is verbose and performs multiple parseInt operations. Consider simplifying it using a single parse operation.

-      weekStartsOn:
-        firstDayOfWeekIdx !== undefined &&
-        !Number.isNaN(parseInt(firstDayOfWeekIdx)) &&
-        parseInt(firstDayOfWeekIdx) >= 0 &&
-        parseInt(firstDayOfWeekIdx) <= 6
-          ? (parseInt(firstDayOfWeekIdx) as 0 | 1 | 2 | 3 | 4 | 5 | 6)
-          : 0,
+      weekStartsOn: (() => {
+        const idx = typeof firstDayOfWeekIdx === 'string' ? parseInt(firstDayOfWeekIdx) : firstDayOfWeekIdx;
+        return idx !== undefined && idx >= 0 && idx <= 6 ? (idx as 0 | 1 | 2 | 3 | 4 | 5 | 6) : 0;
+      })(),

61-62: Enhance error messages for date parsing failures

The current error messages could be more specific to help with debugging.

-      throw new Error('Invalid start date format');
+      throw new Error(`Invalid start date format: "${monthUtils.firstDayOfMonth(start)}". Expected format: yyyy-MM-dd`);
-      throw new Error('Invalid end date format');
+      throw new Error(`Invalid end date format: "${monthUtils.lastDayOfMonth(end)}". Expected format: yyyy-MM-dd`);

Also applies to: 73-74

packages/desktop-client/src/components/reports/graphs/CalendarGraph.tsx (6)

34-43: Remove or implement the commented filter functionality

The commented out onFilter prop and its type definition suggest an incomplete feature. Either implement the filtering functionality or remove the commented code to maintain code cleanliness.


54-60: Optimize firstDayOfWeekIdx parsing

The parseInt function is called multiple times on the same value. Consider parsing it once and storing the result.

-    weekStartsOn:
-      firstDayOfWeekIdx !== undefined &&
-      !Number.isNaN(parseInt(firstDayOfWeekIdx)) &&
-      parseInt(firstDayOfWeekIdx) >= 0 &&
-      parseInt(firstDayOfWeekIdx) <= 6
-        ? (parseInt(firstDayOfWeekIdx) as 0 | 1 | 2 | 3 | 4 | 5 | 6)
-        : 0,
+    weekStartsOn: (() => {
+      const parsed = parseInt(firstDayOfWeekIdx);
+      return firstDayOfWeekIdx !== undefined &&
+        !Number.isNaN(parsed) &&
+        parsed >= 0 &&
+        parsed <= 6
+          ? (parsed as 0 | 1 | 2 | 3 | 4 | 5 | 6)
+          : 0;
+    })(),

64-71: Extract magic numbers into named constants

The font size calculation uses magic numbers (14). Consider extracting these into named constants for better maintainability.

+const MAX_FONT_SIZE = 14;
+
 const buttonRef = useResizeObserver(rect => {
   const newValue = Math.floor(rect.height / 2);
-  if (newValue > 14) {
-    setFontSize(14);
+  if (newValue > MAX_FONT_SIZE) {
+    setFontSize(MAX_FONT_SIZE);
   } else {
     setFontSize(newValue);
   }
 });

143-143: Use translation keys for tooltip labels

The strings "Income:" and "Expenses:" are hardcoded. Since you're using i18n, these should use translation keys for consistency.

-            Income:
+            {t('income.label')}
// ...
-            Expenses:
+            {t('expenses.label')}

Also applies to: 163-163


214-216: Clean up effect dependencies

The useEffect hook doesn't clean up the state when the component unmounts. While this might not cause issues in the current implementation, it's a good practice to include cleanup.

 useEffect(() => {
   setCurrentFontSize(fontSize);
+  return () => {
+    // Clean up any pending state updates
+    setCurrentFontSize(prevSize => prevSize);
+  };
 }, [fontSize]);

267-267: Extract transition styles into theme

The transition properties are duplicated. Consider extracting them into the theme for consistency and reusability.

// In theme.ts
+export const transitions = {
+  barHeight: 'height 0.5s ease-out',
+};

// In this file
-          transition: 'height 0.5s ease-out',
+          transition: theme.transitions.barHeight,
// ...
-          transition: 'height 0.5s ease-out',
+          transition: theme.transitions.barHeight,

Also applies to: 281-281

packages/desktop-client/src/style/themes/development.ts (1)

217-217: Group calendar-related constants together.

Consider moving this constant to be grouped with other calendar-related constants around line 134 for better maintainability and readability.

 export const calendarText = colorPalette.navy50;
 export const calendarBackground = colorPalette.navy900;
 export const calendarItemText = colorPalette.navy150;
 export const calendarItemBackground = colorPalette.navy800;
 export const calendarSelectedBackground = colorPalette.navy500;
+export const calendarCellBackground = colorPalette.navy900;
packages/desktop-client/src/style/themes/dark.ts (1)

217-217: Group calendar-related constants together.

Consider moving this constant to be grouped with other calendar-related constants around line 144 for better code organization and maintainability.

 export const calendarText = colorPalette.navy50;
 export const calendarBackground = colorPalette.navy900;
 export const calendarItemText = colorPalette.navy150;
 export const calendarItemBackground = colorPalette.navy800;
 export const calendarSelectedBackground = buttonNormalSelectedBackground;
+export const calendarCellBackground = colorPalette.navy900;
packages/desktop-client/src/style/themes/light.ts (1)

219-219: Consider grouping with existing calendar constants.

While the new calendarCellBackground constant is valid, consider moving it near the other calendar-related constants (around line 144) to maintain better code organization and make it easier to manage calendar theming.

 export const calendarSelectedBackground = colorPalette.navy500;
+export const calendarCellBackground = colorPalette.navy100;
 
 export const buttonBareText = buttonNormalText;
packages/desktop-client/src/style/themes/midnight.ts (1)

219-219: Group calendar-related constants together.

Consider moving calendarCellBackground to be grouped with other calendar-related constants (around line 150) for better code organization and maintainability.

 export const calendarSelectedBackground = buttonNormalSelectedBackground;
+export const calendarCellBackground = colorPalette.navy900;
 
 export const buttonBareText = buttonNormalText;
-
-export const calendarCellBackground = colorPalette.navy900;
packages/desktop-client/src/components/reports/reports/CalendarCard.tsx (4)

159-169: Consider using TypeScript discriminated unions for menu items

The switch statement uses string literals, which could be made type-safe using a discriminated union type.

+type MenuItem = { name: 'rename'; text: string } | { name: 'remove'; text: string };

-onMenuSelect={item => {
+onMenuSelect={(item: MenuItem['name']) => {
   switch (item) {

207-223: Add aria-label to income and expense sections in tooltip

The tooltip content should have proper aria-labels for better accessibility.

-<View style={{ lineHeight: 1.5 }}>
+<View style={{ lineHeight: 1.5 }} aria-label="Financial summary">
   <View
     style={{
       display: 'grid',
       gridTemplateColumns: '70px 1fr',
       gridAutoRows: '1fr',
     }}
+    role="list"
   >

Also applies to: 224-240


398-423: Enhance keyboard navigation for the month button

The month button should have proper keyboard focus management and ARIA attributes.

 <Button
   variant="bare"
+  role="link"
+  aria-label={`View calendar for ${format(calendar.start, selectedMonthNameFormat)}`}
   style={{
     visibility: monthNameVisible ? 'visible' : 'hidden',
     overflow: 'visible',

489-532: Refactor month format measurement elements

The repeated span elements for month format measurements could be generated programmatically to reduce code duplication.

+const MONTH_FORMATS = [
+  'MMMM yyyy',
+  'MMM yyyy',
+  'MMM yy',
+  'MMM',
+  ''
+] as const;

 <View style={{ fontWeight: 'bold', fontSize: '12px' }}>
-  <span
-    ref={node => {
-      if (node) monthFormatSizeContainers.current[0] = node;
-    }}
-    style={{ position: 'fixed', top: -9999, left: -9999 }}
-    data-format="MMMM yyyy"
-  >
-    {format(calendar.start, 'MMMM yyyy')}:
-  </span>
+  {MONTH_FORMATS.map((formatString, idx) => (
+    <span
+      key={formatString}
+      ref={node => {
+        if (node) monthFormatSizeContainers.current[idx] = node;
+      }}
+      style={{ position: 'fixed', top: -9999, left: -9999 }}
+      data-format={formatString}
+    >
+      {formatString ? `${format(calendar.start, formatString)}:` : ''}
+    </span>
+  ))}
packages/desktop-client/src/components/reports/reports/Calendar.tsx (4)

121-122: Consider using a type-safe default for firstDayOfWeekIdx

The current fallback to '0' could be more explicit and type-safe.

-const [_firstDayOfWeekIdx] = useSyncedPref('firstDayOfWeekIdx');
-const firstDayOfWeekIdx = _firstDayOfWeekIdx || '0';
+const DEFAULT_FIRST_DAY_OF_WEEK = '0';
+const [_firstDayOfWeekIdx] = useSyncedPref('firstDayOfWeekIdx');
+const firstDayOfWeekIdx = _firstDayOfWeekIdx ?? DEFAULT_FIRST_DAY_OF_WEEK;

724-730: Extract hardcoded dimensions as constants

Consider extracting the hardcoded dimensions for better maintainability.

+const CALENDAR_DIMENSIONS = {
+  MIN_WIDTH: '300px',
+  MAX_WIDTH: '300px',
+  PADDING: 10,
+  BORDER_RADIUS: 4,
+};
+
 <View
   style={{
-    minWidth: '300px',
-    maxWidth: '300px',
-    padding: 10,
-    borderRadius: 4,
+    minWidth: CALENDAR_DIMENSIONS.MIN_WIDTH,
+    maxWidth: CALENDAR_DIMENSIONS.MAX_WIDTH,
+    padding: CALENDAR_DIMENSIONS.PADDING,
+    borderRadius: CALENDAR_DIMENSIONS.BORDER_RADIUS,
     backgroundColor: theme.tableBackground,
   }}
 >

889-918: Simplify conditional rendering logic

The conditions for showing income and expense can be simplified to avoid repetition.

-            {totalIncome !== 0 && (
+            {[
+              { label: 'Income:', value: totalIncome, color: chartTheme.colors.blue },
+              { label: 'Expenses:', value: totalExpense, color: chartTheme.colors.red }
+            ].map(({ label, value, color }) => value !== 0 && (
               <>
                 <View
                   style={{
                     textAlign: 'right',
                     marginRight: 4,
                   }}
                 >
-                  Income:
+                  {label}
                 </View>
-                <View style={{ color: chartTheme.colors.blue }}>
-                  {totalIncome !== 0 ? amountToCurrency(totalIncome) : ''}
+                <View style={{ color }}>
+                  {amountToCurrency(value)}
                 </View>
               </>
-            )}
-            {totalExpense !== 0 && (
-              <>
-                <View
-                  style={{
-                    textAlign: 'right',
-                    marginRight: 4,
-                  }}
-                >
-                  Expenses:
-                </View>
-                <View style={{ color: chartTheme.colors.red }}>
-                  {totalExpense !== 0 ? amountToCurrency(totalExpense) : ''}
-                </View>
-              </>
-            )}
+            ))}

926-945: Enhance type safety of field mapping

Consider using TypeScript enums or constants for field names to improve type safety and maintainability.

+enum FieldMapping {
+  Account = 'account',
+  Payee = 'payee',
+  Category = 'category',
+  Payment = 'payment',
+  Deposit = 'deposit',
+}
+
+const FIELD_NAME_MAPPING: Record<FieldMapping, string> = {
+  [FieldMapping.Account]: 'account.name',
+  [FieldMapping.Payee]: 'payee.name',
+  [FieldMapping.Category]: 'category.name',
+  [FieldMapping.Payment]: 'amount',
+  [FieldMapping.Deposit]: 'amount',
+};
+
 function getField(field?: string) {
   if (!field) {
     return 'date';
   }
 
-  switch (field) {
-    case 'account':
-      return 'account.name';
-    case 'payee':
-      return 'payee.name';
-    case 'category':
-      return 'category.name';
-    case 'payment':
-      return 'amount';
-    case 'deposit':
-      return 'amount';
-    default:
-      return field;
-  }
+  return FIELD_NAME_MAPPING[field as FieldMapping] || field;
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 2e89ef2 and e3bd0d0.

📒 Files selected for processing (8)
  • packages/desktop-client/src/components/reports/graphs/CalendarGraph.tsx (1 hunks)
  • packages/desktop-client/src/components/reports/reports/Calendar.tsx (1 hunks)
  • packages/desktop-client/src/components/reports/reports/CalendarCard.tsx (1 hunks)
  • packages/desktop-client/src/components/reports/spreadsheets/calendar-spreadsheet.ts (1 hunks)
  • packages/desktop-client/src/style/themes/dark.ts (1 hunks)
  • packages/desktop-client/src/style/themes/development.ts (1 hunks)
  • packages/desktop-client/src/style/themes/light.ts (1 hunks)
  • packages/desktop-client/src/style/themes/midnight.ts (1 hunks)
🔇 Additional comments (6)
packages/desktop-client/src/components/reports/spreadsheets/calendar-spreadsheet.ts (1)

1-17: LGTM! Well-organized imports and clear type definitions.

The imports are properly organized and the CalendarDataType interface is well-structured with clear field names.

packages/desktop-client/src/style/themes/dark.ts (1)

217-217: Verify color contrast ratios for accessibility.

The calendarCellBackground uses navy900, which should provide sufficient contrast with text colors. However, let's verify the contrast ratios meet WCAG guidelines.

packages/desktop-client/src/style/themes/light.ts (1)

219-219: Verify color contrast with existing calendar colors.

The new calendarCellBackground uses navy100 which needs to work well with existing calendar colors:

  • Text: navy50
  • Background: navy900
  • Item text: navy150
  • Item background: navy800
packages/desktop-client/src/components/reports/reports/CalendarCard.tsx (2)

1-51: LGTM! Well-structured imports and type definitions.

The imports are properly organized, and the type definitions are clear and comprehensive.


68-78: Consider adding error handling for invalid date ranges

The useMemo hook calculates params without validating that start is before end. While the calculateTimeRange function likely handles this, explicit validation would improve robustness.

packages/desktop-client/src/components/reports/reports/Calendar.tsx (1)

73-86: LGTM! Well-structured component with proper loading state handling.

The component follows good practices by:

  • Using TypeScript for type safety
  • Showing a loading indicator during data fetching
  • Having a single responsibility

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Outside diff range and nitpick comments (17)
packages/desktop-client/src/components/reports/spreadsheets/calendar-spreadsheet.ts (3)

18-24: Consider adding input validation for date parameters.

The function accepts date strings but doesn't validate their format at the parameter level. While there's error handling during parsing, early validation could prevent unnecessary processing.

Consider adding validation:

 export function calendarSpreadsheet(
   start: string,
   end: string,
   conditions: RuleConditionEntity[] = [],
   conditionsOp: 'and' | 'or' = 'and',
   firstDayOfWeekIdx?: SyncedPrefs['firstDayOfWeekIdx'],
 ) {
+  const isValidDateFormat = (date: string) => /^\d{4}-\d{2}-\d{2}$/.test(date);
+  if (!isValidDateFormat(start) || !isValidDateFormat(end)) {
+    throw new Error('Invalid date format. Expected: YYYY-MM-DD');
+  }
   return async (

219-223: Consider extracting and documenting the calendar grid calculation logic.

The logic for calculating totalDays and padding to ensure complete weeks is important but could be more self-documenting.

Consider extracting this logic:

-    let totalDays =
-      d.differenceInDays(firstDay, beginDay) + d.getDaysInMonth(firstDay);
-    if (totalDays % 7 !== 0) {
-      totalDays += 7 - (totalDays % 7);
-    }
+    const calculateCalendarGridDays = (firstDay: Date, beginDay: Date) => {
+      // Calculate days needed to display the full month
+      const daysBeforeMonth = d.differenceInDays(firstDay, beginDay);
+      const daysInMonth = d.getDaysInMonth(firstDay);
+      const totalDays = daysBeforeMonth + daysInMonth;
+      
+      // Ensure we have complete weeks by padding if necessary
+      const remainingDays = totalDays % 7;
+      return remainingDays === 0 ? totalDays : totalDays + (7 - remainingDays);
+    };
+    
+    const totalDays = calculateCalendarGridDays(firstDay, beginDay);

244-244: Document currency conversion assumptions.

The code divides monetary values by 100, suggesting a conversion from cents to dollars/euros, but this assumption isn't documented.

Consider adding comments:

     return {
       data: daysArray as CalendarDataType[],
+      // Convert from cents to dollars/euros
       totalExpense: (totalExpenseValue ?? 0) / 100,
       totalIncome: (totalIncomeValue ?? 0) / 100,
     };

Also applies to: 246-246, 253-254

packages/desktop-client/src/components/reports/graphs/CalendarGraph.tsx (6)

34-43: Remove or implement the commented filter functionality

The commented out onFilter prop suggests incomplete functionality. Either implement the filtering feature or remove the commented code to maintain clean types.

-  // onFilter: (
-  //   conditionsOrSavedFilter:
-  //     | null
-  //     | {
-  //         conditions: RuleConditionEntity[];
-  //         conditionsOp: 'and' | 'or';
-  //         id: RuleConditionEntity[];
-  //       }
-  //     | RuleConditionEntity,
-  // ) => void;

23-34: Add JSDoc comments to type definitions

Consider adding documentation to describe the purpose and usage of each prop in the CalendarGraphProps type.

+/**
+ * Props for the CalendarGraph component
+ * @property data - Array of daily financial data
+ * @property start - Starting date for the calendar
+ * @property firstDayOfWeekIdx - User preference for first day of week
+ * @property onDayClick - Callback when a day is clicked
+ */
 type CalendarGraphProps = {
   data: {
     date: Date;
     incomeValue: number;
     expenseValue: number;
     incomeSize: number;
     expenseSize: number;
   }[];
   start: Date;
   firstDayOfWeekIdx?: SyncedPrefs['firstDayOfWeekIdx'];
   onDayClick: (date: Date) => void;
 };

110-110: Remove or implement the commented grid calculation

There's a commented out grid calculation that should either be implemented or removed.

-          //gridTemplateRows: `repeat(${Math.trunc(data.length) <= data.length / 7 ? Math.trunc(data.length) : Math.trunc(data.length) + 1},1fr)`,

119-178: Extract tooltip content into a separate component

The tooltip content is complex enough to warrant its own component. This would improve maintainability and reusability.

Consider creating a DayTooltip component:

type DayTooltipProps = {
  day: {
    date: Date;
    incomeValue: number;
    expenseValue: number;
    incomeSize: number;
    expenseSize: number;
  };
};

function DayTooltip({ day }: DayTooltipProps) {
  const { t } = useTranslation();
  return (
    <View>
      <View style={{ marginBottom: 10 }}>
        <strong>
          {t('Day:') + ' '}
          {format(day.date, 'dd')}
        </strong>
      </View>
      {/* ... rest of the tooltip content ... */}
    </View>
  );
}

211-296: Optimize DayButton component performance

The component could benefit from several performance optimizations:

  1. Memoize the component to prevent unnecessary re-renders
  2. Extract static styles
  3. Use CSS classes instead of inline styles for common properties
+ const dayButtonStyles = {
+   button: {
+     borderColor: 'transparent',
+     backgroundColor: theme.calendarCellBackground,
+     position: 'relative' as const,
+     padding: 'unset',
+     height: '100%',
+     minWidth: 0,
+     minHeight: 0,
+     margin: 0,
+   },
+   // ... other static styles
+ };

- function DayButton({ day, onPress, fontSize, resizeRef }: DayButtonProps) {
+ const DayButton = React.memo(function DayButton({ 
+   day, 
+   onPress, 
+   fontSize, 
+   resizeRef 
+ }: DayButtonProps) {
   // ... component implementation
- }
+ });

234-284: Consider using CSS Grid for bar layout

The current implementation uses absolute positioning for the bars. Consider using CSS Grid for better maintainability and performance.

+ const barContainerStyle = {
+   display: 'grid',
+   gridTemplateColumns: '1fr 1fr',
+   height: '100%',
+   position: 'absolute',
+   width: '100%',
+ };

- {day.expenseSize !== 0 && (
-   <View
-     style={{
-       position: 'absolute',
-       width: '50%',
-       height: '100%',
-       // ...
-     }}
-   />
- )}
+ <View style={barContainerStyle}>
+   <View
+     style={{
+       height: `${Math.ceil(day.incomeSize)}%`,
+       backgroundColor: chartTheme.colors.blue,
+       opacity: 0.9,
+       alignSelf: 'end',
+       transition: 'height 0.5s ease-out',
+     }}
+   />
+   <View
+     style={{
+       height: `${Math.ceil(day.expenseSize)}%`,
+       backgroundColor: chartTheme.colors.red,
+       opacity: 0.9,
+       alignSelf: 'end',
+       transition: 'height 0.5s ease-out',
+     }}
+   />
+ </View>
packages/desktop-client/src/components/reports/reports/CalendarCard.tsx (4)

200-244: Add i18n translations for tooltip content

The tooltip content contains hardcoded English strings that should be translated.

Consider using the translation hook:

  <View style={{ lineHeight: 1.5 }}>
    <View style={{
      display: 'grid',
      gridTemplateColumns: '70px 1fr',
      gridAutoRows: '1fr',
    }}>
      {totalIncome !== 0 && (
        <>
          <View style={{
            textAlign: 'right',
            marginRight: 4,
          }}>
-           Income:
+           {t('Income')}:
          </View>
          <View style={{ color: chartTheme.colors.blue }}>
            {totalIncome !== 0 ? amountToCurrency(totalIncome) : ''}
          </View>
        </>
      )}
      {totalExpense !== 0 && (
        <>
          <View style={{
            textAlign: 'right',
            marginRight: 4,
          }}>
-           Expenses:
+           {t('Expenses')}:
          </View>
          <View style={{ color: chartTheme.colors.red }}>
            {totalExpense !== 0 ? amountToCurrency(totalExpense) : ''}
          </View>
        </>
      )}
    </View>
  </View>

160-170: Improve menu selection error handling

The error handling for unrecognized menu selections could be more graceful.

Consider using a more descriptive error message and logging:

  switch (item) {
    case 'rename':
      setNameMenuOpen(true);
      break;
    case 'remove':
      onRemove();
      break;
    default:
-     throw new Error(`Unrecognized selection: ${item}`);
+     console.error(`Unrecognized menu selection: ${item}`);
+     // Handle gracefully instead of throwing
+     break;
  }

327-333: Memoize month format measurements

The measureMonthFormats function could benefit from memoization to prevent unnecessary recalculations.

Consider using useMemo:

- const measureMonthFormats = useCallback(() => {
+ const measureMonthFormats = useMemo(() => () => {
    const measurements = monthFormatSizeContainers.current.map(container => ({
      width: container?.clientWidth ?? 0,
      format: container?.getAttribute('data-format') ?? '',
    }));
    return measurements;
  }, []);

487-493: Add error boundary for calendar click handler

The calendar day click handler should be wrapped in a try-catch block to handle potential navigation errors gracefully.

Consider adding error handling:

  onDayClick={date => {
+   try {
      navigate(
        isDashboardsFeatureEnabled
          ? `/reports/calendar/${widgetId}?day=${format(date, 'yyyy-MM-dd')}`
          : '/reports/calendar',
      );
+   } catch (error) {
+     console.error('Failed to navigate:', error);
+     // Handle the error gracefully, perhaps show a notification
+   }
  }}
packages/desktop-client/src/components/reports/reports/Calendar.tsx (3)

93-691: Consider breaking down the CalendarInner component.

The component is handling multiple responsibilities including state management, filtering, mobile UI, and transaction list rendering. Consider extracting some of these into separate components for better maintainability.

Potential extractions:

  • Filter management logic
  • Mobile transaction drawer
  • Transaction list wrapper

740-771: Add aria-label to month filter button.

The button shows the month and year but lacks an aria-label to indicate its filtering action.

 <Button
   variant="bare"
+  aria-label={`Filter transactions for ${format(calendar.start, 'MMMM yyyy')}`}
   style={{
     color: theme.pageTextSubdued,
     fontWeight: 'bold',

926-945: Improve type safety of getField function.

Consider using a union type for the field parameter to catch invalid field names at compile time.

+type AllowedFields = 'account' | 'payee' | 'category' | 'payment' | 'deposit' | 'date';
+
-function getField(field?: string) {
+function getField(field?: AllowedFields) {
   if (!field) {
     return 'date';
   }
packages/desktop-client/src/components/transactions/TransactionsTable.jsx (1)

1918-1919: Consider using React Context for prop propagation

While the current implementation correctly propagates the selection and split transaction props through the component hierarchy, consider using React Context to avoid prop drilling, especially since these props are used by multiple nested components.

Example implementation:

+const TransactionContext = React.createContext({
+  showSelection: true,
+  allowSplitTransaction: true
+});
+
+export function TransactionProvider({ children, showSelection, allowSplitTransaction }) {
+  return (
+    <TransactionContext.Provider value={{ showSelection, allowSplitTransaction }}>
+      {children}
+    </TransactionContext.Provider>
+  );
+}

This would simplify prop passing and make the code more maintainable.

Also applies to: 2002-2003, 2028-2028, 2649-2650

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between e3bd0d0 and a2cebb2.

⛔ Files ignored due to path filters (1)
  • upcoming-release-notes/3828.md is excluded by !**/*.md
📒 Files selected for processing (15)
  • packages/desktop-client/src/components/reports/Overview.tsx (5 hunks)
  • packages/desktop-client/src/components/reports/ReportRouter.tsx (2 hunks)
  • packages/desktop-client/src/components/reports/graphs/CalendarGraph.tsx (1 hunks)
  • packages/desktop-client/src/components/reports/reportRanges.ts (1 hunks)
  • packages/desktop-client/src/components/reports/reports/Calendar.tsx (1 hunks)
  • packages/desktop-client/src/components/reports/reports/CalendarCard.tsx (1 hunks)
  • packages/desktop-client/src/components/reports/spreadsheets/calendar-spreadsheet.ts (1 hunks)
  • packages/desktop-client/src/components/transactions/TransactionList.jsx (2 hunks)
  • packages/desktop-client/src/components/transactions/TransactionsTable.jsx (10 hunks)
  • packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx (1 hunks)
  • packages/desktop-client/src/style/themes/dark.ts (1 hunks)
  • packages/desktop-client/src/style/themes/development.ts (1 hunks)
  • packages/desktop-client/src/style/themes/light.ts (1 hunks)
  • packages/desktop-client/src/style/themes/midnight.ts (1 hunks)
  • packages/loot-core/src/types/models/dashboard.d.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (9)
  • packages/desktop-client/src/components/reports/Overview.tsx
  • packages/desktop-client/src/components/reports/ReportRouter.tsx
  • packages/desktop-client/src/components/reports/reportRanges.ts
  • packages/desktop-client/src/components/transactions/TransactionList.jsx
  • packages/desktop-client/src/style/themes/dark.ts
  • packages/desktop-client/src/style/themes/development.ts
  • packages/desktop-client/src/style/themes/light.ts
  • packages/desktop-client/src/style/themes/midnight.ts
  • packages/loot-core/src/types/models/dashboard.d.ts
🧰 Additional context used
📓 Learnings (1)
packages/desktop-client/src/components/reports/reports/Calendar.tsx (1)
Learnt from: lelemm
PR: actualbudget/actual#3828
File: packages/desktop-client/src/components/reports/reports/Calendar.tsx:575-631
Timestamp: 2024-11-12T18:18:07.283Z
Learning: In `Calendar.tsx`, transaction-related callbacks such as `onBatchDelete`, `onBatchDuplicate`, `onCreateRule`, and `onScheduleAction` are intentionally left as empty functions because these operations should not be usable on that page.
🔇 Additional comments (9)
packages/desktop-client/src/components/reports/spreadsheets/calendar-spreadsheet.ts (1)

1-17: LGTM! Well-structured imports and type definitions.

The imports are properly organized, and the CalendarDataType interface is well-defined with clear field names.

packages/desktop-client/src/components/reports/reports/CalendarCard.tsx (1)

64-68: 🛠️ Refactor suggestion

Add date validation in calculateTimeRange

The time range calculation doesn't validate the input dates. This could lead to unexpected behavior if meta?.timeFrame contains invalid values.

Consider adding date validation:

  const [start, end] = calculateTimeRange(meta?.timeFrame, {
+   // Ensure valid dates or fallback to defaults
+   start: monthUtils.isValidDate(monthUtils.dayFromDate(monthUtils.currentMonth()))
+     ? monthUtils.dayFromDate(monthUtils.currentMonth())
+     : new Date(),
+   end: monthUtils.isValidDate(monthUtils.currentDay())
+     ? monthUtils.currentDay()
+     : new Date(),
-   start: monthUtils.dayFromDate(monthUtils.currentMonth()),
-   end: monthUtils.currentDay(),
    mode: 'full',
  });
packages/desktop-client/src/components/reports/reports/Calendar.tsx (3)

73-86: LGTM! Clean and well-structured component.

The Calendar component properly handles loading states and provides good type safety.


417-421: LGTM! Well-structured constants.

Good practice extracting magic numbers into named constants for the drag gesture configuration.


853-924: LGTM! Well-structured responsive component.

The CalendarCardHeader component handles responsive layout well and properly formats financial data.

packages/desktop-client/src/components/transactions/TransactionsTable.test.jsx (1)

Line range hint 1-1000: Well-structured and comprehensive test suite!

The test suite demonstrates excellent practices:

  • Thorough setup with mock data and helper functions
  • Comprehensive coverage of transaction operations
  • Clear test descriptions and assertions
  • Good isolation of test cases
  • Proper cleanup after tests
packages/desktop-client/src/components/transactions/TransactionsTable.jsx (3)

Line range hint 182-235: LGTM! Clean implementation of conditional selection in header

The implementation properly handles both selection and non-selection cases while maintaining layout consistency. The SelectCell component is well configured with appropriate styling and event handling.


886-887: LGTM! Well-integrated selection handling in Transaction component

The changes properly integrate selection handling while maintaining existing functionality. The selection cell rendering logic correctly handles all cases including previews and child transactions.

Also applies to: 1193-1203


1524-1524: LGTM! Good enhancement of split transaction control

The allowSplitTransaction prop is well utilized to provide granular control over split transaction functionality in the category autocomplete component.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Outside diff range and nitpick comments (13)
packages/desktop-client/src/components/reports/graphs/CalendarGraph.tsx (6)

34-43: Clean up commented code or document future plans

The commented onFilter prop suggests an incomplete feature for filtering conditions. Either implement the filtering functionality, remove the commented code, or add a TODO comment explaining future plans.


64-71: Optimize font size calculation

The font size calculation could be memoized to prevent unnecessary updates. Consider using useMemo for the calculation.

-  const buttonRef = useResizeObserver(rect => {
+  const buttonRef = useResizeObserver(rect => {
+    const calculateFontSize = (height: number) => {
+      const newValue = Math.floor(height / 2);
+      return newValue > 14 ? 14 : newValue;
+    };
+
-    const newValue = Math.floor(rect.height / 2);
-    if (newValue > 14) {
-      setFontSize(14);
-    } else {
-      setFontSize(newValue);
-    }
+    setFontSize(calculateFontSize(rect.height));
   });

79-81: Extract grid layout constants

Magic numbers in the grid layout could make maintenance difficult. Consider extracting these values into named constants.

+const CALENDAR_GRID = {
+  COLUMNS: 7,
+  GAP: 2,
+};
+
 <View
   style={{
     color: theme.pageTextSubdued,
     display: 'grid',
-    gridTemplateColumns: 'repeat(7, 1fr)',
+    gridTemplateColumns: `repeat(${CALENDAR_GRID.COLUMNS}, 1fr)`,
     gridAutoRows: '1fr',
-    gap: 2,
+    gap: CALENDAR_GRID.GAP,
   }}
 >

120-177: Extract tooltip content into a separate component

The tooltip content is complex and deeply nested. Consider extracting it into a separate component for better maintainability and reusability.

type DayTooltipProps = {
  day: {
    date: Date;
    incomeValue: number;
    expenseValue: number;
    incomeSize: number;
    expenseSize: number;
  };
};

function DayTooltip({ day }: DayTooltipProps) {
  const { t } = useTranslation();
  return (
    <View>
      <View style={{ marginBottom: 10 }}>
        <strong>
          {t('Day:') + ' '}
          {format(day.date, 'dd')}
        </strong>
      </View>
      {/* Rest of the tooltip content */}
    </View>
  );
}

219-225: Simplify state management

The currentFontSize state and effect could be eliminated since it's just mirroring the fontSize prop.

-  const [currentFontSize, setCurrentFontSize] = useState(fontSize);
-
-  useEffect(() => {
-    setCurrentFontSize(fontSize);
-  }, [fontSize]);

   return (
     <Button
       // ...
       <span
         style={{
-          fontSize: `${currentFontSize}px`,
+          fontSize: `${fontSize}px`,
           fontWeight: 500,
           position: 'relative',
         }}
       >

242-292: Extract common styles and calculations

The bar styles contain duplicated code and inline calculations. Consider extracting common styles and calculations into constants or utility functions.

const BAR_STYLES = {
  base: {
    position: 'absolute',
    width: '50%',
    height: '100%',
    opacity: 0.2,
  },
  active: {
    position: 'absolute',
    width: '50%',
    opacity: 0.9,
    bottom: 0,
    transition: 'height 0.5s ease-out',
  },
};

const calculateBarHeight = (size: number) => `${Math.ceil(size)}%`;

Then use these in your component:

-        <View
-          style={{
-            position: 'absolute',
-            width: '50%',
-            height: '100%',
-            background: chartTheme.colors.red,
-            opacity: 0.2,
-            right: 0,
-          }}
-        />
+        <View
+          style={{
+            ...BAR_STYLES.base,
+            background: chartTheme.colors.red,
+            right: 0,
+          }}
+        />
packages/desktop-client/src/components/reports/reports/CalendarCard.tsx (4)

69-79: Consider adding error boundaries for calendarSpreadsheet calculations.

The useMemo hook wraps complex calculations that could potentially throw errors. Adding error boundaries would improve error handling and user experience.

 const params = useMemo(
   () =>
+    try {
       calendarSpreadsheet(
         start,
         end,
         meta?.conditions,
         meta?.conditionsOp,
         firstDayOfWeekIdx,
       ),
+    } catch (error) {
+      console.error('Failed to calculate calendar data:', error);
+      return null;
+    }
   [start, end, meta?.conditions, meta?.conditionsOp, firstDayOfWeekIdx],
 );

198-248: Consider extracting tooltip content into a separate component.

The tooltip content is complex enough to warrant its own component, which would improve readability and maintainability.


159-170: Consider using TypeScript discriminated unions for menu items.

The current switch statement could benefit from TypeScript's discriminated unions to ensure type safety and exhaustive checking.

type MenuItem = 
  | { name: 'rename'; text: string }
  | { name: 'remove'; text: string };

const handleMenuSelect = (item: MenuItem['name']) => {
  switch (item) {
    case 'rename':
      setNameMenuOpen(true);
      break;
    case 'remove':
      onRemove();
      break;
  }
};

487-493: Add keyboard navigation support for calendar day selection.

The calendar day click handler should be accompanied by keyboard navigation support for better accessibility.

 onDayClick={date => {
   navigate(
     isDashboardsFeatureEnabled
       ? `/reports/calendar/${widgetId}?day=${format(date, 'yyyy-MM-dd')}`
       : '/reports/calendar',
   );
 }}
+onKeyDown={e => {
+  if (e.key === 'Enter' || e.key === ' ') {
+    e.preventDefault();
+    // Handle day selection
+  }
+}}
+role="button"
+tabIndex={0}
packages/desktop-client/src/components/reports/reports/Calendar.tsx (3)

121-122: Consider making the default value more explicit

The fallback value for firstDayOfWeekIdx could be more explicitly defined with a named constant.

+const DEFAULT_FIRST_DAY_OF_WEEK = '0';
 const [_firstDayOfWeekIdx] = useSyncedPref('firstDayOfWeekIdx');
-const firstDayOfWeekIdx = _firstDayOfWeekIdx || '0';
+const firstDayOfWeekIdx = _firstDayOfWeekIdx || DEFAULT_FIRST_DAY_OF_WEEK;

730-757: Consider extracting button styles

The button styles could be extracted into a shared style object or component to improve maintainability.

+const calendarHeaderButtonStyle = {
+  color: theme.pageTextSubdued,
+  fontWeight: 'bold' as const,
+  fontSize: '14px',
+  margin: 0,
+  padding: 0,
+  display: 'inline-block',
+  width: 'max-content',
+};

 <Button
   variant="bare"
-  style={{
-    color: theme.pageTextSubdued,
-    fontWeight: 'bold',
-    fontSize: '14px',
-    margin: 0,
-    padding: 0,
-    display: 'inline-block',
-    width: 'max-content',
-  }}
+  style={calendarHeaderButtonStyle}
   onPress={() => {

914-933: Consider using TypeScript enum for field mappings

The field mapping could be more type-safe using a TypeScript enum or const object.

+const FIELD_MAPPINGS = {
+  account: 'account.name',
+  payee: 'payee.name',
+  category: 'category.name',
+  payment: 'amount',
+  deposit: 'amount',
+} as const;
+
+type FieldKey = keyof typeof FIELD_MAPPINGS;
+
-function getField(field?: string) {
+function getField(field?: FieldKey | string) {
   if (!field) {
     return 'date';
   }

-  switch (field) {
-    case 'account':
-      return 'account.name';
-    case 'payee':
-      return 'payee.name';
-    case 'category':
-      return 'category.name';
-    case 'payment':
-      return 'amount';
-    case 'deposit':
-      return 'amount';
-    default:
-      return field;
-  }
+  return FIELD_MAPPINGS[field as FieldKey] || field;
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between a2cebb2 and c887f22.

📒 Files selected for processing (3)
  • packages/desktop-client/src/components/reports/graphs/CalendarGraph.tsx (1 hunks)
  • packages/desktop-client/src/components/reports/reports/Calendar.tsx (1 hunks)
  • packages/desktop-client/src/components/reports/reports/CalendarCard.tsx (1 hunks)
🧰 Additional context used
📓 Learnings (1)
packages/desktop-client/src/components/reports/reports/Calendar.tsx (1)
Learnt from: lelemm
PR: actualbudget/actual#3828
File: packages/desktop-client/src/components/reports/reports/Calendar.tsx:575-631
Timestamp: 2024-11-12T18:18:07.283Z
Learning: In `Calendar.tsx`, transaction-related callbacks such as `onBatchDelete`, `onBatchDuplicate`, `onCreateRule`, and `onScheduleAction` are intentionally left as empty functions because these operations should not be usable on that page.
🔇 Additional comments (4)
packages/desktop-client/src/components/reports/reports/CalendarCard.tsx (1)

1-52: LGTM! Well-structured imports and type definitions.

The imports are logically grouped and the type definitions are comprehensive with proper TypeScript features.

packages/desktop-client/src/components/reports/reports/Calendar.tsx (3)

73-86: LGTM! Well-structured component with proper loading state handling.

The Calendar component is implemented correctly with:

  • Proper TypeScript typing
  • Loading state handling
  • Clean props passing to CalendarInner

565-621: LGTM! Desktop transaction list implementation

The implementation correctly handles the desktop view with proper props configuration. Empty callbacks are intentionally left empty as these operations should not be usable on this page.


841-912: LGTM! Well-implemented responsive header

The CalendarCardHeader component is well structured with:

  • Proper handling of zero values
  • Responsive layout adaptation
  • Clear TypeScript types

lelemm and others added 3 commits November 13, 2024 08:51
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Outside diff range and nitpick comments (5)
packages/desktop-client/src/components/reports/reports/CalendarCard.tsx (3)

160-170: Improve error handling in menu selection

The current error handling throws a generic error. Consider using a type-safe approach with exhaustive checking.

-      switch (item) {
+      type MenuItems = 'rename' | 'remove';
+      switch (item as MenuItems) {
         case 'rename':
           setNameMenuOpen(true);
           break;
         case 'remove':
           onRemove();
           break;
         default: {
-          throw new Error(`Unrecognized selection: ${item}`);
+          const exhaustiveCheck: never = item;
+          throw new Error(
+            `Unrecognized menu selection: ${exhaustiveCheck}`
+          );
+        }
       }

198-244: Refactor tooltip content to reduce code duplication

The income and expense sections in the tooltip have similar structure. Consider extracting this into a reusable component.

+const TooltipRow = ({ label, value, color }: { label: string; value: number; color: string }) => (
+  <>
+    <View style={{ textAlign: 'right', marginRight: 4 }}>{label}:</View>
+    <View style={{ color }}>{value !== 0 ? amountToCurrency(value) : ''}</View>
+  </>
+);

 <Tooltip
   content={
     <View style={{ lineHeight: 1.5 }}>
       <View style={{
         display: 'grid',
         gridTemplateColumns: '70px 1fr',
         gridAutoRows: '1fr',
       }}>
-        {totalIncome !== 0 && (
-          <>
-            <View style={{ textAlign: 'right', marginRight: 4 }}>Income:</View>
-            <View style={{ color: chartTheme.colors.blue }}>
-              {totalIncome !== 0 ? amountToCurrency(totalIncome) : ''}
-            </View>
-          </>
-        )}
-        {totalExpense !== 0 && (
-          <>
-            <View style={{ textAlign: 'right', marginRight: 4 }}>Expenses:</View>
-            <View style={{ color: chartTheme.colors.red }}>
-              {totalExpense !== 0 ? amountToCurrency(totalExpense) : ''}
-            </View>
-          </>
-        )}
+        {totalIncome !== 0 && 
+          <TooltipRow label="Income" value={totalIncome} color={chartTheme.colors.blue} />
+        }
+        {totalExpense !== 0 &&
+          <TooltipRow label="Expenses" value={totalExpense} color={chartTheme.colors.red} />
+        }
       </View>
     </View>
   }
 >

455-467: Add loading states for income/expense amounts

The income and expense sections should show loading indicators while the amounts are being calculated to improve user experience.

-            {calendar.totalIncome !== 0 ? (
+            {data ? (
+              calendar.totalIncome !== 0 ? (
               <>
                 <SvgArrowThickUp width={16} height={16} style={{ flexShrink: 0 }} />
                 {amountToCurrency(calendar.totalIncome)}
               </>
-            ) : (
-              ''
-            )}
+              ) : ''
+            ) : (
+              <LoadingIndicator size="small" />
+            )}

Also applies to: 476-488

packages/desktop-client/src/components/reports/reports/Calendar.tsx (2)

121-122: Extract hardcoded fallback value into a constant.

The fallback value '0' for firstDayOfWeekIdx should be extracted into a named constant for better maintainability.

+const DEFAULT_FIRST_DAY_OF_WEEK = '0'; // Sunday
 const [_firstDayOfWeekIdx] = useSyncedPref('firstDayOfWeekIdx');
-const firstDayOfWeekIdx = _firstDayOfWeekIdx || '0';
+const firstDayOfWeekIdx = _firstDayOfWeekIdx || DEFAULT_FIRST_DAY_OF_WEEK;

708-834: Consider performance optimization.

The component could benefit from the following improvements:

  1. Memoize the component using React.memo to prevent unnecessary re-renders
  2. Extract the onPress handler to avoid recreation on each render
-function CalendarWithHeader({
+const CalendarWithHeader = React.memo(function CalendarWithHeader({
   calendar,
   totalIncome,
   totalExpense,
   onApplyFilter,
   firstDayOfWeekIdx,
 }: CalendarHeaderProps) {
+  const handleMonthFilter = useCallback(() => {
+    onApplyFilter({
+      conditions: [
+        {
+          field: 'date',
+          op: 'is',
+          value: format(calendar.start, 'yyyy-MM'),
+          options: {
+            month: true,
+          },
+        },
+      ],
+      conditionsOp: 'and',
+      id: [],
+    });
+  }, [onApplyFilter, calendar.start]);

   // ... rest of the component code ...
-          onPress={() => {
-            onApplyFilter({
-              conditions: [
-                {
-                  field: 'date',
-                  op: 'is',
-                  value: format(calendar.start, 'yyyy-MM'),
-                  options: {
-                    month: true,
-                  },
-                },
-              ],
-              conditionsOp: 'and',
-              id: [],
-            });
-          }}
+          onPress={handleMonthFilter}
   // ... rest of the component code ...
-}
+});
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between c887f22 and db47cb5.

📒 Files selected for processing (2)
  • packages/desktop-client/src/components/reports/reports/Calendar.tsx (1 hunks)
  • packages/desktop-client/src/components/reports/reports/CalendarCard.tsx (1 hunks)
🧰 Additional context used
📓 Learnings (1)
packages/desktop-client/src/components/reports/reports/Calendar.tsx (1)
Learnt from: lelemm
PR: actualbudget/actual#3828
File: packages/desktop-client/src/components/reports/reports/Calendar.tsx:575-631
Timestamp: 2024-11-12T18:18:07.283Z
Learning: In `Calendar.tsx`, transaction-related callbacks such as `onBatchDelete`, `onBatchDuplicate`, `onCreateRule`, and `onScheduleAction` are intentionally left as empty functions because these operations should not be usable on that page.
🔇 Additional comments (9)
packages/desktop-client/src/components/reports/reports/CalendarCard.tsx (4)

1-52: LGTM! Well-structured imports and type definitions.

The imports are logically organized, and the CalendarProps type is well-defined with proper typing for all properties.


335-367: Skip: Resize callback optimization

The resize callback implementation has already been addressed in past reviews.


504-516: Skip: Ref assignments refactoring

The ref assignments implementation has already been addressed in past reviews.


64-68: 🛠️ Refactor suggestion

Add validation for invalid date ranges

The calculateTimeRange call should validate that the end date is not before the start date to prevent potential issues with invalid date ranges.

packages/desktop-client/src/components/reports/reports/Calendar.tsx (5)

73-86: LGTM! Clean and well-structured component.

The Calendar component properly handles the loading state and follows React best practices.


179-209: LGTM! Good error handling in useEffect hooks.

The error handling for both async operations (filter generation and earliest transaction fetch) is well implemented.

Also applies to: 236-267


568-624: Empty callbacks are intentional.

As confirmed by the learning, transaction-related callbacks are intentionally left empty as these operations should not be usable on this page.


844-914: LGTM! Well-structured component with clean conditional rendering.

The component effectively handles layout and conditional rendering of income and expense information.


917-936: LGTM! Clear and focused utility function.

The function provides a clean mapping of field names with appropriate default handling.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Outside diff range and nitpick comments (4)
packages/desktop-client/src/components/reports/reports/CalendarCard.tsx (4)

45-52: Consider enhancing type safety for the meta prop.

The meta prop's type could be more strictly defined to prevent potential runtime errors.

 type CalendarProps = {
   widgetId: string;
   isEditing?: boolean;
-  meta?: CalendarWidget['meta'];
+  meta?: {
+    name?: string;
+    timeFrame?: string;
+    conditions?: unknown;
+    conditionsOp?: string;
+  };
   onMetaChange: (newMeta: CalendarWidget['meta']) => void;
   onRemove: () => void;
   firstDayOfWeekIdx?: SyncedPrefs['firstDayOfWeekIdx'];
 };

118-126: Simplify the useEffect logic for monthNameFormats.

The current implementation can be simplified by using the ternary operator.

 useEffect(() => {
-  if (data) {
-    setMonthNameFormats(
-      Array(data.calendarData.length).map(() => 'MMMM yyyy'),
-    );
-  } else {
-    setMonthNameFormats([]);
-  }
+  setMonthNameFormats(data ? Array(data.calendarData.length).fill('MMMM yyyy') : []);
 }, [data]);

159-170: Improve error handling in menu selection.

The current error handling for unrecognized menu items throws a generic Error. Consider using a custom error type and providing more context.

+class MenuSelectionError extends Error {
+  constructor(selection: string) {
+    super(`Unrecognized menu selection: ${selection}`);
+    this.name = 'MenuSelectionError';
+  }
+}

 onMenuSelect={item => {
   switch (item) {
     case 'rename':
       setNameMenuOpen(true);
       break;
     case 'remove':
       onRemove();
       break;
     default:
-      throw new Error(`Unrecognized selection: ${item}`);
+      throw new MenuSelectionError(item);
   }
 }}

335-367: Optimize resize callback performance.

The current implementation of debouncedResizeCallback performs DOM measurements on every resize event. Consider caching the measurements and only updating when necessary.

 const debouncedResizeCallback = useRef(
   debounce((contentRect: DOMRectReadOnly) => {
+    if (!monthNameContainerRef.current) return;
+
     const measurements = measureMonthFormats();
     const containerWidth = contentRect.width;

+    // Cache previous measurements to avoid unnecessary updates
+    const prevMeasurements = useRef(measurements);
+    if (
+      JSON.stringify(measurements) === JSON.stringify(prevMeasurements.current) &&
+      containerWidth === prevWidth.current
+    ) {
+      return;
+    }
+    prevMeasurements.current = measurements;
+    prevWidth.current = containerWidth;

     const suitableFormat = measurements.find(m => containerWidth > m.width);
     // ... rest of the logic
   }, 20),
 );
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between db47cb5 and 1062dc7.

📒 Files selected for processing (1)
  • packages/desktop-client/src/components/reports/reports/CalendarCard.tsx (1 hunks)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Outside diff range and nitpick comments (3)
packages/desktop-client/src/components/reports/reports/CalendarCard.tsx (3)

69-79: Consider memoizing the calendarSpreadsheet function.

The calendarSpreadsheet function is called within useMemo, but the function itself might be recreated on each render. Consider moving it outside the component or memoizing it with useCallback.

+const memoizedCalendarSpreadsheet = useCallback(
+  (start: Date, end: Date, conditions: any, conditionsOp: any, firstDayOfWeekIdx: any) =>
+    calendarSpreadsheet(start, end, conditions, conditionsOp, firstDayOfWeekIdx),
+  []
+);

 const params = useMemo(
   () =>
-    calendarSpreadsheet(
+    memoizedCalendarSpreadsheet(
       start,
       end,
       meta?.conditions,
       meta?.conditionsOp,
       firstDayOfWeekIdx,
     ),
   [start, end, meta?.conditions, meta?.conditionsOp, firstDayOfWeekIdx],
 );

98-112: Optimize total calculations with reduce callback memoization.

The reduce callbacks for calculating totals are recreated on each render. Consider memoizing them with useCallback.

+const calculateTotal = useCallback((key: 'totalIncome' | 'totalExpense') =>
+  (prev: number, cur: any) => prev + cur[key], []);

 const { totalIncome, totalExpense } = useMemo(() => {
   if (!data) {
     return { totalIncome: 0, totalExpense: 0 };
   }
   return {
-    totalIncome: data.calendarData.reduce(
-      (prev, cur) => prev + cur.totalIncome,
-      0,
-    ),
-    totalExpense: data.calendarData.reduce(
-      (prev, cur) => prev + cur.totalExpense,
-      0,
-    ),
+    totalIncome: data.calendarData.reduce(calculateTotal('totalIncome'), 0),
+    totalExpense: data.calendarData.reduce(calculateTotal('totalExpense'), 0),
   };
 }, [data]);

504-516: Consider using CSS custom properties for month format measurements.

The current implementation uses hidden elements for measurements. Consider using CSS custom properties (variables) to store and compare widths, which would be more performant and cleaner.

+const useMonthFormatWidth = (format: string, text: string) => {
+  useEffect(() => {
+    const style = document.createElement('style');
+    style.textContent = `
+      :root {
+        --month-format-${format}-width: ${text.length}ch;
+      }
+    `;
+    document.head.appendChild(style);
+    return () => style.remove();
+  }, [format, text]);
+};

-{monthFormats.map((item, idx) => (
-  <span
-    key={item.format}
-    ref={node => {
-      if (node) monthFormatSizeContainers.current[idx] = node;
-    }}
-    style={{ position: 'fixed', top: -9999, left: -9999 }}
-    data-format={item.format}
-  >
-    {item.text}
-    {item.text && ':'}
-  </span>
-))}
+{monthFormats.map(item => {
+  useMonthFormatWidth(item.format, item.text);
+  return null;
+})}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 1062dc7 and a395a80.

📒 Files selected for processing (1)
  • packages/desktop-client/src/components/reports/reports/CalendarCard.tsx (1 hunks)
🔇 Additional comments (2)
packages/desktop-client/src/components/reports/reports/CalendarCard.tsx (2)

335-367: Optimize resize callback with ResizeObserver's contentRect.

The current implementation manually measures DOM elements, which could be simplified by using ResizeObserver's contentRect values directly.

This issue was already identified in a previous review. The suggestion to use ResizeObserver's contentRect directly remains valid.


413-438: 🛠️ Refactor suggestion

Add aria-label to month name button for better accessibility.

The month name button lacks an aria-label, which could impact screen reader usability.

 <Button
   variant="bare"
+  aria-label={`View calendar for ${format(calendar.start, selectedMonthNameFormat)}`}
   style={{
     visibility: monthNameVisible ? 'visible' : 'hidden',
     overflow: 'visible',

Likely invalid or redundant comment.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Outside diff range and nitpick comments (12)
packages/desktop-client/src/components/reports/graphs/CalendarGraph.tsx (6)

24-45: Clean up commented-out filter functionality

The type definition includes commented-out filter-related properties that should either be implemented or removed to maintain clean code.

Consider either:

  1. Implementing the filter functionality, or
  2. Removing the commented code:
type CalendarGraphProps = {
  data: {
    date: Date;
    incomeValue: number;
    expenseValue: number;
    incomeSize: number;
    expenseSize: number;
  }[];
  start: Date;
  firstDayOfWeekIdx?: SyncedPrefs['firstDayOfWeekIdx'];
  onDayClick: (date: Date) => void;
-  // onFilter: (
-  //   conditionsOrSavedFilter:
-  //     | null
-  //     | {
-  //         conditions: RuleConditionEntity[];
-  //         conditionsOp: 'and' | 'or';
-  //         id: RuleConditionEntity[];
-  //       }
-  //     | RuleConditionEntity,
-  // ) => void;
};

54-62: Optimize week start day parsing

The week start day parsing logic performs multiple parseInt operations on the same value. This can be optimized for better performance and readability.

const startingDate = startOfWeek(new Date(), {
+ const parsedDay = firstDayOfWeekIdx !== undefined ? parseInt(firstDayOfWeekIdx) : null;
  weekStartsOn:
-   firstDayOfWeekIdx !== undefined &&
-   !Number.isNaN(parseInt(firstDayOfWeekIdx)) &&
-   parseInt(firstDayOfWeekIdx) >= 0 &&
-   parseInt(firstDayOfWeekIdx) <= 6
-     ? (parseInt(firstDayOfWeekIdx) as 0 | 1 | 2 | 3 | 4 | 5 | 6)
+   parsedDay !== null &&
+   !Number.isNaN(parsedDay) &&
+   parsedDay >= 0 &&
+   parsedDay <= 6
+     ? (parsedDay as 0 | 1 | 2 | 3 | 4 | 5 | 6)
      : 0,
});

104-113: Clean up grid layout configuration

There's a commented-out grid row configuration that should be either implemented or removed.

<View
  style={{
    display: 'grid',
    gridTemplateColumns: 'repeat(7, 1fr)',
    gridAutoRows: '1fr',
    gap: 2,
    width: '100%',
    height: '100%',
-   //gridTemplateRows: `repeat(${Math.trunc(data.length) <= data.length / 7 ? Math.trunc(data.length) : Math.trunc(data.length) + 1},1fr)`,
  }}
>

120-205: Extract tooltip content to a separate component

The tooltip content is complex and would benefit from being extracted into a separate component for better maintainability and reusability.

Consider creating a DayTooltipContent component:

type DayTooltipContentProps = {
  day: {
    date: Date;
    incomeValue: number;
    expenseValue: number;
    incomeSize: number;
    expenseSize: number;
  };
};

function DayTooltipContent({ day }: DayTooltipContentProps) {
  const { t } = useTranslation();
  return (
    <View>
      <View style={{ marginBottom: 10 }}>
        <strong>
          {t('Day:') + ' '}
          {format(day.date, 'dd')}
        </strong>
      </View>
      {/* ... rest of the tooltip content ... */}
    </View>
  );
}

246-252: Consider lifting font size state

The currentFontSize state and its update effect could be lifted to the parent component to reduce unnecessary re-renders.

- const [currentFontSize, setCurrentFontSize] = useState(fontSize);
- 
- useEffect(() => {
-   setCurrentFontSize(fontSize);
- }, [fontSize]);

  return (
    <Button
      // ...
      style={{
        // ...
-       fontSize: `${currentFontSize}px`,
+       fontSize: `${fontSize}px`,
      }}
    >

293-319: Extract common bar styles to reduce duplication

The positive and negative bars share many common styles. Consider extracting these to reduce code duplication.

const commonBarStyles = {
  position: 'absolute',
  bottom: 0,
  opacity: 0.9,
  width: '50%',
  transition: 'height 0.5s ease-out',
} as const;

// Usage:
<View
  className="bar positive-bar"
  style={{
    ...commonBarStyles,
    left: 0,
    height: `${Math.ceil(day.incomeSize)}%`,
    backgroundColor: chartTheme.colors.blue,
  }}
/>

<View
  className="bar"
  style={{
    ...commonBarStyles,
    right: 0,
    height: `${Math.ceil(day.expenseSize)}%`,
    backgroundColor: chartTheme.colors.red,
  }}
/>
packages/desktop-client/src/components/reports/reports/CalendarCard.tsx (3)

161-171: Improve error handling in menu selection

The current error handling throws a generic error. Consider providing more context and handling the error gracefully.

-          default:
-            throw new Error(`Unrecognized selection: ${item}`);
+          default: {
+            console.error(`Unrecognized menu selection: ${item}`);
+            // Optionally show a user-friendly error message
+            return;
+          }

462-463: Enhance accessibility for financial indicators

The aria-labels for income and expense indicators could be more descriptive by including the actual amounts.

-            aria-label="Income"
+            aria-label={`Income: ${calendar.totalIncome !== 0 ? amountToCurrency(calendar.totalIncome) : 'None'}`}
-            aria-label="Expenses"
+            aria-label={`Expenses: ${calendar.totalExpense !== 0 ? amountToCurrency(calendar.totalExpense) : 'None'}`}

Also applies to: 485-486


508-514: Simplify navigation logic

The navigation URL construction could be simplified by extracting the logic into a helper function.

+  const getNavigationUrl = useCallback(
+    (date: Date) => {
+      const baseUrl = isDashboardsFeatureEnabled
+        ? `/reports/calendar/${widgetId}`
+        : '/reports/calendar';
+      if (!isDashboardsFeatureEnabled) return baseUrl;
+      return `${baseUrl}?day=${format(date, 'yyyy-MM-dd')}`;
+    },
+    [isDashboardsFeatureEnabled, widgetId]
+  );

-          navigate(
-            isDashboardsFeatureEnabled
-              ? `/reports/calendar/${widgetId}?day=${format(date, 'yyyy-MM-dd')}`
-              : '/reports/calendar',
-          );
+          navigate(getNavigationUrl(date));
packages/desktop-client/src/components/reports/reports/Calendar.tsx (3)

333-347: Optimize memoization of total calculations

The current memoization could be more efficient by extracting the reducer functions.

+const calculateTotal = (data: CalendarDataType[], key: 'totalIncome' | 'totalExpense') =>
+  data.reduce((prev, cur) => prev + cur[key], 0);
+
 const { totalIncome, totalExpense } = useMemo(() => {
   if (!data || !data.calendarData) {
     return { totalIncome: 0, totalExpense: 0 };
   }
   return {
-    totalIncome: data.calendarData.reduce(
-      (prev, cur) => prev + cur.totalIncome,
-      0,
-    ),
-    totalExpense: data.calendarData.reduce(
-      (prev, cur) => prev + cur.totalExpense,
-      0,
-    ),
+    totalIncome: calculateTotal(data.calendarData, 'totalIncome'),
+    totalExpense: calculateTotal(data.calendarData, 'totalExpense'),
   };
 }, [data]);

709-839: Optimize CalendarWithHeader component rendering

The component could benefit from memoization to prevent unnecessary re-renders of the calendar visualization.

-function CalendarWithHeader({
+const CalendarWithHeader = React.memo(function CalendarWithHeader({
   calendar,
   totalIncome,
   totalExpense,
   onApplyFilter,
   firstDayOfWeekIdx,
 }: CalendarHeaderProps) {
   // ... component implementation ...
-}
+});

934-953: Improve type safety in getField utility

The function could benefit from stricter typing and exhaustive checks.

+type ValidField = 'account' | 'payee' | 'category' | 'payment' | 'deposit' | 'date';
+
-function getField(field?: string) {
+function getField(field?: ValidField) {
   if (!field) {
     return 'date';
   }

   switch (field) {
     case 'account':
       return 'account.name';
     case 'payee':
       return 'payee.name';
     case 'category':
       return 'category.name';
     case 'payment':
       return 'amount';
     case 'deposit':
       return 'amount';
+    case 'date':
+      return 'date';
     default: {
-      return field;
+      const exhaustiveCheck: never = field;
+      return exhaustiveCheck;
     }
   }
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between a395a80 and 0c928cf.

📒 Files selected for processing (3)
  • packages/desktop-client/src/components/reports/graphs/CalendarGraph.tsx (1 hunks)
  • packages/desktop-client/src/components/reports/reports/Calendar.tsx (1 hunks)
  • packages/desktop-client/src/components/reports/reports/CalendarCard.tsx (1 hunks)
🧰 Additional context used
📓 Learnings (1)
packages/desktop-client/src/components/reports/reports/Calendar.tsx (1)
Learnt from: lelemm
PR: actualbudget/actual#3828
File: packages/desktop-client/src/components/reports/reports/Calendar.tsx:575-631
Timestamp: 2024-11-12T18:18:07.283Z
Learning: In `Calendar.tsx`, transaction-related callbacks such as `onBatchDelete`, `onBatchDuplicate`, `onCreateRule`, and `onScheduleAction` are intentionally left as empty functions because these operations should not be usable on that page.
🔇 Additional comments (1)
packages/desktop-client/src/components/reports/reports/CalendarCard.tsx (1)

1-533: Overall implementation looks good!

The calendar card implementation is well-structured, with good separation of concerns between the main card and inner components. The code handles responsive layout, accessibility, and state management effectively.

lelemm and others added 2 commits November 13, 2024 09:51
…rCard.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Outside diff range and nitpick comments (2)
packages/desktop-client/src/components/reports/reports/CalendarCard.tsx (2)

161-171: Consider using TypeScript discriminated unions for menu selection.

The current implementation throws an error for unrecognized menu items. Consider using TypeScript's discriminated unions for type-safe menu item handling.

+type MenuItem = 
+  | { name: 'rename'; text: string }
+  | { name: 'remove'; text: string };

-onMenuSelect={item => {
+onMenuSelect={(item: MenuItem['name']) => {
   switch (item) {
     case 'rename':
       setNameMenuOpen(true);
       break;
     case 'remove':
       onRemove();
       break;
-    default:
-      throw new Error(`Unrecognized selection: ${item}`);
   }
 }}

394-400: Consider extracting date formats to constants.

The month formats array could be moved to a constants file or defined as a static constant to improve maintainability and reusability.

+const MONTH_FORMAT_OPTIONS = [
+  { format: 'MMMM yyyy', text: 'full' },
+  { format: 'MMM yyyy', text: 'abbreviated' },
+  { format: 'MMM yy', text: 'short' },
+  { format: 'MMM', text: 'month' },
+  { format: '', text: 'none' },
+] as const;

-const monthFormats = [
-  { format: 'MMMM yyyy', text: format(calendar.start, 'MMMM yyyy') },
-  { format: 'MMM yyyy', text: format(calendar.start, 'MMM yyyy') },
-  { format: 'MMM yy', text: format(calendar.start, 'MMM yy') },
-  { format: 'MMM', text: format(calendar.start, 'MMM') },
-  { format: '', text: '' },
-];
+const monthFormats = MONTH_FORMAT_OPTIONS.map(({ format }) => ({
+  format,
+  text: format ? format(calendar.start, format) : '',
+}));
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 0c928cf and 92315bb.

📒 Files selected for processing (1)
  • packages/desktop-client/src/components/reports/reports/CalendarCard.tsx (1 hunks)
🔇 Additional comments (1)
packages/desktop-client/src/components/reports/reports/CalendarCard.tsx (1)

1-53: LGTM! Clean imports and well-structured type definitions.

The imports are properly organized and the type definitions are clear and well-documented.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant