diff --git a/src/CONST.ts b/src/CONST.ts index 3bbc1c889448b..3ac42d8cc992d 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -776,6 +776,7 @@ const CONST = { PER_DIEM: 'newDotPerDiem', NEWDOT_MERGE_ACCOUNTS: 'newDotMergeAccounts', NEWDOT_MANAGER_MCTEST: 'newDotManagerMcTest', + NEWDOT_PDF_EXPORT: 'newDotPDFExport', NEWDOT_INTERNATIONAL_DEPOSIT_BANK_ACCOUNT: 'newDotInternationalDepositBankAccount', NSQS: 'nsqs', CUSTOM_RULES: 'customRules', @@ -3494,7 +3495,8 @@ const CONST = { SETTINGS: 'settings', LEAVE_ROOM: 'leaveRoom', PRIVATE_NOTES: 'privateNotes', - DOWNLOAD: 'download', + DOWNLOAD_CSV: 'downloadCSV', + DOWNLOAD_PDF: 'downloadPDF', EXPORT: 'export', DELETE: 'delete', MARK_AS_INCOMPLETE: 'markAsIncomplete', @@ -3502,6 +3504,7 @@ const CONST = { UNAPPROVE: 'unapprove', DEBUG: 'debug', GO_TO_WORKSPACE: 'goToWorkspace', + ERROR: 'error', TRACK: { SUBMIT: 'submit', CATEGORIZE: 'categorize', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 2572883b1849e..5399699aaabbb 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -569,6 +569,8 @@ const ONYXKEYS = { /** Whether the bank account chosen for Expensify Card in on verification waitlist */ NVP_EXPENSIFY_ON_CARD_WAITLIST: 'nvp_expensify_onCardWaitlist_', + NVP_EXPENSIFY_REPORT_PDFFILENAME: 'nvp_expensify_report_PDFFilename_', + /** Stores the information about the state of issuing a new card */ ISSUE_NEW_EXPENSIFY_CARD: 'issueNewExpensifyCard_', }, @@ -906,6 +908,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.OLD_POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags; [ONYXKEYS.COLLECTION.SELECTED_TAB]: OnyxTypes.SelectedTabRequest; [ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT]: string; + [ONYXKEYS.COLLECTION.NVP_EXPENSIFY_REPORT_PDFFILENAME]: string; [ONYXKEYS.COLLECTION.NEXT_STEP]: OnyxTypes.ReportNextStep; [ONYXKEYS.COLLECTION.POLICY_JOIN_MEMBER]: OnyxTypes.PolicyJoinMember; [ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS]: OnyxTypes.PolicyConnectionSyncProgress; diff --git a/src/languages/en.ts b/src/languages/en.ts index 97d54c3f7d270..dc55db4db9247 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -523,6 +523,8 @@ const translations = { subrate: 'Subrate', perDiem: 'Per diem', validate: 'Validate', + downloadAsPDF: 'Download as PDF', + downloadAsCSV: 'Download as CSV', help: 'Help', expenseReports: 'Expense Reports', rateOutOfPolicy: 'Rate out of policy', @@ -1745,6 +1747,10 @@ const translations = { }, reportDetailsPage: { inWorkspace: ({policyName}: ReportPolicyNameParams) => `in ${policyName}`, + generatingPDF: 'Generating PDF', + waitForPDF: 'Please wait while we generate the PDF', + errorPDF: 'There was an error when trying to generate your PDF.', + generatedPDF: 'Your report PDF has been generated!', }, reportDescriptionPage: { roomDescription: 'Room description', diff --git a/src/languages/es.ts b/src/languages/es.ts index f2d22619a49c6..748c047fd42d8 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -514,6 +514,8 @@ const translations = { subrate: 'Subtasa', perDiem: 'Per diem', validate: 'Validar', + downloadAsPDF: 'Descargar como PDF', + downloadAsCSV: 'Descargar como CSV', help: 'Ayuda', expenseReports: 'Informes de Gastos', rateOutOfPolicy: 'Tasa fuera de póliza', @@ -1747,6 +1749,10 @@ const translations = { }, reportDetailsPage: { inWorkspace: ({policyName}: ReportPolicyNameParams) => `en ${policyName}`, + generatingPDF: 'Creando PDF', + waitForPDF: 'Por favor, espera mientras creamos el PDF', + errorPDF: 'Ocurrió un error al crear el PDF.', + generatedPDF: 'Tu informe PDF ha sido creado!', }, reportDescriptionPage: { roomDescription: 'Descripción de la sala de chat', diff --git a/src/libs/API/parameters/ExportReportPDFParams.ts b/src/libs/API/parameters/ExportReportPDFParams.ts new file mode 100644 index 0000000000000..da8f28bd8da9c --- /dev/null +++ b/src/libs/API/parameters/ExportReportPDFParams.ts @@ -0,0 +1,5 @@ +type ExportReportPDFParams = { + reportID: string; +}; + +export default ExportReportPDFParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 38ee3ee710535..3a0d51a9dac59 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -368,6 +368,7 @@ export type {default as BankAccountCreateCorpayParams} from './BankAccountCreate export type {default as JoinAccessiblePolicyParams} from './JoinAccessiblePolicyParams'; export type {default as ImportPerDiemRatesParams} from './ImportPerDiemRatesParams'; export type {default as ExportPerDiemCSVParams} from './ExportPerDiemCSVParams'; +export type {default as ExportReportPDFParams} from './ExportReportPDFParams'; export type {default as UpdateWorkspaceCustomUnitParams} from './UpdateWorkspaceCustomUnitParams'; export type {default as DismissProductTrainingParams} from './DismissProductTraining'; export type {default as OpenWorkspacePlanPageParams} from './OpenWorkspacePlanPage'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 79f26f7603a48..ad99f057b3c64 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -154,6 +154,7 @@ const WRITE_COMMANDS = { EXPORT_TAGS_CSV: 'ExportTagsCSV', EXPORT_PER_DIEM_CSV: 'ExportPerDiemCSV', EXPORT_REPORT_TO_CSV: 'ExportReportToCSV', + EXPORT_REPORT_TO_PDF: 'ExportReportToPDF', RENAME_WORKSPACE_CATEGORY: 'RenameWorkspaceCategory', CREATE_POLICY_TAG: 'CreatePolicyTag', RENAME_POLICY_TAG: 'RenamePolicyTag', @@ -599,6 +600,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.EXPORT_MEMBERS_CSV]: Parameters.ExportMembersSpreadsheetParams; [WRITE_COMMANDS.EXPORT_TAGS_CSV]: Parameters.ExportTagsSpreadsheetParams; [WRITE_COMMANDS.EXPORT_PER_DIEM_CSV]: Parameters.ExportPerDiemCSVParams; + [WRITE_COMMANDS.EXPORT_REPORT_TO_PDF]: Parameters.ExportReportPDFParams; [WRITE_COMMANDS.RENAME_WORKSPACE_CATEGORY]: Parameters.RenameWorkspaceCategoriesParams; [WRITE_COMMANDS.SET_WORKSPACE_REQUIRES_CATEGORY]: Parameters.SetWorkspaceRequiresCategoryParams; [WRITE_COMMANDS.DELETE_WORKSPACE_CATEGORIES]: Parameters.DeleteWorkspaceCategoriesParams; diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 0790eb35bceae..70a1179a8a72d 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -34,6 +34,10 @@ function canUseMergeAccounts(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.NEWDOT_MERGE_ACCOUNTS) || canUseAllBetas(betas); } +function canUsePDFExport(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.NEWDOT_PDF_EXPORT) || canUseAllBetas(betas); +} + function canUseManagerMcTest(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.NEWDOT_MANAGER_MCTEST) || canUseAllBetas(betas); } @@ -56,6 +60,7 @@ export default { canUseSpotnanaTravel, isBlockedFromSpotnanaTravel, canUseNetSuiteUSATax, + canUsePDFExport, canUseMergeAccounts, canUseManagerMcTest, canUseInternationalBankAccount, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 9f725a3a2a02d..ec6e471e5f9b4 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -17,6 +17,7 @@ import type { CompleteGuidedSetupParams, DeleteCommentParams, ExpandURLPreviewParams, + ExportReportPDFParams, FlagCommentParams, GetNewerActionsParams, GetOlderActionsParams, @@ -52,12 +53,14 @@ import type ExportReportCSVParams from '@libs/API/parameters/ExportReportCSVPara import type UpdateRoomVisibilityParams from '@libs/API/parameters/UpdateRoomVisibilityParams'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as ApiUtils from '@libs/ApiUtils'; +import * as Browser from '@libs/Browser'; import * as CollectionUtils from '@libs/CollectionUtils'; import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; import DateUtils from '@libs/DateUtils'; import {prepareDraftComment} from '@libs/DraftCommentUtils'; import * as EmojiUtils from '@libs/EmojiUtils'; import * as Environment from '@libs/Environment/Environment'; +import {getOldDotURLFromEnvironment} from '@libs/Environment/Environment'; import getEnvironment from '@libs/Environment/getEnvironment'; import type EnvironmentType from '@libs/Environment/getEnvironment/types'; import * as ErrorUtils from '@libs/ErrorUtils'; @@ -128,6 +131,7 @@ import { } from '@libs/ReportUtils'; import shouldSkipDeepLinkNavigation from '@libs/shouldSkipDeepLinkNavigation'; import {getNavatticURL} from '@libs/TourUtils'; +import {addTrailingForwardSlash} from '@libs/Url'; import {generateAccountID} from '@libs/UserUtils'; import Visibility from '@libs/Visibility'; import CONFIG from '@src/CONFIG'; @@ -4655,6 +4659,36 @@ function exportReportToCSV({reportID, transactionIDList}: ExportReportCSVParams, fileDownload(ApiUtils.getCommandURL({command: WRITE_COMMANDS.EXPORT_REPORT_TO_CSV}), 'Expensify.csv', '', false, formData, CONST.NETWORK.METHOD.POST, onDownloadFailed); } +function exportReportToPDF({reportID}: ExportReportPDFParams) { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.NVP_EXPENSIFY_REPORT_PDFFILENAME}${reportID}`, + value: null, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.NVP_EXPENSIFY_REPORT_PDFFILENAME}${reportID}`, + value: 'error', + }, + ]; + const params = { + reportID, + } satisfies ExportReportPDFParams; + + API.write(WRITE_COMMANDS.EXPORT_REPORT_TO_PDF, params, {optimisticData, failureData}); +} + +function downloadReportPDF(fileName: string, reportName: string) { + const baseURL = addTrailingForwardSlash(getOldDotURLFromEnvironment(environment)); + const downloadFileName = `${reportName}.pdf`; + const pdfURL = `${baseURL}secure?secureType=pdfreport&filename=${fileName}&downloadName=${downloadFileName}`; + fileDownload(pdfURL, downloadFileName, '', Browser.isMobileSafari()); +} + function setDeleteTransactionNavigateBackUrl(url: string) { Onyx.set(ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL, url); } @@ -4686,9 +4720,11 @@ export { deleteReportComment, deleteReportField, dismissTrackExpenseActionableWhisper, + downloadReportPDF, editReportComment, expandURLPreview, exportReportToCSV, + exportReportToPDF, exportToIntegration, flagComment, getCurrentUserAccountID, diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 29c15a7c94705..863cc7ca2b57d 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -1,20 +1,23 @@ import {Str} from 'expensify-common'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {View} from 'react-native'; +import {ActivityIndicator, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import AvatarWithImagePicker from '@components/AvatarWithImagePicker'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import Button from '@components/Button'; import ConfirmModal from '@components/ConfirmModal'; import DecisionModal from '@components/DecisionModal'; import DelegateNoAccessModal from '@components/DelegateNoAccessModal'; import DisplayNames from '@components/DisplayNames'; +import Header from '@components/Header'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MentionReportContext from '@components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import Modal from '@components/Modal'; import MultipleAvatars from '@components/MultipleAvatars'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ParentNavigationSubtitle from '@components/ParentNavigationSubtitle'; @@ -30,7 +33,9 @@ import useDelegateUserDetails from '@hooks/useDelegateUserDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; +import usePermissions from '@hooks/usePermissions'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -107,7 +112,9 @@ import { clearAvatarErrors, clearPolicyRoomNameErrors, clearReportFieldKeyErrors, + downloadReportPDF, exportReportToCSV, + exportReportToPDF, getReportPrivateNote, hasErrorInPrivateNotes, leaveGroupChat, @@ -154,12 +161,16 @@ type CaseID = ValueOf; function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDetailsPageProps) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); + const {canUsePDFExport} = usePermissions(); + const theme = useTheme(); const styles = useThemeStyles(); const backTo = route.params.backTo; // The app would crash due to subscribing to the entire report collection if parentReportID is an empty string. So we should have a fallback ID here. /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID || CONST.DEFAULT_NUMBER_ID}`); + + const [reportPDFFilename] = useOnyx(`${ONYXKEYS.COLLECTION.NVP_EXPENSIFY_REPORT_PDFFILENAME}${report?.reportID || CONST.DEFAULT_NUMBER_ID}`) ?? null; const [parentReportAction] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID || CONST.DEFAULT_NUMBER_ID}`, { selector: (actions) => (report?.parentReportActionID ? actions?.[report.parentReportActionID] : undefined), }); @@ -189,6 +200,7 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const [isUnapproveModalVisible, setIsUnapproveModalVisible] = useState(false); const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false); + const [isPDFModalVisible, setIsPDFModalVisible] = useState(false); const [offlineModalVisible, setOfflineModalVisible] = useState(false); const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false); const policy = useMemo(() => policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`], [policies, report?.policyID]); @@ -226,6 +238,17 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta return ''; }, [report]); + + const messagePDF = useMemo(() => { + if (!reportPDFFilename) { + return translate('reportDetailsPage.waitForPDF'); + } + if (reportPDFFilename === CONST.REPORT_DETAILS_MENU_ITEM.ERROR) { + return translate('reportDetailsPage.errorPDF'); + } + return translate('reportDetailsPage.generatedPDF'); + }, [reportPDFFilename, translate]); + const isSystemChat = useMemo(() => isSystemChatUtil(report), [report]); const isGroupChat = useMemo(() => isGroupChatUtil(report), [report]); const isRootGroupChat = useMemo(() => isRootGroupChatUtil(report), [report]); @@ -379,6 +402,11 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta setIsConfirmModalVisible(false); }, [moneyRequestReport, chatReport, backTo]); + const beginPDFExport = useCallback(() => { + setIsPDFModalVisible(true); + exportReportToPDF({reportID: report.reportID}); + }, [report]); + const menuItems: ReportDetailsPageMenuItem[] = useMemo(() => { const items: ReportDetailsPageMenuItem[] = []; @@ -520,9 +548,9 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta if (isMoneyRequestReport) { items.push({ - key: CONST.REPORT_DETAILS_MENU_ITEM.DOWNLOAD, - translationKey: 'common.download', - icon: Expensicons.Download, + key: CONST.REPORT_DETAILS_MENU_ITEM.DOWNLOAD_CSV, + translationKey: 'common.downloadAsCSV', + icon: Expensicons.Table, isAnonymousAction: false, action: () => { if (isOffline) { @@ -535,6 +563,21 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta }); }, }); + if (canUsePDFExport) { + items.push({ + key: CONST.REPORT_DETAILS_MENU_ITEM.DOWNLOAD_PDF, + translationKey: 'common.downloadAsPDF', + icon: Expensicons.Document, + isAnonymousAction: false, + action: () => { + if (isOffline) { + setOfflineModalVisible(true); + } else { + beginPDFExport(); + } + }, + }); + } } if (policy && connectedIntegration && isPolicyAdmin && !isSingleTransactionView && isExpenseReport) { @@ -605,6 +648,8 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta return items; }, [ + beginPDFExport, + canUsePDFExport, isSelfDM, isArchivedRoom, isGroupChat, @@ -1094,6 +1139,46 @@ function ReportDetailsPage({policies, report, route, reportMetadata}: ReportDeta isVisible={downloadErrorModalVisible} onClose={() => setDownloadErrorModalVisible(false)} /> + setIsPDFModalVisible(false)} + isVisible={isPDFModalVisible} + type={isSmallScreenWidth ? CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED : CONST.MODAL.MODAL_TYPE.CONFIRM} + > + + + +
+ + + {messagePDF} + {!reportPDFFilename && ( + + )} + + + {!!reportPDFFilename && reportPDFFilename !== 'error' && ( +