diff --git a/.github/actions/composite/buildAndroidE2EAPK/action.yml b/.github/actions/composite/buildAndroidE2EAPK/action.yml
index 47e13f6313a0..8995c7681d40 100644
--- a/.github/actions/composite/buildAndroidE2EAPK/action.yml
+++ b/.github/actions/composite/buildAndroidE2EAPK/action.yml
@@ -74,7 +74,7 @@ runs:
shell: bash
- name: Upload APK
- uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3
+ uses: actions/upload-artifact@v4
with:
name: ${{ inputs.ARTIFACT_NAME }}
path: ${{ inputs.APP_OUTPUT_PATH }}
diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml
index 0d5879217ea0..add4879d8de1 100644
--- a/.github/workflows/e2ePerformanceTests.yml
+++ b/.github/workflows/e2ePerformanceTests.yml
@@ -156,7 +156,7 @@ jobs:
run: mkdir zip
- name: Download baseline APK
- uses: actions/download-artifact@348754975ef0295bfa2c111cba996120cfdf8a5d
+ uses: actions/download-artifact@v4
id: downloadBaselineAPK
with:
name: baseline-apk-${{ needs.buildBaseline.outputs.VERSION }}
@@ -170,7 +170,7 @@ jobs:
run: mv "${{steps.downloadBaselineAPK.outputs.download-path}}/app-e2e-release.apk" "${{steps.downloadBaselineAPK.outputs.download-path}}/app-e2eRelease.apk"
- name: Download delta APK
- uses: actions/download-artifact@348754975ef0295bfa2c111cba996120cfdf8a5d
+ uses: actions/download-artifact@v4
id: downloadDeltaAPK
with:
name: delta-apk-${{ needs.buildDelta.outputs.DELTA_REF }}
@@ -184,7 +184,7 @@ jobs:
- name: Copy e2e code into zip folder
run: cp tests/e2e/dist/index.js zip/testRunner.ts
-
+
- name: Copy profiler binaries into zip folder
run: cp -r node_modules/@perf-profiler/android/cpp-profiler/bin zip/bin
diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml
index 640d1eaa1172..e1534bfba1c7 100644
--- a/.github/workflows/platformDeploy.yml
+++ b/.github/workflows/platformDeploy.yml
@@ -94,14 +94,14 @@ jobs:
VERSION: ${{ env.VERSION_CODE }}
- name: Archive Android sourcemaps
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: android-sourcemap-${{ github.ref_name }}
path: android/app/build/generated/sourcemaps/react/productionRelease/index.android.bundle.map
- name: Upload Android version to GitHub artifacts
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: app-production-release.aab
path: android/app/build/outputs/bundle/productionRelease/app-production-release.aab
@@ -246,14 +246,14 @@ jobs:
APPLE_DEMO_PASSWORD: ${{ secrets.APPLE_DEMO_PASSWORD }}
- name: Archive iOS sourcemaps
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: ios-sourcemap-${{ github.ref_name }}
path: main.jsbundle.map
- name: Upload iOS version to GitHub artifacts
if: ${{ !fromJSON(env.SHOULD_DEPLOY_PRODUCTION) }}
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: New Expensify.ipa
path: /Users/runner/work/App/App/New Expensify.ipa
diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml
index 10912aaeb436..5d70c16c28e0 100644
--- a/.github/workflows/testBuild.yml
+++ b/.github/workflows/testBuild.yml
@@ -125,7 +125,7 @@ jobs:
MYAPP_UPLOAD_KEY_PASSWORD: ${{ secrets.MYAPP_UPLOAD_KEY_PASSWORD }}
- name: Upload Artifact
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: android
path: ./android_paths.json
@@ -217,7 +217,7 @@ jobs:
S3_REGION: us-east-1
- name: Upload Artifact
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: ios
path: ./ios_paths.json
@@ -321,7 +321,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha || needs.getBranchRef.outputs.REF }}
- name: Download Artifact
- uses: actions/download-artifact@v3
+ uses: actions/download-artifact@v4
if: ${{ fromJSON(needs.validateActor.outputs.READY_TO_BUILD) }}
- name: Read JSONs with android paths
diff --git a/android/app/build.gradle b/android/app/build.gradle
index f722d8426b7e..d6de6cf4fae0 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -15,6 +15,7 @@ fullstory {
org 'o-1WN56P-na1'
enabledVariants 'all'
logcatLevel 'debug'
+ recordOnStart false
}
react {
@@ -107,8 +108,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1009000602
- versionName "9.0.6-2"
+ versionCode 1009000603
+ versionName "9.0.6-3"
// Supported language variants must be declared here to avoid from being removed during the compilation.
// This also helps us to not include unnecessary language variants in the APK.
resConfigs "en", "es"
diff --git a/docs/articles/expensify-classic/expensify-card/Request-the-Card.md b/docs/articles/expensify-classic/expensify-card/Request-the-Card.md
index b65c66c986ad..1f412665fc2f 100644
--- a/docs/articles/expensify-classic/expensify-card/Request-the-Card.md
+++ b/docs/articles/expensify-classic/expensify-card/Request-the-Card.md
@@ -5,7 +5,8 @@ description: Details on requesting the Expensify Card as an employee
To start using the Expensify Card, do the following:
1. **Enable Expensify Cards:** An admin must first enable the cards. Then, an admin can assign you a card by setting a limit, which allows access to the card.
2. **Request the Card:**
- - If you haven’t been assigned a limit, look for the task on your account’s homepage that says, “Ask your admin for the card!” Use this task to message your admin team.
+ - If you haven’t been assigned a limit, look for the task on your account’s homepage that says, “Ask your admin for the card!”
+ - Completing that task will send an in-product notification to your admin team that you requested the card.
- Once you’re assigned a card limit, you’ll receive an email notification. Click the link in the email to provide your shipping address on your account’s homepage.
- Enter your address, and the physical card will be shipped within 3-5 business days.
3. **Activate the Card:** When your physical card arrives, activate it in Expensify by entering the last four digits of the card in the activation task on your homepage.
diff --git a/docs/articles/expensify-classic/integrations/travel-integrations/Egencia.md b/docs/articles/expensify-classic/integrations/travel-integrations/Egencia.md
index 178621a62d90..35d232d2df67 100644
--- a/docs/articles/expensify-classic/integrations/travel-integrations/Egencia.md
+++ b/docs/articles/expensify-classic/integrations/travel-integrations/Egencia.md
@@ -24,7 +24,7 @@ Egencia controls the feed, so to connect Expensify you will need to:
# How to Connect to a Central Purchasing Account
Once your Egencia account manager has established the feed, you can automatically forward all Egencia booking receipts to a single Expensify account. To do this:
1. Open a chat with Concierge.
-2. Tell Concierge “Please enable Central Purchasing Account for our Egencia feed. The account email is: xxx@yourdomain.com”.
+2. Tell Concierge the address of your central purchasing account, “Please enable Central Purchasing Account for our Egencia feed. The account email is: xxx@yourdomain.com”.
-The receipt the traveler receives is a "reservation expense." Reservation expenses are non-reimbursable and won’t be included in any integrated accounting system exports. The reservation sent to the traveler's account is added to their mobile app Trips feature so that the traveler can easily keep tabs on upcoming travel and receive trip notifications.
+A receipt will be sent to both the traveler and the central account. The receipt sent to the traveler is a "reservation expense." Reservation expenses are non-reimbursable and won’t be included in any integrated accounting system exports.
diff --git a/docs/articles/new-expensify/travel/Expensify-Travel-demo-video.md b/docs/articles/new-expensify/travel/Expensify-Travel-demo-video.md
new file mode 100644
index 000000000000..ceb40254c607
--- /dev/null
+++ b/docs/articles/new-expensify/travel/Expensify-Travel-demo-video.md
@@ -0,0 +1,8 @@
+---
+title: Expensify Travel demo video
+description: Check out a demo of Expensify Travel
+---
+
+Check out a video of how Expensify Travel works below:
+
+
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 3473c2bfcec8..3e0b46a292a7 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,11 +40,13 @@
CFBundleVersion
- 9.0.6.2
+ 9.0.6.3
FullStory
OrgId
o-1WN56P-na1
+ RecordOnStart
+
ITSAppUsesNonExemptEncryption
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index f413f0d1ae99..d13eca4d1cad 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 9.0.6.2
+ 9.0.6.3
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 212ae9c1aa43..5125a598997f 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -13,7 +13,7 @@
CFBundleShortVersionString
9.0.6
CFBundleVersion
- 9.0.6.2
+ 9.0.6.3
NSExtension
NSExtensionPointIdentifier
diff --git a/package-lock.json b/package-lock.json
index ea83407c543f..45813001d07c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "9.0.6-2",
+ "version": "9.0.6-3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "9.0.6-2",
+ "version": "9.0.6-3",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 4d6f7f702ab0..1cd32c974031 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "9.0.6-2",
+ "version": "9.0.6-3",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
diff --git a/src/CONST.ts b/src/CONST.ts
index 3b879e10c345..ca2651a51a7c 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -335,6 +335,8 @@ const CONST = {
VERIFICATION_MAX_ATTEMPTS: 7,
STATE: {
VERIFYING: 'VERIFYING',
+ VALIDATING: 'VALIDATING',
+ SETUP: 'SETUP',
PENDING: 'PENDING',
OPEN: 'OPEN',
},
@@ -361,7 +363,6 @@ const CONST = {
DEFAULT_ROOMS: 'defaultRooms',
VIOLATIONS: 'violations',
DUPE_DETECTION: 'dupeDetection',
- REPORT_FIELDS: 'reportFields',
P2P_DISTANCE_REQUESTS: 'p2pDistanceRequests',
WORKFLOWS_DELAYED_SUBMISSION: 'workflowsDelayedSubmission',
SPOTNANA_TRAVEL: 'spotnanaTravel',
diff --git a/src/Expensify.tsx b/src/Expensify.tsx
index f96c51961acc..6151f983e8d0 100644
--- a/src/Expensify.tsx
+++ b/src/Expensify.tsx
@@ -19,6 +19,7 @@ import * as Report from './libs/actions/Report';
import * as User from './libs/actions/User';
import * as ActiveClientManager from './libs/ActiveClientManager';
import BootSplash from './libs/BootSplash';
+import FS from './libs/Fullstory';
import * as Growl from './libs/Growl';
import Log from './libs/Log';
import migrateOnyx from './libs/migrateOnyx';
@@ -147,6 +148,9 @@ function Expensify({
// Initialize this client as being an active client
ActiveClientManager.init();
+ // Initialize Fullstory lib
+ FS.init();
+
// Used for the offline indicator appearing when someone is offline
const unsubscribeNetInfo = NetworkConnection.subscribeToNetInfo();
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 56006d599d6f..1e99e2132203 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -676,10 +676,15 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/accounting/quickbooks-online/invoice-account-selector',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/invoice-account-selector` as const,
},
- WORKSPACE_ACCOUNTING_RECONCILIATION_ACCOUNT_SETTINGS: {
+ WORKSPACE_ACCOUNTING_CARD_RECONCILIATION: {
route: 'settings/workspaces/:policyID/accounting/:connection/card-reconciliation',
getRoute: (policyID: string, connection: ValueOf) => `settings/workspaces/${policyID}/accounting/${connection}/card-reconciliation` as const,
},
+ WORKSPACE_ACCOUNTING_RECONCILIATION_ACCOUNT_SETTINGS: {
+ route: 'settings/workspaces/:policyID/accounting/:connection/card-reconciliation/account',
+ getRoute: (policyID: string, connection: ValueOf) =>
+ `settings/workspaces/${policyID}/accounting/${connection}/card-reconciliation/account` as const,
+ },
WORKSPACE_CATEGORIES: {
route: 'settings/workspaces/:policyID/categories',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/categories` as const,
@@ -704,6 +709,10 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/categories/:categoryName/edit',
getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/edit` as const,
},
+ WORKSPACE_CATEGORY_PAYROLL_CODE: {
+ route: 'settings/workspaces/:policyID/categories/:categoryName/payroll-code',
+ getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/payroll-code` as const,
+ },
WORKSPACE_CATEGORY_GL_CODE: {
route: 'settings/workspaces/:policyID/categories/:categoryName/gl-code',
getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/gl-code` as const,
@@ -842,6 +851,10 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/expensify-card/issue-new',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card/issue-new` as const,
},
+ WORKSPACE_EXPENSIFY_CARD_BANK_ACCOUNT: {
+ route: 'settings/workspaces/:policyID/expensify-card/choose-bank-account',
+ getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card/choose-bank-account` as const,
+ },
WORKSPACE_DISTANCE_RATES: {
route: 'settings/workspaces/:policyID/distance-rates',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/distance-rates` as const,
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 4790ac3c6a32..8a71030dff44 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -330,6 +330,7 @@ const SCREENS = {
SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT: 'Policy_Accounting_Sage_Intacct_Non_Reimbursable_Credit_Card_Account',
SAGE_INTACCT_ADVANCED: 'Policy_Accounting_Sage_Intacct_Advanced',
SAGE_INTACCT_PAYMENT_ACCOUNT: 'Policy_Accounting_Sage_Intacct_Payment_Account',
+ CARD_RECONCILIATION: 'Policy_Accounting_Card_Reconciliation',
RECONCILIATION_ACCOUNT_SETTINGS: 'Policy_Accounting_Reconciliation_Account_Settings',
},
INITIAL: 'Workspace_Initial',
@@ -341,6 +342,7 @@ const SCREENS = {
RATE_AND_UNIT_UNIT: 'Workspace_RateAndUnit_Unit',
EXPENSIFY_CARD: 'Workspace_ExpensifyCard',
EXPENSIFY_CARD_ISSUE_NEW: 'Workspace_ExpensifyCard_New',
+ EXPENSIFY_CARD_BANK_ACCOUNT: 'Workspace_ExpensifyCard_BankAccount',
BILLS: 'Workspace_Bills',
INVOICES: 'Workspace_Invoices',
TRAVEL: 'Workspace_Travel',
@@ -385,6 +387,7 @@ const SCREENS = {
NAME: 'Workspace_Profile_Name',
CATEGORY_CREATE: 'Category_Create',
CATEGORY_EDIT: 'Category_Edit',
+ CATEGORY_PAYROLL_CODE: 'Category_Payroll_Code',
CATEGORY_GL_CODE: 'Category_GL_Code',
CATEGORY_SETTINGS: 'Category_Settings',
CATEGORIES_SETTINGS: 'Categories_Settings',
diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx
index 0fd3cc0728ca..126c81961cee 100644
--- a/src/components/Button/index.tsx
+++ b/src/components/Button/index.tsx
@@ -1,6 +1,6 @@
import {useIsFocused} from '@react-navigation/native';
import type {ForwardedRef} from 'react';
-import React, {useCallback, useMemo} from 'react';
+import React, {useCallback, useMemo, useState} from 'react';
import type {GestureResponderEvent, StyleProp, TextStyle, ViewStyle} from 'react-native';
import {ActivityIndicator, View} from 'react-native';
import Icon from '@components/Icon';
@@ -89,6 +89,9 @@ type ButtonProps = Partial & {
/** Whether we should use the danger theme color */
danger?: boolean;
+ /** Whether we should display the button as a link */
+ link?: boolean;
+
/** Should we remove the right border radius top + bottom? */
shouldRemoveRightBorderRadius?: boolean;
@@ -205,6 +208,7 @@ function Button(
id = '',
accessibilityLabel = '',
isSplitButton = false,
+ link = false,
isContentCentered = false,
...rest
}: ButtonProps,
@@ -213,6 +217,7 @@ function Button(
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
+ const [isHovered, setIsHovered] = useState(false);
const renderContent = () => {
if ('children' in rest) {
@@ -233,6 +238,10 @@ function Button(
danger && styles.buttonDangerText,
!!icon && styles.textAlignLeft,
textStyles,
+ link && styles.link,
+ link && isHovered && StyleUtils.getColorStyle(theme.linkHover),
+ link && styles.fontWeightNormal,
+ link && styles.fontSizeLabel,
]}
dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
>
@@ -343,6 +352,7 @@ function Button(
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
text && shouldShowRightIcon ? styles.alignItemsStretch : undefined,
innerStyles,
+ link && styles.bgTransparent,
]}
hoverStyle={[
shouldUseDefaultHover && !isDisabled ? styles.buttonDefaultHovered : undefined,
@@ -353,6 +363,8 @@ function Button(
accessibilityLabel={accessibilityLabel}
role={CONST.ROLE.BUTTON}
hoverDimmingValue={1}
+ onHoverIn={() => setIsHovered(true)}
+ onHoverOut={() => setIsHovered(false)}
>
{renderContent()}
{isLoading && (
diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx
index a41f983434d8..3889c8597843 100755
--- a/src/components/Composer/index.tsx
+++ b/src/components/Composer/index.tsx
@@ -116,12 +116,11 @@ function Composer(
}, [shouldClear, onClear]);
useEffect(() => {
- setSelection((prevSelection) => {
- if (!!prevSelection && selectionProp.start === prevSelection.start && selectionProp.end === prevSelection.end) {
- return;
- }
- return selectionProp;
- });
+ if (!!selection && selectionProp.start === selection.start && selectionProp.end === selection.end) {
+ return;
+ }
+ setSelection(selectionProp);
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, [selectionProp]);
/**
diff --git a/src/components/FlatList/index.tsx b/src/components/FlatList/index.tsx
index d3e0459a11bb..b45a8418d9a3 100644
--- a/src/components/FlatList/index.tsx
+++ b/src/components/FlatList/index.tsx
@@ -150,10 +150,13 @@ function MVCPFlatList({maintainVisibleContentPosition, horizontal = false
if (!isListRenderedRef.current) {
return;
}
- requestAnimationFrame(() => {
+ const animationFrame = requestAnimationFrame(() => {
prepareForMaintainVisibleContentPosition();
setupMutationObserver();
});
+ return () => {
+ cancelAnimationFrame(animationFrame);
+ };
}, [prepareForMaintainVisibleContentPosition, setupMutationObserver]);
const setMergedRef = useMergeRefs(scrollRef, ref);
@@ -176,6 +179,7 @@ function MVCPFlatList({maintainVisibleContentPosition, horizontal = false
const mutationObserver = mutationObserverRef.current;
return () => {
mutationObserver?.disconnect();
+ mutationObserverRef.current = null;
};
}, []);
@@ -199,6 +203,10 @@ function MVCPFlatList({maintainVisibleContentPosition, horizontal = false
ref={onRef}
onLayout={(e) => {
isListRenderedRef.current = true;
+ if (!mutationObserverRef.current) {
+ prepareForMaintainVisibleContentPosition();
+ setupMutationObserver();
+ }
props.onLayout?.(e);
}}
/>
diff --git a/src/components/MoneyRequestAmountInput.tsx b/src/components/MoneyRequestAmountInput.tsx
index d3e5059d6f3f..702e6c384b58 100644
--- a/src/components/MoneyRequestAmountInput.tsx
+++ b/src/components/MoneyRequestAmountInput.tsx
@@ -91,18 +91,6 @@ type MoneyRequestAmountInputProps = {
/** The width of inner content */
contentWidth?: number;
-
- /**
- * Determines whether the amount should be reset.
- */
- shouldResetAmount?: boolean;
-
- /**
- * Callback function triggered when the amount is reset.
- *
- * @param resetValue - A boolean indicating whether the amount should be reset.
- */
- onResetAmount?: (resetValue: boolean) => void;
};
type Selection = {
@@ -139,8 +127,6 @@ function MoneyRequestAmountInput(
shouldKeepUserInput = false,
autoGrow = true,
contentWidth,
- shouldResetAmount,
- onResetAmount,
...props
}: MoneyRequestAmountInputProps,
forwardedRef: ForwardedRef,
@@ -220,21 +206,10 @@ function MoneyRequestAmountInput(
}));
useEffect(() => {
- const frontendAmount = onFormatAmount(amount, currency);
- setCurrentAmount(frontendAmount);
- if (shouldResetAmount) {
- setSelection({
- start: frontendAmount.length,
- end: frontendAmount.length,
- });
- onResetAmount?.(false);
- return;
- }
-
if ((!currency || typeof amount !== 'number' || (formatAmountOnBlur && isTextInputFocused(textInput))) ?? shouldKeepUserInput) {
return;
}
-
+ const frontendAmount = onFormatAmount(amount, currency);
setCurrentAmount(frontendAmount);
// Only update selection if the amount prop was changed from the outside and is not the same as the current amount we just computed
@@ -245,7 +220,10 @@ function MoneyRequestAmountInput(
end: frontendAmount.length,
});
}
- }, [amount, currency, formatAmountOnBlur, shouldKeepUserInput, onFormatAmount, shouldResetAmount, onResetAmount, currentAmount]);
+
+ // we want to re-initialize the state only when the amount changes
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ }, [amount, shouldKeepUserInput]);
// Modifies the amount to match the decimals for changed currency.
useEffect(() => {
diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx
index a9102eb77492..923d149dca10 100755
--- a/src/components/MoneyRequestConfirmationList.tsx
+++ b/src/components/MoneyRequestConfirmationList.tsx
@@ -297,7 +297,7 @@ function MoneyRequestConfirmationList({
const isMerchantRequired = isPolicyExpenseChat && (!isScanRequest || isEditingSplitBill) && shouldShowMerchant;
const isCategoryRequired = !!policy?.requiresCategory;
- const [shouldResetAmount, setShouldResetAmount] = useState(false);
+
useEffect(() => {
if (shouldDisplayFieldError && didConfirmSplit) {
setFormError('iou.error.genericSmartscanFailureMessage');
@@ -475,8 +475,6 @@ function MoneyRequestConfirmationList({
onAmountChange={(value: string) => onSplitShareChange(participantOption.accountID ?? -1, Number(value))}
maxLength={formattedTotalAmount.length}
contentWidth={formattedTotalAmount.length * 8}
- shouldResetAmount={shouldResetAmount}
- onResetAmount={(resetValue) => setShouldResetAmount(resetValue)}
/>
),
}));
@@ -498,7 +496,6 @@ function MoneyRequestConfirmationList({
transaction?.comment?.splits,
transaction?.splitShares,
onSplitShareChange,
- shouldResetAmount,
]);
const isSplitModified = useMemo(() => {
@@ -516,7 +513,6 @@ function MoneyRequestConfirmationList({
{
IOU.resetSplitShares(transaction);
- setShouldResetAmount(true);
}}
accessibilityLabel={CONST.ROLE.BUTTON}
role={CONST.ROLE.BUTTON}
diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx
index 4bd6d4103bee..d0f35990b507 100644
--- a/src/components/ReportActionItem/MoneyReportView.tsx
+++ b/src/components/ReportActionItem/MoneyReportView.tsx
@@ -62,7 +62,7 @@ function MoneyReportView({report, policy}: MoneyReportViewProps) {
{!ReportUtils.isClosedExpenseReportWithNoExpenses(report) && (
<>
- {ReportUtils.reportFieldsEnabled(report) &&
+ {ReportUtils.isPaidGroupPolicyExpenseReport(report) &&
sortedPolicyReportFields.map((reportField) => {
if (ReportUtils.isReportFieldOfTypeTitle(reportField)) {
return null;
diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index a0d73a1b2844..618503e19c33 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -216,15 +216,14 @@ function MoneyRequestView({
merchantTitle = translate('iou.receiptStatusTitle');
amountTitle = translate('iou.receiptStatusTitle');
}
+
const saveBillable = useCallback(
(newBillable: boolean) => {
// If the value hasn't changed, don't request to save changes on the server and just close the modal
if (newBillable === TransactionUtils.getBillable(transaction)) {
- Navigation.dismissModal();
return;
}
IOU.updateMoneyRequestBillable(transaction?.transactionID ?? '-1', report?.reportID, newBillable, policy, policyTagList, policyCategories);
- Navigation.dismissModal();
},
[transaction, report, policy, policyTagList, policyCategories],
);
diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx
index 4a145d4e79e9..a435a5723670 100644
--- a/src/components/ReportActionItem/ReportPreview.tsx
+++ b/src/components/ReportActionItem/ReportPreview.tsx
@@ -128,6 +128,7 @@ function ReportPreview({
const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false);
const [requestType, setRequestType] = useState();
const [nonHeldAmount, fullAmount] = ReportUtils.getNonHeldAndFullAmount(iouReport, policy);
+ const hasOnlyHeldExpenses = ReportUtils.hasOnlyHeldExpenses(iouReport?.reportID ?? '');
const {isSmallScreenWidth} = useWindowDimensions();
const [paymentType, setPaymentType] = useState();
@@ -203,6 +204,18 @@ function ReportPreview({
}
};
+ const getSettlementAmount = () => {
+ if (hasOnlyHeldExpenses) {
+ return '';
+ }
+
+ if (ReportUtils.hasHeldExpenses(iouReport?.reportID) && canAllowSettlement) {
+ return nonHeldAmount;
+ }
+
+ return CurrencyUtils.convertToDisplayString(reimbursableSpend, iouReport?.currency);
+ };
+
const getDisplayAmount = (): string => {
if (totalDisplaySpend) {
return CurrencyUtils.convertToDisplayString(totalDisplaySpend, iouReport?.currency);
@@ -405,7 +418,7 @@ function ReportPreview({
{shouldShowSettlementButton && (
{isHoldMenuVisible && iouReport && requestType !== undefined && (
1;
const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails), isMultipleParticipant);
- const roomWelcomeMessage = ReportUtils.getRoomWelcomeMessage(report);
+ const welcomeMessage = SidebarUtils.getWelcomeMessage(report, policy);
const moneyRequestOptions = ReportUtils.temporary_getMoneyRequestOptions(report, policy, participantAccountIDs);
const additionalText = moneyRequestOptions
.filter((item): item is Exclude => item !== CONST.IOU.TYPE.INVOICE)
@@ -86,47 +87,47 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP
{isPolicyExpenseChat &&
- (policy?.description ? (
+ (welcomeMessage?.messageHtml ? (
{
if (!canEditPolicyDescription) {
return;
}
- Navigation.navigate(ROUTES.WORKSPACE_PROFILE_DESCRIPTION.getRoute(policy.id));
+ Navigation.navigate(ROUTES.WORKSPACE_PROFILE_DESCRIPTION.getRoute(policy?.id ?? '-1'));
}}
style={[styles.renderHTML, canEditPolicyDescription ? styles.cursorPointer : styles.cursorText]}
accessibilityLabel={translate('reportDescriptionPage.roomDescription')}
>
-
+
) : (
- {translate('reportActionsView.beginningOfChatHistoryPolicyExpenseChatPartOne')}
+ {welcomeMessage.phrase1}
{ReportUtils.getDisplayNameForParticipant(report?.ownerAccountID)}
- {translate('reportActionsView.beginningOfChatHistoryPolicyExpenseChatPartTwo')}
+ {welcomeMessage.phrase2}
{ReportUtils.getPolicyName(report)}
- {translate('reportActionsView.beginningOfChatHistoryPolicyExpenseChatPartThree')}
+ {welcomeMessage.phrase3}
))}
{isChatRoom &&
- (report?.description ? (
+ (welcomeMessage?.messageHtml ? (
{
if (ReportUtils.canEditReportDescription(report, policy)) {
- Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(report.reportID));
+ Navigation.navigate(ROUTES.REPORT_DESCRIPTION.getRoute(report?.reportID ?? '-1'));
return;
}
- Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID));
+ Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report?.reportID ?? '-1'));
}}
style={styles.renderHTML}
accessibilityLabel={translate('reportDescriptionPage.roomDescription')}
>
-
+
) : (
- {roomWelcomeMessage.phrase1}
- {roomWelcomeMessage.showReportName && (
+ {welcomeMessage.phrase1}
+ {welcomeMessage.showReportName && (
)}
- {roomWelcomeMessage.phrase2 !== undefined && {roomWelcomeMessage.phrase2}}
+ {welcomeMessage.phrase2 !== undefined && {welcomeMessage.phrase2}}
))}
{isSelfDM && (
- {translate('reportActionsView.beginningOfChatHistorySelfDM')}
+ {welcomeMessage.phrase1}
)}
{isSystemChat && (
- {translate('reportActionsView.beginningOfChatHistorySystemDM')}
+ {welcomeMessage.phrase1}
)}
{isDefault && (
- {translate('reportActionsView.beginningOfChatHistory')}
+ {welcomeMessage.phrase1}
{displayNamesWithTooltips.map(({displayName, accountID}, index) => (
// eslint-disable-next-line react/no-array-index-key
diff --git a/src/components/SelectionList/Search/ActionCell.tsx b/src/components/SelectionList/Search/ActionCell.tsx
index ad77070c1b99..7888a8b26114 100644
--- a/src/components/SelectionList/Search/ActionCell.tsx
+++ b/src/components/SelectionList/Search/ActionCell.tsx
@@ -31,9 +31,19 @@ type ActionCellProps = {
isLargeScreenWidth?: boolean;
isSelected?: boolean;
goToItem: () => void;
+ isChildListItem?: boolean;
+ parentAction?: string;
};
-function ActionCell({action = CONST.SEARCH.ACTION_TYPES.VIEW, transactionID, isLargeScreenWidth = true, isSelected = false, goToItem}: ActionCellProps) {
+function ActionCell({
+ action = CONST.SEARCH.ACTION_TYPES.VIEW,
+ transactionID,
+ isLargeScreenWidth = true,
+ isSelected = false,
+ goToItem,
+ isChildListItem = false,
+ parentAction = '',
+}: ActionCellProps) {
const {translate} = useLocalize();
const theme = useTheme();
const styles = useThemeStyles();
@@ -53,13 +63,11 @@ function ActionCell({action = CONST.SEARCH.ACTION_TYPES.VIEW, transactionID, isL
}
}, [action, currentSearchHash, transactionID]);
- if (!isLargeScreenWidth) {
- return null;
- }
-
const text = translate(actionTranslationsMap[action]);
- if (action === CONST.SEARCH.ACTION_TYPES.PAID || action === CONST.SEARCH.ACTION_TYPES.DONE) {
+ const shouldUseViewAction = action === CONST.SEARCH.ACTION_TYPES.VIEW || (parentAction === CONST.SEARCH.ACTION_TYPES.PAID && action === CONST.SEARCH.ACTION_TYPES.PAID);
+
+ if ((parentAction !== CONST.SEARCH.ACTION_TYPES.PAID && action === CONST.SEARCH.ACTION_TYPES.PAID) || action === CONST.SEARCH.ACTION_TYPES.DONE) {
return (
+ ) : null;
+ }
+
+ if (action === CONST.SEARCH.ACTION_TYPES.REVIEW) {
return (
);
}
-
return (
diff --git a/src/components/SelectionList/Search/ReportListItem.tsx b/src/components/SelectionList/Search/ReportListItem.tsx
index 7653e12c071f..e6358698d414 100644
--- a/src/components/SelectionList/Search/ReportListItem.tsx
+++ b/src/components/SelectionList/Search/ReportListItem.tsx
@@ -5,7 +5,6 @@ import BaseListItem from '@components/SelectionList/BaseListItem';
import type {ListItem, ReportListItemProps, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
import Text from '@components/Text';
import TextWithTooltip from '@components/TextWithTooltip';
-import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
@@ -65,7 +64,6 @@ function ReportListItem({
const reportItem = item as unknown as ReportListItemType;
const styles = useThemeStyles();
- const {translate} = useLocalize();
const {isLargeScreenWidth} = useWindowDimensions();
const StyleUtils = useStyleUtils();
@@ -73,7 +71,7 @@ function ReportListItem({
return;
}
- const listItemPressableStyle = [styles.selectionListPressableItemWrapper, styles.pv3, item.isSelected && styles.activeComponentBG, isFocused && styles.sidebarLinkActive];
+ const listItemPressableStyle = [styles.selectionListPressableItemWrapper, styles.pv3, item.isSelected && styles.activeComponentBG, isFocused && styles.sidebarLinkActive, styles.ph3];
const handleOnButtonPress = () => {
onSelectRow(item);
@@ -148,7 +146,7 @@ function ReportListItem({
onButtonPress={handleOnButtonPress}
/>
)}
-
+
{canSelectMultiple && (
@@ -163,7 +161,6 @@ function ReportListItem({
)}
{reportItem?.reportName}
- {`${reportItem.transactions.length} ${translate('search.groupedExpenses')}`}
@@ -175,22 +172,18 @@ function ReportListItem({
{isLargeScreenWidth && (
- <>
- {/** We add an empty view with type style to align the total with the table header */}
-
-
-
-
- >
+
+
+
)}
-
{reportItem.transactions.map((transaction) => (
{
diff --git a/src/components/SelectionList/Search/TransactionListItemRow.tsx b/src/components/SelectionList/Search/TransactionListItemRow.tsx
index 6a8e4dc52bb7..a4d567d5f8ab 100644
--- a/src/components/SelectionList/Search/TransactionListItemRow.tsx
+++ b/src/components/SelectionList/Search/TransactionListItemRow.tsx
@@ -53,6 +53,7 @@ type TransactionListItemRowProps = {
isDisabled: boolean;
canSelectMultiple: boolean;
isButtonSelected?: boolean;
+ parentAction?: string;
shouldShowTransactionCheckbox?: boolean;
};
@@ -160,7 +161,7 @@ function TotalCell({showTooltip, isLargeScreenWidth, transactionItem}: TotalCell
function TypeCell({transactionItem, isLargeScreenWidth}: TransactionCellProps) {
const theme = useTheme();
- const typeIcon = getTypeIcon(transactionItem.type);
+ const typeIcon = getTypeIcon(transactionItem.transactionType);
return (
+
+
+
-
-
-
diff --git a/src/components/SelectionList/SearchTableHeader.tsx b/src/components/SelectionList/SearchTableHeader.tsx
index 235cff294f8f..4bf1715e0434 100644
--- a/src/components/SelectionList/SearchTableHeader.tsx
+++ b/src/components/SelectionList/SearchTableHeader.tsx
@@ -25,6 +25,12 @@ const SearchColumns: SearchColumnConfig[] = [
shouldShow: () => true,
isColumnSortable: false,
},
+ {
+ columnName: CONST.SEARCH.TABLE_COLUMNS.TYPE,
+ translationKey: 'common.type',
+ shouldShow: () => true,
+ isColumnSortable: false,
+ },
{
columnName: CONST.SEARCH.TABLE_COLUMNS.DATE,
translationKey: 'common.date',
@@ -71,12 +77,6 @@ const SearchColumns: SearchColumnConfig[] = [
translationKey: 'common.total',
shouldShow: () => true,
},
- {
- columnName: CONST.SEARCH.TABLE_COLUMNS.TYPE,
- translationKey: 'common.type',
- shouldShow: () => true,
- isColumnSortable: false,
- },
{
columnName: CONST.SEARCH.TABLE_COLUMNS.ACTION,
translationKey: 'common.action',
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 3170d0ebb777..8761366685ee 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -2642,6 +2642,12 @@ export default {
'We consider a number of factors when calculating your remaining limit: your tenure as a customer, the business-related information you provided during signup, and the available cash in your business bank account. Your remaining limit can fluctuate on a daily basis.',
cashBack: 'Cash back',
cashBackDescription: 'Cash back balance is based on settled monthly Expensify Card spend across your workspace.',
+ issueNewCard: 'Issue new card',
+ finishSetup: 'Finish setup',
+ chooseBankAccount: 'Choose bank account',
+ chooseExistingBank: 'Choose an existing business bank account to pay your Expensify Card balance, or add a new bank account',
+ accountEndingIn: 'Account ending in',
+ addNewBankAccount: 'Add a new bank account',
},
categories: {
deleteCategories: 'Delete categories',
@@ -2670,6 +2676,8 @@ export default {
existingCategoryError: 'A category with this name already exists.',
invalidCategoryName: 'Invalid category name.',
importedFromAccountingSoftware: 'The categories below are imported from your',
+ payrollCode: 'Payroll code',
+ updatePayrollCodeFailureMessage: 'An error occurred while updating the payroll code, please try again.',
glCode: 'GL code',
updateGLCodeFailureMessage: 'An error occurred while updating the GL code, please try again.',
},
@@ -3134,6 +3142,7 @@ export default {
defaultVendor: 'Default vendor',
autoSync: 'Auto-sync',
reimbursedReports: 'Sync reimbursed reports',
+ cardReconciliation: 'Card reconciliation',
reconciliationAccount: 'Reconciliation account',
chooseReconciliationAccount: {
chooseBankAccount: 'Choose the bank account that your Expensify Card payments will be reconciled against.',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 75fb44a5c394..75d23c8691e3 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1383,7 +1383,7 @@ export default {
groupMembersListTitle: 'Directorio de los miembros del grupo.',
lastMemberTitle: '¡Atención!',
lastMemberWarning: 'Ya que eres la última persona aquí, si te vas, este chat quedará inaccesible para todos los miembros. ¿Estás seguro de que quieres salir del chat?',
- defaultReportName: ({displayName}: {displayName: string}) => `Chat de groupo de ${displayName}`,
+ defaultReportName: ({displayName}: {displayName: string}) => `Chat de grupo de ${displayName}`,
},
languagePage: {
language: 'Idioma',
@@ -1546,7 +1546,7 @@ export default {
localTime: 'Hora local',
},
newChatPage: {
- startGroup: 'Grupo de inicio',
+ startGroup: 'Crear grupo',
addToGroup: 'Añadir al grupo',
},
yearPickerPage: {
@@ -2690,6 +2690,12 @@ export default {
'A la hora de calcular tu límite restante, tenemos en cuenta una serie de factores: su antigüedad como cliente, la información relacionada con tu negocio que nos facilitaste al darte de alta y el efectivo disponible en tu cuenta bancaria comercial. Tu límite restante puede fluctuar a diario.',
cashBack: 'Reembolso',
cashBackDescription: 'El saldo de devolución se basa en el gasto mensual realizado con la tarjeta Expensify en tu espacio de trabajo.',
+ issueNewCard: '',
+ finishSetup: 'Terminar configuración',
+ chooseBankAccount: 'Elegir cuenta bancaria',
+ chooseExistingBank: 'Elige una cuenta bancaria comercial existente para pagar el saldo de su Tarjeta Expensify o añade una nueva cuenta bancaria.',
+ accountEndingIn: 'Cuenta terminada en',
+ addNewBankAccount: 'Añadir nueva cuenta bancaria',
},
categories: {
deleteCategories: 'Eliminar categorías',
@@ -2718,6 +2724,8 @@ export default {
existingCategoryError: 'Ya existe una categoría con este nombre.',
invalidCategoryName: 'Lo nombre de la categoría es invalido.',
importedFromAccountingSoftware: 'Categorías importadas desde',
+ payrollCode: 'Código de nómina',
+ updatePayrollCodeFailureMessage: 'Se produjo un error al actualizar el código de nómina, por favor intente nuevamente.',
glCode: 'Código GL',
updateGLCodeFailureMessage: 'Se produjo un error al actualizar el código GL. Inténtelo nuevamente.',
},
@@ -3122,6 +3130,7 @@ export default {
defaultVendor: 'Proveedor predeterminado',
autoSync: 'Autosincronización',
reimbursedReports: 'Sincronizar informes reembolsados',
+ cardReconciliation: 'Conciliación de tarjetas',
reconciliationAccount: 'Cuenta de conciliación',
chooseReconciliationAccount: {
chooseBankAccount: 'Elige la cuenta bancaria con la que se conciliarán los pagos de tu Tarjeta Expensify.',
@@ -3478,7 +3487,7 @@ export default {
markAsComplete: 'Marcar como completada',
markAsIncomplete: 'Marcar como incompleta',
assigneeError: 'Se ha producido un error al asignar esta tarea. Por favor, inténtalo con otro miembro.',
- genericCreateTaskFailureMessage: 'Error inesperado al crear el tarea. Por favor, inténtalo más tarde.',
+ genericCreateTaskFailureMessage: 'Error inesperado al crear la tarea. Por favor, inténtalo más tarde.',
deleteTask: 'Eliminar tarea',
deleteConfirmation: '¿Estás seguro de que quieres eliminar esta tarea?',
},
diff --git a/src/libs/API/parameters/UpdateNetSuiteCustomersJobsParams.ts b/src/libs/API/parameters/UpdateNetSuiteCustomersJobsParams.ts
new file mode 100644
index 000000000000..c29b503a9acf
--- /dev/null
+++ b/src/libs/API/parameters/UpdateNetSuiteCustomersJobsParams.ts
@@ -0,0 +1,10 @@
+import type {ValueOf} from 'type-fest';
+import type CONST from '@src/CONST';
+
+type UpdateNetSuiteCustomersJobsParams = {
+ policyID: string;
+ customersMapping: ValueOf;
+ jobsMapping: ValueOf;
+};
+
+export default UpdateNetSuiteCustomersJobsParams;
diff --git a/src/libs/API/parameters/UpdatePolicyCategoryPayrollCodeParams.ts b/src/libs/API/parameters/UpdatePolicyCategoryPayrollCodeParams.ts
new file mode 100644
index 000000000000..8751832f7cad
--- /dev/null
+++ b/src/libs/API/parameters/UpdatePolicyCategoryPayrollCodeParams.ts
@@ -0,0 +1,7 @@
+type UpdatePolicyCategoryPayrollCodeParams = {
+ policyID: string;
+ categoryName: string;
+ payrollCode: string;
+};
+
+export default UpdatePolicyCategoryPayrollCodeParams;
diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index 6510be4940d4..9601e5a5a249 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -169,6 +169,7 @@ export type {default as CreateWorkspaceCategoriesParams} from './CreateWorkspace
export type {default as RenameWorkspaceCategoriesParams} from './RenameWorkspaceCategoriesParams';
export type {default as SetWorkspaceRequiresCategoryParams} from './SetWorkspaceRequiresCategoryParams';
export type {default as DeleteWorkspaceCategoriesParams} from './DeleteWorkspaceCategoriesParams';
+export type {default as UpdatePolicyCategoryPayrollCodeParams} from './UpdatePolicyCategoryPayrollCodeParams';
export type {default as UpdatePolicyCategoryGLCodeParams} from './UpdatePolicyCategoryGLCodeParams';
export type {default as SetWorkspaceAutoReportingFrequencyParams} from './SetWorkspaceAutoReportingFrequencyParams';
export type {default as SetWorkspaceAutoReportingMonthlyOffsetParams} from './SetWorkspaceAutoReportingMonthlyOffsetParams';
@@ -255,3 +256,4 @@ export type {default as RequestExpensifyCardLimitIncreaseParams} from './Request
export type {default as UpdateNetSuiteGenericTypeParams} from './UpdateNetSuiteGenericTypeParams';
export type {default as UpdateNetSuiteCustomFormIDParams} from './UpdateNetSuiteCustomFormIDParams';
export type {default as UpdateSageIntacctGenericTypeParams} from './UpdateSageIntacctGenericTypeParams';
+export type {default as UpdateNetSuiteCustomersJobsParams} from './UpdateNetSuiteCustomersJobsParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index 341f7c5033e6..fed54694592c 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -130,6 +130,7 @@ const WRITE_COMMANDS = {
CREATE_POLICY_TAG: 'CreatePolicyTag',
RENAME_POLICY_TAG: 'RenamePolicyTag',
SET_WORKSPACE_REQUIRES_CATEGORY: 'SetWorkspaceRequiresCategory',
+ UPDATE_POLICY_CATEGORY_PAYROLL_CODE: 'UpdatePolicyCategoryPayrollCode',
UPDATE_POLICY_CATEGORY_GL_CODE: 'UpdatePolicyCategoryGLCode',
DELETE_WORKSPACE_CATEGORIES: 'DeleteWorkspaceCategories',
DELETE_POLICY_REPORT_FIELD: 'DeletePolicyReportField',
@@ -251,6 +252,7 @@ const WRITE_COMMANDS = {
UPDATE_NETSUITE_LOCATIONS_MAPPING: 'UpdateNetSuiteLocationsMapping',
UPDATE_NETSUITE_CUSTOMERS_MAPPING: 'UpdateNetSuiteCustomersMapping',
UPDATE_NETSUITE_JOBS_MAPPING: 'UpdateNetSuiteJobsMapping',
+ UPDATE_NETSUITE_CUSTOMERS_JOBS_MAPPING: 'UpdateNetSuiteCustomersJobsMapping',
UPDATE_NETSUITE_EXPORTER: 'UpdateNetSuiteExporter',
UPDATE_NETSUITE_EXPORT_DATE: 'UpdateNetSuiteExportDate',
UPDATE_NETSUITE_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION: 'UpdateNetSuiteReimbursableExpensesExportDestination',
@@ -426,6 +428,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.RENAME_WORKSPACE_CATEGORY]: Parameters.RenameWorkspaceCategoriesParams;
[WRITE_COMMANDS.SET_WORKSPACE_REQUIRES_CATEGORY]: Parameters.SetWorkspaceRequiresCategoryParams;
[WRITE_COMMANDS.DELETE_WORKSPACE_CATEGORIES]: Parameters.DeleteWorkspaceCategoriesParams;
+ [WRITE_COMMANDS.UPDATE_POLICY_CATEGORY_PAYROLL_CODE]: Parameters.UpdatePolicyCategoryPayrollCodeParams;
[WRITE_COMMANDS.UPDATE_POLICY_CATEGORY_GL_CODE]: Parameters.UpdatePolicyCategoryGLCodeParams;
[WRITE_COMMANDS.DELETE_POLICY_REPORT_FIELD]: Parameters.DeletePolicyReportField;
[WRITE_COMMANDS.SET_POLICY_REQUIRES_TAG]: Parameters.SetPolicyRequiresTag;
@@ -581,6 +584,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.UPDATE_NETSUITE_LOCATIONS_MAPPING]: Parameters.UpdateNetSuiteGenericTypeParams<'mapping', ValueOf>;
[WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOMERS_MAPPING]: Parameters.UpdateNetSuiteGenericTypeParams<'mapping', ValueOf>;
[WRITE_COMMANDS.UPDATE_NETSUITE_JOBS_MAPPING]: Parameters.UpdateNetSuiteGenericTypeParams<'mapping', ValueOf>;
+ [WRITE_COMMANDS.UPDATE_NETSUITE_CUSTOMERS_JOBS_MAPPING]: Parameters.UpdateNetSuiteCustomersJobsParams;
[WRITE_COMMANDS.UPDATE_NETSUITE_EXPORTER]: Parameters.UpdateNetSuiteGenericTypeParams<'email', string>;
[WRITE_COMMANDS.UPDATE_NETSUITE_EXPORT_DATE]: Parameters.UpdateNetSuiteGenericTypeParams<'value', ValueOf>;
[WRITE_COMMANDS.UPDATE_NETSUITE_REIMBURSABLE_EXPENSES_EXPORT_DESTINATION]: Parameters.UpdateNetSuiteGenericTypeParams<'value', ValueOf>;
diff --git a/src/libs/Fullstory/index.native.ts b/src/libs/Fullstory/index.native.ts
index 4a7551beca77..1adb607bcfca 100644
--- a/src/libs/Fullstory/index.native.ts
+++ b/src/libs/Fullstory/index.native.ts
@@ -1,6 +1,9 @@
import FullStory, {FSPage} from '@fullstory/react-native';
import type {OnyxEntry} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
+import CONST from '@src/CONST';
import * as Environment from '@src/libs/Environment/Environment';
+import ONYXKEYS from '@src/ONYXKEYS';
import type {UserMetadata} from '@src/types/onyx';
/**
@@ -8,6 +11,21 @@ import type {UserMetadata} from '@src/types/onyx';
* Proxy function calls to React-Native lib
* */
const FS = {
+ /**
+ * Initializes FullStory
+ */
+ init: () => {
+ Environment.getEnvironment().then((envName: string) => {
+ // We only want to start fullstory if the app is running in production
+ if (envName !== CONST.ENVIRONMENT.PRODUCTION) {
+ return;
+ }
+ FullStory.restart();
+ const [session] = useOnyx(ONYXKEYS.USER_METADATA);
+ FS.fsIdentify(session);
+ });
+ },
+
/**
* Sets the identity as anonymous using the FullStory library.
*/
diff --git a/src/libs/Fullstory/index.ts b/src/libs/Fullstory/index.ts
index a9c75ad838e9..8419bccabb90 100644
--- a/src/libs/Fullstory/index.ts
+++ b/src/libs/Fullstory/index.ts
@@ -92,6 +92,11 @@ const FS = {
});
}
},
+
+ /**
+ * Init function, created so we're consistent with the native file
+ */
+ init: () => {},
};
export default FS;
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index ce4028b87ea8..7a6701de1a2d 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -237,6 +237,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/members/WorkspaceOwnerChangeErrorPage').default,
[SCREENS.WORKSPACE.CATEGORY_CREATE]: () => require('../../../../pages/workspace/categories/CreateCategoryPage').default,
[SCREENS.WORKSPACE.CATEGORY_EDIT]: () => require('../../../../pages/workspace/categories/EditCategoryPage').default,
+ [SCREENS.WORKSPACE.CATEGORY_PAYROLL_CODE]: () => require('../../../../pages/workspace/categories/CategoryPayrollCodePage').default,
[SCREENS.WORKSPACE.CATEGORY_GL_CODE]: () => require('../../../../pages/workspace/categories/CategoryGLCodePage').default,
[SCREENS.WORKSPACE.CREATE_DISTANCE_RATE]: () => require('../../../../pages/workspace/distanceRates/CreateDistanceRatePage').default,
[SCREENS.WORKSPACE.DISTANCE_RATES_SETTINGS]: () => require('../../../../pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage').default,
@@ -378,7 +379,6 @@ const SettingsModalStackNavigator = createModalStackNavigator
require('../../../../pages/workspace/accounting/netsuite/advanced/NetSuiteApprovalAccountSelectPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.NETSUITE_CUSTOM_FORM_ID]: () => require('../../../../pages/workspace/accounting/netsuite/advanced/NetSuiteCustomFormIDPage').default,
-
[SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PREREQUISITES]: () => require('../../../../pages/workspace/accounting/intacct/SageIntacctPrerequisitesPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.ENTER_SAGE_INTACCT_CREDENTIALS]: () =>
require('../../../../pages/workspace/accounting/intacct/EnterSageIntacctCredentialsPage').default,
@@ -398,7 +398,9 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage').default,
[SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PAYMENT_ACCOUNT]: () =>
require('../../../../pages/workspace/accounting/intacct/advanced/SageIntacctPaymentAccountPage').default,
- [SCREENS.WORKSPACE.ACCOUNTING.RECONCILIATION_ACCOUNT_SETTINGS]: () => require('../../../../pages/workspace/accounting/ReconciliationAccountSettingsPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.CARD_RECONCILIATION]: () => require('../../../../pages/workspace/accounting/reconciliation/CardReconciliationPage').default,
+ [SCREENS.WORKSPACE.ACCOUNTING.RECONCILIATION_ACCOUNT_SETTINGS]: () =>
+ require('../../../../pages/workspace/accounting/reconciliation/ReconciliationAccountSettingsPage').default,
[SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage').default,
[SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage').default,
[SCREENS.WORKSPACE.TAX_EDIT]: () => require('../../../../pages/workspace/taxes/WorkspaceEditTaxPage').default,
@@ -406,6 +408,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/taxes/ValuePage').default,
[SCREENS.WORKSPACE.TAX_CREATE]: () => require('../../../../pages/workspace/taxes/WorkspaceCreateTaxPage').default,
[SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW]: () => require('../../../../pages/workspace/card/issueNew/IssueNewCardPage').default,
+ [SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts').default,
[SCREENS.SETTINGS.SAVE_THE_WORLD]: () => require('../../../../pages/TeachersUnite/SaveTheWorldPage').default,
[SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_PAYMENT_CURRENCY]: () => require('../../../../pages/settings/PaymentCard/ChangeCurrency').default,
[SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_BILLING_CURRENCY]: () => require('../../../../pages/settings/Subscription/PaymentCard/ChangeBillingCurrency').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 0833d6c9f195..2b0f6f763262 100755
--- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
+++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts
@@ -112,6 +112,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = {
SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT,
SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_ADVANCED,
SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PAYMENT_ACCOUNT,
+ SCREENS.WORKSPACE.ACCOUNTING.CARD_RECONCILIATION,
SCREENS.WORKSPACE.ACCOUNTING.RECONCILIATION_ACCOUNT_SETTINGS,
],
[SCREENS.WORKSPACE.TAXES]: [
@@ -140,6 +141,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = {
SCREENS.WORKSPACE.CATEGORIES_SETTINGS,
SCREENS.WORKSPACE.CATEGORY_EDIT,
SCREENS.WORKSPACE.CATEGORY_GL_CODE,
+ SCREENS.WORKSPACE.CATEGORY_PAYROLL_CODE,
],
[SCREENS.WORKSPACE.DISTANCE_RATES]: [
SCREENS.WORKSPACE.CREATE_DISTANCE_RATE,
@@ -158,7 +160,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = {
SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE,
SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE,
],
- [SCREENS.WORKSPACE.EXPENSIFY_CARD]: [SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW],
+ [SCREENS.WORKSPACE.EXPENSIFY_CARD]: [SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW, SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT],
};
export default FULL_SCREEN_TO_RHP_MAPPING;
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 9d5f1355b74c..b8ee32576365 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -447,6 +447,7 @@ const config: LinkingOptions['config'] = {
},
[SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_ADVANCED]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_ADVANCED.route},
[SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PAYMENT_ACCOUNT]: {path: ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_PAYMENT_ACCOUNT.route},
+ [SCREENS.WORKSPACE.ACCOUNTING.CARD_RECONCILIATION]: {path: ROUTES.WORKSPACE_ACCOUNTING_CARD_RECONCILIATION.route},
[SCREENS.WORKSPACE.ACCOUNTING.RECONCILIATION_ACCOUNT_SETTINGS]: {path: ROUTES.WORKSPACE_ACCOUNTING_RECONCILIATION_ACCOUNT_SETTINGS.route},
[SCREENS.WORKSPACE.DESCRIPTION]: {
path: ROUTES.WORKSPACE_PROFILE_DESCRIPTION.route,
@@ -463,6 +464,9 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW]: {
path: ROUTES.WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW.route,
},
+ [SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT]: {
+ path: ROUTES.WORKSPACE_EXPENSIFY_CARD_BANK_ACCOUNT.route,
+ },
[SCREENS.WORKSPACE.RATE_AND_UNIT]: {
path: ROUTES.WORKSPACE_RATE_AND_UNIT.route,
},
@@ -520,6 +524,12 @@ const config: LinkingOptions['config'] = {
categoryName: (categoryName: string) => decodeURIComponent(categoryName),
},
},
+ [SCREENS.WORKSPACE.CATEGORY_PAYROLL_CODE]: {
+ path: ROUTES.WORKSPACE_CATEGORY_PAYROLL_CODE.route,
+ parse: {
+ categoryName: (categoryName: string) => decodeURIComponent(categoryName),
+ },
+ },
[SCREENS.WORKSPACE.CATEGORY_GL_CODE]: {
path: ROUTES.WORKSPACE_CATEGORY_GL_CODE.route,
parse: {
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index a9bc53b01a2c..82a5be980a1d 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -207,6 +207,10 @@ type SettingsNavigatorParamList = {
categoryName: string;
backTo?: Routes;
};
+ [SCREENS.WORKSPACE.CATEGORY_PAYROLL_CODE]: {
+ policyID: string;
+ categoryName: string;
+ };
[SCREENS.WORKSPACE.CATEGORY_GL_CODE]: {
policyID: string;
categoryName: string;
@@ -603,6 +607,10 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_PAYMENT_ACCOUNT]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.ACCOUNTING.CARD_RECONCILIATION]: {
+ policyID: string;
+ connection: ValueOf;
+ };
[SCREENS.WORKSPACE.ACCOUNTING.RECONCILIATION_ACCOUNT_SETTINGS]: {
policyID: string;
connection: ValueOf;
@@ -642,6 +650,9 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW]: {
policyID: string;
};
+ [SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT]: {
+ policyID: string;
+ };
} & ReimbursementAccountNavigatorParamList;
type NewChatNavigatorParamList = {
diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index 97c9a0965f1d..7337ab4dc359 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -727,6 +727,8 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails
lastMessageTextFromReport = ReportUtils.getIOUApprovedMessage(reportID);
} else if (ReportActionUtils.isActionableAddPaymentCard(lastReportAction)) {
lastMessageTextFromReport = ReportActionUtils.getReportActionMessageText(lastReportAction);
+ } else if (lastReportAction?.actionName && ReportActionUtils.isOldDotReportAction(lastReportAction)) {
+ lastMessageTextFromReport = ReportActionUtils.getMessageOfOldDotReportAction(lastReportAction);
}
return lastMessageTextFromReport || (report?.lastMessageText ?? '');
diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts
index faea5965fee4..23aabcf3c6d0 100644
--- a/src/libs/Permissions.ts
+++ b/src/libs/Permissions.ts
@@ -15,10 +15,6 @@ function canUseDefaultRooms(betas: OnyxEntry): boolean {
return !!betas?.includes(CONST.BETAS.DEFAULT_ROOMS) || canUseAllBetas(betas);
}
-function canUseReportFields(betas: OnyxEntry): boolean {
- return !!betas?.includes(CONST.BETAS.REPORT_FIELDS) || canUseAllBetas(betas);
-}
-
function canUseViolations(betas: OnyxEntry): boolean {
return !!betas?.includes(CONST.BETAS.VIOLATIONS) || canUseAllBetas(betas);
}
@@ -73,7 +69,6 @@ export default {
canUseLinkPreviews,
canUseViolations,
canUseDupeDetection,
- canUseReportFields,
canUseP2PDistanceRequests,
canUseWorkflowsDelayedSubmission,
canUseSpotnanaTravel,
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 9fe81b7de5b1..8f1e3c4edc35 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -83,8 +83,6 @@ import * as UserUtils from './UserUtils';
type AvatarRange = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18;
-type WelcomeMessage = {showReportName: boolean; phrase1?: string; phrase2?: string};
-
type SpendBreakdown = {
nonReimbursableSpend: number;
reimbursableSpend: number;
@@ -1270,7 +1268,7 @@ function isClosedExpenseReportWithNoExpenses(report: OnyxEntry): boolean
*/
function isArchivedRoom(report: OnyxInputOrEntry, reportNameValuePairs?: OnyxInputOrEntry): boolean {
if (reportNameValuePairs) {
- return reportNameValuePairs.isArchived;
+ return reportNameValuePairs.private_isArchived;
}
return report?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED && report?.stateNum === CONST.REPORT.STATE_NUM.APPROVED;
@@ -1632,37 +1630,6 @@ function canDeleteReportAction(reportAction: OnyxInputOrEntry, rep
return isActionOwner || isAdmin;
}
-/**
- * Get welcome message based on room type
- */
-function getRoomWelcomeMessage(report: OnyxEntry): WelcomeMessage {
- const welcomeMessage: WelcomeMessage = {showReportName: true};
- const workspaceName = getPolicyName(report);
-
- if (isArchivedRoom(report)) {
- welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfArchivedRoomPartOne');
- welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfArchivedRoomPartTwo');
- } else if (isDomainRoom(report)) {
- welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryDomainRoomPartOne', {domainRoom: report?.reportName ?? ''});
- welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryDomainRoomPartTwo');
- } else if (isAdminRoom(report)) {
- welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAdminRoomPartOne', {workspaceName});
- welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAdminRoomPartTwo');
- } else if (isAnnounceRoom(report)) {
- welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAnnounceRoomPartOne', {workspaceName});
- welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAnnounceRoomPartTwo', {workspaceName});
- } else if (isInvoiceRoom(report)) {
- welcomeMessage.showReportName = false;
- welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryInvoiceRoom');
- } else {
- // Message for user created rooms or other room types.
- welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryUserRoomPartOne');
- welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryUserRoomPartTwo');
- }
-
- return welcomeMessage;
-}
-
/**
* Returns true if Concierge is one of the chat participants (1:1 as well as group chats)
*/
@@ -2494,13 +2461,6 @@ function isHoldCreator(transaction: OnyxEntry, reportID: string): b
return isActionCreator(holdReportAction);
}
-/**
- * Check if report fields are available to use in a report
- */
-function reportFieldsEnabled(report: Report) {
- return Permissions.canUseReportFields(allBetas ?? []) && isPaidGroupPolicyExpenseReport(report);
-}
-
/**
* Given a report field, check if the field can be edited or not.
* For title fields, its considered disabled if `deletable` prop is `true` (https://github.com/Expensify/App/issues/35043#issuecomment-1911275433)
@@ -2602,7 +2562,7 @@ function getMoneyRequestReportName(report: OnyxEntry, policy?: OnyxEntry
const reportFields = isReportSettled ? report?.fieldList : getReportFieldsByPolicyID(report?.policyID ?? '-1');
const titleReportField = getFormulaTypeReportField(reportFields ?? {});
- if (titleReportField && report?.reportName && reportFieldsEnabled(report)) {
+ if (titleReportField && report?.reportName && isPaidGroupPolicyExpenseReport(report)) {
return report.reportName;
}
@@ -4013,7 +3973,7 @@ function buildOptimisticExpenseReport(chatReportID: string, policyID: string, pa
}
const titleReportField = getTitleReportField(getReportFieldsByPolicyID(policyID) ?? {});
- if (!!titleReportField && reportFieldsEnabled(expenseReport)) {
+ if (!!titleReportField && isPaidGroupPolicyExpenseReport(expenseReport)) {
expenseReport.reportName = populateOptimisticReportFormula(titleReportField.defaultValue, expenseReport, policy);
}
@@ -5118,7 +5078,7 @@ function buildOptimisticTaskReport(
title?: string,
description?: string,
policyID: string = CONST.POLICY.OWNER_EMAIL_FAKE,
- notificationPreference: NotificationPreference = CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS,
+ notificationPreference: NotificationPreference = CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN,
): OptimisticTaskReport {
const participants: Participants = {
[ownerAccountID]: {
@@ -7245,7 +7205,6 @@ export {
getReportPreviewMessage,
getReportRecipientAccountIDs,
getRoom,
- getRoomWelcomeMessage,
getRootParentReport,
getRouteFromLink,
getSystemChat,
@@ -7318,6 +7277,7 @@ export {
isIOUReport,
isIOUReportUsingReport,
isJoinRequestInAdminRoom,
+ isDomainRoom,
isMoneyRequest,
isMoneyRequestReport,
isMoneyRequestReportPendingDeletion,
@@ -7368,7 +7328,6 @@ export {
navigateBackAfterDeleteTransaction,
parseReportRouteParams,
parseReportActionHtmlToText,
- reportFieldsEnabled,
requiresAttentionFromCurrentUser,
shouldAutoFocusOnKeyPress,
shouldCreateNewMoneyRequestReport,
diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts
index 7085171a3ce8..4ec3fa9fb314 100644
--- a/src/libs/SearchUtils.ts
+++ b/src/libs/SearchUtils.ts
@@ -24,7 +24,7 @@ const columnNamesToSortingProperty = {
[CONST.SEARCH.TABLE_COLUMNS.MERCHANT]: 'formattedMerchant' as const,
[CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT]: 'formattedTotal' as const,
[CONST.SEARCH.TABLE_COLUMNS.CATEGORY]: 'category' as const,
- [CONST.SEARCH.TABLE_COLUMNS.TYPE]: 'type' as const,
+ [CONST.SEARCH.TABLE_COLUMNS.TYPE]: 'transactionType' as const,
[CONST.SEARCH.TABLE_COLUMNS.ACTION]: 'action' as const,
[CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION]: 'comment' as const,
[CONST.SEARCH.TABLE_COLUMNS.TAX_AMOUNT]: null,
@@ -160,15 +160,20 @@ function getReportSections(data: OnyxTypes.SearchResults['data'], metadata: Onyx
const reportIDToTransactions: Record = {};
for (const key in data) {
if (key.startsWith(ONYXKEYS.COLLECTION.REPORT)) {
- const value = {...data[key]};
- const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${value.reportID}`;
+ const reportItem = {...data[key]};
+ const reportKey = `${ONYXKEYS.COLLECTION.REPORT}${reportItem.reportID}`;
const transactions = reportIDToTransactions[reportKey]?.transactions ?? [];
+ const isExpenseReport = reportItem.type === CONST.REPORT.TYPE.EXPENSE;
+
+ const to = isExpenseReport
+ ? (data[`${ONYXKEYS.COLLECTION.POLICY}${reportItem.policyID}`] as SearchAccountDetails)
+ : (data.personalDetailsList?.[reportItem.managerID] as SearchAccountDetails);
reportIDToTransactions[reportKey] = {
- ...value,
- keyForList: value.reportID,
- from: data.personalDetailsList?.[value.accountID],
- to: data.personalDetailsList?.[value.managerID],
+ ...reportItem,
+ keyForList: reportItem.reportID,
+ from: data.personalDetailsList?.[reportItem.accountID],
+ to,
transactions,
};
} else if (key.startsWith(ONYXKEYS.COLLECTION.TRANSACTION)) {
diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts
index 3151f522d800..1007ba455fa6 100644
--- a/src/libs/SidebarUtils.ts
+++ b/src/libs/SidebarUtils.ts
@@ -18,12 +18,15 @@ import localeCompare from './LocaleCompare';
import * as LocalePhoneNumber from './LocalePhoneNumber';
import * as Localize from './Localize';
import * as OptionsListUtils from './OptionsListUtils';
+import Parser from './Parser';
import Permissions from './Permissions';
import * as PolicyUtils from './PolicyUtils';
import * as ReportActionsUtils from './ReportActionsUtils';
import * as ReportUtils from './ReportUtils';
import * as TaskUtils from './TaskUtils';
+type WelcomeMessage = {showReportName: boolean; phrase1?: string; phrase2?: string; phrase3?: string; messageText?: string; messageHtml?: string};
+
let allBetas: OnyxEntry;
Onyx.connect({
key: ONYXKEYS.BETAS,
@@ -31,6 +34,13 @@ Onyx.connect({
});
const visibleReportActionItems: ReportActions = {};
+let allPersonalDetails: OnyxEntry;
+Onyx.connect({
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ callback: (value) => {
+ allPersonalDetails = value ?? {};
+ },
+});
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
callback: (actions, key) => {
@@ -421,38 +431,12 @@ function getOptionData({
} else {
result.alternateText = lastMessageTextFromReport.length > 0 ? lastMessageText : ReportActionsUtils.getLastVisibleMessage(report.reportID, {}, lastAction)?.lastMessageText;
if (!result.alternateText) {
- result.alternateText = Localize.translate(preferredLocale, 'report.noActivityYet');
+ result.alternateText = ReportUtils.formatReportLastMessageText(getWelcomeMessage(report, policy).messageText ?? Localize.translateLocal('report.noActivityYet'));
}
}
} else {
if (!lastMessageText) {
- if (ReportUtils.isSystemChat(report)) {
- lastMessageText = Localize.translate(preferredLocale, 'reportActionsView.beginningOfChatHistorySystemDM');
- } else if (ReportUtils.isSelfDM(report)) {
- lastMessageText = Localize.translate(preferredLocale, 'reportActionsView.beginningOfChatHistorySelfDM');
- } else {
- // Here we get the beginning of chat history message and append the display name for each user, adding pronouns if there are any.
- // We also add a fullstop after the final name, the word "and" before the final name and commas between all previous names.
- lastMessageText =
- Localize.translate(preferredLocale, 'reportActionsView.beginningOfChatHistory') +
- displayNamesWithTooltips
- .map(({displayName, pronouns}, index) => {
- const formattedText = !pronouns ? displayName : `${displayName} (${pronouns})`;
-
- if (index === displayNamesWithTooltips.length - 1) {
- return `${formattedText}.`;
- }
- if (index === displayNamesWithTooltips.length - 2) {
- return `${formattedText} ${Localize.translate(preferredLocale, 'common.and')}`;
- }
- if (index < displayNamesWithTooltips.length - 2) {
- return `${formattedText},`;
- }
-
- return '';
- })
- .join(' ');
- }
+ lastMessageText = ReportUtils.formatReportLastMessageText(getWelcomeMessage(report, policy).messageText ?? Localize.translateLocal('report.noActivityYet'));
}
result.alternateText =
@@ -493,7 +477,111 @@ function getOptionData({
return result;
}
+function getWelcomeMessage(report: OnyxEntry, policy: OnyxEntry): WelcomeMessage {
+ const welcomeMessage: WelcomeMessage = {showReportName: true};
+ if (ReportUtils.isChatThread(report) || ReportUtils.isTaskReport(report)) {
+ return welcomeMessage;
+ }
+
+ if (ReportUtils.isChatRoom(report)) {
+ return getRoomWelcomeMessage(report);
+ }
+
+ if (ReportUtils.isPolicyExpenseChat(report)) {
+ if (policy?.description) {
+ welcomeMessage.messageHtml = policy.description;
+ welcomeMessage.messageText = Parser.htmlToText(welcomeMessage.messageHtml);
+ } else {
+ welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryPolicyExpenseChatPartOne');
+ welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryPolicyExpenseChatPartTwo');
+ welcomeMessage.phrase3 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryPolicyExpenseChatPartThree');
+ welcomeMessage.messageText = `${welcomeMessage.phrase1} ${ReportUtils.getDisplayNameForParticipant(report?.ownerAccountID)} ${welcomeMessage.phrase2} ${ReportUtils.getPolicyName(
+ report,
+ )} ${welcomeMessage.phrase3}`;
+ }
+ return welcomeMessage;
+ }
+
+ if (ReportUtils.isSelfDM(report)) {
+ welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistorySelfDM');
+ welcomeMessage.messageText = welcomeMessage.phrase1;
+ return welcomeMessage;
+ }
+
+ if (ReportUtils.isSystemChat(report)) {
+ welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistorySystemDM');
+ welcomeMessage.messageText = welcomeMessage.phrase1;
+ return welcomeMessage;
+ }
+
+ welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistory');
+ const participantAccountIDs = ReportUtils.getParticipantsAccountIDsForDisplay(report);
+ const isMultipleParticipant = participantAccountIDs.length > 1;
+ const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(
+ OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, allPersonalDetails),
+ isMultipleParticipant,
+ );
+ const displayNamesWithTooltipsText = displayNamesWithTooltips
+ .map(({displayName, pronouns}, index) => {
+ const formattedText = !pronouns ? displayName : `${displayName} (${pronouns})`;
+
+ if (index === displayNamesWithTooltips.length - 1) {
+ return `${formattedText}.`;
+ }
+ if (index === displayNamesWithTooltips.length - 2) {
+ return `${formattedText} ${Localize.translateLocal('common.and')}`;
+ }
+ if (index < displayNamesWithTooltips.length - 2) {
+ return `${formattedText},`;
+ }
+
+ return '';
+ })
+ .join(' ');
+
+ welcomeMessage.messageText = `${welcomeMessage.phrase1} ${displayNamesWithTooltipsText}`;
+ return welcomeMessage;
+}
+
+/**
+ * Get welcome message based on room type
+ */
+function getRoomWelcomeMessage(report: OnyxEntry): WelcomeMessage {
+ const welcomeMessage: WelcomeMessage = {showReportName: true};
+ const workspaceName = ReportUtils.getPolicyName(report);
+
+ if (report?.description) {
+ welcomeMessage.messageHtml = report.description;
+ welcomeMessage.messageText = Parser.htmlToText(welcomeMessage.messageHtml);
+ return welcomeMessage;
+ }
+ if (ReportUtils.isArchivedRoom(report)) {
+ welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfArchivedRoomPartOne');
+ welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfArchivedRoomPartTwo');
+ } else if (ReportUtils.isDomainRoom(report)) {
+ welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryDomainRoomPartOne', {domainRoom: report?.reportName ?? ''});
+ welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryDomainRoomPartTwo');
+ } else if (ReportUtils.isAdminRoom(report)) {
+ welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAdminRoomPartOne', {workspaceName});
+ welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAdminRoomPartTwo');
+ } else if (ReportUtils.isAnnounceRoom(report)) {
+ welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAnnounceRoomPartOne', {workspaceName});
+ welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryAnnounceRoomPartTwo', {workspaceName});
+ } else if (ReportUtils.isInvoiceRoom(report)) {
+ welcomeMessage.showReportName = false;
+ welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryInvoiceRoom');
+ } else {
+ // Message for user created rooms or other room types.
+ welcomeMessage.phrase1 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryUserRoomPartOne');
+ welcomeMessage.phrase2 = Localize.translateLocal('reportActionsView.beginningOfChatHistoryUserRoomPartTwo');
+ }
+ welcomeMessage.messageText = `${welcomeMessage.phrase1} ${welcomeMessage.showReportName ? ReportUtils.getReportName(report) : ''} ${welcomeMessage.phrase2 ?? ''}`;
+
+ return welcomeMessage;
+}
+
export default {
getOptionData,
getOrderedReportIDs,
+ getWelcomeMessage,
};
diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts
index d72e7d5c83ba..65f958cad8e8 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -352,8 +352,8 @@ function initMoneyRequest(reportID: string, policy: OnyxEntry,
// Add initial empty waypoints when starting a distance expense
if (iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE) {
comment.waypoints = {
- waypoint0: {},
- waypoint1: {},
+ waypoint0: {keyForList: 'start_waypoint'},
+ waypoint1: {keyForList: 'stop_waypoint'},
};
if (!isFromGlobalCreate) {
const customUnitRateID = DistanceRequestUtils.getCustomUnitRateID(reportID);
diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts
index 3af4749569c9..ae8ed41f22c5 100644
--- a/src/libs/actions/Policy/Category.ts
+++ b/src/libs/actions/Policy/Category.ts
@@ -331,7 +331,75 @@ function renamePolicyCategory(policyID: string, policyCategory: {oldName: string
API.write(WRITE_COMMANDS.RENAME_WORKSPACE_CATEGORY, parameters, onyxData);
}
-function updatePolicyCategoryGLCode(policyID: string, categoryName: string, glCode: string) {
+function setPolicyCategoryPayrollCode(policyID: string, categoryName: string, payrollCode: string) {
+ const policyCategoryToUpdate = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName] ?? {};
+
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
+ value: {
+ [categoryName]: {
+ ...policyCategoryToUpdate,
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ pendingFields: {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'Payroll Code': CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
+ },
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'Payroll Code': payrollCode,
+ },
+ },
+ },
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
+ value: {
+ [categoryName]: {
+ ...policyCategoryToUpdate,
+ pendingAction: null,
+ pendingFields: {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'Payroll Code': null,
+ },
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'Payroll Code': payrollCode,
+ },
+ },
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`,
+ value: {
+ [categoryName]: {
+ ...policyCategoryToUpdate,
+ errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.categories.updatePayrollCodeFailureMessage'),
+ pendingAction: null,
+ pendingFields: {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'Payroll Code': null,
+ },
+ },
+ },
+ },
+ ],
+ };
+
+ const parameters = {
+ policyID,
+ categoryName,
+ payrollCode,
+ };
+
+ API.write(WRITE_COMMANDS.UPDATE_POLICY_CATEGORY_PAYROLL_CODE, parameters, onyxData);
+}
+
+function setPolicyCategoryGLCode(policyID: string, categoryName: string, glCode: string) {
const policyCategoryToUpdate = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName] ?? {};
const onyxData: OnyxData = {
@@ -684,9 +752,10 @@ export {
buildOptimisticPolicyRecentlyUsedCategories,
setWorkspaceCategoryEnabled,
setWorkspaceRequiresCategory,
+ setPolicyCategoryPayrollCode,
createPolicyCategory,
renamePolicyCategory,
- updatePolicyCategoryGLCode,
+ setPolicyCategoryGLCode,
clearCategoryErrors,
enablePolicyCategories,
setPolicyDistanceRatesDefaultCategory,
diff --git a/src/libs/actions/connections/NetSuiteCommands.ts b/src/libs/actions/connections/NetSuiteCommands.ts
index adab82a5da8f..8be8d2cb077a 100644
--- a/src/libs/actions/connections/NetSuiteCommands.ts
+++ b/src/libs/actions/connections/NetSuiteCommands.ts
@@ -7,7 +7,7 @@ import {WRITE_COMMANDS} from '@libs/API/types';
import * as ErrorUtils from '@libs/ErrorUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {Connections, NetSuiteCustomFormID, NetSuiteCustomList, NetSuiteCustomSegment} from '@src/types/onyx/Policy';
+import type {Connections, NetSuiteCustomFormID, NetSuiteCustomList, NetSuiteCustomSegment, NetSuiteMappingValues} from '@src/types/onyx/Policy';
import type {OnyxData} from '@src/types/onyx/Request';
type SubsidiaryParam = {
@@ -402,6 +402,118 @@ function updateNetSuiteImportMapping action.reportActionID === transactionThreadReport.parentReportActionID);
}
@@ -178,6 +178,8 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
return report;
}, [caseID, parentReport, report]);
+ const moneyRequestAction = transactionThreadReportID ? requestParentReportAction : parentReportAction;
+
const canModifyTask = Task.canModifyTask(report, session?.accountID ?? -1);
const shouldShowTaskDeleteButton =
isTaskReport &&
@@ -501,9 +503,11 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
? ReportActionsUtils.getOriginalMessage(requestParentReportAction)?.IOUTransactionID ?? ''
: '';
- const canHoldUnholdReportAction = ReportUtils.canHoldUnholdReportAction(parentReportAction);
+ const canHoldUnholdReportAction = ReportUtils.canHoldUnholdReportAction(moneyRequestAction);
const shouldShowHoldAction =
- caseID !== CASES.MONEY_REPORT && (canHoldUnholdReportAction.canHoldRequest || canHoldUnholdReportAction.canUnholdRequest) && !ReportUtils.isArchivedRoom(parentReport);
+ caseID !== CASES.DEFAULT &&
+ (canHoldUnholdReportAction.canHoldRequest || canHoldUnholdReportAction.canUnholdRequest) &&
+ !ReportUtils.isArchivedRoom(transactionThreadReportID ? report : parentReport);
const canJoin = ReportUtils.canJoinChat(report, parentReportAction, policy);
@@ -515,7 +519,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
}
if (isExpenseReport && shouldShowHoldAction) {
- result.push(PromotedActions.hold({isTextHold: canHoldUnholdReportAction.canHoldRequest, reportAction: parentReportAction}));
+ result.push(PromotedActions.hold({isTextHold: canHoldUnholdReportAction.canHoldRequest, reportAction: moneyRequestAction}));
}
if (report) {
@@ -525,7 +529,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD
result.push(PromotedActions.share(report));
return result;
- }, [report, parentReportAction, canJoin, isExpenseReport, shouldShowHoldAction, canHoldUnholdReportAction.canHoldRequest]);
+ }, [report, moneyRequestAction, canJoin, isExpenseReport, shouldShowHoldAction, canHoldUnholdReportAction.canHoldRequest]);
const nameSectionExpenseIOU = (
diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
index f5143fc9ba21..0ff3a50bc0dd 100644
--- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
+++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
@@ -409,6 +409,9 @@ const ContextMenuActions: ContextMenuAction[] = [
Clipboard.setString(Localize.translateLocal('iou.heldExpense'));
} else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.UNHOLD) {
Clipboard.setString(Localize.translateLocal('iou.unheldExpense'));
+ } else if (ReportActionsUtils.isOldDotReportAction(reportAction)) {
+ const oldDotActionMessage = ReportActionsUtils.getMessageOfOldDotReportAction(reportAction);
+ Clipboard.setString(oldDotActionMessage);
} else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.DISMISSED_VIOLATION) {
const originalMessage = ReportActionsUtils.getOriginalMessage(reportAction) as ReportAction['originalMessage'];
const reason = originalMessage?.reason;
diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx
index ae38d4b278e4..96a29a2b0dc7 100644
--- a/src/pages/iou/request/step/IOURequestStepAmount.tsx
+++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx
@@ -280,12 +280,7 @@ function IOURequestStepAmount({
// If the value hasn't changed, don't request to save changes on the server and just close the modal
const transactionCurrency = TransactionUtils.getCurrency(currentTransaction);
if (newAmount === TransactionUtils.getAmount(currentTransaction) && currency === transactionCurrency) {
- if (isSplitBill) {
- Navigation.goBack(backTo);
- } else {
- Navigation.dismissModal();
- }
-
+ navigateBack();
return;
}
@@ -298,12 +293,12 @@ function IOURequestStepAmount({
if (isSplitBill) {
IOU.setDraftSplitTransaction(transactionID, {amount: newAmount, currency, taxCode, taxAmount});
- Navigation.goBack(backTo);
+ navigateBack();
return;
}
IOU.updateMoneyRequestAmountAndCurrency({transactionID, transactionThreadReportID: reportID, currency, amount: newAmount, taxAmount, policy, taxCode});
- Navigation.dismissModal();
+ navigateBack();
};
return (
diff --git a/src/pages/iou/request/step/IOURequestStepDistance.tsx b/src/pages/iou/request/step/IOURequestStepDistance.tsx
index 126d21fa7d53..cebd37d64697 100644
--- a/src/pages/iou/request/step/IOURequestStepDistance.tsx
+++ b/src/pages/iou/request/step/IOURequestStepDistance.tsx
@@ -32,7 +32,7 @@ import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import type * as OnyxTypes from '@src/types/onyx';
import type {Errors} from '@src/types/onyx/OnyxCommon';
-import type {WaypointCollection} from '@src/types/onyx/Transaction';
+import type {Waypoint, WaypointCollection} from '@src/types/onyx/Transaction';
import StepScreenWrapper from './StepScreenWrapper';
import withFullTransactionOrNotFound from './withFullTransactionOrNotFound';
import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound';
@@ -76,7 +76,15 @@ function IOURequestStepDistance({
const {translate} = useLocalize();
const [optimisticWaypoints, setOptimisticWaypoints] = useState(null);
- const waypoints = useMemo(() => optimisticWaypoints ?? transaction?.comment?.waypoints ?? {waypoint0: {}, waypoint1: {}}, [optimisticWaypoints, transaction]);
+ const waypoints = useMemo(
+ () =>
+ optimisticWaypoints ??
+ transaction?.comment?.waypoints ?? {
+ waypoint0: {keyForList: 'start_waypoint'},
+ waypoint1: {keyForList: 'stop_waypoint'},
+ },
+ [optimisticWaypoints, transaction],
+ );
const waypointsList = Object.keys(waypoints);
const previousWaypoints = usePrevious(waypoints);
const numberOfWaypoints = Object.keys(waypoints).length;
@@ -93,7 +101,15 @@ function IOURequestStepDistance({
const isEmptyCoordinates = !transaction?.routes?.route0?.geometry?.coordinates?.length;
const shouldFetchRoute = (isEmptyCoordinates || isRouteAbsentWithoutErrors || haveValidatedWaypointsChanged) && !isLoadingRoute && Object.keys(validatedWaypoints).length > 1;
const [shouldShowAtLeastTwoDifferentWaypointsError, setShouldShowAtLeastTwoDifferentWaypointsError] = useState(false);
- const nonEmptyWaypointsCount = useMemo(() => Object.keys(waypoints).filter((key) => !isEmpty(waypoints[key])).length, [waypoints]);
+ const isWaypointEmpty = (waypoint?: Waypoint) => {
+ if (!waypoint) {
+ return true;
+ }
+ const {keyForList, ...waypointWithoutKey} = waypoint;
+ return isEmpty(waypointWithoutKey);
+ };
+ const nonEmptyWaypointsCount = useMemo(() => Object.keys(waypoints).filter((key) => !isWaypointEmpty(waypoints[key])).length, [waypoints]);
+
const duplicateWaypointsError = useMemo(
() => nonEmptyWaypointsCount >= 2 && Object.keys(validatedWaypoints).length !== nonEmptyWaypointsCount,
[nonEmptyWaypointsCount, validatedWaypoints],
@@ -172,9 +188,9 @@ function IOURequestStepDistance({
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
}, []);
- const navigateBack = () => {
+ const navigateBack = useCallback(() => {
Navigation.goBack(backTo);
- };
+ }, [backTo]);
/**
* Takes the user to the page for editing a specific waypoint
@@ -345,7 +361,7 @@ function IOURequestStepDistance({
data.forEach((waypoint, index) => {
newWaypoints[`waypoint${index}`] = waypoints[waypoint] ?? {};
// Find waypoint that BECOMES empty after dragging
- if (isEmpty(newWaypoints[`waypoint${index}`]) && !isEmpty(waypoints[`waypoint${index}`] ?? {})) {
+ if (isWaypointEmpty(newWaypoints[`waypoint${index}`]) && !isWaypointEmpty(waypoints[`waypoint${index}`])) {
emptyWaypointIndex = index;
}
});
@@ -378,7 +394,7 @@ function IOURequestStepDistance({
const addresses = Object.fromEntries(Object.entries(waypoints).map(([key, waypoint]) => [key, 'address' in waypoint ? waypoint.address : {}]));
const hasRouteChanged = !isEqual(transactionBackup?.routes, transaction?.routes);
if (isEqual(oldAddresses, addresses)) {
- Navigation.dismissModal();
+ navigateBack();
return;
}
IOU.updateMoneyRequestDistance({
@@ -388,12 +404,13 @@ function IOURequestStepDistance({
...(hasRouteChanged ? {routes: transaction?.routes} : {}),
policy,
});
- Navigation.dismissModal();
+ navigateBack();
return;
}
navigateToNextStep();
}, [
+ navigateBack,
duplicateWaypointsError,
atLeastTwoDifferentWaypointsError,
hasRouteError,
diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
index 9301387d5c51..8d888d695b01 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx
@@ -371,7 +371,7 @@ function IOURequestStepScan({
const updateScanAndNavigate = useCallback(
(file: FileObject, source: string) => {
- Navigation.dismissModal();
+ navigateBack();
IOU.replaceReceipt(transactionID, file as File, source);
},
[transactionID],
diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx
index db03e539cdb5..3b0d74821e28 100644
--- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx
+++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx
@@ -230,9 +230,9 @@ function IOURequestStepScan({
});
}
- const navigateBack = () => {
+ const navigateBack = useCallback(() => {
Navigation.goBack(backTo);
- };
+ }, [backTo]);
const navigateToParticipantPage = useCallback(() => {
switch (iouType) {
@@ -417,9 +417,9 @@ function IOURequestStepScan({
const updateScanAndNavigate = useCallback(
(file: FileObject, source: string) => {
IOU.replaceReceipt(transactionID, file as File, source);
- Navigation.dismissModal();
+ navigateBack();
},
- [transactionID],
+ [transactionID, navigateBack],
);
/**
diff --git a/src/pages/iou/request/step/IOURequestStepTag.tsx b/src/pages/iou/request/step/IOURequestStepTag.tsx
index d50b90f6d1ae..2d068e9acb81 100644
--- a/src/pages/iou/request/step/IOURequestStepTag.tsx
+++ b/src/pages/iou/request/step/IOURequestStepTag.tsx
@@ -98,7 +98,7 @@ function IOURequestStepTag({
}
if (isEditing) {
IOU.updateMoneyRequestTag(transactionID, report?.reportID ?? '-1', updatedTag, policy, policyTags, policyCategories);
- Navigation.dismissModal();
+ navigateBack();
return;
}
IOU.setMoneyRequestTag(transactionID, updatedTag);
diff --git a/src/pages/workspace/AccessOrNotFoundWrapper.tsx b/src/pages/workspace/AccessOrNotFoundWrapper.tsx
index 770358335680..09a4c3b49b3f 100644
--- a/src/pages/workspace/AccessOrNotFoundWrapper.tsx
+++ b/src/pages/workspace/AccessOrNotFoundWrapper.tsx
@@ -1,11 +1,12 @@
/* eslint-disable rulesdir/no-negated-variables */
-import React, {useEffect} from 'react';
+import React, {useEffect, useState} from 'react';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import type {FullPageNotFoundViewProps} from '@components/BlockingViews/FullPageNotFoundView';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
+import useNetwork from '@hooks/useNetwork';
import * as IOUUtils from '@libs/IOUUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as PolicyUtils from '@libs/PolicyUtils';
@@ -23,8 +24,8 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject';
const ACCESS_VARIANTS = {
[CONST.POLICY.ACCESS_VARIANTS.PAID]: (policy: OnyxEntry) => PolicyUtils.isPaidGroupPolicy(policy),
- [CONST.POLICY.ACCESS_VARIANTS.ADMIN]: (policy: OnyxEntry, login: string) => PolicyUtils.isPolicyAdmin(policy, login),
[CONST.POLICY.ACCESS_VARIANTS.CONTROL]: (policy: OnyxEntry) => PolicyUtils.isControlPolicy(policy),
+ [CONST.POLICY.ACCESS_VARIANTS.ADMIN]: (policy: OnyxEntry, login: string) => PolicyUtils.isPolicyAdmin(policy, login),
[CONST.IOU.ACCESS_VARIANTS.CREATE]: (
policy: OnyxEntry,
login: string,
@@ -109,6 +110,7 @@ function AccessOrNotFoundWrapper({accessVariants = [], fullPageNotFoundViewProps
const isPolicyIDInRoute = !!policyID?.length;
const isMoneyRequest = !!iouType && IOUUtils.isValidMoneyRequestType(iouType);
const isFromGlobalCreate = isEmptyObject(report?.reportID);
+ const pendingField = featureName ? props.policy?.pendingFields?.[featureName] : undefined;
useEffect(() => {
if (!isPolicyIDInRoute || !isEmptyObject(policy)) {
@@ -124,13 +126,26 @@ function AccessOrNotFoundWrapper({accessVariants = [], fullPageNotFoundViewProps
const isFeatureEnabled = featureName ? PolicyUtils.isPolicyFeatureEnabled(policy, featureName) : true;
+ const [isPolicyFeatureEnabled, setIsPolicyFeatureEnabled] = useState(isFeatureEnabled);
+ const {isOffline} = useNetwork();
+
const isPageAccessible = accessVariants.reduce((acc, variant) => {
const accessFunction = ACCESS_VARIANTS[variant];
return acc && accessFunction(policy, login, report, allPolicies ?? null, iouType);
}, true);
const isPolicyNotAccessible = isEmptyObject(policy) || (Object.keys(policy).length === 1 && !isEmptyObject(policy.errors)) || !policy?.id;
- const shouldShowNotFoundPage = (!isMoneyRequest && !isFromGlobalCreate && isPolicyNotAccessible) || !isPageAccessible || !isFeatureEnabled || shouldBeBlocked;
+ const shouldShowNotFoundPage = (!isMoneyRequest && !isFromGlobalCreate && isPolicyNotAccessible) || !isPageAccessible || !isPolicyFeatureEnabled || shouldBeBlocked;
+
+ // We only update the feature state if it isn't pending.
+ // This is because the feature state changes several times during the creation of a workspace, while we are waiting for a response from the backend.
+ // Without this, we can have unexpectedly have 'Not Found' be shown.
+ useEffect(() => {
+ if (pendingField && !isOffline && !isFeatureEnabled) {
+ return;
+ }
+ setIsPolicyFeatureEnabled(isFeatureEnabled);
+ }, [pendingField, isOffline, isFeatureEnabled]);
if (shouldShowFullScreenLoadingIndicator) {
return ;
diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx
index a3785ccde384..d622be617f22 100644
--- a/src/pages/workspace/WorkspaceInitialPage.tsx
+++ b/src/pages/workspace/WorkspaceInitialPage.tsx
@@ -211,7 +211,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc
const protectedCollectPolicyMenuItems: WorkspaceMenuItem[] = [];
// We only update feature states if they aren't pending.
- // These changes are made to synchronously change feature states along with FeatureEnabledAccessOrNotFoundComponent.
+ // These changes are made to synchronously change feature states along with AccessOrNotFoundWrapperComponent.
useEffect(() => {
setFeatureStates((currentFeatureStates) => {
const newFeatureStates = {} as PolicyFeatureStates;
diff --git a/src/pages/workspace/WorkspaceProfilePage.tsx b/src/pages/workspace/WorkspaceProfilePage.tsx
index aa2683091f6f..96911233bbb2 100644
--- a/src/pages/workspace/WorkspaceProfilePage.tsx
+++ b/src/pages/workspace/WorkspaceProfilePage.tsx
@@ -148,7 +148,6 @@ function WorkspaceProfilePage({policyDraft, policy: policyProp, currencyList = {
shouldShowOfflineIndicatorInWideScreen
shouldShowNonAdmin
icon={Illustrations.House}
- shouldSkipVBBACall={false}
>
{(hasVBA?: boolean) => (
diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx
index 400d54d0e005..890302419969 100644
--- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx
+++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx
@@ -63,6 +63,7 @@ type AccountingIntegration = {
onImportPagePress: () => void;
onExportPagePress: () => void;
onAdvancedPagePress: () => void;
+ onCardReconciliationPagePress: () => void;
};
function accountingIntegrationData(
connectionName: PolicyConnectionName,
@@ -85,6 +86,7 @@ function accountingIntegrationData(
),
onImportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_IMPORT.getRoute(policyID)),
onExportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT.getRoute(policyID)),
+ onCardReconciliationPagePress: () => Navigation.navigate(ROUTES.WORKSPACE_ACCOUNTING_CARD_RECONCILIATION.getRoute(policyID, CONST.POLICY.CONNECTIONS.NAME.QBO)),
onAdvancedPagePress: () => Navigation.navigate(ROUTES.WORKSPACE_ACCOUNTING_QUICKBOOKS_ONLINE_ADVANCED.getRoute(policyID)),
};
case CONST.POLICY.CONNECTIONS.NAME.XERO:
@@ -100,6 +102,7 @@ function accountingIntegrationData(
),
onImportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_IMPORT.getRoute(policyID)),
onExportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_EXPORT.getRoute(policyID)),
+ onCardReconciliationPagePress: () => Navigation.navigate(ROUTES.WORKSPACE_ACCOUNTING_CARD_RECONCILIATION.getRoute(policyID, CONST.POLICY.CONNECTIONS.NAME.XERO)),
onAdvancedPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_ADVANCED.getRoute(policyID)),
};
case CONST.POLICY.CONNECTIONS.NAME.NETSUITE:
@@ -115,6 +118,7 @@ function accountingIntegrationData(
),
onImportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT.getRoute(policyID)),
onExportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_EXPORT.getRoute(policyID)),
+ onCardReconciliationPagePress: () => Navigation.navigate(ROUTES.WORKSPACE_ACCOUNTING_CARD_RECONCILIATION.getRoute(policyID, CONST.POLICY.CONNECTIONS.NAME.NETSUITE)),
onAdvancedPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_ADVANCED.getRoute(policyID)),
};
case CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT:
@@ -130,6 +134,7 @@ function accountingIntegrationData(
),
onImportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_IMPORT.getRoute(policyID)),
onExportPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.getRoute(policyID)),
+ onCardReconciliationPagePress: () => Navigation.navigate(ROUTES.WORKSPACE_ACCOUNTING_CARD_RECONCILIATION.getRoute(policyID, CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT)),
onAdvancedPagePress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_ADVANCED.getRoute(policyID)),
};
default:
@@ -319,6 +324,15 @@ function PolicyAccountingPage({policy, connectionSyncProgress}: PolicyAccounting
wrapperStyle: [styles.sectionMenuItemTopDescription],
onPress: integrationData?.onExportPagePress,
},
+ {
+ icon: Expensicons.ExpensifyCard,
+ iconRight: Expensicons.ArrowRight,
+ shouldShowRightIcon: true,
+ title: translate('workspace.accounting.cardReconciliation'),
+ wrapperStyle: [styles.sectionMenuItemTopDescription],
+ onPress: integrationData?.onCardReconciliationPagePress,
+ },
+
{
icon: Expensicons.Gear,
iconRight: Expensicons.ArrowRight,
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomersOrProjectSelectPage.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomersOrProjectSelectPage.tsx
index cd0932c89410..a39ae852fd18 100644
--- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomersOrProjectSelectPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomersOrProjectSelectPage.tsx
@@ -1,18 +1,18 @@
import React, {useCallback} from 'react';
-import type {ValueOf} from 'type-fest';
import RadioListItem from '@components/SelectionList/RadioListItem';
import type {SelectorType} from '@components/SelectionScreen';
import SelectionScreen from '@components/SelectionScreen';
import useLocalize from '@hooks/useLocalize';
-import {updateNetSuiteImportMapping} from '@libs/actions/connections/NetSuiteCommands';
+import {updateNetSuiteCustomersJobsMapping} from '@libs/actions/connections/NetSuiteCommands';
import Navigation from '@libs/Navigation/Navigation';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
+import type {NetSuiteMappingValues} from '@src/types/onyx/Policy';
type ImportListItem = SelectorType & {
- value: ValueOf;
+ value: NetSuiteMappingValues;
};
function NetSuiteImportCustomersOrProjectSelectPage({policy}: WithPolicyConnectionsProps) {
@@ -39,14 +39,19 @@ function NetSuiteImportCustomersOrProjectSelectPage({policy}: WithPolicyConnecti
const updateImportMapping = useCallback(
({value}: ImportListItem) => {
if (value !== importedValue) {
- if (importJobs !== CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT) {
- updateNetSuiteImportMapping(policyID, CONST.NETSUITE_CONFIG.SYNC_OPTIONS.CUSTOMER_MAPPINGS.JOBS, value, importMappings?.jobs);
- }
- if (importCustomer !== CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT) {
- updateNetSuiteImportMapping(policyID, CONST.NETSUITE_CONFIG.SYNC_OPTIONS.CUSTOMER_MAPPINGS.CUSTOMERS, value, importMappings?.customers);
- }
+ updateNetSuiteCustomersJobsMapping(
+ policyID,
+ {
+ customersMapping: importCustomer !== CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT ? value : CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT,
+ jobsMapping: importJobs !== CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT ? value : CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT,
+ },
+ {
+ customersMapping: importMappings?.customers,
+ jobsMapping: importMappings?.jobs,
+ },
+ );
}
- Navigation.goBack();
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT_CUSTOMERS_OR_PROJECTS.getRoute(policyID));
},
[importCustomer, importJobs, importMappings?.customers, importMappings?.jobs, importedValue, policyID],
);
diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportMappingPage.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportMappingPage.tsx
index c13b8cd7fa7c..f3e93bf5ee3e 100644
--- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportMappingPage.tsx
+++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportMappingPage.tsx
@@ -85,7 +85,7 @@ function NetSuiteImportMappingPage({
updateNetSuiteImportMapping(policyID, importField as keyof typeof importMappings, value, importValue);
}
- Navigation.goBack();
+ Navigation.goBack(ROUTES.POLICY_ACCOUNTING_NETSUITE_IMPORT.getRoute(policyID));
},
[importField, importValue, policyID],
);
diff --git a/src/pages/workspace/accounting/reconciliation/CardReconciliationPage.tsx b/src/pages/workspace/accounting/reconciliation/CardReconciliationPage.tsx
new file mode 100644
index 000000000000..27f31f587944
--- /dev/null
+++ b/src/pages/workspace/accounting/reconciliation/CardReconciliationPage.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
+import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
+import withPolicyConnections from '@pages/workspace/withPolicyConnections';
+import CONST from '@src/CONST';
+
+function CardReconciliationPage({policy}: WithPolicyConnectionsProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+
+ const policyID = policy?.id ?? '-1';
+
+ return (
+
+
+
+
+
+
+ );
+}
+
+CardReconciliationPage.displayName = 'CardReconciliationPage';
+
+export default withPolicyConnections(CardReconciliationPage);
diff --git a/src/pages/workspace/accounting/ReconciliationAccountSettingsPage.tsx b/src/pages/workspace/accounting/reconciliation/ReconciliationAccountSettingsPage.tsx
similarity index 100%
rename from src/pages/workspace/accounting/ReconciliationAccountSettingsPage.tsx
rename to src/pages/workspace/accounting/reconciliation/ReconciliationAccountSettingsPage.tsx
diff --git a/src/pages/workspace/categories/CategoryGLCodePage.tsx b/src/pages/workspace/categories/CategoryGLCodePage.tsx
index 050ced2e497c..2a56184b7ba8 100644
--- a/src/pages/workspace/categories/CategoryGLCodePage.tsx
+++ b/src/pages/workspace/categories/CategoryGLCodePage.tsx
@@ -36,7 +36,7 @@ function CategoryGLCodePage({route}: EditCategoryPageProps) {
(values: FormOnyxValues) => {
const newGLCode = values.glCode.trim();
if (newGLCode !== glCode) {
- Category.updatePolicyCategoryGLCode(route.params.policyID, categoryName, newGLCode);
+ Category.setPolicyCategoryGLCode(route.params.policyID, categoryName, newGLCode);
}
Navigation.goBack();
},
diff --git a/src/pages/workspace/categories/CategoryPayrollCodePage.tsx b/src/pages/workspace/categories/CategoryPayrollCodePage.tsx
new file mode 100644
index 000000000000..22143add373a
--- /dev/null
+++ b/src/pages/workspace/categories/CategoryPayrollCodePage.tsx
@@ -0,0 +1,87 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useCallback} from 'react';
+import {useOnyx} from 'react-native-onyx';
+import FormProvider from '@components/Form/FormProvider';
+import InputWrapper from '@components/Form/InputWrapper';
+import type {FormOnyxValues} from '@components/Form/types';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import TextInput from '@components/TextInput';
+import useAutoFocusInput from '@hooks/useAutoFocusInput';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import type {SettingsNavigatorParamList} from '@navigation/types';
+import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
+import * as Category from '@userActions/Policy/Category';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import INPUT_IDS from '@src/types/form/WorkspaceCategoryForm';
+
+type EditCategoryPageProps = StackScreenProps;
+
+function CategoryPayrollCodePage({route}: EditCategoryPageProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const policyId = route.params.policyID ?? '-1';
+ const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyId}`);
+
+ const categoryName = route.params.categoryName;
+ const payrollCode = policyCategories?.[categoryName]?.['Payroll Code'];
+ const {inputCallbackRef} = useAutoFocusInput();
+
+ const editPayrollCode = useCallback(
+ (values: FormOnyxValues) => {
+ const newPayrollCode = values.payrollCode.trim();
+ if (newPayrollCode !== payrollCode) {
+ Category.setPolicyCategoryPayrollCode(route.params.policyID, categoryName, newPayrollCode);
+ }
+ Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(route.params.policyID, route.params.categoryName));
+ },
+ [categoryName, payrollCode, route.params.categoryName, route.params.policyID],
+ );
+
+ return (
+
+
+ Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(route.params.policyID, route.params.categoryName))}
+ />
+
+
+
+
+
+ );
+}
+
+CategoryPayrollCodePage.displayName = 'CategoryPayrollCodePage';
+
+export default CategoryPayrollCodePage;
diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx
index 54450a1117cd..959347b621fb 100644
--- a/src/pages/workspace/categories/CategorySettingsPage.tsx
+++ b/src/pages/workspace/categories/CategorySettingsPage.tsx
@@ -16,6 +16,7 @@ import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
+import {isControlPolicy} from '@libs/PolicyUtils';
import type {SettingsNavigatorParamList} from '@navigation/types';
import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
@@ -40,6 +41,7 @@ function CategorySettingsPage({route, policyCategories, navigation}: CategorySet
const [deleteCategoryConfirmModalVisible, setDeleteCategoryConfirmModalVisible] = useState(false);
const backTo = route.params?.backTo;
const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${route.params.policyID}`);
+ const shouldDisablePayrollCode = !isControlPolicy(policy);
const policyCategory =
policyCategories?.[route.params.categoryName] ?? Object.values(policyCategories ?? {}).find((category) => category.previousCategoryName === route.params.categoryName);
@@ -142,6 +144,15 @@ function CategorySettingsPage({route, policyCategories, navigation}: CategorySet
shouldShowRightIcon
/>
+
+ Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_PAYROLL_CODE.getRoute(route.params.policyID, policyCategory.name))}
+ shouldShowRightIcon
+ disabled={shouldDisablePayrollCode}
+ />
+
{!isThereAnyAccountingConnection && (