feat(DEV-15216): Expenses Management feature#820
Conversation
…with pagination, sorting, and filtering functionality with API
…actionsTable components
…ing of Transactions
…ng row count in DataTable
|
Note Reviews pausedUse the following commands to manage reviews:
WalkthroughAdds 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
Possibly related PRs
Suggested labels
✨ Finishing touches🧪 Generate unit tests
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. Comment |
|
@coderabbitai ignore |
🦋 Changeset detectedLatest 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 |
|
🚀 Preview URLs are now available! 🚀 |
There was a problem hiding this comment.
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_PAYvs actual emitted string).Events are dispatched as
${MONITE_EVENT_PREFIX}:${type}, wheretypeis'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 atargetparameter that the function doesn’t accept.
emitMoniteEventhas 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 setsactualCurrencytoundefinedIf the user opens the currency modal and clicks Save without changing the selection,
tempCurrencyremainsundefined, yethandleCurrencySubmittreats it as a change and setsactualCurrencytoundefined. 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.ReactNodetype used without importingReacttypesAvoid relying on the global React namespace; import the
ReactNodetype 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 stateClosing the dialog currently only hides it; it does not reset
confirmSubmissionorformValues. 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 errorsMultiplying 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 produceInvalid Dateand throw
new Date(formValues.payment_date ?? '')yields an invalid date whenpayment_dateisnull, leading totoISOStringthrowing 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_idis optional per OpenAPI (not in therequiredlist) and will be dropped byJSON.stringifyifundefined.payment_intent_idis required and must reference an actual PaymentIntent—usinginvoice.id(an Invoice ID) will be rejected. Generate or fetch a valid PaymentIntent ID for manual payments before callingpostPaymentRecords.
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 typeactionsasPayActionHandlersfor better DX.JSDoc references
PayActionHandlers, but the field isunknown. Import the type from@monite/sdk-reactand 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 derivedremindersSelectedflag to de-duplicate conditionalsKeeps 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 onremindersSelected- if ( - !customerHasDefaultEmail && - customerHasRemindersEnabled && - (values.payment_reminder_id || values.overdue_reminder_id) - ) { + if ( + !customerHasDefaultEmail && + customerHasRemindersEnabled && + remindersSelected + ) {
332-343: And here: rely onremindersSelected- 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 booleansUse the discriminant
counterpart.typeand optional chaining to avoid potential undefined property reads and to keephasValidReminderEmailstrictly boolean. This also simplifiesshouldShowAlert'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 instructionsThe 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-betweenis not a Tailwind utility; the correct class isjustify-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-labelledbywithout a matching element
aria-labelledby="dialog-title"has no corresponding element. AddDialogTitleor 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: MUIsxused; add migration TODO per guidelinesPer 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 conversionConsider 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 settingsIf
useComponentSettings()can ever returnpayablesCallbacksas 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
payablesCallbacksis always defined, ignore.
112-119: Remove explicitundefinedparameter inonPayPassing an explicit
undefinedcan 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 compositionThe 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 entryMinor grammar tweak.
-Add new Expense Management page "Expenses" with listing of Transactions +Add a new Expense Management page, "Expenses", with a list of transactionspackages/sdk-react/src/components/expenses/hooks/useTransactions.ts (1)
5-20: Solid hook; consider stabilizing the query objectLooks 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 keysThe 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 heavyAuto-draining pagination may spike network usage on large result sets. Consider exposing
hasNextPageand 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 selectionThe 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-1300Tailwind 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 consistentlyThis key exists for the data table; ensure pagination components reuse a single key to avoid future drift.
8302-8305: Column controls — consistent casingText 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 styleTwo 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 articleConsider 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,
apiResponsestill reflects the last page’s response and can temporarily shrinkpageCount. Guard withpageTokens.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
unknownoveranyfor 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: Useunknowninstead ofanyfor 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 componentNo 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 feasibleYou’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-proofingOptional: 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_attoDate | stringor normalize at the component boundary (see my comment in UserTransactionsTable).packages/sdk-react/src/core/componentSettings/index.ts (1)
352-356: Defaults wiring forexpenses.pageSizeOptionsis correct.Consistent with other sections. Consider defining a dedicated
ExpensesSettingsinterface for parity withPayableSettings/ReceivableSettingsin a follow-up.packages/sdk-react/src/components/expenses/ExpensesTable/UserTransactionsTable.tsx (1)
140-149: Ensurei18n.datereceives a Date object.
started_atis 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: Useidinstead ofaccessorKeyfor derived/non-data columns.Avoid implying there’s a backing field called
employee.- accessorKey: 'employee', + id: 'employee',
178-189: Type the user map to avoidany.- 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.pageSizeOptionsused 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: Addaria-sortfor 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: UsenoWrapinstead 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 generatedEntityUserResponsetype foruser.
Replace the inlineusershape inUserDisplayCellPropswith the API-generated type from@/api, for example:import { components } from '@/api'; type EntityUser = components['schemas']['EntityUserResponse']; interface UserDisplayCellProps { user: EntityUser; /* … */ }
| <NavigationListItem href="/expenses" icon={<IconUsdCircle />}> | ||
| {t(i18n)`Expenses`} | ||
| </NavigationListItem> |
There was a problem hiding this comment.
💡 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.
packages/sdk-playground/src/App.tsx
Outdated
| <Route path="/products" element={<ProductsPage />} /> | ||
| <Route path="/roles-and-approvals" element={<RolesApprovalsPage />} /> | ||
| <Route path="/tags" element={<TagsPage />} /> | ||
| <Route path="/expenses" element={<ExpensesPage />} /> |
There was a problem hiding this comment.
💡 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.tsxLength 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.tsxLength 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.
| title: 'Financing', | ||
| icon: DollarSignIcon, | ||
| url: '/financing', | ||
| }, |
There was a problem hiding this comment.
💡 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'; |
There was a problem hiding this comment.
🛠️ 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, |
There was a problem hiding this comment.
🛠️ 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.
| {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> | ||
| )} |
There was a problem hiding this comment.
🛠️ 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.
| const { data: settings, isLoading: isSettingsLoading } = | ||
| api.entities.getEntitiesIdSettings.useQuery({ | ||
| path: { entity_id: entityId }, | ||
| }); | ||
| api.entities.getEntitiesIdSettings.useQuery({ | ||
| path: { entity_id: entityId }, | ||
| }); |
There was a problem hiding this comment.
🛠️ 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.
| 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.
| const customerHasRemindersEnabled = | ||
| counterpart && counterpart?.reminders_enabled; | ||
| const customerHasDefaultEmail = | ||
| counterpart && | ||
| counterpartContacts?.find((contact) => contact.is_default)?.email; | ||
|
|
There was a problem hiding this comment.
🛠️ 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.
| 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.
| 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, | ||
| ] | ||
| ); |
There was a problem hiding this comment.
🛠️ 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.
| 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, |
There was a problem hiding this comment.
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.
✅ Actions performedReviews 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
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
Chores