Skip to content

feat(DEV-15216): Expenses Management feature#820

Open
dinis-monite wants to merge 61 commits intomainfrom
feat/DEV-15216_expense-management-feature
Open

feat(DEV-15216): Expenses Management feature#820
dinis-monite wants to merge 61 commits intomainfrom
feat/DEV-15216_expense-management-feature

Conversation

@dinis-monite
Copy link
Collaborator

@dinis-monite dinis-monite commented Sep 4, 2025

Feature branch to hold the Expenses Management developments until all are ready to be merged into main.

While developing, the goal of this PR is to generate a preview link with the implemented Expenses Management developments.

DO NOT MERGE this PR until all features are ready.

Summary by CodeRabbit

  • New Features

    • Introduced an Expenses page with user and manager transaction tables: search, date/employee filters, sorting, pagination, receipt links, and localized UI.
    • Added navigation entries to access Expenses; included a Financing item in the playground sidebar.
    • Manual payment recording now includes a two-step review and confirmation flow.
    • New data table UI with column visibility controls, sorting, and pagination.
    • Component settings now support configurable Expenses page size options.
    • User display cell now supports an “inherit” typography variant.
  • Chores

    • Added English translations for Expenses and data table controls.

…with pagination, sorting, and filtering functionality with API
@dinis-monite dinis-monite added pullpreview Generate a live preview for this pull request do-not-merge labels Sep 4, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 4, 2025

Note

Reviews paused

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Walkthrough

Adds an Expenses feature across SDK and examples: new Expenses pages/components, data-table UI built on TanStack, hooks for transactions and receipts, component settings extension, i18n entries, and navigation wiring in playground and Next.js example. Includes minor refactors and a new dependency addition.

Changes

Cohort / File(s) Summary
Changeset
./.changeset/dirty-items-clap.md
Adds patch changeset describing new “Expenses” page and affected packages.
Expenses components (SDK React)
packages/sdk-react/src/components/expenses/Expenses.tsx, packages/sdk-react/src/components/expenses/ExpensesTable/*, packages/sdk-react/src/components/expenses/index.ts, packages/sdk-react/src/components/index.ts
Introduces Expenses page and tables (UserTransactionsTable, ManagerTransactionsTable), filter consts/types, barrel exports.
Data hooks and queries
packages/sdk-react/src/components/expenses/hooks/useTransactions.ts, packages/sdk-react/src/core/queries/useReceiptsByTransactionIds.ts, packages/sdk-react/src/core/hooks/useDataTableState.ts, packages/sdk-react/src/core/hooks/index.ts
Adds hooks for fetching transactions, receipts, and a reusable data table state manager; re-exports hook.
UI data table and dropdown
packages/sdk-react/src/ui/components/data-table.tsx, packages/sdk-react/src/ui/components/dropdown-menu.tsx
Adds TanStack-based DataTable with sorting, pagination, column visibility; adjusts dropdown menu styles.
Component settings and exports
packages/sdk-react/src/core/componentSettings/index.ts, packages/sdk-react/src/index.ts
Extends ComponentSettings with expenses.pageSizeOptions and updates defaults; reformats type export.
i18n updates (SDK React)
packages/sdk-react/src/core/i18n/locales/en/messages.po
Adds strings for data table and expenses UI; updates source references.
Playground integration
packages/sdk-playground/src/App.tsx, packages/sdk-playground/src/components/app-sidebar.tsx, packages/sdk-playground/src/pages/ExpensesPage.tsx, packages/sdk-playground/src/pages/index.ts
Wires new /expenses route/page, adds sidebar items (“Expenses”, “Financing”), and exports page from barrel.
Next.js + Clerk example integration
examples/with-nextjs-and-clerk-auth/src/app/(monite)/expenses/page.tsx, examples/with-nextjs-and-clerk-auth/src/components/MoniteComponents.tsx, examples/with-nextjs-and-clerk-auth/src/components/NavigationMenu/NavigationList.tsx, examples/with-nextjs-and-clerk-auth/src/locales/en/messages.po
Adds Expenses page and component wrapper; inserts nav item; adds “Expenses” and “Get Help” translations and updates locations.
Dependency addition
packages/sdk-react/package.json
Adds dependency: @tanstack/react-table ~8.21.3.
UserDisplayCell prop update
packages/sdk-react/src/components/UserDisplayCell/UserDisplayCell.tsx
Extends typographyVariant union to include 'inherit'; import reorder.
Formatting and minor UI adjustments
packages/sdk-react/src/components/payables/PayableDetails/usePayableDetails.tsx, packages/sdk-react/src/components/payables/hooks/usePayableCallbacks.ts, packages/sdk-react/src/components/receivables/InvoiceDetails/CreateReceivable/CreateReceivables.tsx, packages/sdk-react/src/components/receivables/components/ReminderSectionContent.tsx, packages/sdk-drop-in/src/lib/MoniteEvents.ts
Formatting-only changes and alert layout tweak; no API changes.
Manual payment modal flow
packages/sdk-react/src/components/receivables/InvoiceDetails/ExistingInvoiceDetails/components/TabPanels/PaymentTabPanel/RecordManualPaymentModal.tsx
Converts to two-step flow with local state; adds exported type PaymentRecordDetails; updates props to accept render-prop child.

Possibly related PRs

Suggested labels

pullpreview

✨ Finishing touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/DEV-15216_expense-management-feature

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@dinis-monite
Copy link
Collaborator Author

@coderabbitai ignore

@changeset-bot
Copy link

changeset-bot bot commented Sep 4, 2025

🦋 Changeset detected

Latest commit: ebe5741

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions
Copy link
Contributor

github-actions bot commented Sep 4, 2025

🚀 Preview URLs are now available! 🚀

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (9)
packages/sdk-drop-in/src/lib/MoniteEvents.ts (2)

57-66: Fix incorrect event name in example (PAYABLE_PAY vs actual emitted string).

Events are dispatched as ${MONITE_EVENT_PREFIX}:${type}, where type is 'payable.pay'. The example uses 'monite.event:PAYABLE_PAY', which won’t fire. Use the lowercase string or the helper.

Apply minimal fix:

- * window.addEventListener('monite.event:PAYABLE_PAY', (event) => {
+ * window.addEventListener('monite.event:payable.pay', (event) => {

Alternatively (preferred DX), show the typed helper:

- * window.addEventListener('monite.event:PAYABLE_PAY', (event) => {
- *   const { id, actions } = event.detail.payload;
+ * addMoniteEventListener(MoniteEventTypes.PAYABLE_PAY, (event) => {
+ *   const { id, actions } = event.detail.payload as MoniteEvent<PayablePayEventPayload>;

101-108: JSDoc mentions a target parameter that the function doesn’t accept.

emitMoniteEvent has only (type, payload). The doc block lists @param target, which is misleading.

 /**
  * Emits a Monite event on the specified target
  *
  * @param type The type of event to emit
  * @param payload The payload to include with the event
- * @param target Optional target element to emit the event from (defaults to auto-detected target)
  * @returns Whether the event was successfully emitted
  */

If you actually intend to allow custom targets, consider adding an optional third param and defaulting to getMoniteAppEventTarget().

packages/sdk-react/src/components/receivables/InvoiceDetails/CreateReceivable/CreateReceivables.tsx (1)

465-513: Fix: saving currency without change sets actualCurrency to undefined

If the user opens the currency modal and clicks Save without changing the selection, tempCurrency remains undefined, yet handleCurrencySubmit treats it as a change and sets actualCurrency to undefined. This can break downstream validation and payloads. Add an early guard.

