From b8cc6296721501f486858eb43d065a711930509d Mon Sep 17 00:00:00 2001 From: Wildan M Date: Fri, 1 Nov 2024 16:27:08 +0700 Subject: [PATCH 001/325] Fix old data briefly appeared --- src/pages/workspace/categories/EditCategoryPage.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/pages/workspace/categories/EditCategoryPage.tsx b/src/pages/workspace/categories/EditCategoryPage.tsx index 330c47f0197f..0892b91fbdd3 100644 --- a/src/pages/workspace/categories/EditCategoryPage.tsx +++ b/src/pages/workspace/categories/EditCategoryPage.tsx @@ -49,11 +49,14 @@ function EditCategoryPage({route}: EditCategoryPageProps) { if (currentCategoryName !== newCategoryName) { Category.renamePolicyCategory(route.params.policyID, {oldName: currentCategoryName, newName: values.categoryName}); } - Navigation.goBack( - isQuickSettingsFlow - ? ROUTES.SETTINGS_CATEGORY_SETTINGS.getRoute(route.params.policyID, route.params.categoryName, backTo) - : ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(route.params.policyID, route.params.categoryName), - ); + + Navigation.setNavigationActionToMicrotaskQueue(()=>{ + Navigation.goBack( + isQuickSettingsFlow + ? ROUTES.SETTINGS_CATEGORY_SETTINGS.getRoute(route.params.policyID, route.params.categoryName, backTo) + : ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(route.params.policyID, route.params.categoryName), + ); + }); }, [isQuickSettingsFlow, currentCategoryName, route.params.categoryName, route.params.policyID, backTo], ); From a162fe71045c28c4d5f1075e27b84e90f3143185 Mon Sep 17 00:00:00 2001 From: NJ-2020 Date: Tue, 5 Nov 2024 15:07:15 -0800 Subject: [PATCH 002/325] fix wallet phone validation page --- src/libs/GetPhysicalCardUtils.ts | 6 +++++- .../settings/Wallet/Card/GetPhysicalCardPhone.tsx | 15 +++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/libs/GetPhysicalCardUtils.ts b/src/libs/GetPhysicalCardUtils.ts index 8dc46204db3c..ff0c02aff54c 100644 --- a/src/libs/GetPhysicalCardUtils.ts +++ b/src/libs/GetPhysicalCardUtils.ts @@ -1,4 +1,6 @@ +import {Str} from 'expensify-common'; import type {OnyxEntry} from 'react-native-onyx'; +import * as PhoneNumberUtils from '@libs/PhoneNumber'; import ROUTES from '@src/ROUTES'; import type {Route} from '@src/ROUTES'; import type {GetPhysicalCardForm} from '@src/types/form'; @@ -11,11 +13,13 @@ import * as UserUtils from './UserUtils'; function getCurrentRoute(domain: string, privatePersonalDetails: OnyxEntry): Route { const {legalFirstName, legalLastName, phoneNumber} = privatePersonalDetails ?? {}; const address = PersonalDetailsUtils.getCurrentAddress(privatePersonalDetails); + const phoneNumberWithCountryCode = LoginUtils.appendCountryCode(phoneNumber ?? ''); + const parsedPhoneNumber = PhoneNumberUtils.parsePhoneNumber(phoneNumberWithCountryCode); if (!legalFirstName && !legalLastName) { return ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_NAME.getRoute(domain); } - if (!phoneNumber || !LoginUtils.validateNumber(phoneNumber)) { + if (!phoneNumber || !parsedPhoneNumber.possible || !Str.isValidE164Phone(phoneNumberWithCountryCode.slice(0))) { return ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_PHONE.getRoute(domain); } if (!(address?.street && address?.city && address?.state && address?.country && address?.zip)) { diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx b/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx index 56d5a29a3203..ad04d9f3fb9c 100644 --- a/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx +++ b/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx @@ -1,4 +1,5 @@ import type {StackScreenProps} from '@react-navigation/stack'; +import {Str} from 'expensify-common'; import React from 'react'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -6,6 +7,8 @@ import InputWrapper from '@components/Form/InputWrapper'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import * as LoginUtils from '@libs/LoginUtils'; +import * as PhoneNumberUtils from '@libs/PhoneNumber'; +import * as ValidationUtils from '@libs/ValidationUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -40,13 +43,17 @@ function GetPhysicalCardPhone({ const {phoneNumber: phoneNumberToValidate = ''} = values ?? {}; const errors: OnValidateResult = {}; - - if (!LoginUtils.validateNumber(phoneNumberToValidate)) { - errors.phoneNumber = translate('common.error.phoneNumber'); - } else if (!phoneNumberToValidate) { + if (!ValidationUtils.isRequiredFulfilled(phoneNumberToValidate)) { errors.phoneNumber = translate('common.error.fieldRequired'); } + const phoneNumberWithCountryCode = LoginUtils.appendCountryCode(phoneNumberToValidate); + const parsedPhoneNumber = PhoneNumberUtils.parsePhoneNumber(phoneNumberWithCountryCode); + + if (!parsedPhoneNumber.possible || !Str.isValidE164Phone(phoneNumberWithCountryCode.slice(0))) { + errors.phoneNumber = translate('bankAccount.error.phoneNumber'); + } + return errors; }; From 8e3babe9a7e599b8f1d18a52d3bfdc77c4d0c464 Mon Sep 17 00:00:00 2001 From: NJ-2020 Date: Wed, 6 Nov 2024 22:39:18 -0800 Subject: [PATCH 003/325] fix eslint warning and unnecessary changes --- src/libs/GetPhysicalCardUtils.ts | 2 +- src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/GetPhysicalCardUtils.ts b/src/libs/GetPhysicalCardUtils.ts index ff0c02aff54c..9ed192b09233 100644 --- a/src/libs/GetPhysicalCardUtils.ts +++ b/src/libs/GetPhysicalCardUtils.ts @@ -1,6 +1,5 @@ import {Str} from 'expensify-common'; import type {OnyxEntry} from 'react-native-onyx'; -import * as PhoneNumberUtils from '@libs/PhoneNumber'; import ROUTES from '@src/ROUTES'; import type {Route} from '@src/ROUTES'; import type {GetPhysicalCardForm} from '@src/types/form'; @@ -8,6 +7,7 @@ import type {LoginList, PrivatePersonalDetails} from '@src/types/onyx'; import * as LoginUtils from './LoginUtils'; import Navigation from './Navigation/Navigation'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; +import * as PhoneNumberUtils from './PhoneNumber'; import * as UserUtils from './UserUtils'; function getCurrentRoute(domain: string, privatePersonalDetails: OnyxEntry): Route { diff --git a/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx b/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx index ad04d9f3fb9c..ce50a224c20d 100644 --- a/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx +++ b/src/pages/settings/Wallet/Card/GetPhysicalCardPhone.tsx @@ -43,6 +43,7 @@ function GetPhysicalCardPhone({ const {phoneNumber: phoneNumberToValidate = ''} = values ?? {}; const errors: OnValidateResult = {}; + if (!ValidationUtils.isRequiredFulfilled(phoneNumberToValidate)) { errors.phoneNumber = translate('common.error.fieldRequired'); } From 81cf18979ba9c934d4eed36deb729ef2fd307e4d Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 14 Nov 2024 16:55:46 +0700 Subject: [PATCH 004/325] Update correct next approver with category/tag rules --- src/libs/ReportUtils.ts | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b220c2db20b6..5211ea986bd9 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -61,6 +61,7 @@ import * as IOU from './actions/IOU'; import * as PolicyActions from './actions/Policy/Policy'; import * as store from './actions/ReimbursementAccount/store'; import * as SessionUtils from './actions/Session'; +import {getCategoryApproverRule} from './CategoryUtils'; import * as CurrencyUtils from './CurrencyUtils'; import DateUtils from './DateUtils'; import {hasValidDraftComment} from './DraftCommentUtils'; @@ -6351,7 +6352,7 @@ function shouldDisplayViolationsRBRInLHN(report: OnyxEntry, transactionV // - Belong to the same workspace // And if any have a violation, then it should have a RBR const allReports = Object.values(ReportConnection.getAllReports() ?? {}) as Report[]; - const potentialReports = allReports.filter((r) => r.ownerAccountID === currentUserAccountID && (r.stateNum ?? 0) <= 1 && r.policyID === report.policyID); + const potentialReports = allReports.filter((r) => r?.ownerAccountID === currentUserAccountID && (r.stateNum ?? 0) <= 1 && r.policyID === report.policyID); return potentialReports.some( (potentialReport) => hasViolations(potentialReport.reportID, transactionViolations) || hasWarningTypeViolations(potentialReport.reportID, transactionViolations), ); @@ -8368,9 +8369,47 @@ function isExported(reportActions: OnyxEntry) { return Object.values(reportActions).some((action) => ReportActionsUtils.isExportIntegrationAction(action)); } +function getRuleApprovers(policy: OnyxEntry, expenseReport: OnyxEntry) { + const categoryAppovers: string[] = []; + const tagApprovers: string[] = []; + const allReportTransactions = TransactionUtils.getAllReportTransactions(expenseReport?.reportID).sort((transA, transB) => (transA.created < transB.created ? -1 : 1)); + + // Before submitting to their `submitsTo` (in a policy on Advanced Approvals), submit to category/tag approvers. + // Category approvers are prioritized, then tag approvers. + for (let i = 0; i < allReportTransactions.length; i++) { + const transaction = allReportTransactions.at(i); + const tag = TransactionUtils.getTag(transaction); + const category = TransactionUtils.getCategory(transaction); + const categoryAppover = getCategoryApproverRule(policy?.rules?.approvalRules ?? [], category)?.approver; + const tagApprover = PolicyUtils.getTagApproverRule(policy?.id ?? '-1', tag)?.approver; + if (categoryAppover) { + categoryAppovers.push(categoryAppover); + } + + if (tagApprover) { + tagApprovers.push(tagApprover); + } + } + + return [...categoryAppovers, ...tagApprovers]; +} + function getApprovalChain(policy: OnyxEntry, expenseReport: OnyxEntry): string[] { const approvalChain: string[] = []; const reportTotal = expenseReport?.total ?? 0; + const submitterEmail = PersonalDetailsUtils.getLoginsByAccountIDs([expenseReport?.ownerAccountID ?? -1]).at(0) ?? ''; + + // Get category/tag approver list + const ruleApprovers = getRuleApprovers(policy, expenseReport); + + // Push rule approvers to approvalChain list before submitsTo/forwardsTo approvers + ruleApprovers.forEach((ruleApprover) => { + // Don't push submiiter to approve as a rule approver + if (approvalChain.includes(ruleApprover) || ruleApprover === submitterEmail) { + return; + } + approvalChain.push(ruleApprover); + }); // If the policy is not on advanced approval mode, we should not use the approval chain even if it exists. if (!PolicyUtils.isControlOnAdvancedApprovalMode(policy)) { From 9b517d4bf2bdb162615aceb318049e12c3105fa5 Mon Sep 17 00:00:00 2001 From: Tomasz Lesniakiewicz Date: Thu, 21 Nov 2024 16:14:13 +0100 Subject: [PATCH 005/325] fix: remove duplicates when report is same as policy --- src/components/ParentNavigationSubtitle.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index 997106f3e649..9edf73e5f36f 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -61,7 +61,9 @@ function ParentNavigationSubtitle({parentNavigationSubtitleData, parentReportAct {reportName} )} - {!!workspaceName && {` ${translate('threads.in')} ${workspaceName}`}} + {workspaceName && workspaceName !== reportName && ( + {` ${translate('threads.in')} ${workspaceName}`} + )} ); From ee7c1f5a65fae14e2cee8cda31ff631b446b42f7 Mon Sep 17 00:00:00 2001 From: Tomasz Lesniakiewicz Date: Thu, 21 Nov 2024 19:17:07 +0100 Subject: [PATCH 006/325] fix lint --- src/components/ParentNavigationSubtitle.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index 9edf73e5f36f..c80e8a8caf1a 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -61,7 +61,7 @@ function ParentNavigationSubtitle({parentNavigationSubtitleData, parentReportAct {reportName} )} - {workspaceName && workspaceName !== reportName && ( + {!!workspaceName && workspaceName !== reportName && ( {` ${translate('threads.in')} ${workspaceName}`} )} From 9fa35aba365bb10caa0048a7087f34c3532d50c7 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Mon, 25 Nov 2024 22:36:08 +0700 Subject: [PATCH 007/325] sort all transactons correctly --- src/libs/PolicyUtils.ts | 4 +-- src/libs/ReportUtils.ts | 2 +- src/libs/TransactionUtils/index.ts | 40 +++++++++++++++++++++++++++++- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index dc1c07388293..e47e19f3d330 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -37,7 +37,7 @@ import * as Localize from './Localize'; import Navigation from './Navigation/Navigation'; import * as NetworkStore from './Network/NetworkStore'; import {getAccountIDsByLogins, getLoginsByAccountIDs, getPersonalDetailByEmail} from './PersonalDetailsUtils'; -import {getAllReportTransactions, getCategory, getTag} from './TransactionUtils'; +import {getAllSortedTransactions, getCategory, getTag} from './TransactionUtils'; type MemberEmailsToAccountIDs = Record; @@ -538,7 +538,7 @@ function getSubmitToAccountID(policy: OnyxEntry, expenseReport: OnyxEntr let categoryAppover; let tagApprover; - const allTransactions = getAllReportTransactions(expenseReport?.reportID).sort((transA, transB) => (transA.created < transB.created ? -1 : 1)); + const allTransactions = getAllSortedTransactions(expenseReport?.reportID ?? '-1'); // Before submitting to their `submitsTo` (in a policy on Advanced Approvals), submit to category/tag approvers. // Category approvers are prioritized, then tag approvers. diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index ff9e85c24e82..e598289a373c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -8387,7 +8387,7 @@ function isExported(reportActions: OnyxEntry) { function getRuleApprovers(policy: OnyxEntry, expenseReport: OnyxEntry) { const categoryAppovers: string[] = []; const tagApprovers: string[] = []; - const allReportTransactions = TransactionUtils.getAllReportTransactions(expenseReport?.reportID).sort((transA, transB) => (transA.created < transB.created ? -1 : 1)); + const allReportTransactions = TransactionUtils.getAllSortedTransactions(expenseReport?.reportID ?? '-1'); // Before submitting to their `submitsTo` (in a policy on Advanced Approvals), submit to category/tag approvers. // Category approvers are prioritized, then tag approvers. diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 1fc7cad2f456..44058cd416d0 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -25,7 +25,20 @@ import type {IOURequestType} from '@userActions/IOU'; import CONST from '@src/CONST'; import type {IOUType} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Beta, OnyxInputOrEntry, Policy, RecentWaypoint, Report, ReviewDuplicates, TaxRate, TaxRates, Transaction, TransactionViolation, TransactionViolations} from '@src/types/onyx'; +import type { + Beta, + OnyxInputOrEntry, + Policy, + RecentWaypoint, + Report, + ReportAction, + ReviewDuplicates, + TaxRate, + TaxRates, + Transaction, + TransactionViolation, + TransactionViolations, +} from '@src/types/onyx'; import type {Attendee} from '@src/types/onyx/IOU'; import type {SearchPolicy, SearchReport} from '@src/types/onyx/SearchResults'; import type {Comment, Receipt, TransactionChanges, TransactionPendingFieldsKey, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; @@ -1218,6 +1231,30 @@ function buildTransactionsMergeParams(reviewDuplicates: OnyxEntry> { + // We need sort all transactions by sorting the parent report actions because `created` of the transaction only has format `YYYY-MM-DD` which can cause the wrong sorting + const allCreatedIOUActions = Object.values(ReportActionsUtils.getAllReportActions(iouReportID)) + ?.filter((reportAction): reportAction is ReportAction => { + if (!ReportActionsUtils.isMoneyRequestAction(reportAction)) { + return false; + } + const message = ReportActionsUtils.getOriginalMessage(reportAction); + if (!message?.IOUTransactionID) { + return false; + } + return true; + }) + .sort((actionA, actionB) => (actionA.created < actionB.created ? -1 : 1)); + + return allCreatedIOUActions.map((iouAction) => { + const transactionID = ReportActionsUtils.getOriginalMessage(iouAction)?.IOUTransactionID ?? '-1'; + return getTransaction(transactionID); + }); +} + export { buildOptimisticTransaction, calculateTaxAmount, @@ -1301,6 +1338,7 @@ export { getCardName, hasReceiptSource, shouldShowAttendees, + getAllSortedTransactions, }; export type {TransactionChanges}; From a33274b0f14aca64e8b9d5d337efac86670249f2 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 20 Nov 2024 10:11:36 +0100 Subject: [PATCH 008/325] Create trip details screen --- src/ROUTES.ts | 4 +++ src/SCREENS.ts | 1 + .../ModalStackNavigators/index.tsx | 1 + src/libs/Navigation/linkingConfig/config.ts | 1 + src/pages/Travel/TripDetails.tsx | 36 +++++++++++++++++++ 5 files changed, 43 insertions(+) create mode 100644 src/pages/Travel/TripDetails.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index d8f8b0f91105..f324d4af6d2e 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1343,6 +1343,10 @@ const ROUTES = { TRAVEL_MY_TRIPS: 'travel', TRAVEL_TCS: 'travel/terms', TRACK_TRAINING_MODAL: 'track-training', + TRAVEL_TRIP_DETAILS: { + route: 'r/:reportID/trip/:transactionID', + getRoute: (reportID: string, transactionID: string) => `r/${reportID}/trip/${transactionID}` as const, + }, ONBOARDING_ROOT: { route: 'onboarding', getRoute: (backTo?: string) => getUrlWithBackToParam(`onboarding`, backTo), diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 5fd64b0fc0d0..f6a53da3f199 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -27,6 +27,7 @@ const SCREENS = { TRAVEL: { MY_TRIPS: 'Travel_MyTrips', TCS: 'Travel_TCS', + TRIP_DETAILS: 'Trip_Details', }, SEARCH: { CENTRAL_PANE: 'Search_Central_Pane', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 9295281755e5..05bec49e9bd9 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -105,6 +105,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator({ [SCREENS.TRAVEL.MY_TRIPS]: () => require('../../../../pages/Travel/MyTripsPage').default, [SCREENS.TRAVEL.TCS]: () => require('../../../../pages/Travel/TravelTerms').default, + [SCREENS.TRAVEL.TRIP_DETAILS]: () => require('../../../pages/TripDetails').default, }); const SplitDetailsModalStackNavigator = createModalStackNavigator({ diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 476711c7c116..2d1511789c24 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1320,6 +1320,7 @@ const config: LinkingOptions['config'] = { screens: { [SCREENS.TRAVEL.MY_TRIPS]: ROUTES.TRAVEL_MY_TRIPS, [SCREENS.TRAVEL.TCS]: ROUTES.TRAVEL_TCS, + [SCREENS.TRAVEL.TRIP_DETAILS]: ROUTES.TRAVEL_TRIP_DETAILS, }, }, [SCREENS.RIGHT_MODAL.SEARCH_REPORT]: { diff --git a/src/pages/Travel/TripDetails.tsx b/src/pages/Travel/TripDetails.tsx new file mode 100644 index 000000000000..8c3d1bbadfc1 --- /dev/null +++ b/src/pages/Travel/TripDetails.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import {NativeModules} from 'react-native'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; + +function TripDetails() { + const {translate} = useLocalize(); + const {canUseSpotnanaTravel} = usePermissions(); + + return ( + + + + + + ); +} + +TripDetails.displayName = 'TripDetails'; + +export default TripDetails; From 15c9ae36fc845f298078a1f28939064158088b7b Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 20 Nov 2024 10:11:49 +0100 Subject: [PATCH 009/325] Add translation for trip support --- src/languages/en.ts | 1 + src/languages/es.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index c1067e195985..ec0e49353bbb 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2424,6 +2424,7 @@ const translations = { hotel: 'Hotel', car: 'Car', viewTrip: 'View trip', + tripSupport: 'Trip support', viewTripDetails: 'View trip details', trip: 'Trip', trips: 'Trips', diff --git a/src/languages/es.ts b/src/languages/es.ts index f7af1be45139..623afb1ff16a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2448,6 +2448,7 @@ const translations = { hotel: 'Hotel', car: 'Auto', viewTrip: 'Ver viaje', + tripSupport: 'Soporte de Viaje', viewTripDetails: 'Ver detalles del viaje', trip: 'Viaje', trips: 'Viajes', From 3ae7f986f7fee31a3f065e2b2be31aece0c3ed49 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 20 Nov 2024 10:16:31 +0100 Subject: [PATCH 010/325] Add undefined type for trip details --- src/libs/Navigation/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 5877c9c10218..caa5ff5d1a06 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1380,6 +1380,7 @@ type RightModalNavigatorParamList = { type TravelNavigatorParamList = { [SCREENS.TRAVEL.MY_TRIPS]: undefined; + [SCREENS.TRAVEL.TRIP_DETAILS]: undefined; }; type FullScreenNavigatorParamList = { From d2e1df5cf70c7bf112174a58369352fff890b69e Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 20 Nov 2024 12:39:33 +0100 Subject: [PATCH 011/325] Fix navigation config --- src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx | 2 +- src/libs/Navigation/linkingConfig/config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 05bec49e9bd9..beb030ccb09d 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -105,7 +105,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator({ [SCREENS.TRAVEL.MY_TRIPS]: () => require('../../../../pages/Travel/MyTripsPage').default, [SCREENS.TRAVEL.TCS]: () => require('../../../../pages/Travel/TravelTerms').default, - [SCREENS.TRAVEL.TRIP_DETAILS]: () => require('../../../pages/TripDetails').default, + [SCREENS.TRAVEL.TRIP_DETAILS]: () => require('../../../../pages/Travel/TripDetails').default, }); const SplitDetailsModalStackNavigator = createModalStackNavigator({ diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 2d1511789c24..317c1e15e04f 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1320,7 +1320,7 @@ const config: LinkingOptions['config'] = { screens: { [SCREENS.TRAVEL.MY_TRIPS]: ROUTES.TRAVEL_MY_TRIPS, [SCREENS.TRAVEL.TCS]: ROUTES.TRAVEL_TCS, - [SCREENS.TRAVEL.TRIP_DETAILS]: ROUTES.TRAVEL_TRIP_DETAILS, + [SCREENS.TRAVEL.TRIP_DETAILS]: ROUTES.TRAVEL_TRIP_DETAILS.route, }, }, [SCREENS.RIGHT_MODAL.SEARCH_REPORT]: { From 7394a09389a39b499723e05548285e930ea34b9f Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 22 Nov 2024 14:19:35 +0100 Subject: [PATCH 012/325] Link the correct backTo on trip details screen --- src/ROUTES.ts | 2 +- src/components/ReportActionItem/MoneyRequestView.tsx | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index f324d4af6d2e..920e328ebfff 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1345,7 +1345,7 @@ const ROUTES = { TRACK_TRAINING_MODAL: 'track-training', TRAVEL_TRIP_DETAILS: { route: 'r/:reportID/trip/:transactionID', - getRoute: (reportID: string, transactionID: string) => `r/${reportID}/trip/${transactionID}` as const, + getRoute: (reportID: string, transactionID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/trip/${transactionID}`, backTo), }, ONBOARDING_ROOT: { route: 'onboarding', diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 381f01aadd89..1533a7515a98 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -34,7 +34,6 @@ import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import Navigation from '@navigation/Navigation'; import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground'; import * as IOU from '@userActions/IOU'; -import * as Link from '@userActions/Link'; import * as Transaction from '@userActions/Transaction'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; @@ -84,7 +83,6 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals const session = useSession(); const {isOffline} = useNetwork(); const {translate, toLocaleDigit} = useLocalize(); - const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const parentReportID = report?.parentReportID ?? '-1'; const policyID = report?.policyID ?? '-1'; const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`); @@ -696,11 +694,9 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals { - Link.openTravelDotLink(activePolicyID, CONST.TRIP_ID_PATH(tripID)); - }} + onPress={() => + Navigation.navigate(ROUTES.TRAVEL_TRIP_DETAILS.getRoute(report?.reportID ?? '-1', transaction?.transactionID ?? '-1', Navigation.getReportRHPActiveRoute())) + } /> )} {shouldShowAttendees && ( From 2c9d2a8c72caa65b5b28a7f5a2294ae00ea975d5 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 22 Nov 2024 16:21:06 +0100 Subject: [PATCH 013/325] Access data and fix getTripIDFromTransactionParentReportID --- src/CONST.ts | 2 +- .../ReportActionItem/MoneyRequestView.tsx | 2 +- src/libs/Navigation/types.ts | 6 ++- src/libs/ReportUtils.ts | 6 +-- src/pages/Travel/TripDetails.tsx | 50 ++++++++++++++++++- 5 files changed, 58 insertions(+), 8 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index ee70e3b29668..80b52c65bfcf 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -4427,7 +4427,7 @@ const CONST = { BOOK_TRAVEL_DEMO_URL: 'https://calendly.com/d/ck2z-xsh-q97/expensify-travel-demo-travel-page', TRAVEL_DOT_URL: 'https://travel.expensify.com', STAGING_TRAVEL_DOT_URL: 'https://staging.travel.expensify.com', - TRIP_ID_PATH: (tripID: string) => `trips/${tripID}`, + TRIP_ID_PATH: (tripID?: string) => (tripID ? `trips/${tripID}` : undefined), SPOTNANA_TMC_ID: '8e8e7258-1cf3-48c0-9cd1-fe78a6e31eed', STAGING_SPOTNANA_TMC_ID: '7a290c6e-5328-4107-aff6-e48765845b81', SCREEN_READER_STATES: { diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 1533a7515a98..6bc356dd6f15 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -186,7 +186,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals const shouldShowAttendees = useMemo(() => TransactionUtils.shouldShowAttendees(iouType, policy), [iouType, policy]); const shouldShowTax = isTaxTrackingEnabled(isPolicyExpenseChat, policy, isDistanceRequest); - const tripID = ReportUtils.getTripIDFromTransactionParentReport(parentReport); + const tripID = ReportUtils.getTripIDFromTransactionParentReportID(parentReport?.parentReportID); const shouldShowViewTripDetails = TransactionUtils.hasReservationList(transaction) && !!tripID; const {getViolationsForField} = useViolations(transactionViolations ?? [], isReceiptBeingScanned || !ReportUtils.isPaidGroupPolicy(report)); diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index caa5ff5d1a06..469d20d15bbc 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1380,7 +1380,11 @@ type RightModalNavigatorParamList = { type TravelNavigatorParamList = { [SCREENS.TRAVEL.MY_TRIPS]: undefined; - [SCREENS.TRAVEL.TRIP_DETAILS]: undefined; + [SCREENS.TRAVEL.TRIP_DETAILS]: { + reportID: string; + transactionID: string; + backTo?: string; + }; }; type FullScreenNavigatorParamList = { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 36095b7d88ec..72f11f4a3150 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -8030,8 +8030,8 @@ function getTripTransactions(tripRoomReportID: string | undefined, reportFieldTo return tripTransactionReportIDs.flatMap((reportID) => reportsTransactions[reportID ?? ''] ?? []); } -function getTripIDFromTransactionParentReport(transactionParentReport: OnyxEntry | undefined | null): string | undefined { - return getReportOrDraftReport(transactionParentReport?.parentReportID)?.tripData?.tripID; +function getTripIDFromTransactionParentReportID(transactionParentReportID: string | undefined): string | undefined { + return getReportOrDraftReport(transactionParentReportID)?.tripData?.tripID; } /** @@ -8700,7 +8700,7 @@ export { updateReportPreview, temporary_getMoneyRequestOptions, getTripTransactions, - getTripIDFromTransactionParentReport, + getTripIDFromTransactionParentReportID, buildOptimisticInvoiceReport, getInvoiceChatByParticipants, shouldShowMerchantColumn, diff --git a/src/pages/Travel/TripDetails.tsx b/src/pages/Travel/TripDetails.tsx index 8c3d1bbadfc1..dab130bde1aa 100644 --- a/src/pages/Travel/TripDetails.tsx +++ b/src/pages/Travel/TripDetails.tsx @@ -1,15 +1,41 @@ +import type {StackScreenProps} from '@react-navigation/stack'; import React from 'react'; import {NativeModules} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Expensicons from '@components/Icon/Expensicons'; +import MenuItem from '@components/MenuItem'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type {TravelNavigatorParamList} from '@libs/Navigation/types'; +import * as ReportUtils from '@libs/ReportUtils'; +import * as Link from '@userActions/Link'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; -function TripDetails() { +type TripDetailsProps = StackScreenProps; + +function TripDetails({route}: TripDetailsProps) { + const styles = useThemeStyles(); + const theme = useTheme(); const {translate} = useLocalize(); const {canUseSpotnanaTravel} = usePermissions(); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); + const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${route.params.transactionID}`); + const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`); + const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID ?? '-1'}`); + const tripID = ReportUtils.getTripIDFromTransactionParentReportID(parentReport?.parentReportID); + + console.log(`transaction = `, transaction); + console.log(`report = `, report); + console.log(`parentReport = `, parentReport); + return ( + { + Link.openTravelDotLink(activePolicyID, CONST.TRIP_ID_PATH(tripID)); + }} + /> + { + Link.openTravelDotLink(activePolicyID, CONST.TRIP_ID_PATH(tripID)); + }} + /> ); From 610ed48107fc58b2dc04a569aaec214da5ab5ffc Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 25 Nov 2024 12:04:05 +0100 Subject: [PATCH 014/325] Add english translations --- src/languages/en.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index ec0e49353bbb..e525ac717729 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2421,9 +2421,34 @@ const translations = { error: 'You must accept the Terms & Conditions for travel to continue', }, flight: 'Flight', + flightDetails: { + layover: 'layover', + takeOff: 'Take-off', + landing: 'Landing', + passenger: 'Passenger', + seat: 'Seat', + class: 'Cabin Class', + recordLocator: 'Record locator', + }, hotel: 'Hotel', + hotelDetails: { + checkIn: 'Check-in', + checkOut: 'Check-out', + roomType: 'Room type', + cancellation: 'Cancellation policy', + confirmation: 'Confirmation number', + }, car: 'Car', + carDetails: { + pickUp: 'Pick-up', + dropOff: 'Drop-off', + driver: 'Driver', + carType: 'Car type', + cancellation: 'Cancellation policy', + confirmation: 'Confirmation number', + }, viewTrip: 'View trip', + modifyTrip: 'Modify trip', tripSupport: 'Trip support', viewTripDetails: 'View trip details', trip: 'Trip', From 644243cc5815c134ed7624000e4103b38f94498f Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 25 Nov 2024 17:33:49 +0100 Subject: [PATCH 015/325] Make reservationType optional in getTripReservationIcon function --- src/libs/TripReservationUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/TripReservationUtils.ts b/src/libs/TripReservationUtils.ts index f2ce5113af81..9fd850ab161b 100644 --- a/src/libs/TripReservationUtils.ts +++ b/src/libs/TripReservationUtils.ts @@ -50,7 +50,7 @@ Onyx.connect({ }, }); -function getTripReservationIcon(reservationType: ReservationType): IconAsset { +function getTripReservationIcon(reservationType?: ReservationType): IconAsset { switch (reservationType) { case CONST.RESERVATION_TYPE.FLIGHT: return Expensicons.Plane; From d5dd2afc853a01393f909472368798583fcc06f4 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 25 Nov 2024 17:33:56 +0100 Subject: [PATCH 016/325] Add arrivalGate and cityName fields to Reservation and ReservationTimeDetails types --- src/types/onyx/Transaction.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 4c7e4facd94d..0183286510f6 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -236,6 +236,12 @@ type Reservation = { /** Payment type of the reservation */ paymentType?: string; + + /** Arrival gate details */ + arrivalGate?: { + /** Arrival terminal number */ + terminal: string; + }; }; /** Model of trip reservation time details */ @@ -257,6 +263,9 @@ type ReservationTimeDetails = { /** Timezone offset */ timezoneOffset?: string; + + /** City name */ + cityName?: string; }; /** Model of airline company details */ From 6c89f5a3f21c2723f18b6b1cab1fc664275c441b Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 25 Nov 2024 17:34:13 +0100 Subject: [PATCH 017/325] Enhance getFormattedTransportDate function to support shorter date format --- src/libs/DateUtils.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index 2cab87639d2f..11482aec46b5 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -798,15 +798,19 @@ function getFormattedReservationRangeDate(date1: Date, date2: Date): string { /** * Returns a formatted date of departure. * Dates are formatted as follows: - * 1. When the date refers to the current day: Departs on Sunday, Mar 17 at 8:00 - * 2. When the date refers not to the current day: Departs on Wednesday, Mar 17, 2023 at 8:00 + * 1. When the date refers to the current year: Departs on Sunday, Mar 17 at 8:00. When shorter is true, the output is: Mar 17, 8:00 AM + * 2. When the date refers not to the current year: Departs on Wednesday, Mar 17, 2023 at 8:00. When shorter is true, the output is: Mar 17, 2023 8:00 AM */ -function getFormattedTransportDate(date: Date): string { +function getFormattedTransportDate(date: Date, shorter = false): string { const {translateLocal} = Localize; if (isThisYear(date)) { - return `${translateLocal('travel.departs')} ${format(date, 'EEEE, MMM d')} ${translateLocal('common.conjunctionAt')} ${format(date, 'HH:MM')}`; + return shorter + ? format(date, 'MMM d, h:mm a') + : `${translateLocal('travel.departs')} ${format(date, 'EEEE, MMM d')} ${translateLocal('common.conjunctionAt')} ${format(date, 'HH:MM')}`; } - return `${translateLocal('travel.departs')} ${format(date, 'EEEE, MMM d, yyyy')} ${translateLocal('common.conjunctionAt')} ${format(date, 'HH:MM')}`; + return shorter + ? format(date, 'MMM d, yyyy, h:mm a') + : `${translateLocal('travel.departs')} ${format(date, 'EEEE, MMM d, yyyy')} ${translateLocal('common.conjunctionAt')} ${format(date, 'HH:MM')}`; } function doesDateBelongToAPastYear(date: string): boolean { From b47f8a2f28ff5bb59b97ce18a0308976256e10e6 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 25 Nov 2024 17:34:21 +0100 Subject: [PATCH 018/325] Add FlightTripDetails component to display flight reservation information --- src/pages/Travel/FlightTripDetails.tsx | 132 +++++++++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 src/pages/Travel/FlightTripDetails.tsx diff --git a/src/pages/Travel/FlightTripDetails.tsx b/src/pages/Travel/FlightTripDetails.tsx new file mode 100644 index 000000000000..6f32aeed0b42 --- /dev/null +++ b/src/pages/Travel/FlightTripDetails.tsx @@ -0,0 +1,132 @@ +import React from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import * as Expensicons from '@components/Icon/Expensicons'; +import MenuItem from '@components/MenuItem'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import DateUtils from '@libs/DateUtils'; +import * as TripReservationUtils from '@libs/TripReservationUtils'; +import CONST from '@src/CONST'; +import type {PersonalDetails, Transaction} from '@src/types/onyx'; + +type FlightTripDetailsProps = { + transaction: OnyxEntry; + personalDetails: OnyxEntry; +}; + +function FlightTripDetails({transaction, personalDetails}: FlightTripDetailsProps) { + const styles = useThemeStyles(); + const theme = useTheme(); + const StyleUtils = useStyleUtils(); + const {translate} = useLocalize(); + + return ( + <> + + + + {transaction?.receipt?.reservationList?.map((reservation) => { + const reservationIcon = TripReservationUtils.getTripReservationIcon(reservation.type); + const startDate = DateUtils.getFormattedTransportDate(new Date(reservation.start.date), true); + const endDate = DateUtils.getFormattedTransportDate(new Date(reservation.end.date), true); + + return ( + <> + {translate('travel.flight')} + + {reservation.start.cityName} ({reservation.start.shortName}) {translate('common.conjunctionTo')} {reservation.end.cityName} ({reservation.end.shortName}) + + + + + + + + {!!reservation.route?.number && ( + + + + )} + {!!reservation.route?.class && ( + + + + )} + {!!reservation.confirmations?.at(0)?.value && ( + + + + )} + + + ); + })} + + ); +} + +FlightTripDetails.displayName = 'FlightTripDetails'; + +export default FlightTripDetails; From aeddf58eb767159b5184e6de188c4aefd9b3e152 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Mon, 25 Nov 2024 17:34:29 +0100 Subject: [PATCH 019/325] Integrate FlightTripDetails component into TripDetails screen and wrap content in ScrollView --- src/pages/Travel/TripDetails.tsx | 60 ++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/src/pages/Travel/TripDetails.tsx b/src/pages/Travel/TripDetails.tsx index dab130bde1aa..6c924d0957b6 100644 --- a/src/pages/Travel/TripDetails.tsx +++ b/src/pages/Travel/TripDetails.tsx @@ -7,21 +7,21 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; import type {TravelNavigatorParamList} from '@libs/Navigation/types'; import * as ReportUtils from '@libs/ReportUtils'; import * as Link from '@userActions/Link'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; +import FlightTripDetails from './FlightTripDetails'; type TripDetailsProps = StackScreenProps; function TripDetails({route}: TripDetailsProps) { - const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); const {canUseSpotnanaTravel} = usePermissions(); @@ -30,11 +30,11 @@ function TripDetails({route}: TripDetailsProps) { const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${route.params.transactionID}`); const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`); const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID ?? '-1'}`); - const tripID = ReportUtils.getTripIDFromTransactionParentReportID(parentReport?.parentReportID); - console.log(`transaction = `, transaction); - console.log(`report = `, report); - console.log(`parentReport = `, parentReport); + const tripID = ReportUtils.getTripIDFromTransactionParentReportID(parentReport?.parentReportID); + const accountID = Object.keys(report?.participants ?? {}).at(0) ?? '-1'; + const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: (data) => data?.[accountID]}); + const reservationType = transaction?.receipt?.reservationList?.at(0)?.type; return ( - { - Link.openTravelDotLink(activePolicyID, CONST.TRIP_ID_PATH(tripID)); - }} - /> - { - Link.openTravelDotLink(activePolicyID, CONST.TRIP_ID_PATH(tripID)); - }} - /> + + {reservationType === 'flight' && ( + + )} + { + Link.openTravelDotLink(activePolicyID, CONST.TRIP_ID_PATH(tripID)); + }} + /> + { + Link.openTravelDotLink(activePolicyID, CONST.TRIP_ID_PATH(tripID)); + }} + /> + ); From ae395901e50f5bf0ba1564941d1aab95a4e4d9d5 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Sun, 24 Nov 2024 20:39:28 +0100 Subject: [PATCH 020/325] Final touches --- src/components/ApprovalWorkflowSection.tsx | 34 +++++++++++++++++++--- src/components/AttachmentModal.tsx | 4 +-- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/components/ApprovalWorkflowSection.tsx b/src/components/ApprovalWorkflowSection.tsx index fd28595e7436..de6626c5bd5f 100644 --- a/src/components/ApprovalWorkflowSection.tsx +++ b/src/components/ApprovalWorkflowSection.tsx @@ -1,4 +1,5 @@ -import React, {useCallback, useMemo} from 'react'; +/* eslint-disable react/function-component-definition */ +import React, {useCallback, useMemo, useRef} from 'react'; import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -20,11 +21,36 @@ type ApprovalWorkflowSectionProps = { onPress: () => void; }; -function ApprovalWorkflowSection({approvalWorkflow, onPress}: ApprovalWorkflowSectionProps) { +const Test = () => { const styles = useThemeStyles(); const theme = useTheme(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); const {translate, toLocaleOrdinal} = useLocalize(); + + const approverTitle = useCallback( + (index: number) => + approvalWorkflow.approvers.length > 1 ? `${toLocaleOrdinal(index + 1, true)} ${translate('workflowsPage.approver').toLowerCase()}` : `${translate('workflowsPage.approver')}`, + [approvalWorkflow.approvers.length, toLocaleOrdinal, translate], + ); + + const members = useMemo(() => { + if (approvalWorkflow.isDefault) { + return translate('workspace.common.everyone'); + } + + return OptionsListUtils.sortAlphabetically(approvalWorkflow.members, 'displayName') + .map((m) => m.displayName) + .join(', '); + }, [approvalWorkflow.isDefault, translate]); + + return ; +}; + +function ApprovalWorkflowSection({approvalWorkflow, onPress}: ApprovalWorkflowSectionProps) { + const styles = useThemeStyles(); + const theme = useTheme(); const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {translate, toLocaleOrdinal} = useLocalize(); const approverTitle = useCallback( (index: number) => @@ -40,7 +66,7 @@ function ApprovalWorkflowSection({approvalWorkflow, onPress}: ApprovalWorkflowSe return OptionsListUtils.sortAlphabetically(approvalWorkflow.members, 'displayName') .map((m) => m.displayName) .join(', '); - }, [approvalWorkflow.isDefault, approvalWorkflow.members, translate]); + }, [approvalWorkflow]); return ( Date: Tue, 26 Nov 2024 09:31:52 +0100 Subject: [PATCH 021/325] Fix flight page after design changes --- src/libs/DateUtils.ts | 11 +++++ src/pages/Travel/FlightTripDetails.tsx | 60 +++++++++++++++----------- src/pages/Travel/TripDetails.tsx | 3 ++ 3 files changed, 48 insertions(+), 26 deletions(-) diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index 11482aec46b5..9bf707acdf46 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -13,6 +13,7 @@ import { formatDistance, getDate, getDay, + intervalToDuration, isAfter, isBefore, isSameDay, @@ -813,6 +814,15 @@ function getFormattedTransportDate(date: Date, shorter = false): string { : `${translateLocal('travel.departs')} ${format(date, 'EEEE, MMM d, yyyy')} ${translateLocal('common.conjunctionAt')} ${format(date, 'HH:MM')}`; } +/** + * Returns a formatted layover duration in format "2h 30m". + */ +function getFormattedDurationBetweenDates(start: Date, end: Date): string { + const {days, hours, minutes} = intervalToDuration({start, end}); + + return `${days ? `${days}d ` : ''}${hours ? `${hours}h ` : ''}${minutes}m`; +} + function doesDateBelongToAPastYear(date: string): boolean { const transactionYear = new Date(date).getFullYear(); return transactionYear !== new Date().getFullYear(); @@ -897,6 +907,7 @@ const DateUtils = { isCardExpired, getDifferenceInDaysFromNow, isValidDateString, + getFormattedDurationBetweenDates, }; export default DateUtils; diff --git a/src/pages/Travel/FlightTripDetails.tsx b/src/pages/Travel/FlightTripDetails.tsx index 6f32aeed0b42..b92c4ad57cce 100644 --- a/src/pages/Travel/FlightTripDetails.tsx +++ b/src/pages/Travel/FlightTripDetails.tsx @@ -28,29 +28,26 @@ function FlightTripDetails({transaction, personalDetails}: FlightTripDetailsProp return ( <> - - {transaction?.receipt?.reservationList?.map((reservation) => { + {transaction?.receipt?.reservationList?.map((reservation, index) => { const reservationIcon = TripReservationUtils.getTripReservationIcon(reservation.type); const startDate = DateUtils.getFormattedTransportDate(new Date(reservation.start.date), true); const endDate = DateUtils.getFormattedTransportDate(new Date(reservation.end.date), true); + const nextFlightStartDate = transaction.receipt?.reservationList?.at(index + 1)?.start.date; + const layover = nextFlightStartDate && DateUtils.getFormattedDurationBetweenDates(new Date(reservation.end.date), new Date(nextFlightStartDate)); + return ( <> - {translate('travel.flight')} + {translate('travel.flight')} {reservation.start.cityName} ({reservation.start.shortName}) {translate('common.conjunctionTo')} {reservation.end.cityName} ({reservation.end.shortName}) @@ -72,26 +69,18 @@ function FlightTripDetails({transaction, personalDetails}: FlightTripDetailsProp description={translate('travel.flightDetails.takeOff')} title={startDate} helperText={`${reservation.start.longName} (${reservation.start.shortName})${reservation.arrivalGate?.terminal ? `, ${reservation.arrivalGate?.terminal}` : ''}`} - helperTextStyle={styles.mtn2} + helperTextStyle={[styles.pb3, styles.mtn2]} interactive={false} /> - - - + + {!!reservation.route?.number && ( )} + + {!!layover && ( + <> + + + + )} ); })} diff --git a/src/pages/Travel/TripDetails.tsx b/src/pages/Travel/TripDetails.tsx index 6c924d0957b6..5eaee8d60dc4 100644 --- a/src/pages/Travel/TripDetails.tsx +++ b/src/pages/Travel/TripDetails.tsx @@ -11,6 +11,7 @@ import ScrollView from '@components/ScrollView'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; import type {TravelNavigatorParamList} from '@libs/Navigation/types'; import * as ReportUtils from '@libs/ReportUtils'; import * as Link from '@userActions/Link'; @@ -23,6 +24,7 @@ type TripDetailsProps = StackScreenProps { Link.openTravelDotLink(activePolicyID, CONST.TRIP_ID_PATH(tripID)); }} + wrapperStyle={styles.mt3} /> Date: Tue, 26 Nov 2024 09:33:48 +0100 Subject: [PATCH 022/325] Change TripDetails into TripDetailsPage --- .../AppNavigator/ModalStackNavigators/index.tsx | 2 +- .../Travel/{TripDetails.tsx => TripDetailsPage.tsx} | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) rename src/pages/Travel/{TripDetails.tsx => TripDetailsPage.tsx} (92%) diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index beb030ccb09d..543b0b374b8d 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -105,7 +105,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator({ [SCREENS.TRAVEL.MY_TRIPS]: () => require('../../../../pages/Travel/MyTripsPage').default, [SCREENS.TRAVEL.TCS]: () => require('../../../../pages/Travel/TravelTerms').default, - [SCREENS.TRAVEL.TRIP_DETAILS]: () => require('../../../../pages/Travel/TripDetails').default, + [SCREENS.TRAVEL.TRIP_DETAILS]: () => require('../../../../pages/Travel/TripDetailsPage').default, }); const SplitDetailsModalStackNavigator = createModalStackNavigator({ diff --git a/src/pages/Travel/TripDetails.tsx b/src/pages/Travel/TripDetailsPage.tsx similarity index 92% rename from src/pages/Travel/TripDetails.tsx rename to src/pages/Travel/TripDetailsPage.tsx index 5eaee8d60dc4..0f35bc0c36e1 100644 --- a/src/pages/Travel/TripDetails.tsx +++ b/src/pages/Travel/TripDetailsPage.tsx @@ -20,9 +20,9 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import FlightTripDetails from './FlightTripDetails'; -type TripDetailsProps = StackScreenProps; +type TripDetailsPageProps = StackScreenProps; -function TripDetails({route}: TripDetailsProps) { +function TripDetailsPage({route}: TripDetailsPageProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -43,7 +43,7 @@ function TripDetails({route}: TripDetailsProps) { includeSafeAreaPaddingBottom={false} shouldEnablePickerAvoiding={false} shouldEnableMaxHeight - testID={TripDetails.displayName} + testID={TripDetailsPage.displayName} shouldShowOfflineIndicatorInWideScreen > Date: Tue, 26 Nov 2024 10:16:35 +0100 Subject: [PATCH 023/325] Create HotelTripDetails --- src/pages/Travel/HotelTripDetails.tsx | 88 +++++++++++++++++++++++++++ src/pages/Travel/TripDetailsPage.tsx | 7 +++ 2 files changed, 95 insertions(+) create mode 100644 src/pages/Travel/HotelTripDetails.tsx diff --git a/src/pages/Travel/HotelTripDetails.tsx b/src/pages/Travel/HotelTripDetails.tsx new file mode 100644 index 000000000000..f0f8ec7398cc --- /dev/null +++ b/src/pages/Travel/HotelTripDetails.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import * as Expensicons from '@components/Icon/Expensicons'; +import MenuItem from '@components/MenuItem'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import DateUtils from '@libs/DateUtils'; +import * as TripReservationUtils from '@libs/TripReservationUtils'; +import CONST from '@src/CONST'; +import type {PersonalDetails, Transaction} from '@src/types/onyx'; + +type HotelTripDetailsProps = { + transaction: OnyxEntry; + personalDetails: OnyxEntry; +}; + +function HotelTripDetails({transaction, personalDetails}: HotelTripDetailsProps) { + const styles = useThemeStyles(); + const theme = useTheme(); + const StyleUtils = useStyleUtils(); + const {translate} = useLocalize(); + + const hotelReservation = transaction?.receipt?.reservationList?.at(0); + + if (!transaction || !hotelReservation) { + return null; + } + const reservationIcon = TripReservationUtils.getTripReservationIcon(hotelReservation.type); + const checkInDate = DateUtils.getFormattedTransportDate(new Date(hotelReservation.start.date), true); + const checkOutDate = DateUtils.getFormattedTransportDate(new Date(hotelReservation.end.date), true); + + return ( + <> + + {translate('travel.hotel')} + {hotelReservation.start.longName} + + + + {!!hotelReservation.confirmations?.at(0)?.value && ( + + + + )} + + ); +} + +HotelTripDetails.displayName = 'FlightTripDetails'; + +export default HotelTripDetails; diff --git a/src/pages/Travel/TripDetailsPage.tsx b/src/pages/Travel/TripDetailsPage.tsx index 0f35bc0c36e1..590ab2949ea5 100644 --- a/src/pages/Travel/TripDetailsPage.tsx +++ b/src/pages/Travel/TripDetailsPage.tsx @@ -19,6 +19,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import FlightTripDetails from './FlightTripDetails'; +import HotelTripDetails from './HotelTripDetails'; type TripDetailsPageProps = StackScreenProps; @@ -61,6 +62,12 @@ function TripDetailsPage({route}: TripDetailsPageProps) { personalDetails={personalDetails} /> )} + {reservationType === 'hotel' && ( + + )} Date: Tue, 26 Nov 2024 10:16:43 +0100 Subject: [PATCH 024/325] Add missing translation --- src/languages/en.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index e525ac717729..81d2a9dfc5c9 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2432,6 +2432,7 @@ const translations = { }, hotel: 'Hotel', hotelDetails: { + guest: 'Guest', checkIn: 'Check-in', checkOut: 'Check-out', roomType: 'Room type', From 2eb1bb96d8d633b605c75a0d9e059cac1ff4b4f8 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 26 Nov 2024 11:48:40 +0100 Subject: [PATCH 025/325] Revert "Final touches" This reverts commit 04d94db6fc38eb5cd30e85373c3ac58ca28ebc26. --- src/components/ApprovalWorkflowSection.tsx | 34 +++------------------- src/components/AttachmentModal.tsx | 4 ++- 2 files changed, 7 insertions(+), 31 deletions(-) diff --git a/src/components/ApprovalWorkflowSection.tsx b/src/components/ApprovalWorkflowSection.tsx index de6626c5bd5f..fd28595e7436 100644 --- a/src/components/ApprovalWorkflowSection.tsx +++ b/src/components/ApprovalWorkflowSection.tsx @@ -1,5 +1,4 @@ -/* eslint-disable react/function-component-definition */ -import React, {useCallback, useMemo, useRef} from 'react'; +import React, {useCallback, useMemo} from 'react'; import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -21,36 +20,11 @@ type ApprovalWorkflowSectionProps = { onPress: () => void; }; -const Test = () => { - const styles = useThemeStyles(); - const theme = useTheme(); - const {shouldUseNarrowLayout} = useResponsiveLayout(); - const {translate, toLocaleOrdinal} = useLocalize(); - - const approverTitle = useCallback( - (index: number) => - approvalWorkflow.approvers.length > 1 ? `${toLocaleOrdinal(index + 1, true)} ${translate('workflowsPage.approver').toLowerCase()}` : `${translate('workflowsPage.approver')}`, - [approvalWorkflow.approvers.length, toLocaleOrdinal, translate], - ); - - const members = useMemo(() => { - if (approvalWorkflow.isDefault) { - return translate('workspace.common.everyone'); - } - - return OptionsListUtils.sortAlphabetically(approvalWorkflow.members, 'displayName') - .map((m) => m.displayName) - .join(', '); - }, [approvalWorkflow.isDefault, translate]); - - return ; -}; - function ApprovalWorkflowSection({approvalWorkflow, onPress}: ApprovalWorkflowSectionProps) { const styles = useThemeStyles(); const theme = useTheme(); - const {shouldUseNarrowLayout} = useResponsiveLayout(); const {translate, toLocaleOrdinal} = useLocalize(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); const approverTitle = useCallback( (index: number) => @@ -66,7 +40,7 @@ function ApprovalWorkflowSection({approvalWorkflow, onPress}: ApprovalWorkflowSe return OptionsListUtils.sortAlphabetically(approvalWorkflow.members, 'displayName') .map((m) => m.displayName) .join(', '); - }, [approvalWorkflow]); + }, [approvalWorkflow.isDefault, approvalWorkflow.members, translate]); return ( Date: Tue, 26 Nov 2024 11:52:58 +0100 Subject: [PATCH 026/325] Remove flight from hotel page --- src/pages/Travel/HotelTripDetails.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/Travel/HotelTripDetails.tsx b/src/pages/Travel/HotelTripDetails.tsx index f0f8ec7398cc..b77b3530fca9 100644 --- a/src/pages/Travel/HotelTripDetails.tsx +++ b/src/pages/Travel/HotelTripDetails.tsx @@ -73,7 +73,7 @@ function HotelTripDetails({transaction, personalDetails}: HotelTripDetailsProps) {!!hotelReservation.confirmations?.at(0)?.value && ( @@ -83,6 +83,6 @@ function HotelTripDetails({transaction, personalDetails}: HotelTripDetailsProps) ); } -HotelTripDetails.displayName = 'FlightTripDetails'; +HotelTripDetails.displayName = 'HotelTripDetails'; export default HotelTripDetails; From a4a0cded4bec256a13dd85c03ffdbf176319e618 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 26 Nov 2024 15:42:19 +0100 Subject: [PATCH 027/325] Add a blank line for improved readability in HotelTripDetails component --- src/pages/Travel/HotelTripDetails.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/Travel/HotelTripDetails.tsx b/src/pages/Travel/HotelTripDetails.tsx index b77b3530fca9..32f0a6836828 100644 --- a/src/pages/Travel/HotelTripDetails.tsx +++ b/src/pages/Travel/HotelTripDetails.tsx @@ -30,6 +30,7 @@ function HotelTripDetails({transaction, personalDetails}: HotelTripDetailsProps) if (!transaction || !hotelReservation) { return null; } + const reservationIcon = TripReservationUtils.getTripReservationIcon(hotelReservation.type); const checkInDate = DateUtils.getFormattedTransportDate(new Date(hotelReservation.start.date), true); const checkOutDate = DateUtils.getFormattedTransportDate(new Date(hotelReservation.end.date), true); From 30e27d1c2ba817f810c4f2e4a2b8c57a1346eb48 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 26 Nov 2024 15:42:26 +0100 Subject: [PATCH 028/325] Add CarTripDetails component for displaying car rental information --- src/pages/Travel/CarTripDetails.tsx | 103 ++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 src/pages/Travel/CarTripDetails.tsx diff --git a/src/pages/Travel/CarTripDetails.tsx b/src/pages/Travel/CarTripDetails.tsx new file mode 100644 index 000000000000..25c417425a9f --- /dev/null +++ b/src/pages/Travel/CarTripDetails.tsx @@ -0,0 +1,103 @@ +import React from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import * as Expensicons from '@components/Icon/Expensicons'; +import MenuItem from '@components/MenuItem'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import DateUtils from '@libs/DateUtils'; +import * as TripReservationUtils from '@libs/TripReservationUtils'; +import CONST from '@src/CONST'; +import type {PersonalDetails, Transaction} from '@src/types/onyx'; + +type CarTripDetailsProps = { + transaction: OnyxEntry; + personalDetails: OnyxEntry; +}; + +function CarTripDetails({transaction, personalDetails}: CarTripDetailsProps) { + const styles = useThemeStyles(); + const theme = useTheme(); + const StyleUtils = useStyleUtils(); + const {translate} = useLocalize(); + + const carReservation = transaction?.receipt?.reservationList?.at(0); + + if (!transaction || !carReservation) { + return null; + } + + const reservationIcon = TripReservationUtils.getTripReservationIcon(carReservation.type); + const checkInDate = DateUtils.getFormattedTransportDate(new Date(carReservation.start.date), true); + const checkOutDate = DateUtils.getFormattedTransportDate(new Date(carReservation.end.date), true); + + return ( + <> + + {translate('travel.carDetails.rentalCar')} + {carReservation.vendor} + + + + + {!!carReservation.carInfo?.name && ( + + + + )} + {!!carReservation.reservationID && ( + + + + )} + + ); +} + +CarTripDetails.displayName = 'FlightTripDetails'; + +export default CarTripDetails; From a8b78f875075575e7400e32e8137c95bf5b1957b Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 26 Nov 2024 15:42:37 +0100 Subject: [PATCH 029/325] Refactor getReservationsFromTripTransactions to return structured reservation data and sort by start date --- src/libs/TripReservationUtils.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/libs/TripReservationUtils.ts b/src/libs/TripReservationUtils.ts index 9fd850ab161b..a8714122da97 100644 --- a/src/libs/TripReservationUtils.ts +++ b/src/libs/TripReservationUtils.ts @@ -63,11 +63,19 @@ function getTripReservationIcon(reservationType?: ReservationType): IconAsset { } } -function getReservationsFromTripTransactions(transactions: Transaction[]): Reservation[] { +type ReservationData = {reservation: Reservation; transactionID: string; reportID: string}; + +function getReservationsFromTripTransactions(transactions: Transaction[]): ReservationData[] { return transactions - .map((item) => item?.receipt?.reservationList ?? []) - .filter((item) => item.length > 0) - .flat(); + .flatMap( + (item) => + item?.receipt?.reservationList?.map((reservation) => ({ + reservation, + transactionID: item.transactionID, + reportID: item.reportID, + })) ?? [], + ) + .sort((a, b) => new Date(a.reservation.start.date).getTime() - new Date(b.reservation.start.date).getTime()); } function getTripEReceiptIcon(transaction?: Transaction): IconAsset | undefined { @@ -115,3 +123,4 @@ function bookATrip(translate: LocaleContextProps['translate'], setCtaErrorMessag }); } export {getTripReservationIcon, getReservationsFromTripTransactions, getTripEReceiptIcon, bookATrip}; +export type {ReservationData}; From f4f5501fe0c7afb0d4ee4c819643c40e9ce2ae79 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 26 Nov 2024 15:42:47 +0100 Subject: [PATCH 030/325] Add translation for rental car --- src/languages/en.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/languages/en.ts b/src/languages/en.ts index 81d2a9dfc5c9..e1b932a3852a 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2441,6 +2441,7 @@ const translations = { }, car: 'Car', carDetails: { + rentalCar: 'Rental car', pickUp: 'Pick-up', dropOff: 'Drop-off', driver: 'Driver', From d49cdce25f3839547511714ccaf716499374616d Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 26 Nov 2024 15:43:01 +0100 Subject: [PATCH 031/325] Fix navigation route in MoneyRequestView to use parentReportID --- src/components/ReportActionItem/MoneyRequestView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 6bc356dd6f15..4fb50ad39cc1 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -695,7 +695,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals title={translate('travel.viewTripDetails')} icon={Expensicons.Suitcase} onPress={() => - Navigation.navigate(ROUTES.TRAVEL_TRIP_DETAILS.getRoute(report?.reportID ?? '-1', transaction?.transactionID ?? '-1', Navigation.getReportRHPActiveRoute())) + Navigation.navigate(ROUTES.TRAVEL_TRIP_DETAILS.getRoute(report?.parentReportID ?? '-1', transaction?.transactionID ?? '-1', Navigation.getReportRHPActiveRoute())) } /> )} From 092e430260226f367a28150edb89477e8cca435c Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 26 Nov 2024 15:43:29 +0100 Subject: [PATCH 032/325] Refactor TripDetailsView and TripRoomPreview to use structured reservation data and improve navigation handling --- .../ReportActionItem/TripDetailsView.tsx | 25 +++++++++++---- .../ReportActionItem/TripRoomPreview.tsx | 32 +++++++------------ src/libs/ReportUtils.ts | 6 ++-- 3 files changed, 33 insertions(+), 30 deletions(-) diff --git a/src/components/ReportActionItem/TripDetailsView.tsx b/src/components/ReportActionItem/TripDetailsView.tsx index 32cbc5dd853e..0043232d1119 100644 --- a/src/components/ReportActionItem/TripDetailsView.tsx +++ b/src/components/ReportActionItem/TripDetailsView.tsx @@ -11,11 +11,13 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; +import Navigation from '@libs/Navigation/Navigation'; import variables from '@styles/variables'; import * as Expensicons from '@src/components/Icon/Expensicons'; import CONST from '@src/CONST'; import * as ReportUtils from '@src/libs/ReportUtils'; import * as TripReservationUtils from '@src/libs/TripReservationUtils'; +import ROUTES from '@src/ROUTES'; import type {Reservation, ReservationTimeDetails} from '@src/types/onyx/Transaction'; type TripDetailsViewProps = { @@ -28,9 +30,11 @@ type TripDetailsViewProps = { type ReservationViewProps = { reservation: Reservation; + transactionID: string; + reportID: string; }; -function ReservationView({reservation}: ReservationViewProps) { +function ReservationView({reservation, transactionID, reportID}: ReservationViewProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -129,6 +133,7 @@ function ReservationView({reservation}: ReservationViewProps) { iconWidth={20} iconStyles={[StyleUtils.getTripReservationIconContainer(false), styles.mr3]} secondaryIconFill={theme.icon} + onPress={() => Navigation.navigate(ROUTES.TRAVEL_TRIP_DETAILS.getRoute(reportID, transactionID, Navigation.getReportRHPActiveRoute()))} /> ); } @@ -138,7 +143,7 @@ function TripDetailsView({tripRoomReportID, shouldShowHorizontalRule}: TripDetai const {translate} = useLocalize(); const tripTransactions = ReportUtils.getTripTransactions(tripRoomReportID); - const reservations: Reservation[] = TripReservationUtils.getReservationsFromTripTransactions(tripTransactions); + const reservationsData: TripReservationUtils.ReservationData[] = TripReservationUtils.getReservationsFromTripTransactions(tripTransactions); return ( @@ -153,11 +158,17 @@ function TripDetailsView({tripRoomReportID, shouldShowHorizontalRule}: TripDetai <> - {reservations.map((reservation) => ( - - - - ))} + {reservationsData.map(({reservation, transactionID, reportID}) => { + return ( + + + + ); + })} ; +const renderItem = ({item}: {item: TripReservationUtils.ReservationData}) => ; function TripRoomPreview({action, chatReportID, containerStyles, contextMenuAnchor, isHovered = false, checkIfContextMenuActive = () => {}}: TripRoomPreviewProps) { const styles = useThemeStyles(); @@ -112,31 +111,22 @@ function TripRoomPreview({action, chatReportID, containerStyles, contextMenuAnch const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`); const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReport?.iouReportID}`); - const tripTransactions = ReportUtils.getTripTransactions(chatReport?.iouReportID, 'reportID'); - const reservations: Reservation[] = TripReservationUtils.getReservationsFromTripTransactions(tripTransactions); + const tripTransactions = ReportUtils.getTripTransactions(chatReport?.reportID); + const reservationsData: TripReservationUtils.ReservationData[] = TripReservationUtils.getReservationsFromTripTransactions(tripTransactions); const dateInfo = chatReport?.tripData ? DateUtils.getFormattedDateRange(new Date(chatReport.tripData.startDate), new Date(chatReport.tripData.endDate)) : ''; const {totalDisplaySpend} = ReportUtils.getMoneyRequestSpendBreakdown(chatReport); + const currency = iouReport?.currency ?? chatReport?.currency; const displayAmount = useMemo(() => { if (totalDisplaySpend) { - return CurrencyUtils.convertToDisplayString(totalDisplaySpend, iouReport?.currency); + return CurrencyUtils.convertToDisplayString(totalDisplaySpend, currency); } - // If iouReport is not available, get amount from the action message (Ex: "Domain20821's Workspace owes $33.00" or "paid ₫60" or "paid -₫60 elsewhere") - let displayAmountValue = ''; - const actionMessage = getReportActionText(action) ?? ''; - const splits = actionMessage.split(' '); - - splits.forEach((split) => { - if (!/\d/.test(split)) { - return; - } - - displayAmountValue = split; - }); - - return displayAmountValue; - }, [action, iouReport?.currency, totalDisplaySpend]); + return CurrencyUtils.convertToDisplayString( + tripTransactions.reduce((acc, transaction) => acc + Math.abs(transaction.amount), 0), + currency, + ); + }, [currency, totalDisplaySpend, tripTransactions]); return ( diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 72f11f4a3150..534559dd8cb8 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -854,7 +854,7 @@ function isChatReport(report: OnyxEntry): boolean { return report?.type === CONST.REPORT.TYPE.CHAT; } -function isInvoiceReport(report: OnyxInputOrEntry | SearchReport): boolean { +function isInvoiceReport(report: OnyxInputOrEntry | SearchReport): report is Report { return report?.type === CONST.REPORT.TYPE.INVOICE; } @@ -1659,6 +1659,8 @@ function isMoneyRequest(reportOrID: OnyxEntry | string): boolean { /** * Checks if a report is an IOU or expense report. */ +function isMoneyRequestReport(reportOrID: OnyxInputOrEntry): reportOrID is Report; +function isMoneyRequestReport(reportOrID: SearchReport | string): boolean; function isMoneyRequestReport(reportOrID: OnyxInputOrEntry | SearchReport | string): boolean { const report = typeof reportOrID === 'string' ? ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null : reportOrID; return isIOUReport(report) || isExpenseReport(report); @@ -2837,7 +2839,7 @@ function hasNonReimbursableTransactions(iouReportID: string | undefined): boolea function getMoneyRequestSpendBreakdown(report: OnyxInputOrEntry, allReportsDict?: OnyxCollection): SpendBreakdown { const allAvailableReports = allReportsDict ?? ReportConnection.getAllReports(); - let moneyRequestReport; + let moneyRequestReport: OnyxEntry; if (isMoneyRequestReport(report) || isInvoiceReport(report)) { moneyRequestReport = report; } From 6c59ca4a4208e2d946c75be3357c551ecb4fe5a9 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 26 Nov 2024 15:43:39 +0100 Subject: [PATCH 033/325] Add CarTripDetails component to TripDetailsPage for displaying car rental information --- src/pages/Travel/TripDetailsPage.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pages/Travel/TripDetailsPage.tsx b/src/pages/Travel/TripDetailsPage.tsx index 590ab2949ea5..971128101486 100644 --- a/src/pages/Travel/TripDetailsPage.tsx +++ b/src/pages/Travel/TripDetailsPage.tsx @@ -18,6 +18,7 @@ import * as Link from '@userActions/Link'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; +import CarTripDetails from './CarTripDetails'; import FlightTripDetails from './FlightTripDetails'; import HotelTripDetails from './HotelTripDetails'; @@ -32,9 +33,8 @@ function TripDetailsPage({route}: TripDetailsPageProps) { const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${route.params.transactionID}`); const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${route.params.reportID}`); - const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID ?? '-1'}`); - const tripID = ReportUtils.getTripIDFromTransactionParentReportID(parentReport?.parentReportID); + const tripID = ReportUtils.getTripIDFromTransactionParentReportID(report?.parentReportID); const accountID = Object.keys(report?.participants ?? {}).at(0) ?? '-1'; const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: (data) => data?.[accountID]}); const reservationType = transaction?.receipt?.reservationList?.at(0)?.type; @@ -68,6 +68,12 @@ function TripDetailsPage({route}: TripDetailsPageProps) { personalDetails={personalDetails} /> )} + {reservationType === 'car' && ( + + )} Date: Wed, 27 Nov 2024 18:29:42 +0100 Subject: [PATCH 034/325] Add Trip Summary screen --- src/ROUTES.ts | 7 +- src/SCREENS.ts | 1 + .../ReportActionItem/TripDetailsView.tsx | 9 ++- src/pages/Travel/TripSummary.tsx | 65 +++++++++++++++++++ 4 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 src/pages/Travel/TripSummary.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 920e328ebfff..2be153e4dad3 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1343,10 +1343,15 @@ const ROUTES = { TRAVEL_MY_TRIPS: 'travel', TRAVEL_TCS: 'travel/terms', TRACK_TRAINING_MODAL: 'track-training', - TRAVEL_TRIP_DETAILS: { + TRAVEL_TRIP_SUMMARY: { route: 'r/:reportID/trip/:transactionID', getRoute: (reportID: string, transactionID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/trip/${transactionID}`, backTo), }, + TRAVEL_TRIP_DETAILS: { + route: 'r/:reportID/trip/:transactionID/:reservationIndex', + getRoute: (reportID: string, transactionID: string, reservationIndex: number, backTo?: string) => + getUrlWithBackToParam(`r/${reportID}/trip/${transactionID}/${reservationIndex}`, backTo), + }, ONBOARDING_ROOT: { route: 'onboarding', getRoute: (backTo?: string) => getUrlWithBackToParam(`onboarding`, backTo), diff --git a/src/SCREENS.ts b/src/SCREENS.ts index f6a53da3f199..eeb805d0ca36 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -27,6 +27,7 @@ const SCREENS = { TRAVEL: { MY_TRIPS: 'Travel_MyTrips', TCS: 'Travel_TCS', + TRIP_SUMMARY: 'Travel_TripSummary', TRIP_DETAILS: 'Trip_Details', }, SEARCH: { diff --git a/src/components/ReportActionItem/TripDetailsView.tsx b/src/components/ReportActionItem/TripDetailsView.tsx index 0043232d1119..dfc282b230e4 100644 --- a/src/components/ReportActionItem/TripDetailsView.tsx +++ b/src/components/ReportActionItem/TripDetailsView.tsx @@ -32,9 +32,10 @@ type ReservationViewProps = { reservation: Reservation; transactionID: string; reportID: string; + reservationIndex: number; }; -function ReservationView({reservation, transactionID, reportID}: ReservationViewProps) { +function ReservationView({reservation, transactionID, reportID, reservationIndex}: ReservationViewProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -133,7 +134,7 @@ function ReservationView({reservation, transactionID, reportID}: ReservationView iconWidth={20} iconStyles={[StyleUtils.getTripReservationIconContainer(false), styles.mr3]} secondaryIconFill={theme.icon} - onPress={() => Navigation.navigate(ROUTES.TRAVEL_TRIP_DETAILS.getRoute(reportID, transactionID, Navigation.getReportRHPActiveRoute()))} + onPress={() => Navigation.navigate(ROUTES.TRAVEL_TRIP_DETAILS.getRoute(reportID, transactionID, reservationIndex, Navigation.getReportRHPActiveRoute()))} /> ); } @@ -158,13 +159,14 @@ function TripDetailsView({tripRoomReportID, shouldShowHorizontalRule}: TripDetai <> - {reservationsData.map(({reservation, transactionID, reportID}) => { + {reservationsData.map(({reservation, transactionID, reportID, reservationIndex}) => { return ( ); @@ -181,3 +183,4 @@ function TripDetailsView({tripRoomReportID, shouldShowHorizontalRule}: TripDetai TripDetailsView.displayName = 'TripDetailsView'; export default TripDetailsView; +export {ReservationView}; diff --git a/src/pages/Travel/TripSummary.tsx b/src/pages/Travel/TripSummary.tsx new file mode 100644 index 000000000000..a024d28bd6db --- /dev/null +++ b/src/pages/Travel/TripSummary.tsx @@ -0,0 +1,65 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React from 'react'; +import {NativeModules} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import {ReservationView} from '@components/ReportActionItem/TripDetailsView'; +import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; +import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; +import type {TravelNavigatorParamList} from '@libs/Navigation/types'; +import * as TripReservationUtils from '@src/libs/TripReservationUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; + +type TripDetailsPageProps = StackScreenProps; + +function TripDetailsPage({route}: TripDetailsPageProps) { + const {translate} = useLocalize(); + const {canUseSpotnanaTravel} = usePermissions(); + + const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${route.params.transactionID}`); + const reservationType = transaction?.receipt?.reservationList?.at(0)?.type; + const reservationsData: TripReservationUtils.ReservationData[] = TripReservationUtils.getReservationsFromTripTransactions(transaction ? [transaction] : []); + + return ( + + + + + {reservationsData.map(({reservation, transactionID, reportID, reservationIndex}) => { + return ( + + + + ); + })} + + + + ); +} + +TripDetailsPage.displayName = 'TripDetailsPage'; + +export default TripDetailsPage; From 9c5963e50f51d541e70f171454947e9e440c6d22 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 27 Nov 2024 18:30:02 +0100 Subject: [PATCH 035/325] Add address translation to common --- src/languages/en.ts | 3 ++- src/languages/es.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index e1b932a3852a..f745d2d3f579 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -474,6 +474,7 @@ const translations = { links: 'Links', days: 'days', rename: 'Rename', + address: 'Address', }, location: { useCurrent: 'Use current location', @@ -2441,7 +2442,7 @@ const translations = { }, car: 'Car', carDetails: { - rentalCar: 'Rental car', + rentalCar: 'Car rental', pickUp: 'Pick-up', dropOff: 'Drop-off', driver: 'Driver', diff --git a/src/languages/es.ts b/src/languages/es.ts index 623afb1ff16a..edcba33bd58a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -465,6 +465,7 @@ const translations = { sent: 'Enviado', links: 'Enlaces', days: 'días', + address: 'Dirección', }, connectionComplete: { title: 'Conexión completa', From 34553ef3aeb1a09e42257e9703f15f1b463c2cf3 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 27 Nov 2024 18:30:50 +0100 Subject: [PATCH 036/325] Add screen to navigator and fix types --- .../AppNavigator/ModalStackNavigators/index.tsx | 1 + src/libs/Navigation/types.ts | 6 ++++++ .../Travel/{TripSummary.tsx => TripSummaryPage.tsx} | 10 +++++----- 3 files changed, 12 insertions(+), 5 deletions(-) rename src/pages/Travel/{TripSummary.tsx => TripSummaryPage.tsx} (90%) diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 543b0b374b8d..377013af0ada 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -105,6 +105,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator({ [SCREENS.TRAVEL.MY_TRIPS]: () => require('../../../../pages/Travel/MyTripsPage').default, [SCREENS.TRAVEL.TCS]: () => require('../../../../pages/Travel/TravelTerms').default, + [SCREENS.TRAVEL.TRIP_SUMMARY]: () => require('../../../../pages/Travel/TripSummaryPage').default, [SCREENS.TRAVEL.TRIP_DETAILS]: () => require('../../../../pages/Travel/TripDetailsPage').default, }); diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 469d20d15bbc..750542a55172 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1380,9 +1380,15 @@ type RightModalNavigatorParamList = { type TravelNavigatorParamList = { [SCREENS.TRAVEL.MY_TRIPS]: undefined; + [SCREENS.TRAVEL.TRIP_SUMMARY]: { + reportID: string; + transactionID: string; + backTo?: string; + }; [SCREENS.TRAVEL.TRIP_DETAILS]: { reportID: string; transactionID: string; + reservationIndex: number; backTo?: string; }; }; diff --git a/src/pages/Travel/TripSummary.tsx b/src/pages/Travel/TripSummaryPage.tsx similarity index 90% rename from src/pages/Travel/TripSummary.tsx rename to src/pages/Travel/TripSummaryPage.tsx index a024d28bd6db..92a9aab8e107 100644 --- a/src/pages/Travel/TripSummary.tsx +++ b/src/pages/Travel/TripSummaryPage.tsx @@ -15,9 +15,9 @@ import * as TripReservationUtils from '@src/libs/TripReservationUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; -type TripDetailsPageProps = StackScreenProps; +type TripSummaryPageProps = StackScreenProps; -function TripDetailsPage({route}: TripDetailsPageProps) { +function TripSummaryPage({route}: TripSummaryPageProps) { const {translate} = useLocalize(); const {canUseSpotnanaTravel} = usePermissions(); @@ -30,7 +30,7 @@ function TripDetailsPage({route}: TripDetailsPageProps) { includeSafeAreaPaddingBottom={false} shouldEnablePickerAvoiding={false} shouldEnableMaxHeight - testID={TripDetailsPage.displayName} + testID={TripSummaryPage.displayName} shouldShowOfflineIndicatorInWideScreen > Date: Wed, 27 Nov 2024 18:32:54 +0100 Subject: [PATCH 037/325] Navigate to summary conditionally --- .../ReportActionItem/MoneyRequestView.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 4fb50ad39cc1..56fdf582f5b7 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -694,9 +694,17 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals - Navigation.navigate(ROUTES.TRAVEL_TRIP_DETAILS.getRoute(report?.parentReportID ?? '-1', transaction?.transactionID ?? '-1', Navigation.getReportRHPActiveRoute())) - } + onPress={() => { + const reservations = transaction?.receipt?.reservationList?.length ?? 0; + if (reservations > 1) { + Navigation.navigate( + ROUTES.TRAVEL_TRIP_SUMMARY.getRoute(report?.parentReportID ?? '-1', transaction?.transactionID ?? '-1', Navigation.getReportRHPActiveRoute()), + ); + } + Navigation.navigate( + ROUTES.TRAVEL_TRIP_DETAILS.getRoute(report?.parentReportID ?? '-1', transaction?.transactionID ?? '-1', 0, Navigation.getReportRHPActiveRoute()), + ); + }} /> )} {shouldShowAttendees && ( From a1ebfe7db4c327d6d9d8ff755976fb526ab0ac1f Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 27 Nov 2024 18:33:13 +0100 Subject: [PATCH 038/325] Clean date utils around transport --- src/libs/DateUtils.ts | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index 9bf707acdf46..21c0a2b7e375 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -799,19 +799,34 @@ function getFormattedReservationRangeDate(date1: Date, date2: Date): string { /** * Returns a formatted date of departure. * Dates are formatted as follows: - * 1. When the date refers to the current year: Departs on Sunday, Mar 17 at 8:00. When shorter is true, the output is: Mar 17, 8:00 AM - * 2. When the date refers not to the current year: Departs on Wednesday, Mar 17, 2023 at 8:00. When shorter is true, the output is: Mar 17, 2023 8:00 AM + * 1. When the date refers to the current year: Departs on Sunday, Mar 17 at 8:00. + * 2. When the date refers not to the current year: Departs on Wednesday, Mar 17, 2023 at 8:00. */ -function getFormattedTransportDate(date: Date, shorter = false): string { +function getFormattedTransportDate(date: Date): string { const {translateLocal} = Localize; if (isThisYear(date)) { - return shorter - ? format(date, 'MMM d, h:mm a') - : `${translateLocal('travel.departs')} ${format(date, 'EEEE, MMM d')} ${translateLocal('common.conjunctionAt')} ${format(date, 'HH:MM')}`; + return `${translateLocal('travel.departs')} ${format(date, 'EEEE, MMM d')} ${translateLocal('common.conjunctionAt')} ${format(date, 'HH:MM')}`; } - return shorter - ? format(date, 'MMM d, yyyy, h:mm a') - : `${translateLocal('travel.departs')} ${format(date, 'EEEE, MMM d, yyyy')} ${translateLocal('common.conjunctionAt')} ${format(date, 'HH:MM')}`; + return `${translateLocal('travel.departs')} ${format(date, 'EEEE, MMM d, yyyy')} ${translateLocal('common.conjunctionAt')} ${format(date, 'HH:MM')}`; +} + +/** + * Returns a formatted flight date and hour. + * Dates are formatted as follows: + * 1. When the date refers to the current year: Wednesday, Mar 17 8:00 AM + * 2. When the date refers not to the current year: Wednesday, Mar 17, 2023 8:00 AM + */ +function getFormattedTransportDateAndHour(date: Date): {date: string; hour: string} { + if (isThisYear(date)) { + return { + date: format(date, 'EEEE, MMM d'), + hour: format(date, 'h:mm a'), + }; + } + return { + date: format(date, 'EEEE, MMM d, yyyy'), + hour: format(date, 'h:mm a'), + }; } /** @@ -903,6 +918,7 @@ const DateUtils = { getFormattedDateRange, getFormattedReservationRangeDate, getFormattedTransportDate, + getFormattedTransportDateAndHour, doesDateBelongToAPastYear, isCardExpired, getDifferenceInDaysFromNow, From aa0fb9403d8c8d67ec0dc4800e859c6fe01f9610 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 27 Nov 2024 18:33:36 +0100 Subject: [PATCH 039/325] Add reservation index to display just one flight --- src/libs/TripReservationUtils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/TripReservationUtils.ts b/src/libs/TripReservationUtils.ts index a8714122da97..6ddf0dc30f57 100644 --- a/src/libs/TripReservationUtils.ts +++ b/src/libs/TripReservationUtils.ts @@ -63,16 +63,17 @@ function getTripReservationIcon(reservationType?: ReservationType): IconAsset { } } -type ReservationData = {reservation: Reservation; transactionID: string; reportID: string}; +type ReservationData = {reservation: Reservation; transactionID: string; reportID: string; reservationIndex: number}; function getReservationsFromTripTransactions(transactions: Transaction[]): ReservationData[] { return transactions .flatMap( (item) => - item?.receipt?.reservationList?.map((reservation) => ({ + item?.receipt?.reservationList?.map((reservation, reservationIndex) => ({ reservation, transactionID: item.transactionID, reportID: item.reportID, + reservationIndex, })) ?? [], ) .sort((a, b) => new Date(a.reservation.start.date).getTime() - new Date(b.reservation.start.date).getTime()); From 1d541a1e95271c6682c74a7ad9f55c23363952f4 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 27 Nov 2024 18:33:48 +0100 Subject: [PATCH 040/325] Update config configuration --- src/libs/Navigation/linkingConfig/config.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 317c1e15e04f..2cf2f28a08e7 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1320,7 +1320,13 @@ const config: LinkingOptions['config'] = { screens: { [SCREENS.TRAVEL.MY_TRIPS]: ROUTES.TRAVEL_MY_TRIPS, [SCREENS.TRAVEL.TCS]: ROUTES.TRAVEL_TCS, - [SCREENS.TRAVEL.TRIP_DETAILS]: ROUTES.TRAVEL_TRIP_DETAILS.route, + [SCREENS.TRAVEL.TRIP_SUMMARY]: ROUTES.TRAVEL_TRIP_SUMMARY.route, + [SCREENS.TRAVEL.TRIP_DETAILS]: { + path: ROUTES.TRAVEL_TRIP_DETAILS.route, + parse: { + reservationIndex: (reservationIndex: string) => parseInt(reservationIndex, 10), + }, + }, }, }, [SCREENS.RIGHT_MODAL.SEARCH_REPORT]: { From fe182384b590466d33be942542788135551a16d1 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 27 Nov 2024 18:34:03 +0100 Subject: [PATCH 041/325] Update styles after design changes --- src/pages/Travel/CarTripDetails.tsx | 19 ++- src/pages/Travel/FlightTripDetails.tsx | 171 ++++++++++++------------- src/pages/Travel/HotelTripDetails.tsx | 24 ++-- src/pages/Travel/TripDetailsPage.tsx | 3 +- 4 files changed, 108 insertions(+), 109 deletions(-) diff --git a/src/pages/Travel/CarTripDetails.tsx b/src/pages/Travel/CarTripDetails.tsx index 25c417425a9f..ceff885c349b 100644 --- a/src/pages/Travel/CarTripDetails.tsx +++ b/src/pages/Travel/CarTripDetails.tsx @@ -4,7 +4,6 @@ import type {OnyxEntry} from 'react-native-onyx'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; -import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; @@ -32,8 +31,8 @@ function CarTripDetails({transaction, personalDetails}: CarTripDetailsProps) { } const reservationIcon = TripReservationUtils.getTripReservationIcon(carReservation.type); - const checkInDate = DateUtils.getFormattedTransportDate(new Date(carReservation.start.date), true); - const checkOutDate = DateUtils.getFormattedTransportDate(new Date(carReservation.end.date), true); + const pickUpDate = DateUtils.getFormattedTransportDateAndHour(new Date(carReservation.start.date)); + const dropOffDate = DateUtils.getFormattedTransportDateAndHour(new Date(carReservation.end.date)); return ( <> @@ -46,12 +45,10 @@ function CarTripDetails({transaction, personalDetails}: CarTripDetailsProps) { interactive={false} wrapperStyle={styles.pb3} /> - {translate('travel.carDetails.rentalCar')} - {carReservation.vendor} ; personalDetails: OnyxEntry; + reservationIndex: number; }; -function FlightTripDetails({transaction, personalDetails}: FlightTripDetailsProps) { +function FlightTripDetails({transaction, personalDetails, reservationIndex}: FlightTripDetailsProps) { const styles = useThemeStyles(); const theme = useTheme(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); + const flightReservation = transaction?.receipt?.reservationList?.at(reservationIndex); + + if (!flightReservation) { + return null; + } + + const reservationIcon = TripReservationUtils.getTripReservationIcon(flightReservation.type); + const startDate = DateUtils.getFormattedTransportDateAndHour(new Date(flightReservation.start.date)); + const endDate = DateUtils.getFormattedTransportDateAndHour(new Date(flightReservation.end.date)); + + const prevFlightStartDate = reservationIndex > 0 && transaction?.receipt?.reservationList?.at(reservationIndex - 1)?.end.date; + const layover = prevFlightStartDate && DateUtils.getFormattedDurationBetweenDates(new Date(prevFlightStartDate), new Date(flightReservation.start.date)); + return ( <> - {transaction?.receipt?.reservationList?.map((reservation, index) => { - const reservationIcon = TripReservationUtils.getTripReservationIcon(reservation.type); - const startDate = DateUtils.getFormattedTransportDate(new Date(reservation.start.date), true); - const endDate = DateUtils.getFormattedTransportDate(new Date(reservation.end.date), true); + {!!layover && ( + <> + + + + )} - const nextFlightStartDate = transaction.receipt?.reservationList?.at(index + 1)?.start.date; - const layover = nextFlightStartDate && DateUtils.getFormattedDurationBetweenDates(new Date(reservation.end.date), new Date(nextFlightStartDate)); + + + - return ( - <> - {translate('travel.flight')} - - {reservation.start.cityName} ({reservation.start.shortName}) {translate('common.conjunctionTo')} {reservation.end.cityName} ({reservation.end.shortName}) - - + {!!flightReservation.route?.number && ( + + + + )} + {!!flightReservation.route?.class && ( + + + )} + {!!flightReservation.confirmations?.at(0)?.value && ( + - - - {!!reservation.route?.number && ( - - - - )} - {!!reservation.route?.class && ( - - - - )} - {!!reservation.confirmations?.at(0)?.value && ( - - - - )} - - - {!!layover && ( - <> - - - - )} - - ); - })} + + )} + ); } diff --git a/src/pages/Travel/HotelTripDetails.tsx b/src/pages/Travel/HotelTripDetails.tsx index 32f0a6836828..4ce3fbabb7ad 100644 --- a/src/pages/Travel/HotelTripDetails.tsx +++ b/src/pages/Travel/HotelTripDetails.tsx @@ -4,7 +4,6 @@ import type {OnyxEntry} from 'react-native-onyx'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; -import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; @@ -32,8 +31,8 @@ function HotelTripDetails({transaction, personalDetails}: HotelTripDetailsProps) } const reservationIcon = TripReservationUtils.getTripReservationIcon(hotelReservation.type); - const checkInDate = DateUtils.getFormattedTransportDate(new Date(hotelReservation.start.date), true); - const checkOutDate = DateUtils.getFormattedTransportDate(new Date(hotelReservation.end.date), true); + const checkInDate = DateUtils.getFormattedTransportDateAndHour(new Date(hotelReservation.start.date)); + const checkOutDate = DateUtils.getFormattedTransportDateAndHour(new Date(hotelReservation.end.date)); return ( <> @@ -46,12 +45,10 @@ function HotelTripDetails({transaction, personalDetails}: HotelTripDetailsProps) interactive={false} wrapperStyle={styles.pb3} /> - {translate('travel.hotel')} - {hotelReservation.start.longName} + {!!hotelReservation.confirmations?.at(0)?.value && ( diff --git a/src/pages/Travel/TripDetailsPage.tsx b/src/pages/Travel/TripDetailsPage.tsx index 971128101486..96d7ec2b56ba 100644 --- a/src/pages/Travel/TripDetailsPage.tsx +++ b/src/pages/Travel/TripDetailsPage.tsx @@ -52,7 +52,7 @@ function TripDetailsPage({route}: TripDetailsPageProps) { shouldShow={!canUseSpotnanaTravel && !NativeModules.HybridAppModule} > @@ -60,6 +60,7 @@ function TripDetailsPage({route}: TripDetailsPageProps) { )} {reservationType === 'hotel' && ( From c882db81eb5712c02fb0b01a652163b651a87e2d Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 27 Nov 2024 18:52:46 +0100 Subject: [PATCH 042/325] Rename TRIP_DETAILS screen identifier to maintain consistent naming convention --- src/SCREENS.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 2efe8084b3bb..5b708e537147 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -28,7 +28,7 @@ const SCREENS = { MY_TRIPS: 'Travel_MyTrips', TCS: 'Travel_TCS', TRIP_SUMMARY: 'Travel_TripSummary', - TRIP_DETAILS: 'Trip_Details', + TRIP_DETAILS: 'Travel_TripDetails', }, SEARCH: { CENTRAL_PANE: 'Search_Central_Pane', From c20e09e6810ddc7f4979d5fc8e72d3116fdff964 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 27 Nov 2024 18:54:41 +0100 Subject: [PATCH 043/325] Fix typecheck and lint --- src/libs/ReportUtils.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 16d1d65d99f1..c647223a4715 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -860,7 +860,7 @@ function isChatReport(report: OnyxEntry): boolean { return report?.type === CONST.REPORT.TYPE.CHAT; } -function isInvoiceReport(report: OnyxInputOrEntry | SearchReport): report is Report { +function isInvoiceReport(report: OnyxInputOrEntry | SearchReport): boolean { return report?.type === CONST.REPORT.TYPE.INVOICE; } @@ -1667,8 +1667,6 @@ function isMoneyRequest(reportOrID: OnyxEntry | string): boolean { /** * Checks if a report is an IOU or expense report. */ -function isMoneyRequestReport(reportOrID: OnyxInputOrEntry): reportOrID is Report; -function isMoneyRequestReport(reportOrID: SearchReport | string): boolean; function isMoneyRequestReport(reportOrID: OnyxInputOrEntry | SearchReport | string): boolean { const report = typeof reportOrID === 'string' ? ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportOrID}`] ?? null : reportOrID; return isIOUReport(report) || isExpenseReport(report); @@ -2848,7 +2846,7 @@ function hasNonReimbursableTransactions(iouReportID: string | undefined): boolea function getMoneyRequestSpendBreakdown(report: OnyxInputOrEntry, allReportsDict?: OnyxCollection): SpendBreakdown { const allAvailableReports = allReportsDict ?? ReportConnection.getAllReports(); let moneyRequestReport: OnyxEntry; - if (isMoneyRequestReport(report) || isInvoiceReport(report)) { + if (report && (isMoneyRequestReport(report) || isInvoiceReport(report))) { moneyRequestReport = report; } if (allAvailableReports && report?.iouReportID) { From 7dcfbd0159a50dba5fd7a7a4f559f844a5d8bcb6 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 27 Nov 2024 19:01:16 +0100 Subject: [PATCH 044/325] Add Spanish translations for flight, hotel, and car details --- src/languages/es.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/languages/es.ts b/src/languages/es.ts index 1897e10ccaac..3710c3080c99 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2449,9 +2449,36 @@ const translations = { error: 'Debes aceptar los Términos y condiciones para que el viaje continúe', }, flight: 'Vuelo', + flightDetails: { + layover: 'Escala', + takeOff: 'Despegue', + landing: 'Aterrizaje', + passenger: 'Pasajero', + seat: 'Asiento', + class: 'Clase de cabina', + recordLocator: 'Localizador de la reserva', + }, hotel: 'Hotel', + hotelDetails: { + guest: 'Cliente', + checkIn: 'Entrada', + checkOut: 'Salida', + roomType: 'Tipo de habitación', + cancellation: 'Política de cancelación', + confirmation: 'Número de confirmación', + }, car: 'Auto', + carDetails: { + rentalCar: 'Coche de alquiler', + pickUp: 'Recogida', + dropOff: 'Devolución', + driver: 'Conductor', + carType: 'Tipo de coche', + cancellation: 'Política de cancelación', + confirmation: 'Número de confirmación', + }, viewTrip: 'Ver viaje', + modifyTrip: 'Modificar viaje', tripSupport: 'Soporte de Viaje', viewTripDetails: 'Ver detalles del viaje', trip: 'Viaje', From 5ba408aa59533d7f32c22e48d69bc1340b8cff2a Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Thu, 28 Nov 2024 14:52:46 +0100 Subject: [PATCH 045/325] Refactor reservation type checks to use constants --- src/pages/Travel/TripDetailsPage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/Travel/TripDetailsPage.tsx b/src/pages/Travel/TripDetailsPage.tsx index 96d7ec2b56ba..ee7328299219 100644 --- a/src/pages/Travel/TripDetailsPage.tsx +++ b/src/pages/Travel/TripDetailsPage.tsx @@ -56,20 +56,20 @@ function TripDetailsPage({route}: TripDetailsPageProps) { shouldShowBackButton /> - {reservationType === 'flight' && ( + {reservationType === CONST.RESERVATION_TYPE.FLIGHT && ( )} - {reservationType === 'hotel' && ( + {reservationType === CONST.RESERVATION_TYPE.HOTEL && ( )} - {reservationType === 'car' && ( + {reservationType === CONST.RESERVATION_TYPE.CAR && ( Date: Thu, 28 Nov 2024 15:45:44 +0100 Subject: [PATCH 046/325] Add cancellation deadline and policy to car and hotel trip details --- src/languages/en.ts | 2 ++ src/pages/Travel/CarTripDetails.tsx | 16 ++++++++++++++-- src/pages/Travel/HotelTripDetails.tsx | 14 +++++++++++++- src/types/onyx/Transaction.ts | 6 ++++++ 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 148f3463495f..753b4600e78d 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2441,6 +2441,7 @@ const translations = { checkOut: 'Check-out', roomType: 'Room type', cancellation: 'Cancellation policy', + cancellationUntil: 'Free cancellation until', confirmation: 'Confirmation number', }, car: 'Car', @@ -2451,6 +2452,7 @@ const translations = { driver: 'Driver', carType: 'Car type', cancellation: 'Cancellation policy', + cancellationUntil: 'Free cancellation until', confirmation: 'Confirmation number', }, viewTrip: 'View trip', diff --git a/src/pages/Travel/CarTripDetails.tsx b/src/pages/Travel/CarTripDetails.tsx index ceff885c349b..5e87b0f5b79f 100644 --- a/src/pages/Travel/CarTripDetails.tsx +++ b/src/pages/Travel/CarTripDetails.tsx @@ -33,6 +33,9 @@ function CarTripDetails({transaction, personalDetails}: CarTripDetailsProps) { const reservationIcon = TripReservationUtils.getTripReservationIcon(carReservation.type); const pickUpDate = DateUtils.getFormattedTransportDateAndHour(new Date(carReservation.start.date)); const dropOffDate = DateUtils.getFormattedTransportDateAndHour(new Date(carReservation.end.date)); + const cancellationText = carReservation.deadline + ? `${translate('travel.carDetails.cancellationUntil')} ${DateUtils.getFormattedTransportDateAndHour(new Date(carReservation.deadline)).date}` + : carReservation.cancellationPolicy; return ( <> @@ -74,7 +77,7 @@ function CarTripDetails({transaction, personalDetails}: CarTripDetailsProps) { /> {!!carReservation.carInfo?.name && ( - + )} + {!!cancellationText && ( + + + + )} {!!carReservation.reservationID && ( - + @@ -73,8 +76,17 @@ function HotelTripDetails({transaction, personalDetails}: HotelTripDetailsProps) title={checkOutDate.hour} interactive={false} /> + {!!cancellationText && ( + + + + )} {!!hotelReservation.confirmations?.at(0)?.value && ( - + Date: Fri, 29 Nov 2024 11:49:48 +0100 Subject: [PATCH 047/325] Fix mutation dropping --- ...x-dropping-mutations-in-transactions.patch | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 patches/react-native+0.75.2+024+fix-dropping-mutations-in-transactions.patch diff --git a/patches/react-native+0.75.2+024+fix-dropping-mutations-in-transactions.patch b/patches/react-native+0.75.2+024+fix-dropping-mutations-in-transactions.patch new file mode 100644 index 000000000000..974a0d090fb9 --- /dev/null +++ b/patches/react-native+0.75.2+024+fix-dropping-mutations-in-transactions.patch @@ -0,0 +1,67 @@ +diff --git a/node_modules/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp b/node_modules/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp +index 572fb3d..0efa1ed 100644 +--- a/node_modules/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp ++++ b/node_modules/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp +@@ -468,7 +468,7 @@ void Binding::schedulerDidFinishTransaction( + mountingTransaction->getSurfaceId(); + }); + +- if (pendingTransaction != pendingTransactions_.end()) { ++ if (pendingTransaction != pendingTransactions_.end() && pendingTransaction->canMergeWith(*mountingTransaction)) { + pendingTransaction->mergeWith(std::move(*mountingTransaction)); + } else { + pendingTransactions_.push_back(std::move(*mountingTransaction)); +diff --git a/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.cpp b/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.cpp +index d7dd1bc..d95d779 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.cpp ++++ b/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.cpp +@@ -5,6 +5,8 @@ + * LICENSE file in the root directory of this source tree. + */ + ++#include ++ + #include "MountingTransaction.h" + + namespace facebook::react { +@@ -54,4 +56,21 @@ void MountingTransaction::mergeWith(MountingTransaction&& transaction) { + telemetry_ = std::move(transaction.telemetry_); + } + ++bool MountingTransaction::canMergeWith(MountingTransaction& transaction) { ++ std::set deletedTags; ++ for (const auto& mutation : mutations_) { ++ if (mutation.type == ShadowViewMutation::Type::Delete) { ++ deletedTags.insert(mutation.oldChildShadowView.tag); ++ } ++ } ++ ++ for (const auto& mutation : transaction.getMutations()) { ++ if (mutation.type == ShadowViewMutation::Type::Create && deletedTags.contains(mutation.newChildShadowView.tag)) { ++ return false; ++ } ++ } ++ ++ return true; ++} ++ + } // namespace facebook::react +diff --git a/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.h b/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.h +index 277e9f4..38629db 100644 +--- a/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.h ++++ b/node_modules/react-native/ReactCommon/react/renderer/mounting/MountingTransaction.h +@@ -85,6 +85,14 @@ class MountingTransaction final { + */ + void mergeWith(MountingTransaction&& transaction); + ++ /* ++ * Checks whether the two transactions can be safely merged. Due to ++ * reordering of mutations during mount, the sequence of ++ * REMOVE -> DELETE | CREATE -> INSERT (2 transactions) may get changed to ++ * INSERT -> REMOVE -> DELETE and the state will diverge from there. ++ */ ++ bool canMergeWith(MountingTransaction& transaction); ++ + private: + SurfaceId surfaceId_; + Number number_; From 0389b737e12da98f81af5b2fe4cc951c9bbb936d Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 29 Nov 2024 13:51:28 +0100 Subject: [PATCH 048/325] Rename reservation deadline property and update related components to use the new name --- src/libs/DateUtils.ts | 8 ++++++-- src/pages/Travel/CarTripDetails.tsx | 4 ++-- src/pages/Travel/HotelTripDetails.tsx | 5 +++-- src/types/onyx/Transaction.ts | 2 +- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index 21c0a2b7e375..8bdbdd2d21d0 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -832,10 +832,14 @@ function getFormattedTransportDateAndHour(date: Date): {date: string; hour: stri /** * Returns a formatted layover duration in format "2h 30m". */ -function getFormattedDurationBetweenDates(start: Date, end: Date): string { +function getFormattedDurationBetweenDates(start: Date, end: Date): string | undefined { const {days, hours, minutes} = intervalToDuration({start, end}); - return `${days ? `${days}d ` : ''}${hours ? `${hours}h ` : ''}${minutes}m`; + if (days && days > 0) { + return; + } + + return `${hours ? `${hours}h ` : ''}${minutes}m`; } function doesDateBelongToAPastYear(date: string): boolean { diff --git a/src/pages/Travel/CarTripDetails.tsx b/src/pages/Travel/CarTripDetails.tsx index 5e87b0f5b79f..4ad406e9b868 100644 --- a/src/pages/Travel/CarTripDetails.tsx +++ b/src/pages/Travel/CarTripDetails.tsx @@ -33,8 +33,8 @@ function CarTripDetails({transaction, personalDetails}: CarTripDetailsProps) { const reservationIcon = TripReservationUtils.getTripReservationIcon(carReservation.type); const pickUpDate = DateUtils.getFormattedTransportDateAndHour(new Date(carReservation.start.date)); const dropOffDate = DateUtils.getFormattedTransportDateAndHour(new Date(carReservation.end.date)); - const cancellationText = carReservation.deadline - ? `${translate('travel.carDetails.cancellationUntil')} ${DateUtils.getFormattedTransportDateAndHour(new Date(carReservation.deadline)).date}` + const cancellationText = carReservation.cancellationDeadline + ? `${translate('travel.carDetails.cancellationUntil')} ${DateUtils.getFormattedTransportDateAndHour(new Date(carReservation.cancellationDeadline)).date}` : carReservation.cancellationPolicy; return ( diff --git a/src/pages/Travel/HotelTripDetails.tsx b/src/pages/Travel/HotelTripDetails.tsx index 6e3e5e8c8bfb..f0fa279fbbe2 100644 --- a/src/pages/Travel/HotelTripDetails.tsx +++ b/src/pages/Travel/HotelTripDetails.tsx @@ -30,11 +30,12 @@ function HotelTripDetails({transaction, personalDetails}: HotelTripDetailsProps) return null; } + const reservationIcon = TripReservationUtils.getTripReservationIcon(hotelReservation.type); const checkInDate = DateUtils.getFormattedTransportDateAndHour(new Date(hotelReservation.start.date)); const checkOutDate = DateUtils.getFormattedTransportDateAndHour(new Date(hotelReservation.end.date)); - const cancellationText = hotelReservation.deadline - ? `${translate('travel.hotelDetails.cancellationUntil')} ${DateUtils.getFormattedTransportDateAndHour(new Date(hotelReservation.deadline)).date}` + const cancellationText = hotelReservation.cancellationDeadline + ? `${translate('travel.hotelDetails.cancellationUntil')} ${DateUtils.getFormattedTransportDateAndHour(new Date(hotelReservation.cancellationDeadline)).date}` : hotelReservation.cancellationPolicy; return ( diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 703769dfabf7..991b5230440f 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -211,7 +211,7 @@ type Reservation = { cancellationPolicy?: string; /** In car and hotel reservations, this represents the cancellation deadline */ - deadline?: string; + cancellationDeadline?: string; /** Collection of passenger confirmations */ confirmations?: ReservationConfirmation[]; From f906103b84cadcd8612120cbe0fefb592af54afd Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 29 Nov 2024 14:30:47 +0100 Subject: [PATCH 049/325] Rename displayName for CarTripDetails component --- src/pages/Travel/CarTripDetails.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Travel/CarTripDetails.tsx b/src/pages/Travel/CarTripDetails.tsx index 4ad406e9b868..711e2b07332c 100644 --- a/src/pages/Travel/CarTripDetails.tsx +++ b/src/pages/Travel/CarTripDetails.tsx @@ -107,6 +107,6 @@ function CarTripDetails({transaction, personalDetails}: CarTripDetailsProps) { ); } -CarTripDetails.displayName = 'FlightTripDetails'; +CarTripDetails.displayName = 'CarTripDetails'; export default CarTripDetails; From 79b0cbd84970de02ffeddc99d328729bd55fc81a Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 29 Nov 2024 14:35:31 +0100 Subject: [PATCH 050/325] Add Spanish translation for cancellationUntil in booking and car sections --- src/languages/es.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/languages/es.ts b/src/languages/es.ts index 9a8a6c69ebe3..c25a9a0f6027 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2474,6 +2474,7 @@ const translations = { checkOut: 'Salida', roomType: 'Tipo de habitación', cancellation: 'Política de cancelación', + cancellationUntil: 'Cancelación gratuita hasta el', confirmation: 'Número de confirmación', }, car: 'Auto', @@ -2484,6 +2485,7 @@ const translations = { driver: 'Conductor', carType: 'Tipo de coche', cancellation: 'Política de cancelación', + cancellationUntil: 'Cancelación gratuita hasta el', confirmation: 'Número de confirmación', }, viewTrip: 'Ver viaje', From 7bdea21dc5f71150e9e4c16b916b571dd796b896 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 29 Nov 2024 14:35:39 +0100 Subject: [PATCH 051/325] Remove unnecessary blank line in HotelTripDetails component --- src/pages/Travel/HotelTripDetails.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/Travel/HotelTripDetails.tsx b/src/pages/Travel/HotelTripDetails.tsx index f0fa279fbbe2..f0b16b0cb142 100644 --- a/src/pages/Travel/HotelTripDetails.tsx +++ b/src/pages/Travel/HotelTripDetails.tsx @@ -30,7 +30,6 @@ function HotelTripDetails({transaction, personalDetails}: HotelTripDetailsProps) return null; } - const reservationIcon = TripReservationUtils.getTripReservationIcon(hotelReservation.type); const checkInDate = DateUtils.getFormattedTransportDateAndHour(new Date(hotelReservation.start.date)); const checkOutDate = DateUtils.getFormattedTransportDateAndHour(new Date(hotelReservation.end.date)); From b4c8b46bc8e4a51eae99310499d946937d85cfc4 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Sat, 30 Nov 2024 17:16:29 +0800 Subject: [PATCH 052/325] apply tax rule when selecting category --- src/libs/actions/IOU.ts | 8 ++++ .../request/step/IOURequestStepCategory.tsx | 39 +++++++++++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 90719ffeed55..97aa5bb4c717 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3203,6 +3203,8 @@ function updateMoneyRequestCategory( transactionID: string, transactionThreadReportID: string, category: string, + categoryTaxCode: string | undefined, + categoryTaxAmount: number | undefined, policy: OnyxEntry, policyTagList: OnyxEntry, policyCategories: OnyxEntry, @@ -3210,6 +3212,12 @@ function updateMoneyRequestCategory( const transactionChanges: TransactionChanges = { category, }; + + if (categoryTaxCode && categoryTaxAmount !== undefined) { + transactionChanges['taxCode'] = categoryTaxCode; + transactionChanges['taxAmount'] = categoryTaxAmount; + } + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories); API.write(WRITE_COMMANDS.UPDATE_MONEY_REQUEST_CATEGORY, params, onyxData); } diff --git a/src/pages/iou/request/step/IOURequestStepCategory.tsx b/src/pages/iou/request/step/IOURequestStepCategory.tsx index 6f81d6ea3443..7b478cff75ec 100644 --- a/src/pages/iou/request/step/IOURequestStepCategory.tsx +++ b/src/pages/iou/request/step/IOURequestStepCategory.tsx @@ -14,6 +14,8 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as CategoryUtils from '@libs/CategoryUtils'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; @@ -68,7 +70,8 @@ function IOURequestStepCategory({ const {translate} = useLocalize(); const isEditing = action === CONST.IOU.ACTION.EDIT; const isEditingSplitBill = isEditing && iouType === CONST.IOU.TYPE.SPLIT; - const transactionCategory = ReportUtils.getTransactionDetails(isEditingSplitBill && !lodashIsEmpty(splitDraftTransaction) ? splitDraftTransaction : transaction)?.category; + const currentTransaction = isEditingSplitBill && !lodashIsEmpty(splitDraftTransaction) ? splitDraftTransaction : transaction; + const transactionCategory = ReportUtils.getTransactionDetails(currentTransaction)?.category; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const reportAction = reportActions?.[report?.parentReportActionID || reportActionID] ?? null; @@ -109,17 +112,42 @@ function IOURequestStepCategory({ const categorySearchText = category.searchText ?? ''; const isSelectedCategory = categorySearchText === transactionCategory; const updatedCategory = isSelectedCategory ? '' : categorySearchText; + const categoryTaxCode = CategoryUtils.getCategoryDefaultTaxRate(policy?.rules?.expenseRules ?? [], updatedCategory, policy?.taxRates?.defaultExternalID); + let categoryTaxPercentage; + let categoryTaxAmount; + + if (categoryTaxCode) { + categoryTaxPercentage = TransactionUtils.getTaxValue(policy, currentTransaction, categoryTaxCode); + + if (categoryTaxPercentage) { + const isFromExpenseReport = ReportUtils.isExpenseReport(report) || ReportUtils.isPolicyExpenseChat(report); + categoryTaxAmount = CurrencyUtils.convertToBackendAmount( + TransactionUtils.calculateTaxAmount( + categoryTaxPercentage, + TransactionUtils.getAmount(currentTransaction, isFromExpenseReport), + TransactionUtils.getCurrency(transaction), + ), + ); + } + } + + console.log(categoryTaxPercentage, categoryTaxAmount); if (transaction) { // In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value if (isEditingSplitBill) { - IOU.setDraftSplitTransaction(transaction.transactionID, {category: updatedCategory}); + const transactionChanges: TransactionUtils.TransactionChanges = {category: updatedCategory}; + if (categoryTaxCode && categoryTaxAmount !== undefined) { + transactionChanges.taxCode = categoryTaxCode; + transactionChanges.taxAmount = categoryTaxAmount; + } + IOU.setDraftSplitTransaction(transaction.transactionID, transactionChanges); navigateBack(); return; } if (isEditing && report) { - IOU.updateMoneyRequestCategory(transaction.transactionID, report.reportID, updatedCategory, policy, policyTags, policyCategories); + IOU.updateMoneyRequestCategory(transaction.transactionID, report.reportID, updatedCategory, categoryTaxCode, categoryTaxAmount, policy, policyTags, policyCategories); navigateBack(); return; } @@ -127,6 +155,11 @@ function IOURequestStepCategory({ IOU.setMoneyRequestCategory(transactionID, updatedCategory); + if (categoryTaxCode && categoryTaxAmount !== undefined) { + IOU.setMoneyRequestTaxRate(transactionID, categoryTaxCode); + IOU.setMoneyRequestTaxAmount(transactionID, categoryTaxAmount); + } + if (action === CONST.IOU.ACTION.CATEGORIZE) { Navigation.closeAndNavigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(action, iouType, transactionID, report?.reportID ?? '-1')); return; From 622fe8bb19e9cb99f1a316d39c6981a4e15ed39b Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Sat, 30 Nov 2024 17:37:41 +0800 Subject: [PATCH 053/325] remove log --- src/pages/iou/request/step/IOURequestStepCategory.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepCategory.tsx b/src/pages/iou/request/step/IOURequestStepCategory.tsx index 7b478cff75ec..e148b527f19a 100644 --- a/src/pages/iou/request/step/IOURequestStepCategory.tsx +++ b/src/pages/iou/request/step/IOURequestStepCategory.tsx @@ -131,8 +131,6 @@ function IOURequestStepCategory({ } } - console.log(categoryTaxPercentage, categoryTaxAmount); - if (transaction) { // In the split flow, when editing we use SPLIT_TRANSACTION_DRAFT to save draft value if (isEditingSplitBill) { From 438ad047da5893d05fe4f193ae27631e453f632c Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Sat, 30 Nov 2024 17:43:34 +0800 Subject: [PATCH 054/325] lint --- src/libs/actions/IOU.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 97aa5bb4c717..3c50ee3f32d4 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -3214,8 +3214,8 @@ function updateMoneyRequestCategory( }; if (categoryTaxCode && categoryTaxAmount !== undefined) { - transactionChanges['taxCode'] = categoryTaxCode; - transactionChanges['taxAmount'] = categoryTaxAmount; + transactionChanges.taxCode = categoryTaxCode; + transactionChanges.taxAmount = categoryTaxAmount; } const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, policy, policyTagList, policyCategories); From bde424e883f610c125de70f7cafc0760c2e8323a Mon Sep 17 00:00:00 2001 From: jayeshmangwani Date: Sun, 1 Dec 2024 02:20:31 +0530 Subject: [PATCH 055/325] added ws plan type page and added upgrade generic features view --- src/ROUTES.ts | 10 +- src/SCREENS.ts | 1 + src/languages/en.ts | 32 +++++ src/languages/es.ts | 32 +++++ src/languages/params.ts | 6 + .../parameters/UpgradeToCorporateParams.ts | 2 +- .../ModalStackNavigators/index.tsx | 1 + .../FULL_SCREEN_TO_RHP_MAPPING.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 3 + src/libs/Navigation/types.ts | 2 +- src/libs/PolicyUtils.ts | 12 ++ src/libs/actions/Policy/Policy.ts | 6 +- src/pages/workspace/WorkspaceProfilePage.tsx | 17 +++ .../WorkspaceProfilePlanTypePage.tsx | 128 ++++++++++++++++++ src/pages/workspace/WorkspacesListRow.tsx | 28 ++-- .../workspace/upgrade/GenericFeaturesView.tsx | 75 ++++++++++ src/pages/workspace/upgrade/UpgradeIntro.tsx | 13 +- .../upgrade/WorkspaceUpgradePage.tsx | 5 +- 18 files changed, 346 insertions(+), 28 deletions(-) create mode 100644 src/pages/workspace/WorkspaceProfilePlanTypePage.tsx create mode 100644 src/pages/workspace/upgrade/GenericFeaturesView.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index a6eb3c1166df..45a6345622fc 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -711,6 +711,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/profile/address', getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/profile/address` as const, backTo), }, + WORKSPACE_PROFILE_PLAN: { + route: 'settings/workspaces/:policyID/profile/plan', + getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/profile/plan` as const, backTo), + }, WORKSPACE_ACCOUNTING: { route: 'settings/workspaces/:policyID/accounting', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting` as const, @@ -974,9 +978,9 @@ const ROUTES = { getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/category/${encodeURIComponent(categoryName)}` as const, }, WORKSPACE_UPGRADE: { - route: 'settings/workspaces/:policyID/upgrade/:featureName', - getRoute: (policyID: string, featureName: string, backTo?: string) => - getUrlWithBackToParam(`settings/workspaces/${policyID}/upgrade/${encodeURIComponent(featureName)}` as const, backTo), + route: 'settings/workspaces/:policyID/upgrade/:featureName?', + getRoute: (policyID: string, featureName?: string, backTo?: string) => + getUrlWithBackToParam(`settings/workspaces/${policyID}/upgrade/${encodeURIComponent(featureName ?? '')}` as const, backTo), }, WORKSPACE_DOWNGRADE: { route: 'settings/workspaces/:policyID/downgrade/', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index e4fa03bf4815..1b4ecb1ea1c8 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -498,6 +498,7 @@ const SCREENS = { TAG_GL_CODE: 'Tag_GL_Code', CURRENCY: 'Workspace_Profile_Currency', ADDRESS: 'Workspace_Profile_Address', + PLAN: 'Workspace_Profile_Plan_Type', WORKFLOWS: 'Workspace_Workflows', WORKFLOWS_PAYER: 'Workspace_Workflows_Payer', WORKFLOWS_APPROVALS_NEW: 'Workspace_Approvals_New', diff --git a/src/languages/en.ts b/src/languages/en.ts index 13ff38a0bc58..54cff7b78575 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -191,6 +191,7 @@ import type { WelcomeNoteParams, WelcomeToRoomParams, WeSentYouMagicSignInLinkParams, + WorkspaceLockedPlanTypeParams, WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams, YourPlanPriceParams, ZipCodeExampleFormatParams, @@ -2538,6 +2539,7 @@ const translations = { return 'Member'; } }, + planType: 'Plan type', submitExpense: 'Submit expenses using your workspace chat below:', defaultCategory: 'Default category', }, @@ -4280,6 +4282,19 @@ const translations = { moreDetails: 'for more details.', gotIt: 'Got it, thanks', }, + commonFeatures: { + title: 'Upgrade Workspace to Control', + note: 'Get access to all our most advanced functionality, including:', + benefits: { + note: 'The Control plan starts at $9 per active member per month.', + learnMore: 'Learn more', + pricing: 'about our plans and pricing.', + benefit1: 'Advanced accounting connections (NetSuite, Sage Intacct and more)', + benefit2: 'Expense rules', + benefit3: 'Multiple approval workflows', + benefit4: 'Enhanced security controls', + }, + }, }, restrictedAction: { restricted: 'Restricted', @@ -4383,6 +4398,23 @@ const translations = { andEnableWorkflows: 'and enable workflows, then add approvals to unlock this feature.', }, }, + planTypePage: { + planTypes: { + team: { + label: 'Collect', + description: 'For teams looking to automate their processes.', + }, + corporate: { + label: 'Control', + description: 'For organizations with advanced requirements.', + }, + }, + description: "Choose a plan that's right for you. For a detailed list of features and pricing, check out our", + subscriptionLink: 'plan types and pricing help page', + lockedPlanDescription: ({subscriptionUsersCount, annualSubscriptionEndDate}: WorkspaceLockedPlanTypeParams) => + `You've committed to ${subscriptionUsersCount} active users on the Control plan until your annual subscription ends on ${annualSubscriptionEndDate}. You can switch to pay-per-use subscription and downgrade to the Collect plan starting ${annualSubscriptionEndDate} by disabling auto-renew in`, + subscriptions: 'Subscriptions', + }, }, getAssistancePage: { title: 'Get assistance', diff --git a/src/languages/es.ts b/src/languages/es.ts index 6477827fd0a1..b40a65c6eb6e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -191,6 +191,7 @@ import type { WelcomeNoteParams, WelcomeToRoomParams, WeSentYouMagicSignInLinkParams, + WorkspaceLockedPlanTypeParams, WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams, YourPlanPriceParams, ZipCodeExampleFormatParams, @@ -2562,6 +2563,7 @@ const translations = { return 'Miembro'; } }, + planType: 'Tipo de plan', submitExpense: 'Envíe los gastos utilizando el chat de su espacio de trabajo:', defaultCategory: 'Categoría predeterminada', }, @@ -4247,6 +4249,23 @@ const translations = { confirmText: 'Sí, exportar de nuevo', cancelText: 'Cancelar', }, + planTypePage: { + planTypes: { + team: { + label: 'Collect', + description: 'Para equipos que buscan automatizar sus procesos.', + }, + corporate: { + label: 'Recolectar', + description: 'Para organizaciones con requisitos avanzados.', + }, + }, + description: 'Elige el plan adecuado para ti. Para ver una lista detallada de funciones y precios, consulta nuestra', + subscriptionLink: 'página de ayuda sobre tipos de planes y precios', + lockedPlanDescription: ({subscriptionUsersCount, annualSubscriptionEndDate}: WorkspaceLockedPlanTypeParams) => + `Tienes un compromiso anual de ${subscriptionUsersCount} miembros activos en el plan Control hasta el ${annualSubscriptionEndDate}. Puedes cambiar a una suscripción de pago por uso y desmejorar al plan Recopilar a partir del ${annualSubscriptionEndDate} desactivando la renovación automática en`, + subscriptions: 'Suscripciones', + }, upgrade: { reportFields: { title: 'Los campos', @@ -4328,6 +4347,19 @@ const translations = { moreDetails: 'para obtener más información.', gotIt: 'Entendido, gracias.', }, + commonFeatures: { + title: 'Actualiza tu espacio de trabajo al plan Controlar', + note: 'Obtén acceso a todas nuestras funciones más avanzadas, incluyendo:', + benefits: { + note: 'El plan Controlar comienza en $9 por miembro activo al mes.', + learnMore: 'Obtén más información', + pricing: 'sobre nuestros planes y precios.', + benefit1: 'Conexiones contables avanzadas (NetSuite, Sage Intacct y más)', + benefit2: 'Reglas de gastos', + benefit3: 'Flujos de aprobación múltiples', + benefit4: 'Controles de seguridad mejorados', + }, + }, }, restrictedAction: { restricted: 'Restringido', diff --git a/src/languages/params.ts b/src/languages/params.ts index 90b789efae58..9b9b3aeca092 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -559,6 +559,11 @@ type CurrencyCodeParams = { currencyCode: string; }; +type WorkspaceLockedPlanTypeParams = { + subscriptionUsersCount: number; + annualSubscriptionEndDate: string; +}; + type CompanyNameParams = { companyName: string; }; @@ -769,6 +774,7 @@ export type { ImportedTypesParams, ImportPerDiemRatesSuccessfullDescriptionParams, CurrencyCodeParams, + WorkspaceLockedPlanTypeParams, CompanyNameParams, ChatWithAccountManagerParams, }; diff --git a/src/libs/API/parameters/UpgradeToCorporateParams.ts b/src/libs/API/parameters/UpgradeToCorporateParams.ts index ee9d1359c4dd..7b7ff3e0adcc 100644 --- a/src/libs/API/parameters/UpgradeToCorporateParams.ts +++ b/src/libs/API/parameters/UpgradeToCorporateParams.ts @@ -1,6 +1,6 @@ type UpgradeToCorporateParams = { policyID: string; - featureName: string; + featureName?: string; }; export default UpgradeToCorporateParams; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 9822c60faaa8..73b256ae6fbd 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -269,6 +269,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/WorkspaceProfileCurrencyPage').default, [SCREENS.WORKSPACE.CATEGORY_SETTINGS]: () => require('../../../../pages/workspace/categories/CategorySettingsPage').default, [SCREENS.WORKSPACE.ADDRESS]: () => require('../../../../pages/workspace/WorkspaceProfileAddressPage').default, + [SCREENS.WORKSPACE.PLAN]: () => require('../../../../pages/workspace/WorkspaceProfilePlanTypePage').default, [SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: () => require('../../../../pages/workspace/categories/WorkspaceCategoriesSettingsPage').default, [SCREENS.WORKSPACE.CATEGORIES_IMPORT]: () => require('../../../../pages/workspace/categories/ImportCategoriesPage').default, [SCREENS.WORKSPACE.CATEGORIES_IMPORTED]: () => require('../../../../pages/workspace/categories/ImportedCategoriesPage').default, diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index f36f154819f5..4365c5e65e25 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -5,6 +5,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { [SCREENS.WORKSPACE.PROFILE]: [ SCREENS.WORKSPACE.NAME, SCREENS.WORKSPACE.ADDRESS, + SCREENS.WORKSPACE.PLAN, SCREENS.WORKSPACE.CURRENCY, SCREENS.WORKSPACE.DESCRIPTION, SCREENS.WORKSPACE.SHARE, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 02f7f6950a0d..e9f627ee4341 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -347,6 +347,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.ADDRESS]: { path: ROUTES.WORKSPACE_PROFILE_ADDRESS.route, }, + [SCREENS.WORKSPACE.PLAN]: { + path: ROUTES.WORKSPACE_PROFILE_PLAN.route, + }, [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_IMPORT]: {path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_IMPORT.route}, [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_CHART_OF_ACCOUNTS]: {path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_CHART_OF_ACCOUNTS.route}, [SCREENS.WORKSPACE.ACCOUNTING.QUICKBOOKS_ONLINE_CLASSES]: {path: ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_CLASSES.route}, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 3674a24a907b..2bb786884bbe 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -246,7 +246,7 @@ type SettingsNavigatorParamList = { }; [SCREENS.WORKSPACE.UPGRADE]: { policyID: string; - featureName: string; + featureName?: string; backTo?: Routes; categoryId?: string; }; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index f6b277d69d6b..562b2ba62198 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1119,6 +1119,17 @@ function getActivePolicy(): OnyxEntry { return getPolicy(activePolicyId); } +function getUserFriendlyWorkspaceType(workspaceType: ValueOf) { + switch (workspaceType) { + case CONST.POLICY.TYPE.CORPORATE: + return Localize.translateLocal('workspace.type.control'); + case CONST.POLICY.TYPE.TEAM: + return Localize.translateLocal('workspace.type.collect'); + default: + return Localize.translateLocal('workspace.type.free'); + } +} + function isPolicyAccessible(policy: OnyxEntry): boolean { return !isEmptyObject(policy) && (Object.keys(policy).length !== 1 || isEmptyObject(policy.errors)) && !!policy?.id; } @@ -1252,6 +1263,7 @@ export { getNetSuiteImportCustomFieldLabel, getAllPoliciesLength, getActivePolicy, + getUserFriendlyWorkspaceType, isPolicyAccessible, areAllGroupPoliciesExpenseChatDisabled, }; diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 0b72d2de3f98..01a5909de9db 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -1663,6 +1663,7 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName outputCurrency: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, address: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, description: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + type: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, }, }, }, @@ -1735,6 +1736,7 @@ function buildPolicyData(policyOwnerEmail = '', makeMeAdmin = false, policyName outputCurrency: null, address: null, description: null, + type: null, }, }, }, @@ -3324,7 +3326,7 @@ function setForeignCurrencyDefault(policyID: string, taxCode: string) { API.write(WRITE_COMMANDS.SET_POLICY_TAXES_FOREIGN_CURRENCY_DEFAULT, parameters, onyxData); } -function upgradeToCorporate(policyID: string, featureName: string) { +function upgradeToCorporate(policyID: string, featureName?: string) { const policy = getPolicy(policyID); const optimisticData: OnyxUpdate[] = [ { @@ -3376,7 +3378,7 @@ function upgradeToCorporate(policyID: string, featureName: string) { }, ]; - const parameters: UpgradeToCorporateParams = {policyID, featureName}; + const parameters: UpgradeToCorporateParams = {policyID, ...(featureName ? {featureName} : {})}; API.write(WRITE_COMMANDS.UPGRADE_TO_CORPORATE, parameters, {optimisticData, successData, failureData}); } diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx index 4a4862152d53..f46ac071a81b 100644 --- a/src/pages/workspace/WorkspaceProfilePage.tsx +++ b/src/pages/workspace/WorkspaceProfilePage.tsx @@ -77,6 +77,7 @@ function WorkspaceProfilePage({policyDraft, policy: policyProp, route}: Workspac const onPressName = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE_NAME.getRoute(policy?.id ?? '-1')), [policy?.id]); const onPressDescription = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE_DESCRIPTION.getRoute(policy?.id ?? '-1')), [policy?.id]); const onPressShare = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE_SHARE.getRoute(policy?.id ?? '-1')), [policy?.id]); + const onPressPlanType = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE_PLAN.getRoute(policy?.id ?? '-1')), [policy?.id]); const policyName = policy?.name ?? ''; const policyDescription = @@ -267,6 +268,22 @@ function WorkspaceProfilePage({policyDraft, policy: policyProp, route}: Workspac )} + {!readOnly && !!policy?.type && ( + + + + + + )} {!readOnly && (