diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index f0335a266b025..b031cb79cd8fd 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -1146,7 +1146,6 @@ function MoneyRequestConfirmationList({ currency={currency} didConfirm={!!didConfirm} distance={distance} - rawAmount={amountToBeUsed} formattedAmount={formattedAmount} formattedAmountPerAttendee={formattedAmountPerAttendee} formError={formError} diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx index 29974e5742a05..d21a1ba7627ce 100644 --- a/src/components/MoneyRequestConfirmationListFooter.tsx +++ b/src/components/MoneyRequestConfirmationListFooter.tsx @@ -19,16 +19,7 @@ import {getDestinationForDisplay, getSubratesFields, getSubratesForDisplay, getT import {canSendInvoice, getPerDiemCustomUnit} from '@libs/PolicyUtils'; import type {ThumbnailAndImageURI} from '@libs/ReceiptUtils'; import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils'; -import { - buildOptimisticExpenseReport, - getDefaultWorkspaceAvatar, - getOutstandingReportsForUser, - getReportName, - isArchivedReport, - isMoneyRequestReport, - isReportOutstanding, - populateOptimisticReportFormula, -} from '@libs/ReportUtils'; +import {generateReportID, getDefaultWorkspaceAvatar, getOutstandingReportsForUser, getReportName, isArchivedReport, isMoneyRequestReport, isReportOutstanding} from '@libs/ReportUtils'; import {getTagVisibility, hasEnabledTags} from '@libs/TagsOptionsListUtils'; import { getTagForDisplay, @@ -75,9 +66,6 @@ type MoneyRequestConfirmationListFooterProps = { /** The distance of the transaction */ distance: number; - /** The raw numeric amount of the transaction */ - rawAmount: number; - /** The formatted amount of the transaction */ formattedAmount: string; @@ -246,7 +234,6 @@ function MoneyRequestConfirmationListFooter({ onToggleBillable, policy, policyTags, - rawAmount, policyTagLists, rate, receiptFilename, @@ -299,7 +286,6 @@ function MoneyRequestConfirmationListFooter({ const hasErrors = !isEmptyObject(transaction?.errors) || !isEmptyObject(transaction?.errorFields?.route) || !isEmptyObject(transaction?.errorFields?.waypoints); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const shouldShowMap = isDistanceRequest && !isManualDistanceRequest && !!(hasErrors || hasPendingWaypoints || iouType !== CONST.IOU.TYPE.SPLIT || !isReadOnly); - const isFromGlobalCreate = !!transaction?.isFromGlobalCreate; const senderWorkspace = useMemo(() => { @@ -317,35 +303,34 @@ function MoneyRequestConfirmationListFooter({ * We need to check if the transaction report exists first in order to prevent the outstanding reports from being used. * Also we need to check if transaction report exists in outstanding reports in order to show a correct report name. */ + const isUnreported = transaction?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID; const transactionReport = transaction?.reportID ? Object.values(allReports ?? {}).find((report) => report?.reportID === transaction.reportID) : undefined; const policyID = selectedParticipants?.at(0)?.policyID; const selectedPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; - const shouldUseTransactionReport = !!transactionReport && isReportOutstanding(transactionReport, policyID, undefined, false); + const shouldUseTransactionReport = (!!transactionReport && isReportOutstanding(transactionReport, policyID, undefined, false)) || isUnreported; const availableOutstandingReports = getOutstandingReportsForUser(policyID, selectedParticipants?.at(0)?.ownerAccountID, allReports, reportNameValuePairs, false).sort((a, b) => localeCompare(a?.reportName?.toLowerCase() ?? '', b?.reportName?.toLowerCase() ?? ''), ); const iouReportID = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.iouReportID; const outstandingReportID = isPolicyExpenseChat ? (iouReportID ?? availableOutstandingReports.at(0)?.reportID) : reportID; - let selectedReportID = shouldUseTransactionReport ? transactionReport.reportID : outstandingReportID; - const selectedReport = useMemo(() => { - if (!selectedReportID) { - return; + const [selectedReportID, selectedReport] = useMemo(() => { + const reportIDToUse = shouldUseTransactionReport ? transaction?.reportID : outstandingReportID; + if (!reportIDToUse) { + // Even if we have no report to use we still need a report id for proper navigation + return [generateReportID(), undefined]; } - return allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${selectedReportID}`]; - }, [allReports, selectedReportID]); - let reportName = getReportName(selectedReport, selectedPolicy); - if (!reportName) { - const optimisticReport = buildOptimisticExpenseReport( - reportID, - selectedPolicy?.id, - selectedPolicy?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID, - rawAmount ?? transaction?.amount ?? 0, - currency, - ); - selectedReportID = !selectedReportID ? optimisticReport.reportID : selectedReportID; - reportName = populateOptimisticReportFormula(selectedPolicy?.fieldList?.text_title?.defaultValue ?? '', optimisticReport, selectedPolicy, true); - } + const reportToUse = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportIDToUse}`]; + return [reportIDToUse, reportToUse]; + }, [allReports, shouldUseTransactionReport, transaction?.reportID, outstandingReportID]); + + const reportName = useMemo(() => { + const name = getReportName(selectedReport, selectedPolicy); + if (!name) { + return isUnreported ? translate('common.none') : translate('iou.newReport'); + } + return name; + }, [isUnreported, selectedReport, selectedPolicy, translate]); // When creating an expense in an individual report, the report field becomes read-only // since the destination is already determined and there's no need to show a selectable list. @@ -1024,7 +1009,6 @@ export default memo( prevProps.currency === nextProps.currency && prevProps.didConfirm === nextProps.didConfirm && prevProps.distance === nextProps.distance && - prevProps.rawAmount === nextProps.rawAmount && prevProps.formattedAmount === nextProps.formattedAmount && prevProps.formError === nextProps.formError && prevProps.hasRoute === nextProps.hasRoute && diff --git a/src/hooks/usePersonalPolicy.ts b/src/hooks/usePersonalPolicy.ts new file mode 100644 index 0000000000000..ee71cdaa1caef --- /dev/null +++ b/src/hooks/usePersonalPolicy.ts @@ -0,0 +1,24 @@ +import {useMemo} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type Policy from '@src/types/onyx/Policy'; +import mapOnyxCollectionItems from '@src/utils/mapOnyxCollectionItems'; +import useOnyx from './useOnyx'; + +type PolicySelector = Pick; + +const policySelector = (policy: OnyxEntry): PolicySelector => + (policy && { + id: policy.id, + type: policy.type, + autoReporting: policy.autoReporting, + }) as PolicySelector; + +function usePersonalPolicy() { + const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: (c) => mapOnyxCollectionItems(c, policySelector), canBeMissing: true}); + const personalPolicy = useMemo(() => Object.values(allPolicies ?? {}).find((policy) => policy?.type === CONST.POLICY.TYPE.PERSONAL), [allPolicies]); + return personalPolicy; +} + +export default usePersonalPolicy; diff --git a/src/languages/de.ts b/src/languages/de.ts index d16c26902501c..e5a41f457502a 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -667,6 +667,7 @@ const translations = { unstableInternetConnection: 'Instabile Internetverbindung. Bitte überprüfe dein Netzwerk und versuche es erneut.', enableGlobalReimbursements: 'Globale Rückerstattungen aktivieren', purchaseAmount: 'Kaufbetrag', + frequency: 'Frequenz', link: 'Link', pinned: 'Angeheftet', read: 'Gelesen', @@ -2021,9 +2022,8 @@ const translations = { workflowsPage: { workflowTitle: 'Ausgaben', workflowDescription: 'Konfigurieren Sie einen Workflow ab dem Moment, in dem Ausgaben anfallen, einschließlich Genehmigung und Zahlung.', - delaySubmissionTitle: 'Einreichungen verzögern', - delaySubmissionDescription: 'Wählen Sie einen benutzerdefinierten Zeitplan für die Einreichung von Ausgaben oder lassen Sie dies für Echtzeit-Updates zu Ausgaben aus.', submissionFrequency: 'Einreichungshäufigkeit', + submissionFrequencyDescription: 'Wählen Sie einen benutzerdefinierten Zeitplan für die Einreichung von Ausgaben oder lassen Sie dies für Echtzeit-Updates zu Ausgaben aus.', submissionFrequencyDateOfMonth: 'Datum des Monats', addApprovalsTitle: 'Genehmigungen hinzufügen', addApprovalButton: 'Genehmigungsworkflow hinzufügen', @@ -2075,7 +2075,6 @@ const translations = { }, }, workflowsDelayedSubmissionPage: { - autoReportingErrorMessage: 'Verspätete Einreichung konnte nicht geändert werden. Bitte versuchen Sie es erneut oder kontaktieren Sie den Support.', autoReportingFrequencyErrorMessage: 'Die Einreichungshäufigkeit konnte nicht geändert werden. Bitte versuchen Sie es erneut oder kontaktieren Sie den Support.', monthlyOffsetErrorMessage: 'Die monatliche Frequenz konnte nicht geändert werden. Bitte versuchen Sie es erneut oder kontaktieren Sie den Support.', }, diff --git a/src/languages/en.ts b/src/languages/en.ts index 20ea856b77a4e..8311778e16987 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -662,6 +662,7 @@ const translations = { unstableInternetConnection: 'Unstable internet connection. Please check your network and try again.', enableGlobalReimbursements: 'Enable Global Reimbursements', purchaseAmount: 'Purchase amount', + frequency: 'Frequency', link: 'Link', pinned: 'Pinned', read: 'Read', @@ -1995,9 +1996,8 @@ const translations = { workflowsPage: { workflowTitle: 'Spend', workflowDescription: 'Configure a workflow from the moment spend occurs, including approval and payment.', - delaySubmissionTitle: 'Delay submissions', - delaySubmissionDescription: 'Choose a custom schedule for submitting expenses, or leave this off for realtime updates on spending.', submissionFrequency: 'Submission frequency', + submissionFrequencyDescription: 'Choose a custom schedule for submitting expenses, or leave this off for realtime updates on spending.', submissionFrequencyDateOfMonth: 'Date of month', addApprovalsTitle: 'Add approvals', addApprovalButton: 'Add approval workflow', @@ -2011,7 +2011,7 @@ const translations = { }, frequencyDescription: 'Choose how often you’d like expenses to submit automatically, or make it manual', frequencies: { - instant: 'Instant', + instant: 'Instantly', weekly: 'Weekly', monthly: 'Monthly', twiceAMonth: 'Twice a month', @@ -2049,7 +2049,6 @@ const translations = { }, }, workflowsDelayedSubmissionPage: { - autoReportingErrorMessage: "Delayed submission couldn't be changed. Please try again or contact support.", autoReportingFrequencyErrorMessage: "Submission frequency couldn't be changed. Please try again or contact support.", monthlyOffsetErrorMessage: "Monthly frequency couldn't be changed. Please try again or contact support.", }, diff --git a/src/languages/es.ts b/src/languages/es.ts index dd6fd02f38965..ac4a3da24b436 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -648,6 +648,7 @@ const translations = { unstableInternetConnection: 'Conexión a internet inestable. Por favor, revisa tu red e inténtalo de nuevo.', enableGlobalReimbursements: 'Habilitar Reembolsos Globales', purchaseAmount: 'Importe de compra', + frequency: 'Frecuencia', link: 'Enlace', pinned: 'Fijado', read: 'Leído', @@ -1987,9 +1988,8 @@ const translations = { workflowsPage: { workflowTitle: 'Gasto', workflowDescription: 'Configure un flujo de trabajo desde el momento en que se produce el gasto, incluida la aprobación y el pago', - delaySubmissionTitle: 'Retrasar envíos', - delaySubmissionDescription: 'Elige una frecuencia para enviar los gastos, o dejalo desactivado para recibir actualizaciones en tiempo real sobre los gastos.', submissionFrequency: 'Frecuencia de envíos', + submissionFrequencyDescription: 'Elige una frecuencia para enviar los gastos, o dejalo desactivado para recibir actualizaciones en tiempo real sobre los gastos.', submissionFrequencyDateOfMonth: 'Fecha del mes', addApprovalsTitle: 'Aprobaciones', addApprovalButton: 'Añadir flujo de aprobación', @@ -2003,7 +2003,7 @@ const translations = { }, frequencyDescription: 'Elige la frecuencia de presentación automática de gastos, o preséntalos manualmente', frequencies: { - instant: 'Instante', + instant: 'Al instante', weekly: 'Semanal', monthly: 'Mensual', twiceAMonth: 'Dos veces al mes', @@ -2041,7 +2041,6 @@ const translations = { }, }, workflowsDelayedSubmissionPage: { - autoReportingErrorMessage: 'El parámetro de envío retrasado no pudo ser cambiado. Por favor, inténtelo de nuevo o contacte al soporte.', autoReportingFrequencyErrorMessage: 'La frecuencia de envío no pudo ser cambiada. Por favor, inténtelo de nuevo o contacte al soporte.', monthlyOffsetErrorMessage: 'La frecuencia mensual no pudo ser cambiada. Por favor, inténtelo de nuevo o contacte al soporte.', }, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index fc33d797e8df8..eaadbe8dba6ab 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -667,6 +667,7 @@ const translations = { unstableInternetConnection: 'Connexion Internet instable. Veuillez vérifier votre réseau et réessayer.', enableGlobalReimbursements: 'Activer les remboursements globaux', purchaseAmount: "Montant de l'achat", + frequency: 'Fréquence', link: 'Lien', pinned: 'Épinglé', read: 'Lu', @@ -2021,10 +2022,9 @@ const translations = { workflowsPage: { workflowTitle: 'Dépenser', workflowDescription: "Configurez un flux de travail dès que la dépense survient, y compris l'approbation et le paiement.", - delaySubmissionTitle: 'Retarder les soumissions', - delaySubmissionDescription: - 'Choisissez un calendrier personnalisé pour soumettre les dépenses, ou laissez cette option désactivée pour des mises à jour en temps réel sur les dépenses.', submissionFrequency: 'Fréquence de soumission', + submissionFrequencyDescription: + 'Choisissez un calendrier personnalisé pour soumettre les dépenses, ou laissez cette option désactivée pour des mises à jour en temps réel sur les dépenses.', submissionFrequencyDateOfMonth: 'Date du mois', addApprovalsTitle: 'Ajouter des approbations', addApprovalButton: "Ajouter un flux de travail d'approbation", @@ -2038,7 +2038,7 @@ const translations = { }, frequencyDescription: 'Choisissez la fréquence à laquelle vous souhaitez que les dépenses soient soumises automatiquement, ou faites-le manuellement.', frequencies: { - instant: 'Instantané', + instant: 'Immédiatement', weekly: 'Hebdomadaire', monthly: 'Mensuel', twiceAMonth: 'Deux fois par mois', @@ -2076,7 +2076,6 @@ const translations = { }, }, workflowsDelayedSubmissionPage: { - autoReportingErrorMessage: "La soumission retardée n'a pas pu être modifiée. Veuillez réessayer ou contacter le support.", autoReportingFrequencyErrorMessage: "La fréquence de soumission n'a pas pu être modifiée. Veuillez réessayer ou contacter le support.", monthlyOffsetErrorMessage: "La fréquence mensuelle n'a pas pu être modifiée. Veuillez réessayer ou contacter le support.", }, diff --git a/src/languages/it.ts b/src/languages/it.ts index ee40b97a38938..9b2b7e4c5ce5b 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -667,6 +667,7 @@ const translations = { unstableInternetConnection: 'Connessione Internet instabile. Controlla la tua rete e riprova.', enableGlobalReimbursements: 'Abilita i rimborsi globali', purchaseAmount: 'Importo di acquisto', + frequency: 'Frequenza', link: 'Link', pinned: 'Fissato', read: 'Letto', @@ -2012,9 +2013,8 @@ const translations = { workflowsPage: { workflowTitle: 'Spendere', workflowDescription: 'Configura un flusso di lavoro dal momento in cui si verifica una spesa, inclusi approvazione e pagamento.', - delaySubmissionTitle: 'Ritarda invii', - delaySubmissionDescription: "Scegli un programma personalizzato per l'invio delle spese, oppure lascia disattivato per aggiornamenti in tempo reale sulle spese.", submissionFrequency: 'Frequenza di invio', + submissionFrequencyDescription: "Scegli un programma personalizzato per l'invio delle spese, oppure lascia disattivato per aggiornamenti in tempo reale sulle spese.", submissionFrequencyDateOfMonth: 'Data del mese', addApprovalsTitle: 'Aggiungi approvazioni', addApprovalButton: 'Aggiungi flusso di lavoro di approvazione', @@ -2028,7 +2028,7 @@ const translations = { }, frequencyDescription: 'Scegli con quale frequenza desideri che le spese vengano inviate automaticamente, oppure impostale manualmente.', frequencies: { - instant: 'Istantaneo', + instant: 'Immediatamente', weekly: 'Settimanale', monthly: 'Mensile', twiceAMonth: 'Due volte al mese', @@ -2066,7 +2066,6 @@ const translations = { }, }, workflowsDelayedSubmissionPage: { - autoReportingErrorMessage: 'La presentazione ritardata non può essere modificata. Per favore, riprova o contatta il supporto.', autoReportingFrequencyErrorMessage: "La frequenza di invio non può essere modificata. Riprova o contatta l'assistenza.", monthlyOffsetErrorMessage: 'La frequenza mensile non può essere modificata. Riprova o contatta il supporto.', }, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 7dc0fb271423d..42a14e8798e6e 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -667,6 +667,7 @@ const translations = { unstableInternetConnection: 'インターネット接続が不安定です。ネットワークを確認してもう一度お試しください。', enableGlobalReimbursements: 'グローバル払い戻しを有効にする', purchaseAmount: '購入金額', + frequency: '頻度', link: 'リンク', pinned: '固定済み', read: '既読', @@ -2005,9 +2006,8 @@ const translations = { workflowsPage: { workflowTitle: '支出', workflowDescription: '支出が発生した瞬間から、承認および支払いを含むワークフローを設定します。', - delaySubmissionTitle: '提出を遅らせる', - delaySubmissionDescription: '経費提出のカスタムスケジュールを選択するか、支出のリアルタイム更新のためにこれをオフにしておいてください。', submissionFrequency: '提出頻度', + submissionFrequencyDescription: '経費提出のカスタムスケジュールを選択するか、支出のリアルタイム更新のためにこれをオフにしておいてください。', submissionFrequencyDateOfMonth: '月の日付', addApprovalsTitle: '承認を追加', addApprovalButton: '承認ワークフローを追加', @@ -2021,7 +2021,7 @@ const translations = { }, frequencyDescription: '経費を自動で提出する頻度を選択するか、手動で行うように設定してください。', frequencies: { - instant: 'インスタント', + instant: '即座に', weekly: '毎週', monthly: '毎月', twiceAMonth: '月に2回', @@ -2059,7 +2059,6 @@ const translations = { }, }, workflowsDelayedSubmissionPage: { - autoReportingErrorMessage: '遅延した提出は変更できませんでした。もう一度お試しいただくか、サポートにお問い合わせください。', autoReportingFrequencyErrorMessage: '提出頻度を変更できませんでした。もう一度お試しいただくか、サポートにお問い合わせください。', monthlyOffsetErrorMessage: '月次の頻度を変更できませんでした。もう一度お試しいただくか、サポートにお問い合わせください。', }, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 71cd1bdb7cd62..c8743061b0c7e 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -666,6 +666,7 @@ const translations = { unstableInternetConnection: 'Onstabiele internetverbinding. Controleer je netwerk en probeer het opnieuw.', enableGlobalReimbursements: 'Wereldwijde terugbetalingen inschakelen', purchaseAmount: 'Aankoopbedrag', + frequency: 'Frequentie', link: 'Link', pinned: 'Vastgezet', read: 'Gelezen', @@ -2011,9 +2012,8 @@ const translations = { workflowsPage: { workflowTitle: 'Uitgaven', workflowDescription: 'Configureer een workflow vanaf het moment dat uitgaven plaatsvinden, inclusief goedkeuring en betaling.', - delaySubmissionTitle: 'Vertraging van inzendingen', - delaySubmissionDescription: 'Kies een aangepast schema voor het indienen van onkosten, of laat dit uitgeschakeld voor realtime updates over uitgaven.', submissionFrequency: 'Indieningsfrequentie', + submissionFrequencyDescription: 'Kies een aangepast schema voor het indienen van onkosten, of laat dit uitgeschakeld voor realtime updates over uitgaven.', submissionFrequencyDateOfMonth: 'Datum van de maand', addApprovalsTitle: 'Goedkeuringen toevoegen', addApprovalButton: 'Goedkeuringsworkflow toevoegen', @@ -2027,7 +2027,7 @@ const translations = { }, frequencyDescription: 'Kies hoe vaak je wilt dat uitgaven automatisch worden ingediend, of maak het handmatig.', frequencies: { - instant: 'Instant', + instant: 'Onmiddellijk', weekly: 'Wekelijks', monthly: 'Maandelijks', twiceAMonth: 'Twee keer per maand', @@ -2065,7 +2065,6 @@ const translations = { }, }, workflowsDelayedSubmissionPage: { - autoReportingErrorMessage: 'Vertraagde inzending kon niet worden gewijzigd. Probeer het opnieuw of neem contact op met de ondersteuning.', autoReportingFrequencyErrorMessage: 'De frequentie van inzendingen kon niet worden gewijzigd. Probeer het opnieuw of neem contact op met de ondersteuning.', monthlyOffsetErrorMessage: 'Maandelijkse frequentie kon niet worden gewijzigd. Probeer het opnieuw of neem contact op met de ondersteuning.', }, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index b497afc53cda4..59b7b76c640f6 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -667,6 +667,7 @@ const translations = { unstableInternetConnection: 'Niestabilne połączenie internetowe. Sprawdź swoją sieć i spróbuj ponownie.', enableGlobalReimbursements: 'Włącz globalne zwroty', purchaseAmount: 'Kwota zakupu', + frequency: 'Częstotliwość', link: 'Link', pinned: 'Przypięte', read: 'Przeczytane', @@ -2008,10 +2009,9 @@ const translations = { workflowsPage: { workflowTitle: 'Wydatki', workflowDescription: 'Skonfiguruj przepływ pracy od momentu wystąpienia wydatku, w tym zatwierdzenie i płatność.', - delaySubmissionTitle: 'Opóźnij zgłoszenia', - delaySubmissionDescription: - 'Wybierz niestandardowy harmonogram przesyłania wydatków lub pozostaw to wyłączone, aby otrzymywać aktualizacje w czasie rzeczywistym dotyczące wydatków.', submissionFrequency: 'Częstotliwość składania wniosków', + submissionFrequencyDescription: + 'Wybierz niestandardowy harmonogram przesyłania wydatków lub pozostaw to wyłączone, aby otrzymywać aktualizacje w czasie rzeczywistym dotyczące wydatków.', submissionFrequencyDateOfMonth: 'Data miesiąca', addApprovalsTitle: 'Dodaj zatwierdzenia', addApprovalButton: 'Dodaj przepływ pracy zatwierdzania', @@ -2025,7 +2025,7 @@ const translations = { }, frequencyDescription: 'Wybierz, jak często chcesz, aby wydatki były przesyłane automatycznie, lub ustaw je na ręczne przesyłanie.', frequencies: { - instant: 'Natychmiastowy', + instant: 'Natychmiast', weekly: 'Cotygodniowo', monthly: 'Miesięczny', twiceAMonth: 'Dwa razy w miesiącu', @@ -2063,7 +2063,6 @@ const translations = { }, }, workflowsDelayedSubmissionPage: { - autoReportingErrorMessage: 'Opóźnione zgłoszenie nie mogło zostać zmienione. Spróbuj ponownie lub skontaktuj się z pomocą techniczną.', autoReportingFrequencyErrorMessage: 'Nie można było zmienić częstotliwości przesyłania. Spróbuj ponownie lub skontaktuj się z pomocą techniczną.', monthlyOffsetErrorMessage: 'Nie można było zmienić miesięcznej częstotliwości. Spróbuj ponownie lub skontaktuj się z pomocą techniczną.', }, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 93cd16faeea4a..e4f939efcb03b 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -667,6 +667,7 @@ const translations = { unstableInternetConnection: 'Conexão de internet instável. Verifique sua rede e tente novamente.', enableGlobalReimbursements: 'Ativar reembolsos globais', purchaseAmount: 'Valor da compra', + frequency: 'Freqüência', link: 'Link', pinned: 'Fixado', read: 'Lido', @@ -2011,9 +2012,8 @@ const translations = { workflowsPage: { workflowTitle: 'Gastar', workflowDescription: 'Configurar um fluxo de trabalho desde o momento em que a despesa ocorre, incluindo aprovação e pagamento.', - delaySubmissionTitle: 'Atrasar envios', - delaySubmissionDescription: 'Escolha um cronograma personalizado para enviar despesas ou deixe isso desativado para atualizações em tempo real sobre gastos.', submissionFrequency: 'Frequência de envio', + submissionFrequencyDescription: 'Escolha um cronograma personalizado para enviar despesas ou deixe isso desativado para atualizações em tempo real sobre gastos.', submissionFrequencyDateOfMonth: 'Data do mês', addApprovalsTitle: 'Adicionar aprovações', addApprovalButton: 'Adicionar fluxo de trabalho de aprovação', @@ -2027,7 +2027,7 @@ const translations = { }, frequencyDescription: 'Escolha com que frequência você gostaria que as despesas fossem enviadas automaticamente ou faça isso manualmente.', frequencies: { - instant: 'Instantâneo', + instant: 'Imediatamente', weekly: 'Semanalmente', monthly: 'Mensalmente', twiceAMonth: 'Duas vezes por mês', @@ -2065,7 +2065,6 @@ const translations = { }, }, workflowsDelayedSubmissionPage: { - autoReportingErrorMessage: 'A submissão atrasada não pôde ser alterada. Por favor, tente novamente ou entre em contato com o suporte.', autoReportingFrequencyErrorMessage: 'A frequência de envio não pôde ser alterada. Por favor, tente novamente ou entre em contato com o suporte.', monthlyOffsetErrorMessage: 'A frequência mensal não pôde ser alterada. Por favor, tente novamente ou entre em contato com o suporte.', }, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index bee3dbb04dc7b..4632cbee4837a 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -666,6 +666,7 @@ const translations = { unstableInternetConnection: '互联网连接不稳定。请检查你的网络,然后重试。', enableGlobalReimbursements: '启用全球报销', purchaseAmount: '购买金额', + frequency: '频率', link: '链接', pinned: '已固定', read: '已读', @@ -1984,9 +1985,8 @@ const translations = { workflowsPage: { workflowTitle: '花费', workflowDescription: '配置从支出发生到审批和支付的工作流程。', - delaySubmissionTitle: '延迟提交', - delaySubmissionDescription: '选择自定义的费用提交时间表,或者关闭此选项以实时更新支出。', submissionFrequency: '提交频率', + submissionFrequencyDescription: '选择自定义的费用提交时间表,或者关闭此选项以实时更新支出。', submissionFrequencyDateOfMonth: '月份日期', addApprovalsTitle: '添加审批', addApprovalButton: '添加审批工作流程', @@ -2000,7 +2000,7 @@ const translations = { }, frequencyDescription: '选择您希望自动提交费用的频率,或者选择手动提交', frequencies: { - instant: '即时', + instant: '即刻', weekly: '每周', monthly: '每月', twiceAMonth: '每月两次', @@ -2038,7 +2038,6 @@ const translations = { }, }, workflowsDelayedSubmissionPage: { - autoReportingErrorMessage: '延迟提交无法更改。请重试或联系客服。', autoReportingFrequencyErrorMessage: '提交频率无法更改。请重试或联系客服。', monthlyOffsetErrorMessage: '无法更改每月频率。请重试或联系支持。', }, diff --git a/src/libs/API/parameters/SetWorkspaceAutoHarvestingParams.ts b/src/libs/API/parameters/SetWorkspaceAutoHarvestingParams.ts new file mode 100644 index 0000000000000..4a8baa1fd69a7 --- /dev/null +++ b/src/libs/API/parameters/SetWorkspaceAutoHarvestingParams.ts @@ -0,0 +1,6 @@ +type SetWorkspaceAutoHarvestingParams = { + policyID: string; + enabled: boolean; +}; + +export default SetWorkspaceAutoHarvestingParams; diff --git a/src/libs/API/parameters/TrackExpenseParams.ts b/src/libs/API/parameters/TrackExpenseParams.ts index b66f45e5b1044..7cb8fb86a9ec2 100644 --- a/src/libs/API/parameters/TrackExpenseParams.ts +++ b/src/libs/API/parameters/TrackExpenseParams.ts @@ -15,6 +15,8 @@ type TrackExpenseParams = { createdChatReportActionID?: string; createdIOUReportActionID?: string; reportPreviewReportActionID?: string; + optimisticReportID?: string; + optimisticReportActionID?: string; receipt?: Receipt; receiptState?: ValueOf; category?: string; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 287e150bf9903..f0299c943bc8f 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -196,6 +196,7 @@ export type {default as SetWorkspaceRequiresCategoryParams} from './SetWorkspace export type {default as DeleteWorkspaceCategoriesParams} from './DeleteWorkspaceCategoriesParams'; export type {default as UpdatePolicyCategoryPayrollCodeParams} from './UpdatePolicyCategoryPayrollCodeParams'; export type {default as UpdatePolicyCategoryGLCodeParams} from './UpdatePolicyCategoryGLCodeParams'; +export type {default as SetWorkspaceAutoHarvestingParams} from './SetWorkspaceAutoHarvestingParams'; export type {default as SetWorkspaceAutoReportingFrequencyParams} from './SetWorkspaceAutoReportingFrequencyParams'; export type {default as SetWorkspaceAutoReportingMonthlyOffsetParams} from './SetWorkspaceAutoReportingMonthlyOffsetParams'; export type {default as SetWorkspaceApprovalModeParams} from './SetWorkspaceApprovalModeParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index cfe596d190ffa..81255e374e01b 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -13,6 +13,7 @@ type ApiRequestType = ValueOf; const WRITE_COMMANDS = { CLEAN_POLICY_TAGS: 'ClearPolicyTags', IMPORT_MULTI_LEVEL_TAGS: 'ImportMultiLevelTags', + SET_WORKSPACE_AUTO_HARVESTING: 'SetWorkspaceAutoHarvesting', SET_WORKSPACE_AUTO_REPORTING_FREQUENCY: 'SetWorkspaceAutoReportingFrequency', SET_WORKSPACE_AUTO_REPORTING_MONTHLY_OFFSET: 'SetWorkspaceAutoReportingOffset', SET_WORKSPACE_APPROVAL_MODE: 'SetWorkspaceApprovalMode', @@ -728,6 +729,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.CANCEL_PAYMENT]: Parameters.CancelPaymentParams; [WRITE_COMMANDS.ACCEPT_ACH_CONTRACT_FOR_BANK_ACCOUNT]: Parameters.AcceptACHContractForBankAccount; [WRITE_COMMANDS.UPDATE_WORKSPACE_DESCRIPTION]: Parameters.UpdateWorkspaceDescriptionParams; + [WRITE_COMMANDS.SET_WORKSPACE_AUTO_HARVESTING]: Parameters.SetWorkspaceAutoHarvestingParams; [WRITE_COMMANDS.SET_WORKSPACE_AUTO_REPORTING_FREQUENCY]: Parameters.SetWorkspaceAutoReportingFrequencyParams; [WRITE_COMMANDS.SET_WORKSPACE_AUTO_REPORTING_MONTHLY_OFFSET]: Parameters.SetWorkspaceAutoReportingMonthlyOffsetParams; [WRITE_COMMANDS.SET_WORKSPACE_APPROVAL_MODE]: Parameters.SetWorkspaceApprovalModeParams; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 7df7dc09ae1a1..4e913b129e012 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -8,6 +8,7 @@ import isEmpty from 'lodash/isEmpty'; import isNumber from 'lodash/isNumber'; import mapValues from 'lodash/mapValues'; import lodashMaxBy from 'lodash/maxBy'; +import {InteractionManager} from 'react-native'; import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {SvgProps} from 'react-native-svg'; @@ -84,7 +85,7 @@ import { import {isApprover as isApproverUtils} from './actions/Policy/Member'; import {createDraftWorkspace} from './actions/Policy/Policy'; import {hasCreditBankAccount} from './actions/ReimbursementAccount/store'; -import {handleReportChanged} from './actions/Report'; +import {handlePreexistingReport} from './actions/Report'; import type {GuidedSetupData, TaskForParameters} from './actions/Report'; import {isAnonymousUser as isAnonymousUserSession} from './actions/Session'; import {removeDraftTransactions} from './actions/TransactionEdit'; @@ -998,7 +999,9 @@ Onyx.connect({ return acc; } - handleReportChanged(report); + InteractionManager.runAfterInteractions(() => { + handlePreexistingReport(report); + }); // Get all reports, which are the ones that are: // - Owned by the same user diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 658d74e7af2cf..4c195aa410bc6 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -140,6 +140,7 @@ import { buildOptimisticReportPreview, buildOptimisticResolvedDuplicatesReportAction, buildOptimisticRetractedReportAction, + buildOptimisticSelfDMReport, buildOptimisticSubmittedReportAction, buildOptimisticUnapprovedReportAction, buildOptimisticUnHoldReportAction, @@ -322,6 +323,8 @@ type TrackExpenseInformation = { transactionThreadReportID: string; createdReportActionIDForThread: string | undefined; actionableWhisperReportActionIDParam?: string; + optimisticReportID: string | undefined; + optimisticReportActionID: string | undefined; onyxData: OnyxData; }; @@ -609,7 +612,7 @@ type TrackExpenseAccountantParams = { }; type CreateTrackExpenseParams = { - report: OnyxTypes.Report; + report: OnyxEntry; isDraftPolicy: boolean; action?: IOUAction; participantParams: RequestMoneyParticipantParams; @@ -3912,15 +3915,89 @@ function getTrackExpenseInformation(params: GetTrackExpenseInformationParams): T // STEP 1: Get existing chat report let chatReport = !isEmptyObject(parentChatReport) && parentChatReport?.reportID ? parentChatReport : null; - // The chatReport always exists, and we can get it from Onyx if chatReport is null. + + // If no chat report is passed, defaults to the self-DM report if (!chatReport) { - chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${participant.reportID}`] ?? null; + chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${findSelfDMReportID()}`] ?? null; } - // If we still don't have a report, it likely doesn't exist, and we will early return here as it should not happen - // Maybe later, we can build an optimistic selfDM chat. + // If we are still missing the chat report then optimistically create the self-DM report and use it + let optimisticReportID: string | undefined; + let optimisticReportActionID: string | undefined; if (!chatReport) { - return null; + const currentTime = DateUtils.getDBTime(); + const selfDMReport = buildOptimisticSelfDMReport(currentTime); + const selfDMCreatedReportAction = buildOptimisticCreatedReportAction(currentUserEmail ?? '', currentTime); + optimisticReportID = selfDMReport.reportID; + optimisticReportActionID = selfDMCreatedReportAction.reportActionID; + chatReport = selfDMReport; + + optimisticData.push( + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticReportID}`, + value: { + ...selfDMReport, + pendingFields: { + createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${optimisticReportID}`, + value: {isOptimisticReport: true}, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticReportID}`, + value: { + [optimisticReportActionID]: selfDMCreatedReportAction, + }, + }, + ); + successData.push( + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticReportID}`, + value: { + pendingFields: { + createChat: null, + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${optimisticReportID}`, + value: {isOptimisticReport: false}, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticReportID}`, + value: { + [optimisticReportActionID]: { + pendingAction: null, + }, + }, + }, + ); + failureData.push( + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticReportID}`, + value: null, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${optimisticReportID}`, + value: null, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticReportID}`, + value: null, + }, + ); } // Check if the report is a draft @@ -3945,7 +4022,7 @@ function getTrackExpenseInformation(params: GetTrackExpenseInformationParams): T // STEP 2: If not in the self-DM flow, we need to use the expense report. // For this, first use the chatReport.iouReportID property. Build a new optimistic expense report if needed. - const shouldUseMoneyReport = !!isPolicyExpenseChat; + const shouldUseMoneyReport = !!isPolicyExpenseChat && chatReport.chatType !== CONST.REPORT.CHAT_TYPE.SELF_DM; let iouReport: OnyxInputValue = null; let shouldCreateNewMoneyRequestReport = false; @@ -4084,6 +4161,8 @@ function getTrackExpenseInformation(params: GetTrackExpenseInformationParams): T transactionThreadReportID: optimisticTransactionThread.reportID, createdReportActionIDForThread: optimisticCreatedActionForTransactionThread?.reportActionID, actionableWhisperReportActionIDParam: actionableTrackExpenseWhisper?.reportActionID, + optimisticReportID, + optimisticReportActionID, onyxData: { optimisticData: optimisticData.concat(trackExpenseOnyxData[0]), successData: successData.concat(trackExpenseOnyxData[1]), @@ -6084,8 +6163,8 @@ function trackExpense(params: CreateTrackExpenseParams) { } = transactionData; const isMoneyRequestReport = isMoneyRequestReportReportUtils(report); - const currentChatReport = isMoneyRequestReport ? getReportOrDraftReport(report.chatReportID) : report; - const moneyRequestReportID = isMoneyRequestReport ? report.reportID : ''; + const currentChatReport = isMoneyRequestReport ? getReportOrDraftReport(report?.chatReportID) : report; + const moneyRequestReportID = isMoneyRequestReport ? report?.reportID : ''; const isMovingTransactionFromTrackExpense = isMovingTransactionFromTrackExpenseIOUUtils(action); // Pass an open receipt so the distance expense will show a map with the route optimistically @@ -6135,6 +6214,8 @@ function trackExpense(params: CreateTrackExpenseParams) { transactionThreadReportID, createdReportActionIDForThread, actionableWhisperReportActionIDParam, + optimisticReportID, + optimisticReportActionID, onyxData, } = getTrackExpenseInformation({ @@ -6172,7 +6253,7 @@ function trackExpense(params: CreateTrackExpenseParams) { }, retryParams, }) ?? {}; - const activeReportID = isMoneyRequestReport ? report.reportID : chatReport?.reportID; + const activeReportID = isMoneyRequestReport ? report?.reportID : chatReport?.reportID; const recentServerValidatedWaypoints = getRecentWaypoints().filter((item) => !item.pendingAction); onyxData?.failureData?.push({ @@ -6303,12 +6384,15 @@ function trackExpense(params: CreateTrackExpenseParams) { created, merchant, iouReportID: iouReport?.reportID, - chatReportID: chatReport?.reportID, + // If we are passing an optimisticReportID then we are creating a new chat (selfDM) and we don't have an *existing* chatReportID + chatReportID: optimisticReportID ? undefined : chatReport?.reportID, transactionID: transaction?.transactionID, reportActionID: iouAction?.reportActionID, createdChatReportActionID, createdIOUReportActionID, reportPreviewReportActionID: reportPreviewAction?.reportActionID, + optimisticReportID, + optimisticReportActionID, receipt: isFileUploadable(trackedReceipt) ? trackedReceipt : undefined, receiptState: trackedReceipt?.state, category, diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 0a51efa4556a7..27e67396c8a18 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -51,6 +51,7 @@ import type { SetPolicyProhibitedExpensesParams, SetPolicyRulesEnabledParams, SetWorkspaceApprovalModeParams, + SetWorkspaceAutoHarvestingParams, SetWorkspaceAutoReportingFrequencyParams, SetWorkspaceAutoReportingMonthlyOffsetParams, SetWorkspacePayerParams, @@ -601,6 +602,62 @@ function deleteWorkspace( } } +/* Set the auto harvesting on a workspace. This goes in tandem with auto reporting. so when you enable/disable + * harvesting, you are enabling/disabling auto reporting too. + */ +function setWorkspaceAutoHarvesting(policy: Policy, enabled: boolean) { + const policyID = policy.id; + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + autoReporting: enabled, + harvesting: {enabled}, + pendingFields: { + autoReporting: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + errorFields: { + autoReporting: null, + }, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + autoReporting: policy?.autoReporting ?? null, + harvesting: policy?.harvesting ?? null, + pendingFields: { + autoReporting: null, + }, + errorFields: { + autoReporting: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workflowsDelayedSubmissionPage.autoReportingFrequencyErrorMessage'), + }, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + pendingFields: { + autoReporting: null, + }, + }, + }, + ]; + + const params: SetWorkspaceAutoHarvestingParams = {policyID, enabled}; + API.write(WRITE_COMMANDS.SET_WORKSPACE_AUTO_HARVESTING, params, {optimisticData, failureData, successData}); +} + function setWorkspaceAutoReportingFrequency(policyID: string, frequency: ValueOf) { // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 // eslint-disable-next-line deprecation/deprecation @@ -6378,6 +6435,7 @@ export { buildOptimisticRecentlyUsedCurrencies, setWorkspaceInviteMessageDraft, setWorkspaceApprovalMode, + setWorkspaceAutoHarvesting, setWorkspaceAutoReportingFrequency, setWorkspaceAutoReportingMonthlyOffset, updateWorkspaceDescription, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 1dd7db52af428..c01cfaacd8b27 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -1856,16 +1856,15 @@ function broadcastUserIsLeavingRoom(reportID: string) { Pusher.sendEvent(privateReportChannelName, Pusher.TYPE.USER_IS_LEAVING_ROOM, leavingStatus); } -/** When a report changes in Onyx, this fetches the report from the API if the report doesn't have a name */ -function handleReportChanged(report: OnyxEntry) { - if (!report) { +function handlePreexistingReport(report: Report) { + const {reportID, preexistingReportID, parentReportID, parentReportActionID} = report; + + if (!reportID || !preexistingReportID) { return; } - const {reportID, preexistingReportID, parentReportID, parentReportActionID} = report; - // Handle cleanup of stale optimistic IOU report and its report preview separately - if (reportID && preexistingReportID && isMoneyRequestReport(report) && parentReportActionID) { + if (isMoneyRequestReport(report) && parentReportActionID) { Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, { [parentReportActionID]: null, }); @@ -1876,48 +1875,46 @@ function handleReportChanged(report: OnyxEntry) { // It is possible that we optimistically created a DM/group-DM for a set of users for which a report already exists. // In this case, the API will let us know by returning a preexistingReportID. // We should clear out the optimistically created report and re-route the user to the preexisting report. - if (reportID && preexistingReportID) { - let callback = () => { - const existingReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${preexistingReportID}`]; - - Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, null); - Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${preexistingReportID}`, { - ...report, - reportID: preexistingReportID, - preexistingReportID: null, - // Replacing the existing report's participants to avoid duplicates - participants: existingReport?.participants ?? report.participants, - }); - Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, null); + let callback = () => { + const existingReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${preexistingReportID}`]; + + Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, null); + Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${preexistingReportID}`, { + ...report, + reportID: preexistingReportID, + preexistingReportID: null, + // Replacing the existing report's participants to avoid duplicates + participants: existingReport?.participants ?? report.participants, + }); + Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, null); + }; + // Only re-route them if they are still looking at the optimistically created report + if (Navigation.getActiveRoute().includes(`/r/${reportID}`)) { + const currCallback = callback; + callback = () => { + currCallback(); + Navigation.setParams({reportID: preexistingReportID.toString()}); }; - // Only re-route them if they are still looking at the optimistically created report - if (Navigation.getActiveRoute().includes(`/r/${reportID}`)) { - const currCallback = callback; - callback = () => { - currCallback(); - Navigation.setParams({reportID: preexistingReportID.toString()}); - }; - - // The report screen will listen to this event and transfer the draft comment to the existing report - // This will allow the newest draft comment to be transferred to the existing report - DeviceEventEmitter.emit(`switchToPreExistingReport_${reportID}`, { - preexistingReportID, - callback, - }); - return; - } + // The report screen will listen to this event and transfer the draft comment to the existing report + // This will allow the newest draft comment to be transferred to the existing report + DeviceEventEmitter.emit(`switchToPreExistingReport_${reportID}`, { + preexistingReportID, + callback, + }); - // In case the user is not on the report screen, we will transfer the report draft comment directly to the existing report - // after that clear the optimistically created report - const draftReportComment = allReportDraftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`]; - if (!draftReportComment) { - callback(); - return; - } + return; + } - saveReportDraftComment(preexistingReportID, draftReportComment, callback); + // In case the user is not on the report screen, we will transfer the report draft comment directly to the existing report + // after that clear the optimistically created report + const draftReportComment = allReportDraftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`]; + if (!draftReportComment) { + callback(); + return; } + + saveReportDraftComment(preexistingReportID, draftReportComment, callback); } /** Deletes a comment from the report, basically sets it as empty string */ @@ -6159,7 +6156,7 @@ export { getNewerActions, getOlderActions, getReportPrivateNote, - handleReportChanged, + handlePreexistingReport, handleUserDeletedLinksInHtml, hasErrorInPrivateNotes, inviteToGroupChat, diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 7353f4fda205e..f4c52be296cf6 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -28,6 +28,7 @@ import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; +import usePersonalPolicy from '@hooks/usePersonalPolicy'; import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; @@ -47,6 +48,7 @@ import { search, unholdMoneyRequestOnSearch, } from '@libs/actions/Search'; +import {setTransactionReport} from '@libs/actions/Transaction'; import {navigateToParticipantPage} from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -88,6 +90,7 @@ function SearchPage({route}: SearchPageProps) { const [newParentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${newReport?.parentReportID}`, {canBeMissing: true}); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: false}); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); + const personalPolicy = usePersonalPolicy(); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); const [integrationsExportTemplates] = useOnyx(ONYXKEYS.NVP_INTEGRATION_SERVER_EXPORT_TEMPLATES, {canBeMissing: true}); const [csvExportLayouts] = useOnyx(ONYXKEYS.NVP_CSV_EXPORT_LAYOUTS, {canBeMissing: true}); @@ -551,7 +554,12 @@ function SearchPage({route}: SearchPageProps) { if (isPaidGroupPolicy(activePolicy) && activePolicy?.isPolicyExpenseChatEnabled && !shouldRestrictUserBillableActions(activePolicy.id)) { const activePolicyExpenseChat = getPolicyExpenseChat(currentUserPersonalDetails.accountID, activePolicy?.id); - const setParticipantsPromises = newReceiptFiles.map((receiptFile) => setMoneyRequestParticipantsFromReport(receiptFile.transactionID, activePolicyExpenseChat)); + const shouldAutoReport = !!activePolicy?.autoReporting || !!personalPolicy?.autoReporting; + const transactionReportID = shouldAutoReport ? activePolicyExpenseChat?.reportID : CONST.REPORT.UNREPORTED_REPORT_ID; + const setParticipantsPromises = newReceiptFiles.map((receiptFile) => { + setTransactionReport(receiptFile.transactionID, {reportID: transactionReportID}, true); + return setMoneyRequestParticipantsFromReport(receiptFile.transactionID, activePolicyExpenseChat); + }); Promise.all(setParticipantsPromises).then(() => Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute( diff --git a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx index b7ccb7a4cc8d6..f5b49b649555e 100644 --- a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx @@ -157,6 +157,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref canBeMissing: true, selector: (policies) => Object.values(policies ?? {}).some((policy) => isPaidGroupPolicy(policy) && isPolicyMember(policy, currentUserPersonalDetails.login)), }); + const reportID = useMemo(() => generateReportID(), []); const { taskReport: viewTourTaskReport, @@ -165,7 +166,6 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref } = useOnboardingTaskInformation(CONST.ONBOARDING_TASK_TYPE.VIEW_TOUR); const isReportInSearch = isOnSearchMoneyRequestReportPage(); - const groupPoliciesWithChatEnabled = getGroupPaidPoliciesWithExpenseChatEnabled(); /** @@ -242,9 +242,9 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref } // Start the scan flow directly - startMoneyRequest(CONST.IOU.TYPE.CREATE, generateReportID(), CONST.IOU.REQUEST_TYPE.SCAN, false, undefined, allTransactionDrafts); + startMoneyRequest(CONST.IOU.TYPE.CREATE, reportID, CONST.IOU.REQUEST_TYPE.SCAN, false, undefined, allTransactionDrafts); }); - }, [shouldRedirectToExpensifyClassic, allTransactionDrafts]); + }, [shouldRedirectToExpensifyClassic, allTransactionDrafts, reportID]); /** * Check if LHN status changed from active to inactive. @@ -325,20 +325,11 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref setModalVisible(true); return; } - startMoneyRequest( - CONST.IOU.TYPE.CREATE, - // When starting to create an expense from the global FAB, there is not an existing report yet. A random optimistic reportID is generated and used - // for all of the routes in the creation flow. - generateReportID(), - undefined, - undefined, - undefined, - allTransactionDrafts, - ); + startMoneyRequest(CONST.IOU.TYPE.CREATE, reportID, undefined, undefined, undefined, allTransactionDrafts); }), }, ]; - }, [translate, shouldRedirectToExpensifyClassic, shouldUseNarrowLayout, allTransactionDrafts]); + }, [translate, shouldRedirectToExpensifyClassic, shouldUseNarrowLayout, allTransactionDrafts, reportID]); const quickActionMenuItems = useMemo(() => { // Define common properties in baseQuickAction @@ -388,7 +379,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref return; } - const quickActionReportID = policyChatForActivePolicy?.reportID || generateReportID(); + const quickActionReportID = policyChatForActivePolicy?.reportID || reportID; startMoneyRequest(CONST.IOU.TYPE.SUBMIT, quickActionReportID, CONST.IOU.REQUEST_TYPE.SCAN, true, undefined, allTransactionDrafts); }); }; @@ -426,6 +417,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref isReportArchived, lastDistanceExpenseType, allTransactionDrafts, + reportID, isRestrictedToPreferredPolicy, ]); @@ -460,13 +452,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref return; } // Start the flow to start tracking a distance request - startDistanceRequest( - CONST.IOU.TYPE.CREATE, - // When starting to create an expense from the global FAB, there is not an existing report yet. A random optimistic reportID is generated and used - // for all of the routes in the creation flow. - generateReportID(), - lastDistanceExpenseType, - ); + startDistanceRequest(CONST.IOU.TYPE.CREATE, reportID, lastDistanceExpenseType); }); }, }, @@ -540,16 +526,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, ref return; } - startMoneyRequest( - CONST.IOU.TYPE.INVOICE, - // When starting to create an invoice from the global FAB, there is not an existing report yet. A random optimistic reportID is generated and used - // for all of the routes in the creation flow. - generateReportID(), - undefined, - undefined, - undefined, - allTransactionDrafts, - ); + startMoneyRequest(CONST.IOU.TYPE.INVOICE, reportID, undefined, undefined, undefined, allTransactionDrafts); }), }, ] diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index 38f46b8364487..4cd0a7026ab3c 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -10,8 +10,10 @@ import useDuplicateTransactionsAndViolations from '@hooks/useDuplicateTransactio import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; +import usePersonalPolicy from '@hooks/usePersonalPolicy'; import useReportIsArchived from '@hooks/useReportIsArchived'; import useShowNotFoundPageInIOUStep from '@hooks/useShowNotFoundPageInIOUStep'; +import {setTransactionReport} from '@libs/actions/Transaction'; import {createDraftTransaction, removeDraftTransaction} from '@libs/actions/TransactionEdit'; import {convertToBackendAmount, isValidCurrencyCode} from '@libs/CurrencyUtils'; import {navigateToParticipantPage} from '@libs/IOUUtils'; @@ -89,6 +91,7 @@ function IOURequestStepAmount({ const [skipConfirmation] = useOnyx(`${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${transactionID}`, {canBeMissing: true}); const defaultExpensePolicy = useDefaultExpensePolicy(); const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); + const personalPolicy = usePersonalPolicy(); const {duplicateTransactions, duplicateTransactionViolations} = useDuplicateTransactionsAndViolations(transactionID ? [transactionID] : []); const [reportAttributesDerived] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {canBeMissing: true, selector: reportsSelector}); const isEditing = action === CONST.IOU.ACTION.EDIT; @@ -265,6 +268,9 @@ function IOURequestStepAmount({ !shouldRestrictUserBillableActions(defaultExpensePolicy.id) ) { const activePolicyExpenseChat = getPolicyExpenseChat(currentUserPersonalDetails.accountID, defaultExpensePolicy?.id); + const shouldAutoReport = !!defaultExpensePolicy?.autoReporting || !!personalPolicy?.autoReporting; + const transactionReportID = shouldAutoReport ? activePolicyExpenseChat?.reportID : CONST.REPORT.UNREPORTED_REPORT_ID; + setTransactionReport(transactionID, {reportID: transactionReportID}, true); setMoneyRequestParticipantsFromReport(transactionID, activePolicyExpenseChat).then(() => { Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute( diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index dc8ef1e3cf452..69dbc0d579047 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -164,11 +164,21 @@ function IOURequestStepConfirmation({ * We want to use a report from the transaction if it exists * Also if the report was submitted and delayed submission is on, then we should use an initial report */ + const isUnreported = transaction?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID; const transactionReport = getReportOrDraftReport(transaction?.reportID); const shouldUseTransactionReport = transactionReport && !(isProcessingReport(transactionReport) && !policyReal?.harvesting?.enabled) && isReportOutstanding(transactionReport, policyReal?.id, undefined, false); - const report = shouldUseTransactionReport ? transactionReport : (reportReal ?? reportDraft); + const report = useMemo(() => { + if (isUnreported) { + return undefined; + } + if (shouldUseTransactionReport) { + return transactionReport; + } + return reportReal ?? reportDraft; + }, [isUnreported, shouldUseTransactionReport, transactionReport, reportReal, reportDraft]); const policy = policyReal ?? policyDraft; + const policyID = isUnreported ? policy?.id : getIOURequestPolicyID(transaction, report); const isDraftPolicy = policy === policyDraft; const policyCategories = policyCategoriesReal ?? policyCategoriesDraft; const receiverParticipant: Participant | InvoiceReceiver | undefined = transaction?.participants?.find((participant) => participant?.accountID) ?? report?.invoiceReceiver; @@ -624,7 +634,7 @@ function IOURequestStepConfirmation({ const trackExpense = useCallback( (selectedParticipants: Participant[], gpsPoints?: GpsPoint) => { - if (!report || !transactions.length) { + if (!transactions.length) { return; } const participant = selectedParticipants.at(0); @@ -888,7 +898,7 @@ function IOURequestStepConfirmation({ return; } - if (iouType === CONST.IOU.TYPE.TRACK || isCategorizingTrackExpense || isSharingTrackExpense) { + if (iouType === CONST.IOU.TYPE.TRACK || isCategorizingTrackExpense || isSharingTrackExpense || isUnreported) { if (Object.values(receiptFiles).filter((receipt) => !!receipt).length && transaction) { // If the transaction amount is zero, then the money is being requested through the "Scan" flow and the GPS coordinates need to be included. if (transaction.amount === 0 && !isSharingTrackExpense && !isCategorizingTrackExpense && locationPermissionGranted) { @@ -1002,6 +1012,7 @@ function IOURequestStepConfirmation({ userLocation, submitPerDiemExpense, existingInvoiceReport, + isUnreported, ], ); @@ -1205,7 +1216,7 @@ function IOURequestStepConfirmation({ reportID={reportID} shouldDisplayReceipt={!isMovingTransactionFromTrackExpense && !isDistanceRequest && !isPerDiemRequest} isPolicyExpenseChat={isPolicyExpenseChat} - policyID={getIOURequestPolicyID(transaction, report)} + policyID={policyID} iouMerchant={transaction?.merchant} iouCreated={transaction?.created} isDistanceRequest={isDistanceRequest} diff --git a/src/pages/iou/request/step/IOURequestStepDestination.tsx b/src/pages/iou/request/step/IOURequestStepDestination.tsx index 074cf26bf7a84..8123cf056c498 100644 --- a/src/pages/iou/request/step/IOURequestStepDestination.tsx +++ b/src/pages/iou/request/step/IOURequestStepDestination.tsx @@ -13,8 +13,10 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails' import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; +import usePersonalPolicy from '@hooks/usePersonalPolicy'; import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; import useThemeStyles from '@hooks/useThemeStyles'; +import {setTransactionReport} from '@libs/actions/Transaction'; import Navigation from '@libs/Navigation/Navigation'; import {getPerDiemCustomUnit, isPolicyAdmin} from '@libs/PolicyUtils'; import {getPolicyExpenseChat} from '@libs/ReportUtils'; @@ -56,6 +58,7 @@ function IOURequestStepDestination({ explicitPolicyID, }: IOURequestStepDestinationProps) { const [policy, policyMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${explicitPolicyID ?? getIOURequestPolicyID(transaction, report)}`, {canBeMissing: false}); + const personalPolicy = usePersonalPolicy(); const {accountID} = useCurrentUserPersonalDetails(); const policyExpenseReport = policy?.id ? getPolicyExpenseChat(accountID, policy.id) : undefined; const {top} = useSafeAreaInsets(); @@ -83,6 +86,9 @@ function IOURequestStepDestination({ } if (selectedDestination !== destination.keyForList) { if (openedFromStartPage) { + const shouldAutoReport = !!policy?.autoReporting || !!personalPolicy?.autoReporting || action !== CONST.IOU.ACTION.CREATE; + const transactionReportID = shouldAutoReport ? policyExpenseReport?.reportID : CONST.REPORT.UNREPORTED_REPORT_ID; + setTransactionReport(transactionID, {reportID: transactionReportID}, true); setMoneyRequestParticipantsFromReport(transactionID, policyExpenseReport); setCustomUnitID(transactionID, customUnit.customUnitID); setMoneyRequestCategory(transactionID, customUnit?.defaultCategory ?? ''); diff --git a/src/pages/iou/request/step/IOURequestStepDistance.tsx b/src/pages/iou/request/step/IOURequestStepDistance.tsx index f5fc78f29a37f..00744e5940bbd 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistance.tsx @@ -19,6 +19,7 @@ import useFetchRoute from '@hooks/useFetchRoute'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; +import usePersonalPolicy from '@hooks/usePersonalPolicy'; import usePolicy from '@hooks/usePolicy'; import usePrevious from '@hooks/usePrevious'; import useShowNotFoundPageInIOUStep from '@hooks/useShowNotFoundPageInIOUStep'; @@ -39,7 +40,7 @@ import { } from '@libs/actions/IOU'; import {init, stop} from '@libs/actions/MapboxToken'; import {openReport} from '@libs/actions/Report'; -import {openDraftDistanceExpense, removeWaypoint, updateWaypoints as updateWaypointsUtil} from '@libs/actions/Transaction'; +import {openDraftDistanceExpense, removeWaypoint, setTransactionReport, updateWaypoints as updateWaypointsUtil} from '@libs/actions/Transaction'; import {createBackupTransaction, removeBackupTransaction, restoreOriginalTransactionFromBackup} from '@libs/actions/TransactionEdit'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import type {MileageRate} from '@libs/DistanceRequestUtils'; @@ -85,6 +86,7 @@ function IOURequestStepDistance({ const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`, {canBeMissing: true}); const [transactionBackup] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${transactionID}`, {canBeMissing: true}); const policy = usePolicy(report?.policyID); + const personalPolicy = usePersonalPolicy(); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: false}); const defaultExpensePolicy = useDefaultExpensePolicy(); const [skipConfirmation] = useOnyx(`${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${transactionID}`, {canBeMissing: false}); @@ -392,6 +394,9 @@ function IOURequestStepDistance({ policy: defaultExpensePolicy, lastSelectedDistanceRates, }); + const shouldAutoReport = !!defaultExpensePolicy?.autoReporting || !!personalPolicy?.autoReporting; + const transactionReportID = shouldAutoReport ? activePolicyExpenseChat?.reportID : CONST.REPORT.UNREPORTED_REPORT_ID; + setTransactionReport(transactionID, {reportID: transactionReportID}, true); setCustomUnitRateID(transactionID, rateID); setMoneyRequestParticipantsFromReport(transactionID, activePolicyExpenseChat).then(() => { Navigation.navigate( @@ -412,6 +417,7 @@ function IOURequestStepDistance({ report, reportNameValuePairs, iouType, + personalPolicy?.autoReporting, defaultExpensePolicy, setDistanceRequestData, shouldSkipConfirmation, diff --git a/src/pages/iou/request/step/IOURequestStepDistanceManual.tsx b/src/pages/iou/request/step/IOURequestStepDistanceManual.tsx index 25aa9e3ad2c78..bf1302f721319 100644 --- a/src/pages/iou/request/step/IOURequestStepDistanceManual.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistanceManual.tsx @@ -12,6 +12,7 @@ import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentU import useDefaultExpensePolicy from '@hooks/useDefaultExpensePolicy'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import usePersonalPolicy from '@hooks/usePersonalPolicy'; import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -26,6 +27,7 @@ import { trackExpense, updateMoneyRequestDistance, } from '@libs/actions/IOU'; +import {setTransactionReport} from '@libs/actions/Transaction'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import {navigateToParticipantPage, shouldUseTransactionDraft} from '@libs/IOUUtils'; @@ -75,6 +77,7 @@ function IOURequestStepDistanceManual({ const [selectedTab, selectedTabResult] = useOnyx(`${ONYXKEYS.COLLECTION.SELECTED_TAB}${CONST.TAB.DISTANCE_REQUEST_TYPE}`, {canBeMissing: true}); const isLoadingSelectedTab = isLoadingOnyxValue(selectedTabResult); const policy = usePolicy(report?.policyID); + const personalPolicy = usePersonalPolicy(); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: false}); const defaultExpensePolicy = useDefaultExpensePolicy(); const [skipConfirmation] = useOnyx(`${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${transactionID}`, {canBeMissing: true}); @@ -247,6 +250,9 @@ function IOURequestStepDistanceManual({ policy: defaultExpensePolicy, lastSelectedDistanceRates, }); + const shouldAutoReport = !!defaultExpensePolicy?.autoReporting || !!personalPolicy?.autoReporting; + const transactionReportID = shouldAutoReport ? activePolicyExpenseChat?.reportID : CONST.REPORT.UNREPORTED_REPORT_ID; + setTransactionReport(transactionID, {reportID: transactionReportID}, true); setCustomUnitRateID(transactionID, rateID); setMoneyRequestParticipantsFromReport(transactionID, activePolicyExpenseChat).then(() => { Navigation.navigate( @@ -274,6 +280,7 @@ function IOURequestStepDistanceManual({ currentUserPersonalDetails.accountID, reportNameValuePairs, isTransactionDraft, + personalPolicy?.autoReporting, defaultExpensePolicy, shouldSkipConfirmation, personalDetails, diff --git a/src/pages/iou/request/step/IOURequestStepDistanceMap.tsx b/src/pages/iou/request/step/IOURequestStepDistanceMap.tsx index f095f69c13e9f..e599521040a5d 100644 --- a/src/pages/iou/request/step/IOURequestStepDistanceMap.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistanceMap.tsx @@ -19,6 +19,7 @@ import useFetchRoute from '@hooks/useFetchRoute'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; +import usePersonalPolicy from '@hooks/usePersonalPolicy'; import usePolicy from '@hooks/usePolicy'; import usePrevious from '@hooks/usePrevious'; import useShowNotFoundPageInIOUStep from '@hooks/useShowNotFoundPageInIOUStep'; @@ -39,7 +40,7 @@ import { } from '@libs/actions/IOU'; import {init, stop} from '@libs/actions/MapboxToken'; import {openReport} from '@libs/actions/Report'; -import {openDraftDistanceExpense, removeWaypoint, updateWaypoints as updateWaypointsUtil} from '@libs/actions/Transaction'; +import {openDraftDistanceExpense, removeWaypoint, setTransactionReport, updateWaypoints as updateWaypointsUtil} from '@libs/actions/Transaction'; import {createBackupTransaction, removeBackupTransaction, restoreOriginalTransactionFromBackup} from '@libs/actions/TransactionEdit'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import type {MileageRate} from '@libs/DistanceRequestUtils'; @@ -85,6 +86,7 @@ function IOURequestStepDistanceMap({ const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`, {canBeMissing: true}); const [transactionBackup] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${transactionID}`, {canBeMissing: true}); const policy = usePolicy(report?.policyID); + const personalPolicy = usePersonalPolicy(); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: false}); const defaultExpensePolicy = useDefaultExpensePolicy(); const [skipConfirmation] = useOnyx(`${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${transactionID}`, {canBeMissing: false}); @@ -392,6 +394,9 @@ function IOURequestStepDistanceMap({ policy: defaultExpensePolicy, lastSelectedDistanceRates, }); + const shouldAutoReport = !!defaultExpensePolicy?.autoReporting || !!personalPolicy?.autoReporting; + const transactionReportID = shouldAutoReport ? activePolicyExpenseChat?.reportID : CONST.REPORT.UNREPORTED_REPORT_ID; + setTransactionReport(transactionID, {reportID: transactionReportID}, true); setCustomUnitRateID(transactionID, rateID); setMoneyRequestParticipantsFromReport(transactionID, activePolicyExpenseChat).then(() => { Navigation.navigate( @@ -412,6 +417,7 @@ function IOURequestStepDistanceMap({ report, reportNameValuePairs, iouType, + personalPolicy?.autoReporting, defaultExpensePolicy, setDistanceRequestData, shouldSkipConfirmation, diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.tsx b/src/pages/iou/request/step/IOURequestStepParticipants.tsx index 2dc1bc57dbb7b..f9ad84c322dad 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.tsx +++ b/src/pages/iou/request/step/IOURequestStepParticipants.tsx @@ -58,6 +58,7 @@ const policySelector = (policy: OnyxEntry): OnyxEntry => outputCurrency: policy.outputCurrency, isPolicyExpenseChatEnabled: policy.isPolicyExpenseChatEnabled, customUnits: policy.customUnits, + autoReporting: policy.autoReporting, }; const policiesSelector = (policies: OnyxCollection) => createPoliciesSelector(policies, policySelector); @@ -92,6 +93,8 @@ function IOURequestStepParticipants({ // We need to set selectedReportID if user has navigated back from confirmation page and navigates to confirmation page with already selected participant const selectedReportID = useRef(participants?.length === 1 ? (participants.at(0)?.reportID ?? reportID) : reportID); + // We can assume that shouldAutoReport is true as the initial value is not used. shouldAutoReport is only used after the selectedReportID changes in addParticipant where we'd update shouldAutoReport too + const shouldAutoReport = useRef(true); const numberOfParticipants = useRef(participants?.length ?? 0); const iouRequestType = getRequestType(initialTransaction); const isSplitRequest = iouType === CONST.IOU.TYPE.SPLIT; @@ -119,6 +122,7 @@ function IOURequestStepParticipants({ const [selfDMReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${selfDMReportID}`, {canBeMissing: true}); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: false}); const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, {canBeMissing: true}); + const personalPolicy = useMemo(() => Object.values(allPolicies ?? {}).find((policy) => policy?.type === CONST.POLICY.TYPE.PERSONAL), [allPolicies]); const isActivePolicyRequest = iouType === CONST.IOU.TYPE.CREATE && isPaidGroupPolicy(activePolicy) && activePolicy?.isPolicyExpenseChatEnabled && !shouldRestrictUserBillableActions(activePolicy.id); @@ -209,6 +213,8 @@ function IOURequestStepParticipants({ } const firstParticipantReportID = val.at(0)?.reportID; + const isPolicyExpenseChat = !!firstParticipant?.isPolicyExpenseChat; + const policy = isPolicyExpenseChat && firstParticipant?.policyID ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${firstParticipant.policyID}`] : undefined; const isInvoice = iouType === CONST.IOU.TYPE.INVOICE && isInvoiceRoomWithID(firstParticipantReportID); numberOfParticipants.current = val.length; @@ -226,8 +232,6 @@ function IOURequestStepParticipants({ if (!isMovingTransactionFromTrackExpense) { // If not moving the transaction from track expense, select the default rate automatically. // Otherwise, keep the original p2p rate and let the user manually change it to the one they want from the workspace. - const isPolicyExpenseChat = !!firstParticipant?.isPolicyExpenseChat; - const policy = isPolicyExpenseChat && firstParticipant?.policyID ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${firstParticipant.policyID}`] : undefined; const rateID = DistanceRequestUtils.getCustomUnitRateID({reportID: firstParticipantReportID, isPolicyExpenseChat, policy, lastSelectedDistanceRates}); if (transactions.length > 0) { @@ -248,6 +252,7 @@ function IOURequestStepParticipants({ if (hasOneValidParticipant && !isInvoice) { selectedReportID.current = reportID; + shouldAutoReport.current = true; return; } @@ -255,8 +260,15 @@ function IOURequestStepParticipants({ // We use || to be sure that if the first participant doesn't have a reportID, we generate a new one. // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing selectedReportID.current = firstParticipantReportID || generateReportID(); + + // IOUs are always reported. non-CREATE actions require a report + if (!isPolicyExpenseChat || action !== CONST.IOU.ACTION.CREATE) { + shouldAutoReport.current = true; + } else { + shouldAutoReport.current = !!policy?.autoReporting || !!personalPolicy?.autoReporting; + } }, - [iouType, transactions, isMovingTransactionFromTrackExpense, reportID, trackExpense, allPolicies, lastSelectedDistanceRates, initialTransactionID], + [action, iouType, transactions, isMovingTransactionFromTrackExpense, reportID, trackExpense, allPolicies, personalPolicy, lastSelectedDistanceRates, initialTransactionID], ); const goToNextStep = useCallback(() => { @@ -270,11 +282,13 @@ function IOURequestStepParticipants({ } const newReportID = selectedReportID.current; + const shouldUpdateTransactionReportID = participants?.at(0)?.reportID !== newReportID; + const transactionReportID = shouldAutoReport.current ? newReportID : CONST.REPORT.UNREPORTED_REPORT_ID; transactions.forEach((transaction) => { setMoneyRequestTag(transaction.transactionID, ''); setMoneyRequestCategory(transaction.transactionID, ''); - if (participants?.at(0)?.reportID !== newReportID) { - setTransactionReport(transaction.transactionID, {reportID: newReportID}, true); + if (shouldUpdateTransactionReportID) { + setTransactionReport(transaction.transactionID, {reportID: transactionReportID}, true); } }); if ((isCategorizing || isShareAction) && numberOfParticipants.current === 0) { diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index a350478533a82..c9b4e8b2c3c79 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -33,10 +33,12 @@ import useIOUUtils from '@hooks/useIOUUtils'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; +import usePersonalPolicy from '@hooks/usePersonalPolicy'; import usePolicy from '@hooks/usePolicy'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import setTestReceipt from '@libs/actions/setTestReceipt'; +import {setTransactionReport} from '@libs/actions/Transaction'; import {dismissProductTraining} from '@libs/actions/Welcome'; import {showCameraPermissionsAlert} from '@libs/fileDownload/FileUtils'; import getPhotoSource from '@libs/fileDownload/getPhotoSource'; @@ -115,6 +117,7 @@ function IOURequestStepScan({ const [receiptFiles, setReceiptFiles] = useState([]); const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`, {canBeMissing: true}); const policy = usePolicy(report?.policyID); + const personalPolicy = usePersonalPolicy(); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: false}); const [skipConfirmation] = useOnyx(`${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${initialTransactionID}`, {canBeMissing: true}); const defaultExpensePolicy = useDefaultExpensePolicy(); @@ -469,6 +472,8 @@ function IOURequestStepScan({ !shouldRestrictUserBillableActions(defaultExpensePolicy.id) ) { const activePolicyExpenseChat = getPolicyExpenseChat(currentUserPersonalDetails.accountID, defaultExpensePolicy?.id); + const shouldAutoReport = !!defaultExpensePolicy?.autoReporting || !!personalPolicy?.autoReporting; + const transactionReportID = shouldAutoReport ? activePolicyExpenseChat?.reportID : CONST.REPORT.UNREPORTED_REPORT_ID; // If the initial transaction has different participants selected that means that the user has changed the participant in the confirmation step if (initialTransaction?.participants && initialTransaction?.participants?.at(0)?.reportID !== activePolicyExpenseChat?.reportID) { @@ -485,7 +490,10 @@ function IOURequestStepScan({ return; } - const setParticipantsPromises = files.map((receiptFile) => setMoneyRequestParticipantsFromReport(receiptFile.transactionID, activePolicyExpenseChat)); + const setParticipantsPromises = files.map((receiptFile) => { + setTransactionReport(receiptFile.transactionID, {reportID: transactionReportID}, true); + return setMoneyRequestParticipantsFromReport(receiptFile.transactionID, activePolicyExpenseChat); + }); Promise.all(setParticipantsPromises).then(() => Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute( @@ -508,6 +516,7 @@ function IOURequestStepScan({ initialTransaction?.reportID, reportNameValuePairs, iouType, + personalPolicy, defaultExpensePolicy, report, initialTransactionID, diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx index c84c87b853006..df8465854c07e 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx @@ -34,11 +34,13 @@ import useIOUUtils from '@hooks/useIOUUtils'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; +import usePersonalPolicy from '@hooks/usePersonalPolicy'; import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import setTestReceipt from '@libs/actions/setTestReceipt'; +import {setTransactionReport} from '@libs/actions/Transaction'; import {clearUserLocation, setUserLocation} from '@libs/actions/UserLocation'; import {dismissProductTraining} from '@libs/actions/Welcome'; import {isMobile, isMobileWebKit} from '@libs/Browser'; @@ -121,6 +123,7 @@ function IOURequestStepScan({ const getScreenshotTimeoutRef = useRef(null); const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`, {canBeMissing: true}); const policy = usePolicy(report?.policyID); + const personalPolicy = usePersonalPolicy(); const [account] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true}); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: false}); const [skipConfirmation] = useOnyx(`${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${initialTransactionID}`, {canBeMissing: true}); @@ -524,6 +527,8 @@ function IOURequestStepScan({ !shouldRestrictUserBillableActions(defaultExpensePolicy.id) ) { const activePolicyExpenseChat = getPolicyExpenseChat(currentUserPersonalDetails.accountID, defaultExpensePolicy?.id); + const shouldAutoReport = !!defaultExpensePolicy?.autoReporting || !!personalPolicy?.autoReporting; + const transactionReportID = shouldAutoReport ? activePolicyExpenseChat?.reportID : CONST.REPORT.UNREPORTED_REPORT_ID; // If the initial transaction has different participants selected that means that the user has changed the participant in the confirmation step if (initialTransaction?.participants && initialTransaction?.participants?.at(0)?.reportID !== activePolicyExpenseChat?.reportID) { @@ -540,7 +545,10 @@ function IOURequestStepScan({ return; } - const setParticipantsPromises = files.map((receiptFile) => setMoneyRequestParticipantsFromReport(receiptFile.transactionID, activePolicyExpenseChat)); + const setParticipantsPromises = files.map((receiptFile) => { + setTransactionReport(receiptFile.transactionID, {reportID: transactionReportID}, true); + return setMoneyRequestParticipantsFromReport(receiptFile.transactionID, activePolicyExpenseChat); + }); Promise.all(setParticipantsPromises).then(() => Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute( @@ -563,6 +571,7 @@ function IOURequestStepScan({ initialTransaction?.reportID, reportNameValuePairs, iouType, + personalPolicy?.autoReporting, defaultExpensePolicy, report, initialTransactionID, diff --git a/src/pages/workspace/duplicate/utils.ts b/src/pages/workspace/duplicate/utils.ts index aa4a78b79e705..a672c22087c39 100644 --- a/src/pages/workspace/duplicate/utils.ts +++ b/src/pages/workspace/duplicate/utils.ts @@ -1,7 +1,6 @@ import type {LocaleContextProps} from '@components/LocaleContextProvider'; import {getCorrectedAutoReportingFrequency, getWorkflowApprovalsUnavailable, hasVBBA} from '@libs/PolicyUtils'; import {getAutoReportingFrequencyDisplayNames} from '@pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage'; -import type {AutoReportingFrequencyKey} from '@pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage'; import {isAuthenticationError} from '@userActions/connections'; import CONST from '@src/CONST'; import type {Policy} from '@src/types/onyx'; @@ -53,8 +52,7 @@ function getWorkflowRules(policy: Policy | undefined, translate: LocaleContextPr const shouldShowBankAccount = !!bankAccountID && policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES; if (policy?.autoReportingFrequency !== CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT && !hasDelayedSubmissionError) { - const title = - getAutoReportingFrequencyDisplayNames(translate)[(getCorrectedAutoReportingFrequency(policy) as AutoReportingFrequencyKey) ?? CONST.POLICY.AUTO_REPORTING_FREQUENCIES.WEEKLY]; + const title = getAutoReportingFrequencyDisplayNames(translate)[getCorrectedAutoReportingFrequency(policy) ?? CONST.POLICY.AUTO_REPORTING_FREQUENCIES.WEEKLY]; total.push(`${title} ${translate('workspace.duplicateWorkspace.delayedSubmission')}`); } if ([CONST.POLICY.APPROVAL_MODE.BASIC, CONST.POLICY.APPROVAL_MODE.ADVANCED].some((approvalMode) => approvalMode === policy?.approvalMode) && !hasApprovalError) { diff --git a/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx b/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx index 5d410dc92e351..1cd3207b0841f 100644 --- a/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage.tsx @@ -24,7 +24,7 @@ import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -type AutoReportingFrequencyKey = Exclude, 'instant'>; +type AutoReportingFrequencyKey = ValueOf; type WorkspaceAutoReportingFrequencyPageProps = WithPolicyOnyxProps & PlatformStackScreenProps; @@ -43,6 +43,7 @@ const getAutoReportingFrequencyDisplayNames = (translate: LocaleContextProps['tr [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.WEEKLY]: translate('workflowsPage.frequencies.weekly'), [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.SEMI_MONTHLY]: translate('workflowsPage.frequencies.twiceAMonth'), [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP]: translate('workflowsPage.frequencies.byTrip'), + [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT]: translate('workflowsPage.frequencies.instant'), [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL]: translate('workflowsPage.frequencies.manually'), }); @@ -122,7 +123,7 @@ function WorkspaceAutoReportingFrequencyPage({policy, route}: WorkspaceAutoRepor addBottomSafeAreaPadding > ; @@ -193,30 +192,24 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { return [ { - title: translate('workflowsPage.delaySubmissionTitle'), - subtitle: translate('workflowsPage.delaySubmissionDescription'), - switchAccessibilityLabel: translate('workflowsPage.delaySubmissionDescription'), - onToggle: (isEnabled: boolean) => { - setWorkspaceAutoReportingFrequency(route.params.policyID, isEnabled ? CONST.POLICY.AUTO_REPORTING_FREQUENCIES.WEEKLY : CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT); - }, + title: translate('workflowsPage.submissionFrequency'), + subtitle: translate('workflowsPage.submissionFrequencyDescription'), + switchAccessibilityLabel: translate('workflowsPage.submissionFrequencyDescription'), + onToggle: (isEnabled: boolean) => (policy ? setWorkspaceAutoHarvesting(policy, isEnabled) : undefined), subMenuItems: ( ), - isActive: (policy?.autoReportingFrequency !== CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT && !hasDelayedSubmissionError) ?? false, + isActive: (policy?.autoReporting && !hasDelayedSubmissionError) ?? false, pendingAction: policy?.pendingFields?.autoReporting ?? policy?.pendingFields?.autoReportingFrequency, errors: getLatestErrorField(policy ?? {}, CONST.POLICY.COLLECTION_KEYS.AUTOREPORTING), onCloseError: () => clearPolicyErrorField(route.params.policyID, CONST.POLICY.COLLECTION_KEYS.AUTOREPORTING),