Skip to content

Commit

Permalink
Merge pull request #44170 from software-mansion-labs/@kosmydel/qbo-ex…
Browse files Browse the repository at this point in the history
…port/manual-exports

[QBO Export] Manual exports
  • Loading branch information
arosiclair authored Jul 16, 2024
2 parents 936ae92 + ebb2c20 commit 303c398
Show file tree
Hide file tree
Showing 38 changed files with 798 additions and 45 deletions.
245 changes: 245 additions & 0 deletions assets/images/LaptopwithSecondScreenandHourglass.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,10 @@ const CONST = {
INDIVIDUAL: 'individual',
BUSINESS: 'policy',
},
EXPORT_OPTIONS: {
EXPORT_TO_INTEGRATION: 'exportToIntegration',
MARK_AS_EXPORTED: 'markAsExported',
},
},
NEXT_STEP: {
FINISHED: 'Finished!',
Expand Down Expand Up @@ -2456,6 +2460,7 @@ const CONST = {
SETTINGS: 'settings',
LEAVE_ROOM: 'leaveRoom',
PRIVATE_NOTES: 'privateNotes',
EXPORT: 'export',
DELETE: 'delete',
MARK_AS_INCOMPLETE: 'markAsIncomplete',
CANCEL_PAYMENT: 'cancelPayment',
Expand Down
4 changes: 4 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,9 @@ const ONYXKEYS = {
/** Stores info during review duplicates flow */
REVIEW_DUPLICATES: 'reviewDuplicates',

/** Stores the last export method for policy */
LAST_EXPORT_METHOD: 'lastExportMethod',

/** Stores the information about the state of issuing a new card */
ISSUE_NEW_EXPENSIFY_CARD: 'issueNewExpensifyCard',

Expand Down Expand Up @@ -748,6 +751,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION]: boolean;
[ONYXKEYS.FOCUS_MODE_NOTIFICATION]: boolean;
[ONYXKEYS.NVP_LAST_PAYMENT_METHOD]: OnyxTypes.LastPaymentMethod;
[ONYXKEYS.LAST_EXPORT_METHOD]: OnyxTypes.LastExportMethod;
[ONYXKEYS.NVP_RECENT_WAYPOINTS]: OnyxTypes.RecentWaypoint[];
[ONYXKEYS.NVP_INTRO_SELECTED]: OnyxTypes.IntroSelected;
[ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES]: OnyxTypes.LastSelectedDistanceRates;
Expand Down
6 changes: 5 additions & 1 deletion src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type CONST from './CONST';
import type {IOUAction, IOUType} from './CONST';
import type {IOURequestType} from './libs/actions/IOU';
import type {AuthScreensParamList} from './libs/Navigation/types';
import type {SageIntacctMappingName} from './types/onyx/Policy';
import type {ConnectionName, SageIntacctMappingName} from './types/onyx/Policy';
import type {SearchQuery} from './types/onyx/SearchResults';
import type AssertTypesNotEqual from './types/utils/AssertTypesNotEqual';

Expand Down Expand Up @@ -286,6 +286,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/:connectionName',
getRoute: (reportID: string, connectionName: ConnectionName) => `r/${reportID}/details/export/${connectionName}` as const,
},
REPORT_SETTINGS: {
route: 'r/:reportID/settings',
getRoute: (reportID: string) => `r/${reportID}/settings` as const,
Expand Down
2 changes: 2 additions & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ const SCREENS = {
SEARCH_REPORT: 'SearchReport',
SETTINGS_CATEGORIES: 'SettingsCategories',
RESTRICTED_ACTION: 'RestrictedAction',
REPORT_EXPORT: 'Report_Export',
},
ONBOARDING_MODAL: {
ONBOARDING: 'Onboarding',
Expand Down Expand Up @@ -240,6 +241,7 @@ const SCREENS = {
REPORT_DETAILS: {
ROOT: 'Report_Details_Root',
SHARE_CODE: 'Report_Details_Share_Code',
EXPORT: 'Report_Details_Export',
},

WORKSPACE: {
Expand Down
12 changes: 11 additions & 1 deletion src/components/ButtonWithDropdownMenu/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ type WorkspaceDistanceRatesBulkActionType = DeepValueOf<typeof CONST.POLICY.BULK

type WorkspaceTaxRatesBulkActionType = DeepValueOf<typeof CONST.POLICY.BULK_ACTION_TYPES>;

type ReportExportType = DeepValueOf<typeof CONST.REPORT.EXPORT_OPTIONS>;

type DropdownOption<TValueType> = {
value: TValueType;
text: string;
Expand Down Expand Up @@ -84,4 +86,12 @@ type ButtonWithDropdownMenuProps<TValueType> = {
isSplitButton?: boolean;
};

export type {PaymentType, WorkspaceMemberBulkActionType, WorkspaceDistanceRatesBulkActionType, DropdownOption, ButtonWithDropdownMenuProps, WorkspaceTaxRatesBulkActionType};
export type {
PaymentType,
WorkspaceMemberBulkActionType,
WorkspaceDistanceRatesBulkActionType,
DropdownOption,
ButtonWithDropdownMenuProps,
WorkspaceTaxRatesBulkActionType,
ReportExportType,
};
35 changes: 26 additions & 9 deletions src/components/ConfirmationPage.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import React from 'react';
import type {TextStyle} from 'react-native';
import type {TextStyle, ViewStyle} from 'react-native';
import {View} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
import isIllustrationLottieAnimation from '@libs/isIllustrationLottieAnimation';
import type IconAsset from '@src/types/utils/IconAsset';
import Button from './Button';
import FixedFooter from './FixedFooter';
import ImageSVG from './ImageSVG';
import Lottie from './Lottie';
import LottieAnimations from './LottieAnimations';
import type DotLottieAnimation from './LottieAnimations/types';
import Text from './Text';

type ConfirmationPageProps = {
/** The asset to render */
animation?: DotLottieAnimation;
illustration?: DotLottieAnimation | IconAsset;

/** Heading of the confirmation page */
heading: string;
Expand All @@ -31,31 +34,45 @@ type ConfirmationPageProps = {
/** Additional style for the heading */
headingStyle?: TextStyle;

/** Additional style for the animation */
illustrationStyle?: ViewStyle;

/** Additional style for the description */
descriptionStyle?: TextStyle;
};

function ConfirmationPage({
animation = LottieAnimations.Fireworks,
illustration = LottieAnimations.Fireworks,
heading,
description,
buttonText = '',
onButtonPress = () => {},
shouldShowButton = false,
headingStyle,
illustrationStyle,
descriptionStyle,
}: ConfirmationPageProps) {
const styles = useThemeStyles();
const isLottie = isIllustrationLottieAnimation(illustration);

return (
<>
<View style={[styles.screenCenteredContainer, styles.alignItemsCenter]}>
<Lottie
source={animation}
autoPlay
loop
style={styles.confirmationAnimation}
/>
{isLottie ? (
<Lottie
source={illustration}
autoPlay
loop
style={[styles.confirmationAnimation, illustrationStyle]}
/>
) : (
<View style={[styles.confirmationAnimation, illustrationStyle]}>
<ImageSVG
src={illustration}
contentFit="contain"
/>
</View>
)}
<Text style={[styles.textHeadline, styles.textAlignCenter, styles.mv2, headingStyle]}>{heading}</Text>
<Text style={[styles.textAlignCenter, descriptionStyle]}>{description}</Text>
</View>
Expand Down
2 changes: 2 additions & 0 deletions src/components/Icon/Illustrations.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ExpensifyCardIllustration from '@assets/images/expensifyCard/cardIllustration.svg';
import LaptopwithSecondScreenandHourglass from '@assets/images/LaptopwithSecondScreenandHourglass.svg';
import Abracadabra from '@assets/images/product-illustrations/abracadabra.svg';
import BankArrowPink from '@assets/images/product-illustrations/bank-arrow--pink.svg';
import BankMouseGreen from '@assets/images/product-illustrations/bank-mouse--green.svg';
Expand Down Expand Up @@ -146,6 +147,7 @@ export {
PinkBill,
CreditCardsNew,
InvoiceBlue,
LaptopwithSecondScreenandHourglass,
LockOpen,
Luggage,
MoneyIntoWallet,
Expand Down
29 changes: 26 additions & 3 deletions src/components/MoneyReportHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
Expand All @@ -34,6 +35,7 @@ import type {MoneyRequestHeaderStatusBarProps} from './MoneyRequestHeaderStatusB
import type {ActionHandledType} from './ProcessMoneyReportHoldMenu';
import ProcessMoneyReportHoldMenu from './ProcessMoneyReportHoldMenu';
import ProcessMoneyRequestHoldMenu from './ProcessMoneyRequestHoldMenu';
import ExportWithDropdownMenu from './ReportActionItem/ExportWithDropdownMenu';
import SettlementButton from './SettlementButton';

type MoneyReportHeaderProps = {
Expand Down Expand Up @@ -99,6 +101,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const canAllowSettlement = ReportUtils.hasUpdatedTotal(moneyRequestReport, policy);
const policyType = policy?.type;
const isDraft = ReportUtils.isOpenExpenseReport(moneyRequestReport);
const connectedIntegration = PolicyUtils.getConnectedIntegration(policy);

const navigateBackToAfterDelete = useRef<Route>();
const hasScanningReceipt = ReportUtils.getTransactionsWithReceipts(moneyRequestReport?.reportID).some((t) => TransactionUtils.isReceiptBeingScanned(t));
Expand All @@ -113,15 +116,19 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea

const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(moneyRequestReport);

const shouldShowSettlementButton = (shouldShowPayButton || shouldShowApproveButton) && !allHavePendingRTERViolation;

const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0 && !allHavePendingRTERViolation;

const shouldShowExportIntegrationButton = !shouldShowPayButton && !shouldShowSubmitButton && connectedIntegration && !!policy;

const shouldShowSettlementButton = (shouldShowPayButton || shouldShowApproveButton) && !allHavePendingRTERViolation && !shouldShowExportIntegrationButton;

const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport);
const shouldShowMarkAsCashButton = isDraft && allHavePendingRTERViolation;
const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE;
const shouldShowStatusBar = allHavePendingRTERViolation || hasOnlyHeldExpenses || hasScanningReceipt;
const shouldShowNextStep = !ReportUtils.isClosedExpenseReportWithNoExpenses(moneyRequestReport) && isFromPaidPolicy && !!nextStep?.message?.length && !shouldShowStatusBar;
const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep || allHavePendingRTERViolation;
const shouldShowAnyButton =
shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep || allHavePendingRTERViolation || shouldShowExportIntegrationButton;
const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport);
const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, moneyRequestReport.currency);
const [nonHeldAmount, fullAmount] = ReportUtils.getNonHeldAndFullAmount(moneyRequestReport, policy);
Expand Down Expand Up @@ -275,6 +282,15 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
/>
</View>
)}
{shouldShowExportIntegrationButton && !shouldUseNarrowLayout && (
<View style={[styles.pv2]}>
<ExportWithDropdownMenu
policy={policy}
report={moneyRequestReport}
connectionName={connectedIntegration}
/>
</View>
)}
{shouldShowSubmitButton && !shouldUseNarrowLayout && (
<View style={styles.pv2}>
<Button
Expand Down Expand Up @@ -319,6 +335,13 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
isLoading={!isOffline && !canAllowSettlement}
/>
)}
{shouldShowExportIntegrationButton && shouldUseNarrowLayout && (
<ExportWithDropdownMenu
policy={policy}
report={moneyRequestReport}
connectionName={connectedIntegration}
/>
)}
{shouldShowSubmitButton && shouldUseNarrowLayout && (
<Button
medium
Expand Down
130 changes: 130 additions & 0 deletions src/components/ReportActionItem/ExportWithDropdownMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import React, {useCallback, useMemo, useState} from 'react';
import {useOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu';
import type {DropdownOption, ReportExportType} from '@components/ButtonWithDropdownMenu/types';
import ConfirmModal from '@components/ConfirmModal';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import * as PolicyActions from '@libs/actions/Policy/Policy';
import * as ReportActions from '@libs/actions/Report';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReportUtils from '@libs/ReportUtils';
import type {ExportType} from '@pages/home/report/ReportDetailsExportPage';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Policy, Report} from '@src/types/onyx';
import type {ConnectionName} from '@src/types/onyx/Policy';

type ExportWithDropdownMenuProps = {
policy: OnyxEntry<Policy>;

report: OnyxEntry<Report>;

connectionName: ConnectionName;
};

function ExportWithDropdownMenu({policy, report, connectionName}: ExportWithDropdownMenuProps) {
const reportID = report?.reportID;
const styles = useThemeStyles();
const {translate} = useLocalize();
const {isSmallScreenWidth} = useResponsiveLayout();
const [modalStatus, setModalStatus] = useState<ExportType | null>(null);
const [exportMethods] = useOnyx(ONYXKEYS.LAST_EXPORT_METHOD);
const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`);

const iconToDisplay = ReportUtils.getIntegrationIcon(connectionName);
const canBeExported = ReportUtils.canBeExported(report);
const isExported = ReportUtils.isExported(reportActions);
const hasIntegrationAutoSync = PolicyUtils.hasIntegrationAutoSync(policy, connectionName);

const dropdownOptions: Array<DropdownOption<ReportExportType>> = useMemo(() => {
const optionTemplate = {
icon: iconToDisplay,
disabled: !canBeExported,
displayInDefaultIconColor: true,
};
const options = [
{
value: CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION,
text: translate('workspace.common.exportIntegrationSelected', connectionName),
...optionTemplate,
},
{
value: CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED,
text: translate('workspace.common.markAsExported'),
...optionTemplate,
},
];
const exportMethod = exportMethods?.[report?.policyID ?? ''] ?? null;
if (exportMethod) {
options.sort((method) => (method.value === exportMethod ? -1 : 0));
}
return options;
// We do not include exportMethods not to re-render the component when the preffered export method changes
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [canBeExported, iconToDisplay, connectionName, report?.policyID, translate]);

const confirmExport = useCallback(() => {
setModalStatus(null);
if (!reportID) {
return;
}
if (modalStatus === CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION) {
ReportActions.exportToIntegration(reportID, connectionName);
} else if (modalStatus === CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED) {
ReportActions.markAsManuallyExported(reportID);
}
}, [connectionName, modalStatus, reportID]);

const savePreferredExportMethod = (value: ReportExportType) => {
if (!report?.policyID) {
return;
}
PolicyActions.savePreferredExportMethod(report?.policyID, value);
};

return (
<>
<ButtonWithDropdownMenu<ReportExportType>
success={!hasIntegrationAutoSync}
pressOnEnter
shouldAlwaysShowDropdownMenu
anchorAlignment={{
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT,
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP,
}}
onPress={(_, value) => {
if (isExported) {
setModalStatus(value);
return;
}
if (!reportID) {
return;
}
if (value === CONST.REPORT.EXPORT_OPTIONS.EXPORT_TO_INTEGRATION) {
ReportActions.exportToIntegration(reportID, connectionName);
} else if (value === CONST.REPORT.EXPORT_OPTIONS.MARK_AS_EXPORTED) {
ReportActions.markAsManuallyExported(reportID);
}
}}
onOptionSelected={({value}) => savePreferredExportMethod(value)}
options={dropdownOptions}
style={[isSmallScreenWidth && styles.flexGrow1]}
buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM}
/>
<ConfirmModal
title={translate('workspace.exportAgainModal.title')}
onConfirm={confirmExport}
onCancel={() => setModalStatus(null)}
prompt={translate('workspace.exportAgainModal.description', report?.reportName ?? '', connectionName)}
confirmText={translate('workspace.exportAgainModal.confirmText')}
cancelText={translate('workspace.exportAgainModal.cancelText')}
isVisible={!!modalStatus}
/>
</>
);
}

export default ExportWithDropdownMenu;
Loading

0 comments on commit 303c398

Please sign in to comment.