From 9a0d2c219812e7996d6966665d501ecb11345855 Mon Sep 17 00:00:00 2001 From: Hans Date: Thu, 24 Sep 2026 12:15:56 +0700 Subject: [PATCH 01/28] Init bulk update start date --- src/ROUTES.ts | 7 + src/SCREENS.ts | 1 + .../WorkspaceCompanyCardsTableControls.tsx | 31 +++- .../WorkspaceCompanyCardsTable/index.tsx | 1 + src/languages/en.ts | 1 + .../ModalStackNavigators/index.tsx | 2 + .../RELATIONS/WORKSPACE_TO_RHP.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 3 + src/libs/Navigation/types.ts | 5 + ...yCardsBulkEditTransactionStartDatePage.tsx | 162 ++++++++++++++++++ ...WorkspaceCompanyCardsTableControlsTest.tsx | 36 +++- 11 files changed, 245 insertions(+), 5 deletions(-) create mode 100644 src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index f6338375d029..0025092892a2 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -3758,6 +3758,13 @@ const ROUTES = { getRoute: (policyID: string, cardID: string, feed: CompanyCardFeedWithDomainID) => `workspaces/${policyID}/company-cards/${encodeURIComponent(feed)}/${encodeURIComponent(cardID)}/edit/transaction-start-date` as const, }, + WORKSPACE_COMPANY_CARDS_BULK_EDIT_TRANSACTION_START_DATE: { + route: 'workspaces/:policyID/company-cards/:feed/bulk-edit/transaction-start-date', + getRoute: (policyID: string, feed: CompanyCardFeedWithDomainID, cardIDs?: string[]) => { + const cardIDsQuery = cardIDs?.length ? `?cardIDs=${encodeURIComponent(cardIDs.join(','))}` : ''; + return `workspaces/${policyID}/company-cards/${encodeURIComponent(feed)}/bulk-edit/transaction-start-date${cardIDsQuery}` as const; + }, + }, WORKSPACE_EXPENSIFY_CARD: { route: 'workspaces/:policyID/expensify-card', getRoute: (policyID: string | undefined) => { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 1a650658647b..baf069a80de9 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -789,6 +789,7 @@ const SCREENS = { COMPANY_CARD_ADD_WORK_EMAIL: 'Workspace_CompanyCard_Add_Work_Email', COMPANY_CARD_VERIFY_WORK_EMAIL: 'Workspace_CompanyCard_Verify_Work_Email', COMPANY_CARD_EDIT_TRANSACTION_START_DATE: 'Workspace_CompanyCard_Edit_Transaction_Start_Date', + COMPANY_CARDS_BULK_EDIT_TRANSACTION_START_DATE: 'Workspace_CompanyCards_Bulk_Edit_Transaction_Start_Date', DYNAMIC_COMPANY_CARD_EXPORT: 'Dynamic_Workspace_CompanyCard_Export', EXPENSIFY_CARD: 'Workspace_ExpensifyCard', DYNAMIC_EXPENSIFY_CARD_DETAILS: 'Dynamic_Workspace_ExpensifyCard_Details', diff --git a/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls.tsx b/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls.tsx index 38af51ec52bd..fd84c507cb8b 100644 --- a/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls.tsx +++ b/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls.tsx @@ -15,8 +15,10 @@ import {unassignWorkspaceCompanyCard} from '@libs/actions/CompanyCards'; import navigateToCardTransactions from '@libs/CardNavigationUtils'; import {formatMaskedCardName} from '@libs/CardUtils'; import localFileDownload from '@libs/localFileDownload'; +import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; import {format, parseISO} from 'date-fns'; import React from 'react'; @@ -24,12 +26,13 @@ import {View} from 'react-native'; import type {WorkspaceCompanyCardTableItemData} from './WorkspaceCompanyCardsTableRow'; -type WorkspaceCompanyCardBulkActionType = 'unassign' | 'viewTransactions' | 'exportCSV'; +type WorkspaceCompanyCardBulkActionType = 'editTransactionStartDate' | 'unassign' | 'viewTransactions' | 'exportCSV'; type WorkspaceCompanyCardsTableControlsProps = { policyID: string; domainOrWorkspaceAccountID: number; bankName: UseCompanyCardsResult['bankName']; + feedName: UseCompanyCardsResult['feedName']; /** Whether the current member can edit company cards */ canWriteCompanyCards: boolean; @@ -55,6 +58,7 @@ function WorkspaceCompanyCardsTableControls({ policyID, domainOrWorkspaceAccountID, bankName, + feedName, canWriteCompanyCards, clearCardSelection, isSelectionModeEnabled, @@ -62,7 +66,7 @@ function WorkspaceCompanyCardsTableControls({ const styles = useThemeStyles(); const {translate, getLocalDateFromDatetime} = useLocalize(); const {showConfirmModal} = useConfirmModal(); - const icons = useMemoizedLazyExpensifyIcons(['Export', 'MoneySearch', 'RemoveMembers']); + const icons = useMemoizedLazyExpensifyIcons(['CalendarSolid', 'Export', 'MoneySearch', 'RemoveMembers']); const {processedData, shouldUseNarrowTableLayout} = useTableContext(); const {shouldUseNarrowLayout} = useResponsiveLayout(); @@ -146,11 +150,34 @@ function WorkspaceCompanyCardsTableControls({ clearCardSelection(); }; + const editSelectedCardTransactionStartDates = () => { + if (!feedName) { + return; + } + + const selectedCardIDs = selectedAssignedCards + .map((card) => card.assignedCard?.cardID) + .filter((cardID): cardID is number => cardID !== undefined) + .map(String); + if (selectedCardIDs.length === 0) { + return; + } + + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_BULK_EDIT_TRANSACTION_START_DATE.getRoute(policyID, feedName, selectedCardIDs)); + clearCardSelection(); + }; + const getBulkActionOptions = (): Array> => { const options: Array> = []; if (isOnlyAssignedCardsSelected) { if (canWriteCompanyCards) { + options.push({ + icon: icons.CalendarSolid, + text: translate('workspace.moreFeatures.companyCards.bulkUpdateTransactionStartDate'), + value: 'editTransactionStartDate', + onSelected: editSelectedCardTransactionStartDates, + }); options.push({ icon: icons.RemoveMembers, text: translate(isSingleAssignedCard ? 'workspace.moreFeatures.companyCards.unassignCard' : 'workspace.moreFeatures.companyCards.unassignCards'), diff --git a/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx b/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx index 02803b9eca57..dd8096aea3db 100644 --- a/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx +++ b/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx @@ -462,6 +462,7 @@ function WorkspaceCompanyCardsTable({ policyID={policyID} domainOrWorkspaceAccountID={domainOrWorkspaceAccountID} bankName={bankName} + feedName={feedName} canWriteCompanyCards={canWriteCompanyCards} clearCardSelection={clearCardSelection} isSelectionModeEnabled={isSelectionModeEnabled} diff --git a/src/languages/en.ts b/src/languages/en.ts index 66a041a63f90..e9fa3a498098 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -6799,6 +6799,7 @@ const translations = { `Choose the ${integration} account where transactions should be exported.${exportPageLink ? ` Select a different export option to change the available accounts.` : ''}`, lastUpdated: 'Last updated', transactionStartDate: 'Transaction start date', + bulkUpdateTransactionStartDate: 'Update transaction start date', updateCard: 'Update card', unassignCard: 'Unassign card', unassignCards: 'Unassign cards', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 072b67281f64..30fad748af4a 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -1038,6 +1038,8 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/companyCards/WorkspaceCompanyCardEditCardNamePage').default, [SCREENS.WORKSPACE.COMPANY_CARD_EDIT_TRANSACTION_START_DATE]: () => require('../../../../pages/workspace/companyCards/WorkspaceCompanyCardEditTransactionStartDatePage').default, + [SCREENS.WORKSPACE.COMPANY_CARDS_BULK_EDIT_TRANSACTION_START_DATE]: () => + require('../../../../pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage').default, [SCREENS.WORKSPACE.DYNAMIC_COMPANY_CARD_EXPORT]: () => require('../../../../pages/workspace/companyCards/DynamicWorkspaceCompanyCardAccountSelectCardPage').default, [SCREENS.WORKSPACE.DYNAMIC_WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW]: () => require('../../../../pages/workspace/expensifyCard/issueNew/DynamicIssueNewCardPage').default, [SCREENS.WORKSPACE.DYNAMIC_WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW_CONFIRM_VALIDATE_CODE]: () => diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts index 337d8029b1d7..cd86443ba98f 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts @@ -416,6 +416,7 @@ const WORKSPACE_TO_RHP: Partial['config'] = { [SCREENS.WORKSPACE.COMPANY_CARD_EDIT_TRANSACTION_START_DATE]: { path: ROUTES.WORKSPACE_COMPANY_CARD_EDIT_TRANSACTION_START_DATE.route, }, + [SCREENS.WORKSPACE.COMPANY_CARDS_BULK_EDIT_TRANSACTION_START_DATE]: { + path: ROUTES.WORKSPACE_COMPANY_CARDS_BULK_EDIT_TRANSACTION_START_DATE.route, + }, [SCREENS.WORKSPACE.DYNAMIC_COMPANY_CARD_EXPORT]: DYNAMIC_ROUTES.WORKSPACE_COMPANY_CARD_EXPORT.path, [SCREENS.WORKSPACE.DYNAMIC_WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW]: DYNAMIC_ROUTES.WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW.path, [SCREENS.WORKSPACE.DYNAMIC_WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW_CONFIRM_VALIDATE_CODE]: DYNAMIC_ROUTES.WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW_CONFIRM_VALIDATE_CODE.path, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 30f52b7a98c5..d1dd814c1a50 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1554,6 +1554,11 @@ type SettingsNavigatorParamList = { feed: string; cardID: string; }; + [SCREENS.WORKSPACE.COMPANY_CARDS_BULK_EDIT_TRANSACTION_START_DATE]: { + policyID: string; + feed: CompanyCardFeedWithDomainID; + cardIDs?: string; + }; [SCREENS.WORKSPACE.DYNAMIC_COMPANY_CARD_EXPORT]: { policyID: string; cardID: string; diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx new file mode 100644 index 000000000000..f037e39d81fa --- /dev/null +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx @@ -0,0 +1,162 @@ +import Button from '@components/Button'; +import DatePicker from '@components/DatePicker'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import SelectionList from '@components/SelectionList'; +import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem'; +import Text from '@components/Text'; + +import useCardFeeds from '@hooks/useCardFeeds'; +import useCardsList from '@hooks/useCardsList'; +import useLocalize from '@hooks/useLocalize'; +import usePolicy from '@hooks/usePolicy'; +import useThemeStyles from '@hooks/useThemeStyles'; + +import {getCompanyCardFeed, getCompanyFeeds, getDomainOrWorkspaceAccountID} from '@libs/CardUtils'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import {isRequiredFulfilled} from '@libs/ValidationUtils'; + +import Navigation from '@navigation/Navigation'; + +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; + +import {updateCardTransactionStartDate} from '@userActions/CompanyCards'; + +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type {CompanyCardFeedWithDomainID} from '@src/types/onyx'; + +import type {ValueOf} from 'type-fest'; + +import {format} from 'date-fns'; +import React, {useState} from 'react'; +import {View} from 'react-native'; + +type DateOption = ValueOf; +type WorkspaceCompanyCardsBulkEditTransactionStartDatePageProps = PlatformStackScreenProps< + SettingsNavigatorParamList, + typeof SCREENS.WORKSPACE.COMPANY_CARDS_BULK_EDIT_TRANSACTION_START_DATE +>; + +function WorkspaceCompanyCardsBulkEditTransactionStartDatePage({route}: WorkspaceCompanyCardsBulkEditTransactionStartDatePageProps) { + const {policyID} = route.params; + const feedName: CompanyCardFeedWithDomainID = route.params.feed; + const cardIDs = (route.params.cardIDs ?? '').split(',').filter(Boolean); + const bank = getCompanyCardFeed(feedName); + + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const policy = usePolicy(policyID); + const workspaceAccountID = policy?.policyAccountID ?? CONST.DEFAULT_NUMBER_ID; + + const [cardFeeds] = useCardFeeds(policyID); + const companyFeeds = getCompanyFeeds(cardFeeds); + const domainOrWorkspaceAccountID = getDomainOrWorkspaceAccountID(workspaceAccountID, companyFeeds[feedName]); + const [allBankCards] = useCardsList(feedName); + + const [dateOptionSelected, setDateOptionSelected] = useState(CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM); + const [startDate, setStartDate] = useState(() => format(new Date(), CONST.DATE.FNS_FORMAT_STRING)); + const [errorText, setErrorText] = useState(''); + + const goBackToCompanyCards = () => Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); + + const handleSelectDateOption = (dateOption: DateOption) => { + setErrorText(''); + setDateOptionSelected(dateOption); + }; + + const submit = () => { + if (dateOptionSelected === CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM && !isRequiredFulfilled(startDate)) { + setErrorText(translate('common.error.fieldRequired')); + return; + } + + const newStartDate = dateOptionSelected === CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING ? '' : startDate; + for (const cardID of cardIDs) { + updateCardTransactionStartDate(domainOrWorkspaceAccountID, cardID, newStartDate, bank, allBankCards?.[cardID]?.scrapeMinDate); + } + goBackToCompanyCards(); + }; + + const dateOptions = [ + { + value: CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING, + text: translate('workspace.companyCards.fromTheBeginning'), + keyForList: CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING, + isSelected: dateOptionSelected === CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING, + }, + { + value: CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM, + text: translate('workspace.companyCards.customStartDate'), + keyForList: CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM, + isSelected: dateOptionSelected === CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM, + }, + ]; + + return ( + + + + {translate('workspace.companyCards.editStartDateDescription')} + + handleSelectDateOption(value)} + data={dateOptions} + shouldSingleExecuteRowSelect + initiallyFocusedItemKey={dateOptionSelected} + shouldUpdateFocusedIndex + addBottomSafeAreaPadding + footerContent={ + + } + listFooterContent={ + dateOptionSelected === CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM ? ( + + { + if (!isRequiredFulfilled(value)) { + setErrorText(translate('common.error.fieldRequired')); + } else { + setErrorText(''); + } + setStartDate(value); + }} + minDate={CONST.CALENDAR_PICKER.MIN_DATE} + errorText={errorText} + /> + + ) : null + } + /> + + + + ); +} + +export default WorkspaceCompanyCardsBulkEditTransactionStartDatePage; diff --git a/tests/ui/WorkspaceCompanyCardsTableControlsTest.tsx b/tests/ui/WorkspaceCompanyCardsTableControlsTest.tsx index 5eced387c3f2..0c022986663e 100644 --- a/tests/ui/WorkspaceCompanyCardsTableControlsTest.tsx +++ b/tests/ui/WorkspaceCompanyCardsTableControlsTest.tsx @@ -2,15 +2,25 @@ import {render, screen} from '@testing-library/react-native'; import WorkspaceCompanyCardsTableControls from '@components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls'; +import Navigation from '@libs/Navigation/Navigation'; + +import ROUTES from '@src/ROUTES'; + import React from 'react'; +type MockDropdownOption = { + value: string; + onSelected: () => void; +}; + type MockButtonProps = { customText: string; isDisabled?: boolean; + options: MockDropdownOption[]; }; const mockButtonProps: {current?: MockButtonProps} = {}; -let mockProcessedData: Array<{selected?: boolean; disabled?: boolean; isAssigned?: boolean}> = []; +let mockProcessedData: Array<{selected?: boolean; disabled?: boolean; isAssigned?: boolean; assignedCard?: {cardID: number}}> = []; let mockShouldUseNarrowLayout = true; jest.mock('@components/ButtonWithDropdownMenu', () => { @@ -82,18 +92,22 @@ jest.mock('@libs/actions/CompanyCards', () => ({unassignWorkspaceCompanyCard: je jest.mock('@libs/CardNavigationUtils', () => jest.fn()); jest.mock('@libs/CardUtils', () => ({formatMaskedCardName: jest.fn()})); jest.mock('@libs/localFileDownload', () => jest.fn()); +jest.mock('@libs/Navigation/Navigation', () => ({navigate: jest.fn()})); function renderControls(isSelectionModeEnabled: boolean) { - return render( + const clearCardSelection = jest.fn(); + const renderResult = render( , ); + return {clearCardSelection, ...renderResult}; } describe('WorkspaceCompanyCardsTableControls narrow-layout selection mode', () => { @@ -127,4 +141,20 @@ describe('WorkspaceCompanyCardsTableControls narrow-layout selection mode', () = expect(screen.queryByTestId('WorkspaceCompanyCardsBulkActions')).toBeNull(); expect(screen.getByTestId('WorkspaceCompanyCardsFilterBar')).toBeTruthy(); }); + + it('opens the bulk transaction start date screen for the selected assigned cards', () => { + // Given two assigned cards are selected + mockProcessedData = [ + {selected: true, disabled: false, isAssigned: true, assignedCard: {cardID: 123}}, + {selected: true, disabled: false, isAssigned: true, assignedCard: {cardID: 456}}, + ]; + const {clearCardSelection} = renderControls(true); + + // When the bulk transaction start date action is selected + mockButtonProps.current?.options.find((option) => option.value === 'editTransactionStartDate')?.onSelected(); + + // Then the editor opens with both card IDs and clears the table selection + expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.WORKSPACE_COMPANY_CARDS_BULK_EDIT_TRANSACTION_START_DATE.getRoute('policy123', 'oauth.chase.com#123', ['123', '456'])); + expect(clearCardSelection).toHaveBeenCalledTimes(1); + }); }); From df1eeed186d98c824348e560c9613113573fe3c6 Mon Sep 17 00:00:00 2001 From: Hans Date: Thu, 24 Sep 2026 12:18:23 +0700 Subject: [PATCH 02/28] update translations --- src/languages/de.ts | 1 + src/languages/el.ts | 1 + src/languages/es.ts | 1 + src/languages/fr.ts | 1 + src/languages/it.ts | 1 + src/languages/ja.ts | 1 + src/languages/nl.ts | 1 + src/languages/pl.ts | 1 + src/languages/pt-BR.ts | 1 + src/languages/zh-hans.ts | 1 + 10 files changed, 10 insertions(+) diff --git a/src/languages/de.ts b/src/languages/de.ts index 98c4c3d8d97a..a7a8e4c09a3a 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -6618,6 +6618,7 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU `Wähle das ${integration}-Konto, in das Transaktionen exportiert werden sollen.${exportPageLink ? ` Wähle eine andere Exportoption, um die verfügbaren Konten zu ändern.` : ''}`, lastUpdated: 'Zuletzt aktualisiert', transactionStartDate: 'Transaktionsstartdatum', + bulkUpdateTransactionStartDate: 'Transaktionsstartdatum aktualisieren', updateCard: 'Karte aktualisieren', unassignCard: 'Karte zuweisen aufheben', unassignCards: 'Kartenzuweisung aufheben', diff --git a/src/languages/el.ts b/src/languages/el.ts index bb06f389c27e..62b35c4780bc 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -6742,6 +6742,7 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ `Επιλέξτε τον λογαριασμό ${integration} στον οποίο θα εξαχθούν οι συναλλαγές.${exportPageLink ? `Επιλέξτε μια διαφορετική επιλογή εξαγωγής για να αλλάξετε τους διαθέσιμους λογαριασμούς.` : ''}`, lastUpdated: 'Τελευταία ενημέρωση', transactionStartDate: 'Ημερομηνία έναρξης συναλλαγής', + bulkUpdateTransactionStartDate: 'Ενημέρωση ημερομηνίας έναρξης συναλλαγής', updateCard: 'Ενημέρωση κάρτας', unassignCard: 'Αποδέσμευση κάρτας', unassignCards: 'Αποδέσμευση καρτών', diff --git a/src/languages/es.ts b/src/languages/es.ts index e4b5f48f814a..b0128094d7d0 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -6495,6 +6495,7 @@ ${amount} para ${merchant} - ${date}`, `Seleccione la cuenta ${integration} donde se deben exportar las transacciones.${exportPageLink ? ` Seleccione una cuenta diferente opción de exportación para cambiar las cuentas disponibles.` : ''}`, lastUpdated: 'Última actualización', transactionStartDate: 'Fecha de inicio de transacciones', + bulkUpdateTransactionStartDate: 'Actualizar fecha de inicio de transacciones', updateCard: 'Actualizar tarjeta', unassignCard: 'Desasignar tarjeta', unassignCards: 'Desasignar tarjetas', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index e134bdbd6dc7..d1eeab9ebe6f 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -6635,6 +6635,7 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. `Choisissez le compte ${integration} vers lequel les transactions doivent être exportées.${exportPageLink ? ` Sélectionnez une autre option d’exportation pour modifier les comptes disponibles.` : ''}`, lastUpdated: 'Dernière mise à jour', transactionStartDate: 'Date de début de la transaction', + bulkUpdateTransactionStartDate: 'Mettre à jour la date de début de la transaction', updateCard: 'Mettre à jour la carte', unassignCard: 'Retirer l’assignation de la carte', unassignCards: 'Retirer l’assignation des cartes', diff --git a/src/languages/it.ts b/src/languages/it.ts index a8a4651a443b..d2d1113057b3 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -6583,6 +6583,7 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. `Scegli il conto ${integration} in cui esportare le transazioni.${exportPageLink ? ` Seleziona un’altra opzione di esportazione per modificare i conti disponibili.` : ''}`, lastUpdated: 'Ultimo aggiornamento', transactionStartDate: 'Data di inizio transazione', + bulkUpdateTransactionStartDate: 'Aggiorna data di inizio transazione', updateCard: 'Aggiorna carta', unassignCard: 'Rimuovi carta', unassignCards: 'Rimuovi carte', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 2f1660e46af6..99a1ccf398b1 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -6495,6 +6495,7 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO `取引をエクスポートする${integration}アカウントを選択してください。${exportPageLink ? `利用可能なアカウントを変更するには、別のエクスポートオプションを選択してください。` : ''}`, lastUpdated: '最終更新日時', transactionStartDate: '取引開始日', + bulkUpdateTransactionStartDate: '取引開始日を更新', updateCard: 'カードを更新', unassignCard: 'カードの割り当てを解除', unassignCards: 'カードの割り当てを解除', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 416aed362a28..d9a852a9d8fa 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -6569,6 +6569,7 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ `Kies de ${integration}-rekening waarnaar transacties moeten worden geëxporteerd.${exportPageLink ? ` Selecteer een andere exportoptie om de beschikbare rekeningen te wijzigen.` : ''}`, lastUpdated: 'Laatst bijgewerkt', transactionStartDate: 'Begindatum transactie', + bulkUpdateTransactionStartDate: 'Begindatum transactie bijwerken', updateCard: 'Kaart bijwerken', unassignCard: 'Kaart loskoppelen', unassignCards: 'Kaarten loskoppelen', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 8a04e87cbe79..7256b02e5db1 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -6594,6 +6594,7 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy `Wybierz konto ${integration}, do którego mają być eksportowane transakcje.${exportPageLink ? ` Wybierz inną opcję eksportu, aby zmienić dostępne konta.` : ''}`, lastUpdated: 'Ostatnia aktualizacja', transactionStartDate: 'Data początkowa transakcji', + bulkUpdateTransactionStartDate: 'Zaktualizuj datę początkową transakcji', updateCard: 'Zaktualizuj kartę', unassignCard: 'Cofnij przypisanie karty', unassignCards: 'Cofnij przypisanie kart', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 283c4ecb5cf4..812ff0d36b4f 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -6572,6 +6572,7 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS `Escolha a conta do ${integration} para a qual as transações devem ser exportadas.${exportPageLink ? ` Selecione uma opção de exportação diferente para alterar as contas disponíveis.` : ''}`, lastUpdated: 'Última atualização', transactionStartDate: 'Data de início da transação', + bulkUpdateTransactionStartDate: 'Atualizar data de início da transação', updateCard: 'Atualizar cartão', unassignCard: 'Remover cartão', unassignCards: 'Remover cartões', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 2fc7a5ca7435..2d77cf53fb7f 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -6335,6 +6335,7 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM `选择要导出交易的 ${integration} 账户。${exportPageLink ? `选择其他导出选项以更改可用账户。` : ''}`, lastUpdated: '最近更新', transactionStartDate: '交易开始日期', + bulkUpdateTransactionStartDate: '更新交易开始日期', updateCard: '更新卡片', unassignCard: '取消分配卡片', unassignCards: '取消分配卡片', From a18dd334e9e40c9c35c6a6ed9b9e8a4758cdf592 Mon Sep 17 00:00:00 2001 From: Hans Date: Thu, 24 Sep 2026 13:21:30 +0700 Subject: [PATCH 03/28] Use Bulk API instead --- .../WorkspaceCompanyCardsTableControls.tsx | 13 +++-- src/languages/de.ts | 3 ++ src/languages/el.ts | 3 ++ src/languages/en.ts | 2 + src/languages/es.ts | 3 ++ src/languages/fr.ts | 3 ++ src/languages/it.ts | 3 ++ src/languages/ja.ts | 2 + src/languages/nl.ts | 3 ++ src/languages/pl.ts | 3 ++ src/languages/pt-BR.ts | 3 ++ src/languages/zh-hans.ts | 2 + ...ulkUpdateCardTransactionStartDateParams.ts | 7 +++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + .../Navigation/linkingConfig/OldRoutes.ts | 1 + src/libs/actions/CompanyCards.ts | 48 +++++++++++++++++++ ...yCardsBulkEditTransactionStartDatePage.tsx | 18 +++++-- 18 files changed, 108 insertions(+), 12 deletions(-) create mode 100644 src/libs/API/parameters/BulkUpdateCardTransactionStartDateParams.ts diff --git a/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls.tsx b/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls.tsx index fd84c507cb8b..046a3abcd7ec 100644 --- a/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls.tsx +++ b/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls.tsx @@ -162,7 +162,6 @@ function WorkspaceCompanyCardsTableControls({ if (selectedCardIDs.length === 0) { return; } - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_BULK_EDIT_TRANSACTION_START_DATE.getRoute(policyID, feedName, selectedCardIDs)); clearCardSelection(); }; @@ -172,12 +171,6 @@ function WorkspaceCompanyCardsTableControls({ if (isOnlyAssignedCardsSelected) { if (canWriteCompanyCards) { - options.push({ - icon: icons.CalendarSolid, - text: translate('workspace.moreFeatures.companyCards.bulkUpdateTransactionStartDate'), - value: 'editTransactionStartDate', - onSelected: editSelectedCardTransactionStartDates, - }); options.push({ icon: icons.RemoveMembers, text: translate(isSingleAssignedCard ? 'workspace.moreFeatures.companyCards.unassignCard' : 'workspace.moreFeatures.companyCards.unassignCards'), @@ -185,6 +178,12 @@ function WorkspaceCompanyCardsTableControls({ shouldSkipFocusRestore: !!bankName, onSelected: confirmBulkUnassign, }); + options.push({ + icon: icons.CalendarSolid, + text: translate('workspace.moreFeatures.companyCards.bulkUpdateTransactionStartDate'), + value: 'editTransactionStartDate', + onSelected: editSelectedCardTransactionStartDates, + }); } options.push({ diff --git a/src/languages/de.ts b/src/languages/de.ts index a7a8e4c09a3a..78d84d085872 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -6283,6 +6283,9 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU startDateDescription: 'Wähle dein Import-Startdatum. Wir synchronisieren alle Transaktionen ab diesem Datum.', editStartDateDescription: 'Wähle ein neues Startdatum für Transaktionen. Wir synchronisieren alle Transaktionen ab diesem Datum, mit Ausnahme derer, die wir bereits importiert haben.', + bulkEditStartDateDescription: + 'Wähle ein Startdatum für Transaktionen für alle ausgewählten Karten. Wir synchronisieren Transaktionen ab diesem Datum, mit Ausnahme derer, die wir bereits importiert haben.', + bulkStartDateUpdated: 'Startdatum für Transaktionen aktualisiert', fromTheBeginning: 'Von Anfang an', customStartDate: 'Benutzerdefiniertes Startdatum', customCloseDate: 'Benutzerdefiniertes Abschlussdatum', diff --git a/src/languages/el.ts b/src/languages/el.ts index 62b35c4780bc..b440d5092042 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -6394,6 +6394,9 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ startDateDescription: 'Επιλέξτε την ημερομηνία έναρξης εισαγωγής. Θα συγχρονίσουμε όλες τις συναλλαγές από αυτήν την ημερομηνία και μετά.', editStartDateDescription: 'Επιλέξτε μια νέα ημερομηνία έναρξης συναλλαγών. Θα συγχρονίσουμε όλες τις συναλλαγές από αυτήν την ημερομηνία και μετά, εξαιρώντας όσες έχουμε ήδη εισαγάγει.', + bulkEditStartDateDescription: + 'Επιλέξτε ημερομηνία έναρξης συναλλαγών για όλες τις επιλεγμένες κάρτες. Θα συγχρονίσουμε τις συναλλαγές από αυτήν την ημερομηνία και μετά, εξαιρώντας όσες έχουμε ήδη εισαγάγει.', + bulkStartDateUpdated: 'Η ημερομηνία έναρξης συναλλαγών ενημερώθηκε', fromTheBeginning: 'Από την αρχή', customStartDate: 'Προσαρμοσμένη ημερομηνία έναρξης', customCloseDate: 'Προσαρμοσμένη ημερομηνία λήξης', diff --git a/src/languages/en.ts b/src/languages/en.ts index e9fa3a498098..d6888866983b 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -6461,6 +6461,8 @@ const translations = { chooseTransactionStartDate: 'Choose a transaction start date', startDateDescription: "Choose your import start date. We'll sync all transactions from this date onwards.", editStartDateDescription: "Choose a new transaction start date. We'll sync all transactions from that date onwards, excluding those we already imported.", + bulkEditStartDateDescription: "Choose a transaction start date for all selected cards. We'll sync transactions from that date onwards, excluding those we already imported.", + bulkStartDateUpdated: 'Transaction start date updated', fromTheBeginning: 'From the beginning', customStartDate: 'Custom start date', customCloseDate: 'Custom close date', diff --git a/src/languages/es.ts b/src/languages/es.ts index b0128094d7d0..7ec836f3e8f7 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -6151,6 +6151,9 @@ ${amount} para ${merchant} - ${date}`, startDateDescription: 'Importaremos todas las transacciones desde esta fecha en adelante. Si no se especifica una fecha, iremos tan atrás como lo permita tu banco.', editStartDateDescription: 'Elige una nueva fecha de inicio de transacciones. Sincronizaremos todas las transacciones a partir de esa fecha, excepto las que ya hayamos importado.', + bulkEditStartDateDescription: + 'Elige una fecha de inicio de transacciones para todas las tarjetas seleccionadas. Sincronizaremos las transacciones a partir de esa fecha, excepto las que ya hayamos importado.', + bulkStartDateUpdated: 'Fecha de inicio de transacciones actualizada', fromTheBeginning: 'Desde el principio', customStartDate: 'Fecha de inicio personalizada', customCloseDate: 'Fecha de cierre personalizada', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index d1eeab9ebe6f..4f9bd0c8d288 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -6288,6 +6288,9 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. startDateDescription: 'Choisissez votre date de début d’importation. Nous synchroniserons toutes les transactions à partir de cette date.', editStartDateDescription: 'Choisissez une nouvelle date de début de transaction. Nous synchroniserons toutes les transactions à partir de cette date, en excluant celles que nous avons déjà importées.', + bulkEditStartDateDescription: + 'Choisissez une date de début de transaction pour toutes les cartes sélectionnées. Nous synchroniserons les transactions à partir de cette date, en excluant celles que nous avons déjà importées.', + bulkStartDateUpdated: 'Date de début de transaction mise à jour', fromTheBeginning: 'Depuis le début', customStartDate: 'Date de début personnalisée', customCloseDate: 'Date de clôture personnalisée', diff --git a/src/languages/it.ts b/src/languages/it.ts index d2d1113057b3..c3963d178993 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -6238,6 +6238,9 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. chooseTransactionStartDate: 'Scegli una data di inizio transazione', startDateDescription: 'Scegli la data di inizio per l’importazione. Sincronizzeremo tutte le transazioni da questa data in poi.', editStartDateDescription: 'Scegli una nuova data di inizio transazione. Sincronizzeremo tutte le transazioni da quella data in poi, escludendo quelle già importate.', + bulkEditStartDateDescription: + 'Scegli una data di inizio transazione per tutte le carte selezionate. Sincronizzeremo le transazioni da quella data in poi, escludendo quelle già importate.', + bulkStartDateUpdated: 'Data di inizio transazione aggiornata', fromTheBeginning: "Dall'inizio", customStartDate: 'Data di inizio personalizzata', customCloseDate: 'Data di chiusura personalizzata', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 99a1ccf398b1..78e22cda2ee8 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -6169,6 +6169,8 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO chooseTransactionStartDate: '取引の開始日を選択', startDateDescription: 'インポート開始日を選択してください。この日付以降のすべての取引を同期します。', editStartDateDescription: '新しい取引開始日を選択してください。その日以降のすべての取引を、すでに取り込んだものを除いて同期します。', + bulkEditStartDateDescription: '選択したすべてのカードの取引開始日を選択してください。その日以降の取引を、すでに取り込んだものを除いて同期します。', + bulkStartDateUpdated: '取引開始日を更新しました', fromTheBeginning: '最初から', customStartDate: 'カスタム開始日', customCloseDate: 'カスタム締め日', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index d9a852a9d8fa..d4443da3151e 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -6233,6 +6233,9 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ startDateDescription: 'Kies je importstartdatum. We synchroniseren alle transacties vanaf deze datum.', editStartDateDescription: 'Kies een nieuwe startdatum voor transacties. We synchroniseren alle transacties vanaf die datum, met uitzondering van transacties die we al hebben geïmporteerd.', + bulkEditStartDateDescription: + 'Kies een startdatum voor transacties voor alle geselecteerde kaarten. We synchroniseren transacties vanaf die datum, met uitzondering van transacties die we al hebben geïmporteerd.', + bulkStartDateUpdated: 'Startdatum voor transacties bijgewerkt', fromTheBeginning: 'Vanaf het begin', customStartDate: 'Aangepaste startdatum', customCloseDate: 'Aangepaste sluitingsdatum', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 7256b02e5db1..4feea9dbc8bc 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -6258,6 +6258,9 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy chooseTransactionStartDate: 'Wybierz datę początkową transakcji', startDateDescription: 'Wybierz datę rozpoczęcia importu. Zsynchronizujemy wszystkie transakcje od tego dnia.', editStartDateDescription: 'Wybierz nową datę początkową transakcji. Zsynchronizujemy wszystkie transakcje od tej daty wzwyż, z wyłączeniem tych, które już zaimportowaliśmy.', + bulkEditStartDateDescription: + 'Wybierz datę początkową transakcji dla wszystkich wybranych kart. Zsynchronizujemy transakcje od tej daty wzwyż, z wyłączeniem tych, które już zaimportowaliśmy.', + bulkStartDateUpdated: 'Zaktualizowano datę początkową transakcji', fromTheBeginning: 'Od początku', customStartDate: 'Niestandardowa data początkowa', customCloseDate: 'Niestandardowa data zamknięcia', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 812ff0d36b4f..4735b414a597 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -6233,6 +6233,9 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS chooseTransactionStartDate: 'Escolha uma data de início da transação', startDateDescription: 'Escolha a data inicial de importação. Vamos sincronizar todas as transações a partir desta data.', editStartDateDescription: 'Escolha uma nova data de início de transações. Vamos sincronizar todas as transações a partir dessa data, excluindo as que já importamos.', + bulkEditStartDateDescription: + 'Escolha uma data de início de transações para todos os cartões selecionados. Vamos sincronizar as transações a partir dessa data, excluindo as que já importamos.', + bulkStartDateUpdated: 'Data de início de transações atualizada', fromTheBeginning: 'Desde o início', customStartDate: 'Data de início personalizada', customCloseDate: 'Data de fechamento personalizada', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 2d77cf53fb7f..c0fe2fe53198 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -6026,6 +6026,8 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM chooseTransactionStartDate: '选择交易开始日期', startDateDescription: '选择导入起始日期。我们会从该日期起同步所有交易。', editStartDateDescription: '选择新的交易起始日期。我们会从该日期开始同步所有交易,但不包括已经导入的交易。', + bulkEditStartDateDescription: '为所有选中的卡片选择交易起始日期。我们会从该日期开始同步交易,但不包括已经导入的交易。', + bulkStartDateUpdated: '交易起始日期已更新', fromTheBeginning: '从头开始', customStartDate: '自定义开始日期', customCloseDate: '自定义关闭日期', diff --git a/src/libs/API/parameters/BulkUpdateCardTransactionStartDateParams.ts b/src/libs/API/parameters/BulkUpdateCardTransactionStartDateParams.ts new file mode 100644 index 000000000000..80278df42a21 --- /dev/null +++ b/src/libs/API/parameters/BulkUpdateCardTransactionStartDateParams.ts @@ -0,0 +1,7 @@ +type BulkUpdateCardTransactionStartDateParams = { + /** JSON-encoded array of card IDs */ + cardIDs: string; + startDate: string; +}; + +export default BulkUpdateCardTransactionStartDateParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index c81eb85df926..dc73c61a9240 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -520,6 +520,7 @@ export type {default as UnassignCardParams} from './UnassignCardParams'; export type {default as SyncCardParams} from './SyncCardParams'; export type {default as UpdateCompanyCardNameParams} from './UpdateCompanyCardNameParams'; export type {default as UpdateCardTransactionStartDateParams} from './UpdateCardTransactionStartDateParams'; +export type {default as BulkUpdateCardTransactionStartDateParams} from './BulkUpdateCardTransactionStartDateParams'; export type {default as SetPersonalCardReimbursableParams} from './SetPersonalCardReimbursableParams'; export type {default as SetCompanyCardExportAccountParams} from './SetCompanyCardExportAccountParams'; export type {default as SetPersonalDetailsAndShipExpensifyCardsParams} from './SetPersonalDetailsAndShipExpensifyCardsParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 0c2c603ca395..010609c43f89 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -649,6 +649,7 @@ const WRITE_COMMANDS = { SYNC_CARD: 'SyncCard', UPDATE_COMPANY_CARD_NAME: 'SetCardName', UPDATE_CARD_TRANSACTION_START_DATE: 'UpdateCardTransactionStartDate', + BULK_UPDATE_CARD_TRANSACTION_START_DATE: 'BulkUpdateCardTransactionStartDate', SET_PERSONAL_CARD_REIMBURSABLE: 'SetPersonalCardReimbursable', SET_CARD_EXPORT_ACCOUNT: 'SetCardExportAccount', SET_PERSONAL_DETAILS_AND_SHIP_EXPENSIFY_CARDS: 'SetPersonalDetailsAndShipExpensifyCards', @@ -811,6 +812,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SYNC_CARD]: Parameters.SyncCardParams; [WRITE_COMMANDS.UPDATE_COMPANY_CARD_NAME]: Parameters.UpdateCompanyCardNameParams; [WRITE_COMMANDS.UPDATE_CARD_TRANSACTION_START_DATE]: Parameters.UpdateCardTransactionStartDateParams; + [WRITE_COMMANDS.BULK_UPDATE_CARD_TRANSACTION_START_DATE]: Parameters.BulkUpdateCardTransactionStartDateParams; [WRITE_COMMANDS.SET_PERSONAL_CARD_REIMBURSABLE]: Parameters.SetPersonalCardReimbursableParams; [WRITE_COMMANDS.SET_CARD_EXPORT_ACCOUNT]: Parameters.SetCompanyCardExportAccountParams; [WRITE_COMMANDS.SET_COMPANY_CARD_TRANSACTION_LIABILITY]: Parameters.SetCompanyCardTransactionLiability; diff --git a/src/libs/Navigation/linkingConfig/OldRoutes.ts b/src/libs/Navigation/linkingConfig/OldRoutes.ts index c3911599856f..325d5b3c720e 100644 --- a/src/libs/Navigation/linkingConfig/OldRoutes.ts +++ b/src/libs/Navigation/linkingConfig/OldRoutes.ts @@ -118,6 +118,7 @@ const oldRoutes: Record = { '/workspaces/*/company-cards/*/refresh-card-feed-connection': '/workspaces/$1/company-cards/$2/refresh-card-feed-connection', '/workspaces/*/company-cards/*/work-email': '/workspaces/$1/company-cards/$2/work-email', '/workspaces/*/company-cards/*/verify-work-email': '/workspaces/$1/company-cards/$2/verify-work-email', + '/workspaces/*/company-cards/*/bulk-edit/transaction-start-date': '/workspaces/$1/company-cards/$2/bulk-edit/transaction-start-date', '/workspaces/*/company-cards/settings/*': '/workspaces/$1/company-cards/settings/$2', '/workspaces/*/company-cards/add-card-feed/*': '/workspaces/$1/company-cards/add-card-feed/$2', '/workspaces/*/company-cards/select-feed/*': '/workspaces/$1/company-cards/select-feed/$2', diff --git a/src/libs/actions/CompanyCards.ts b/src/libs/actions/CompanyCards.ts index a402b12f3d9b..ee7a460d6c78 100644 --- a/src/libs/actions/CompanyCards.ts +++ b/src/libs/actions/CompanyCards.ts @@ -5,6 +5,7 @@ import type {CombinedCardFeeds} from '@hooks/useCardFeeds'; import * as API from '@libs/API'; import type { AssignCompanyCardParams, + BulkUpdateCardTransactionStartDateParams, GetExpensifyCardStatementPDFParams, ImportCSVCompanyCardsParams, OpenPolicyAddCardFeedPageParams, @@ -905,6 +906,52 @@ function updateCardTransactionStartDate(domainOrWorkspaceAccountID: number, card }); } +function bulkUpdateCardTransactionStartDate( + domainOrWorkspaceAccountID: number, + bankName: CompanyCardFeedWithNumber, + cards: Array<{cardID: string; oldStartDate?: string}>, + newStartDate: string, +) { + const optimisticCards: Record> = {}; + const finallyCards: Record> = {}; + const failureCards: Record> = {}; + for (const {cardID, oldStartDate} of cards) { + optimisticCards[cardID] = { + scrapeMinDate: newStartDate, + pendingFields: { + scrapeMinDate: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + errorFields: {scrapeMinDate: null}, + }; + finallyCards[cardID] = { + pendingFields: {scrapeMinDate: null}, + }; + failureCards[cardID] = { + scrapeMinDate: oldStartDate, + pendingFields: {scrapeMinDate: null}, + errorFields: { + scrapeMinDate: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + }, + }; + } + + const key = `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${domainOrWorkspaceAccountID}_${bankName}` as const; + const optimisticData: Array> = [{onyxMethod: Onyx.METHOD.MERGE, key, value: optimisticCards}]; + const finallyData: Array> = [{onyxMethod: Onyx.METHOD.MERGE, key, value: finallyCards}]; + const failureData: Array> = [{onyxMethod: Onyx.METHOD.MERGE, key, value: failureCards}]; + + const parameters: BulkUpdateCardTransactionStartDateParams = { + cardIDs: JSON.stringify(cards.map(({cardID}) => Number(cardID))), + startDate: newStartDate, + }; + + API.write(WRITE_COMMANDS.BULK_UPDATE_CARD_TRANSACTION_START_DATE, parameters, { + optimisticData, + finallyData, + failureData, + }); +} + function setCompanyCardExportAccount(policyID: string, domainOrWorkspaceAccountID: number, cardID: string, accountKey: string, newAccount: string, bank: CompanyCardFeedWithNumber) { const optimisticData: Array> = [ { @@ -1486,6 +1533,7 @@ export { updateWorkspaceCompanyCard, updateCompanyCardName, updateCardTransactionStartDate, + bulkUpdateCardTransactionStartDate, setCompanyCardExportAccount, clearCompanyCardErrorField, setAddNewCompanyCardStepAndData, diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx index f037e39d81fa..961ad7c817a5 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx @@ -13,6 +13,7 @@ import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; import {getCompanyCardFeed, getCompanyFeeds, getDomainOrWorkspaceAccountID} from '@libs/CardUtils'; +import Growl from '@libs/Growl'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import {isRequiredFulfilled} from '@libs/ValidationUtils'; @@ -21,7 +22,7 @@ import Navigation from '@navigation/Navigation'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import {updateCardTransactionStartDate} from '@userActions/CompanyCards'; +import {bulkUpdateCardTransactionStartDate} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -74,10 +75,17 @@ function WorkspaceCompanyCardsBulkEditTransactionStartDatePage({route}: Workspac } const newStartDate = dateOptionSelected === CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING ? '' : startDate; - for (const cardID of cardIDs) { - updateCardTransactionStartDate(domainOrWorkspaceAccountID, cardID, newStartDate, bank, allBankCards?.[cardID]?.scrapeMinDate); - } + bulkUpdateCardTransactionStartDate( + domainOrWorkspaceAccountID, + bank, + cardIDs.map((cardID) => ({ + cardID, + oldStartDate: allBankCards?.[cardID]?.scrapeMinDate, + })), + newStartDate, + ); goBackToCompanyCards(); + Growl.success(translate('workspace.companyCards.bulkStartDateUpdated')); }; const dateOptions = [ @@ -110,7 +118,7 @@ function WorkspaceCompanyCardsBulkEditTransactionStartDatePage({route}: Workspac title={translate('workspace.moreFeatures.companyCards.transactionStartDate')} onBackButtonPress={goBackToCompanyCards} /> - {translate('workspace.companyCards.editStartDateDescription')} + {translate('workspace.companyCards.bulkEditStartDateDescription')} Date: Thu, 24 Sep 2026 13:48:54 +0700 Subject: [PATCH 04/28] Add bottom right position to Growl --- src/CONST/index.ts | 4 +++ .../GrowlNotificationContainer/types.ts | 2 +- .../GrowlNotificationContent.tsx | 7 ++-- src/components/GrowlNotification/index.tsx | 8 +++-- src/libs/Growl.ts | 34 ++++++++++++++----- src/libs/actions/CompanyCards.ts | 2 +- ...yCardsBulkEditTransactionStartDatePage.tsx | 5 +-- 7 files changed, 44 insertions(+), 18 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index d816013a9837..c2c1821b6d8b 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -4349,6 +4349,10 @@ const CONST = { SUCCESS: 'success', ERROR: 'error', WARNING: 'warning', + POSITION: { + TOP: 'top', + BOTTOM_RIGHT: 'bottomRight', + }, DURATION: 2000, DURATION_LONG: 3500, // Longer duration for growls with an actionable button (e.g. "View"), giving the user enough time to tap it. diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/types.ts b/src/components/GrowlNotification/GrowlNotificationContainer/types.ts index 57b636921b11..7a8b88231a52 100644 --- a/src/components/GrowlNotification/GrowlNotificationContainer/types.ts +++ b/src/components/GrowlNotification/GrowlNotificationContainer/types.ts @@ -9,7 +9,7 @@ type GrowlNotificationContainerProps = ChildrenProps & { /** Pixel offset that corresponds to the current "offscreen" position for the active anchor. */ inactiveY: number; - /** When true, position the growl at the bottom-right (wide screens with an action button). */ + /** When true, position the growl at the bottom-right on wide screens. */ useBottomPosition?: boolean; }; diff --git a/src/components/GrowlNotification/GrowlNotificationContent.tsx b/src/components/GrowlNotification/GrowlNotificationContent.tsx index 89951c9af8bb..9f6dc97c4e9d 100644 --- a/src/components/GrowlNotification/GrowlNotificationContent.tsx +++ b/src/components/GrowlNotification/GrowlNotificationContent.tsx @@ -8,7 +8,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import type {GrowlAction, GrowlType} from '@libs/Growl'; +import type {GrowlAction, GrowlPosition, GrowlType} from '@libs/Growl'; import CONST from '@src/CONST'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -33,6 +33,7 @@ type GrowlNotificationContentProps = { type: GrowlType; duration: number; action?: GrowlAction; + position?: GrowlPosition; /** Identifies this growl instance; passed back through onDismissed so the parent can ignore stale dismissals. */ nonce: number; @@ -48,7 +49,7 @@ type GrowlIconTypes = Record< } >; -function GrowlNotificationContent({bodyText, type, duration, action, nonce, onDismissed}: GrowlNotificationContentProps) { +function GrowlNotificationContent({bodyText, type, duration, action, position, nonce, onDismissed}: GrowlNotificationContentProps) { // Normalized: 0 = fully offscreen for the current anchor, 1 = fully visible. The container // multiplies this against the live `inactiveY`, so the offscreen position stays correct // even when the responsive layout flips after the growl is dismissed. @@ -63,7 +64,7 @@ function GrowlNotificationContent({bodyText, type, duration, action, nonce, onDi const icons = useMemoizedLazyExpensifyIcons(['Exclamation', 'Checkmark']); // Derived live so that resizing the window flips position + slide direction during display. - const useBottomPosition = !!action && !shouldUseNarrowLayout; + const useBottomPosition = (!!action || position === CONST.GROWL.POSITION.BOTTOM_RIGHT) && !shouldUseNarrowLayout; const inactiveY = useBottomPosition ? INACTIVE_OFFSET : INACTIVE_POSITION_Y; const types: GrowlIconTypes = { diff --git a/src/components/GrowlNotification/index.tsx b/src/components/GrowlNotification/index.tsx index aea3afdf456a..e0f0cf40163e 100644 --- a/src/components/GrowlNotification/index.tsx +++ b/src/components/GrowlNotification/index.tsx @@ -1,5 +1,5 @@ import {setIsReady} from '@libs/Growl'; -import type {GrowlAction, GrowlRef, GrowlType} from '@libs/Growl'; +import type {GrowlAction, GrowlPosition, GrowlRef, GrowlType} from '@libs/Growl'; import type {ForwardedRef} from 'react'; @@ -12,6 +12,7 @@ type GrowlContent = { type: GrowlType; duration: number; action?: GrowlAction; + position?: GrowlPosition; /** Bumped on every show() call so identical-args re-shows still remount and re-trigger the growl. */ nonce: number; @@ -29,8 +30,8 @@ type GrowlNotificationProps = { function GrowlNotification({ref}: GrowlNotificationProps) { const [content, setContent] = useState(null); - const show = (text: string, growlType: GrowlType, duration: number, action?: GrowlAction) => { - setContent((prev) => ({bodyText: text, type: growlType, duration, action, nonce: (prev?.nonce ?? 0) + 1})); + const show = (text: string, growlType: GrowlType, duration: number, action?: GrowlAction, position?: GrowlPosition) => { + setContent((prev) => ({bodyText: text, type: growlType, duration, action, position, nonce: (prev?.nonce ?? 0) + 1})); }; useImperativeHandle(ref, () => ({show}), [show]); @@ -56,6 +57,7 @@ function GrowlNotification({ref}: GrowlNotificationProps) { type={content.type} duration={content.duration} action={content.action} + position={content.position} nonce={content.nonce} onDismissed={handleDismissed} /> diff --git a/src/libs/Growl.ts b/src/libs/Growl.ts index d1a3066f0171..965ab1bdb56e 100644 --- a/src/libs/Growl.ts +++ b/src/libs/Growl.ts @@ -1,5 +1,7 @@ import CONST from '@src/CONST'; +import type {ValueOf} from 'type-fest'; + import React from 'react'; type GrowlAction = { @@ -9,9 +11,16 @@ type GrowlAction = { /** The set of growl variants the notification UI knows how to render. */ type GrowlType = typeof CONST.GROWL.SUCCESS | typeof CONST.GROWL.ERROR | typeof CONST.GROWL.WARNING; +type GrowlPosition = ValueOf; + +type GrowlOptions = { + duration?: number; + action?: GrowlAction; + position?: GrowlPosition; +}; type GrowlRef = { - show?: (bodyText: string, type: GrowlType, duration: number, action?: GrowlAction) => void; + show?: (bodyText: string, type: GrowlType, duration: number, action?: GrowlAction, position?: GrowlPosition) => void; }; const growlRef = React.createRef(); @@ -30,29 +39,38 @@ function setIsReady() { /** * Show the growl notification */ -function show(bodyText: string, type: GrowlType, duration?: number, action?: GrowlAction) { +function show(bodyText: string, type: GrowlType, duration?: number, action?: GrowlAction, position?: GrowlPosition) { // Default to a longer duration when there's an action button so users have time to tap it. const resolvedDuration = duration ?? (action ? CONST.GROWL.DURATION_WITH_ACTION : CONST.GROWL.DURATION); isReadyPromise.then(() => { if (!growlRef?.current?.show) { return; } - growlRef.current.show(bodyText, type, resolvedDuration, action); + growlRef.current.show(bodyText, type, resolvedDuration, action, position); }); } +function showWithOptions(bodyText: string, type: GrowlType, durationOrOptions?: number | GrowlOptions, legacyAction?: GrowlAction) { + if (typeof durationOrOptions === 'number' || durationOrOptions === undefined) { + show(bodyText, type, durationOrOptions, legacyAction); + return; + } + + show(bodyText, type, durationOrOptions.duration, durationOrOptions.action, durationOrOptions.position); +} + /** * Show error growl */ -function error(bodyText: string, duration?: number, action?: GrowlAction) { - show(bodyText, CONST.GROWL.ERROR, duration, action); +function error(bodyText: string, durationOrOptions?: number | GrowlOptions, action?: GrowlAction) { + showWithOptions(bodyText, CONST.GROWL.ERROR, durationOrOptions, action); } /** * Show success growl */ -function success(bodyText: string, duration?: number, action?: GrowlAction) { - show(bodyText, CONST.GROWL.SUCCESS, duration, action); +function success(bodyText: string, durationOrOptions?: number | GrowlOptions, action?: GrowlAction) { + showWithOptions(bodyText, CONST.GROWL.SUCCESS, durationOrOptions, action); } export default { @@ -61,6 +79,6 @@ export default { success, }; -export type {GrowlRef, GrowlAction, GrowlType}; +export type {GrowlRef, GrowlAction, GrowlOptions, GrowlPosition, GrowlType}; export {growlRef, setIsReady}; diff --git a/src/libs/actions/CompanyCards.ts b/src/libs/actions/CompanyCards.ts index ee7a460d6c78..3b26fe556153 100644 --- a/src/libs/actions/CompanyCards.ts +++ b/src/libs/actions/CompanyCards.ts @@ -945,7 +945,7 @@ function bulkUpdateCardTransactionStartDate( startDate: newStartDate, }; - API.write(WRITE_COMMANDS.BULK_UPDATE_CARD_TRANSACTION_START_DATE, parameters, { + return API.write(WRITE_COMMANDS.BULK_UPDATE_CARD_TRANSACTION_START_DATE, parameters, { optimisticData, finallyData, failureData, diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx index 961ad7c817a5..694dc3d20822 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx @@ -83,9 +83,10 @@ function WorkspaceCompanyCardsBulkEditTransactionStartDatePage({route}: Workspac oldStartDate: allBankCards?.[cardID]?.scrapeMinDate, })), newStartDate, - ); + ).then(() => { + Growl.success(translate('workspace.companyCards.bulkStartDateUpdated'), {position: CONST.GROWL.POSITION.BOTTOM_RIGHT}); + }); goBackToCompanyCards(); - Growl.success(translate('workspace.companyCards.bulkStartDateUpdated')); }; const dateOptions = [ From a62e883cdb9f09c6a0ad77e4f4f19e9a60408604 Mon Sep 17 00:00:00 2001 From: Hans Date: Thu, 24 Sep 2026 14:09:56 +0700 Subject: [PATCH 05/28] fix failing type check --- src/libs/Growl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Growl.ts b/src/libs/Growl.ts index 965ab1bdb56e..01c7d1d074a9 100644 --- a/src/libs/Growl.ts +++ b/src/libs/Growl.ts @@ -79,6 +79,6 @@ export default { success, }; -export type {GrowlRef, GrowlAction, GrowlOptions, GrowlPosition, GrowlType}; +export type {GrowlRef, GrowlAction, GrowlPosition, GrowlType}; export {growlRef, setIsReady}; From 6893edc7f0072f9e3ba49e99408b8db33b1b3877 Mon Sep 17 00:00:00 2001 From: Hans Date: Mon, 12 Oct 2026 10:48:33 +0700 Subject: [PATCH 06/28] Fix company card start date navigation --- .../WorkspaceCompanyCardsTableControls.tsx | 10 ++++++++-- ...aceCompanyCardsBulkEditTransactionStartDatePage.tsx | 10 +++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls.tsx b/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls.tsx index 899c154f7537..66226dadc44f 100644 --- a/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls.tsx +++ b/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls.tsx @@ -161,10 +161,16 @@ function WorkspaceCompanyCardsTableControls({ .map((card) => card.assignedCard?.cardID) .filter((cardID): cardID is number => cardID !== undefined) .map(String); - if (selectedCardIDs.length === 0) { + const [selectedCardID] = selectedCardIDs; + if (!selectedCardID) { return; } - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_BULK_EDIT_TRANSACTION_START_DATE.getRoute(policyID, feedName, selectedCardIDs)); + + if (selectedCardIDs.length === 1) { + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARD_EDIT_TRANSACTION_START_DATE.getRoute(policyID, selectedCardID, feedName)); + } else { + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_BULK_EDIT_TRANSACTION_START_DATE.getRoute(policyID, feedName, selectedCardIDs)); + } clearCardSelection(); }; diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx index 694dc3d20822..f010e425f730 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx @@ -1,6 +1,6 @@ import Button from '@components/Button'; import DatePicker from '@components/DatePicker'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import Header from '@components/Header'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem'; @@ -115,10 +115,10 @@ function WorkspaceCompanyCardsBulkEditTransactionStartDatePage({route}: Workspac testID="WorkspaceCompanyCardsBulkEditTransactionStartDatePage" enableEdgeToEdgeBottomSafeAreaPadding > - +
+ + +
{translate('workspace.companyCards.bulkEditStartDateDescription')} Date: Mon, 12 Oct 2026 10:59:38 +0700 Subject: [PATCH 07/28] Fix single-card bulk start date updates --- src/ROUTES.ts | 4 +-- .../WorkspaceCompanyCardsTableControls.tsx | 2 +- src/libs/Navigation/types.ts | 3 +- ...ompanyCardEditTransactionStartDatePage.tsx | 28 +++++++++++-------- 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index cf9172fa4eb6..1d28c3a2e4c0 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -3868,8 +3868,8 @@ const ROUTES = { }, WORKSPACE_COMPANY_CARD_EDIT_TRANSACTION_START_DATE: { route: 'workspaces/:policyID/company-cards/:feed/:cardID/edit/transaction-start-date', - getRoute: (policyID: string, cardID: string, feed: CompanyCardFeedWithDomainID) => - `workspaces/${policyID}/company-cards/${encodeURIComponent(feed)}/${encodeURIComponent(cardID)}/edit/transaction-start-date` as const, + getRoute: (policyID: string, cardID: string, feed: CompanyCardFeedWithDomainID, isBulkAction?: boolean) => + `workspaces/${policyID}/company-cards/${encodeURIComponent(feed)}/${encodeURIComponent(cardID)}/edit/transaction-start-date${isBulkAction ? '?isBulkAction=true' : ''}` as const, }, WORKSPACE_COMPANY_CARDS_BULK_EDIT_TRANSACTION_START_DATE: { route: 'workspaces/:policyID/company-cards/:feed/bulk-edit/transaction-start-date', diff --git a/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls.tsx b/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls.tsx index 66226dadc44f..81b954578016 100644 --- a/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls.tsx +++ b/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls.tsx @@ -167,7 +167,7 @@ function WorkspaceCompanyCardsTableControls({ } if (selectedCardIDs.length === 1) { - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARD_EDIT_TRANSACTION_START_DATE.getRoute(policyID, selectedCardID, feedName)); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARD_EDIT_TRANSACTION_START_DATE.getRoute(policyID, selectedCardID, feedName, true)); } else { Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_BULK_EDIT_TRANSACTION_START_DATE.getRoute(policyID, feedName, selectedCardIDs)); } diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index a458352c22f2..37c75626f926 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1660,8 +1660,9 @@ type SettingsNavigatorParamList = { }; [SCREENS.WORKSPACE.COMPANY_CARD_EDIT_TRANSACTION_START_DATE]: { policyID: string; - feed: string; + feed: CompanyCardFeedWithDomainID; cardID: string; + isBulkAction?: string; }; [SCREENS.WORKSPACE.COMPANY_CARDS_BULK_EDIT_TRANSACTION_START_DATE]: { policyID: string; diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardEditTransactionStartDatePage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardEditTransactionStartDatePage.tsx index ef3e2833d584..d79aebf80073 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardEditTransactionStartDatePage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardEditTransactionStartDatePage.tsx @@ -1,6 +1,6 @@ import Button from '@components/Button'; import DatePicker from '@components/DatePicker'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import Header from '@components/Header'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem'; @@ -21,11 +21,11 @@ import Navigation from '@navigation/Navigation'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import {updateCardTransactionStartDate} from '@userActions/CompanyCards'; +import {bulkUpdateCardTransactionStartDate, updateCardTransactionStartDate} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {CompanyCardFeedWithDomainID} from '@src/types/onyx'; import type {ValueOf} from 'type-fest'; @@ -40,7 +40,8 @@ type WorkspaceCompanyCardEditTransactionStartDatePageProps = PlatformStackScreen function WorkspaceCompanyCardEditTransactionStartDatePage({route, navigation}: WorkspaceCompanyCardEditTransactionStartDatePageProps) { const {policyID, cardID} = route.params; - const feedName = decodeURIComponent(route.params.feed) as CompanyCardFeedWithDomainID; + const isBulkAction = route.params.isBulkAction === 'true'; + const feedName = route.params.feed; const bank = getCompanyCardFeed(feedName); const {translate} = useLocalize(); @@ -51,7 +52,8 @@ function WorkspaceCompanyCardEditTransactionStartDatePage({route, navigation}: W const [cardFeeds] = useCardFeeds(policyID); const companyFeeds = getCompanyFeeds(cardFeeds); const domainOrWorkspaceAccountID = getDomainOrWorkspaceAccountID(workspaceAccountID, companyFeeds[feedName]); - const goBackToCardDetails = () => Navigation.goBack(getCompanyCardDetailsBackPath(policyID, feedName, cardID, navigation.getState())); + const goBack = () => + Navigation.goBack(isBulkAction ? ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID) : getCompanyCardDetailsBackPath(policyID, feedName, cardID, navigation.getState())); const [allBankCards] = useCardsList(feedName); const card = allBankCards?.[cardID]; @@ -90,8 +92,12 @@ function WorkspaceCompanyCardEditTransactionStartDatePage({route, navigation}: W const newStartDate = dateOptionSelected === CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING ? '' : startDate; - updateCardTransactionStartDate(domainOrWorkspaceAccountID, cardID, newStartDate, bank, currentStartDate); - goBackToCardDetails(); + if (isBulkAction) { + bulkUpdateCardTransactionStartDate(domainOrWorkspaceAccountID, bank, [{cardID, oldStartDate: currentStartDate}], newStartDate); + } else { + updateCardTransactionStartDate(domainOrWorkspaceAccountID, cardID, newStartDate, bank, currentStartDate); + } + goBack(); }; const dateOptions = [ @@ -120,10 +126,10 @@ function WorkspaceCompanyCardEditTransactionStartDatePage({route, navigation}: W testID="WorkspaceCompanyCardEditTransactionStartDatePage" enableEdgeToEdgeBottomSafeAreaPadding > - +
+ + +
{translate('workspace.companyCards.editStartDateDescription')} Date: Mon, 12 Oct 2026 11:06:35 +0700 Subject: [PATCH 08/28] Use single-card editor for one-card bulk routes --- src/ROUTES.ts | 4 +-- .../WorkspaceCompanyCardsTableControls.tsx | 2 +- src/libs/Navigation/types.ts | 1 - ...ompanyCardEditTransactionStartDatePage.tsx | 17 ++++-------- ...yCardsBulkEditTransactionStartDatePage.tsx | 26 ++++++++++++++++--- 5 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 1d28c3a2e4c0..cf9172fa4eb6 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -3868,8 +3868,8 @@ const ROUTES = { }, WORKSPACE_COMPANY_CARD_EDIT_TRANSACTION_START_DATE: { route: 'workspaces/:policyID/company-cards/:feed/:cardID/edit/transaction-start-date', - getRoute: (policyID: string, cardID: string, feed: CompanyCardFeedWithDomainID, isBulkAction?: boolean) => - `workspaces/${policyID}/company-cards/${encodeURIComponent(feed)}/${encodeURIComponent(cardID)}/edit/transaction-start-date${isBulkAction ? '?isBulkAction=true' : ''}` as const, + getRoute: (policyID: string, cardID: string, feed: CompanyCardFeedWithDomainID) => + `workspaces/${policyID}/company-cards/${encodeURIComponent(feed)}/${encodeURIComponent(cardID)}/edit/transaction-start-date` as const, }, WORKSPACE_COMPANY_CARDS_BULK_EDIT_TRANSACTION_START_DATE: { route: 'workspaces/:policyID/company-cards/:feed/bulk-edit/transaction-start-date', diff --git a/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls.tsx b/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls.tsx index 81b954578016..66226dadc44f 100644 --- a/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls.tsx +++ b/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls.tsx @@ -167,7 +167,7 @@ function WorkspaceCompanyCardsTableControls({ } if (selectedCardIDs.length === 1) { - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARD_EDIT_TRANSACTION_START_DATE.getRoute(policyID, selectedCardID, feedName, true)); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARD_EDIT_TRANSACTION_START_DATE.getRoute(policyID, selectedCardID, feedName)); } else { Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_BULK_EDIT_TRANSACTION_START_DATE.getRoute(policyID, feedName, selectedCardIDs)); } diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 37c75626f926..64dc3cbe9b33 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1662,7 +1662,6 @@ type SettingsNavigatorParamList = { policyID: string; feed: CompanyCardFeedWithDomainID; cardID: string; - isBulkAction?: string; }; [SCREENS.WORKSPACE.COMPANY_CARDS_BULK_EDIT_TRANSACTION_START_DATE]: { policyID: string; diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardEditTransactionStartDatePage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardEditTransactionStartDatePage.tsx index d79aebf80073..588e8c7868f3 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardEditTransactionStartDatePage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardEditTransactionStartDatePage.tsx @@ -21,10 +21,9 @@ import Navigation from '@navigation/Navigation'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import {bulkUpdateCardTransactionStartDate, updateCardTransactionStartDate} from '@userActions/CompanyCards'; +import {updateCardTransactionStartDate} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {ValueOf} from 'type-fest'; @@ -40,7 +39,6 @@ type WorkspaceCompanyCardEditTransactionStartDatePageProps = PlatformStackScreen function WorkspaceCompanyCardEditTransactionStartDatePage({route, navigation}: WorkspaceCompanyCardEditTransactionStartDatePageProps) { const {policyID, cardID} = route.params; - const isBulkAction = route.params.isBulkAction === 'true'; const feedName = route.params.feed; const bank = getCompanyCardFeed(feedName); @@ -52,8 +50,7 @@ function WorkspaceCompanyCardEditTransactionStartDatePage({route, navigation}: W const [cardFeeds] = useCardFeeds(policyID); const companyFeeds = getCompanyFeeds(cardFeeds); const domainOrWorkspaceAccountID = getDomainOrWorkspaceAccountID(workspaceAccountID, companyFeeds[feedName]); - const goBack = () => - Navigation.goBack(isBulkAction ? ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID) : getCompanyCardDetailsBackPath(policyID, feedName, cardID, navigation.getState())); + const goBackToCardDetails = () => Navigation.goBack(getCompanyCardDetailsBackPath(policyID, feedName, cardID, navigation.getState())); const [allBankCards] = useCardsList(feedName); const card = allBankCards?.[cardID]; @@ -92,12 +89,8 @@ function WorkspaceCompanyCardEditTransactionStartDatePage({route, navigation}: W const newStartDate = dateOptionSelected === CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING ? '' : startDate; - if (isBulkAction) { - bulkUpdateCardTransactionStartDate(domainOrWorkspaceAccountID, bank, [{cardID, oldStartDate: currentStartDate}], newStartDate); - } else { - updateCardTransactionStartDate(domainOrWorkspaceAccountID, cardID, newStartDate, bank, currentStartDate); - } - goBack(); + updateCardTransactionStartDate(domainOrWorkspaceAccountID, cardID, newStartDate, bank, currentStartDate); + goBackToCardDetails(); }; const dateOptions = [ @@ -127,7 +120,7 @@ function WorkspaceCompanyCardEditTransactionStartDatePage({route, navigation}: W enableEdgeToEdgeBottomSafeAreaPadding >
- +
{translate('workspace.companyCards.editStartDateDescription')} diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx index f010e425f730..2b96dd90b921 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx @@ -26,13 +26,14 @@ import {bulkUpdateCardTransactionStartDate} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import type SCREENS from '@src/SCREENS'; +import SCREENS from '@src/SCREENS'; import type {CompanyCardFeedWithDomainID} from '@src/types/onyx'; import type {ValueOf} from 'type-fest'; +import {StackActions} from '@react-navigation/native'; import {format} from 'date-fns'; -import React, {useState} from 'react'; +import React, {useEffect, useState} from 'react'; import {View} from 'react-native'; type DateOption = ValueOf; @@ -41,10 +42,11 @@ type WorkspaceCompanyCardsBulkEditTransactionStartDatePageProps = PlatformStackS typeof SCREENS.WORKSPACE.COMPANY_CARDS_BULK_EDIT_TRANSACTION_START_DATE >; -function WorkspaceCompanyCardsBulkEditTransactionStartDatePage({route}: WorkspaceCompanyCardsBulkEditTransactionStartDatePageProps) { +function WorkspaceCompanyCardsBulkEditTransactionStartDatePage({route, navigation}: WorkspaceCompanyCardsBulkEditTransactionStartDatePageProps) { const {policyID} = route.params; const feedName: CompanyCardFeedWithDomainID = route.params.feed; const cardIDs = (route.params.cardIDs ?? '').split(',').filter(Boolean); + const singleCardID = cardIDs.length === 1 ? cardIDs.at(0) : undefined; const bank = getCompanyCardFeed(feedName); const {translate} = useLocalize(); @@ -61,6 +63,20 @@ function WorkspaceCompanyCardsBulkEditTransactionStartDatePage({route}: Workspac const [startDate, setStartDate] = useState(() => format(new Date(), CONST.DATE.FNS_FORMAT_STRING)); const [errorText, setErrorText] = useState(''); + useEffect(() => { + if (!singleCardID) { + return; + } + + navigation.dispatch( + StackActions.replace(SCREENS.WORKSPACE.COMPANY_CARD_EDIT_TRANSACTION_START_DATE, { + policyID, + cardID: singleCardID, + feed: feedName, + }), + ); + }, [feedName, navigation, policyID, singleCardID]); + const goBackToCompanyCards = () => Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); const handleSelectDateOption = (dateOption: DateOption) => { @@ -104,6 +120,10 @@ function WorkspaceCompanyCardsBulkEditTransactionStartDatePage({route}: Workspac }, ]; + if (singleCardID) { + return null; + } + return ( Date: Mon, 12 Oct 2026 11:09:02 +0700 Subject: [PATCH 09/28] Decode company card feed for start date edits --- .../WorkspaceCompanyCardEditTransactionStartDatePage.tsx | 5 ++++- ...WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardEditTransactionStartDatePage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardEditTransactionStartDatePage.tsx index 588e8c7868f3..0977cfa10f47 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardEditTransactionStartDatePage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardEditTransactionStartDatePage.tsx @@ -25,6 +25,7 @@ import {updateCardTransactionStartDate} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; import type SCREENS from '@src/SCREENS'; +import type {CompanyCardFeedWithDomainID} from '@src/types/onyx'; import type {ValueOf} from 'type-fest'; @@ -39,7 +40,9 @@ type WorkspaceCompanyCardEditTransactionStartDatePageProps = PlatformStackScreen function WorkspaceCompanyCardEditTransactionStartDatePage({route, navigation}: WorkspaceCompanyCardEditTransactionStartDatePageProps) { const {policyID, cardID} = route.params; - const feedName = route.params.feed; + // The linking layer can provide the path parameter with the feed separator still encoded. + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- decoding preserves the company-feed shape guaranteed by this route + const feedName = decodeURIComponent(route.params.feed) as CompanyCardFeedWithDomainID; const bank = getCompanyCardFeed(feedName); const {translate} = useLocalize(); diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx index 2b96dd90b921..458d6ae38669 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx @@ -44,7 +44,9 @@ type WorkspaceCompanyCardsBulkEditTransactionStartDatePageProps = PlatformStackS function WorkspaceCompanyCardsBulkEditTransactionStartDatePage({route, navigation}: WorkspaceCompanyCardsBulkEditTransactionStartDatePageProps) { const {policyID} = route.params; - const feedName: CompanyCardFeedWithDomainID = route.params.feed; + // The linking layer can provide the path parameter with the feed separator still encoded. + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- decoding preserves the company-feed shape guaranteed by this route + const feedName = decodeURIComponent(route.params.feed) as CompanyCardFeedWithDomainID; const cardIDs = (route.params.cardIDs ?? '').split(',').filter(Boolean); const singleCardID = cardIDs.length === 1 ? cardIDs.at(0) : undefined; const bank = getCompanyCardFeed(feedName); From 54db2348c61f67c7943cdda876a849150e9652f8 Mon Sep 17 00:00:00 2001 From: Hans Date: Mon, 12 Oct 2026 11:38:23 +0700 Subject: [PATCH 10/28] Handle failed updating case --- .../WorkspaceCompanyCardsTableRow.tsx | 16 +++++++++++---- .../WorkspaceCompanyCardsTable/index.tsx | 20 ++++++++++++++++--- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableRow.tsx b/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableRow.tsx index 37f31ddc3b21..1f32f2fd8cb0 100644 --- a/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableRow.tsx +++ b/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableRow.tsx @@ -12,7 +12,7 @@ import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {formatMaskedCardName} from '@libs/CardUtils'; +import {formatMaskedCardName, getCompanyCardFeed} from '@libs/CardUtils'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; @@ -20,8 +20,9 @@ import variables from '@styles/variables'; import CONST from '@src/CONST'; import {DYNAMIC_ROUTES} from '@src/ROUTES'; -import type {Card, CompanyCardFeed, CompanyCardFeedWithDomainID} from '@src/types/onyx'; +import type {Card, CompanyCardFeedWithDomainID} from '@src/types/onyx'; import type {CardAssignmentData} from '@src/types/onyx/Card'; +import {isEmptyValueObject} from '@src/types/utils/EmptyObject'; import {Str} from 'expensify-common'; import React from 'react'; @@ -82,10 +83,11 @@ function WorkspaceCompanyCardTableRow({ const theme = useTheme(); const styles = useThemeStyles(); const {translate, formatPhoneNumber} = useLocalize(); - const Expensicons = useMemoizedLazyExpensifyIcons(['ArrowRight']); + const Expensicons = useMemoizedLazyExpensifyIcons(['ArrowRight', 'DotIndicator']); const isTableSemanticsEnabled = shouldUseTableSemantics(shouldUseNarrowTableLayout); const {cardName, encryptedCardNumber, customCardName, cardholder, assignedCard, isAssigned, errors, pendingAction, isCardDeleted, onDismissError} = item; + const hasErrors = !isEmptyValueObject(errors ?? {}); const formattedCustomCardName = customCardName ?? ''; const formattedCardDetails = formatMaskedCardName(cardName); @@ -147,7 +149,7 @@ function WorkspaceCompanyCardTableRow({ {isAssigned ? ( + {hasErrors && ( + + )} {!isAssigned && canWriteCompanyCards && ( + } + listFooterContent={ + dateOptionSelected === CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM ? ( + + { + setErrorText(isRequiredFulfilled(value) ? '' : translate('common.error.fieldRequired')); + setStartDate(value); + }} + minDate={CONST.CALENDAR_PICKER.MIN_DATE} + errorText={errorText} + /> + + ) : null + } + /> +
+ + ); +} + +export default CompanyCardTransactionStartDateForm; diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardEditTransactionStartDatePage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardEditTransactionStartDatePage.tsx index 0977cfa10f47..f5ab9617ec18 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardEditTransactionStartDatePage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardEditTransactionStartDatePage.tsx @@ -1,21 +1,11 @@ -import Button from '@components/Button'; -import DatePicker from '@components/DatePicker'; -import Header from '@components/Header'; -import ScreenWrapper from '@components/ScreenWrapper'; -import SelectionList from '@components/SelectionList'; -import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem'; -import Text from '@components/Text'; - import useCardFeeds from '@hooks/useCardFeeds'; import useCardsList from '@hooks/useCardsList'; import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; -import useThemeStyles from '@hooks/useThemeStyles'; import {getCompanyCardFeed, getCompanyFeeds, getDomainOrWorkspaceAccountID} from '@libs/CardUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; -import {isRequiredFulfilled} from '@libs/ValidationUtils'; import Navigation from '@navigation/Navigation'; @@ -27,15 +17,12 @@ import CONST from '@src/CONST'; import type SCREENS from '@src/SCREENS'; import type {CompanyCardFeedWithDomainID} from '@src/types/onyx'; -import type {ValueOf} from 'type-fest'; - import {format, parseISO} from 'date-fns'; -import React, {useState} from 'react'; -import {View} from 'react-native'; +import React from 'react'; +import CompanyCardTransactionStartDateForm from './CompanyCardTransactionStartDateForm'; import {getCompanyCardDetailsBackPath} from './utils'; -type DateOption = ValueOf; type WorkspaceCompanyCardEditTransactionStartDatePageProps = PlatformStackScreenProps; function WorkspaceCompanyCardEditTransactionStartDatePage({route, navigation}: WorkspaceCompanyCardEditTransactionStartDatePageProps) { @@ -46,7 +33,6 @@ function WorkspaceCompanyCardEditTransactionStartDatePage({route, navigation}: W const bank = getCompanyCardFeed(feedName); const {translate} = useLocalize(); - const styles = useThemeStyles(); const policy = usePolicy(policyID); const workspaceAccountID = policy?.policyAccountID ?? CONST.DEFAULT_NUMBER_ID; @@ -59,58 +45,12 @@ function WorkspaceCompanyCardEditTransactionStartDatePage({route, navigation}: W const card = allBankCards?.[cardID]; const currentStartDate = card?.scrapeMinDate; const savedDateOption = currentStartDate ? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM : CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING; - - const [localDateOption, setLocalDateOption] = useState(undefined); - const dateOptionSelected = localDateOption ?? savedDateOption; - - const [startDate, setStartDate] = useState(() => { - if (currentStartDate) { - return format(parseISO(currentStartDate), CONST.DATE.FNS_FORMAT_STRING); - } - return format(new Date(), CONST.DATE.FNS_FORMAT_STRING); - }); - - const [errorText, setErrorText] = useState(''); - - const handleSelectDateOption = (dateOption: DateOption) => { - setErrorText(''); - setLocalDateOption(dateOption); - if (dateOption === CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING) { - return; - } - // Reset to current date when switching to custom - if (!currentStartDate) { - setStartDate(format(new Date(), CONST.DATE.FNS_FORMAT_STRING)); - } - }; - - const submit = () => { - if (dateOptionSelected === CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM && !isRequiredFulfilled(startDate)) { - setErrorText(translate('common.error.fieldRequired')); - return; - } - - const newStartDate = dateOptionSelected === CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING ? '' : startDate; - + const initialStartDate = currentStartDate ? format(parseISO(currentStartDate), CONST.DATE.FNS_FORMAT_STRING) : format(new Date(), CONST.DATE.FNS_FORMAT_STRING); + const submit = (newStartDate: string) => { updateCardTransactionStartDate(domainOrWorkspaceAccountID, cardID, newStartDate, bank, currentStartDate); goBackToCardDetails(); }; - const dateOptions = [ - { - value: CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING, - text: translate('workspace.companyCards.fromTheBeginning'), - keyForList: CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING, - isSelected: dateOptionSelected === CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING, - }, - { - value: CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM, - text: translate('workspace.companyCards.customStartDate'), - keyForList: CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM, - isSelected: dateOptionSelected === CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM, - }, - ]; - return ( - -
- - -
- {translate('workspace.companyCards.editStartDateDescription')} - - handleSelectDateOption(value)} - data={dateOptions} - shouldSingleExecuteRowSelect - initiallyFocusedItemKey={dateOptionSelected} - shouldUpdateFocusedIndex - addBottomSafeAreaPadding - footerContent={ - - } - listFooterContent={ - dateOptionSelected === CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM ? ( - - { - if (!isRequiredFulfilled(value)) { - setErrorText(translate('common.error.fieldRequired')); - } else { - setErrorText(''); - } - setStartDate(value); - }} - minDate={CONST.CALENDAR_PICKER.MIN_DATE} - errorText={errorText} - /> - - ) : null - } - /> - -
+ description={translate('workspace.companyCards.editStartDateDescription')} + initialDateOption={savedDateOption} + initialStartDate={initialStartDate} + onBackButtonPress={goBackToCardDetails} + onSubmit={submit} + shouldResetStartDateOnCustomSelection={!currentStartDate} + />
); } diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx index 458d6ae38669..da724f17b631 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx @@ -1,22 +1,12 @@ -import Button from '@components/Button'; -import DatePicker from '@components/DatePicker'; -import Header from '@components/Header'; -import ScreenWrapper from '@components/ScreenWrapper'; -import SelectionList from '@components/SelectionList'; -import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem'; -import Text from '@components/Text'; - import useCardFeeds from '@hooks/useCardFeeds'; import useCardsList from '@hooks/useCardsList'; import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; -import useThemeStyles from '@hooks/useThemeStyles'; import {getCompanyCardFeed, getCompanyFeeds, getDomainOrWorkspaceAccountID} from '@libs/CardUtils'; import Growl from '@libs/Growl'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; -import {isRequiredFulfilled} from '@libs/ValidationUtils'; import Navigation from '@navigation/Navigation'; @@ -29,14 +19,12 @@ import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import type {CompanyCardFeedWithDomainID} from '@src/types/onyx'; -import type {ValueOf} from 'type-fest'; - import {StackActions} from '@react-navigation/native'; import {format} from 'date-fns'; -import React, {useEffect, useState} from 'react'; -import {View} from 'react-native'; +import React, {useEffect} from 'react'; + +import CompanyCardTransactionStartDateForm from './CompanyCardTransactionStartDateForm'; -type DateOption = ValueOf; type WorkspaceCompanyCardsBulkEditTransactionStartDatePageProps = PlatformStackScreenProps< SettingsNavigatorParamList, typeof SCREENS.WORKSPACE.COMPANY_CARDS_BULK_EDIT_TRANSACTION_START_DATE @@ -52,7 +40,6 @@ function WorkspaceCompanyCardsBulkEditTransactionStartDatePage({route, navigatio const bank = getCompanyCardFeed(feedName); const {translate} = useLocalize(); - const styles = useThemeStyles(); const policy = usePolicy(policyID); const workspaceAccountID = policy?.policyAccountID ?? CONST.DEFAULT_NUMBER_ID; @@ -61,10 +48,6 @@ function WorkspaceCompanyCardsBulkEditTransactionStartDatePage({route, navigatio const domainOrWorkspaceAccountID = getDomainOrWorkspaceAccountID(workspaceAccountID, companyFeeds[feedName]); const [allBankCards] = useCardsList(feedName); - const [dateOptionSelected, setDateOptionSelected] = useState(CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM); - const [startDate, setStartDate] = useState(() => format(new Date(), CONST.DATE.FNS_FORMAT_STRING)); - const [errorText, setErrorText] = useState(''); - useEffect(() => { if (!singleCardID) { return; @@ -81,18 +64,7 @@ function WorkspaceCompanyCardsBulkEditTransactionStartDatePage({route, navigatio const goBackToCompanyCards = () => Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); - const handleSelectDateOption = (dateOption: DateOption) => { - setErrorText(''); - setDateOptionSelected(dateOption); - }; - - const submit = () => { - if (dateOptionSelected === CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM && !isRequiredFulfilled(startDate)) { - setErrorText(translate('common.error.fieldRequired')); - return; - } - - const newStartDate = dateOptionSelected === CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING ? '' : startDate; + const submit = (newStartDate: string) => { bulkUpdateCardTransactionStartDate( domainOrWorkspaceAccountID, bank, @@ -107,21 +79,6 @@ function WorkspaceCompanyCardsBulkEditTransactionStartDatePage({route, navigatio goBackToCompanyCards(); }; - const dateOptions = [ - { - value: CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING, - text: translate('workspace.companyCards.fromTheBeginning'), - keyForList: CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING, - isSelected: dateOptionSelected === CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING, - }, - { - value: CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM, - text: translate('workspace.companyCards.customStartDate'), - keyForList: CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM, - isSelected: dateOptionSelected === CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM, - }, - ]; - if (singleCardID) { return null; } @@ -133,59 +90,15 @@ function WorkspaceCompanyCardsBulkEditTransactionStartDatePage({route, navigatio policyFeature={CONST.POLICY.POLICY_FEATURE.COMPANY_CARDS} policyFeatureAccess={CONST.POLICY.POLICY_FEATURE_ACCESS.WRITE} > - -
- - -
- {translate('workspace.companyCards.bulkEditStartDateDescription')} - - handleSelectDateOption(value)} - data={dateOptions} - shouldSingleExecuteRowSelect - initiallyFocusedItemKey={dateOptionSelected} - shouldUpdateFocusedIndex - addBottomSafeAreaPadding - footerContent={ - - } - listFooterContent={ - dateOptionSelected === CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM ? ( - - { - if (!isRequiredFulfilled(value)) { - setErrorText(translate('common.error.fieldRequired')); - } else { - setErrorText(''); - } - setStartDate(value); - }} - minDate={CONST.CALENDAR_PICKER.MIN_DATE} - errorText={errorText} - /> - - ) : null - } - /> - -
+ description={translate('workspace.companyCards.bulkEditStartDateDescription')} + initialDateOption={CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM} + initialStartDate={format(new Date(), CONST.DATE.FNS_FORMAT_STRING)} + onBackButtonPress={goBackToCompanyCards} + onSubmit={submit} + isSaveDisabled={cardIDs.length === 0} + /> ); } From 059c367a986f9066e90a56d922350bd4dc6ab8ce Mon Sep 17 00:00:00 2001 From: Hans Date: Tue, 13 Oct 2026 09:44:52 +0700 Subject: [PATCH 14/28] Address bulk start date review feedback --- .../GrowlNotificationContainer/index.tsx | 18 ++--- .../GrowlNotificationContent.tsx | 2 + src/components/GrowlNotification/index.tsx | 2 + .../WorkspaceCompanyCardsTableControls.tsx | 6 +- .../WorkspaceCompanyCardsTable/index.tsx | 13 ++-- ...ulkUpdateCardTransactionStartDateParams.ts | 2 + src/libs/Growl.ts | 2 + src/libs/actions/CompanyCards.ts | 19 +++++- .../CompanyCardTransactionStartDateForm.tsx | 15 +++++ ...yCardsBulkEditTransactionStartDatePage.tsx | 41 ++++-------- .../WorkspaceCompanyCardsPage.tsx | 19 +++++- src/types/onyx/CompanyCardsLoadingState.ts | 3 + .../CompanyCardsTransactionStartDateTest.ts | 66 +++++++++++++++++++ ...aceCompanyCardsExportAccountColumnTest.tsx | 57 +++++++++++++++- ...WorkspaceCompanyCardsTableControlsTest.tsx | 13 ++++ 15 files changed, 221 insertions(+), 57 deletions(-) create mode 100644 tests/actions/CompanyCardsTransactionStartDateTest.ts diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/index.tsx b/src/components/GrowlNotification/GrowlNotificationContainer/index.tsx index cef6191e4f4d..c80059f5a74c 100644 --- a/src/components/GrowlNotification/GrowlNotificationContainer/index.tsx +++ b/src/components/GrowlNotification/GrowlNotificationContainer/index.tsx @@ -14,21 +14,15 @@ function GrowlNotificationContainer({children, progress, inactiveY, useBottomPos const animatedStyles = useAnimatedStyle(() => ({transform: [{translateY: inactiveY * (1 - progress.get())}]}), [inactiveY]); if (useBottomPosition) { - return {children}; + return ( + + {children} + + ); } return ( - - {children} - + {children} ); } diff --git a/src/components/GrowlNotification/GrowlNotificationContent.tsx b/src/components/GrowlNotification/GrowlNotificationContent.tsx index 3227238b5046..d95fdaf7af9e 100644 --- a/src/components/GrowlNotification/GrowlNotificationContent.tsx +++ b/src/components/GrowlNotification/GrowlNotificationContent.tsx @@ -35,6 +35,8 @@ type GrowlNotificationContentProps = { type: GrowlType; duration: number; action?: GrowlAction; + + /** Position where the growl is anchored */ position?: GrowlPosition; /** Identifies this growl instance; passed back through onDismissed so the parent can ignore stale dismissals. */ diff --git a/src/components/GrowlNotification/index.tsx b/src/components/GrowlNotification/index.tsx index e0f0cf40163e..03d91e9f3258 100644 --- a/src/components/GrowlNotification/index.tsx +++ b/src/components/GrowlNotification/index.tsx @@ -12,6 +12,8 @@ type GrowlContent = { type: GrowlType; duration: number; action?: GrowlAction; + + /** Position where the growl is anchored */ position?: GrowlPosition; /** Bumped on every show() call so identical-args re-shows still remount and re-trigger the growl. */ diff --git a/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls.tsx b/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls.tsx index 66226dadc44f..4df7483dfad5 100644 --- a/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls.tsx +++ b/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls.tsx @@ -166,11 +166,7 @@ function WorkspaceCompanyCardsTableControls({ return; } - if (selectedCardIDs.length === 1) { - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARD_EDIT_TRANSACTION_START_DATE.getRoute(policyID, selectedCardID, feedName)); - } else { - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_BULK_EDIT_TRANSACTION_START_DATE.getRoute(policyID, feedName, selectedCardIDs)); - } + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_BULK_EDIT_TRANSACTION_START_DATE.getRoute(policyID, feedName, selectedCardIDs)); clearCardSelection(); }; diff --git a/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx b/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx index 79303c057044..e02921aad1a2 100644 --- a/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx +++ b/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx @@ -289,10 +289,8 @@ function WorkspaceCompanyCardsTable({ const canEditName = canWriteCompanyCards && !!bankName && isAssigned && cardID !== undefined && !isCardDeleted && !isSelectionModeActive; const transactionStartDateErrors = getLatestErrorField(assignedCard ?? {}, 'scrapeMinDate'); const hasTransactionStartDateErrors = !isEmptyObject(transactionStartDateErrors); - let rowErrors = hasTransactionStartDateErrors ? transactionStartDateErrors : assignedCard?.errors; - if (!hasTransactionStartDateErrors && (isFeedConnectionBroken || assignedCard?.pendingFields?.lastScrape)) { - rowErrors = undefined; - } + const hasCardErrors = !isEmptyObject(assignedCard?.errors ?? {}); + const rowErrors = isFeedConnectionBroken || assignedCard?.pendingFields?.lastScrape ? undefined : {...assignedCard?.errors, ...transactionStartDateErrors}; return { cardName, @@ -307,7 +305,7 @@ function WorkspaceCompanyCardsTable({ // Unassigned cards have no details page and so no Accounting section to match, hence no title. exportAccountTitle: shouldShowExportAccountColumn && assignedCard ? getCardExportAccountTitle(cardExportSettings, assignedCard) : undefined, errors: rowErrors, - pendingAction: assignedCard?.pendingAction, + pendingAction: assignedCard?.pendingFields?.scrapeMinDate ?? assignedCard?.pendingAction, canEditName, onRenameName: (newName: string) => { if (!bankName || cardID === undefined) { @@ -318,9 +316,10 @@ function WorkspaceCompanyCardsTable({ onDismissError: () => { if (hasTransactionStartDateErrors && bankName && cardID !== undefined) { clearCompanyCardErrorField(domainOrWorkspaceAccountID, String(cardID), bankName, 'scrapeMinDate', true); - return; } - resetFailedWorkspaceCompanyCardUnassignment(domainOrWorkspaceAccountID, bankName, assignedCard?.cardID); + if (hasCardErrors) { + resetFailedWorkspaceCompanyCardUnassignment(domainOrWorkspaceAccountID, bankName, assignedCard?.cardID); + } }, }; }) diff --git a/src/libs/API/parameters/BulkUpdateCardTransactionStartDateParams.ts b/src/libs/API/parameters/BulkUpdateCardTransactionStartDateParams.ts index 80278df42a21..f1456bfa89c7 100644 --- a/src/libs/API/parameters/BulkUpdateCardTransactionStartDateParams.ts +++ b/src/libs/API/parameters/BulkUpdateCardTransactionStartDateParams.ts @@ -1,6 +1,8 @@ type BulkUpdateCardTransactionStartDateParams = { /** JSON-encoded array of card IDs */ cardIDs: string; + + /** Earliest date to import transactions from, or an empty string to import from the beginning */ startDate: string; }; diff --git a/src/libs/Growl.ts b/src/libs/Growl.ts index 01c7d1d074a9..2783bdec7a24 100644 --- a/src/libs/Growl.ts +++ b/src/libs/Growl.ts @@ -16,6 +16,8 @@ type GrowlPosition = ValueOf; type GrowlOptions = { duration?: number; action?: GrowlAction; + + /** Position where the growl is anchored */ position?: GrowlPosition; }; diff --git a/src/libs/actions/CompanyCards.ts b/src/libs/actions/CompanyCards.ts index 885cb31a0622..bf4846f99a56 100644 --- a/src/libs/actions/CompanyCards.ts +++ b/src/libs/actions/CompanyCards.ts @@ -888,7 +888,7 @@ function updateCardTransactionStartDate(domainOrWorkspaceAccountID: number, card key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${domainOrWorkspaceAccountID}_${bankName}`, value: { [cardID]: { - scrapeMinDate: oldStartDate, + scrapeMinDate: oldStartDate ?? null, pendingFields: { scrapeMinDate: null, }, @@ -933,7 +933,7 @@ function bulkUpdateCardTransactionStartDate( pendingFields: {scrapeMinDate: null}, }; failureCards[cardID] = { - scrapeMinDate: oldStartDate, + scrapeMinDate: oldStartDate ?? null, pendingFields: {scrapeMinDate: null}, errorFields: { scrapeMinDate: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), @@ -945,6 +945,13 @@ function bulkUpdateCardTransactionStartDate( const optimisticData: Array> = [{onyxMethod: Onyx.METHOD.MERGE, key, value: optimisticCards}]; const finallyData: Array> = [{onyxMethod: Onyx.METHOD.MERGE, key, value: finallyCards}]; const failureData: Array> = [{onyxMethod: Onyx.METHOD.MERGE, key, value: failureCards}]; + const successData: Array> = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.RAM_ONLY_COMPANY_CARDS_LOADING_STATE}${domainOrWorkspaceAccountID}`, + value: {bulkTransactionStartDateUpdateSuccessID: rand64()}, + }, + ]; const parameters: BulkUpdateCardTransactionStartDateParams = { cardIDs: JSON.stringify(cards.map(({cardID}) => Number(cardID))), @@ -953,11 +960,18 @@ function bulkUpdateCardTransactionStartDate( return API.write(WRITE_COMMANDS.BULK_UPDATE_CARD_TRANSACTION_START_DATE, parameters, { optimisticData, + successData, finallyData, failureData, }); } +function clearBulkUpdateTransactionStartDateSuccess(domainOrWorkspaceAccountID: number) { + Onyx.merge(`${ONYXKEYS.COLLECTION.RAM_ONLY_COMPANY_CARDS_LOADING_STATE}${domainOrWorkspaceAccountID}`, { + bulkTransactionStartDateUpdateSuccessID: null, + }); +} + function setCompanyCardExportAccount(policyID: string, domainOrWorkspaceAccountID: number, cardID: string, accountKey: string, newAccount: string, bank: CompanyCardFeedWithNumber) { const optimisticData: Array> = [ { @@ -1540,6 +1554,7 @@ export { updateCompanyCardName, updateCardTransactionStartDate, bulkUpdateCardTransactionStartDate, + clearBulkUpdateTransactionStartDateSuccess, setCompanyCardExportAccount, clearCompanyCardErrorField, setAddNewCompanyCardStepAndData, diff --git a/src/pages/workspace/companyCards/CompanyCardTransactionStartDateForm.tsx b/src/pages/workspace/companyCards/CompanyCardTransactionStartDateForm.tsx index f059e120e281..793bc1c47109 100644 --- a/src/pages/workspace/companyCards/CompanyCardTransactionStartDateForm.tsx +++ b/src/pages/workspace/companyCards/CompanyCardTransactionStartDateForm.tsx @@ -22,13 +22,28 @@ import {View} from 'react-native'; type DateOption = ValueOf; type CompanyCardTransactionStartDateFormProps = { + /** Supporting copy shown below the page header */ description: string; + + /** Date option selected before the user makes a local selection */ initialDateOption: DateOption; + + /** Date shown by the custom date picker on initial render */ initialStartDate: string; + + /** Called when the header back button is pressed */ onBackButtonPress: () => void; + + /** Called with the selected date, or an empty string for transactions from the beginning */ onSubmit: (startDate: string) => void; + + /** Identifier passed to the screen wrapper */ testID: string; + + /** Whether the save button is disabled */ isSaveDisabled?: boolean; + + /** Whether selecting the custom option resets its value to today's date */ shouldResetStartDateOnCustomSelection?: boolean; }; diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx index da724f17b631..c4882c25fecc 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx @@ -4,7 +4,6 @@ import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; import {getCompanyCardFeed, getCompanyFeeds, getDomainOrWorkspaceAccountID} from '@libs/CardUtils'; -import Growl from '@libs/Growl'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; @@ -16,12 +15,11 @@ import {bulkUpdateCardTransactionStartDate} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; +import type SCREENS from '@src/SCREENS'; import type {CompanyCardFeedWithDomainID} from '@src/types/onyx'; -import {StackActions} from '@react-navigation/native'; -import {format} from 'date-fns'; -import React, {useEffect} from 'react'; +import {format, parseISO} from 'date-fns'; +import React from 'react'; import CompanyCardTransactionStartDateForm from './CompanyCardTransactionStartDateForm'; @@ -30,7 +28,7 @@ type WorkspaceCompanyCardsBulkEditTransactionStartDatePageProps = PlatformStackS typeof SCREENS.WORKSPACE.COMPANY_CARDS_BULK_EDIT_TRANSACTION_START_DATE >; -function WorkspaceCompanyCardsBulkEditTransactionStartDatePage({route, navigation}: WorkspaceCompanyCardsBulkEditTransactionStartDatePageProps) { +function WorkspaceCompanyCardsBulkEditTransactionStartDatePage({route}: WorkspaceCompanyCardsBulkEditTransactionStartDatePageProps) { const {policyID} = route.params; // The linking layer can provide the path parameter with the feed separator still encoded. // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- decoding preserves the company-feed shape guaranteed by this route @@ -47,20 +45,10 @@ function WorkspaceCompanyCardsBulkEditTransactionStartDatePage({route, navigatio const companyFeeds = getCompanyFeeds(cardFeeds); const domainOrWorkspaceAccountID = getDomainOrWorkspaceAccountID(workspaceAccountID, companyFeeds[feedName]); const [allBankCards] = useCardsList(feedName); - - useEffect(() => { - if (!singleCardID) { - return; - } - - navigation.dispatch( - StackActions.replace(SCREENS.WORKSPACE.COMPANY_CARD_EDIT_TRANSACTION_START_DATE, { - policyID, - cardID: singleCardID, - feed: feedName, - }), - ); - }, [feedName, navigation, policyID, singleCardID]); + const singleCardStartDate = singleCardID ? allBankCards?.[singleCardID]?.scrapeMinDate : undefined; + const initialDateOption = + singleCardID && !singleCardStartDate ? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING : CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM; + const initialStartDate = singleCardStartDate ? format(parseISO(singleCardStartDate), CONST.DATE.FNS_FORMAT_STRING) : format(new Date(), CONST.DATE.FNS_FORMAT_STRING); const goBackToCompanyCards = () => Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); @@ -73,16 +61,10 @@ function WorkspaceCompanyCardsBulkEditTransactionStartDatePage({route, navigatio oldStartDate: allBankCards?.[cardID]?.scrapeMinDate, })), newStartDate, - ).then(() => { - Growl.success(translate('workspace.companyCards.bulkStartDateUpdated'), {position: CONST.GROWL.POSITION.BOTTOM_RIGHT}); - }); + ); goBackToCompanyCards(); }; - if (singleCardID) { - return null; - } - return ( ); diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx index 56bcae33b2e5..6bf03cca9de9 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx @@ -8,6 +8,7 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails' import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; +import useOnyx from '@hooks/useOnyx'; import {usePersonalDetailsByLogins} from '@hooks/usePersonalDetailByLogin'; import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -15,6 +16,7 @@ import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import {getDomainOrWorkspaceAccountID} from '@libs/CardUtils'; +import Growl from '@libs/Growl'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; @@ -23,9 +25,10 @@ import {canMemberWrite, getMemberAccountIDsForWorkspace} from '@libs/PolicyUtils import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; -import {openPolicyCompanyCardsFeed, openPolicyCompanyCardsPage} from '@userActions/CompanyCards'; +import {clearBulkUpdateTransactionStartDateSuccess, openPolicyCompanyCardsFeed, openPolicyCompanyCardsPage} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; @@ -57,6 +60,20 @@ function WorkspaceCompanyCardsPage({route}: WorkspaceCompanyCardsPageProps) { } = companyCards; const domainOrWorkspaceAccountID = getDomainOrWorkspaceAccountID(workspaceAccountID, selectedFeed); + const [companyCardsLoadingState] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_COMPANY_CARDS_LOADING_STATE}${domainOrWorkspaceAccountID}`); + + const bulkTransactionStartDateUpdateSuccessID = companyCardsLoadingState?.bulkTransactionStartDateUpdateSuccessID; + const lastHandledBulkTransactionStartDateUpdateSuccessID = useRef(undefined); + + useEffect(() => { + if (!bulkTransactionStartDateUpdateSuccessID || lastHandledBulkTransactionStartDateUpdateSuccessID.current === bulkTransactionStartDateUpdateSuccessID) { + return; + } + + lastHandledBulkTransactionStartDateUpdateSuccessID.current = bulkTransactionStartDateUpdateSuccessID; + clearBulkUpdateTransactionStartDateSuccess(domainOrWorkspaceAccountID); + Growl.success(translate('workspace.companyCards.bulkStartDateUpdated'), {position: CONST.GROWL.POSITION.BOTTOM_RIGHT}); + }, [bulkTransactionStartDateUpdateSuccessID, domainOrWorkspaceAccountID, translate]); const employeePersonalDetails = usePersonalDetailsByLogins(Object.keys(policy?.employeeList ?? {})); diff --git a/src/types/onyx/CompanyCardsLoadingState.ts b/src/types/onyx/CompanyCardsLoadingState.ts index 6940dc20480f..d2f96535fa56 100644 --- a/src/types/onyx/CompanyCardsLoadingState.ts +++ b/src/types/onyx/CompanyCardsLoadingState.ts @@ -18,6 +18,9 @@ type CompanyCardsLoadingState = { /** Whether workspace company card feeds have been fetched at least once in this session */ hasOnceLoadedPage?: boolean; + /** Identifies the latest successful bulk transaction start date update */ + bulkTransactionStartDateUpdateSuccessID?: string; + /** Per-feed session loading state keyed by feed name */ feeds?: Partial>; }; diff --git a/tests/actions/CompanyCardsTransactionStartDateTest.ts b/tests/actions/CompanyCardsTransactionStartDateTest.ts new file mode 100644 index 000000000000..0c0b01edc472 --- /dev/null +++ b/tests/actions/CompanyCardsTransactionStartDateTest.ts @@ -0,0 +1,66 @@ +import {bulkUpdateCardTransactionStartDate, updateCardTransactionStartDate} from '@libs/actions/CompanyCards'; +import {WRITE_COMMANDS} from '@libs/API/types'; + +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +const DOMAIN_OR_WORKSPACE_ACCOUNT_ID = 1234567; +const CARD_ID = '123'; +const FEED = CONST.COMPANY_CARD.FEED_BANK_NAME.VISA; + +describe('actions/CompanyCards transaction start date updates', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('removes an optimistically added date when a single-card update fails', () => { + // Given a card that previously imported transactions from the beginning + const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + + // When setting a custom start date + updateCardTransactionStartDate(DOMAIN_OR_WORKSPACE_ACCOUNT_ID, CARD_ID, '2026-10-13', FEED, undefined); + + // Then a failed request rolls the missing date back with null so Onyx removes the optimistic value + expect(apiWriteSpy).toHaveBeenCalledWith( + WRITE_COMMANDS.UPDATE_CARD_TRANSACTION_START_DATE, + expect.anything(), + expect.objectContaining({ + failureData: [ + expect.objectContaining({ + value: { + [CARD_ID]: expect.objectContaining({scrapeMinDate: null}), + }, + }), + ], + }), + ); + }); + + it('rolls back absent dates and signals success only through success data for bulk updates', () => { + // Given cards that previously imported transactions from the beginning + const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve()); + + // When setting a custom start date in bulk + bulkUpdateCardTransactionStartDate(DOMAIN_OR_WORKSPACE_ACCOUNT_ID, FEED, [{cardID: CARD_ID, oldStartDate: undefined}], '2026-10-13'); + + // Then failure data removes the optimistic date and success data signals the confirmed server response + expect(apiWriteSpy).toHaveBeenCalledWith( + WRITE_COMMANDS.BULK_UPDATE_CARD_TRANSACTION_START_DATE, + expect.anything(), + expect.objectContaining({ + failureData: [ + expect.objectContaining({ + value: { + [CARD_ID]: expect.objectContaining({scrapeMinDate: null}), + }, + }), + ], + successData: [ + expect.objectContaining({ + key: `${ONYXKEYS.COLLECTION.RAM_ONLY_COMPANY_CARDS_LOADING_STATE}${DOMAIN_OR_WORKSPACE_ACCOUNT_ID}`, + }), + ], + }), + ); + }); +}); diff --git a/tests/ui/WorkspaceCompanyCardsExportAccountColumnTest.tsx b/tests/ui/WorkspaceCompanyCardsExportAccountColumnTest.tsx index 17132407f942..1db872dceb9c 100644 --- a/tests/ui/WorkspaceCompanyCardsExportAccountColumnTest.tsx +++ b/tests/ui/WorkspaceCompanyCardsExportAccountColumnTest.tsx @@ -31,10 +31,13 @@ const FEED_NAME = `${CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE}#${DOMAIN_OR_WORKSP const BANK_NAME = CONST.COMPANY_CARD.FEED_BANK_NAME.CHASE as CompanyCardFeedWithNumber; const ACCOUNT_ID = 1; const QBO_EXPORT_ACCOUNT_NVP = CONST.COMPANY_CARDS.EXPORT_CARD_TYPES.NVP_QUICKBOOKS_ONLINE_EXPORT_ACCOUNT; +const ROOT_ERROR_KEY = 'rootError'; +const START_DATE_ERROR_KEY = 'startDateError'; let capturedColumns: Array> = []; let capturedData: WorkspaceCompanyCardTableItemData[] = []; let capturedCompareItems: CompareItemsCallback | undefined; +let mockCardFeedErrors: Record = {}; jest.mock('@hooks/useLazyAsset', () => ({ useMemoizedLazyIllustrations: () => ({}), @@ -43,7 +46,7 @@ jest.mock('@hooks/useLazyAsset', () => ({ jest.mock('@hooks/useCardFeedErrors', () => ({ __esModule: true, - default: () => ({cardFeedErrors: {}}), + default: () => ({cardFeedErrors: mockCardFeedErrors}), })); jest.mock('@hooks/useNetwork', () => ({ @@ -206,6 +209,7 @@ async function setQBOPolicy(nvpExportAccount?: string) { describe('WorkspaceCompanyCardsTable Export account column', () => { beforeEach(async () => { + mockCardFeedErrors = {}; await Onyx.clear(); await waitForBatchedUpdates(); }); @@ -314,3 +318,54 @@ describe('WorkspaceCompanyCardsTable Export account column', () => { expect(screen.getByTestId('WorkspaceCompanyCardsTable')).toBeTruthy(); }); }); + +describe('WorkspaceCompanyCardsTable row feedback', () => { + beforeEach(async () => { + mockCardFeedErrors = {}; + await Onyx.clear(); + await waitForBatchedUpdates(); + }); + + it('combines root and transaction start date errors on the same row', async () => { + // Given an assigned card whose unassignment and start-date updates both failed + const card = buildAssignedCard({ + errors: {[ROOT_ERROR_KEY]: 'Unassignment failed'}, + errorFields: {scrapeMinDate: {[START_DATE_ERROR_KEY]: 'Start date update failed'}}, + }); + + // When the company cards table builds its row data + renderTable(buildCompanyCards(card)); + await waitForBatchedUpdates(); + + // Then both failures are forwarded together instead of competing for one slot + expect(capturedData.at(0)?.errors).toEqual({[ROOT_ERROR_KEY]: 'Unassignment failed', [START_DATE_ERROR_KEY]: 'Start date update failed'}); + }); + + it('forwards the transaction start date pending state to the row', async () => { + // Given an assigned card with an offline transaction start date update + const card = buildAssignedCard({pendingFields: {scrapeMinDate: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}}); + + // When the company cards table builds its row data + renderTable(buildCompanyCards(card)); + await waitForBatchedUpdates(); + + // Then OfflineWithFeedback receives the field-level pending state + expect(capturedData.at(0)?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + }); + + it('hides row errors while the feed-level connection error is displayed', async () => { + // Given a broken feed and a card with both root and start-date errors + mockCardFeedErrors = {[FEED_NAME]: {shouldPromptBrokenConnection: true}}; + const card = buildAssignedCard({ + errors: {[ROOT_ERROR_KEY]: 'Unassignment failed'}, + errorFields: {scrapeMinDate: {[START_DATE_ERROR_KEY]: 'Start date update failed'}}, + }); + + // When the company cards table builds its row data + renderTable(buildCompanyCards(card)); + await waitForBatchedUpdates(); + + // Then the feed-level error remains the single visible error source + expect(capturedData.at(0)?.errors).toBeUndefined(); + }); +}); diff --git a/tests/ui/WorkspaceCompanyCardsTableControlsTest.tsx b/tests/ui/WorkspaceCompanyCardsTableControlsTest.tsx index 0c022986663e..d9477b6336c8 100644 --- a/tests/ui/WorkspaceCompanyCardsTableControlsTest.tsx +++ b/tests/ui/WorkspaceCompanyCardsTableControlsTest.tsx @@ -157,4 +157,17 @@ describe('WorkspaceCompanyCardsTableControls narrow-layout selection mode', () = expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.WORKSPACE_COMPANY_CARDS_BULK_EDIT_TRANSACTION_START_DATE.getRoute('policy123', 'oauth.chase.com#123', ['123', '456'])); expect(clearCardSelection).toHaveBeenCalledTimes(1); }); + + it('keeps a single selected card in the bulk transaction start date flow', () => { + // Given one assigned card is selected from the company cards table + mockProcessedData = [{selected: true, disabled: false, isAssigned: true, assignedCard: {cardID: 123}}]; + const {clearCardSelection} = renderControls(true); + + // When the bulk transaction start date action is selected + mockButtonProps.current?.options.find((option) => option.value === 'editTransactionStartDate')?.onSelected(); + + // Then the bulk editor opens so its save, back, and success feedback behavior is preserved + expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.WORKSPACE_COMPANY_CARDS_BULK_EDIT_TRANSACTION_START_DATE.getRoute('policy123', 'oauth.chase.com#123', ['123'])); + expect(clearCardSelection).toHaveBeenCalledTimes(1); + }); }); From b6dfcec0e34ad238ef05a322399b48ef13c1ee5e Mon Sep 17 00:00:00 2001 From: Hans Date: Wed, 14 Oct 2026 12:43:43 +0700 Subject: [PATCH 15/28] cleanup optimistic data, prevent clearing selection --- .../GrowlNotificationContent.tsx | 13 ++++++------- .../WorkspaceCompanyCardsTableControls.tsx | 16 ++++++++-------- .../Tables/WorkspaceCompanyCardsTable/index.tsx | 2 +- ...kspaceCompanyCardsExportAccountColumnTest.tsx | 15 +++++++++++++++ 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/components/GrowlNotification/GrowlNotificationContent.tsx b/src/components/GrowlNotification/GrowlNotificationContent.tsx index d95fdaf7af9e..598a593a8b49 100644 --- a/src/components/GrowlNotification/GrowlNotificationContent.tsx +++ b/src/components/GrowlNotification/GrowlNotificationContent.tsx @@ -17,7 +17,7 @@ import type {SvgProps} from 'react-native-svg'; import React, {useEffect, useRef} from 'react'; import {View} from 'react-native'; -import {Directions, Gesture, GestureDetector} from 'react-native-gesture-handler'; +import {Directions, GestureDetector, useFlingGesture} from 'react-native-gesture-handler'; import {useSharedValue, withSpring} from 'react-native-reanimated'; import {scheduleOnRN} from 'react-native-worklets'; @@ -129,12 +129,11 @@ function GrowlNotificationContent({bodyText, type, duration, action, position, n // GestureDetector by default runs callbacks on UI thread using Reanimated. In this // case we want to trigger an RN's Animated animation, which needs to be done on JS thread. - // The builder keeps the responsive direction and JS-thread callback on the same gesture instance. - // eslint-disable-next-line @typescript-eslint/no-deprecated - const flingGesture = Gesture.Fling() - .direction(useBottomPosition ? Directions.DOWN : Directions.UP) - .runOnJS(true) - .onStart(triggerDismiss); + const flingGesture = useFlingGesture({ + direction: useBottomPosition ? Directions.DOWN : Directions.UP, + runOnJS: true, + onActivate: triggerDismiss, + }); return ( diff --git a/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls.tsx b/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls.tsx index 4df7483dfad5..9f1c5be54632 100644 --- a/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls.tsx +++ b/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls.tsx @@ -167,7 +167,6 @@ function WorkspaceCompanyCardsTableControls({ } Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_BULK_EDIT_TRANSACTION_START_DATE.getRoute(policyID, feedName, selectedCardIDs)); - clearCardSelection(); }; const getBulkActionOptions = (): Array> => { @@ -182,20 +181,21 @@ function WorkspaceCompanyCardsTableControls({ shouldSkipFocusRestore: !!bankName, onSelected: confirmBulkUnassign, }); - options.push({ - icon: icons.CalendarSolid, - text: translate('workspace.moreFeatures.companyCards.bulkUpdateTransactionStartDate'), - value: 'editTransactionStartDate', - onSelected: editSelectedCardTransactionStartDates, - }); } - options.push({ icon: icons.MoneySearch, text: translate('workspace.common.viewTransactions'), value: 'viewTransactions', onSelected: viewSelectedCardTransactions, }); + if (canWriteCompanyCards) { + options.push({ + icon: icons.CalendarSolid, + text: translate('workspace.moreFeatures.companyCards.bulkUpdateTransactionStartDate'), + value: 'editTransactionStartDate', + onSelected: editSelectedCardTransactionStartDates, + }); + } } options.push({ diff --git a/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx b/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx index e02921aad1a2..6a9ad5c6fe75 100644 --- a/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx +++ b/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx @@ -305,7 +305,7 @@ function WorkspaceCompanyCardsTable({ // Unassigned cards have no details page and so no Accounting section to match, hence no title. exportAccountTitle: shouldShowExportAccountColumn && assignedCard ? getCardExportAccountTitle(cardExportSettings, assignedCard) : undefined, errors: rowErrors, - pendingAction: assignedCard?.pendingFields?.scrapeMinDate ?? assignedCard?.pendingAction, + pendingAction: assignedCard?.pendingAction ?? assignedCard?.pendingFields?.scrapeMinDate, canEditName, onRenameName: (newName: string) => { if (!bankName || cardID === undefined) { diff --git a/tests/ui/WorkspaceCompanyCardsExportAccountColumnTest.tsx b/tests/ui/WorkspaceCompanyCardsExportAccountColumnTest.tsx index 1db872dceb9c..20c2778d1806 100644 --- a/tests/ui/WorkspaceCompanyCardsExportAccountColumnTest.tsx +++ b/tests/ui/WorkspaceCompanyCardsExportAccountColumnTest.tsx @@ -353,6 +353,21 @@ describe('WorkspaceCompanyCardsTable row feedback', () => { expect(capturedData.at(0)?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); }); + it('prioritizes card deletion over a pending transaction start date update', async () => { + // Given a card that is being unassigned while its transaction start date update is still pending + const card = buildAssignedCard({ + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + pendingFields: {scrapeMinDate: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, + }); + + // When the company cards table builds its row data + renderTable(buildCompanyCards(card)); + await waitForBatchedUpdates(); + + // Then the root deletion remains authoritative and the row is filtered from the online table + expect(capturedData).toHaveLength(0); + }); + it('hides row errors while the feed-level connection error is displayed', async () => { // Given a broken feed and a card with both root and start-date errors mockCardFeedErrors = {[FEED_NAME]: {shouldPromptBrokenConnection: true}}; From c7f8a3b8a499f9f10ba94d8ede69082a2a255d52 Mon Sep 17 00:00:00 2001 From: Hans Date: Wed, 14 Oct 2026 13:07:02 +0700 Subject: [PATCH 16/28] share the date between cards --- .../WorkspaceCompanyCardsTable/index.tsx | 5 +++- ...yCardsBulkEditTransactionStartDatePage.tsx | 5 ++-- ...aceCompanyCardsExportAccountColumnTest.tsx | 24 ++++++++++++++++--- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx b/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx index 6a9ad5c6fe75..6083d6217d33 100644 --- a/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx +++ b/src/components/Tables/WorkspaceCompanyCardsTable/index.tsx @@ -290,7 +290,10 @@ function WorkspaceCompanyCardsTable({ const transactionStartDateErrors = getLatestErrorField(assignedCard ?? {}, 'scrapeMinDate'); const hasTransactionStartDateErrors = !isEmptyObject(transactionStartDateErrors); const hasCardErrors = !isEmptyObject(assignedCard?.errors ?? {}); - const rowErrors = isFeedConnectionBroken || assignedCard?.pendingFields?.lastScrape ? undefined : {...assignedCard?.errors, ...transactionStartDateErrors}; + // The feed-level prompt owns connection errors, but a failed start-date edit is a separate user action + // and must remain visible and dismissible on the affected row. + const cardErrors = isFeedConnectionBroken || assignedCard?.pendingFields?.lastScrape ? undefined : assignedCard?.errors; + const rowErrors = hasTransactionStartDateErrors ? {...cardErrors, ...transactionStartDateErrors} : cardErrors; return { cardName, diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx index c4882c25fecc..9fe5b7007a2d 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx @@ -46,9 +46,10 @@ function WorkspaceCompanyCardsBulkEditTransactionStartDatePage({route}: Workspac const domainOrWorkspaceAccountID = getDomainOrWorkspaceAccountID(workspaceAccountID, companyFeeds[feedName]); const [allBankCards] = useCardsList(feedName); const singleCardStartDate = singleCardID ? allBankCards?.[singleCardID]?.scrapeMinDate : undefined; + const areScrapeDateEqual = cardIDs.length === 1 || cardIDs.every((cardID) => allBankCards?.[cardID]?.scrapeMinDate === singleCardStartDate); const initialDateOption = - singleCardID && !singleCardStartDate ? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING : CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM; - const initialStartDate = singleCardStartDate ? format(parseISO(singleCardStartDate), CONST.DATE.FNS_FORMAT_STRING) : format(new Date(), CONST.DATE.FNS_FORMAT_STRING); + areScrapeDateEqual && !singleCardStartDate ? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING : CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM; + const initialStartDate = areScrapeDateEqual ? format(parseISO(singleCardStartDate), CONST.DATE.FNS_FORMAT_STRING) : null; const goBackToCompanyCards = () => Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); diff --git a/tests/ui/WorkspaceCompanyCardsExportAccountColumnTest.tsx b/tests/ui/WorkspaceCompanyCardsExportAccountColumnTest.tsx index 20c2778d1806..c1b37d061fe5 100644 --- a/tests/ui/WorkspaceCompanyCardsExportAccountColumnTest.tsx +++ b/tests/ui/WorkspaceCompanyCardsExportAccountColumnTest.tsx @@ -368,7 +368,25 @@ describe('WorkspaceCompanyCardsTable row feedback', () => { expect(capturedData).toHaveLength(0); }); - it('hides row errors while the feed-level connection error is displayed', async () => { + it('shows a failed unassignment after its optimistic deletion is rolled back', async () => { + // Given an unassignment failure while the transaction start date update is still pending + const card = buildAssignedCard({ + pendingAction: null, + pendingFields: {scrapeMinDate: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, + errors: {[ROOT_ERROR_KEY]: 'Unassignment failed'}, + }); + + // When the company cards table builds its row data + renderTable(buildCompanyCards(card)); + await waitForBatchedUpdates(); + + // Then the row returns with its RBR while retaining the field-level pending feedback + expect(capturedData).toHaveLength(1); + expect(capturedData.at(0)?.errors).toEqual({[ROOT_ERROR_KEY]: 'Unassignment failed'}); + expect(capturedData.at(0)?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); + }); + + it('keeps a failed transaction start date update visible while the feed-level connection error is displayed', async () => { // Given a broken feed and a card with both root and start-date errors mockCardFeedErrors = {[FEED_NAME]: {shouldPromptBrokenConnection: true}}; const card = buildAssignedCard({ @@ -380,7 +398,7 @@ describe('WorkspaceCompanyCardsTable row feedback', () => { renderTable(buildCompanyCards(card)); await waitForBatchedUpdates(); - // Then the feed-level error remains the single visible error source - expect(capturedData.at(0)?.errors).toBeUndefined(); + // Then the feed-level prompt owns the root error while the independent field failure remains actionable on the row + expect(capturedData.at(0)?.errors).toEqual({[START_DATE_ERROR_KEY]: 'Start date update failed'}); }); }); From 1a7f5849874df6ac205116857f160f36abcb23dd Mon Sep 17 00:00:00 2001 From: Hans Date: Wed, 14 Oct 2026 13:19:58 +0700 Subject: [PATCH 17/28] fix start date issue --- ...aceCompanyCardsBulkEditTransactionStartDatePage.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx index 9fe5b7007a2d..8165bcb5409e 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx @@ -34,7 +34,7 @@ function WorkspaceCompanyCardsBulkEditTransactionStartDatePage({route}: Workspac // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- decoding preserves the company-feed shape guaranteed by this route const feedName = decodeURIComponent(route.params.feed) as CompanyCardFeedWithDomainID; const cardIDs = (route.params.cardIDs ?? '').split(',').filter(Boolean); - const singleCardID = cardIDs.length === 1 ? cardIDs.at(0) : undefined; + const firstCardID = cardIDs.at(0); const bank = getCompanyCardFeed(feedName); const {translate} = useLocalize(); @@ -45,11 +45,11 @@ function WorkspaceCompanyCardsBulkEditTransactionStartDatePage({route}: Workspac const companyFeeds = getCompanyFeeds(cardFeeds); const domainOrWorkspaceAccountID = getDomainOrWorkspaceAccountID(workspaceAccountID, companyFeeds[feedName]); const [allBankCards] = useCardsList(feedName); - const singleCardStartDate = singleCardID ? allBankCards?.[singleCardID]?.scrapeMinDate : undefined; - const areScrapeDateEqual = cardIDs.length === 1 || cardIDs.every((cardID) => allBankCards?.[cardID]?.scrapeMinDate === singleCardStartDate); + const singleCardStartDate = firstCardID && allBankCards?.[firstCardID]?.scrapeMinDate; + const areScrapeDateEqual = singleCardStartDate && (cardIDs.length === 1 || cardIDs.every((cardID) => allBankCards?.[cardID]?.scrapeMinDate === singleCardStartDate)); const initialDateOption = areScrapeDateEqual && !singleCardStartDate ? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING : CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM; - const initialStartDate = areScrapeDateEqual ? format(parseISO(singleCardStartDate), CONST.DATE.FNS_FORMAT_STRING) : null; + const initialStartDate = areScrapeDateEqual ? format(parseISO(singleCardStartDate), CONST.DATE.FNS_FORMAT_STRING) : ''; const goBackToCompanyCards = () => Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); @@ -81,7 +81,7 @@ function WorkspaceCompanyCardsBulkEditTransactionStartDatePage({route}: Workspac onBackButtonPress={goBackToCompanyCards} onSubmit={submit} isSaveDisabled={cardIDs.length === 0} - shouldResetStartDateOnCustomSelection={!!singleCardID && !singleCardStartDate} + shouldResetStartDateOnCustomSelection={!!firstCardID && !singleCardStartDate} /> ); From 6dfb2f3de1ac798b7602d15e5d85069419773757 Mon Sep 17 00:00:00 2001 From: Hans Date: Wed, 14 Oct 2026 13:27:22 +0700 Subject: [PATCH 18/28] fix initial start date option --- .../WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx index 8165bcb5409e..4a3d69257bd3 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx @@ -46,9 +46,9 @@ function WorkspaceCompanyCardsBulkEditTransactionStartDatePage({route}: Workspac const domainOrWorkspaceAccountID = getDomainOrWorkspaceAccountID(workspaceAccountID, companyFeeds[feedName]); const [allBankCards] = useCardsList(feedName); const singleCardStartDate = firstCardID && allBankCards?.[firstCardID]?.scrapeMinDate; - const areScrapeDateEqual = singleCardStartDate && (cardIDs.length === 1 || cardIDs.every((cardID) => allBankCards?.[cardID]?.scrapeMinDate === singleCardStartDate)); + const areScrapeDateEqual = !!singleCardStartDate && (cardIDs.length === 1 || cardIDs.every((cardID) => allBankCards?.[cardID]?.scrapeMinDate === singleCardStartDate)); const initialDateOption = - areScrapeDateEqual && !singleCardStartDate ? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING : CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM; + !areScrapeDateEqual && !singleCardStartDate ? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING : CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM; const initialStartDate = areScrapeDateEqual ? format(parseISO(singleCardStartDate), CONST.DATE.FNS_FORMAT_STRING) : ''; const goBackToCompanyCards = () => Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); From 83ae29185aec45d26f3221b963090f8a7f274f3d Mon Sep 17 00:00:00 2001 From: Hans Date: Wed, 14 Oct 2026 13:31:58 +0700 Subject: [PATCH 19/28] remove redundant test since we used the new flow --- ...WorkspaceCompanyCardsTableControlsTest.tsx | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/tests/ui/WorkspaceCompanyCardsTableControlsTest.tsx b/tests/ui/WorkspaceCompanyCardsTableControlsTest.tsx index d9477b6336c8..35570fd539db 100644 --- a/tests/ui/WorkspaceCompanyCardsTableControlsTest.tsx +++ b/tests/ui/WorkspaceCompanyCardsTableControlsTest.tsx @@ -141,33 +141,4 @@ describe('WorkspaceCompanyCardsTableControls narrow-layout selection mode', () = expect(screen.queryByTestId('WorkspaceCompanyCardsBulkActions')).toBeNull(); expect(screen.getByTestId('WorkspaceCompanyCardsFilterBar')).toBeTruthy(); }); - - it('opens the bulk transaction start date screen for the selected assigned cards', () => { - // Given two assigned cards are selected - mockProcessedData = [ - {selected: true, disabled: false, isAssigned: true, assignedCard: {cardID: 123}}, - {selected: true, disabled: false, isAssigned: true, assignedCard: {cardID: 456}}, - ]; - const {clearCardSelection} = renderControls(true); - - // When the bulk transaction start date action is selected - mockButtonProps.current?.options.find((option) => option.value === 'editTransactionStartDate')?.onSelected(); - - // Then the editor opens with both card IDs and clears the table selection - expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.WORKSPACE_COMPANY_CARDS_BULK_EDIT_TRANSACTION_START_DATE.getRoute('policy123', 'oauth.chase.com#123', ['123', '456'])); - expect(clearCardSelection).toHaveBeenCalledTimes(1); - }); - - it('keeps a single selected card in the bulk transaction start date flow', () => { - // Given one assigned card is selected from the company cards table - mockProcessedData = [{selected: true, disabled: false, isAssigned: true, assignedCard: {cardID: 123}}]; - const {clearCardSelection} = renderControls(true); - - // When the bulk transaction start date action is selected - mockButtonProps.current?.options.find((option) => option.value === 'editTransactionStartDate')?.onSelected(); - - // Then the bulk editor opens so its save, back, and success feedback behavior is preserved - expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.WORKSPACE_COMPANY_CARDS_BULK_EDIT_TRANSACTION_START_DATE.getRoute('policy123', 'oauth.chase.com#123', ['123'])); - expect(clearCardSelection).toHaveBeenCalledTimes(1); - }); }); From f3df9426ace12852410bb98c5b78319c7a9a4981 Mon Sep 17 00:00:00 2001 From: Hans Date: Wed, 14 Oct 2026 13:47:44 +0700 Subject: [PATCH 20/28] linting --- tests/ui/WorkspaceCompanyCardsTableControlsTest.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/ui/WorkspaceCompanyCardsTableControlsTest.tsx b/tests/ui/WorkspaceCompanyCardsTableControlsTest.tsx index 35570fd539db..269b55090543 100644 --- a/tests/ui/WorkspaceCompanyCardsTableControlsTest.tsx +++ b/tests/ui/WorkspaceCompanyCardsTableControlsTest.tsx @@ -2,10 +2,6 @@ import {render, screen} from '@testing-library/react-native'; import WorkspaceCompanyCardsTableControls from '@components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableControls'; -import Navigation from '@libs/Navigation/Navigation'; - -import ROUTES from '@src/ROUTES'; - import React from 'react'; type MockDropdownOption = { From 378c6e7adbaf29ba08b4cf6a7864767b208ae8e1 Mon Sep 17 00:00:00 2001 From: Hans Date: Wed, 14 Oct 2026 13:54:16 +0700 Subject: [PATCH 21/28] use set for better date handling --- ...yCardsBulkEditTransactionStartDatePage.tsx | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx index 4a3d69257bd3..897650ee7400 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx @@ -1,3 +1,5 @@ +import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; + import useCardFeeds from '@hooks/useCardFeeds'; import useCardsList from '@hooks/useCardsList'; import useLocalize from '@hooks/useLocalize'; @@ -17,6 +19,7 @@ import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {CompanyCardFeedWithDomainID} from '@src/types/onyx'; +import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import {format, parseISO} from 'date-fns'; import React from 'react'; @@ -34,22 +37,24 @@ function WorkspaceCompanyCardsBulkEditTransactionStartDatePage({route}: Workspac // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- decoding preserves the company-feed shape guaranteed by this route const feedName = decodeURIComponent(route.params.feed) as CompanyCardFeedWithDomainID; const cardIDs = (route.params.cardIDs ?? '').split(',').filter(Boolean); - const firstCardID = cardIDs.at(0); const bank = getCompanyCardFeed(feedName); const {translate} = useLocalize(); const policy = usePolicy(policyID); const workspaceAccountID = policy?.policyAccountID ?? CONST.DEFAULT_NUMBER_ID; - const [cardFeeds] = useCardFeeds(policyID); + const [cardFeeds, cardFeedsMetadata] = useCardFeeds(policyID); const companyFeeds = getCompanyFeeds(cardFeeds); const domainOrWorkspaceAccountID = getDomainOrWorkspaceAccountID(workspaceAccountID, companyFeeds[feedName]); - const [allBankCards] = useCardsList(feedName); - const singleCardStartDate = firstCardID && allBankCards?.[firstCardID]?.scrapeMinDate; - const areScrapeDateEqual = !!singleCardStartDate && (cardIDs.length === 1 || cardIDs.every((cardID) => allBankCards?.[cardID]?.scrapeMinDate === singleCardStartDate)); - const initialDateOption = - !areScrapeDateEqual && !singleCardStartDate ? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING : CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM; - const initialStartDate = areScrapeDateEqual ? format(parseISO(singleCardStartDate), CONST.DATE.FNS_FORMAT_STRING) : ''; + const [allBankCards, cardsMetadata] = useCardsList(feedName); + const isLoading = isLoadingOnyxValue(cardFeedsMetadata, cardsMetadata); + const hasMissingCards = cardIDs.some((cardID) => !allBankCards?.[cardID]); + const selectedDates = cardIDs.map((cardID) => allBankCards?.[cardID]?.scrapeMinDate ?? ''); + const uniqueDates = new Set(selectedDates); + const areAllFromBeginning = selectedDates.every((date) => !date); + const sharedStartDate = uniqueDates.size === 1 ? selectedDates.at(0) : undefined; + const initialDateOption = areAllFromBeginning ? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING : CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM; + const initialStartDate = sharedStartDate ? format(parseISO(sharedStartDate), CONST.DATE.FNS_FORMAT_STRING) : ''; const goBackToCompanyCards = () => Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); @@ -72,17 +77,22 @@ function WorkspaceCompanyCardsBulkEditTransactionStartDatePage({route}: Workspac featureName={CONST.POLICY.MORE_FEATURES.ARE_COMPANY_CARDS_ENABLED} policyFeature={CONST.POLICY.POLICY_FEATURE.COMPANY_CARDS} policyFeatureAccess={CONST.POLICY.POLICY_FEATURE_ACCESS.WRITE} + shouldBeBlocked={!isLoading && hasMissingCards} > - + {isLoading ? ( + + ) : ( + + )} ); } From c82bec3061dcb5b72aceec760ae70a78f89457ee Mon Sep 17 00:00:00 2001 From: Hans Date: Thu, 15 Oct 2026 22:08:39 +0700 Subject: [PATCH 22/28] Fix default selected logic --- .../CompanyCardTransactionStartDateForm.tsx | 15 +++++++++++++-- ...mpanyCardsBulkEditTransactionStartDatePage.tsx | 3 ++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/pages/workspace/companyCards/CompanyCardTransactionStartDateForm.tsx b/src/pages/workspace/companyCards/CompanyCardTransactionStartDateForm.tsx index 793bc1c47109..da6dc2cde479 100644 --- a/src/pages/workspace/companyCards/CompanyCardTransactionStartDateForm.tsx +++ b/src/pages/workspace/companyCards/CompanyCardTransactionStartDateForm.tsx @@ -1,5 +1,6 @@ import Button from '@components/Button'; import DatePicker from '@components/DatePicker'; +import FormHelpMessage from '@components/FormHelpMessage'; import Header from '@components/Header'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; @@ -26,7 +27,7 @@ type CompanyCardTransactionStartDateFormProps = { description: string; /** Date option selected before the user makes a local selection */ - initialDateOption: DateOption; + initialDateOption: DateOption | ''; /** Date shown by the custom date picker on initial render */ initialStartDate: string; @@ -74,6 +75,11 @@ function CompanyCardTransactionStartDateForm({ }; const submit = () => { + if (!isRequiredFulfilled(dateOptionSelected)) { + setErrorText(translate('common.error.fieldRequired')); + return; + } + if (dateOptionSelected === CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM && !isRequiredFulfilled(startDate)) { setErrorText(translate('common.error.fieldRequired')); return; @@ -142,7 +148,12 @@ function CompanyCardTransactionStartDateForm({ errorText={errorText} />
- ) : null + ) : ( + + ) } />
diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx index 897650ee7400..04696085ecad 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsBulkEditTransactionStartDatePage.tsx @@ -53,7 +53,8 @@ function WorkspaceCompanyCardsBulkEditTransactionStartDatePage({route}: Workspac const uniqueDates = new Set(selectedDates); const areAllFromBeginning = selectedDates.every((date) => !date); const sharedStartDate = uniqueDates.size === 1 ? selectedDates.at(0) : undefined; - const initialDateOption = areAllFromBeginning ? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING : CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM; + const sharedDateOption = areAllFromBeginning ? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.FROM_BEGINNING : CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM; + const initialDateOption = uniqueDates.size > 1 ? '' : sharedDateOption; const initialStartDate = sharedStartDate ? format(parseISO(sharedStartDate), CONST.DATE.FNS_FORMAT_STRING) : ''; const goBackToCompanyCards = () => Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); From 05ca6c009e98cd248ff10eb403104f600063b06a Mon Sep 17 00:00:00 2001 From: Hans Date: Thu, 15 Oct 2026 22:14:20 +0700 Subject: [PATCH 23/28] clear selection when updating sucessfully --- src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx index 6bf03cca9de9..e3bf396c7629 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx @@ -72,6 +72,7 @@ function WorkspaceCompanyCardsPage({route}: WorkspaceCompanyCardsPageProps) { lastHandledBulkTransactionStartDateUpdateSuccessID.current = bulkTransactionStartDateUpdateSuccessID; clearBulkUpdateTransactionStartDateSuccess(domainOrWorkspaceAccountID); + companyCardsTableRef.current?.clearSelection(); Growl.success(translate('workspace.companyCards.bulkStartDateUpdated'), {position: CONST.GROWL.POSITION.BOTTOM_RIGHT}); }, [bulkTransactionStartDateUpdateSuccessID, domainOrWorkspaceAccountID, translate]); From f4635bbe7d70f0fd6ff2069ba0c57c0f9ed65acb Mon Sep 17 00:00:00 2001 From: Hans Date: Thu, 15 Oct 2026 22:16:53 +0700 Subject: [PATCH 24/28] add explanation for growl --- .../GrowlNotification/shouldUseExplicitBottomPosition/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/GrowlNotification/shouldUseExplicitBottomPosition/index.ts b/src/components/GrowlNotification/shouldUseExplicitBottomPosition/index.ts index 84263042bdd5..e2c7af21f3e5 100644 --- a/src/components/GrowlNotification/shouldUseExplicitBottomPosition/index.ts +++ b/src/components/GrowlNotification/shouldUseExplicitBottomPosition/index.ts @@ -2,6 +2,9 @@ import type {GrowlPosition} from '@libs/Growl'; import CONST from '@src/CONST'; +// This file is used to determine if we should use an explicit bottom position for the growl notification. +// This is necessary for certain layouts where the growl notification may be obscured by other UI elements, such as a bottom tab bar or a floating action button. +// By using an explicit bottom position, we can ensure that the growl notification is always visible to the user. function shouldUseExplicitBottomPosition(position?: GrowlPosition) { return position === CONST.GROWL.POSITION.BOTTOM_RIGHT; } From bc90bfea31c06196c96fb45d29814e3a527efea8 Mon Sep 17 00:00:00 2001 From: Hans Date: Fri, 16 Oct 2026 11:44:08 +0700 Subject: [PATCH 25/28] Fix reviews --- .../WorkspaceCompanyCardsTableRow.tsx | 6 ------ src/libs/Growl.ts | 17 +++++------------ 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableRow.tsx b/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableRow.tsx index 1f32f2fd8cb0..7e516b19608f 100644 --- a/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableRow.tsx +++ b/src/components/Tables/WorkspaceCompanyCardsTable/WorkspaceCompanyCardsTableRow.tsx @@ -222,12 +222,6 @@ function WorkspaceCompanyCardTableRow({ style={[styles.flexRow, styles.alignItemsCenter, styles.justifyContentEnd, styles.gap3]} {...getCellAccessibilityProps(isTableSemanticsEnabled)} > - {hasErrors && ( - - )} {!isAssigned && canWriteCompanyCards && (