-  const handleCurrencySubmit = () => {
-    if (tempCurrency !== actualCurrency) {
+  const handleCurrencySubmit = () => {
+    if (!tempCurrency) {
+      setRemoveItemsWarning(false);
+      setTempCurrency(actualCurrency);
+      handleCloseCurrencyModal();
+      return;
+    }
+    if (tempCurrency !== actualCurrency) {

Optional hardening (outside selected range): when opening the modal, preset tempCurrency:

onClick={() => {
  setTempCurrency(actualCurrency);
  setIsCurrencyModalOpen(true);
}}
packages/sdk-react/src/components/receivables/InvoiceDetails/ExistingInvoiceDetails/components/TabPanels/PaymentTabPanel/RecordManualPaymentModal.tsx (6)

22-25: Fix: React.ReactNode type used without importing React types

Avoid relying on the global React namespace; import the ReactNode type and reference it directly to prevent TS errors in strict configs.

-import { useState } from 'react';
+import { useState, type ReactNode } from 'react';
 
 type Props = {
   invoice: components['schemas']['InvoiceResponsePayload'];
-  children: ({ openModal }: { openModal: () => void }) => React.ReactNode;
+  children: ({ openModal }: { openModal: () => void }) => ReactNode;
 };

49-51: Ensure modal close resets the wizard state

Closing the dialog currently only hides it; it does not reset confirmSubmission or formValues. Re-opening can show stale confirmation state.

-const openModal = () => setModalOpen(true);
-const closeModal = () => setModalOpen(false);
+const openModal = () => {
+  setModalOpen(true);
+};
+const closeModal = () => {
+  setModalOpen(false);
+  resetForm();
+};

Also applies to: 62-65


57-60: Round amount to minor units to avoid floating-point errors

Multiplying by 100 without rounding can produce values like 1098.999999999 due to FP precision.

- setFormValues({ ...data, amount: (data?.amount ?? 0) * 100 });
+ setFormValues({
+   ...data,
+   amount: Math.round((data?.amount ?? 0) * 100),
+ });

67-76: Harden date/time construction; current code can produce Invalid Date and throw

new Date(formValues.payment_date ?? '') yields an invalid date when payment_date is null, leading to toISOString throwing at runtime.

-const createManualPaymentRecord = () => {
-  const dateTimeWithReplacedTime = new Date(
-    formValues.payment_date ?? ''
-  ).setHours(
-    formValues.payment_time?.getHours() ?? 0,
-    formValues.payment_time?.getMinutes() ?? 0
-  );
-
-  const paid_at = new Date(dateTimeWithReplacedTime);
+const createManualPaymentRecord = () => {
+  const date =
+    formValues.payment_date ? new Date(formValues.payment_date) : new Date();
+
+  // If parsing failed for any reason, fall back to now
+  if (Number.isNaN(date.getTime())) {
+    date.setTime(Date.now());
+  }
+
+  const hours = formValues.payment_time?.getHours() ?? 0;
+  const minutes = formValues.payment_time?.getMinutes() ?? 0;
+  date.setHours(hours, minutes, 0, 0);
+
+  const paid_at = date;

77-89: Use valid PaymentIntent ID and omit undefined entity_user_id

  • entity_user_id is optional per OpenAPI (not in the required list) and will be dropped by JSON.stringify if undefined.
  • payment_intent_id is required and must reference an actual PaymentIntent—using invoice.id (an Invoice ID) will be rejected. Generate or fetch a valid PaymentIntent ID for manual payments before calling postPaymentRecords.

107-120: Add missing statuses to the disallowed list in RecordManualPaymentModal.tsx (lines 116–120). The gating array currently omits ‘issuing’, ‘issued’, ‘failed’, ‘partially_paid’, and ‘overdue’ from ReceivablesStatusEnum—include these or confirm if manual payments should remain allowed for any.

🧹 Nitpick comments (48)
packages/sdk-drop-in/src/lib/MoniteEvents.ts (2)

70-75: Strongly type actions as PayActionHandlers for better DX.

JSDoc references PayActionHandlers, but the field is unknown. Import the type from @monite/sdk-react and wire it here to enable IntelliSense and type safety.

@@
-export interface PayablePayEventPayload extends BaseEventPayload {
+export interface PayablePayEventPayload extends BaseEventPayload {
   /**
    * Action handlers for custom payment flows.
    * @see {@link PayActionHandlers} for complete API documentation and usage examples.
    */
-  actions?: unknown;
+  actions?: PayActionHandlers;
 }

Add the type-only import near the top:

 import type {
   ComponentSettings,
   MoniteReceivablesTableProps,
 } from '@monite/sdk-react';
+import type { PayActionHandlers } from '@monite/sdk-react';

131-141: Doc/behavior mismatch: handler invocation order vs description.

Comment says “calls the original handler and emits an event,” but the implementation emits first, then calls the original handler. Clarify which order is intended; the difference can matter for side-effects and listeners.

Option A — update docs to match current behavior:

- * Creates a wrapped handler that calls the original handler and emits an event.
+ * Creates a wrapped handler that emits an event, then calls the original handler.

Option B — change implementation to match current docs:

   return (id: string, data?: D, actions?: A) => {
     const payload = createPayload(id, data, actions);
-    emitMoniteEvent(eventType, payload);
-    originalHandler?.(id, data, actions);
+    originalHandler?.(id, data, actions);
+    emitMoniteEvent(eventType, payload);
   };

Confirm which is desired and keep it consistent across usages.

packages/sdk-react/src/components/receivables/InvoiceDetails/CreateReceivable/CreateReceivables.tsx (3)

304-314: Use the derived remindersSelected flag to de-duplicate conditionals

Keeps conditions concise and less error-prone.

-    if (
-      !customerHasRemindersEnabled &&
-      customerHasDefaultEmail &&
-      (values.payment_reminder_id || values.overdue_reminder_id)
-    ) {
+    if (
+      !customerHasRemindersEnabled &&
+      customerHasDefaultEmail &&
+      remindersSelected
+    ) {

318-329: Same here: rely on remindersSelected

-    if (
-      !customerHasDefaultEmail &&
-      customerHasRemindersEnabled &&
-      (values.payment_reminder_id || values.overdue_reminder_id)
-    ) {
+    if (
+      !customerHasDefaultEmail &&
+      customerHasRemindersEnabled &&
+      remindersSelected
+    ) {

332-343: And here: rely on remindersSelected

-    if (
-      !customerHasRemindersEnabled &&
-      !customerHasDefaultEmail &&
-      (values.payment_reminder_id || values.overdue_reminder_id)
-    ) {
+    if (
+      !customerHasRemindersEnabled &&
+      !customerHasDefaultEmail &&
+      remindersSelected
+    ) {
packages/sdk-react/src/components/receivables/components/ReminderSectionContent.tsx (3)

56-63: Make the email-type check robust and ensure booleans are always booleans

Use the discriminant counterpart.type and optional chaining to avoid potential undefined property reads and to keep hasValidReminderEmail strictly boolean. This also simplifies shouldShowAlert's logic.

-  const hasValidReminderEmail =
-    counterpart && 'individual' in counterpart
-      ? Boolean(counterpart.individual.email)
-      : hasCounterpartDefaultContactEmail;
+  const hasValidReminderEmail =
+    counterpart?.type === 'individual'
+      ? Boolean(counterpart.individual?.email)
+      : Boolean(hasCounterpartDefaultContactEmail);

71-75: Tighten alert copy to avoid contradictory instructions

The current messages say “enable … or turn them off” even when reminders are already disabled. Clarify the alternative action as “deselect reminders for this invoice.”

-                )`Payment reminders are disabled for this customer. Please enable them in the customer details or turn them off.`}
+                )`Payment reminders are disabled for this customer. Enable them in the customer details, or deselect reminders for this invoice.`}
-                )`No email address is added for the selected customer. Please add it to the customer details or turn off the reminders.`}
+                )`No email address is added for the selected customer. Add it in the customer details, or deselect reminders for this invoice.`}
-                )`Reminders are disabled for this customer, and no email address has been added for it. Please update the details or turn off reminders.`}
+                )`Reminders are disabled for this customer, and no email address is set. Update the customer details, or deselect reminders for this invoice.`}

Also applies to: 78-81, 86-89


110-122: Fix invalid Tailwind utility: use justify-between instead of space-between

space-between is not a Tailwind utility; the correct class is justify-between.

-      <div className="mtw:flex mtw:gap-6 mtw:space-between mtw:w-full">
+      <div className="mtw:flex mtw:gap-6 mtw:justify-between mtw:w-full">
packages/sdk-react/src/components/receivables/InvoiceDetails/ExistingInvoiceDetails/components/TabPanels/PaymentTabPanel/RecordManualPaymentModal.tsx (3)

125-133: A11y: aria-labelledby without a matching element

aria-labelledby="dialog-title" has no corresponding element. Add DialogTitle or remove the attribute.

 import {
   Box,
   Button,
   Dialog,
   DialogActions,
   DialogContent,
+  DialogTitle,
 } from '@mui/material';
@@
   <Dialog
     open={modalOpen}
     onClose={closeModal}
     container={root}
-    aria-labelledby="dialog-title"
+    aria-labelledby="dialog-title"
     fullWidth
     maxWidth="sm"
   >
+    <DialogTitle id="dialog-title">
+      {t(i18n)`Record manual payment`}
+    </DialogTitle>

141-153: Style system: MUI sx used; add migration TODO per guidelines

Per repo guidelines, new UI should use shadcn/ui + Tailwind (mtw: classes). Since this component has significant changes, add a TODO to migrate.

-            <DialogActions
-              sx={{ px: 4, pb: 4, justifyContent: 'space-between' }}
-            >
+            {/* TODO(DEV-15216): Migrate Dialog/Button/Box from MUI to shadcn/ui and Tailwind (mtw:*) per guidelines. */}
+            <DialogActions sx={{ px: 4, pb: 4, justifyContent: 'space-between' }}>

164-169: Minor: normalize minor-unit to major-unit conversion

Consider rounding to 2 decimals when deriving initial amount to avoid values like 10.019999 from prior FP math.

-amount: formValues.amount ? formValues.amount / 100 : null,
+amount: formValues.amount != null ? Math.round(formValues.amount) / 100 : null,
packages/sdk-react/src/components/payables/hooks/usePayableCallbacks.ts (3)

40-49: Guard against possibly undefined component settings

If useComponentSettings() can ever return payablesCallbacks as undefined, these accesses will throw before optional chaining on the method. Prefer guarding the object, not just the method.

Apply if needed:

-      payablesCallbacks.onSaved?.(id);
+      payablesCallbacks?.onSaved?.(id);
...
-  const handleSaved =
-    onSaved || payablesCallbacks.onSaved ? savedCallback : undefined;
+  const handleSaved =
+    onSaved || payablesCallbacks?.onSaved ? savedCallback : undefined;

If the type guarantees payablesCallbacks is always defined, ignore.


112-119: Remove explicit undefined parameter in onPay

Passing an explicit undefined can be noisy and brittle if the signature changes. Prefer omitting it if the second arg is optional.

-      onPay?.(id, undefined, actions);
-      payablesCallbacks.onPay?.(id, undefined, actions);
+      onPay?.(id, actions);
+      payablesCallbacks.onPay?.(id, actions);

If the second parameter is required by type, keep as-is.


40-46: DRY the callback composition

The same “call prop callback then settings callback” pattern is repeated. A tiny helper reduces repetition and dependency array churn.

 export const usePayableCallbacks = (props: PayableCallbackProps) => {
   const { payablesCallbacks } = useComponentSettings();
+  const combine = <T extends any[]>(
+    propCb?: (...args: T) => void,
+    settingsCb?: (...args: T) => void
+  ) => {
+    if (!propCb && !settingsCb) return undefined;
+    return (...args: T) => {
+      propCb?.(...args);
+      settingsCb?.(...args);
+    };
+  };

   const {
     onSaved,
     onCanceled,
     onSubmitted,
     onRejected,
     onApproved,
     onReopened,
     onDeleted,
     onPay,
   } = props;

-  const savedCallback = useCallback(
-    (id: string) => {
-      onSaved?.(id);
-      payablesCallbacks.onSaved?.(id);
-    },
-    [onSaved, payablesCallbacks]
-  );
-  const handleSaved =
-    onSaved || payablesCallbacks.onSaved ? savedCallback : undefined;
+  const handleSaved = useCallback(
+    combine(onSaved, payablesCallbacks?.onSaved),
+    [onSaved, payablesCallbacks]
+  );

Apply similarly to canceled/submitted/rejected/approved/reopened/deleted/pay.

Also applies to: 50-56, 60-66, 72-78, 82-88, 92-98, 102-108, 112-118

.changeset/dirty-items-clap.md (1)

6-6: Polish wording for the changeset entry

Minor grammar tweak.

-Add new Expense Management page "Expenses" with listing of Transactions
+Add a new Expense Management page, "Expenses", with a list of transactions
packages/sdk-react/src/components/expenses/hooks/useTransactions.ts (1)

5-20: Solid hook; consider stabilizing the query object

Looks good. To avoid accidental refetches when callers pass a freshly created object each render, consider expecting a memoized/sorted query from callers or memoizing inside.

Example minimal change:

-export const useGetTransactions = (
-  query: Services['transactions']['getTransactions']['types']['parameters']['query'],
+export const useGetTransactions = (
+  query: Services['transactions']['getTransactions']['types']['parameters']['query'],
   enabled: boolean = true
 ) => {
   const { api } = useMoniteContext();
 
   const { data, isLoading, error, refetch } =
     api.transactions.getTransactions.useQuery(
       {
-        query,
+        // If caller passes a new object each render, this still stabilizes the key a bit.
+        query: { ...query },
       },
       {
         enabled: enabled,
         placeholderData: keepPreviousData,
       }
     );
packages/sdk-react/src/core/queries/useReceiptsByTransactionIds.ts (3)

16-19: Stabilize transaction IDs for consistent query keys

The memo just returns the input array; if callers pass a new reference or different order, cache keys may churn. Sort and dedupe to keep query keys stable.

-  const memoizedTransactionIds = useMemo(
-    () => transactionIds,
-    [transactionIds]
-  );
+  const memoizedTransactionIds = useMemo(() => {
+    // dedupe + sort for stable keys
+    return Array.from(new Set(transactionIds)).sort();
+  }, [transactionIds]);

50-57: Auto-fetching all pages can be heavy

Auto-draining pagination may spike network usage on large result sets. Consider exposing hasNextPage and letting callers decide, or add a soft cap.

-  useEffect(() => {
-    if (hasNextPage && !isFetchingNextPage) {
-      fetchNextPage().catch((error) => {
-        console.error('Error fetching next page of receipts:', error);
-      });
-    }
-  }, [hasNextPage, isFetchingNextPage, fetchNextPage]);
+  // Consider removing auto-drain; let caller invoke fetchNextPage when needed.

60-74: Multiple receipts per transaction? Clarify selection

The reduce keeps only one receipt per transaction, last-wins. If multiple receipts can exist, pick deterministically (e.g., latest by created_at) or return an array.

Possible tweak (latest by created_at if available):

-      (acc, receipt) => {
-        if (receipt.transaction_id) {
-          acc[receipt.transaction_id] = receipt;
-        }
+      (acc, receipt) => {
+        if (!receipt.transaction_id) return acc;
+        const prev = acc[receipt.transaction_id];
+        if (!prev) {
+          acc[receipt.transaction_id] = receipt;
+        } else if (
+          // @ts-expect-error: adjust field name if different
+          new Date(receipt.created_at).getTime() > new Date(prev.created_at).getTime()
+        ) {
+          acc[receipt.transaction_id] = receipt;
+        }
         return acc;
       },
packages/sdk-react/src/ui/components/dropdown-menu.tsx (1)

42-44: Potential non-standard Tailwind class: z-1300

Tailwind doesn’t ship z-1300 by default. If not extended in tailwind.config, it won’t compile. Prefer an arbitrary value.

Apply:

- ... mtw:z-1300 ...
+ ... mtw:z-[1300] ...

Please update in both DropdownMenuContent and DropdownMenuSubContent.

Also applies to: 230-232

packages/sdk-react/src/core/i18n/locales/en/messages.po (8)

1031-1034: Sorting labels OK; consider a11y-specific phrasing (optional)

"Ascending/Descending" are fine; if these are used as aria-labels for buttons, "Sort ascending/descending" can be clearer to screen readers.

Also applies to: 3017-3020


7705-7707: "Results per page" — reuse consistently

This key exists for the data table; ensure pagination components reuse a single key to avoid future drift.


8302-8305: Column controls — consistent casing

Text is fine. If the design system prefers Title Case in controls (e.g., “Payment Details”), consider “Sort Column” / “Visible Columns” for consistency.

Also applies to: 9890-9892


6260-6262: Remove trailing period for empty state consistency (optional)

Most empty states don’t end with a period. Consider:

- msgstr "No results."
+ msgstr "No results"

6366-6368: Status "Not matched" — consider "Unmatched" (optional)

Shorter and aligns better with other one-word statuses.


7438-7444: Receipt strings — consider adding msgctxt to disambiguate (optional)

Adding a context (e.g., msgctxt "ExpensesTable") avoids collisions with other “Receipt” usages.

Also applies to: 9878-9880


10119-10121: Unify apostrophe style

Two variants exist: "don't" and "don’t". Pick one (project typically uses curly ones) and keep it consistent.

- msgstr "You don't have any expenses yet"
+ msgstr "You don’t have any expenses yet"

Also applies to: 10123-10124


533-536: Grammar nit: add article

Consider improving the English msgstr:

- msgstr "Add your entity's tax registration number to issue invoice"
+ msgstr "Add your entity’s tax registration number to issue an invoice"
packages/sdk-react/src/core/hooks/useDataTableState.ts (3)

265-277: Keep pageCount monotonic to avoid UI jitter when navigating back.

When navigating back, apiResponse still reflects the last page’s response and can temporarily shrink pageCount. Guard with pageTokens.length.

   const pageCount = useMemo(() => {
     if (!apiResponse) return -1; // Unknown page count while loading

     // If there's a next page token, we know there's at least one more page
     if (apiResponse.next_pagination_token) {
       return Math.max(pagination.pageIndex + 2, pageTokens.length + 1);
     }

-    // If there's no next page token, current page is the last page
-    return pagination.pageIndex + 1;
+    // If there's no next token, treat current page as last but never shrink below discovered pages
+    return Math.max(pageTokens.length, pagination.pageIndex + 1);
   }, [apiResponse, pagination.pageIndex, pageTokens.length]);

25-31: Tighten API typing and remove inline response shape.

Per guidelines, avoid ad-hoc API shapes. Use a dedicated “page info” type and prefer unknown over any for filters.

-interface DataTableApiResponse {
-  /** Current API response data */
-  apiResponse?: {
-    next_pagination_token?: string | null;
-    prev_pagination_token?: string | null;
-  };
-}
+// Consider importing this from '@/types/api' if an equivalent exists.
+type CursorPageInfo = {
+  next_pagination_token?: string | null;
+  prev_pagination_token?: string | null;
+};
-  // Update function for API response data
-  updateApiResponse: (
-    apiResponse?: DataTableApiResponse['apiResponse']
-  ) => void;
+  // Update function for API response data
+  updateApiResponse: (apiResponse?: CursorPageInfo) => void;
-  const [apiResponse, setApiResponse] =
-    useState<DataTableApiResponse['apiResponse']>();
+  const [apiResponse, setApiResponse] = useState<CursorPageInfo | undefined>();
-  const updateApiResponse = useCallback(
-    (newApiResponse?: DataTableApiResponse['apiResponse']) => {
+  const updateApiResponse = useCallback((newApiResponse?: CursorPageInfo) => {
       setApiResponse(newApiResponse);
-    },
-    []
-  );
+    }, []);

Also applies to: 66-69, 128-131, 281-285


10-11: Use unknown instead of any for safer filter typing.

This preserves type-safety while still allowing consumers to specify concrete filter types.

-  TFilters extends Record<string, any> = Record<string, any>,
+  TFilters extends Record<string, unknown> = Record<string, unknown>,

Apply in DataTableStateOptions, DataTableStateReturn, and function generics.

Also:

-  const [filtersState, setFiltersState] = useState<TFilters>(
-    filters?.initialValue ?? ({} as TFilters)
-  );
+  const [filtersState, setFiltersState] = useState<TFilters>(
+    (filters?.initialValue ?? {}) as TFilters
+  );

Also applies to: 35-36, 118-120, 151-153

examples/with-nextjs-and-clerk-auth/src/app/(monite)/expenses/page.tsx (1)

4-10: Remove unnecessary async from page component

No await used; drop async to avoid returning a Promise unnecessarily.

-export default async function ExpensesPage() {
+export default function ExpensesPage() {
packages/sdk-react/src/components/expenses/Expenses.tsx (1)

2-2: Note: PageHeader is MUI-based; plan shadcn/ui migration when feasible

You’re consuming an existing MUI wrapper (PageHeader). That’s fine short-term per guidelines, but add/track a TODO in PageHeader to migrate to shadcn/ui and Tailwind (replace sx) when it next needs changes.

If helpful, I can open a follow-up issue to track the PageHeader migration.

Also applies to: 15-15

packages/sdk-react/src/components/expenses/ExpensesTable/consts.ts (1)

1-3: Constants are fine; consider a typed key union for future-proofing

Optional: expose a FilterKey union to help consumers constrain keys and enable refactors without string drift.

You could add (non-breaking):

export const FILTER_KEYS = ['search', 'started_at', 'user'] as const;
export type FilterKey = typeof FILTER_KEYS[number];
packages/sdk-react/src/components/expenses/ExpensesTable/types.ts (1)

7-11: LGTM: filter state shape is clear and minimal.

Optional: if a non-Date picker is used elsewhere, consider widening started_at to Date | string or normalize at the component boundary (see my comment in UserTransactionsTable).

packages/sdk-react/src/core/componentSettings/index.ts (1)

352-356: Defaults wiring for expenses.pageSizeOptions is correct.

Consistent with other sections. Consider defining a dedicated ExpensesSettings interface for parity with PayableSettings/ReceivableSettings in a follow-up.

packages/sdk-react/src/components/expenses/ExpensesTable/UserTransactionsTable.tsx (1)

140-149: Ensure i18n.date receives a Date object.

started_at is typically an ISO string. Passing a Date avoids locale inconsistencies.

-            return i18n.date(date, locale.dateTimeFormat);
+            return i18n.date(new Date(date), locale.dateTimeFormat);
packages/sdk-react/src/components/expenses/ExpensesTable/ManagerTransactionsTable.tsx (5)

45-45: Typo: isTransaxtionReadSupportedLoading → isTransactionReadSupportedLoading.

Minor readability/consistency fix.

- isTransaxtionReadSupportedLoading,
+ isTransactionReadSupportedLoading,
@@
- if (isTransaxtionReadSupportedLoading || isUserReadSupportedLoading) {
+ if (isTransactionReadSupportedLoading || isUserReadSupportedLoading) {

Also applies to: 285-287


231-237: Ensure started_at is formatted from a valid Date.

OpenAPI models often use ISO strings. Convert safely to Date to prevent invalid formatting.

- const date = row.original.started_at;
- if (!date) {
-   return '-';
- }
- return i18n.date(date, locale.dateTimeFormat);
+ const date = row.original.started_at;
+ if (!date) return '—';
+ const dt = typeof date === 'string' ? new Date(date) : (date as Date);
+ if (Number.isNaN(dt.getTime())) return '—';
+ return i18n.date(dt, locale.dateTimeFormat);

217-222: Use id instead of accessorKey for derived/non-data columns.

Avoid implying there’s a backing field called employee.

- accessorKey: 'employee',
+ id: 'employee',

178-189: Type the user map to avoid any.

- const userMap = new Map();
+ const userMap = new Map<
+   string,
+   components['schemas']['EntityUserReadSchema']
+ >();

240-260: Normalize placeholders to a single style ().

Keep UI consistent: use the same placeholder symbol across branches.

- return '-';
+ return '—';
@@
- : '—';
+ : '—';
@@
- return '—';
+ return '—';
packages/sdk-react/src/ui/components/data-table.tsx (2)

54-56: Expose page-size options via props instead of hardcoding.

Aligns with componentSettings.expenses.pageSizeOptions used by callers.

 const PAGES_SIZES: number[] = [20, 50, 100];
 const DEFAULT_PAGE_SIZE: number = 20;
@@
-interface DataTablePaginationProps<TData> {
+interface DataTablePaginationProps<TData> {
   table: ReactTableTable<TData>;
   isControlledPagination?: boolean;
+  pageSizeOptions?: number[];
 }
@@
-export function DataTablePagination<TData>({
-  table,
-  isControlledPagination,
-}: DataTablePaginationProps<TData>) {
+export function DataTablePagination<TData>({
+  table,
+  isControlledPagination,
+  pageSizeOptions,
+}: DataTablePaginationProps<TData>) {
   const { i18n } = useLingui();
+  const options = pageSizeOptions && pageSizeOptions.length > 0 ? pageSizeOptions : PAGES_SIZES;
@@
-            {PAGES_SIZES.map((pageSize) => (
+            {options.map((pageSize) => (
               <SelectItem key={pageSize} value={`${pageSize}`}>
                 {pageSize}
               </SelectItem>
             ))}
-interface DataTableProps<TData, TValue> {
+interface DataTableProps<TData, TValue> {
   columns: ColumnDef<TData, TValue>[];
   data: TData[];
   // Loading state
   loading?: boolean;
+  // Page size options
+  pageSizeOptions?: number[];
@@
 export function DataTable<TData, TValue>({
   columns,
   data,
   loading = false,
+  pageSizeOptions,
   pagination: controlledPagination,
   onPaginationChange: controlledOnPaginationChange,
@@
   <DataTablePagination
     table={table}
     isControlledPagination={!!controlledPagination}
+    pageSizeOptions={pageSizeOptions}
   />

Callers can now pass pageSizeOptions={componentSettings?.expenses?.pageSizeOptions}.

Also applies to: 165-173, 235-257, 418-421


347-359: Add aria-sort for accessibility.

Exposes current sort state to assistive tech.

- <th
+ <th
+   aria-sort={
+     header.column.getIsSorted() === 'asc'
+       ? 'ascending'
+       : header.column.getIsSorted() === 'desc'
+       ? 'descending'
+       : 'none'
+   }
    key={header.id}
    className="mtw:sticky mtw:top-0 mtw:z-10 mtw:bg-background mtw:text-foreground mtw:h-10 mtw:px-3 mtw:text-left mtw:align-middle mtw:whitespace-nowrap mtw:border-b mtw:border-border mtw:after:content-[''] mtw:after:absolute mtw:after:bottom-0 mtw:after:left-0 mtw:after:right-0 mtw:after:border-b mtw:after:border-border"
  >
packages/sdk-react/src/components/UserDisplayCell/UserDisplayCell.tsx (4)

20-20: Type the Typography variant via MUI to avoid drift.

Rely on MUI’s source-of-truth type instead of a hand-rolled union while still constraining to the allowed subset.

-import { type SxProps, Box, Typography } from '@mui/material';
+import { type SxProps, Box, Typography, type TypographyProps } from '@mui/material'
@@
-  typographyVariant?: 'body1' | 'body2' | 'inherit';
+  typographyVariant?: Extract<TypographyProps['variant'], 'body1' | 'body2' | 'inherit'>;

Alternative: widen to TypographyProps['variant'] if you want to allow headings, captions, etc.


60-71: Use noWrap instead of manual ellipsis styles.

Leverages built-in behavior and trims verbose sx.

         <Typography
           variant={typographyVariant}
           title={displayName ?? ''}
-          sx={{
-            overflow: 'hidden',
-            textOverflow: 'ellipsis',
-            whiteSpace: 'nowrap',
-            textAlign: 'center',
-          }}
+          noWrap
+          sx={{ textAlign: 'center' }}
         >
           {displayName ?? ''}
         </Typography>
@@
       <Typography
         variant={typographyVariant}
         title={displayName ?? ''}
-        sx={{
-          overflow: 'hidden',
-          textOverflow: 'ellipsis',
-          whiteSpace: 'nowrap',
-        }}
+        noWrap
       >
         {displayName ?? ''}
       </Typography>

Also applies to: 85-95


3-4: Optional: track MUI→shadcn/ui migration.

Minor edit here, so keeping MUI is fine; add a TODO per guidelines to revisit during larger changes.

 import { type Theme } from '@monite/sdk-react/mui-styles';
-import { type SxProps, Box, Typography } from '@mui/material';
+// TODO(ui): Consider migrating this component to shadcn/ui when substantial changes are made.
+import { type SxProps, Box, Typography } from '@mui/material';

8-15: Use generated EntityUserResponse type for user.
Replace the inline user shape in UserDisplayCellProps with the API-generated type from @/api, for example:

import { components } from '@/api';

type EntityUser = components['schemas']['EntityUserResponse'];

interface UserDisplayCellProps {
  user: EntityUser;
  /* … */
}

Comment on lines 38 to 40
<NavigationListItem href="/expenses" icon={<IconUsdCircle />}>
{t(i18n)`Expenses`}
</NavigationListItem>
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

Add “Expenses” nav — OK

Matches existing nav patterns and i18n usage. Ensure the /expenses page is included in the example app.


🏁 Script executed:

#!/bin/bash
# Confirm page route exists in the Next.js example
fd -a 'expenses/page.tsx' examples/with-nextjs-and-clerk-auth/src/app || echo "Missing /expenses page"

Length of output: 69


🏁 Script executed:

#!/bin/bash
# Search for Expenses page in App Router folder
find examples/with-nextjs-and-clerk-auth/src/app -type f -iname "*expenses*.tsx"
# Search for Expenses page in Pages Router folder
find examples/with-nextjs-and-clerk-auth/pages -type f -iname "*expenses*.tsx"

Length of output: 240


Missing “/expenses” route
A navigation item links to /expenses, but there’s no corresponding src/app/expenses/page.tsx (or equivalent) in the example. Add the page file or update the route to match an existing page.

🤖 Prompt for AI Agents
In
examples/with-nextjs-and-clerk-auth/src/components/NavigationMenu/NavigationList.tsx
around lines 38-40, the navigation includes a link to "/expenses" but the
example project lacks a corresponding page at src/app/expenses/page.tsx (or an
equivalent route). Add a new page file at src/app/expenses/page.tsx (or update
the existing routing) to match the "/expenses" path — the new page should export
a default React component (or Next.js page) so the route resolves —
alternatively, change the href in NavigationList to point to an existing route
if you do not want to add a new page.

<Route path="/products" element={<ProductsPage />} />
<Route path="/roles-and-approvals" element={<RolesApprovalsPage />} />
<Route path="/tags" element={<TagsPage />} />
<Route path="/expenses" element={<ExpensesPage />} />
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Protected /expenses route wired correctly

Route placement and protection match the app’s routing pattern. Ensure the sidebar links to this path so users can navigate to it.


🏁 Script executed:

#!/bin/bash
# Confirm sidebar navigation includes /expenses
rg -nP --type=tsx -C2 'to=\\{?["\\\']/expenses["\\\']' packages/sdk-playground/src/components/app-sidebar.tsx

Length of output: 116


🏁 Script executed:

#!/bin/bash
# Confirm sidebar navigation includes /expenses
rg -nP --type=tsx 'to\s*=\s*["\']/expenses["\']' packages/sdk-playground/src/components/app-sidebar.tsx

Length of output: 116


🏁 Script executed:

#!/bin/bash
# Locate sidebar and verify link to /expenses
FILE=packages/sdk-playground/src/components/app-sidebar.tsx

echo "Checking for double-quoted link:"
grep -n 'to="/expenses"' "$FILE" || echo "Double-quoted link not found"

echo "Checking for single-quoted link:"
grep -n "to='/expenses'" "$FILE" || echo "Single-quoted link not found"

Length of output: 515


Add navigation link for /expenses in the sidebar

The sidebar (packages/sdk-playground/src/components/app-sidebar.tsx) currently lacks a to="/expenses" link, so users cannot navigate to the protected Expenses page. Add an entry such as:

<Link to="/expenses">Expenses</Link>
🤖 Prompt for AI Agents
In packages/sdk-playground/src/components/app-sidebar.tsx (add near the existing
navigation links), the sidebar is missing a route to the protected Expenses
page; add a navigation entry that links to "/expenses" (e.g., a Link or NavLink
with to="/expenses" and visible label "Expenses") alongside the other sidebar
items so users can navigate to the <Route path="/expenses" .../> defined in
App.tsx.

Comment on lines +78 to +81
title: 'Financing',
icon: DollarSignIcon,
url: '/financing',
},
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Financing link: verify route before shipping

Ensure /financing exists to avoid a broken sidebar link.


🏁 Script executed:

#!/bin/bash
# Look for a Financing page or route
rg -nP 'Financing(Page)?' packages/sdk-playground/src || echo "No Financing route/page detected"

Length of output: 137


🏁 Script executed:

#!/bin/bash
# Search for '/financing' string in source files
rg -n "/financing" packages/sdk-playground/src || echo "No '/financing' string found"
# Search for a route definition with path '/financing'
rg -nP "path\\s*=\\s*['\"]/financing['\"]" packages/sdk-playground/src || echo "No Route path '/financing' found"

Length of output: 283


Implement or remove the /financing sidebar entry
No route or page for /financing exists—update your router config to handle this path or remove the link from packages/sdk-playground/src/components/app-sidebar.tsx:78–81.

🤖 Prompt for AI Agents
In packages/sdk-playground/src/components/app-sidebar.tsx around lines 78–81,
the sidebar contains a link to '/financing' but no route or page exists; either
remove this entry from the sidebar items array or add a matching route and page
component. If you choose to add the route, create a Financing page component and
register it in the app router (or routes config) so '/financing' resolves,
import and map the component in the router, and ensure navigation and any
permissions match other sidebar items; if you remove it, simply delete the
object with title 'Financing' (and related imports/usages) so the sidebar only
links to valid routes.

import { LoadingPage } from '@/ui/loadingPage';
import { t } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { DatePicker } from '@mui/x-date-pickers';
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid introducing new MUI; use shadcn/ui (or a local wrapper) for DatePicker and Tailwind for styling.

Repo guidelines prohibit new MUI usage and inline styles. Replace @mui/x-date-pickers with a shadcn/ui-based date picker (Popover + Calendar) or a local wrapper in @/ui/components/date-picker, and remove sx in favor of Tailwind mtw:* classes. If a migration isn’t feasible right now, add a TODO and rationale.

Apply minimally viable changes now (adds TODO and switches classes), and plan migration:

- import { DatePicker } from '@mui/x-date-pickers';
+// TODO(DEV-15216): Replace with shadcn/ui date picker (Popover + Calendar) or a wrapper in @/ui/components/date-picker
+import { DatePicker } from '@mui/x-date-pickers';

- <DatePicker
-   className="Monite-ExpensesStartedAtFilter Monite-FilterControl Monite-DateFilterControl"
+ <DatePicker
+   className="mtw:w-[220px]"
    onChange={(value, error) => {
      if (error.validationError) {
        return;
      }
      onFilterChange(FILTER_TYPE_STARTED_AT, value as Date | null);
    }}
    slotProps={{
      textField: {
-       variant: 'standard',
-       placeholder: t(i18n)`Filter by date`,
-       InputProps: {
-         sx: {
-           '&::placeholder': { opacity: 1, color: 'text.primary' },
-           '& input::placeholder': { opacity: 1, color: 'text.primary' },
-         },
-       },
+       variant: 'standard',
+       placeholder: t(i18n)`Filter by date`,
      },
      popper: { container: root },
      dialog: { container: root },
      actionBar: { actions: ['clear', 'today'] },
    }}
    views={['year', 'month', 'day']}
  />

Also applies to: 321-357

components['schemas']['TransactionCursorFields'],
FilterTypes
>({
initialPageSize: componentSettings.expenses.pageSizeOptions[0] ?? 20,
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Guard against undefined componentSettings.expenses.pageSizeOptions.

Use optional chaining to avoid a crash when settings are missing.

- initialPageSize: componentSettings.expenses.pageSizeOptions[0] ?? 20,
+ initialPageSize: componentSettings?.expenses?.pageSizeOptions?.[0] ?? 20,
🤖 Prompt for AI Agents
In
packages/sdk-react/src/components/expenses/ExpensesTable/ManagerTransactionsTable.tsx
around line 75, the code accesses componentSettings.expenses.pageSizeOptions[0]
without guarding for componentSettings.expenses or pageSizeOptions; update the
expression to use optional chaining and a fallback (e.g.
componentSettings?.expenses?.pageSizeOptions?.[0] ?? 20) so it won't throw when
settings or pageSizeOptions are undefined.

Comment on lines 66 to 108
{shouldShowAlert && (
<Alert severity="warning">
<div className="mtw:flex mtw:flex-col mtw:items-start mtw:gap-2">
{!counterpart?.reminders_enabled && hasValidReminderEmail && (
<span>
{t(
i18n
)`Payment reminders are disabled for this customer. Please enable them in the customer details or turn them off.`}
</span>
)}
{!hasValidReminderEmail && counterpart?.reminders_enabled && (
<span>
{t(
i18n
)`No email address is added for the selected customer. Please add it to the customer details or turn off the reminders.`}
</span>
)}

{handleEditCounterpartModal && (
<button
className="mtw:underline mtw:p-0 mtw:border-none mtw:outline-none mtw:hover:cursor-pointer mtw:transition-all mtw:hover:opacity-80"
type="button"
onClick={() => {
if (handleEditProfileState) {
handleEditProfileState(true);
}
handleEditCounterpartModal(true);
}}
>
{t(i18n)`Edit customer`}
</button>
)}
</div>
</Alert>
)}
{!hasValidReminderEmail && !counterpart?.reminders_enabled && (
<span>
{t(
i18n
)`Reminders are disabled for this customer, and no email address has been added for it. Please update the details or turn off reminders.`}
</span>
)}

<div className="mtw:flex mtw:gap-6 mtw:space-between mtw:w-full">
<ReminderBeforeDueDate
handleCreate={onCreateReminder}
onUpdatePaymentReminder={onUpdatePaymentReminder}
disabled={disabled || isCounterpartLoading}
/>
{handleEditCounterpartModal && (
<button
className="mtw:underline mtw:p-0 mtw:border-none mtw:outline-none mtw:hover:cursor-pointer mtw:transition-all mtw:hover:opacity-80"
type="button"
onClick={() => {
if (handleEditProfileState) {
handleEditProfileState(true);
}
handleEditCounterpartModal(true);
}}
>
{t(i18n)`Edit customer`}
</button>
)}
</div>
</Alert>
)}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Replace native button with MUI Button to restore focus a11y and stay consistent with existing MUI in this file

A raw <button> removes the default focus outline and diverges from the current MUI usage in this module. Given the “modify MUI → keep MUI” guideline, use MUI’s Button with a text variant and keep Tailwind utilities via className.

- import { Alert } from '@mui/material';
+ import { Alert, Button } from '@mui/material';
-            {handleEditCounterpartModal && (
-              <button
-                className="mtw:underline mtw:p-0 mtw:border-none mtw:outline-none mtw:hover:cursor-pointer mtw:transition-all mtw:hover:opacity-80"
-                type="button"
-                onClick={() => {
-                  if (handleEditProfileState) {
-                    handleEditProfileState(true);
-                  }
-                  handleEditCounterpartModal(true);
-                }}
-              >
-                {t(i18n)`Edit customer`}
-              </button>
-            )}
+            {handleEditCounterpartModal && (
+              <Button
+                type="button"
+                variant="text"
+                size="small"
+                className="mtw:underline mtw:px-0 mtw:py-0 mtw:h-auto mtw:transition-all mtw:hover:opacity-80"
+                onClick={() => {
+                  if (handleEditProfileState) {
+                    handleEditProfileState(true);
+                  }
+                  handleEditCounterpartModal(true);
+                }}
+              >
+                {t(i18n)`Edit customer`}
+              </Button>
+            )}

Also applies to: 9-9

🤖 Prompt for AI Agents
packages/sdk-react/src/components/receivables/components/ReminderSectionContent.tsx
around lines 66 to 108: replace the native <button> with MUI's Button (text
variant) to restore keyboard focus/a11y and match existing MUI usage; keep the
same className tailwind utilities, type="button" and onClick behavior (call
handleEditProfileState(true) if present then handleEditCounterpartModal(true)),
and ensure you add an import for Button from @mui/material at the top of the
file.

Comment on lines 118 to 121
const { data: settings, isLoading: isSettingsLoading } =
api.entities.getEntitiesIdSettings.useQuery({
path: { entity_id: entityId },
});
api.entities.getEntitiesIdSettings.useQuery({
path: { entity_id: entityId },
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Guard the settings query with enabled: !!entityId to avoid 400s/race on undefined entity

Other queries in this file already gate on entityId. Mirror that here to prevent making a request with an undefined ID.

-  const { data: settings, isLoading: isSettingsLoading } =
-    api.entities.getEntitiesIdSettings.useQuery({
-      path: { entity_id: entityId },
-    });
+  const { data: settings, isLoading: isSettingsLoading } =
+    api.entities.getEntitiesIdSettings.useQuery(
+      { path: { entity_id: entityId ?? '' } },
+      { enabled: !!entityId }
+    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { data: settings, isLoading: isSettingsLoading } =
api.entities.getEntitiesIdSettings.useQuery({
path: { entity_id: entityId },
});
api.entities.getEntitiesIdSettings.useQuery({
path: { entity_id: entityId },
});
const { data: settings, isLoading: isSettingsLoading } =
api.entities.getEntitiesIdSettings.useQuery(
{ path: { entity_id: entityId ?? '' } },
{ enabled: !!entityId }
);
🤖 Prompt for AI Agents
In
packages/sdk-react/src/components/receivables/InvoiceDetails/CreateReceivable/CreateReceivables.tsx
around lines 118 to 121, the settings query is executed unguarded which can
trigger 400s or races when entityId is undefined; update the
api.entities.getEntitiesIdSettings.useQuery call to pass an options object with
enabled: !!entityId (i.e., useQuery({ path: { entity_id: entityId } }, {
enabled: !!entityId })) so the query only runs when entityId is truthy, matching
the gating used by other queries in the file.

Comment on lines 280 to 285
const customerHasRemindersEnabled =
counterpart && counterpart?.reminders_enabled;
const customerHasDefaultEmail =
counterpart &&
counterpartContacts?.find((contact) => contact.is_default)?.email;

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Coerce to booleans to satisfy TS and prevent truthy string/object leaks

The current expressions yield union types (string | undefined | boolean). Normalize once and reuse.

-    const customerHasRemindersEnabled =
-      counterpart && counterpart?.reminders_enabled;
-    const customerHasDefaultEmail =
-      counterpart &&
-      counterpartContacts?.find((contact) => contact.is_default)?.email;
+    const customerHasRemindersEnabled = Boolean(
+      counterpart?.reminders_enabled
+    );
+    const customerHasDefaultEmail = Boolean(
+      counterpart &&
+        counterpartContacts?.find((contact) => contact.is_default)?.email
+    );
+    const remindersSelected = Boolean(
+      values.payment_reminder_id || values.overdue_reminder_id
+    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const customerHasRemindersEnabled =
counterpart && counterpart?.reminders_enabled;
const customerHasDefaultEmail =
counterpart &&
counterpartContacts?.find((contact) => contact.is_default)?.email;
const customerHasRemindersEnabled = Boolean(
counterpart?.reminders_enabled
);
const customerHasDefaultEmail = Boolean(
counterpart &&
counterpartContacts?.find((contact) => contact.is_default)?.email
);
const remindersSelected = Boolean(
values.payment_reminder_id || values.overdue_reminder_id
);
🤖 Prompt for AI Agents
In
packages/sdk-react/src/components/receivables/InvoiceDetails/CreateReceivable/CreateReceivables.tsx
around lines 280 to 285, the two variables produce union types (string |
undefined | boolean) leading to potential truthy string/object leaks; coerce
each result to an explicit boolean once (e.g., using !! or Boolean()) and assign
to clearly named constants (e.g., hasReminders, hasDefaultEmail) and then reuse
those boolean constants throughout the component so TypeScript sees a consistent
boolean type and you avoid accidental truthy non-boolean values.

Comment on lines +174 to +216
const handlePaginationChange: OnChangeFn<PaginationState> = useCallback(
(updater) => {
const newPagination =
typeof updater === 'function' ? updater(pagination) : updater;

// If page size changed, reset to first page
if (newPagination.pageSize !== pagination.pageSize) {
setPageTokens([null]);
setCurrentPageIndex(0);
setPaginationState({ pageIndex: 0, pageSize: newPagination.pageSize });
setApiResponse(undefined);
return;
}

// Handle page navigation
if (newPagination.pageIndex !== pagination.pageIndex) {
const targetPageIndex = newPagination.pageIndex;

// If going forward and we don't have the token yet, we need to store it
if (
targetPageIndex > currentPageIndex &&
targetPageIndex === pageTokens.length
) {
// We're going to the next page - store the next_pagination_token from current response
if (apiResponse?.next_pagination_token) {
setPageTokens((prev) => [
...prev,
apiResponse.next_pagination_token!,
]);
}
}

setCurrentPageIndex(targetPageIndex);
setPaginationState(newPagination);
}
},
[
pagination,
currentPageIndex,
pageTokens,
apiResponse?.next_pagination_token,
]
);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Prevent multi-page “jumps” with cursor pagination (can fetch the wrong page).

If pageIndex is set beyond pageTokens.length, currentPaginationToken becomes undefined and the UI can show page N while fetching page 1. Clamp the target to the max discoverable page (the next page) before proceeding.

-      if (newPagination.pageIndex !== pagination.pageIndex) {
-        const targetPageIndex = newPagination.pageIndex;
+      if (newPagination.pageIndex !== pagination.pageIndex) {
+        const rawTargetPageIndex = newPagination.pageIndex;
+        // Allow direct nav to any known page, or at most the next not-yet-known page
+        const maxAllowedIndex = pageTokens.length;
+        const targetPageIndex = Math.min(Math.max(0, rawTargetPageIndex), maxAllowedIndex);
🤖 Prompt for AI Agents
In packages/sdk-react/src/core/hooks/useDataTableState.ts around lines 174 to
216, the handler allows setting pageIndex beyond the number of discoverable
pageTokens which makes currentPaginationToken undefined and can cause the UI to
display the wrong page; clamp targetPageIndex to at most pageTokens.length (the
next discoverable page) before any logic that reads pageTokens or apiResponse,
then use that clamped value for token storing, setCurrentPageIndex, and
setPaginationState so we never attempt to navigate to a page without a known
token; update the branching to compare and store tokens using the clamped index
and return early if the clamped index equals the current page to avoid
unnecessary state changes.

Comment on lines +38 to +46
initialPageParam: { query: { pagination_token: undefined } },
getNextPageParam: (currentPage) => {
if (!currentPage.next_pagination_token) return undefined;
return {
query: { pagination_token: currentPage.next_pagination_token },
};
},
enabled: memoizedTransactionIds.length > 0,
placeholderData: keepPreviousData,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix pagination param merge: next pages may drop filters

getNextPageParam returns only pagination_token. If the generated client replaces (not deep-merges) the query, subsequent pages can lose transaction_id__in and limit, returning unrelated receipts. Include the base filters in the returned page param.

Apply:

-      getNextPageParam: (currentPage) => {
-        if (!currentPage.next_pagination_token) return undefined;
-        return {
-          query: { pagination_token: currentPage.next_pagination_token },
-        };
-      },
+      getNextPageParam: (currentPage) => {
+        if (!currentPage.next_pagination_token) return undefined;
+        return {
+          query: {
+            transaction_id__in: memoizedTransactionIds,
+            limit: 100,
+            pagination_token: currentPage.next_pagination_token,
+          },
+        };
+      },

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/sdk-react/src/core/queries/useReceiptsByTransactionIds.ts around
lines 38 to 46, getNextPageParam currently returns only pagination_token which
can cause subsequent requests to lose base filters (e.g. transaction_id__in,
limit) if the client replaces (not deep-merges) query objects; change
getNextPageParam to return a merged query that preserves the original filters by
combining the base/previous query (e.g. initialPageParam.query or
currentPage.query) with the new pagination_token so the returned page param
includes pagination_token plus the existing transaction_id__in and limit (and
any other query fields) instead of replacing them.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 4, 2025

✅ Actions performed

Reviews paused.

* refactor: Improve sheet component styles and add SheetContentWrapper with scroll

* feat: Add row click handler prop to DataTable component

* feat Add TransactionDetails component that shows transaction data in a sheet

* feat: Integrate TransactionDetails modal in UserTransactionsTable on row click

* chore: Extract translations

* feat: Integrate TransactionDetails modal in ManagerTransactionsTable on row click

* docs(changeset): Add Expense Management Transaction details dialog sheet on row click of the "Expenses" listing of Transactions

* fix: Update onOpenChange handler in TransactionDetails sheet to ensure onClose is called correctly

* fix: Convert transaction amount from minor units

* feat: Extract new ImageFileViewer component from FileViewer

* fix: Update TransactionDetails to use ImageFileViewer and improve file URL handling

* fix: Update ManagerTransactionsTable to use formatFromMinorUnits for transaction amounts

* refactor: Improve dialog component styles for close button

* refactor: Extract getMimetypeFromUrl into files utils file

* feat: Add ReceiptPreview component for displaying receipt details in a dialog

* feat: Integrate ReceiptPreview component into TransactionDetails for receipt viewing experience

* feat: Integrate ReceiptPreview component into UserTransactionsTable for receipt viewing experience

* chore: Extract translations

* feat: Enhance TransactionDetails component with Employee display when Manager view

* feat: Add isManagerView prop to TransactionDetails for ManagerTransactionsTable

* fix: Improve styling and layout for FileViewer components to enhance responsiveness

* fix: Update ReceiptPreview layout and styling for improved responsiveness

* chore: Extract translations

* refactor: Remove commented out icon

* refactor: Replace formatFromMinorUnits with formatCurrencyToDisplay

* feat: Show transaction merchant amount and amount

* feat: Make TransactionDetails and ReceiptPreview non-modal and ensure clean state transition
* style: Fix danger color variables and adjust component styles for consistency

* feat: Add ResponsiveGrid component with infinite scroll and filtering capabilities

* feat: Add Separator component

* feat: Implement TabBar component according to new design

* feat: Enhance ImageFileViewer component with customizable objectFit property

* feat: Implement ReceiptsInbox component with filtering, infinite scrolling, and receipt display

* Feat: Show receipts mailbox address

* feat: Show badge if has any unmatched receipt

* fix: Add custom scroll container support to ResponsiveGrid for use in dialogs or modals

* feat: Make ReceiptsInbox a fullscreen dialog in Expenses page

* chore: Extract translations

* docs(changeset): Add Receipts Inbox screen to list receipts in Expenses page

* feat: Make grid items accessible when clickable; avoid pointer cursor when not

* refactor: Update ReceiptsInbox to use HasTransactionFilterValue type for tab changes

* refactor: Replace setTimeout with requestAnimationFrame for smoother tab change scrolling in ReceiptsInbox

* refactor: Update TabBar components for improved styling and accessibility

* refactor: Enhance ImageFileViewer with referrer policy and async decoding for improved performance

* refactor: Add aria-label for accessibility to the receipts inbox button in Expenses component

* refactor: Remove memoization of ReceiptCard component

* refactor: Use non-breaking space constant for better readability

* refactor: Rename query hooks

* refactor: Replace API calls with custom hook for fetching users by IDs

* refactor: Move filter constants and types directly into ReceiptsInbox component

* chore: Extract translations

* fix: Update mailbox filter to include only active inboxes

* chore: Update API schema file for receipts related endpoints

* refactor: Add enabled parameter to useGetMailboxes hook for conditional querying

* feat: Integrate permissions check for reading receipts in Expenses and ReceiptsInbox components

* feat: Add support for 'no-data=receipts' state in DataGridEmptyState component

* feat: Implement no-items overlay in ReceiptsInbox

* feat: Add TODO for filtering receipts by current user in ReceiptsInbox
* fix: Add destructiveGhost variant to button component

* style: Update SheetContentWrapper to use overflow-y-auto and overflow-x-hidden for improved scrolling behavior

* refactor: Extract ReceiptStatusChip into reusable component

* feat: Add hooks for managing receipts and query transactions

* feat: Implement ReceiptDetails sheet component for displaying and managing receipt information

* fix: Allow interaction with ReceiptDetails when open in ReceiptsInbox component

* feat: Add confirmation modal for receipt deletion in ReceiptDetails component

* chore: Extract translations

* docs(changeset): Add Expense Management Receipt details dialog sheet on click of the Receipt card

* refactor: Invalidate receipts queries after delete to avoid stale UI

* fix: Correct Zod import

* style: Enhance destructiveGhost variant with focus-visible ring styles for improved accessibility

* fix: Update validation messages and improve date handling in ReceiptDetails component

* refactor:  Improve TabBar default value handling in ReceiptsInbox component and fix mailboxes typo

* refactor: Remove unused root context from ReceiptDetails and TransactionDetails

* fix: Update ReceiptDetails validation schema for total amount min or empty values and improve error messages

* refactor: Enhance getUserDisplayName function with improved fallback logic and additional test cases

* refactor: Simplify user display name logic in TransactionDetails component

* chore: Extract translations

* fix: Update ReceiptDetails inputProps to include step attribute for decimal values

* feat: Add utility function to coerce strings to numbers with handling for empty values

* fix: Update ReceiptDetails validation schema to use coerceNumber for totalAmount and improve layout styling

* fix: Update ReceiptCard to handle total_amount check for null values
* refactor: Improve file type handling across components to use OcrFileType constants

* refactor: Convert PopoverContent to a forwardRef component for improved ref handling to allow better focus management

* refactor: Convert DialogContent to a forwardRef component for improved ref handling to allow better focus management

* refactor: Extract FileUpload component with file select or drag-and-drop from CreatePayableMenu

* feat: Add custom hook for uploading new receipt files

* feat: Enhance ResponsiveGrid with item clickability control

* feat: Add receipt upload functionality to ReceiptsInbox with file upload component

* feat: Add OCR processing state to ReceiptCard

* feat: Implement refetch interval for receipts in OCR processing

* docs(changeset): Add receipt upload feature in Receipts Inbox screen

* chore: Extract translations

* refactor: Remove unused imports from ManualPaymentRecordDetails and PaymentRecordForm components

* chore: Extract translations

* chore: Extract translations

* chore: Extract translations

* chore: Extract translations
* refactor: Rname Expenses naming with Transactions across components and routes

- Deleted the ExpensesPage component and replaced it with TransactionsPage.
- Updated MoniteComponents to export Transactions instead of Expenses.
- Modified NavigationList and App sidebar to link to Transactions instead of Expenses.
- Introduced TransactionsTable component with necessary hooks and filters.
- Updated component settings to reflect the change from expenses to transactions.
- Removed obsolete ExpensesTable and related files.

* chore: Extract translations

* refactor: Rename expenses component directory into transactions

* chore: Extract translations
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge pullpreview Generate a live preview for this pull request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant