From f48cff175441e2dff69d453c74ac2c1d72e958d5 Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:05:37 +0200 Subject: [PATCH 01/43] feat: setup the ReportDetailsExportPage.tsx and navigation --- src/CONST.ts | 1 + src/ROUTES.ts | 4 ++ src/SCREENS.ts | 2 + .../ModalStackNavigators/index.tsx | 1 + src/libs/Navigation/linkingConfig/config.ts | 1 + src/libs/Navigation/types.ts | 5 +++ src/pages/ReportDetailsPage.tsx | 12 ++++++ .../home/report/ReportDetailsExportPage.tsx | 38 +++++++++++++++++++ 8 files changed, 64 insertions(+) create mode 100644 src/pages/home/report/ReportDetailsExportPage.tsx diff --git a/src/CONST.ts b/src/CONST.ts index 1d6c3a92faa9..46f15c49d2f4 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2123,6 +2123,7 @@ const CONST = { SETTINGS: 'settings', LEAVE_ROOM: 'leaveRoom', PRIVATE_NOTES: 'privateNotes', + EXPORT: 'export', }, EDIT_REQUEST_FIELD: { AMOUNT: 'amount', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index c1fdd68951fa..66f6961b845a 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -277,6 +277,10 @@ const ROUTES = { route: 'r/:reportID/details', getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/details`, backTo), }, + REPORT_WITH_ID_DETAILS_EXPORT: { + route: 'r/:reportID/details/export/:integrationName', + getRoute: (reportID: string, integrationName: ValueOf) => `r/${reportID}/details/export/${integrationName}` as const, + }, REPORT_SETTINGS: { route: 'r/:reportID/settings', getRoute: (reportID: string) => `r/${reportID}/settings` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index f884cca94ef5..5b0d7ff4df42 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -141,6 +141,7 @@ const SCREENS = { TRAVEL: 'Travel', SEARCH_REPORT: 'SearchReport', SETTINGS_CATEGORIES: 'SettingsCategories', + REPORT_EXPORT: 'Report_Export', }, ONBOARDING_MODAL: { ONBOARDING: 'Onboarding', @@ -223,6 +224,7 @@ const SCREENS = { REPORT_DETAILS: { ROOT: 'Report_Details_Root', SHARE_CODE: 'Report_Details_Share_Code', + EXPORT: 'Report_Details_Export', }, WORKSPACE: { diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 0577fdcfc5aa..4f0fc28f698d 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -110,6 +110,7 @@ const ProfileModalStackNavigator = createModalStackNavigator({ [SCREENS.REPORT_DETAILS.ROOT]: () => require('../../../../pages/ReportDetailsPage').default as React.ComponentType, [SCREENS.REPORT_DETAILS.SHARE_CODE]: () => require('../../../../pages/home/report/ReportDetailsShareCodePage').default as React.ComponentType, + [SCREENS.REPORT_DETAILS.EXPORT]: () => require('../../../../pages/home/report/ReportDetailsExportPage').default as React.ComponentType, }); const ReportSettingsModalStackNavigator = createModalStackNavigator({ diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 1b4288a9b3a9..9052d00fc0f5 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -518,6 +518,7 @@ const config: LinkingOptions['config'] = { screens: { [SCREENS.REPORT_DETAILS.ROOT]: ROUTES.REPORT_WITH_ID_DETAILS.route, [SCREENS.REPORT_DETAILS.SHARE_CODE]: ROUTES.REPORT_WITH_ID_DETAILS_SHARE_CODE.route, + [SCREENS.REPORT_DETAILS.EXPORT]: ROUTES.REPORT_WITH_ID_DETAILS_EXPORT.route, }, }, [SCREENS.RIGHT_MODAL.REPORT_SETTINGS]: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index f90a91fe0f19..1f7d13115a2e 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -446,6 +446,11 @@ type ReportDetailsNavigatorParamList = { [SCREENS.REPORT_DETAILS.SHARE_CODE]: { reportID: string; }; + [SCREENS.REPORT_DETAILS.EXPORT]: { + reportID: string; + policyID: string; + integrationName: ValueOf; + }; }; type ReportSettingsNavigatorParamList = { diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 8689801ca3cc..742f492d43a2 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -216,6 +216,18 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD }); } + items.push({ + key: CONST.REPORT_DETAILS_MENU_ITEM.EXPORT, + // TODO: Change to common.export + translationKey: 'workspace.qbo.export', + icon: Expensicons.Upload, + isAnonymousAction: false, + action: () => { + // TODO: Add correct integration + Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS_EXPORT.getRoute(report?.reportID ?? '', 'xero')); + }, + }); + return items; }, [ isSelfDM, diff --git a/src/pages/home/report/ReportDetailsExportPage.tsx b/src/pages/home/report/ReportDetailsExportPage.tsx new file mode 100644 index 000000000000..62d4d24dc916 --- /dev/null +++ b/src/pages/home/report/ReportDetailsExportPage.tsx @@ -0,0 +1,38 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React from 'react'; +import {useOnyx} from 'react-native-onyx'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type {ReportDetailsNavigatorParamList} from '@libs/Navigation/types'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; + +type ReportDetailsExportPageProps = StackScreenProps; + +function ReportDetailsExportPage({route}: ReportDetailsExportPageProps) { + const reportID = route.params.reportID; + const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + return ( + + + + + {report?.policyName} + + + + ); +} + +ReportDetailsExportPage.displayName = 'ReportDetailsExportPage'; + +export default ReportDetailsExportPage; From 6eb2f4d1d704eda60e7fde87fab0b4f919d4c4c8 Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:13:24 +0200 Subject: [PATCH 02/43] feat: add Onyx actions --- src/libs/API/parameters/MarkAsExportedParams.ts | 6 ++++++ src/libs/API/parameters/ReportExportParams.ts | 10 ++++++++++ src/libs/API/parameters/index.ts | 2 ++ src/libs/API/types.ts | 4 ++++ src/libs/actions/Report.ts | 17 +++++++++++++++++ 5 files changed, 39 insertions(+) create mode 100644 src/libs/API/parameters/MarkAsExportedParams.ts create mode 100644 src/libs/API/parameters/ReportExportParams.ts diff --git a/src/libs/API/parameters/MarkAsExportedParams.ts b/src/libs/API/parameters/MarkAsExportedParams.ts new file mode 100644 index 000000000000..03348e856b15 --- /dev/null +++ b/src/libs/API/parameters/MarkAsExportedParams.ts @@ -0,0 +1,6 @@ +type MarkAsExportedParams = { + reportIDList: string; + markedManually: true; +}; + +export default MarkAsExportedParams; diff --git a/src/libs/API/parameters/ReportExportParams.ts b/src/libs/API/parameters/ReportExportParams.ts new file mode 100644 index 000000000000..dc87ce2170c4 --- /dev/null +++ b/src/libs/API/parameters/ReportExportParams.ts @@ -0,0 +1,10 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +type ReportExportParams = { + reportIDList: string; + connectionName: ValueOf; + type: 'MANUAL'; +}; + +export default ReportExportParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 26df06fc6294..610d7164db49 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -231,3 +231,5 @@ export type {default as UpdateSubscriptionAutoRenewParams} from './UpdateSubscri export type {default as UpdateSubscriptionAddNewUsersAutomaticallyParams} from './UpdateSubscriptionAddNewUsersAutomaticallyParams'; export type {default as GenerateSpotnanaTokenParams} from './GenerateSpotnanaTokenParams'; export type {default as UpdateSubscriptionSizeParams} from './UpdateSubscriptionSizeParams'; +export type {default as ReportExportParams} from './ReportExportParams'; +export type {default as MarkAsExportedParams} from './MarkAsExportedParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 8f093ee827c3..3a23364ec5fe 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -227,6 +227,8 @@ const WRITE_COMMANDS = { UPDATE_SUBSCRIPTION_AUTO_RENEW: 'UpdateSubscriptionAutoRenew', UPDATE_SUBSCRIPTION_ADD_NEW_USERS_AUTOMATICALLY: 'UpdateSubscriptionAddNewUsersAutomatically', UPDATE_SUBSCRIPTION_SIZE: 'UpdateSubscriptionSize', + REPORT_EXPORT: 'Report_Export', + MARK_AS_EXPORTED: 'MarkAsExported', } as const; type WriteCommand = ValueOf; @@ -430,6 +432,8 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_UNIT]: Parameters.SetPolicyDistanceRatesUnitParams; [WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_DEFAULT_CATEGORY]: Parameters.SetPolicyDistanceRatesDefaultCategoryParams; [WRITE_COMMANDS.ENABLE_DISTANCE_REQUEST_TAX]: Parameters.SetPolicyDistanceRatesDefaultCategoryParams; + [WRITE_COMMANDS.REPORT_EXPORT]: Parameters.ReportExportParams; + [WRITE_COMMANDS.MARK_AS_EXPORTED]: Parameters.MarkAsExportedParams; // eslint-disable-next-line @typescript-eslint/no-explicit-any [WRITE_COMMANDS.UPDATE_POLICY_CONNECTION_CONFIG]: Parameters.UpdatePolicyConnectionConfigParams; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index b261bb0ade90..da7c785dba6d 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3728,6 +3728,21 @@ function setGroupDraft(newGroupDraft: Partial) { Onyx.merge(ONYXKEYS.NEW_GROUP_CHAT_DRAFT, newGroupDraft); } +function exportToIntegration(reportID: string, connectionName: ValueOf) { + API.write('Report_Export', { + reportIDList: reportID, + connectionName, + type: 'MANUAL', + }); +} + +function markAsManuallyExported(reportID: string) { + API.write('MarkAsExported', { + reportIDList: reportID, + markedManually: true, + }); +} + export { searchInServer, addComment, @@ -3812,4 +3827,6 @@ export { updateLoadingInitialReportAction, clearAddRoomMemberError, clearAvatarErrors, + exportToIntegration, + markAsManuallyExported, }; From 80389e97ea945d9a5f55bdd2049ae0c423d1e885 Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Fri, 21 Jun 2024 14:35:18 +0200 Subject: [PATCH 03/43] feat: getConnectedIntegration --- src/libs/PolicyUtils.ts | 5 +++ src/pages/ReportDetailsPage.tsx | 33 ++++++++++--------- .../accounting/PolicyAccountingPage.tsx | 7 ++-- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index af3f3b264d13..d705ba40f3df 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -495,6 +495,10 @@ function navigateWhenEnableFeature(policyID: string) { }, CONST.WORKSPACE_ENABLE_FEATURE_REDIRECT_DELAY); } +function getConnectedIntegration(policy: Policy | undefined) { + return Object.values(CONST.POLICY.CONNECTIONS.NAME).find((integration) => !!policy?.connections?.[integration]); +} + export { canEditTaxRate, extractPolicyIDFromPath, @@ -502,6 +506,7 @@ export { getActivePolicies, getAdminEmployees, getCleanedTagName, + getConnectedIntegration, getCountOfEnabledTagsOfList, getIneligibleInvitees, getMemberAccountIDsForWorkspace, diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 742f492d43a2..7898f3f2ceeb 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -93,6 +93,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD const shouldExcludeHiddenParticipants = !isGroupChat && !isSystemChat; return ReportUtils.getParticipantsAccountIDsForDisplay(report, shouldExcludeHiddenParticipants); }, [report, isGroupChat, isSystemChat]); + const connectedIntegration = PolicyUtils.getConnectedIntegration(policy); // Get the active chat members by filtering out the pending members with delete action const activeChatMembers = participants.flatMap((accountID) => { @@ -216,37 +217,37 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD }); } - items.push({ - key: CONST.REPORT_DETAILS_MENU_ITEM.EXPORT, - // TODO: Change to common.export - translationKey: 'workspace.qbo.export', - icon: Expensicons.Upload, - isAnonymousAction: false, - action: () => { - // TODO: Add correct integration - Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS_EXPORT.getRoute(report?.reportID ?? '', 'xero')); - }, - }); - + if (policy && connectedIntegration && isPolicyAdmin) { + items.push({ + key: CONST.REPORT_DETAILS_MENU_ITEM.EXPORT, + translationKey: 'workspace.accounting.export', + icon: Expensicons.Upload, + isAnonymousAction: false, + action: () => { + Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS_EXPORT.getRoute(report?.reportID ?? '', connectedIntegration)); + }, + }); + } return items; }, [ isSelfDM, - isSystemChat, isArchivedRoom, isGroupChat, isDefaultRoom, - isThread, isChatThread, isPolicyEmployee, - isPolicyExpenseChat, - isPolicyAdmin, isUserCreatedPolicyRoom, participants.length, report, + isSystemChat, + isPolicyExpenseChat, isMoneyRequestReport, isInvoiceReport, + isThread, isChatRoom, policy, + connectedIntegration, + isPolicyAdmin, activeChatMembers.length, session, leaveChat, diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx index 78dc434b3d2c..86d90725165b 100644 --- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx +++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx @@ -23,11 +23,11 @@ import ThreeDotsMenu from '@components/ThreeDotsMenu'; import type ThreeDotsMenuProps from '@components/ThreeDotsMenu/types'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import usePermissions from '@hooks/usePermissions'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import {hasSynchronizationError, removePolicyConnection, syncConnection} from '@libs/actions/connections'; +import * as PolicyUtils from '@libs/PolicyUtils'; import {findCurrentXeroOrganization, getCurrentXeroOrganizationName, getXeroTenants} from '@libs/PolicyUtils'; import Navigation from '@navigation/Navigation'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; @@ -110,7 +110,6 @@ function PolicyAccountingPage({policy, connectionSyncProgress}: PolicyAccounting const styles = useThemeStyles(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const {canUseXeroIntegration} = usePermissions(); const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); const [threeDotsMenuPosition, setThreeDotsMenuPosition] = useState({horizontal: 0, vertical: 0}); const [isDisconnectModalOpen, setIsDisconnectModalOpen] = useState(false); @@ -124,8 +123,8 @@ function PolicyAccountingPage({policy, connectionSyncProgress}: PolicyAccounting isValid(lastSyncProgressDate) && differenceInMinutes(new Date(), lastSyncProgressDate) < CONST.POLICY.CONNECTIONS.SYNC_STAGE_TIMEOUT_MINUTES; - const accountingIntegrations = Object.values(CONST.POLICY.CONNECTIONS.NAME).filter((name) => !(name === CONST.POLICY.CONNECTIONS.NAME.XERO && !canUseXeroIntegration)); - const connectedIntegration = accountingIntegrations.find((integration) => !!policy?.connections?.[integration]) ?? connectionSyncProgress?.connectionName; + const accountingIntegrations = Object.values(CONST.POLICY.CONNECTIONS.NAME); + const connectedIntegration = PolicyUtils.getConnectedIntegration(policy) ?? connectionSyncProgress?.connectionName; const policyID = policy?.id ?? '-1'; const successfulDate = policy?.connections?.quickbooksOnline?.lastSync?.successfulDate; const formattedDate = useMemo(() => (successfulDate ? new Date(successfulDate) : new Date()), [successfulDate]); From bdb29173d81a9f76de871a274fc1e77ea2fec757 Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Fri, 21 Jun 2024 15:33:18 +0200 Subject: [PATCH 04/43] feat: use SelectionScreen in ReportDetailsExportPage --- src/CONST.ts | 4 + src/components/SelectionScreen.tsx | 3 + src/languages/en.ts | 2 + src/libs/ReportUtils.ts | 20 ++++- src/pages/ReportDetailsPage.tsx | 2 +- .../home/report/ReportDetailsExportPage.tsx | 86 +++++++++++++++---- 6 files changed, 97 insertions(+), 20 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 46f15c49d2f4..f24d41e8575a 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -880,6 +880,10 @@ const CONST = { INDIVIDUAL: 'individual', BUSINESS: 'policy', }, + EXPORT_OPTIONS: { + EXPORT_TO_INTEGRATION: 'exportToIntegration', + MARK_AS_EXPORTED: 'markAsExported', + }, }, NEXT_STEP: { FINISHED: 'Finished!', diff --git a/src/components/SelectionScreen.tsx b/src/components/SelectionScreen.tsx index 20bc30f1d937..1ba919d2c6d1 100644 --- a/src/components/SelectionScreen.tsx +++ b/src/components/SelectionScreen.tsx @@ -16,6 +16,8 @@ import type UserListItem from './SelectionList/UserListItem'; type SelectorType = ListItem & { value: string; + + onPress?: () => void; }; type SelectionScreenProps = { @@ -82,6 +84,7 @@ function SelectionScreen({ const policy = PolicyUtils.getPolicy(policyID ?? ''); const isConnectionEmpty = isEmpty(policy.connections?.[connectionName]); + console.log('policy', policy, 'isConnectionEmpty', isConnectionEmpty, 'policy.connections?', policy.connections, 'connectionName', connectionName); return ( { return Object.values(allReports ?? {}).find(isChatUsedForOnboarding); } +function getIntegrationIcon(integrationName: string) { + return XeroSquare; +} + +function canBeExported(report: OnyxEntry) { + return true; +} + +function isExported(report: OnyxEntry) { + return false; +} + export { addDomainToShortMention, areAllRequestsBeingSmartScanned, @@ -7240,6 +7253,9 @@ export { createDraftWorkspaceAndNavigateToConfirmationScreen, isChatUsedForOnboarding, getChatUsedForOnboarding, + getIntegrationIcon, + canBeExported, + isExported, }; export type { diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 7898f3f2ceeb..52b6245e47e5 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -220,7 +220,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD if (policy && connectedIntegration && isPolicyAdmin) { items.push({ key: CONST.REPORT_DETAILS_MENU_ITEM.EXPORT, - translationKey: 'workspace.accounting.export', + translationKey: 'common.export', icon: Expensicons.Upload, isAnonymousAction: false, action: () => { diff --git a/src/pages/home/report/ReportDetailsExportPage.tsx b/src/pages/home/report/ReportDetailsExportPage.tsx index 62d4d24dc916..1f121c1a73b6 100644 --- a/src/pages/home/report/ReportDetailsExportPage.tsx +++ b/src/pages/home/report/ReportDetailsExportPage.tsx @@ -1,35 +1,87 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import React from 'react'; +import React, {useState} from 'react'; import {useOnyx} from 'react-native-onyx'; -import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import ScrollView from '@components/ScrollView'; -import Text from '@components/Text'; +import UserListItem from '@components/SelectionList/UserListItem'; +import type {SelectorType} from '@components/SelectionScreen'; +import SelectionScreen from '@components/SelectionScreen'; import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; +import * as Report from '@libs/actions/Report'; +import Navigation from '@libs/Navigation/Navigation'; import type {ReportDetailsNavigatorParamList} from '@libs/Navigation/types'; +import * as ReportUtils from '@libs/ReportUtils'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; type ReportDetailsExportPageProps = StackScreenProps; function ReportDetailsExportPage({route}: ReportDetailsExportPageProps) { const reportID = route.params.reportID; const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); + const policyID = report?.policyID; const {translate} = useLocalize(); - const styles = useThemeStyles(); + const [shouldShowModal, setShouldShowModal] = useState(false); + const integrationName = route?.params?.integrationName; + const iconToDisplay = ReportUtils.getIntegrationIcon(integrationName); + const canBeExported = ReportUtils.canBeExported(report); + const integrationText = CONST.POLICY.CONNECTIONS.NAME.QBO ? translate('workspace.accounting.qbo') : translate('workspace.accounting.xero'); + + const exportSelectorOptions: SelectorType[] = [ + { + value: CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION, + text: integrationText, + icons: [ + { + source: iconToDisplay, + type: 'avatar', + }, + ], + isDisabled: !canBeExported, + onPress: () => { + if (ReportUtils.isExported(report)) { + setShouldShowModal(true); + } else { + Report.exportToIntegration(reportID, integrationName); + } + }, + }, + { + value: CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED, + text: translate('common.markAsExported'), + icons: [ + { + source: iconToDisplay, + type: 'avatar', + }, + ], + isDisabled: !canBeExported, + onPress: () => { + if (ReportUtils.isExported(report)) { + setShouldShowModal(true); + } else { + Report.markAsManuallyExported(reportID); + } + }, + }, + ]; return ( - - - - - {report?.policyName} - - - + Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID))} + title="common.export" + connectionName={integrationName} + onSelectRow={(option) => { + option.onPress?.(); + }} + /> ); } From 87ea4022308315a45131d71d25a2c357fbc18b7b Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Fri, 21 Jun 2024 15:44:22 +0200 Subject: [PATCH 05/43] feat: add ConfirmModal --- src/languages/en.ts | 10 ++++- src/libs/ReportActionsUtils.ts | 5 +++ src/libs/ReportUtils.ts | 24 +++++++--- .../home/report/ReportDetailsExportPage.tsx | 44 ++++++++++++------- 4 files changed, 62 insertions(+), 21 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index afd0fbb51efb..d55e49f39566 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -340,7 +340,8 @@ export default { drafts: 'Drafts', finished: 'Finished', export: 'Export', - markAsExported: 'Mark as exported', + markAsExported: 'Mark as manually entered', + exportIntegrationSelected: ({integrationName}: {integrationName: string}) => `Export to ${integrationName}`, }, location: { useCurrent: 'Use current location', @@ -3310,4 +3311,11 @@ export default { additionalInfoTitle: 'What software are you moving to and why?', additionalInfoInputLabel: 'Your response', }, + exportAgainModal: { + title: 'Careful!', + description: ({reportName, integrationName}: {reportName: string; integrationName: string}) => + `The following reports have already been exported to ${integrationName}:\n\n${reportName}\n\nAre you sure you want to export them again?`, + confirmText: 'Yes, export again', + cancelText: 'Cancel', + }, } satisfies TranslationBase; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index a12e9ce61a63..d233f8b8bda1 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -146,6 +146,10 @@ function isModifiedExpenseAction(reportAction: OnyxInputOrEntry | return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIED_EXPENSE; } +function isExportIntegrationAction(reportAction: OnyxInputOrEntry): boolean { + return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_INTEGRATION; +} + /** * We are in the process of deprecating reportAction.originalMessage and will be setting the db version of "message" to reportAction.message in the future see: https://github.com/Expensify/App/issues/39797 * In the interim, we must check to see if we have an object or array for the reportAction.message, if we have an array we will use the originalMessage as this means we have not yet migrated. @@ -1278,6 +1282,7 @@ export { isCreatedTaskReportAction, isDeletedAction, isDeletedParentAction, + isExportIntegrationAction, isMessageDeleted, isModifiedExpenseAction, isMoneyRequestAction, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b594e3d6c1e4..97d31f54e136 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -9,7 +9,7 @@ import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {TupleToUnion, ValueOf} from 'type-fest'; import type {FileObject} from '@components/AttachmentModal'; -import {FallbackAvatar, XeroSquare} from '@components/Icon/Expensicons'; +import {FallbackAvatar, QBOSquare, XeroSquare} from '@components/Icon/Expensicons'; import * as defaultGroupAvatars from '@components/Icon/GroupDefaultAvatars'; import * as defaultWorkspaceAvatars from '@components/Icon/WorkspaceDefaultAvatars'; import type {MoneyRequestAmountInputProps} from '@components/MoneyRequestAmountInput'; @@ -6968,16 +6968,30 @@ function getChatUsedForOnboarding(): OnyxEntry { return Object.values(allReports ?? {}).find(isChatUsedForOnboarding); } -function getIntegrationIcon(integrationName: string) { - return XeroSquare; +function getIntegrationIcon(integrationName: ValueOf) { + if (integrationName === CONST.POLICY.CONNECTIONS.NAME.XERO) { + return XeroSquare; + } + if (integrationName === CONST.POLICY.CONNECTIONS.NAME.QBO) { + return QBOSquare; + } } function canBeExported(report: OnyxEntry) { - return true; + if (!report?.statusNum) { + return false; + } + const isCorrectState = [CONST.REPORT.STATUS_NUM.APPROVED, CONST.REPORT.STATUS_NUM.CLOSED, CONST.REPORT.STATUS_NUM.REIMBURSED].some((status) => status === report.statusNum); + console.log('canBeExported', isExpenseReport(report), isCorrectState); + return isExpenseReport(report) && isCorrectState; } function isExported(report: OnyxEntry) { - return false; + if (!report) { + return false; + } + const reportActions = Object.values(ReportActionsUtils.getAllReportActions(report?.reportID)); + return reportActions.some((action) => ReportActionsUtils.isExportIntegrationAction(action)); } export { diff --git a/src/pages/home/report/ReportDetailsExportPage.tsx b/src/pages/home/report/ReportDetailsExportPage.tsx index 1f121c1a73b6..87a8cfb7d80e 100644 --- a/src/pages/home/report/ReportDetailsExportPage.tsx +++ b/src/pages/home/report/ReportDetailsExportPage.tsx @@ -1,6 +1,7 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useState} from 'react'; import {useOnyx} from 'react-native-onyx'; +import ConfirmModal from '@components/ConfirmModal'; import UserListItem from '@components/SelectionList/UserListItem'; import type {SelectorType} from '@components/SelectionScreen'; import SelectionScreen from '@components/SelectionScreen'; @@ -67,21 +68,34 @@ function ReportDetailsExportPage({route}: ReportDetailsExportPageProps) { ]; return ( - Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID))} - title="common.export" - connectionName={integrationName} - onSelectRow={(option) => { - option.onPress?.(); - }} - /> + <> + Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID))} + title="common.export" + connectionName={integrationName} + onSelectRow={(option) => { + option.onPress?.(); + }} + /> + {shouldShowModal && ( + setShouldShowModal(false)} + onCancel={() => setShouldShowModal(false)} + prompt={translate('exportAgainModal.description', {reportName: report?.reportName ?? '', integrationName})} + confirmText={translate('exportAgainModal.confirmText')} + cancelText={translate('exportAgainModal.cancelText')} + isVisible + /> + )} + ); } From 01b5268dd333049199846254af3ac2a5a23bffa8 Mon Sep 17 00:00:00 2001 From: Jakub Kosmydel <104823336+kosmydel@users.noreply.github.com> Date: Fri, 21 Jun 2024 17:31:55 +0200 Subject: [PATCH 06/43] feat: export in ReportPreview --- .../ExportWithDropdownMenu.tsx | 42 +++++++++++++++++++ .../ReportActionItem/ReportPreview.tsx | 41 +++++++++++++++++- src/languages/en.ts | 4 +- src/libs/ReportUtils.ts | 15 ++++++- .../home/report/ReportDetailsExportPage.tsx | 4 +- 5 files changed, 99 insertions(+), 7 deletions(-) create mode 100644 src/components/ReportActionItem/ExportWithDropdownMenu.tsx diff --git a/src/components/ReportActionItem/ExportWithDropdownMenu.tsx b/src/components/ReportActionItem/ExportWithDropdownMenu.tsx new file mode 100644 index 000000000000..5da69c736f22 --- /dev/null +++ b/src/components/ReportActionItem/ExportWithDropdownMenu.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import type {ValueOf} from 'type-fest'; +import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; +import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; +import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; + +type ExportWithDropdownMenuProps = { + dropdownOptions: Array>; + + integrationName: ValueOf; +}; + +function ExportWithDropdownMenu({dropdownOptions, integrationName}: ExportWithDropdownMenuProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {isSmallScreenWidth} = useResponsiveLayout(); + + return ( + {}} + onOptionSelected={() => { + // do logic with API calls depending on the option, "export" or "markAsExported" + }} + options={dropdownOptions} + customText={translate('workspace.common.exportIntegrationSelected', {integrationName})} + style={[isSmallScreenWidth && styles.flexGrow1, isSmallScreenWidth && styles.mb3]} + buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} + /> + ); +} + +export default ExportWithDropdownMenu; diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index daa7e24709c2..f57dfd50f68a 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -24,6 +24,7 @@ import ControlSelection from '@libs/ControlSelection'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import Navigation from '@libs/Navigation/Navigation'; +import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportUtils from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; @@ -37,6 +38,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Policy, Report, ReportAction, Transaction, TransactionViolations, UserWallet} from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; +import ExportWithDropdownMenu from './ExportWithDropdownMenu'; import type {PendingMessageProps} from './MoneyRequestPreview/types'; import ReportActionItemImages from './ReportActionItemImages'; @@ -319,6 +321,37 @@ function ReportPreview({ }; }, [formattedMerchant, formattedDescription, moneyRequestComment, translate, numberOfRequests, numberOfScanningReceipts, numberOfPendingRequests]); + /* + * Manual export part + */ + + const connectedIntegration = PolicyUtils.getConnectedIntegration(policy); + const hasIntegrationAutoSync = (connectedIntegration && policy?.connections?.[connectedIntegration]?.config?.autoSync.enabled) ?? false; + const iconToDisplay = ReportUtils.getIntegrationIcon(connectedIntegration); + // TODO: Implement the logic to disable the dropdown options + const shouldIntegrationDropdownOptionsBeDisabled = false; + // TODO: Check if we can merge it with ReportDetailsExportPage options + const integrationsDropdownOptions = useMemo( + () => [ + { + value: CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION, + text: translate('common.export'), + icon: iconToDisplay, + disabled: shouldIntegrationDropdownOptionsBeDisabled, + }, + { + value: CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED, + text: translate('workspace.common.markAsExported'), + icon: iconToDisplay, + disabled: shouldIntegrationDropdownOptionsBeDisabled, + }, + ], + [translate, iconToDisplay, shouldIntegrationDropdownOptionsBeDisabled], + ); + + const shouldShowExportIntegrationButton = !hasIntegrationAutoSync && shouldShowPayButton && !shouldShowSubmitButton && connectedIntegration; + console.log({shouldShowExportIntegrationButton, hasIntegrationAutoSync, shouldShowPayButton, shouldShowSubmitButton, connectedIntegration}); + return ( - {shouldShowSettlementButton && ( + {shouldShowSettlementButton && !shouldShowExportIntegrationButton && ( )} + {shouldShowExportIntegrationButton && ( + + )} {shouldShowSubmitButton && (