diff --git a/.eslintrc.js b/.eslintrc.js index aa98b7bdc464..9a3a9998c836 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -150,8 +150,14 @@ module.exports = { { selector: ['variable', 'property'], format: ['camelCase', 'UPPER_CASE', 'PascalCase'], + // This filter excludes variables and properties that start with "private_" to make them valid. + // + // Examples: + // - "private_a" → valid + // - "private_test" → valid + // - "private_" → not valid filter: { - regex: '^private_[a-z][a-zA-Z0-9]+$', + regex: '^private_[a-z][a-zA-Z0-9]*$', match: false, }, }, diff --git a/Mobile-Expensify b/Mobile-Expensify index c92e59b1cc08..a426e7b48fbb 160000 --- a/Mobile-Expensify +++ b/Mobile-Expensify @@ -1 +1 @@ -Subproject commit c92e59b1cc08497d1b00adce8fe471cbfe23caa0 +Subproject commit a426e7b48fbb282879e0607a28ecac8d3859403e diff --git a/android/app/build.gradle b/android/app/build.gradle index f2e22693e65b..1787de1ae733 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -110,8 +110,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1009009800 - versionName "9.0.98-0" + versionCode 1009009803 + versionName "9.0.98-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/_data/_routes.yml b/docs/_data/_routes.yml index a8cfd9e87b52..8153030ca97a 100644 --- a/docs/_data/_routes.yml +++ b/docs/_data/_routes.yml @@ -54,11 +54,6 @@ platforms: icon: /assets/images/hand-card.svg description: Explore the perks and benefits of the Expensify Card. - - href: travel - title: Travel - icon: /assets/images/plane.svg - description: Manage all your corporate travel needs with Expensify Travel. - - href: copilots-and-delegates title: Copilots & Delegates icon: /assets/images/envelope-receipt.svg diff --git a/docs/articles/expensify-classic/travel/Approve-travel-expenses.md b/docs/articles/expensify-classic/travel/Approve-travel-expenses.md deleted file mode 100644 index 585e930a3dde..000000000000 --- a/docs/articles/expensify-classic/travel/Approve-travel-expenses.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -title: Approve travel expenses -description: Determine how travel expenses are approved ---- -
- -Travel expenses follow the same approval workflow as other expenses. Admins can configure travel expenses to be approved as soft approval, hard approval or passive approval. The approval method for in-policy and out-of-policy bookings can be managed under the **Policies** section in the **Program** menu for Expensify Travel. - -- **Soft Approval**: Bookings are automatically approved as long as a manager does not decline them within 24 hours. However, this also means that if a manager does not decline the expenses, the arrangements will be booked even if they are out of policy. If a booking is declined, it is refunded based on the voiding/refund terms of the service provider. -- **Hard Approval**: Bookings are automatically canceled/voided and refunded if a manager does not approve them within 24 hours. -- **Passive Approval**: Managers are informed of out-of-policy travel, but there is no action to be taken. - -# Set approval method - -1. Click the **Travel** tab. -2. Click **Book or manage travel**. -3. Click the **Program** tab at the top and select **Policies**. -4. Under General, select approval methods for Flights, Hotels, Cars and Rail. - -![Screenshot of Expensify Travel policy approval settings](https://help.expensify.com/assets/images/Travel_Policy.png){:width="100%"} - -# Approve travel - -![Screenshot of Expensify Travel approval email](https://help.expensify.com/assets/images/Travel_Email.png){:width="100%"} - -## Soft approval - -Once an employee has booked a trip, their approver will receive an email notifying them of the booking with a prompt to decline it if needed. - -- To approve the booking, no action is required. -- To decline the booking, click **Decline booking** within 24 hours. Then click **Deny Booking**. - -## Hard approval - -Once an employee has booked a trip, their approver will receive an email notifying them of the booking with a prompt to accept or decline the booking. - -To approve the booking, click **Approve booking**. Then click **Approve**. -To decline the booking, click **Decline booking**. Then click **Deny**. - -# FAQs - -## Are extended approval windows given for trips booked over the weekend or during company holidays? - -No, the approval window will always be 24 hours from when the trip is booked. - -## How does Expensify Travel handle approvals when the assigned approver is out of office? - -It is recommended to have multiple approvers for travel, as there is no delegated approval for out-of-office approvers. - -## Can travelers upload a document when submitting a trip for approval? - -Travelers are unable to add a document when submitting a trip for approval, but the company can add a ‘reason code’ in the Out of Policy rules that the traveler can complete at checkout. The traveler can then add the document to the expense report in Expensify when submitting the report. - -
- -
- -Travel expenses follow the same approval workflow as other expenses. Admins can configure travel expenses to be approved as soft approval, hard approval or passive approval. The approval method for in-policy and out-of-policy bookings can be managed under the **Policies** section in the **Program** menu for Expensify Travel. - -- **Soft Approval**: Bookings are automatically approved as long as a manager does not decline them within 24 hours. However, this also means that if a manager does not decline the expenses, the arrangements will be booked even if they are out of policy. If a booking is declined, it is refunded based on the voiding/refund terms of the service provider. -- **Hard Approval**: Bookings are automatically canceled/voided and refunded if a manager does not approve them within 24 hours. -- **Passive Approval**: Managers are informed of out-of-policy travel, but there is no action to be taken. - -# Set approval method - -1. Click the + icon in the bottom left menu and select **Book travel**. -2. Click **Book or manage travel**. -3. Click the **Program** tab at the top and select **Policies**. -4. Under General, select approval methods for Flights, Hotels, Cars and Rail. - -![Screenshot of Expensify Travel policy approval settings](https://help.expensify.com/assets/images/Travel_Policy.png){:width="100%"} - -# Approve travel - -![Screenshot of Expensify Travel approval email](https://help.expensify.com/assets/images/Travel_Email.png){:width="100%"} - -## Soft approval - -Once an employee has booked a trip, their approver will receive an email notifying them of the booking with a prompt to decline it if needed. - -- To approve the booking, no action is required. -- To decline the booking, click **Decline booking** within 24 hours. Then click **Deny Booking**. - -## Hard approval - -Once an employee has booked a trip, their approver will receive an email notifying them of the booking with a prompt to accept or decline the booking. - -To approve the booking, click **Approve booking**. Then click **Approve**. -To decline the booking, click **Decline booking**. Then click **Deny**. - -# FAQs - -## Are extended approval windows given for trips booked over the weekend or during company holidays? - -No, the approval window will always be 24 hours from when the trip is booked. - -## How does Expensify Travel handle approvals when the assigned approver is out of office? - -It is recommended to have multiple approvers for travel, as there is no delegated approval for out-of-office approvers. - -## Can travelers upload a document when submitting a trip for approval? - -Travelers are unable to add a document when submitting a trip for approval, but the company can add a ‘reason code’ in the Out of Policy rules that the traveler can complete at checkout. The traveler can then add the document to the expense report in Expensify when submitting the report. - -
diff --git a/docs/articles/expensify-classic/travel/Book-with-Expensify-Travel.md b/docs/articles/expensify-classic/travel/Book-with-Expensify-Travel.md deleted file mode 100644 index f48d069e21dc..000000000000 --- a/docs/articles/expensify-classic/travel/Book-with-Expensify-Travel.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -title: Book with Expensify Travel -description: How to book flights, hotels, cars, trains, and more with Expensify Travel ---- - -Expensify Travel allows members to search and book flights, hotels, cars, and trains globally at the most competitive rates available. - -With Expensify Travel, you can: -- Search and book travel arrangements all in one place -- Book travel for yourself or for someone else -- Get real-time support by chat or phone -- Manage all your T&E expenses in Expensify -- Create specific rules for booking travel -- Enable approvals for out-of-policy trips -- Book with any credit card on the market -- Book with the Expensify Card to get cash back and automatically reconcile transactions - -There is a flat fee of $15 per trip booked. A single trip can include multiple bookings, such as a flight, a hotel, and a car rental. - -# Book travel - -To book travel from the Expensify web app, - -1. Click the **Travel** tab. -2. Click **Book or manage travel**. -3. Use the icons at the top to select the type of travel arrangement you want to book: flights, hotels, cars, or trains. -4. Enter the travel information relevant to the travel arrangement selected (for example, the destination, dates of travel, etc.). -5. Select all the details for the arrangement you want to book. -6. Review the booking details and click **Book Flight / Book Hotel / Book Car / Book Rail** to complete the booking. - -The traveler is emailed an itinerary of the booking. Additionally, -- Their travel details are added to a Trip chat room under their primary workspace. -- An expense report for the trip is created. -- If booked with an Expensify Card, the trip is automatically reconciled. - -{% include info.html %} -The travel itinerary is also emailed to the traveler’s [copilots](https://help.expensify.com/articles/expensify-classic/copilots-and-delegates/Assign-or-remove-a-Copilot), if applicable. -{% include end-info.html %} - -# Edit or cancel travel arrangements - -Click **Get Support** on your emailed travel itinerary for real-time help with the booking. Any modifications, exchanges, or voidings made to a trip via support will incur a $25 booking change fee. diff --git a/docs/articles/expensify-classic/travel/Configure-travel-policy-and-preferences.md b/docs/articles/expensify-classic/travel/Configure-travel-policy-and-preferences.md deleted file mode 100644 index 2b2731fae117..000000000000 --- a/docs/articles/expensify-classic/travel/Configure-travel-policy-and-preferences.md +++ /dev/null @@ -1,165 +0,0 @@ ---- -title: Configure travel policy and preferences -description: Set and update travel policies and preferences for your Expensify Workspace ---- -
- -As a Workspace Admin, you can set travel policies for all travel booked under a workspace, including approval methods, flight booking class, and hotel star preferences. You can also create multiple policies with specific conditions for particular groups of employees and/or non-employees. - -# Create or update a travel policy - -Workspace admins can create different travel policies that provide travel rules for different groups of travelers. When using Expensify Travel for the first time, you will need to create a new Travel Policy. - -To create or update a travel policy, - -1. Click the **Travel** tab. -2. Click **Book or manage travel**. -3. Click the **Program** tab at the top and select **Policies**. -4. Under Employee or Non-employee in the left menu, click **Add new** to create a new policy. -5. Use the **Edit members** section to select the group of employees that belong to this policy. - 1. **To select an existing policy:** Select the policy in the left menu. - 2. **To add a new policy:** Click **Add new** under Employee or Non-employee in the left menu. Then under the Edit members section, select the group of employees that belong to this policy. - -{% include info.html %} -The Company name in Expensify Travel is the domain of the Expensify workspace billing owner -A Legal Entity in Expensify Travel is the equivalent of an Expensify Workspace. -{% include end-info.html %} - -10. Select which travel preferences you want to modify: General, flight, hotel, car, or rail. -11. Click the paperclip icon next to each setting to de-couple it from your default policy. -12. Update the desired settings. - -# General - -Determine the currency and [approval type](https://help.expensify.com/articles/expensify-classic/travel/Approve-travel-expenses#set-approval-method) for different types of travel bookings. You can also upload a PDF of your company travel policy for employees to access when booking travel. - -![Screenshot of Expensify Travel policy approval settings](https://help.expensify.com/assets/images/Travel_Policy.png){:width="100%"} - -# Flight - -Flight preferences include multiple sections with different settings: - -- **Flights not allowed to be booked:** Restrict specific flight classes from being booked by employees. -- **Cabin class settings:** Set the highest cabin class allowed for booking and whether employees can accept cabin upgrades. -- **Budget:** Set a percentage or fare cap for the median or cheapest fares on flights, and a maximum capped price. Flight fares above this cap will be considered out of policy. You can also add customization to allow for different caps for different flight durations. -- **Lowest logical fare settings:** Set a preference for layovers, number of stops, flight time window, and airport connection changes. Expensify automatically takes these preferences into account when showing the lowest logical travel fare. -- **Add Ons and Preferences:** Select what types of add-ons your employees can book, including: - - Additional baggage - - Early check-in - - Seat preference - - No add-ons allowed -- **Booking windows:** Add a time limit to prevent employees from booking less than a certain number of days in advance to prohibit bookings too close to the flight time. -- **Refundable/changeable tickets:** Allow employees to book fully or partially refundable fares. To allow all options, you can leave this as the default option. -- **Maximum CO2 per kilometer:** If the CO2 per passenger km exceeds this threshold, the flight will be marked as out of policy. You can leave the entry blank if you do not wish to use this policy. -- **Out of policy reason codes:** If enabled, travelers will be asked to enter a reason code for an out-of-policy flight booking. This gives them a way to provide context for why the booking is still being placed. You can also modify the reason codes by clicking Manage reason codes below the toggle. - -# Hotel - -- **Restrict booking by keyword:** Keywords entered here will be compared to the hotel rate description. If any of the keywords are found, the hotel rate will be restricted. You also have the option to provide a reason why each keyword is restricted. -- **Hotel rates not allowed to be booked:** Restrict specific hotel rates (such as non-refundable, prepaid, and requires deposit) from being booked by employees. This overrides all other hotel policy rules. You also have the option to add a reason why these options aren’t available. -- **Maximum price:** Set a maximum price per night for bookings. You can also select the Customizations by location option to set a maximum price for each location. -- **Booking window:** Add a time limit to prevent employees from booking less than a certain number of days in advance to prohibit bookings too close to the flight time. -- **Cancellation policy:** Allow your employees to book fully or partially refundable rooms. To allow all options, you can leave this as the default option. -- **Experience:** Set hotel ratings that are in and out of policy. -- **Nightly median rate:** Determine which hotels to consider when calculating the median price. You can set a radius around the search location and include/disclude hotels above or below a certain rating. -- **Out of policy reason codes:** If enabled, travelers will be asked to enter a reason code for an out-of-policy hotel booking. This gives them a way to provide context for why the booking is still being placed. You can also modify the reason codes by clicking Manage reason codes below the toggle. - -# Car - -- **Car categories not allowed:** Restrict specific car types from being booked by employees. This overrides any other car policy. -- **Car categories in policy:** Define the types of cars that are in policy. If you do not list a specific car category, it will still be booked as long as it isn’t included in the Car categories not allowed setting. However, the booking will be classed as out of policy. -- **Car engine types not allowed:** Restrict specific engine types from being booked by employees regardless of other policy settings. -- **Maximum price:** Set a daily price cap per car (not including taxes and fees). -- **Out of policy reason codes:** If enabled, travelers will be asked to enter a reason code for an out-of-policy car booking. This gives them a way to provide context for why the booking is still being placed. You can also modify the reason codes by clicking Manage reason codes below the toggle. - -# Rail - -- **Maximum price:** Set a maximum price per booking or customise by rail trip duration. -- **Highest travel class:** Set a maximum travel class per booking or customise by rail trip duration. -- **Booking window:** Add a time limit to prevent employees from booking less than a certain number of days in advance to prohibit bookings too close to the journey time. -- **Out-of-policy reason code for rail:** If enabled, travelers will be asked to enter a reason code for an out-of-policy rail booking. This gives them a way to provide context for why the booking is still being placed. You can also modify the reason codes by clicking Manage reason codes below the toggle. - -# FAQ - -How do travel policy rules interact with Expensify’s [approval flows](https://help.expensify.com/articles/expensify-classic/travel/Approve-travel-expenses)? - -Travel policy rules define what can and can’t be booked by your employees while they’re making the booking. Once a booking is placed and the travel itself is [approved](https://help.expensify.com/articles/expensify-classic/travel/Approve-travel-expenses), the expense will appear in Expensify. It will then be coded, submitted, pushed through the existing expense approval process as defined by your workspace, and exported to your preferred accounting platform (if applicable). - -
- -
- -As a Workspace Admin, you can set travel policies for all travel booked under your workspace, including approval methods, flight booking class, and hotel star preferences. You can also create multiple policies with specific conditions for particular groups of employees and/or non-employees. - -# Create a travel policy - -Workspace admins can create different travel policies that provide travel rules for different groups of travelers. When using Expensify Travel for the first time, you will need to create a new Travel Policy. - -To create or update a travel policy, - -1. Click the + icon in the bottom left menu and select **Book travel**. -2. Click **Book or manage travel**. -3. Click the **Program** tab at the top and select **Policies**. -4. Under Employee or Non-employee in the left menu, click **Add new** to create a new policy. -5. Use the **Edit members** section to select the group of employees that belong to this policy. - 1. **To select an existing policy:** Select the policy in the left menu. - 2. **To add a new policy:** Click **Add new** under Employee or Non-employee in the left menu. Then under the Edit members section, select the group of employees that belong to this policy. - -{% include info.html %} -A Legal Entity in Expensify Travel is the equivalent of an Expensify Workspace. -{% include end-info.html %} - -10. Select which travel preferences you want to modify: General, flight, hotel, car, or rail. -11. Click the paperclip icon next to each setting to de-couple it from your default policy. -12. Update the desired settings. - -# General - -Determine the currency and [approval type](https://help.expensify.com/articles/expensify-classic/travel/Approve-travel-expenses#set-approval-method) for different types of travel bookings. You can also upload a PDF of your company travel policy for employees to access when booking travel. - -![Screenshot of Expensify Travel policy approval settings](https://help.expensify.com/assets/images/Travel_Policy.png){:width="100%"} - -# Flight - -Flight preferences include multiple sections with different settings: - -- **Flights not allowed to be booked:** Restrict specific flight classes from being booked by employees. -- **Cabin class settings:** Set the highest cabin class allowed for booking and whether employees can accept cabin upgrades. -- **Budget:** Set a percentage or fare cap for the median or cheapest fares on flights, and a maximum capped price. Flight fares above this cap will be considered out of policy. You can also add customization to allow for different caps for different flight durations. -- **Lowest logical fare settings:** Set a preference for layovers, number of stops, flight time window, and airport connection changes. Expensify automatically takes these preferences into account when showing the lowest logical travel fare. -- **Add Ons and Preferences:** Select what types of add-ons your employees can book, including: - - Additional baggage - - Early check-in - - Seat preference - - No add-ons allowed -- **Booking windows:** Add a time limit to prevent employees from booking less than a certain number of days in advance to prohibit bookings too close to the flight time. -- **Refundable/changeable tickets:** Allow employees to book fully or partially refundable fares. To allow all options, you can leave this as the default option. -- **Maximum CO2 per kilometer:** If the CO2 per passenger km exceeds this threshold, the flight will be marked as out of policy. You can leave the entry blank if you do not wish to use this policy. -- **Out of policy reason codes:** If enabled, travelers will be asked to enter a reason code for an out-of-policy flight booking. This gives them a way to provide context for why the booking is still being placed. You can also modify the reason codes by clicking Manage reason codes below the toggle. - -# Hotel - -- **Restrict booking by keyword:** Keywords entered here will be compared to the hotel rate description. If any of the keywords are found, the hotel rate will be restricted. You also have the option to provide a reason why each keyword is restricted. -- **Hotel rates not allowed to be booked:** Restrict specific hotel rates (such as non-refundable, prepaid, and requires deposit) from being booked by employees. This overrides all other hotel policy rules. You also have the option to add a reason why these options aren’t available. -- **Maximum price:** Set a maximum price per night for bookings. You can also select the Customizations by location option to set a maximum price for each location. -- **Booking window:** Add a time limit to prevent employees from booking less than a certain number of days in advance to prohibit bookings too close to the flight time. -- **Cancellation policy:** Allow your employees to book fully or partially refundable rooms. To allow all options, you can leave this as the default option. -- **Experience:** Set hotel ratings that are in and out of policy. -- **Nightly median rate:** Determine which hotels to consider when calculating the median price. You can set a radius around the search location and include/disclude hotels above or below a certain rating. -- **Out of policy reason codes:** If enabled, travelers will be asked to enter a reason code for an out-of-policy hotel booking. This gives them a way to provide context for why the booking is still being placed. You can also modify the reason codes by clicking Manage reason codes below the toggle. - -# Car - -- **Car categories not allowed:** Restrict specific car types from being booked by employees. This overrides any other car policy. -- **Car categories in policy:** Define the types of cars that are in policy. If you do not list a specific car category, it will still be booked as long as it isn’t included in the Car categories not allowed setting. However, the booking will be classed as out of policy. -- **Car engine types not allowed:** Restrict specific engine types from being booked by employees regardless of other policy settings. -- **Maximum price:** Set a daily price cap per car (not including taxes and fees). -- **Out of policy reason codes:** If enabled, travelers will be asked to enter a reason code for an out-of-policy car booking. This gives them a way to provide context for why the booking is still being placed. You can also modify the reason codes by clicking Manage reason codes below the toggle. - -# FAQ - -How do travel policy rules interact with Expensify’s [approval flows](https://help.expensify.com/articles/expensify-classic/travel/Approve-travel-expenses)? - -Travel policy rules define what can and can’t be booked by your employees while they’re making the booking. Once a booking is placed and the travel itself is [approved](https://help.expensify.com/articles/expensify-classic/travel/Approve-travel-expenses), the expense will appear in Expensify. It will then be coded, submitted, pushed through the existing expense approval process as defined by your workspace, and exported to your preferred accounting platform (if applicable). - -
diff --git a/docs/articles/expensify-classic/travel/Track-Travel-Analytics.md b/docs/articles/expensify-classic/travel/Track-Travel-Analytics.md deleted file mode 100644 index 18506215635e..000000000000 --- a/docs/articles/expensify-classic/travel/Track-Travel-Analytics.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: Track Travel Analytics -description: Get insight into company travel bookings to ensure real-time duty of care reporting and travel policy compliance. ---- -
- -Expensify Travel provides insights into company travel bookings to ensure real-time duty of care reporting and travel policy compliance. These analytics help Workspace Admins: - -- See global employee locations with a real-time employee location map -- Analyze travel spend based on details such as trip, traveler, or carrier -- Monitor booking trends and adherence to travel policy compliance -- Generate environmental, social, and governance (ESG) reporting - -To view your analytics, - -1. Click the + icon in the bottom left menu and select **Book travel**. -2. Click **Book travel**. -3. Click the **Analytics** tab at the top of the screen. - -From here, you can see a variety of reports, including the Duty of Care report, Spend, and ESG metrics. - -## Duty of Care report - -Duty of care is a legal obligation for employers to safeguard the health, safety, and well-being of their employees both in the office and during business trips. With Expensify’s Duty of Care analytics, you can view a global map showing real-time employee locations. - -1. Click the **Analytics** tab at the top and select Travelers. -2. Use the map to see employee locations. If desired, you can use the filters above the map to show past and future trips, or travel booked to specific locations. - -## Spend and compliance report - -Workspace Admins can analyze travel data based on a variety of trip, traveler, and compliance attributes. - -1. Click the **Analytics** tab at the top and select Company Reports. -2. Review the overview data, or select a specific report from the left menu. -3. Click the three dot menu on the right of the screen to download the report as a PDF. - -## ESG report - -Expensify Travel provides various ESG metrics, including carbon footprint analysis, sustainability scores, and ethical travel spending. - -1. Click the **Analytics** tab at the top and select Company Reports. -2. Click **Air Manifest** in the left menu. -3. Review the CO2 Emissions column in the table. - -
diff --git a/docs/articles/new-expensify/connections/netsuite/Netsuite-Troubleshooting.md b/docs/articles/new-expensify/connections/netsuite/Netsuite-Troubleshooting.md index 15a74cf925fa..8f8d076700a4 100644 --- a/docs/articles/new-expensify/connections/netsuite/Netsuite-Troubleshooting.md +++ b/docs/articles/new-expensify/connections/netsuite/Netsuite-Troubleshooting.md @@ -1,442 +1,366 @@ --- -title: Netsuite Troubleshooting -description: Troubleshoot common NetSuite sync and export errors. +title: NetSuite Troubleshooting +description: Troubleshoot common NetSuite sync and export errors. --- -Synchronizing and exporting data between Expensify and NetSuite can streamline your financial processes, but occasionally, users may encounter errors that prevent a smooth integration. These errors often arise from discrepancies in settings, missing data, or configuration issues within NetSuite or Expensify. +Synchronizing and exporting data between Expensify and NetSuite helps streamline financial processes, but errors can occasionally disrupt the integration. These errors typically arise from missing data, incorrect settings, or configuration issues in NetSuite or Expensify. -This troubleshooting guide aims to help you identify and resolve common sync and export errors, ensuring a seamless connection between your financial management systems. By following the step-by-step solutions provided for each specific error, you can quickly address issues and maintain accurate and efficient expense reporting and data management. +This guide provides step-by-step solutions for resolving common NetSuite sync and export errors, ensuring accurate and efficient expense reporting and data management. -# ExpensiError NS0005: Please enter value(s) for Department, Location or Class - -**Why does this happen?** - -This error occurs when the classification (like Location) is required at the header level of your transaction form in NetSuite. - -For expense reports and journal entries, NetSuite uses classifications from the employee record default. Expensify only exports this information at the line item level. - -For vendor bills, these classifications can't be mandatory because we use the vendor record instead of the employee record, and vendor records don’t have default classifications. - -## How to fix it for vendor bills +--- +# ExpensiError NS0005: Please Enter Value(s) for Department, Location, or Class -Note: When exporting as a Vendor Bill, we pull from the vendor record, not the employee. Therefore, employee defaults don’t apply at the header ("main") level. This error appears if your NetSuite transaction form requires those fields. +## Why does this happen? +This error occurs when NetSuite requires classifications (Department, Location, or Class) at the header level, but Expensify only exports them at the line item level. +## Fix for Vendor Bills 1. Go to **Customization > Forms > Transaction Forms**. -2. Click **"Edit"** on your preferred vendor bill form. -3. Go to **Screen Fields > Main**. -4. Uncheck both **"Show"** and **"Mandatory"** for the listed fields in your error message. -5. Sync the NetSuite connection in Expensify (**Settings > Workspaces > Workspace Name > Accounting > three-dot menu > Sync Now**.) -6. Attempt the export again by clicking on Search, then clicking the Approved (company card expenses) or Paid (reimbursable expenses) filter. -Click on the report in question and it will open in the right-hand panel. -Click on Export to NetSuite to try to export again. - -## How to fix it for journal entries and expense reports - -Note: If you see this error when exporting a Journal Entry or Expense Report, it might be because the report submitter doesn’t have default settings for Departments, Classes, or Locations. +2. Click **Edit** on your preferred Vendor Bill form. +3. Navigate to **Screen Fields > Main**. +4. Uncheck **Show** and **Mandatory** for the fields listed in the error message. +5. Sync NetSuite in Expensify: **Settings > Workspaces > Workspace Name > Accounting > Three-dot menu > Sync Now**. +6. Reattempt the export. +## Fix for Journal Entries and Expense Reports 1. Go to **Lists > Employees** in NetSuite. -2. Click **"Edit"** next to the employee's name who submitted the report. -3. Scroll down to the **Classification** section. -4. Select a default **Department**, **Class**, and **Location** for the employee. -5. Click **Save**. -6. Sync the NetSuite connection in Expensify (**Settings > Workspaces > Workspace Name > Accounting > three-dot menu > Sync Now**.) -7. Attempt the export again by clicking on Search, then clicking the Approved (company card expenses) or Paid (reimbursable expenses) filter. -Click on the report in question and it will open in the right-hand panel. -Click on Export to NetSuite to try to export again. - - -# ExpensiError NS0012: Currency Does Not Exist In NetSuite - -**Why does this happen? (scenario 1)** - -When dealing with foreign transactions, Expensify sends the conversion rate and currency of the original expense to NetSuite. If the currency isn't listed in your NetSuite subsidiary, you'll see an error message saying the currency does not exist in NetSuite. - -## How to fix it - -1. Ensure the currency in Expensify matches what's in your NetSuite subsidiary. -2. If you see an error saying 'The currency X does not exist in NetSuite', re-sync your connection to NetSuite through the workspace admin section in Expensify. -3. Attempt the export again by clicking on Search, then clicking the Approved (company card expenses) or Paid (reimbursable expenses) filter. -Click on the report in question and it will open in the right-hand panel. -Click on Export to NetSuite to try to export again. - -**Why does this happen? (scenario 2)** - -This error can happen if you’re using a non-OneWorld NetSuite instance and exporting a currency other than EUR, GBP, USD, or CAD. - -## How to fix it - -1. Head to NetSuite. -2. Go to **Setup > Enable Features**. -3. Check the **Multiple Currencies** box. - -Once you've done this, you can add the offending currency by searching **New Currencies** in the NetSuite global search. - -# ExpensiError NS0021: Invalid tax code reference key - -**Why does this happen?** - -This error usually indicates an issue with the Tax Group settings in NetSuite, which can arise from several sources. - -## How to fix it - -If a Tax Code on Sales Transactions is mapped to a Tax Group, an error will occur. To fix this, the Tax Code must be mapped to a Tax Code on Purchase Transactions instead. - -To verify if a Tax Code is for Sales or Purchase transactions, view the relevant Tax Code(s). - -**For Australian Taxes:** - -Ensure your Tax Groups are mapped correctly: -- **GST 10%** to **NCT-AU** (not the Sales Transaction Tax Code TS-AU) -- **No GST 0%** to **NCF-AU** (not the Sales Transaction Tax Code TFS-AU) - -### Tax Group Type -Tax Groups can represent different types of taxes. For compatibility with Expensify, ensure the tax type is set to GST/VAT. - -### Enable Tax Groups -Some subsidiaries require you to enable Tax Groups. Go to **Set Up Taxes** for the subsidiary's country and ensure the Tax Code lists include both Tax Codes and Tax Groups. - -# ExpensiError NS0023: Employee Does Not Exist in NetSuite (Invalid Employee) - -**Why does this happen?** - -This can happen if the employee’s subsidiary in NetSuite doesn’t match the subsidiary selected for the connection in Expensify. - -## How to fix it +2. Click **Edit** next to the employee who submitted the report. +3. Scroll to **Classification** and assign a **Department**, **Class**, and **Location**. +4. Click **Save**. +5. Sync NetSuite in Expensify. +6. Reattempt the export. -1. **Check the Employee's Subsidiary** - - Go to the employee record in NetSuite. - - Confirm the employee's subsidiary matches what’s listed as the subsidiary at the workspace level. - - To find this in Expensify navigate to **Settings > Workspaces > click workspace name > Accounting > Subsidiary**. - - If the subsidiaries don’t match, update the subsidiary in Expensify to match what’s listed in NetSuite. - - Sync the NetSuite connection in Expensify (**Settings > Workspaces > click workspace name > Accounting > three-dot menu > Sync Now**.) -2. **Verify Access Restrictions:** - - Go to **Lists > Employees > Employees > [Select Employee] > Edit > Access**. - - Uncheck **Restrict Access to Expensify**. -3. **Additional Checks:** - - Ensure the email on the employee record in NetSuite matches the email address of the report submitter in Expensify. - - In NetSuite, make sure the employee's hire date is in the past and/or the termination date is in the future. -4. **Currency Match for Journal Entries:** - - If exporting as Journal Entries, ensure the currency for the NetSuite employee record, NetSuite subsidiary, and Expensify workspace all match. - - In NetSuite, go to the **Human Resources** tab > **Expense Report Currencies**, and add the subsidiary/policy currency if necessary. - -# ExpensiError NS0085: Expense Does Not Have Appropriate Permissions for Settings an Exchange Rate in NetSuite - -**Why does this happen?** - -This error occurs when the exchange rate settings in NetSuite aren't updated correctly. - -## How to fix it - -1. In NetSuite, go to Customization > Forms > Transaction Forms. -2. Search for the form type that the report is being exported as (Expense Report, Journal Entry, or Vendor Bill) and click Edit next to the form that has the Preferred checkbox checked. - - **For Expense Reports:** - - Go to Screen Fields > Expenses (the Expenses tab farthest to the right). - - Ensure the Exchange Rate field under the Description column has the Show checkbox checked. - - **For Vendor Bills:** - - Go to Screen Fields > Main. - - Ensure the Exchange Rate field under the Description column has the Show checkbox checked. - - **For Journal Entries:** - - Go to Screen Fields > Lines. - - Ensure the Exchange Rate field under the Description column has the Show checkbox checked. - - Go to Screen Fields > Main and ensure the Show checkbox is checked in the Exchange Rate field under the Description column. -3. Sync the NetSuite connection in Expensify (**Settings > Workspaces > Workspace Name > Accounting > three-dot menu > Sync Now**.) -4. Attempt the export again by clicking on Search, then clicking the Approved (company card expenses) or Paid (reimbursable expenses) filter. -Click on the report in question and it will open in the right-hand panel. -Click on Export to NetSuite to try to export again. - -# ExpensiError NS0079: The Transaction Date is Not Within the Date Range of Your Accounting Period - -**Why does this happen?** - -The transaction date you specified is not within the date range of your accounting period. When the posting period settings in NetSuite are not configured to allow a transaction date outside the posting period, you can't export a report to the next open period, which is why you’ll run into this error. - -## How to fix it - -1. In NetSuite, navigate to Setup > Accounting > Accounting Preferences. -2. Under the General Ledger section, ensure the field Allow Transaction Date Outside of the Posting Period is set to Warn. -3. Then, choose whether to export your reports to the First Open Period or the Current Period. +--- +# ExpensiError NS0012: Currency Does Not Exist in NetSuite -**Additionally, ensure the Export to Next Open Period feature is enabled within Expensify:** -1. Navigate to **Settings > Workspaces > Workspace Name > Accounting > Export. -2. Scroll down and confirm that the toggle for **Export to next open period** is enabled. +## Why does this happen? +This occurs when: +- Expensify sends a currency not listed in your NetSuite subsidiary. +- You are using a non-OneWorld NetSuite instance and exporting a currency other than EUR, GBP, USD, or CAD. -If any configuration settings are updated on the NetSuite connection, be sure to sync the connection before trying the export again. +## How to Fix It +1. Ensure the currency in Expensify matches NetSuite. +2. Sync NetSuite in Expensify. +3. Enable **Multiple Currencies** in NetSuite: **Setup > Enable Features**. +4. Add the missing currency via **New Currencies** in the NetSuite global search. +5. Reattempt the export. -# ExpensiError NS0055: The Vendor You are Trying to Export to Does Not Have Access to the Currency X +--- +# ExpensiError NS0021: Invalid Tax Code Reference Key -**Why does this happen?** +## Why does this happen? +This error usually results from an issue with Tax Group settings in NetSuite, such as a Tax Code being mapped incorrectly. -This error occurs when a vendor tied to a report in Expensify does not have access to a currency on the report in NetSuite. The vendor used in NetSuite depends on the type of expenses on the report you're exporting. +## How to Fix It +1. Verify that Tax Codes on Sales Transactions are not mapped to Tax Groups. +2. Ensure the correct Tax Code is assigned to Purchase Transactions. +3. For Australian users: + - **GST 10%** should be mapped to **NCT-AU** (not TS-AU). + - **No GST 0%** should be mapped to **NCF-AU** (not TFS-AU). +4. Ensure Tax Groups are enabled under **Set Up Taxes** in NetSuite. +5. Reattempt the export. -- For **reimbursable** (out-of-pocket) expenses, this is the employee who submitted the report. -- For **non-reimbursable** (e.g., company card) expenses, this is the default vendor set via the Settings > Workspaces > click workspace name > Accounting > Export settings. +--- +# ExpensiError NS0023: Employee Does Not Exist in NetSuite -## How to fix it +## Why does this happen? +This occurs when the employee’s subsidiary in NetSuite does not match the one selected for the connection in Expensify. -To fix this, the vendor needs to be given access to the applicable currency: -1. In NetSuite, navigate to Lists > Relationships > Vendors to access the list of Vendors. -2. Click Edit next to the Vendor tied to the report: - - For reimbursable (out-of-pocket) expenses, this is the report's submitter. - - For non-reimbursable (e.g., company card) expenses, this is the default vendor set via **Settings > Workspaces > click workspace name > Accounting > Export > click Export company card expenses as > Default vendor.** -3. Navigate to the Financial tab. -4. Scroll down to the Currencies section and add all the currencies that are on the report you are trying to export. -5. Click Save. +## How to Fix It +1. Verify the employee's subsidiary in NetSuite. +2. Confirm the Expensify workspace’s subsidiary under **Settings > Workspaces > Accounting > Subsidiary**. +3. Check **Lists > Employees > Edit > Access** and uncheck **Restrict Access to Expensify**. +4. Ensure the employee’s email matches in both NetSuite and Expensify. +5. Sync NetSuite in Expensify. +6. Reattempt the export. -# ExpensiError NS0068: You do not have permission to set a value for element - “Created From” +--- +# ExpensiError NS0085: Expense Lacks Permissions to Set Exchange Rate -**Why does this happen?** +## Why does this happen? +This occurs when NetSuite’s exchange rate settings are not configured correctly. -This error typically occurs due to insufficient permissions or misconfigured settings in NetSuite on the preferred transaction form for your export type. +## How to Fix It +1. Go to **Customization > Forms > Transaction Forms**. +2. Select the form being used for export (Expense Report, Journal Entry, or Vendor Bill) and click **Edit**. +3. Ensure the **Exchange Rate** field is set to **Show** under: + - **Screen Fields > Expenses** (Expense Reports) + - **Screen Fields > Main** (Vendor Bills) + - **Screen Fields > Lines** and **Screen Fields > Main** (Journal Entries) +4. Sync NetSuite in Expensify. +5. Reattempt the export. -## How to fix it +--- -1. In NetSuite, go to Customization > Forms > Transaction Forms. -2. Search for the form type that the report is being exported as in NetSuite (Expense Report, Journal Entry, Vendor Bill, or if the report total is negative, Vendor Credit). -3. Click Edit next to the form that has the Preferred checkbox checked. -4. Go to Screen Fields > Main and ensure the field Created From has the Show checkbox checked. -5. Sync the NetSuite connection in Expensify (**Settings > Workspaces > Workspace Name > Accounting > three-dot menu > Sync Now**.) -6. Attempt the export again by clicking on Search, then clicking the Approved (company card expenses) or Paid (reimbursable expenses) filter. -Click on the report in question and it will open in the right-hand panel. -Click on Export to NetSuite to try to export again. +# ExpensiError NS0079: Transaction Date Outside Accounting Period -## ExpensiError NS0068: Reports with Expensify Card expenses +## Why does this happen? +NetSuite prevents transactions from being posted outside of designated accounting periods. -**Why does this happen?** +## How to Fix It +1. In NetSuite, go to **Setup > Accounting > Accounting Preferences**. +2. Under **General Ledger**, set **Allow Transaction Date Outside of Posting Period** to **Warn**. +3. Enable **Export to Next Open Period** in Expensify under **Settings > Workspaces > Accounting > Export**. +4. Sync NetSuite in Expensify. +5. Reattempt the export. -Expensify Card expenses export as Journal Entries. If you encounter this error when exporting a report with Expensify Card non-reimbursable expenses, ensure the field Created From has the Show checkbox checked for Journal Entries in NetSuite. +--- +# ExpensiError NS0055: Vendor Lacks Access to Currency -## How to fix it -1. In NetSuite, go to Customization > Forms > Transaction Forms. -2. Click Edit next to the journal entry form that has the Preferred checkbox checked. -3. Ensure the field Created From has the Show checkbox checked. -4. Sync the NetSuite connection in Expensify (**Settings > Workspaces > Workspace Name > Accounting > three-dot menu > Sync Now**.) -5. Attempt the export again by clicking on Search, then clicking the Approved (company card expenses) or Paid (reimbursable expenses) filter. -Click on the report in question and it will open in the right-hand panel. -Click on Export to NetSuite to try to export again. +## Why does this happen? +This occurs when a vendor in NetSuite is not configured to accept a specific currency. -# ExpensiError NS0037: You do not have permission to set a value for element - “Receipt URL” +## How to Fix It +1. In NetSuite, go to **Lists > Relationships > Vendors**. +2. Edit the vendor assigned to the report. +3. Under the **Financial** tab, add the missing currency. +4. Click **Save**. +5. Sync NetSuite in Expensify. +6. Reattempt the export. -**Why does this happen?** +--- +# ExpensiError NS0068: Missing "Created From" Permission -This error typically occurs due to insufficient permissions or misconfigured settings in NetSuite on the preferred transaction form for your export type. +## Why does this happen? +This occurs due to insufficient permissions on the transaction form being used for export. -## How to fix it +## How to Fix It +1. Go to **Customization > Forms > Transaction Forms**. +2. Edit the form marked as **Preferred**. +3. Ensure the **Created From** field is set to **Show** under **Screen Fields > Main**. +4. Sync NetSuite in Expensify. +5. Reattempt the export. -1. In NetSuite, go to Customization > Forms > Transaction Forms. -2. Search for the form type that the report is being exported as in NetSuite (Expense Report, Journal Entry, or Vendor Bill). -3. Click Edit next to the form that has the Preferred checkbox checked. - - If the report is being exported as an Expense Report: - - Go to Screen Fields > Expenses (the Expenses tab farthest to the right). - - Ensure the field ReceiptURL has the Show checkbox checked. - - If the report is being exported as a Journal Entry: - - Go to Screen Fields > Lines. - - Ensure the field ReceiptURL has the Show checkbox checked. - - If the report is being exported as a Vendor Bill: - - Go to Screen Fields > Main. - - Ensure the field ReceiptURL has the Show checkbox checked. -4. Sync the NetSuite connection in Expensify (**Settings > Workspaces > click workspace name > Accounting > three-dot menu > Sync Now**.) -5. Attempt the export again by clicking on Search, then clicking the Approved (company card expenses) or Paid (reimbursable expenses) filter. -Click on the report in question and it will open in the right-hand panel. -Click on Export to NetSuite to try to export again. +--- +# ExpensiError NS0109: NetSuite Login Failed -# ExpensiError NS0042: Error creating vendor - this entity already exists +## Why does this happen? +This error indicates a problem with the authentication tokens used to connect NetSuite and Expensify. -**Why does this happen?** +## How to Fix It +1. Review the [NetSuite Connection Guide](https://help.expensify.com/articles/new-expensify/connections/netsuite/Connect-to-NetSuite). +2. If using an existing token, create a new one and update the connection in Expensify. +3. Sync NetSuite in Expensify. +4. Reattempt the export. -This error occurs when a vendor record already exists in NetSuite, but Expensify is still attempting to create a new one. This typically means that Expensify cannot find the existing vendor during export. -- The vendor record already exists in NetSuite, but there may be discrepancies preventing Expensify from recognizing it. -- The email on the NetSuite vendor record does not match the email of the report submitter in Expensify. -- The vendor record might not be associated with the correct subsidiary in NetSuite. +--- +# ExpensiError NS0037: You Do Not Have Permission to Set a Value for “Receipt URL” -## How to fix it +## Why does this happen? +This error occurs when the **Receipt URL** field is not visible in NetSuite's transaction form settings. -1. **Check Email Matching:** - - Ensure the email on the NetSuite vendor record matches the email of the report submitter in Expensify. - - If it doesn’t match update the existing vendor record in NetSuite to match the report submitter's email and name. - - If there is no email listed, add the email address of the report’s submitter to the existing vendor record in NetSuite. -2. **Check Subsidiary Association:** - - Ensure the vendor record is associated with the same subsidiary selected in the connection configurations - - You can review this under **Settings > Workspaces > click workspace name > Accounting > Subsidiary.** -3. **Automatic Vendor Creation:** - - If you want Expensify to automatically create vendors, ensure the "Auto-create employees/vendors" option is enabled under **Settings > Workspaces > click workspace name > Accounting > Advanced.** - - If appropriate, delete the existing vendor record in NetSuite to allow Expensify to create a new one. -4. After making the necessary changes, sync the NetSuite connection in Expensify (**Settings > Workspaces > click workspace name > Accounting > three-dot menu > Sync Now**.) -5. Attempt the export again by clicking on Search, then clicking the Approved (company card expenses) or Paid (reimbursable expenses) filter. -Click on the report in question and it will open in the right-hand panel. -Click on Export to NetSuite to try to export again. +## How to Fix It +1. **Go to NetSuite**: Navigate to **Customization > Forms > Transaction Forms**. +2. **Find the transaction form**: Locate the form used for the export type (Expense Report, Journal Entry, or Vendor Bill). +3. **Edit the form**: + - **Expense Reports**: Go to **Screen Fields > Expenses** and ensure **ReceiptURL** is set to **Show**. + - **Journal Entries**: Go to **Screen Fields > Lines** and ensure **ReceiptURL** is set to **Show**. + - **Vendor Bills**: Go to **Screen Fields > Main** and ensure **ReceiptURL** is set to **Show**. +4. **Save the changes** and **sync NetSuite in Expensify** (**Settings > Workspaces > Accounting > Sync Now**). +5. **Retry the export**. -# ExpensiError NS0109: Failed to login to NetSuite, please verify your credentials +--- -**Why does this happen?** +# ExpensiError NS0042: Error Creating Vendor - This Entity Already Exists -This error indicates a problem with the tokens created for the connection between Expensify and NetSuite. The error message will say, "Login Error. Please check your credentials." +## Why does this happen? +Expensify is trying to create a new vendor in NetSuite, but a vendor with the same name or email **already exists**. -## How to fix it +## How to Fix It +1. **Verify vendor details in NetSuite**: + - Go to **Lists > Relationships > Vendors** and search for the vendor's name and email. +2. **Ensure email matches**: + - The email in NetSuite should match the email of the **report submitter in Expensify**. + - If missing, update the NetSuite vendor record with the correct email. +3. **Check subsidiary association**: + - Ensure the vendor belongs to the **same subsidiary** as set in Expensify (**Settings > Workspaces > Accounting > Subsidiary**). +4. **Enable automatic vendor creation (if needed)**: + - In Expensify, go to **Settings > Workspaces > Accounting > Advanced** and enable **Auto-create employees/vendors**. +5. **Sync NetSuite in Expensify** and **retry the export**. -1. Review the [Connect to NetSuite](https://help.expensify.com/articles/new-expensify/connections/netsuite/Connect-to-NetSuite) guide and follow steps 1 and 2 exactly as outlined. -2. If you're using an existing token and encounter a problem, you may need to create a new token. +--- -# ExpensiError NS0123 Login Error: Please make sure that the Expensify integration is enabled +# ExpensiError NS0045: Expenses Not Categorized with a NetSuite Account -**Why does this happen?** +## Why does this happen? +This error occurs when expenses in Expensify are assigned to a **category that does not exist in NetSuite** or **was not imported into Expensify**. -This error indicates that the Expensify integration is not enabled in NetSuite. +## How to Fix It +1. **Check the missing category in NetSuite**: + - Search for the category using the **NetSuite Global Search**. + - Ensure it is **active** and correctly named. + - Confirm it is associated with the correct **subsidiary**. +2. **Re-sync categories**: + - In Expensify, go to **Settings > Workspaces > Accounting > Sync Now**. +3. **Reapply the category in Expensify**: + - Open the report, select the affected expense(s), and **reapply the correct category**. +4. **Retry the export**. -## How to fix it +--- -1. **Enable the Expensify Integration:** - - In NetSuite, navigate to Setup > Integrations > Manage Integrations. - - Ensure that the Expensify Integration is listed and that the State is Enabled. -2. **If you can't find the Expensify integration:** - - Click "Show Inactives" to see if Expensify is listed as inactive. - - If Expensify is listed, update its state to Enabled. -3. Once the Expensify integration is enabled, sync the NetSuite connection in Expensify (**Settings > Workspaces > Workspace Name > Accounting > three-dot menu > Sync Now**.) +# ExpensiError NS0046: Billable Expenses Not Coded with a NetSuite Customer or Billable Project -# ExpensiError NS0045: Expenses Not Categorized with a NetSuite Account +## Why does this happen? +In NetSuite, **billable expenses** must be assigned to a **Customer** or **Billable Project**. If they are missing, this error occurs. -**Why does this happen?** +## How to Fix It +1. **Check the affected expenses in Expensify**: + - Open the report and review **each billable expense**. + - Confirm that a **Customer or Project** tag is assigned. +2. **Update the expense**: + - Apply the correct **Customer or Project** in Expensify. +3. **Retry the export**. -This happens when approved expenses are categorized with an option that didn’t import from NetSuite. For NetSuite to accept expense coding, it must first exits and be imported into Expensify from NetSuite. +--- -## How to fix it +# ExpensiError NS0061: Please Enter Value(s) for: Tax Code -1. Log into NetSuite -2. Do a global search for the missing record. - - Ensure the expense category is active and correctly named. - - Ensure the category is associated with the correct subsidiary that the Expensify workspace is linked to. -3. Sync the NetSuite connection in Expensify (**Settings > Workspaces > click workspace name > Accounting > three-dot menu > Sync Now**.) -4. Go back to the report, click on the offending expense(s), and re-apply the category in question. -5. Attempt the export again by clicking on Search, then clicking the Approved (company card expenses) or Paid (reimbursable expenses) filter. -Click on the report in question and it will open in the right-hand panel. -Click on Export to NetSuite to try to export again. +## Why does this happen? +This error occurs when attempting to export **expense reports to a NetSuite Canadian subsidiary** that requires a **Tax Code**, but none is set. +## How to Fix It +1. **Enable Tax in NetSuite**: + - Go to **Setup > Company > Enable Features** and confirm that **Tax Codes** are enabled. +2. **Ensure the Tax Code exists**: + - In NetSuite, go to **Setup > Accounting > Tax Codes** and confirm the correct tax codes exist. +3. **Assign a Tax Posting Account in Expensify**: + - Go to **Settings > Workspaces > Accounting > Export** and select a **Journal Entry tax posting account**. +4. **Sync NetSuite in Expensify** and **retry the export**. -# ExpensiError NS0061: Please Enter Value(s) for: Tax Code +--- -**Why does this happen?** +# ExpensiError NS0068 (Expensify Card Expenses): Missing "Created From" Permission -This error typically occurs when attempting to export expense reports to a Canadian subsidiary in NetSuite for the first time and/or if your subsidiary in NetSuite has Tax enabled. +## Why does this happen? +Expensify Card expenses export as **Journal Entries**. If the **Created From** field is not visible in the Journal Entry form, this error occurs. -## How to fix it +## How to Fix It +1. **Edit the Journal Entry form in NetSuite**: + - Go to **Customization > Forms > Transaction Forms**. + - Click **Edit** next to the preferred Journal Entry form. + - Navigate to **Screen Fields > Main**. + - Ensure **Created From** is set to **Show**. +2. **Save the changes** and **sync NetSuite in Expensify**. +3. **Retry the export**. -To fix this, you need to enable Tax in the NetSuite configuration settings. +--- -1. Go to **Settings > Workspaces > click workspace name > Accounting > Export**. - - Select a Journal Entry tax posting account if you plan on exporting any expenses with taxes. -2. Wait for the connection to sync, it will automatically do so after you make a change. -3. Attempt the export again. +# ExpensiError NS0123: Login Error - Expensify Integration Not Enabled -**Note:** Expenses created before Tax was enabled might need to have the newly imported taxes applied to them retroactively to be exported. +## Why does this happen? +This error occurs when **Expensify is not enabled** as an integration in NetSuite. -# Error creating employee: Your current role does not have permission to access this record. +## How to Fix It +1. **Check if Expensify is enabled in NetSuite**: + - Go to **Setup > Integrations > Manage Integrations**. + - Look for **Expensify Integration** and ensure its **State** is **Enabled**. +2. **If Expensify is missing**: + - Click **Show Inactives** to see if Expensify is listed. + - If it appears, **reactivate it**. +3. **Sync NetSuite in Expensify** and **retry the export**. -**Why does this happen?** +--- -This error indicates that the credentials or role used to connect NetSuite to Expensify do not have the necessary permissions within NetSuite. You can find setup instructions for configuring permissions in NetSuite [here](https://help.expensify.com/articles/new-expensify/connections/netsuite/Connect-to-NetSuite). +# Error Creating Employee: Your Role Does Not Have Permission to Access This Record -## How to fix it +## Why does this happen? +The **NetSuite role** used for the Expensify connection **does not have permission** to create or access employees. -1. If permissions are configured correctly, confirm the report submitter exists in the subsidiary set for the workspace connection and that their Expensify email address matches the email on the NetSuite Employee Record. -2. If the above is true, try toggling off _Auto create employees/vendors_ under the **Settings > Workspaces > Group > click workspace name > Accounting > Advanced tab of the NetSuite configuration window. -3. Sync the NetSuite connection in Expensify (**Settings > Workspaces > click workspace name > Accounting > three-dot menu > Sync Now**.) -4. Attempt the export again by clicking on Search, then clicking the Approved (company card expenses) or Paid (reimbursable expenses) filter. -Click on the report in question and it will open in the right-hand panel. -Click on Export to NetSuite to try to export again. +## How to Fix It +1. **Verify permissions in NetSuite**: + - Follow the [NetSuite Setup Guide](https://help.expensify.com/articles/new-expensify/connections/netsuite/Connect-to-NetSuite) to configure the correct permissions. +2. **Ensure the report submitter exists in NetSuite**: + - The email in NetSuite should match the **report submitter’s email in Expensify**. + - The employee must belong to the **correct subsidiary**. +3. **Disable automatic employee creation (if needed)**: + - In Expensify, go to **Settings > Workspaces > Accounting > Advanced**. + - Toggle **Auto-create employees/vendors** **off**. +4. **Sync NetSuite in Expensify** and **retry the export**. -# Elimination Settings for X Do Not Match +--- -**Why does this happen?** +# ExpensiError: Elimination Settings for X Do Not Match -This error occurs when an Intercompany Payable account is set as the default in the Default Payable Account field in the NetSuite subsidiary preferences, and the Accounting Approval option is enabled for Expense Reports. +## Why does this happen? +This occurs when an **Intercompany Payable account** is set as the default **Payable Account** in NetSuite **subsidiary preferences** while **Accounting Approval** is enabled for Expense Reports. -## How to fix it +## How to Fix It +1. **Edit the Default Payable Account for Expense Reports**: + - In NetSuite, go to **Setup > Company > Subsidiaries**. + - Click **Edit** next to the affected subsidiary. + - Go to the **Preferences** tab. + - Select a **valid payable account** for **Default Payable Account for Expense Reports**. +2. **Repeat this for all subsidiaries** to ensure consistency. +3. **Sync NetSuite in Expensify** and **retry the export**. -Set the Default Payable Account for Expense Reports on each subsidiary in NetSuite to ensure the correct payable account is active. +--- -1. Navigate to Subsidiaries: - - Go to Setup > Company > Subsidiaries. -2. Edit Subsidiary Preferences: - - Click Edit for the desired subsidiary. - - Go to the Preferences tab. -3. Set Default Payable Account: - - Choose the preferred account for Default Payable Account for Expense Reports. +# FAQ -Repeat these steps for each subsidiary to ensure the settings are correct, and then sync the NetSuite connection in Expensify (**Settings > Workspaces > click workspace name > Accounting > three-dot menu > Sync Now**.) +## Why Are Reports Exporting as _Accounting Approved_ Instead of _Paid in Full_? -# ExpensiError NS0046: Billable Expenses Not Coded with a NetSuite Customer or Billable Project +This happens due to: +- **Missing Locations, Classes, or Departments in the Bill Payment Form** +- **Incorrect Expensify Workspace Settings** -**Why does this happen?** +## How to fix for Missing Locations, Classes, or Departments -NetSuite requires billable expenses to be assigned to a Customer or a Project that is configured as billable to a Customer. If this is not set up correctly in NetSuite, this error can occur. +If your accounting classifications require locations, classes, or departments but they are not set to "Show" in your bill payment form, update them in NetSuite: -## How to fix it +- Go to **Customization > Forms > Transaction Forms**. +- Find the **preferred Bill Payment form** (checkmarked). +- Click **Edit or Customize**. +- Under **Screen Fields > Main**, enable "Show" for **Department, Class, and Location**. -1. Check the billable expenses and confirm that a Customer or Project tag is selected. -2. Make any necessary adjustments to the billable expense. -3. Attempt the export again by clicking on Search, then clicking the Approved (company card expenses) or Paid (reimbursable expenses) filter. -Click on the report in question and it will open in the right-hand panel. -Click on Export to NetSuite to try to export again. +## How to fix for Incorrect Expensify Workspace Settings: +Check your NetSuite connection settings in Expensify: -{% include faq-begin.md %} -## Why are reports exporting as _Accounting Approved_ instead of _Paid in Full_? +- Go to **Settings > Workspaces > [Select Workspace] > Accounting > Advanced**. +- Ensure: + - **Sync Reimbursed Reports** is enabled with a payment account selected. + - **Journal Entry Approval Level** is set to **Approved for Posting**. + - **A/P Approval Account** matches the account used for bill payments. -**This can occur for two reasons:** -- Missing Locations, Classes, or Departments in the Bill Payment Form -- Incorrect Settings in Expensify Workspace Configuration +**To verify the A/P Approval Account:** +- Open the **bill or expense report** causing the issue. +- Click **Make Payment**. +- Ensure the account matches what is set in Expensify. -**Missing Locations, Classes, or Departments in Bill Payment Form:** If locations, classes, or departments are required in your accounting classifications but are not marked as 'Show' on the preferred bill payment form, this error can occur, and you will need to update the bill payment form in NetSuite: +Lastly, confirm that the **A/P Approval Account** is selected on the **Expense Report List**. -1. Go to Customization > Forms > Transaction Forms. -2. Find your preferred (checkmarked) Bill Payment form. -3. Click Edit or Customize. -4. Under the Screen Fields > Main tab, check 'Show' near the department, class, and location options. +--- -**Incorrect Settings in Expensify Workspace Configuration:** To fix this, you'll want to confirm the NetSuite connection settings are set up correctly in Expensify: +## Why Are Reports Exporting as _Pending Approval_? -1. Head to **Settings > Workspaces > click workspace name > Accounting > Advanced.** -2. **Ensure the following settings are correct:** - - Sync Reimbursed Reports: Enabled and payment account chosen. - - Journal Entry Approval Level: Approved for Posting. - - A/P Approval Account: This must match the current account being used for bill payment. -3. **Verify A/P Approval Account:** - - To ensure the A/P Approval Account matches the account in NetSuite: - - Go to your bill/expense report causing the error. - - Click Make Payment. - - This account needs to match the account selected in your Expensify configuration. -4. **Check Expense Report List:** - - Make sure this is also the account selected on the expense report by looking at the expense report list. +If reports are marked **"Pending Approval"** instead of **"Approved"**, adjust NetSuite approval settings. -Following these steps will help ensure that reports are exported as "Paid in Full" instead of "Accounting Approved." +**For Journal Entries/Vendor Bills:** +- Go to **Setup > Accounting > Accounting Preferences** in NetSuite. +- Under the **General** tab, uncheck **Require Approvals on Journal Entries**. +- Under the **Approval Routing** tab, disable approval for **Journal Entries/Vendor Bills**. -## Why are reports exporting as _Pending Approval_? -If reports are exporting as "Pending Approval" instead of "Approved," you'll need to adjust the approval preferences in NetSuite. +**Note:** This applies to all Journal Entries, not just Expensify reports. -**Exporting as Journal Entries/Vendor Bills:** -1. In NetSuite, go to Setup > Accounting > Accounting Preferences. -2. On the **General** tab, uncheck **Require Approvals on Journal Entries**. -3. On the **Approval Routing** tab, uncheck Journal Entries/Vendor Bills to remove the approval requirement for Journal Entries created in NetSuite. +**For Expense Reports:** +- Go to **Setup > Company > Enable Features**. +- Under the **Employee** tab, uncheck **Approval Routing** to remove approval for Expense Reports. -**Note:** This change affects all Journal Entries, not just those created by Expensify. +**Note:** This also affects purchase orders. -**Exporting as Expense Reports:** -1. In NetSuite, navigate to Setup > Company > Enable Features. -2. On the "Employee" tab, uncheck "Approval Routing" to remove the approval requirement for Expense Reports created in NetSuite. Please note that this setting also applies to purchase orders. +--- -## How do I Change the Default Payable Account for Reimbursable Expenses in NetSuite? +## How to Change the Default Payable Account for Reimbursable Expenses in NetSuite -NetSuite is set up with a default payable account that is credited each time reimbursable expenses are exported as Expense Reports to NetSuite (once approved by the supervisor and accounting). If you need to change this to credit a different account, follow the below steps: +When exporting reimbursable expenses, NetSuite uses a default payable account. To change this: **For OneWorld Accounts:** -1. Navigate to Setup > Company > Subsidiaries in NetSuite. -2. Next to the subsidiary you want to update, click Edit. -3. Click the Preferences tab. -4. In the Default Payable Account for Expense Reports field, select the desired payable account. -5. Click Save. +- Go to **Setup > Company > Subsidiaries**. +- Click **Edit** next to the subsidiary. +- Under **Preferences**, update the **Default Payable Account for Expense Reports**. +- Click **Save**. **For Non-OneWorld Accounts:** -1. Navigate to Setup > Accounting > Accounting Preferences in NetSuite. -2. Click the Time & Expenses tab. -3. Under the Expenses section, locate the Default Payable Account for Expense Reports field and choose the preferred account. -4. Click Save. +- Go to **Setup > Accounting > Accounting Preferences**. +- Under the **Time & Expenses** tab, update the **Default Payable Account for Expense Reports**. +- Click **Save**. + -{% include faq-end.md %} diff --git a/docs/articles/new-expensify/expenses-&-payments/create-per-diem-expense.md b/docs/articles/new-expensify/expenses-&-payments/create-per-diem-expense.md new file mode 100644 index 000000000000..2a0677969417 --- /dev/null +++ b/docs/articles/new-expensify/expenses-&-payments/create-per-diem-expense.md @@ -0,0 +1,91 @@ +--- +title: Configure and submit Per Diem expenses +description: Learn how to create and submit a Per Diem expense, including selecting a workspace, destination, time details, and sub-rates. +--- +
+ +# Configuring Per Diem in a workspace + +Per Diem is available as a feature under the **Spend** section within **More Features** in the workspace settings. Once enabled, it will appear as a dedicated menu item in the workspace settings LHN. + +## Uploading and exporting Per Diem rates + +Admins can manage Per Diem rates by uploading or exporting data. + +- To upload rates, use the **Import spreadsheet** option. +- To download existing rates, use the **Download CSV** option. + +Both options are accessible from the **three-dot menu** in the page header. + +## Editing or deleting Per Diem rates + +Each Per Diem rate is listed as an individual line item. Admins can: + +- Select a **single** rate or **multiple** rates. +- Edit a rate by clicking on it and adjusting the details. +- Delete rates using the **"X selected" drop-down menu**. + +## Setting the default Per Diem category + +Admins can assign a default category to Per Diem expenses: + +1. Click the **Settings** button in the top-right corner. +2. In the right-hand panel, select **Default category**. +3. Choose from the available categories. + +# FAQ + +## Why don’t I see the Per Diem option when submitting an expense? +The Per Diem option is only available if you are a member of a workspace with Per Diem enabled. If you are submitting an expense outside a workspace (such as in a group chat or DM), the option will not appear. + +## Can I bulk-edit or delete Per Diem rates? +Yes, you can select multiple rates at once and apply bulk actions such as editing or deleting. + +# How to create a Per Diem expense + +If your workspace has **Per Diem** enabled, you can create a Per Diem expense directly from the **Submit Expense** flow. Follow the steps below to complete the process. + +## Submitting a Per Diem expense + +### 1. Open the expense submission flow +- Tap **Submit Expense** from the **Global Create** menu. +- If your workspace has **Per Diem** enabled, you’ll see the **Per Diem** option. + +> **Note:** If you're submitting an expense from a group chat or DM (outside a workspace), you won’t see the **Per Diem** option. + +### 2. Select a workspace +- If you are a member of multiple workspaces with Per Diem enabled, select the workspace you want to submit the expense under. +- If you only belong to one workspace with Per Diem enabled, this step will be skipped. + +### 3. Choose a destination +- Select the country or region where the Per Diem expense applies. + +### 4. Enter time details +- Set the **Start Date** and **Start Time**. +- Set the **End Date** and **End Time**. + +### 5. Select your Per Diem sub-rate +- Choose a **sub-rate** (e.g., Full day, Breakfast, Dinner). +- Enter the **quantity** (e.g., the number of days or meals covered). + +### 6. Review and adjust details +- You can go back and update any previous selections. +- Add more **sub-rates** if needed (e.g., if your trip includes multiple types of expenses). +- Optionally, add a **category, tag, or description** to your expense. + +### 7. Submit the Per Diem expense +- Once everything is correct, tap **Submit Expense**. + +![Open the expense submission flow, and follow the prompts to submit a Per Diem expense]({{site.url}}/assets/images/perdiem_05.png){:width="100%"} + +## Quick access to Per Diem expenses + +If you create Per Diem expenses frequently, you can add them to the **Quick Action Button (QAB)** for faster access. The QAB helps surface your most common actions, making it easy to submit Per Diem expenses with fewer taps. + +# FAQ + +## Why don’t I see the Per Diem option when submitting an expense? +The **Per Diem** option only appears if you are a member of a workspace with **Per Diem enabled**. If you're submitting an expense outside of a workspace (such as in a group chat or DM), this option won’t be available. + +## Can I create Per Diem expenses for multiple days? +Yes! When selecting your **Start Date** and **End Date**, you can add multiple **sub-rates** to cover different parts of your trip (e.g., Full day, Breakfast, Dinner). diff --git a/docs/expensify-classic/hubs/travel/index.html b/docs/expensify-classic/hubs/travel/index.html deleted file mode 100644 index 7c8c3d363d5e..000000000000 --- a/docs/expensify-classic/hubs/travel/index.html +++ /dev/null @@ -1,6 +0,0 @@ ---- -layout: default -title: Travel ---- - -{% include hub.html %} diff --git a/docs/redirects.csv b/docs/redirects.csv index f4970373fb7a..785fab329f4b 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -636,4 +636,8 @@ https://help.expensify.com/articles/new-expensify/settings/Switch-to-light-or-da https://help.expensify.com/articles/new-expensify/settings/Add-personal-information,https://help.expensify.com/articles/new-expensify/settings/Manage-Profile-and-Account-Preferences https://help.expensify.com/articles/new-expensify/settings/Update-your-name,https://help.expensify.com/articles/new-expensify/settings/Manage-Profile-and-Account-Preferences https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Assign-Company-Cards,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Manage-Company-Cards -https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Configure-Company-Card-Settings,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Manage-Company-Cards \ No newline at end of file +https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Configure-Company-Card-Settings,https://help.expensify.com/articles/expensify-classic/connect-credit-cards/Manage-Company-Cards +https://help.expensify.com/articles/expensify-classic/travel/Approve-travel-expenses.md,https://help.expensify.com/articles/new-expensify/travel/Approve-travel-expenses +https://help.expensify.com/articles/expensify-classic/travel/Book-with-Expensify-Travel.md,https://help.expensify.com/articles/new-expensify/travel/Book-with-Expensify-Travel +https://help.expensify.com/articles/expensify-classic/travel/Configure-travel-policy-and-preferences.md,https://help.expensify.com/articles/new-expensify/travel/Configure-travel-policy-and-preferences +https://help.expensify.com/articles/expensify-classic/travel/Track-Travel-Analytics.md,https://help.expensify.com/articles/new-expensify/travel/Track-Travel-Analytics \ No newline at end of file diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 39ce30e2ad64..0ae2dc8fdecf 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -44,7 +44,7 @@ CFBundleVersion - 9.0.98.0 + 9.0.98.3 FullStory OrgId diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 8b582bea0735..511483a00a48 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 9.0.98.0 + 9.0.98.3 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 90b593948766..a97dbfc58aab 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 9.0.98 CFBundleVersion - 9.0.98.0 + 9.0.98.3 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index b0d2af709954..425b949ee6fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "9.0.98-0", + "version": "9.0.98-3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "9.0.98-0", + "version": "9.0.98-3", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -76,7 +76,7 @@ "react-content-loader": "^7.0.0", "react-dom": "18.3.1", "react-error-boundary": "^4.0.11", - "react-fast-pdf": "^1.0.22", + "react-fast-pdf": "^1.0.26", "react-map-gl": "^7.1.3", "react-native": "0.76.3", "react-native-advanced-input-mask": "1.2.1", @@ -6968,8 +6968,10 @@ }, "node_modules/@mapbox/node-pre-gyp": { "version": "1.0.10", + "dev": true, "license": "BSD-3-Clause", "optional": true, + "peer": true, "dependencies": { "detect-libc": "^2.0.0", "https-proxy-agent": "^5.0.0", @@ -6987,8 +6989,10 @@ }, "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { "version": "3.1.0", + "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "semver": "^6.0.0" }, @@ -7001,16 +7005,20 @@ }, "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { "version": "6.3.1", + "dev": true, "license": "ISC", "optional": true, + "peer": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { "version": "5.0.0", + "dev": true, "license": "ISC", "optional": true, + "peer": true, "dependencies": { "abbrev": "1" }, @@ -14950,7 +14958,7 @@ }, "node_modules/abbrev": { "version": "1.1.1", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/abort-controller": { @@ -15035,7 +15043,7 @@ }, "node_modules/agent-base": { "version": "6.0.2", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "debug": "4" @@ -15411,7 +15419,7 @@ }, "node_modules/aproba": { "version": "1.2.0", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/archiver": { @@ -15477,8 +15485,10 @@ }, "node_modules/are-we-there-yet": { "version": "2.0.0", + "dev": true, "license": "ISC", "optional": true, + "peer": true, "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" @@ -15489,8 +15499,10 @@ }, "node_modules/are-we-there-yet/node_modules/readable-stream": { "version": "3.6.2", + "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -17304,9 +17316,11 @@ }, "node_modules/canvas": { "version": "2.11.2", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@mapbox/node-pre-gyp": "^1.0.0", "nan": "^2.17.0", @@ -17632,7 +17646,9 @@ } }, "node_modules/clsx": { - "version": "2.0.0", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "license": "MIT", "engines": { "node": ">=6" @@ -17693,7 +17709,7 @@ }, "node_modules/color-support": { "version": "1.1.3", - "devOptional": true, + "dev": true, "license": "ISC", "bin": { "color-support": "bin.js" @@ -18140,7 +18156,7 @@ }, "node_modules/console-control-strings": { "version": "1.1.0", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/constants-browserify": { @@ -18884,7 +18900,7 @@ }, "node_modules/decompress-response": { "version": "6.0.0", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "mimic-response": "^3.1.0" @@ -18898,7 +18914,7 @@ }, "node_modules/decompress-response/node_modules/mimic-response": { "version": "3.1.0", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10" @@ -19146,7 +19162,7 @@ }, "node_modules/delegates": { "version": "1.0.0", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/denodeify": { @@ -21544,6 +21560,16 @@ "node": ">= 0.8.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/expect": { "version": "29.7.0", "dev": true, @@ -22758,9 +22784,8 @@ }, "node_modules/fs-constants": { "version": "1.0.0", - "dev": true, - "license": "MIT", - "peer": true + "devOptional": true, + "license": "MIT" }, "node_modules/fs-extra": { "version": "9.1.0", @@ -22841,8 +22866,10 @@ }, "node_modules/gauge": { "version": "3.0.2", + "dev": true, "license": "ISC", "optional": true, + "peer": true, "dependencies": { "aproba": "^1.0.3 || ^2.0.0", "color-support": "^1.1.2", @@ -23082,6 +23109,13 @@ "giget": "dist/cli.mjs" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT", + "optional": true + }, "node_modules/github-slugger": { "version": "2.0.0", "dev": true, @@ -23438,7 +23472,7 @@ }, "node_modules/has-unicode": { "version": "2.0.1", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/hasown": { @@ -23877,7 +23911,7 @@ }, "node_modules/https-proxy-agent": { "version": "5.0.1", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "agent-base": "6", @@ -28203,6 +28237,8 @@ }, "node_modules/make-cancellable-promise": { "version": "1.3.2", + "resolved": "https://registry.npmjs.org/make-cancellable-promise/-/make-cancellable-promise-1.3.2.tgz", + "integrity": "sha512-GCXh3bq/WuMbS+Ky4JBPW1hYTOU+znU+Q5m9Pu+pI8EoUqIHk9+tviOKC6/qhHh8C4/As3tzJ69IF32kdz85ww==", "license": "MIT", "funding": { "url": "https://github.com/wojtekmaj/make-cancellable-promise?sponsor=1" @@ -28232,7 +28268,9 @@ "license": "ISC" }, "node_modules/make-event-props": { - "version": "1.6.1", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-1.6.2.tgz", + "integrity": "sha512-iDwf7mA03WPiR8QxvcVHmVWEPfMY1RZXerDVNCRYW7dUr2ppH3J58Rwb39/WG39yTZdRSxr3x+2v22tvI0VEvA==", "license": "MIT", "funding": { "url": "https://github.com/wojtekmaj/make-event-props?sponsor=1" @@ -28612,6 +28650,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-1.3.0.tgz", "integrity": "sha512-nqXPXbso+1dcKDpPCXvwZyJILz+vSLqGGOnDrYHQYE+B8n9JTCekVLC65AfCpR4ggVyA/45Y0iR9LDyS2iI+zA==", + "license": "MIT", "funding": { "url": "https://github.com/wojtekmaj/merge-refs?sponsor=1" }, @@ -29490,6 +29529,13 @@ "node": ">=10" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT", + "optional": true + }, "node_modules/mlly": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.1.tgz", @@ -29546,8 +29592,10 @@ }, "node_modules/nan": { "version": "2.17.0", + "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/nanoid": { "version": "3.3.8", @@ -29567,6 +29615,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT", + "optional": true + }, "node_modules/natural-compare": { "version": "1.4.0", "dev": true, @@ -29635,7 +29690,7 @@ }, "node_modules/node-abi": { "version": "3.65.0", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "semver": "^7.3.5" @@ -29912,8 +29967,10 @@ }, "node_modules/npmlog": { "version": "5.0.1", + "dev": true, "license": "ISC", "optional": true, + "peer": true, "dependencies": { "are-we-there-yet": "^2.0.0", "console-control-strings": "^1.1.0", @@ -30862,9 +30919,10 @@ } }, "node_modules/path2d": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/path2d/-/path2d-0.2.1.tgz", - "integrity": "sha512-Fl2z/BHvkTNvkuBzYTpTuirHZg6wW9z8+4SND/3mDTEcYbbNKWAy21dz9D3ePNNwrrK8pqZO5vLPZ1hLF6T7XA==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/path2d/-/path2d-0.2.2.tgz", + "integrity": "sha512-+vnG6S4dYcYxZd+CZxzXCNKdELYZSKfohrk98yajCo1PtRoDgCTrrwOvK1GT0UoAdVszagDVllQc0U1vaX4NUQ==", + "license": "MIT", "optional": true, "engines": { "node": ">=6" @@ -30889,17 +30947,40 @@ } }, "node_modules/pdfjs-dist": { - "version": "4.4.168", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.4.168.tgz", - "integrity": "sha512-MbkAjpwka/dMHaCfQ75RY1FXX3IewBVu6NGZOcxerRFlaBiIkZmUoR0jotX5VUzYZEXAGzSFtknWs5xRKliXPA==", + "version": "4.8.69", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.8.69.tgz", + "integrity": "sha512-IHZsA4T7YElCKNNXtiLgqScw4zPd3pG9do8UrznC757gMd7UPeHSL2qwNNMJo4r79fl8oj1Xx+1nh2YkzdMpLQ==", + "license": "Apache-2.0", "engines": { "node": ">=18" }, "optionalDependencies": { - "canvas": "^2.11.2", - "path2d": "^0.2.0" + "canvas": "^3.0.0-rc2", + "path2d": "^0.2.1" + } + }, + "node_modules/pdfjs-dist/node_modules/canvas": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.1.0.tgz", + "integrity": "sha512-tTj3CqqukVJ9NgSahykNwtGda7V33VLObwrHfzT0vqJXu7J4d4C/7kQQW3fOEGDfZZoILPut5H00gOjyttPGyg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "^18.12.0 || >= 20.9.0" } }, + "node_modules/pdfjs-dist/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT", + "optional": true + }, "node_modules/pe-library": { "version": "0.4.0", "dev": true, @@ -31200,6 +31281,59 @@ "version": "2.0.0", "license": "ISC" }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "dev": true, @@ -31912,13 +32046,13 @@ } }, "node_modules/react-fast-pdf": { - "version": "1.0.22", - "resolved": "https://registry.npmjs.org/react-fast-pdf/-/react-fast-pdf-1.0.22.tgz", - "integrity": "sha512-bU1YEHFfazKFSdmNAauD267GtjVHdcuE39jyHJQ8CRI8ZWWLwckZ8azPuE25i+hodCBmQuTNBdg6Gx4OhP8HOQ==", + "version": "1.0.26", + "resolved": "https://registry.npmjs.org/react-fast-pdf/-/react-fast-pdf-1.0.26.tgz", + "integrity": "sha512-90ZzyfYtJYLLNV782kOrRRZt2C0M6p0DoCL80kIdhq5/63Y+6+/tzpF5aO0tmnA2G0uM6Pm+plwPsG0bWUjmJg==", "license": "MIT", "dependencies": { - "react-pdf": "^9.1.1", - "react-window": "^1.8.10" + "react-pdf": "9.2.0", + "react-window": "^1.8.11" }, "engines": { "node": ">=20.10.0", @@ -31926,7 +32060,7 @@ }, "peerDependencies": { "lodash": "4.x", - "pdfjs-dist": "4.x", + "pdfjs-dist": "4.8.69", "react": "18.x", "react-dom": "18.x" } @@ -32851,16 +32985,17 @@ } }, "node_modules/react-pdf": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-9.1.1.tgz", - "integrity": "sha512-Cn3RTJZMqVOOCgLMRXDamLk4LPGfyB2Np3OwQAUjmHIh47EpuGW1OpAA1Z1GVDLoHx4d5duEDo/YbUkDbr4QFQ==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-9.2.0.tgz", + "integrity": "sha512-FILVJWfzaBKmF+MSppBnhqTC+HEgbDIpaycBaVkCZfLl2CUeMOd5r0kFYivKSGWR5g2l74dYsBB+xMPx0C0eTw==", + "license": "MIT", "dependencies": { "clsx": "^2.0.0", "dequal": "^2.0.3", "make-cancellable-promise": "^1.3.1", "make-event-props": "^1.6.0", "merge-refs": "^1.3.0", - "pdfjs-dist": "4.4.168", + "pdfjs-dist": "4.8.69", "tiny-invariant": "^1.0.0", "warning": "^4.0.0" }, @@ -33075,7 +33210,9 @@ } }, "node_modules/react-window": { - "version": "1.8.10", + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.11.tgz", + "integrity": "sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.0.0", @@ -33085,8 +33222,8 @@ "node": ">8.0.0" }, "peerDependencies": { - "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/read-binary-file-arch": { @@ -34059,7 +34196,7 @@ }, "node_modules/set-blocking": { "version": "2.0.0", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/set-function-length": { @@ -34232,8 +34369,10 @@ }, "node_modules/simple-get": { "version": "3.1.1", + "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "decompress-response": "^4.2.0", "once": "^1.3.1", @@ -34242,8 +34381,10 @@ }, "node_modules/simple-get/node_modules/decompress-response": { "version": "4.2.1", + "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "mimic-response": "^2.0.0" }, @@ -34253,8 +34394,10 @@ }, "node_modules/simple-get/node_modules/mimic-response": { "version": "2.1.0", + "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=8" }, @@ -35392,11 +35535,30 @@ "node": ">=10" } }, + "node_modules/tar-fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC", + "optional": true + }, "node_modules/tar-stream": { "version": "2.2.0", - "dev": true, + "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -35410,9 +35572,8 @@ }, "node_modules/tar-stream/node_modules/readable-stream": { "version": "3.6.2", - "dev": true, + "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -36124,6 +36285,19 @@ "node": ">=0.6.11 <=0.7.0 || >=0.7.3" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/tweetnacl": { "version": "1.0.3", "license": "Unlicense" @@ -36819,6 +36993,8 @@ }, "node_modules/warning": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", "license": "MIT", "dependencies": { "loose-envify": "^1.0.0" @@ -37473,7 +37649,7 @@ }, "node_modules/wide-align": { "version": "1.1.5", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" diff --git a/package.json b/package.json index 12b027c1b9a9..f92ba0dbf24c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "9.0.98-0", + "version": "9.0.98-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.", @@ -143,7 +143,7 @@ "react-content-loader": "^7.0.0", "react-dom": "18.3.1", "react-error-boundary": "^4.0.11", - "react-fast-pdf": "^1.0.22", + "react-fast-pdf": "^1.0.26", "react-map-gl": "^7.1.3", "react-native": "0.76.3", "react-native-advanced-input-mask": "1.2.1", diff --git a/src/CONST.ts b/src/CONST.ts index 8000635e4e43..c6963ed02564 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -762,7 +762,6 @@ const CONST = { REPORT_FIELDS_FEATURE: 'reportFieldsFeature', NETSUITE_USA_TAX: 'netsuiteUsaTax', COMBINED_TRACK_SUBMIT: 'combinedTrackSubmit', - CATEGORY_AND_TAG_APPROVERS: 'categoryAndTagApprovers', PER_DIEM: 'newDotPerDiem', NEWDOT_MERGE_ACCOUNTS: 'newDotMergeAccounts', NEWDOT_MANAGER_MCTEST: 'newDotManagerMcTest', @@ -1163,6 +1162,7 @@ const CONST = { REIMBURSEMENT_SETUP_REQUESTED: 'REIMBURSEMENTSETUPREQUESTED', // Deprecated OldDot Action REJECTED: 'REJECTED', REMOVED_FROM_APPROVAL_CHAIN: 'REMOVEDFROMAPPROVALCHAIN', + DEMOTED_FROM_WORKSPACE: 'DEMOTEDFROMWORKSPACE', RENAMED: 'RENAMED', REPORT_PREVIEW: 'REPORTPREVIEW', SELECTED_FOR_RANDOM_AUDIT: 'SELECTEDFORRANDOMAUDIT', // OldDot Action @@ -2633,6 +2633,14 @@ const CONST = { SMARTREPORT: 'SMARTREPORT', BILLCOM: 'BILLCOM', }, + APPROVAL_MODE_TRANSLATION_KEYS: { + OPTIONAL: 'submitAndClose', + BASIC: 'submitAndApprove', + ADVANCED: 'advanced', + DYNAMICEXTERNAL: 'dynamictExternal', + SMARTREPORT: 'smartReport', + BILLCOM: 'billcom', + }, ROOM_PREFIX: '#', CUSTOM_UNIT_RATE_BASE_OFFSET: 100, OWNER_EMAIL_FAKE: '_FAKE_', @@ -2709,6 +2717,10 @@ const CONST = { AUTOREPORTING_OFFSET: 'autoReportingOffset', GENERAL_SETTINGS: 'generalSettings', }, + EXPENSE_REPORT_RULES: { + PREVENT_SELF_APPROVAL: 'preventSelfApproval', + MAX_EXPENSE_AGE: 'maxExpenseAge', + }, CONNECTIONS: { NAME: { // Here we will add other connections names when we add support for them diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index d943886982e4..b98baab062ba 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -145,6 +145,9 @@ type ButtonProps = Partial & { /** Whether the Enter keyboard listening is active whether or not the screen that contains the button is focused */ isPressOnEnterActive?: boolean; + + /** The text displays under the first line */ + secondLineText?: string; }; type KeyboardShortcutComponentProps = Pick; @@ -246,6 +249,7 @@ function Button( link = false, isContentCentered = false, isPressOnEnterActive, + secondLineText = '', ...rest }: ButtonProps, ref: ForwardedRef, @@ -260,7 +264,7 @@ function Button( return rest.children; } - const textComponent = ( + const primaryText = ( ); + const textComponent = secondLineText ? ( + + {primaryText} + {secondLineText} + + ) : ( + primaryText + ); + const defaultFill = success || danger ? theme.textLight : theme.icon; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index dce7373c54bb..e7fbf9baab1c 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -45,6 +45,7 @@ function ButtonWithDropdownMenu({ defaultSelectedIndex = 0, shouldShowSelectedItemCheck = false, testID, + secondLineText = '', }: ButtonWithDropdownMenuProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -144,6 +145,7 @@ function ButtonWithDropdownMenu({ shouldShowRightIcon={!isSplitButton} isSplitButton={isSplitButton} testID={testID} + secondLineText={secondLineText} /> {isSplitButton && ( @@ -193,6 +195,7 @@ function ButtonWithDropdownMenu({ small={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.SMALL} innerStyles={[innerStyleDropButton]} enterKeyEventListenerPriority={enterKeyEventListenerPriority} + secondLineText={secondLineText} /> )} {(shouldAlwaysShowDropdownMenu || options.length > 1) && !!popoverAnchorPosition && ( diff --git a/src/components/ButtonWithDropdownMenu/types.ts b/src/components/ButtonWithDropdownMenu/types.ts index dbafbc497105..dd7782bb39ba 100644 --- a/src/components/ButtonWithDropdownMenu/types.ts +++ b/src/components/ButtonWithDropdownMenu/types.ts @@ -111,6 +111,9 @@ type ButtonWithDropdownMenuProps = { /** Used to locate the component in the tests */ testID?: string; + + /** The second line text displays under the first line */ + secondLineText?: string; }; export type { diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx index 4e7f271b2cf2..74d403099316 100644 --- a/src/components/MoneyRequestConfirmationListFooter.tsx +++ b/src/components/MoneyRequestConfirmationListFooter.tsx @@ -806,7 +806,7 @@ function MoneyRequestConfirmationListFooter({ if (!transactionID) { return; } - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TIME_EDIT.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute())); + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_TIME_EDIT.getRoute(action, iouType, transactionID, reportID)); }} disabled={didConfirm} interactive={!isReadOnly} diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index bad9a1a169c9..1fd0cd657e85 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -24,6 +24,7 @@ import {canUseTouchScreen as canUseTouchScreenLib} from '@libs/DeviceCapabilitie import CONST from '@src/CONST'; import shouldReplayVideo from './shouldReplayVideo'; import type {VideoPlayerProps, VideoWithOnFullScreenUpdate} from './types'; +import useHandleNativeVideoControls from './useHandleNativeVideoControls'; import * as VideoUtils from './utils'; import VideoPlayerControls from './VideoPlayerControls'; @@ -91,6 +92,11 @@ function BaseVideoPlayer({ const isUploading = CONST.ATTACHMENT_LOCAL_URL_PREFIX.some((prefix) => url.startsWith(prefix)); const videoStateRef = useRef(null); const {updateVolume, lastNonZeroVolume} = useVolumeContext(); + useHandleNativeVideoControls({ + videoPlayerRef, + isOffline, + isLocalFile: isUploading, + }); const {videoPopoverMenuPlayerRef, currentPlaybackSpeed, setCurrentPlaybackSpeed, setSource: setPopoverMenuSource} = useVideoPopoverMenuContext(); const {source} = videoPopoverMenuPlayerRef.current?.props ?? {}; const shouldUseNewRate = typeof source === 'number' || !source || source.uri !== sourceURL; diff --git a/src/components/VideoPlayer/useHandleNativeVideoControls/index.native.ts b/src/components/VideoPlayer/useHandleNativeVideoControls/index.native.ts new file mode 100644 index 000000000000..478ef60c62a4 --- /dev/null +++ b/src/components/VideoPlayer/useHandleNativeVideoControls/index.native.ts @@ -0,0 +1,5 @@ +import type UseHandleVideoNativeControl from './types'; + +const useHandleNativeVideoControls: UseHandleVideoNativeControl = () => {}; + +export default useHandleNativeVideoControls; diff --git a/src/components/VideoPlayer/useHandleNativeVideoControls/index.ts b/src/components/VideoPlayer/useHandleNativeVideoControls/index.ts new file mode 100644 index 000000000000..1c682ef73f71 --- /dev/null +++ b/src/components/VideoPlayer/useHandleNativeVideoControls/index.ts @@ -0,0 +1,27 @@ +import {useEffect} from 'react'; +import type UseHandleNativeVideoControl from './types'; + +/** + * Web implementation for managing native video controls. + * This hook hides the download button on the native video player in full-screen mode + * when playing a local or offline video. + */ +const useHandleNativeVideoControls: UseHandleNativeVideoControl = ({videoPlayerRef, isLocalFile, isOffline}) => { + useEffect(() => { + // @ts-expect-error Property '_video' does not exist on type VideoWithOnFullScreenUpdate + // eslint-disable-next-line no-underscore-dangle + const videoElement = videoPlayerRef?.current?._nativeRef?.current?._video as HTMLVideoElement; + if (!videoElement) { + return; + } + + if (isOffline || isLocalFile) { + videoElement.setAttribute('controlsList', 'nodownload'); + } else { + videoElement.removeAttribute('controlsList'); + } + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps + }, [isOffline, isLocalFile]); +}; + +export default useHandleNativeVideoControls; diff --git a/src/components/VideoPlayer/useHandleNativeVideoControls/types.ts b/src/components/VideoPlayer/useHandleNativeVideoControls/types.ts new file mode 100644 index 000000000000..ea6bfd4b9a19 --- /dev/null +++ b/src/components/VideoPlayer/useHandleNativeVideoControls/types.ts @@ -0,0 +1,11 @@ +import type {MutableRefObject} from 'react'; +import type {VideoWithOnFullScreenUpdate} from '@components/VideoPlayer/types'; + +type UseHandleNativeVideoControlParams = { + videoPlayerRef: MutableRefObject; + isLocalFile: boolean; + isOffline: boolean; +}; +type UseHandleNativeVideoControl = (params: UseHandleNativeVideoControlParams) => void; + +export default UseHandleNativeVideoControl; diff --git a/src/languages/en.ts b/src/languages/en.ts index 9095e46ce4c3..f72e0da067c2 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -5,6 +5,8 @@ import type {Country} from '@src/CONST'; import type { AccountOwnerParams, ActionsAreCurrentlyRestricted, + AddedOrDeletedPolicyReportFieldParams, + AddedPolicyCustomUnitRateParams, AddEmployeeParams, AddressLineParams, AdminCanceledRequestParams, @@ -60,6 +62,7 @@ import type { DeleteActionParams, DeleteConfirmationParams, DeleteTransactionParams, + DemotedFromWorkspaceParams, DidSplitAmountMessageParams, EarlyDiscountSubtitleParams, EarlyDiscountTitleParams, @@ -175,7 +178,18 @@ import type { UnapproveWithIntegrationWarningParams, UnshareParams, UntilTimeParams, - UpdateAutoReportingFrequencyParams, + UpdatedPolicyCategoryNameParams, + UpdatedPolicyCategoryParams, + UpdatedPolicyCurrencyParams, + UpdatedPolicyDescriptionParams, + UpdatedPolicyFieldWithNewAndOldValueParams, + UpdatedPolicyFieldWithValueParam, + UpdatedPolicyFrequencyParams, + UpdatedPolicyPreventSelfApprovalParams, + UpdatedPolicyReportFieldDefaultValueParams, + UpdatedPolicyTagFieldParams, + UpdatedPolicyTagNameParams, + UpdatedPolicyTagParams, UpdatedTheDistanceMerchantParams, UpdatedTheRequestParams, UpdateRoleParams, @@ -1551,6 +1565,7 @@ const translations = { }, frequencyDescription: 'Choose how often you’d like expenses to submit automatically, or make it manual', frequencies: { + instant: 'Instant', weekly: 'Weekly', monthly: 'Monthly', twiceAMonth: 'Twice a month', @@ -2853,6 +2868,7 @@ const translations = { itemsDescription: 'Choose how to handle QuickBooks Desktop items in Expensify.', }, qbo: { + connectedTo: 'Connected to', importDescription: 'Choose which coding configurations to import from QuickBooks Online to Expensify.', classes: 'Classes', locations: 'Locations', @@ -4800,8 +4816,48 @@ const translations = { public_announce: 'Public Announce', }, }, + workspaceApprovalModes: { + submitAndClose: 'Submit and Close', + submitAndApprove: 'Submit and Approve', + advanced: 'ADVANCED', + dynamictExternal: 'DYNAMIC_EXTERNAL', + smartReport: 'SMARTREPORT', + billcom: 'BILLCOM', + }, workspaceActions: { + addCategory: ({categoryName}: UpdatedPolicyCategoryParams) => `added the category "${categoryName}"`, + deleteCategory: ({categoryName}: UpdatedPolicyCategoryParams) => `removed the category "${categoryName}"`, + updateCategory: ({oldValue, categoryName}: UpdatedPolicyCategoryParams) => `${oldValue ? 'disabled' : 'enabled'} the category "${categoryName}"`, + setCategoryName: ({oldName, newName}: UpdatedPolicyCategoryNameParams) => `renamed the category "${oldName}" to "${newName}"`, + addTag: ({tagListName, tagName}: UpdatedPolicyTagParams) => `added the tag "${tagName}" to the list "${tagListName}"`, + updateTagName: ({tagListName, newName, oldName}: UpdatedPolicyTagNameParams) => `updated the tag list "${tagListName}" by changing the tag "${oldName}" to "${newName}`, + updateTagEnabled: ({tagListName, tagName, enabled}: UpdatedPolicyTagParams) => `${enabled ? 'enabled' : 'disabled'} the tag "${tagName}" on the list "${tagListName}"`, + deleteTag: ({tagListName, tagName}: UpdatedPolicyTagParams) => `removed the tag "${tagName}" from the list "${tagListName}"`, + updateTag: ({tagListName, newValue, tagName, updatedField, oldValue}: UpdatedPolicyTagFieldParams) => { + if (oldValue) { + return `updated the tag "${tagName}" on the list "${tagListName}" by changing the ${updatedField} to "${newValue}" (previously "${oldValue}")`; + } + return `updated the tag "${tagName}" on the list "${tagListName}" by adding a ${updatedField} of "${newValue}"`; + }, + addCustomUnitRate: ({customUnitName, rateName}: AddedPolicyCustomUnitRateParams) => `added a new "${customUnitName}" rate "${rateName}"`, + addedReportField: ({fieldType, fieldName}: AddedOrDeletedPolicyReportFieldParams) => `added ${fieldType} Report Field "${fieldName}"`, + updateReportFieldDefaultValue: ({defaultValue, fieldName}: UpdatedPolicyReportFieldDefaultValueParams) => `set the default value of report field "${fieldName}" to "${defaultValue}"`, + deleteReportField: ({fieldType, fieldName}: AddedOrDeletedPolicyReportFieldParams) => `removed ${fieldType} Report Field "${fieldName}"`, + preventSelfApproval: ({oldValue, newValue}: UpdatedPolicyPreventSelfApprovalParams) => + `updated "Prevent self-approval" to "${newValue === 'true' ? 'Enabled' : 'Disabled'}" (previously "${oldValue === 'true' ? 'Enabled' : 'Disabled'}")`, + updateMaxExpenseAmountNoReceipt: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => + `changed the maximum receipt required expense amount to ${newValue} (previously ${oldValue})`, + updateMaxExpenseAmount: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => + `changed the maximum expense amount for violations to ${newValue} (previously ${oldValue})`, + updateMaxExpenseAge: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => + `updated "Max expense age (days)" to "${newValue}" (previously "${oldValue === 'false' ? CONST.POLICY.DEFAULT_MAX_EXPENSE_AGE : oldValue}")`, + updateDefaultBillable: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => `updated "Re-bill expenses to clients" to "${newValue}" (previously "${oldValue}")`, + updateDefaultTitleEnforced: ({value}: UpdatedPolicyFieldWithValueParam) => `turned "Enforce default report titles" ${value ? 'on' : 'off'}`, renamedWorkspaceNameAction: ({oldName, newName}: RenamedRoomActionParams) => `updated the name of this workspace to "${newName}" (previously "${oldName}")`, + updateWorkspaceDescription: ({newDescription, oldDescription}: UpdatedPolicyDescriptionParams) => + !oldDescription + ? `set the description of this workspace to "${newDescription}"` + : `updated the description of this workspace to "${newDescription}" (previously "${oldDescription}")`, removedFromApprovalWorkflow: ({submittersNames}: RemovedFromApprovalWorkflowParams) => { let joinedNames = ''; if (submittersNames.length === 1) { @@ -4816,6 +4872,12 @@ const translations = { other: `removed you from ${joinedNames}'s approval workflows and workspace chats. Previously submitted reports will remain available for approval in your Inbox.`, }; }, + demotedFromWorkspace: ({policyName, oldRole}: DemotedFromWorkspaceParams) => + `updated your role in ${policyName} from ${oldRole} to user. You have been removed from all submitter workspace chats except for you own.`, + updatedWorkspaceCurrencyAction: ({oldCurrency, newCurrency}: UpdatedPolicyCurrencyParams) => `updated the default currency to ${newCurrency} (previously ${oldCurrency})`, + updatedWorkspaceFrequencyAction: ({oldFrequency, newFrequency}: UpdatedPolicyFrequencyParams) => + `updated the auto-reporting frequency to "${newFrequency}" (previously "${oldFrequency}")`, + updateApprovalMode: ({newValue, oldValue}: ChangeFieldParams) => `updated the approval mode to "${newValue}" (previously "${oldValue}")`, upgradedWorkspace: 'upgraded this workspace to the Control plan', downgradedWorkspace: 'downgraded this workspace to the Collect plan', }, @@ -5071,7 +5133,8 @@ const translations = { nonReimbursableLink: 'View company card expenses.', pending: ({label}: ExportedToIntegrationParams) => `started exporting this report to ${label}...`, }, - integrationsMessage: ({errorMessage, label}: IntegrationSyncFailedParams) => `failed to export this report to ${label} ("${errorMessage}")`, + integrationsMessage: ({errorMessage, label, linkText, linkURL}: IntegrationSyncFailedParams) => + `failed to export this report to ${label} ("${errorMessage} ${linkText ? `${linkText}` : ''}")`, managerAttachReceipt: `added a receipt`, managerDetachReceipt: `removed a receipt`, markedReimbursed: ({amount, currency}: MarkedReimbursedParams) => `paid ${currency}${amount} elsewhere`, @@ -5096,8 +5159,6 @@ const translations = { leftWorkspace: ({nameOrEmail}: LeftWorkspaceParams) => `${nameOrEmail} left the workspace`, removeMember: ({email, role}: AddEmployeeParams) => `removed ${role === 'member' || role === 'user' ? 'member' : 'admin'} ${email}`, removedConnection: ({connectionName}: ConnectionNameParams) => `removed connection to ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}`, - updateAutoReportingFrequency: ({oldFrequency, newFrequency}: UpdateAutoReportingFrequencyParams) => - `updated the submission frequency to "${newFrequency}" (previously "${oldFrequency}")`, }, }, }, diff --git a/src/languages/es.ts b/src/languages/es.ts index b821224d9cc8..adc04c57ac56 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4,6 +4,8 @@ import type en from './en'; import type { AccountOwnerParams, ActionsAreCurrentlyRestricted, + AddedOrDeletedPolicyReportFieldParams, + AddedPolicyCustomUnitRateParams, AddEmployeeParams, AddressLineParams, AdminCanceledRequestParams, @@ -59,6 +61,7 @@ import type { DeleteActionParams, DeleteConfirmationParams, DeleteTransactionParams, + DemotedFromWorkspaceParams, DidSplitAmountMessageParams, EarlyDiscountSubtitleParams, EarlyDiscountTitleParams, @@ -174,7 +177,18 @@ import type { UnapproveWithIntegrationWarningParams, UnshareParams, UntilTimeParams, - UpdateAutoReportingFrequencyParams, + UpdatedPolicyCategoryNameParams, + UpdatedPolicyCategoryParams, + UpdatedPolicyCurrencyParams, + UpdatedPolicyDescriptionParams, + UpdatedPolicyFieldWithNewAndOldValueParams, + UpdatedPolicyFieldWithValueParam, + UpdatedPolicyFrequencyParams, + UpdatedPolicyPreventSelfApprovalParams, + UpdatedPolicyReportFieldDefaultValueParams, + UpdatedPolicyTagFieldParams, + UpdatedPolicyTagNameParams, + UpdatedPolicyTagParams, UpdatedTheDistanceMerchantParams, UpdatedTheRequestParams, UpdateRoleParams, @@ -1552,6 +1566,7 @@ const translations = { }, frequencyDescription: 'Elige la frecuencia de presentación automática de gastos, o preséntalos manualmente', frequencies: { + instant: 'Instante', weekly: 'Semanal', monthly: 'Mensual', twiceAMonth: 'Dos veces al mes', @@ -2881,6 +2896,7 @@ const translations = { itemsDescription: 'Elige cómo gestionar los elementos de QuickBooks Desktop en Expensify.', }, qbo: { + connectedTo: 'Conectado a', importDescription: 'Elige que configuraciónes de codificación son importadas desde QuickBooks Online a Expensify.', classes: 'Clases', locations: 'Lugares', @@ -4853,7 +4869,49 @@ const translations = { public_announce: 'Anuncio Público', }, }, + workspaceApprovalModes: { + submitAndClose: 'Enviar y Cerrar', + submitAndApprove: 'Enviar y Aprobar', + advanced: 'AVANZADO', + dynamictExternal: 'DINÁMICO_EXTERNO', + smartReport: 'INFORME_INTELIGENTE', + billcom: 'BILLCOM', + }, workspaceActions: { + addCategory: ({categoryName}: UpdatedPolicyCategoryParams) => `añadió la categoría "${categoryName}""`, + deleteCategory: ({categoryName}: UpdatedPolicyCategoryParams) => `eliminó la categoría "${categoryName}"`, + updateCategory: ({oldValue, categoryName}: UpdatedPolicyCategoryParams) => `${oldValue ? 'deshabilitó' : 'habilitó'} la categoría "${categoryName}"`, + setCategoryName: ({oldName, newName}: UpdatedPolicyCategoryNameParams) => `renombró la categoría "${oldName}" a "${newName}`, + addTag: ({tagListName, tagName}: UpdatedPolicyTagParams) => `añadió la etiqueta "${tagName}" a la lista "${tagListName}"`, + updateTagName: ({tagListName, newName, oldName}: UpdatedPolicyTagNameParams) => `actualizó la lista de etiquetas "${tagListName}" cambiando la etiqueta "${oldName}" a "${newName}"`, + updateTagEnabled: ({tagListName, tagName, enabled}: UpdatedPolicyTagParams) => `${enabled ? 'habilitó' : 'deshabilitó'} la etiqueta "${tagName}" en la lista "${tagListName}"`, + deleteTag: ({tagListName, tagName}: UpdatedPolicyTagParams) => `eliminó la etiqueta "${tagName}" de la lista "${tagListName}"`, + updateTag: ({tagListName, newValue, tagName, updatedField, oldValue}: UpdatedPolicyTagFieldParams) => { + if (oldValue) { + return `actualizó la etiqueta "${tagName}" en la lista "${tagListName}" cambiando el ${updatedField} a "${newValue}" (previamente "${oldValue}")`; + } + return `actualizó la etiqueta "${tagName}" en la lista "${tagListName}" añadiendo un ${updatedField} de "${newValue}"`; + }, + addCustomUnitRate: ({customUnitName, rateName}: AddedPolicyCustomUnitRateParams) => `añadió una nueva tasa de "${rateName}" para "${customUnitName}"`, + addedReportField: ({fieldType, fieldName}: AddedOrDeletedPolicyReportFieldParams) => `añadió el campo de informe ${fieldType} "${fieldName}"`, + updateReportFieldDefaultValue: ({defaultValue, fieldName}: UpdatedPolicyReportFieldDefaultValueParams) => + `estableció el valor predeterminado del campo de informe "${fieldName}" en "${defaultValue}"`, + deleteReportField: ({fieldType, fieldName}: AddedOrDeletedPolicyReportFieldParams) => `eliminó el campo de informe ${fieldType} "${fieldName}"`, + preventSelfApproval: ({oldValue, newValue}: UpdatedPolicyPreventSelfApprovalParams) => + `actualizó "Evitar la autoaprobación" a "${newValue === 'true' ? 'Habilitada' : 'Deshabilitada'}" (previamente "${oldValue === 'true' ? 'Habilitada' : 'Deshabilitada'}")`, + updateMaxExpenseAmountNoReceipt: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => + `cambió el monto máximo de gasto requerido sin recibo a ${newValue} (previamente ${oldValue})`, + updateMaxExpenseAmount: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => + `cambió el monto máximo de gasto para violaciones a ${newValue} (previamente ${oldValue})`, + updateMaxExpenseAge: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => + `actualizó "Antigüedad máxima de gastos (días)" a "${newValue}" (previamente "${oldValue === 'false' ? CONST.POLICY.DEFAULT_MAX_EXPENSE_AGE : oldValue}")`, + updateDefaultBillable: ({oldValue, newValue}: UpdatedPolicyFieldWithNewAndOldValueParams) => + `actualizó "Volver a facturar gastos a clientes" a "${newValue}" (previamente "${oldValue}")`, + updateDefaultTitleEnforced: ({value}: UpdatedPolicyFieldWithValueParam) => `cambió "Requerir título predeterminado de informe" a ${value ? 'activado' : 'desactivado'}`, + updateWorkspaceDescription: ({newDescription, oldDescription}: UpdatedPolicyDescriptionParams) => + !oldDescription + ? `estableció la descripción de este espacio de trabajo como "${newDescription}"` + : `actualizó la descripción de este espacio de trabajo a "${newDescription}" (previamente "${oldDescription}")`, renamedWorkspaceNameAction: ({oldName, newName}: RenamedRoomActionParams) => `actualizó el nombre de este espacio de trabajo a "${newName}" (previamente "${oldName}")`, removedFromApprovalWorkflow: ({submittersNames}: RemovedFromApprovalWorkflowParams) => { let joinedNames = ''; @@ -4869,6 +4927,12 @@ const translations = { other: `te eliminó de los flujos de trabajo de aprobaciones y de los chats del espacio de trabajo de ${joinedNames}. Los informes enviados anteriormente seguirán estando disponibles para su aprobación en tu bandeja de entrada.`, }; }, + demotedFromWorkspace: ({policyName, oldRole}: DemotedFromWorkspaceParams) => + `cambió tu rol en ${policyName} de ${oldRole} a miembro. Te eliminamos de todos los chats del espacio de trabajo, excepto el suyo.`, + updatedWorkspaceCurrencyAction: ({oldCurrency, newCurrency}: UpdatedPolicyCurrencyParams) => `actualizó la moneda predeterminada a ${newCurrency} (previamente ${oldCurrency})`, + updatedWorkspaceFrequencyAction: ({oldFrequency, newFrequency}: UpdatedPolicyFrequencyParams) => + `actualizó la frecuencia de generación automática de informes a "${newFrequency}" (previamente "${oldFrequency}")`, + updateApprovalMode: ({newValue, oldValue}: ChangeFieldParams) => `actualizó el modo de aprobación a "${newValue}" (previamente "${oldValue}")`, upgradedWorkspace: 'mejoró este espacio de trabajo al plan Controlar', downgradedWorkspace: 'bajó de categoría este espacio de trabajo al plan Recopilar', }, @@ -5125,7 +5189,8 @@ const translations = { nonReimbursableLink: 'Ver los gastos de la tarjeta de empresa.', pending: ({label}: ExportedToIntegrationParams) => `comenzó a exportar este informe a ${label}...`, }, - integrationsMessage: ({label, errorMessage}: IntegrationSyncFailedParams) => `no se pudo exportar este informe a ${label} ("${errorMessage}")`, + integrationsMessage: ({label, errorMessage, linkText, linkURL}: IntegrationSyncFailedParams) => + `no se pudo exportar este informe a ${label} ("${errorMessage} ${linkText ? `${linkText}` : ''}")`, managerAttachReceipt: `agregó un recibo`, managerDetachReceipt: `quitó un recibo`, markedReimbursed: ({amount, currency}: MarkedReimbursedParams) => `pagó ${currency}${amount} en otro lugar`, @@ -5150,8 +5215,6 @@ const translations = { leftWorkspace: ({nameOrEmail}: LeftWorkspaceParams) => `${nameOrEmail} salió del espacio de trabajo`, removeMember: ({email, role}: AddEmployeeParams) => `eliminado ${role === 'miembro' || role === 'user' ? 'miembro' : 'administrador'} ${email}`, removedConnection: ({connectionName}: ConnectionNameParams) => `eliminó la conexión a ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}`, - updateAutoReportingFrequency: ({oldFrequency, newFrequency}: UpdateAutoReportingFrequencyParams) => - `actualizó la frecuencia de envíos a "${newFrequency}" (previamente "${oldFrequency}")`, }, }, }, diff --git a/src/languages/params.ts b/src/languages/params.ts index 2a04be84ae21..2224e9c2426b 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -293,6 +293,34 @@ type ChangeFieldParams = {oldValue?: string; newValue: string; fieldName: string type ChangePolicyParams = {fromPolicy: string; toPolicy: string}; +type UpdatedPolicyDescriptionParams = {oldDescription: string; newDescription: string}; + +type UpdatedPolicyCurrencyParams = {oldCurrency: string; newCurrency: string}; + +type UpdatedPolicyCategoryParams = {categoryName: string; oldValue?: boolean}; + +type UpdatedPolicyTagParams = {tagListName: string; tagName: string; enabled?: boolean}; + +type UpdatedPolicyTagNameParams = {oldName: string; newName: string; tagListName: string}; + +type UpdatedPolicyTagFieldParams = {oldValue?: string; newValue: string; tagName: string; tagListName: string; updatedField: string}; + +type UpdatedPolicyCategoryNameParams = {oldName: string; newName?: string}; + +type AddedPolicyCustomUnitRateParams = {customUnitName: string; rateName: string}; + +type AddedOrDeletedPolicyReportFieldParams = {fieldType: string; fieldName?: string}; + +type UpdatedPolicyReportFieldDefaultValueParams = {fieldName?: string; defaultValue?: string}; + +type UpdatedPolicyPreventSelfApprovalParams = {oldValue: string; newValue: string}; + +type UpdatedPolicyFieldWithNewAndOldValueParams = {oldValue: string; newValue: string}; + +type UpdatedPolicyFieldWithValueParam = {value: boolean}; + +type UpdatedPolicyFrequencyParams = {oldFrequency: string; newFrequency: string}; + type ChangeTypeParams = {oldType: string; newType: string}; type DelegateSubmitParams = {delegateUser: string; originalManager: string}; @@ -349,11 +377,6 @@ type ConnectionNameParams = { connectionName: AllConnectionName; }; -type UpdateAutoReportingFrequencyParams = { - oldFrequency: string; - newFrequency: string; -}; - type LastSyncDateParams = { connectionName: string; formattedDate: string; @@ -369,7 +392,7 @@ type ExportAgainModalDescriptionParams = { connectionName: ConnectionName; }; -type IntegrationSyncFailedParams = {label: string; errorMessage: string}; +type IntegrationSyncFailedParams = {label: string; errorMessage: string; linkText?: string; linkURL?: string}; type AddEmployeeParams = {email: string; role: string}; @@ -515,6 +538,11 @@ type RemovedFromApprovalWorkflowParams = { submittersNames: string[]; }; +type DemotedFromWorkspaceParams = { + policyName: string; + oldRole: string; +}; + type IntegrationExportParams = { integration: string; type?: string; @@ -631,6 +659,7 @@ export type { ConnectionParams, IntegrationExportParams, RemovedFromApprovalWorkflowParams, + DemotedFromWorkspaceParams, DefaultAmountParams, AutoPayApprovedReportsLimitErrorParams, FeatureNameParams, @@ -815,7 +844,6 @@ export type { UpdateRoleParams, LeftWorkspaceParams, RemoveMemberParams, - UpdateAutoReportingFrequencyParams, DateParams, FiltersAmountBetweenParams, StatementPageTitleParams, @@ -834,8 +862,22 @@ export type { CompanyNameParams, CustomUnitRateParams, ChatWithAccountManagerParams, + UpdatedPolicyCurrencyParams, + UpdatedPolicyFrequencyParams, + UpdatedPolicyCategoryParams, + UpdatedPolicyCategoryNameParams, + UpdatedPolicyPreventSelfApprovalParams, + UpdatedPolicyFieldWithNewAndOldValueParams, + UpdatedPolicyFieldWithValueParam, + UpdatedPolicyDescriptionParams, EditDestinationSubtitleParams, FlightLayoverParams, + AddedOrDeletedPolicyReportFieldParams, + AddedPolicyCustomUnitRateParams, + UpdatedPolicyTagParams, + UpdatedPolicyTagNameParams, + UpdatedPolicyTagFieldParams, + UpdatedPolicyReportFieldDefaultValueParams, SubmitsToParams, SettlementDateParams, }; diff --git a/src/libs/Navigation/navigateAfterInteraction/index.ios.ts b/src/libs/Navigation/navigateAfterInteraction/index.ios.ts new file mode 100644 index 000000000000..6faedf3c7616 --- /dev/null +++ b/src/libs/Navigation/navigateAfterInteraction/index.ios.ts @@ -0,0 +1,14 @@ +import {InteractionManager} from 'react-native'; +import Navigation from '@libs/Navigation/Navigation'; + +/** + * On iOS, the navigation transition can sometimes break other animations, such as the closing modal. + * In this case we need to wait for the animation to be complete before executing the navigation + */ +function navigateAfterInteraction(callback: () => void) { + InteractionManager.runAfterInteractions(() => { + Navigation.setNavigationActionToMicrotaskQueue(callback); + }); +} + +export default navigateAfterInteraction; diff --git a/src/libs/Navigation/navigateAfterInteraction/index.ts b/src/libs/Navigation/navigateAfterInteraction/index.ts new file mode 100644 index 000000000000..34ef4e1fce33 --- /dev/null +++ b/src/libs/Navigation/navigateAfterInteraction/index.ts @@ -0,0 +1,5 @@ +function navigateAfterInteraction(callback: () => void) { + callback(); +} + +export default navigateAfterInteraction; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 76986c7cad98..66b70d90c171 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -792,7 +792,6 @@ function createOption( if (report) { result.isChatRoom = reportUtilsIsChatRoom(report); result.isDefaultRoom = isDefaultRoom(report); - // eslint-disable-next-line @typescript-eslint/naming-convention result.private_isArchived = getReportNameValuePairs(report.reportID)?.private_isArchived; result.isExpenseReport = isExpenseReport(report); result.isInvoiceRoom = isInvoiceRoom(report); diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 9ed5881513f2..d26ff8c245a9 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -18,10 +18,6 @@ function canUseNetSuiteUSATax(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.NETSUITE_USA_TAX) || canUseAllBetas(betas); } -function canUseCategoryAndTagApprovers(betas: OnyxEntry): boolean { - return !!betas?.includes(CONST.BETAS.CATEGORY_AND_TAG_APPROVERS) || canUseAllBetas(betas); -} - function canUsePerDiem(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.PER_DIEM) || canUseAllBetas(betas); } @@ -58,7 +54,6 @@ export default { canUseLinkPreviews, canUseSpotnanaTravel, canUseNetSuiteUSATax, - canUseCategoryAndTagApprovers, canUsePerDiem, canUseMergeAccounts, canUseManagerMcTest, diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 607de0fee0e7..43b3cd8518fd 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -11,11 +11,13 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Locale, OnyxInputOrEntry, PersonalDetailsList, PrivatePersonalDetails} from '@src/types/onyx'; import type {JoinWorkspaceResolution, OriginalMessageChangeLog, OriginalMessageExportIntegration} from '@src/types/onyx/OriginalMessage'; +import type {PolicyReportFieldType} from '@src/types/onyx/Policy'; import type Report from '@src/types/onyx/Report'; import type ReportAction from '@src/types/onyx/ReportAction'; import type {Message, OldDotReportAction, OriginalMessage, ReportActions} from '@src/types/onyx/ReportAction'; import type ReportActionName from '@src/types/onyx/ReportActionName'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import {convertToDisplayString} from './CurrencyUtils'; import DateUtils from './DateUtils'; import {getEnvironmentURL} from './Environment/Environment'; import getBase62ReportID from './getBase62ReportID'; @@ -30,6 +32,7 @@ import {getPolicy, isPolicyAdmin as isPolicyAdminPolicyUtils} from './PolicyUtil import type {getReportName, OptimisticIOUReportAction, PartialReportAction} from './ReportUtils'; import StringUtils from './StringUtils'; import {isOnHoldByTransactionID} from './TransactionUtils'; +import {getReportFieldAlternativeTextTranslationKey} from './WorkspaceReportFieldUtils'; type LastVisibleMessage = { lastMessageText: string; @@ -1368,7 +1371,9 @@ function getMessageOfOldDotReportAction(oldDotAction: PartialReportAction | OldD case CONST.REPORT.ACTIONS.TYPE.INTEGRATIONS_MESSAGE: { const {result, label} = originalMessage; const errorMessage = result?.messages?.join(', ') ?? ''; - return translateLocal('report.actions.type.integrationsMessage', {errorMessage, label}); + const linkText = result?.link?.text ?? ''; + const linkURL = result?.link?.url ?? ''; + return translateLocal('report.actions.type.integrationsMessage', {errorMessage, label, linkText, linkURL}); } case CONST.REPORT.ACTIONS.TYPE.MANAGER_ATTACH_RECEIPT: return translateLocal('report.actions.type.managerAttachReceipt'); @@ -1758,8 +1763,8 @@ function getPolicyChangeLogChangeRoleMessage(reportAction: OnyxInputOrEntry): string { - if (!isPolicyChangeLogDeleteMemberMessage(reportAction)) { - return ''; +function getWorkspaceNameUpdatedMessage(action: ReportAction) { + const {oldName, newName} = getOriginalMessage(action as ReportAction) ?? {}; + const message = oldName && newName ? translateLocal('workspaceActions.renamedWorkspaceNameAction', {oldName, newName}) : getReportActionText(action); + return message; +} + +function getWorkspaceDescriptionUpdatedMessage(action: ReportAction) { + const {oldDescription, newDescription} = getOriginalMessage(action as ReportAction) ?? {}; + const message = + typeof oldDescription === 'string' && newDescription ? translateLocal('workspaceActions.updateWorkspaceDescription', {newDescription, oldDescription}) : getReportActionText(action); + return message; +} + +function getWorkspaceCurrencyUpdateMessage(action: ReportAction) { + const {oldCurrency, newCurrency} = getOriginalMessage(action as ReportAction) ?? {}; + const message = oldCurrency && newCurrency ? translateLocal('workspaceActions.updatedWorkspaceCurrencyAction', {oldCurrency, newCurrency}) : getReportActionText(action); + return message; +} + +type AutoReportingFrequencyKey = ValueOf; +type AutoReportingFrequencyDisplayNames = Record; + +const getAutoReportingFrequencyDisplayNames = (): AutoReportingFrequencyDisplayNames => ({ + [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MONTHLY]: translateLocal('workflowsPage.frequencies.monthly'), + [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.IMMEDIATE]: translateLocal('workflowsPage.frequencies.daily'), + [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.WEEKLY]: translateLocal('workflowsPage.frequencies.weekly'), + [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.SEMI_MONTHLY]: translateLocal('workflowsPage.frequencies.twiceAMonth'), + [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.TRIP]: translateLocal('workflowsPage.frequencies.byTrip'), + [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.MANUAL]: translateLocal('workflowsPage.frequencies.manually'), + [CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT]: translateLocal('workflowsPage.frequencies.instant'), +}); + +function getWorkspaceFrequencyUpdateMessage(action: ReportAction): string { + const {oldFrequency, newFrequency} = getOriginalMessage(action as ReportAction) ?? {}; + + if (!oldFrequency || !newFrequency) { + return getReportActionText(action); } - const originalMessage = getOriginalMessage(reportAction); - const email = originalMessage?.email ?? ''; - const role = originalMessage?.role ?? ''; - return translateLocal('report.actions.type.removeMember', {email, role}); + + const frequencyDisplayNames = getAutoReportingFrequencyDisplayNames(); + const oldFrequencyTranslation = frequencyDisplayNames[oldFrequency]?.toLowerCase(); + const newFrequencyTranslation = frequencyDisplayNames[newFrequency]?.toLowerCase(); + + if (!oldFrequencyTranslation || !newFrequencyTranslation) { + return getReportActionText(action); + } + + return translateLocal('workspaceActions.updatedWorkspaceFrequencyAction', { + oldFrequency: oldFrequencyTranslation, + newFrequency: newFrequencyTranslation, + }); } -function isPolicyChangeLogUpdateAutoReportingFrequencyMessage( - reportAction: OnyxInputOrEntry, -): reportAction is ReportAction { - return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AUTO_REPORTING_FREQUENCY); +function getWorkspaceCategoryUpdateMessage(action: ReportAction): string { + const {categoryName, oldValue, newName, oldName} = getOriginalMessage(action as ReportAction) ?? {}; + + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_CATEGORY && categoryName) { + return translateLocal('workspaceActions.addCategory', { + categoryName, + }); + } + + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_CATEGORY && categoryName) { + return translateLocal('workspaceActions.deleteCategory', { + categoryName, + }); + } + + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_CATEGORY && categoryName) { + return translateLocal('workspaceActions.updateCategory', { + oldValue: !!oldValue, + categoryName, + }); + } + + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.SET_CATEGORY_NAME && oldName && newName) { + return translateLocal('workspaceActions.setCategoryName', { + oldName, + newName, + }); + } + + return getReportActionText(action); } -function getPolicyChangeLogUpdateAutoReportingFrequencyMessage(reportAction: OnyxInputOrEntry): string { - if (!isPolicyChangeLogUpdateAutoReportingFrequencyMessage(reportAction)) { +function getWorkspaceTagUpdateMessage(action: ReportAction): string { + const {tagListName, tagName, enabled, newName, newValue, oldName, oldValue, updatedField} = + getOriginalMessage(action as ReportAction) ?? {}; + + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_TAG && tagListName && tagName) { + return translateLocal('workspaceActions.addTag', { + tagListName, + tagName, + }); + } + + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_TAG && tagListName && tagName) { + return translateLocal('workspaceActions.deleteTag', { + tagListName, + tagName, + }); + } + + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_TAG_ENABLED && tagListName && tagName) { + return translateLocal('workspaceActions.updateTagEnabled', { + tagListName, + tagName, + enabled, + }); + } + + if (action.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_TAG_NAME && tagListName && newName && oldName) { + return translateLocal('workspaceActions.updateTagName', { + tagListName, + newName, + oldName, + }); + } + + if ( + action.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_TAG && + tagListName && + typeof oldValue === 'string' && + typeof newValue === 'string' && + tagName && + updatedField + ) { + return translateLocal('workspaceActions.updateTag', { + tagListName, + oldValue, + newValue, + tagName, + updatedField, + }); + } + + return getReportActionText(action); +} + +function getWorkspaceCustomUnitRateAddedMessage(action: ReportAction): string { + const {customUnitName, rateName} = getOriginalMessage(action as ReportAction) ?? {}; + + if (customUnitName && rateName) { + return translateLocal('workspaceActions.addCustomUnitRate', { + customUnitName, + rateName, + }); + } + + return getReportActionText(action); +} + +function getWorkspaceReportFieldAddMessage(action: ReportAction): string { + const {fieldName, fieldType} = getOriginalMessage(action as ReportAction) ?? {}; + + if (fieldName && fieldType) { + return translateLocal('workspaceActions.addedReportField', { + fieldName, + fieldType: translateLocal(getReportFieldAlternativeTextTranslationKey(fieldType as PolicyReportFieldType)), + }); + } + + return getReportActionText(action); +} + +function getWorkspaceReportFieldUpdateMessage(action: ReportAction): string { + const {updateType, fieldName, defaultValue} = getOriginalMessage(action as ReportAction) ?? {}; + + if (updateType === 'updatedDefaultValue' && fieldName && defaultValue) { + return translateLocal('workspaceActions.updateReportFieldDefaultValue', { + fieldName, + defaultValue, + }); + } + + return getReportActionText(action); +} + +function getWorkspaceReportFieldDeleteMessage(action: ReportAction): string { + const {fieldType, fieldName} = getOriginalMessage(action as ReportAction) ?? {}; + + if (fieldType && fieldName) { + return translateLocal('workspaceActions.deleteReportField', { + fieldName, + fieldType: translateLocal(getReportFieldAlternativeTextTranslationKey(fieldType as PolicyReportFieldType)), + }); + } + + return getReportActionText(action); +} + +function getWorkspaceUpdateFieldMessage(action: ReportAction): string { + const {newValue, oldValue, updatedField} = getOriginalMessage(action as ReportAction) ?? {}; + + const newValueTranslationKey = CONST.POLICY.APPROVAL_MODE_TRANSLATION_KEYS[newValue as keyof typeof CONST.POLICY.APPROVAL_MODE_TRANSLATION_KEYS]; + const oldValueTranslationKey = CONST.POLICY.APPROVAL_MODE_TRANSLATION_KEYS[oldValue as keyof typeof CONST.POLICY.APPROVAL_MODE_TRANSLATION_KEYS]; + + if (updatedField && updatedField === CONST.POLICY.COLLECTION_KEYS.APPROVAL_MODE && oldValueTranslationKey && newValueTranslationKey) { + return translateLocal('workspaceActions.updateApprovalMode', { + newValue: translateLocal(`workspaceApprovalModes.${newValueTranslationKey}` as TranslationPaths), + oldValue: translateLocal(`workspaceApprovalModes.${oldValueTranslationKey}` as TranslationPaths), + fieldName: updatedField, + }); + } + + if (updatedField && updatedField === CONST.POLICY.EXPENSE_REPORT_RULES.PREVENT_SELF_APPROVAL && typeof oldValue === 'string' && typeof newValue === 'string') { + return translateLocal('workspaceActions.preventSelfApproval', { + oldValue, + newValue, + }); + } + + if (updatedField && updatedField === CONST.POLICY.EXPENSE_REPORT_RULES.MAX_EXPENSE_AGE && typeof oldValue === 'string' && typeof newValue === 'string') { + return translateLocal('workspaceActions.updateMaxExpenseAge', { + oldValue, + newValue, + }); + } + + return getReportActionText(action); +} + +function getPolicyChangeLogMaxExpesnseAmountNoReceiptMessage(action: ReportAction): string { + const {oldMaxExpenseAmountNoReceipt, newMaxExpenseAmountNoReceipt, currency} = + getOriginalMessage(action as ReportAction) ?? {}; + + if (typeof oldMaxExpenseAmountNoReceipt === 'number' && typeof newMaxExpenseAmountNoReceipt === 'number') { + return translateLocal('workspaceActions.updateMaxExpenseAmountNoReceipt', { + oldValue: convertToDisplayString(oldMaxExpenseAmountNoReceipt, currency), + newValue: convertToDisplayString(newMaxExpenseAmountNoReceipt, currency), + }); + } + + return getReportActionText(action); +} + +function getPolicyChangeLogMaxExpenseAmountMessage(action: ReportAction): string { + const {oldMaxExpenseAmount, newMaxExpenseAmount, currency} = + getOriginalMessage(action as ReportAction) ?? {}; + + if (typeof oldMaxExpenseAmount === 'number' && typeof newMaxExpenseAmount === 'number') { + return translateLocal('workspaceActions.updateMaxExpenseAmount', { + oldValue: convertToDisplayString(oldMaxExpenseAmount, currency), + newValue: convertToDisplayString(newMaxExpenseAmount, currency), + }); + } + + return getReportActionText(action); +} + +function getPolicyChangeLogDefaultBillableMessage(action: ReportAction): string { + const {oldDefaultBillable, newDefaultBillable} = getOriginalMessage(action as ReportAction) ?? {}; + + if (typeof oldDefaultBillable === 'string' && typeof newDefaultBillable === 'string') { + return translateLocal('workspaceActions.updateDefaultBillable', { + oldValue: oldDefaultBillable, + newValue: newDefaultBillable, + }); + } + + return getReportActionText(action); +} + +function getPolicyChangeLogDefaultTitleEnforcedMessage(action: ReportAction): string { + const {value} = getOriginalMessage(action as ReportAction) ?? {}; + + if (typeof value === 'boolean') { + return translateLocal('workspaceActions.updateDefaultTitleEnforced', { + value, + }); + } + + return getReportActionText(action); +} + +function getPolicyChangeLogDeleteMemberMessage(reportAction: OnyxInputOrEntry): string { + if (!isPolicyChangeLogDeleteMemberMessage(reportAction)) { return ''; } const originalMessage = getOriginalMessage(reportAction); - const oldFrequency = translateLocal(`workspace.common.frequency.${originalMessage?.oldFrequency}` as TranslationPaths); - const newFrequency = translateLocal(`workspace.common.frequency.${originalMessage?.newFrequency}` as TranslationPaths); - return translateLocal('report.actions.type.updateAutoReportingFrequency', {oldFrequency, newFrequency}); + const email = originalMessage?.email ?? ''; + const role = originalMessage?.role ?? ''; + return translateLocal('report.actions.type.removeMember', {email, role}); } function getRemovedConnectionMessage(reportAction: OnyxEntry): string { @@ -1835,6 +2099,13 @@ function getRemovedFromApprovalChainMessage(reportAction: OnyxEntry>) { + const originalMessage = getOriginalMessage(reportAction); + const policyName = originalMessage?.policyName ?? translateLocal('workspace.common.workspace'); + const oldRole = translateLocal('workspace.common.roleName', {role: originalMessage?.oldRole}).toLowerCase(); + return translateLocal('workspaceActions.demotedFromWorkspace', {policyName, oldRole}); +} + function isCardIssuedAction(reportAction: OnyxEntry) { return isActionOfType( reportAction, @@ -1964,6 +2235,7 @@ export { getOneTransactionThreadReportID, getOriginalMessage, getRemovedFromApprovalChainMessage, + getDemotedFromWorkspaceMessage, getReportAction, getReportActionHtml, getReportActionMessage, @@ -2042,7 +2314,6 @@ export { getPolicyChangeLogAddEmployeeMessage, getPolicyChangeLogChangeRoleMessage, getPolicyChangeLogDeleteMemberMessage, - getPolicyChangeLogUpdateAutoReportingFrequencyMessage, getPolicyChangeLogEmployeeLeftMessage, getRenamedAction, isCardIssuedAction, @@ -2051,6 +2322,21 @@ export { getActionableJoinRequestPendingReportAction, getReportActionsLength, wasActionCreatedWhileOffline, + getWorkspaceCategoryUpdateMessage, + getWorkspaceUpdateFieldMessage, + getWorkspaceNameUpdatedMessage, + getWorkspaceCurrencyUpdateMessage, + getWorkspaceFrequencyUpdateMessage, + getPolicyChangeLogMaxExpesnseAmountNoReceiptMessage, + getPolicyChangeLogMaxExpenseAmountMessage, + getPolicyChangeLogDefaultBillableMessage, + getPolicyChangeLogDefaultTitleEnforcedMessage, + getWorkspaceDescriptionUpdatedMessage, + getWorkspaceReportFieldAddMessage, + getWorkspaceCustomUnitRateAddedMessage, + getWorkspaceTagUpdateMessage, + getWorkspaceReportFieldUpdateMessage, + getWorkspaceReportFieldDeleteMessage, }; export type {LastVisibleMessage}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index a7a67f6de26f..f0defab02586 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -64,7 +64,7 @@ import {autoSwitchToFocusMode} from './actions/PriorityMode'; import {hasCreditBankAccount} from './actions/ReimbursementAccount/store'; import {handleReportChanged, prepareOnboardingOnyxData} from './actions/Report'; import {isAnonymousUser as isAnonymousUserSession} from './actions/Session'; -import {convertToDisplayString, getCurrencySymbol} from './CurrencyUtils'; +import {convertToDisplayString} from './CurrencyUtils'; import DateUtils from './DateUtils'; import {hasValidDraftComment} from './DraftCommentUtils'; import {getMicroSecondOnyxErrorWithTranslationKey} from './ErrorUtils'; @@ -3599,14 +3599,14 @@ function canEditFieldOfMoneyRequest(reportAction: OnyxInputOrEntry if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.RECEIPT) { const isRequestor = currentUserAccountID === reportAction?.actorAccountID; - return !isInvoiceReport(moneyRequestReport) && + return ( + !isInvoiceReport(moneyRequestReport) && !isReceiptBeingScanned(transaction) && !isDistanceRequest(transaction) && !isPerDiemRequest(transaction) && (isAdmin || isManager || isRequestor) && - isDeleteAction - ? isRequestor - : true; + (isDeleteAction ? isRequestor : true) + ); } if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.DISTANCE_RATE) { @@ -5341,10 +5341,11 @@ function getWorkspaceNameUpdatedMessage(action: ReportAction) { function getDeletedTransactionMessage(action: ReportAction) { const deletedTransactionOriginalMessage = getOriginalMessage(action as ReportAction) ?? {}; - const amount = Math.abs(deletedTransactionOriginalMessage.amount ?? 0) / 100; - const currency = getCurrencySymbol(deletedTransactionOriginalMessage.currency ?? ''); + const amount = Math.abs(deletedTransactionOriginalMessage.amount ?? 0); + const currency = deletedTransactionOriginalMessage.currency ?? ''; + const formattedAmount = convertToDisplayString(amount, currency) ?? ''; const message = translateLocal('iou.deletedTransaction', { - amount: `${currency}${amount}`, + amount: formattedAmount, merchant: deletedTransactionOriginalMessage.merchant ?? '', }); return message; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index fc9ba60be715..f6234da9263a 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -28,16 +28,28 @@ import { getOriginalMessage, getPolicyChangeLogAddEmployeeMessage, getPolicyChangeLogChangeRoleMessage, + getPolicyChangeLogDefaultBillableMessage, + getPolicyChangeLogDefaultTitleEnforcedMessage, getPolicyChangeLogDeleteMemberMessage, getPolicyChangeLogEmployeeLeftMessage, - getPolicyChangeLogUpdateAutoReportingFrequencyMessage, + getPolicyChangeLogMaxExpenseAmountMessage, + getPolicyChangeLogMaxExpesnseAmountNoReceiptMessage, getRemovedConnectionMessage, getRenamedAction, getReportAction, - getReportActionMessage, getReportActionMessageText, getSortedReportActions, getUpdateRoomDescriptionMessage, + getWorkspaceCategoryUpdateMessage, + getWorkspaceCurrencyUpdateMessage, + getWorkspaceCustomUnitRateAddedMessage, + getWorkspaceDescriptionUpdatedMessage, + getWorkspaceFrequencyUpdateMessage, + getWorkspaceReportFieldAddMessage, + getWorkspaceReportFieldDeleteMessage, + getWorkspaceReportFieldUpdateMessage, + getWorkspaceTagUpdateMessage, + getWorkspaceUpdateFieldMessage, isActionOfType, isCardIssuedAction, isInviteOrRemovedAction, @@ -541,14 +553,49 @@ function getOptionData({ result.alternateText = `${lastActorDisplayName} ${getUpdateRoomDescriptionMessage(lastAction)}`; } else if (isActionOfType(lastAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_NAME)) { result.alternateText = getWorkspaceNameUpdatedMessage(lastAction); + } else if (isActionOfType(lastAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DESCRIPTION)) { + result.alternateText = getWorkspaceDescriptionUpdatedMessage(lastAction); + } else if (isActionOfType(lastAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_CURRENCY)) { + result.alternateText = getWorkspaceCurrencyUpdateMessage(lastAction); + } else if (isActionOfType(lastAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AUTO_REPORTING_FREQUENCY)) { + result.alternateText = getWorkspaceFrequencyUpdateMessage(lastAction); + } else if (isActionOfType(lastAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.CORPORATE_UPGRADE)) { + result.alternateText = translateLocal('workspaceActions.upgradedWorkspace'); + } else if (isActionOfType(lastAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.TEAM_DOWNGRADE)) { + result.alternateText = translateLocal('workspaceActions.downgradedWorkspace'); + } else if ( + isActionOfType(lastAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_CATEGORY) || + isActionOfType(lastAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_CATEGORY) || + isActionOfType(lastAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_CATEGORY) || + isActionOfType(lastAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.SET_CATEGORY_NAME) + ) { + result.alternateText = getWorkspaceCategoryUpdateMessage(lastAction); + } else if (isTagModificationAction(lastAction?.actionName)) { + result.alternateText = getCleanedTagName(getWorkspaceTagUpdateMessage(lastAction) ?? ''); + } else if (isActionOfType(lastAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_CUSTOM_UNIT_RATE)) { + result.alternateText = getWorkspaceCustomUnitRateAddedMessage(lastAction); + } else if (isActionOfType(lastAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_REPORT_FIELD)) { + result.alternateText = getWorkspaceReportFieldAddMessage(lastAction); + } else if (isActionOfType(lastAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_REPORT_FIELD)) { + result.alternateText = getWorkspaceReportFieldUpdateMessage(lastAction); + } else if (isActionOfType(lastAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_REPORT_FIELD)) { + result.alternateText = getWorkspaceReportFieldDeleteMessage(lastAction); + } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_FIELD) { + result.alternateText = getWorkspaceUpdateFieldMessage(lastAction); + } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MAX_EXPENSE_AMOUNT_NO_RECEIPT) { + result.alternateText = getPolicyChangeLogMaxExpesnseAmountNoReceiptMessage(lastAction); + } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MAX_EXPENSE_AMOUNT) { + result.alternateText = getPolicyChangeLogMaxExpenseAmountMessage(lastAction); + } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DEFAULT_BILLABLE) { + result.alternateText = getPolicyChangeLogDefaultBillableMessage(lastAction); + } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DEFAULT_TITLE_ENFORCED) { + result.alternateText = getPolicyChangeLogDefaultTitleEnforcedMessage(lastAction); } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.LEAVE_POLICY) { result.alternateText = getPolicyChangeLogEmployeeLeftMessage(lastAction, true); } else if (isCardIssuedAction(lastAction)) { result.alternateText = getCardIssuedMessage({reportAction: lastAction}); } else if (lastAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && lastActorDisplayName && lastMessageTextFromReport) { result.alternateText = formatReportLastMessageText(Parser.htmlToText(`${lastActorDisplayName}: ${lastMessageText}`)); - } else if (isTagModificationAction(lastAction?.actionName)) { - result.alternateText = getCleanedTagName(getReportActionMessage(lastAction)?.text ?? ''); } else if (lastAction && isOldDotReportAction(lastAction)) { result.alternateText = getMessageOfOldDotReportAction(lastAction); } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EMPLOYEE) { @@ -561,8 +608,6 @@ function getOptionData({ result.alternateText = getReportActionMessageText(lastAction) ?? ''; } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_INTEGRATION) { result.alternateText = getRemovedConnectionMessage(lastAction); - } else if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AUTO_REPORTING_FREQUENCY) { - result.alternateText = getPolicyChangeLogUpdateAutoReportingFrequencyMessage(lastAction); } else { result.alternateText = lastMessageTextFromReport.length > 0 diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 2a7f935b07e6..bf188de6e189 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -206,7 +206,7 @@ function hasCardExpiredError() { * @returns Whether there is an insufficient funds error. */ function hasInsufficientFundsError() { - return billingStatus?.declineReason === 'insufficient_funds' && amountOwed !== 0; + return billingStatus?.declineReason === 'insufficient_funds' && getAmountOwed() !== 0; } function shouldShowPreTrialBillingBanner(): boolean { diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index a65b3493171e..21ea41bab8a8 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -425,7 +425,6 @@ function deleteWorkspace(policyID: string, policyName: string) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`, value: { - // eslint-disable-next-line @typescript-eslint/naming-convention private_isArchived: null, }, }); diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index ffcbc813477b..1b151f47fde1 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -892,6 +892,7 @@ function playSoundForMessageType(pushJSON: OnyxServerUpdate[]) { }); } +let pongHasBeenMissed = false; let lastPingSentTimestamp = Date.now(); let lastPongReceivedTimestamp = Date.now(); function subscribeToPusherPong() { @@ -910,6 +911,9 @@ function subscribeToPusherPong() { Log.info(`[Pusher PINGPONG] The event took ${latency} ms`); Timing.end(CONST.TIMING.PUSHER_PING_PONG); + + // When any PONG event comes in, reset this flag so that checkforLatePongReplies will resume looking for missed PONGs + pongHasBeenMissed = false; }); } @@ -947,6 +951,16 @@ function pingPusher() { } function checkforLatePongReplies() { + if (isOffline()) { + Log.info('[Pusher PINGPONG] Skipping checkforLatePongReplies because the client is offline'); + return; + } + + if (pongHasBeenMissed) { + Log.info(`[Pusher PINGPONG] Skipped checking for late PONG events because a PONG has already been missed`); + return; + } + Log.info(`[Pusher PINGPONG] Checking for late PONG events`); const timeSinceLastPongReceived = Date.now() - lastPongReceivedTimestamp; @@ -956,12 +970,14 @@ function checkforLatePongReplies() { // When going offline, reset the pingpong state so that when the network reconnects, the client will start fresh lastPingSentTimestamp = Date.now(); + pongHasBeenMissed = true; } else { Log.info(`[Pusher PINGPONG] Last PONG event was ${timeSinceLastPongReceived} ms ago so not going offline`); } } -let pingPongStarted = false; +let pingPusherIntervalID: ReturnType; +let checkforLatePongRepliesIntervalID: ReturnType; function initializePusherPingPong() { // Only run the ping pong from the leader client if (!ActiveClientManager.isClientTheLeader()) { @@ -969,26 +985,29 @@ function initializePusherPingPong() { return; } - // Ignore any additional calls to initialize the ping pong if it's already been started - if (pingPongStarted) { - return; - } - pingPongStarted = true; - Log.info(`[Pusher PINGPONG] Starting Pusher PING PONG and pinging every ${PING_INTERVAL_LENGTH_IN_SECONDS} seconds`); // Subscribe to the pong event from Pusher. Unfortunately, there is no way of knowing when the client is actually subscribed // so there could be a little delay before the client is actually listening to this event. subscribeToPusherPong(); + // If things are initializing again (which is fine because it will reinitialize each time Pusher authenticates), clear the old intervals + if (pingPusherIntervalID) { + clearInterval(pingPusherIntervalID); + } + // Send a ping to pusher on a regular interval - setInterval(pingPusher, PING_INTERVAL_LENGTH_IN_SECONDS * 1000); + pingPusherIntervalID = setInterval(pingPusher, PING_INTERVAL_LENGTH_IN_SECONDS * 1000); // Delay the start of this by double the length of PING_INTERVAL_LENGTH_IN_SECONDS to give a chance for the first // events to be sent and received setTimeout(() => { + // If things are initializing again (which is fine because it will reinitialize each time Pusher authenticates), clear the old intervals + if (checkforLatePongRepliesIntervalID) { + clearInterval(checkforLatePongRepliesIntervalID); + } // Check for any missing pong events on a regular interval - setInterval(checkforLatePongReplies, CHECK_LATE_PONG_INTERVAL_LENGTH_IN_SECONDS * 1000); + checkforLatePongRepliesIntervalID = setInterval(checkforLatePongReplies, CHECK_LATE_PONG_INTERVAL_LENGTH_IN_SECONDS * 1000); }, PING_INTERVAL_LENGTH_IN_SECONDS * 2); } diff --git a/src/libs/migrations/NVPMigration.ts b/src/libs/migrations/NVPMigration.ts index 09274468a1e3..6a4f61f30407 100644 --- a/src/libs/migrations/NVPMigration.ts +++ b/src/libs/migrations/NVPMigration.ts @@ -12,9 +12,7 @@ const migrations = { preferredLocale: ONYXKEYS.NVP_PREFERRED_LOCALE, preferredEmojiSkinTone: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, frequentlyUsedEmojis: ONYXKEYS.FREQUENTLY_USED_EMOJIS, - // eslint-disable-next-line @typescript-eslint/naming-convention private_blockedFromConcierge: ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE, - // eslint-disable-next-line @typescript-eslint/naming-convention private_pushNotificationID: ONYXKEYS.NVP_PRIVATE_PUSH_NOTIFICATION_ID, tryFocusMode: ONYXKEYS.NVP_TRY_FOCUS_MODE, introSelected: ONYXKEYS.NVP_INTRO_SELECTED, diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 2ab9fca6d0d8..c71bcf45e828 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -19,6 +19,7 @@ import {translateLocal} from '@libs/Localize'; import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; import Navigation from '@libs/Navigation/Navigation'; import Parser from '@libs/Parser'; +import {getCleanedTagName} from '@libs/PolicyUtils'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import { getActionableMentionWhisperMessage, @@ -30,12 +31,25 @@ import { getOriginalMessage, getPolicyChangeLogAddEmployeeMessage, getPolicyChangeLogChangeRoleMessage, + getPolicyChangeLogDefaultBillableMessage, + getPolicyChangeLogDefaultTitleEnforcedMessage, getPolicyChangeLogDeleteMemberMessage, - getPolicyChangeLogUpdateAutoReportingFrequencyMessage, + getPolicyChangeLogMaxExpenseAmountMessage, + getPolicyChangeLogMaxExpesnseAmountNoReceiptMessage, getRemovedConnectionMessage, getRenamedAction, getReportActionMessageText, getUpdateRoomDescriptionMessage, + getWorkspaceCategoryUpdateMessage, + getWorkspaceCurrencyUpdateMessage, + getWorkspaceCustomUnitRateAddedMessage, + getWorkspaceDescriptionUpdatedMessage, + getWorkspaceFrequencyUpdateMessage, + getWorkspaceReportFieldAddMessage, + getWorkspaceReportFieldDeleteMessage, + getWorkspaceReportFieldUpdateMessage, + getWorkspaceTagUpdateMessage, + getWorkspaceUpdateFieldMessage, isActionableMentionWhisper, isActionableTrackExpense, isActionOfType, @@ -52,6 +66,7 @@ import { isRenamedAction, isReportActionAttachment, isReportPreviewAction as isReportPreviewActionReportActionsUtils, + isTagModificationAction, isTaskAction as isTaskActionReportActionsUtils, isTripPreview, isUnapprovedAction, @@ -482,6 +497,39 @@ const ContextMenuActions: ContextMenuAction[] = [ setClipboardMessage(logMessage); } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_NAME) { Clipboard.setString(Str.htmlDecode(getWorkspaceNameUpdatedMessage(reportAction))); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DESCRIPTION) { + Clipboard.setString(getWorkspaceDescriptionUpdatedMessage(reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_CURRENCY) { + Clipboard.setString(getWorkspaceCurrencyUpdateMessage(reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AUTO_REPORTING_FREQUENCY) { + Clipboard.setString(getWorkspaceFrequencyUpdateMessage(reportAction)); + } else if ( + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_CATEGORY || + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_CATEGORY || + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_CATEGORY || + reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.SET_CATEGORY_NAME + ) { + Clipboard.setString(getWorkspaceCategoryUpdateMessage(reportAction)); + } else if (isTagModificationAction(reportAction.actionName)) { + Clipboard.setString(getCleanedTagName(getWorkspaceTagUpdateMessage(reportAction))); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_CUSTOM_UNIT_RATE) { + Clipboard.setString(getWorkspaceCustomUnitRateAddedMessage(reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_REPORT_FIELD) { + Clipboard.setString(getWorkspaceReportFieldAddMessage(reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_REPORT_FIELD) { + Clipboard.setString(getWorkspaceReportFieldUpdateMessage(reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_REPORT_FIELD) { + Clipboard.setString(getWorkspaceReportFieldDeleteMessage(reportAction)); + } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_FIELD) { + setClipboardMessage(getWorkspaceUpdateFieldMessage(reportAction)); + } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MAX_EXPENSE_AMOUNT_NO_RECEIPT) { + Clipboard.setString(getPolicyChangeLogMaxExpesnseAmountNoReceiptMessage(reportAction)); + } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MAX_EXPENSE_AMOUNT) { + Clipboard.setString(getPolicyChangeLogMaxExpenseAmountMessage(reportAction)); + } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DEFAULT_BILLABLE) { + Clipboard.setString(getPolicyChangeLogDefaultBillableMessage(reportAction)); + } else if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DEFAULT_TITLE_ENFORCED) { + Clipboard.setString(getPolicyChangeLogDefaultTitleEnforcedMessage(reportAction)); } else if (isReimbursementQueuedAction(reportAction)) { Clipboard.setString(getReimbursementQueuedActionMessage({reportAction, reportOrID: reportID, shouldUseShortDisplayName: false})); } else if (isActionableMentionWhisper(reportAction)) { @@ -545,8 +593,6 @@ const ContextMenuActions: ContextMenuAction[] = [ setClipboardMessage(getPolicyChangeLogChangeRoleMessage(reportAction)); } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_EMPLOYEE) { setClipboardMessage(getPolicyChangeLogDeleteMemberMessage(reportAction)); - } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AUTO_REPORTING_FREQUENCY) { - setClipboardMessage(getPolicyChangeLogUpdateAutoReportingFrequencyMessage(reportAction)); } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.DELETED_TRANSACTION) { setClipboardMessage(getDeletedTransactionMessage(reportAction)); } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.INTEGRATION_SYNC_FAILED)) { diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index 49a855fc690b..7d9a0fe81d78 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -50,19 +50,33 @@ import {getCleanedTagName} from '@libs/PolicyUtils'; import { extractLinksFromMessageHtml, getAllReportActions, + getDemotedFromWorkspaceMessage, getDismissedViolationMessageText, getIOUReportIDFromReportActionPreview, getOriginalMessage, getPolicyChangeLogAddEmployeeMessage, getPolicyChangeLogChangeRoleMessage, + getPolicyChangeLogDefaultBillableMessage, + getPolicyChangeLogDefaultTitleEnforcedMessage, getPolicyChangeLogDeleteMemberMessage, - getPolicyChangeLogUpdateAutoReportingFrequencyMessage, + getPolicyChangeLogMaxExpenseAmountMessage, + getPolicyChangeLogMaxExpesnseAmountNoReceiptMessage, getRemovedConnectionMessage, getRemovedFromApprovalChainMessage, getRenamedAction, getReportActionMessage, getReportActionText, getWhisperedTo, + getWorkspaceCategoryUpdateMessage, + getWorkspaceCurrencyUpdateMessage, + getWorkspaceCustomUnitRateAddedMessage, + getWorkspaceDescriptionUpdatedMessage, + getWorkspaceFrequencyUpdateMessage, + getWorkspaceReportFieldAddMessage, + getWorkspaceReportFieldDeleteMessage, + getWorkspaceReportFieldUpdateMessage, + getWorkspaceTagUpdateMessage, + getWorkspaceUpdateFieldMessage, isActionableAddPaymentCard, isActionableJoinRequest, isActionableMentionWhisper, @@ -868,20 +882,51 @@ function PureReportActionItem({ children = ; } else if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.DISMISSED_VIOLATION)) { children = ; - } else if (isTagModificationAction(action.actionName)) { - children = ; } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_NAME) { children = ; + } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DESCRIPTION) { + children = ; + } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_CURRENCY) { + children = ; + } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AUTO_REPORTING_FREQUENCY) { + children = ; + } else if ( + action.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_CATEGORY || + action.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_CATEGORY || + action.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_CATEGORY || + action.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.SET_CATEGORY_NAME + ) { + children = ; + } else if (isTagModificationAction(action.actionName)) { + children = ; + } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_CUSTOM_UNIT_RATE) { + children = ; + } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_REPORT_FIELD) { + children = ; + } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_REPORT_FIELD) { + children = ; + } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_REPORT_FIELD) { + children = ; + } else if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_FIELD)) { + children = ; + } else if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MAX_EXPENSE_AMOUNT_NO_RECEIPT)) { + children = ; + } else if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MAX_EXPENSE_AMOUNT)) { + children = ; + } else if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DEFAULT_BILLABLE)) { + children = ; + } else if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_DEFAULT_TITLE_ENFORCED)) { + children = ; } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.ADD_EMPLOYEE) { children = ; } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_EMPLOYEE) { children = ; } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.DELETE_EMPLOYEE) { children = ; - } else if (action.actionName === CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_AUTO_REPORTING_FREQUENCY) { - children = ; } else if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.REMOVED_FROM_APPROVAL_CHAIN)) { children = ; + } else if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.DEMOTED_FROM_WORKSPACE)) { + children = ; } else if ( isActionOfType( action, diff --git a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx index 38c084ce830b..2f61efe871d5 100644 --- a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx @@ -31,6 +31,7 @@ import {canActionTask as canActionTaskUtils, canModifyTask as canModifyTaskUtils import {setSelfTourViewed} from '@libs/actions/Welcome'; import getIconForAction from '@libs/getIconForAction'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import navigateAfterInteraction from '@libs/Navigation/navigateAfterInteraction'; import Navigation from '@libs/Navigation/Navigation'; import {hasSeenTourSelector} from '@libs/onboardingSelectors'; import {areAllGroupPoliciesExpenseChatDisabled, canSendInvoice as canSendInvoicePolicyUtils, shouldShowPolicy} from '@libs/PolicyUtils'; @@ -454,6 +455,80 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT const canModifyTask = canModifyTaskUtils(viewTourTaskReport, currentUserPersonalDetails.accountID); const canActionTask = canActionTaskUtils(viewTourTaskReport, currentUserPersonalDetails.accountID); + const menuItems = [ + ...expenseMenuItems, + { + icon: Expensicons.ChatBubble, + text: translate('sidebarScreen.fabNewChat'), + onSelected: () => interceptAnonymousUser(startNewChat), + }, + ...(canSendInvoice + ? [ + { + icon: Expensicons.InvoiceGeneric, + text: translate('workspace.invoices.sendInvoice'), + shouldCallAfterModalHide: shouldRedirectToExpensifyClassic, + onSelected: () => + interceptAnonymousUser(() => { + if (shouldRedirectToExpensifyClassic) { + setModalVisible(true); + return; + } + + startMoneyRequest( + CONST.IOU.TYPE.INVOICE, + // When starting to create an invoice from the global FAB, there is not an existing report yet. A random optimistic reportID is generated and used + // for all of the routes in the creation flow. + generateReportID(), + ); + }), + }, + ] + : []), + ...(canUseSpotnanaTravel + ? [ + { + icon: Expensicons.Suitcase, + text: translate('travel.bookTravel'), + onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS)), + }, + ] + : []), + ...(!hasSeenTour + ? [ + { + icon: Expensicons.Binoculars, + iconStyles: styles.popoverIconCircle, + iconFill: theme.icon, + text: translate('tour.takeATwoMinuteTour'), + description: translate('tour.exploreExpensify'), + onSelected: () => { + openExternalLink(navatticURL); + setSelfTourViewed(isAnonymousUser()); + if (viewTourTaskReport && canModifyTask && canActionTask) { + completeTask(viewTourTaskReport); + } + }, + }, + ] + : []), + ...(!isLoading && shouldShowNewWorkspaceButton + ? [ + { + displayInDefaultIconColor: true, + contentFit: 'contain' as ImageContentFit, + icon: Expensicons.NewWorkspace, + iconWidth: variables.w46, + iconHeight: variables.h40, + text: translate('workspace.new.newWorkspace'), + description: translate('workspace.new.getTheExpensifyCardAndMore'), + onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.WORKSPACE_CONFIRMATION.getRoute(Navigation.getActiveRoute()))), + }, + ] + : []), + ...quickActionMenuItems, + ]; + return ( interceptAnonymousUser(startNewChat), - }, - ...(canSendInvoice - ? [ - { - icon: Expensicons.InvoiceGeneric, - text: translate('workspace.invoices.sendInvoice'), - shouldCallAfterModalHide: shouldRedirectToExpensifyClassic, - onSelected: () => - interceptAnonymousUser(() => { - if (shouldRedirectToExpensifyClassic) { - setModalVisible(true); - return; - } - - startMoneyRequest( - CONST.IOU.TYPE.INVOICE, - // When starting to create an invoice from the global FAB, there is not an existing report yet. A random optimistic reportID is generated and used - // for all of the routes in the creation flow. - generateReportID(), - ); - }), - }, - ] - : []), - ...(canUseSpotnanaTravel - ? [ - { - icon: Expensicons.Suitcase, - text: translate('travel.bookTravel'), - onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS)), - }, - ] - : []), - ...(!hasSeenTour - ? [ - { - icon: Expensicons.Binoculars, - iconStyles: styles.popoverIconCircle, - iconFill: theme.icon, - text: translate('tour.takeATwoMinuteTour'), - description: translate('tour.exploreExpensify'), - onSelected: () => { - openExternalLink(navatticURL); - setSelfTourViewed(isAnonymousUser()); - if (viewTourTaskReport && canModifyTask && canActionTask) { - completeTask(viewTourTaskReport); - } - }, - }, - ] - : []), - ...(!isLoading && shouldShowNewWorkspaceButton - ? [ - { - displayInDefaultIconColor: true, - contentFit: 'contain' as ImageContentFit, - icon: Expensicons.NewWorkspace, - iconWidth: variables.w46, - iconHeight: variables.h40, - text: translate('workspace.new.newWorkspace'), - description: translate('workspace.new.getTheExpensifyCardAndMore'), - onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.WORKSPACE_CONFIRMATION.getRoute(Navigation.getActiveRoute()))), - }, - ] - : []), - ...quickActionMenuItems, - ]} + menuItems={menuItems.map((item) => { + return { + ...item, + onSelected: () => { + if (!item.onSelected) { + return; + } + navigateAfterInteraction(item.onSelected); + }, + }; + })} withoutOverlay anchorRef={fabRef} /> diff --git a/src/pages/iou/request/step/IOURequestStepTime.tsx b/src/pages/iou/request/step/IOURequestStepTime.tsx index 9b78de8951bd..76dca80e8cd9 100644 --- a/src/pages/iou/request/step/IOURequestStepTime.tsx +++ b/src/pages/iou/request/step/IOURequestStepTime.tsx @@ -17,7 +17,7 @@ import {getIOURequestPolicyID, setMoneyRequestDateAttribute} from '@userActions/ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type SCREENS from '@src/SCREENS'; +import SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/MoneyRequestTimeForm'; import type * as OnyxTypes from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -26,7 +26,7 @@ import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; -type IOURequestStepTimeProps = WithWritableReportOrNotFoundProps & { +type IOURequestStepTimeProps = WithWritableReportOrNotFoundProps & { /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ transaction: OnyxEntry; @@ -37,6 +37,7 @@ type IOURequestStepTimeProps = WithWritableReportOrNotFoundProps { + if (isEditPage) { + Navigation.goBack(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(action, iouType, transactionID, reportID)); + return; + } + if (backTo) { Navigation.goBack(backTo); return; @@ -78,7 +85,7 @@ function IOURequestStepTime({ setMoneyRequestDateAttribute(transactionID, newStart, newEnd); - if (backTo) { + if (isEditPage) { navigateBack(); } else { Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_SUBRATE.getRoute(action, iouType, transactionID, reportID)); diff --git a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx index 5aa581426b50..3f0512e94c46 100644 --- a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx +++ b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx @@ -43,6 +43,7 @@ type MoneyRequestRouteName = | typeof SCREENS.MONEY_REQUEST.STEP_COMPANY_INFO | typeof SCREENS.MONEY_REQUEST.STEP_DESTINATION | typeof SCREENS.MONEY_REQUEST.STEP_TIME + | typeof SCREENS.MONEY_REQUEST.STEP_TIME_EDIT | typeof SCREENS.MONEY_REQUEST.STEP_SUBRATE; type WithFullTransactionOrNotFoundProps = WithFullTransactionOrNotFoundOnyxProps & diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx index 9ee3740be2a1..e3ffa11a2e24 100644 --- a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx +++ b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx @@ -45,6 +45,7 @@ type MoneyRequestRouteName = | typeof SCREENS.MONEY_REQUEST.STEP_UPGRADE | typeof SCREENS.MONEY_REQUEST.STEP_DESTINATION | typeof SCREENS.MONEY_REQUEST.STEP_TIME + | typeof SCREENS.MONEY_REQUEST.STEP_TIME_EDIT | typeof SCREENS.MONEY_REQUEST.STEP_SUBRATE; type WithWritableReportOrNotFoundProps = WithWritableReportOrNotFoundOnyxProps & PlatformStackScreenProps; diff --git a/src/pages/workspace/accounting/PolicyAccountingPage.tsx b/src/pages/workspace/accounting/PolicyAccountingPage.tsx index dae6d7b20432..73cf0ad12df6 100644 --- a/src/pages/workspace/accounting/PolicyAccountingPage.tsx +++ b/src/pages/workspace/accounting/PolicyAccountingPage.tsx @@ -256,6 +256,18 @@ function PolicyAccountingPage({policy}: PolicyAccountingPageProps) { Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_ENTITY.getRoute(policyID)); }, }; + case CONST.POLICY.CONNECTIONS.NAME.QBO: + return !policy?.connections?.quickbooksOnline?.config?.companyName + ? {} + : { + description: translate('workspace.qbo.connectedTo'), + title: policy?.connections?.quickbooksOnline?.config?.companyName, + wrapperStyle: [styles.sectionMenuItemTopDescription], + titleStyle: styles.fontWeightNormal, + shouldShowDescriptionOnTop: true, + interactive: false, + }; + default: return undefined; } diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx index 016cbe91cb72..983795fd3e7c 100644 --- a/src/pages/workspace/categories/CategorySettingsPage.tsx +++ b/src/pages/workspace/categories/CategorySettingsPage.tsx @@ -3,7 +3,7 @@ import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import * as Expensicons from '@components/Icon/Expensicons'; +import {Trashcan} from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -13,21 +13,25 @@ import Switch from '@components/Switch'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; -import usePermissions from '@hooks/usePermissions'; import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as CategoryUtils from '@libs/CategoryUtils'; -import * as CurrencyUtils from '@libs/CurrencyUtils'; -import * as ErrorUtils from '@libs/ErrorUtils'; +import {formatDefaultTaxRateText, formatRequireReceiptsOverText, getCategoryApproverRule, getCategoryDefaultTaxRate} from '@libs/CategoryUtils'; +import {convertToDisplayString} from '@libs/CurrencyUtils'; +import {getLatestErrorMessageField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; -import {isControlPolicy} from '@libs/PolicyUtils'; -import * as PolicyUtils from '@libs/PolicyUtils'; +import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils'; +import {getWorkflowApprovalsUnavailable, isControlPolicy} from '@libs/PolicyUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import * as Category from '@userActions/Policy/Category'; +import { + clearCategoryErrors, + deleteWorkspaceCategories, + setPolicyCategoryDescriptionRequired, + setWorkspaceCategoryDescriptionHint, + setWorkspaceCategoryEnabled, +} from '@userActions/Policy/Category'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -44,7 +48,6 @@ function CategorySettingsPage({ const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); const styles = useThemeStyles(); const {translate} = useLocalize(); - const {canUseCategoryAndTagApprovers} = usePermissions(); const [deleteCategoryConfirmModalVisible, setDeleteCategoryConfirmModalVisible] = useState(false); const policy = usePolicy(policyID); @@ -71,19 +74,19 @@ function CategorySettingsPage({ return ''; } - return `${CurrencyUtils.convertToDisplayString(policyCategory?.maxExpenseAmount, policyCurrency)} ${CONST.DOT_SEPARATOR} ${translate( + return `${convertToDisplayString(policyCategory?.maxExpenseAmount, policyCurrency)} ${CONST.DOT_SEPARATOR} ${translate( `workspace.rules.categoryRules.expenseLimitTypes.${policyCategoryExpenseLimitType}`, )}`; }, [policyCategory?.maxExpenseAmount, policyCategoryExpenseLimitType, policyCurrency, translate]); const approverText = useMemo(() => { - const categoryApprover = CategoryUtils.getCategoryApproverRule(policy?.rules?.approvalRules ?? [], categoryName)?.approver ?? ''; - const approver = PersonalDetailsUtils.getPersonalDetailByEmail(categoryApprover); + const categoryApprover = getCategoryApproverRule(policy?.rules?.approvalRules ?? [], categoryName)?.approver ?? ''; + const approver = getPersonalDetailByEmail(categoryApprover); return approver?.displayName ?? categoryApprover; }, [categoryName, policy?.rules?.approvalRules]); const defaultTaxRateText = useMemo(() => { - const taxID = CategoryUtils.getCategoryDefaultTaxRate(policy?.rules?.expenseRules ?? [], categoryName, policy?.taxRates?.defaultExternalID); + const taxID = getCategoryDefaultTaxRate(policy?.rules?.expenseRules ?? [], categoryName, policy?.taxRates?.defaultExternalID); if (!taxID) { return ''; @@ -95,14 +98,14 @@ function CategorySettingsPage({ return ''; } - return CategoryUtils.formatDefaultTaxRateText(translate, taxID, taxRate, policy?.taxRates); + return formatDefaultTaxRateText(translate, taxID, taxRate, policy?.taxRates); }, [categoryName, policy?.rules?.expenseRules, policy?.taxRates, translate]); const requireReceiptsOverText = useMemo(() => { if (!policy) { return ''; } - return CategoryUtils.formatRequireReceiptsOverText(translate, policy, policyCategory?.maxAmountNoReceipt); + return formatRequireReceiptsOverText(translate, policy, policyCategory?.maxAmountNoReceipt); }, [policy, policyCategory?.maxAmountNoReceipt, translate]); if (!policyCategory) { @@ -110,7 +113,7 @@ function CategorySettingsPage({ } const updateWorkspaceRequiresCategory = (value: boolean) => { - Category.setWorkspaceCategoryEnabled(policyID, {[policyCategory.name]: {name: policyCategory.name, enabled: value}}); + setWorkspaceCategoryEnabled(policyID, {[policyCategory.name]: {name: policyCategory.name, enabled: value}}); }; const navigateToEditCategory = () => { @@ -120,13 +123,13 @@ function CategorySettingsPage({ }; const deleteCategory = () => { - Category.deleteWorkspaceCategories(policyID, [categoryName]); + deleteWorkspaceCategories(policyID, [categoryName]); setDeleteCategoryConfirmModalVisible(false); navigateBack(); }; const isThereAnyAccountingConnection = Object.keys(policy?.connections ?? {}).length !== 0; - const workflowApprovalsUnavailable = PolicyUtils.getWorkflowApprovalsUnavailable(policy); + const workflowApprovalsUnavailable = getWorkflowApprovalsUnavailable(policy); const approverDisabled = !policy?.areWorkflowsEnabled || workflowApprovalsUnavailable; return ( @@ -158,10 +161,10 @@ function CategorySettingsPage({ /> Category.clearCategoryErrors(policyID, categoryName)} + onClose={() => clearCategoryErrors(policyID, categoryName)} > @@ -247,9 +250,9 @@ function CategorySettingsPage({ accessibilityLabel={translate('workspace.rules.categoryRules.requireDescription')} onToggle={() => { if (policyCategory.commentHint && areCommentsRequired) { - Category.setWorkspaceCategoryDescriptionHint(policyID, categoryName, ''); + setWorkspaceCategoryDescriptionHint(policyID, categoryName, ''); } - Category.setPolicyCategoryDescriptionRequired(policyID, categoryName, !areCommentsRequired); + setPolicyCategoryDescriptionRequired(policyID, categoryName, !areCommentsRequired); }} /> @@ -267,30 +270,26 @@ function CategorySettingsPage({ /> )} - {!!canUseCategoryAndTagApprovers && ( - <> - { - Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_APPROVER.getRoute(policyID, policyCategory.name)); - }} - shouldShowRightIcon - disabled={approverDisabled} - /> - {approverDisabled && ( - - {translate('workspace.rules.categoryRules.goTo')}{' '} - Navigation.navigate(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID))} - > - {translate('workspace.common.moreFeatures')} - {' '} - {translate('workspace.rules.categoryRules.andEnableWorkflows')} - - )} - + { + Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_APPROVER.getRoute(policyID, policyCategory.name)); + }} + shouldShowRightIcon + disabled={approverDisabled} + /> + {approverDisabled && ( + + {translate('workspace.rules.categoryRules.goTo')}{' '} + Navigation.navigate(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID))} + > + {translate('workspace.common.moreFeatures')} + {' '} + {translate('workspace.rules.categoryRules.andEnableWorkflows')} + )} {!!policy?.tax?.trackingEnabled && ( setDeleteCategoryConfirmModalVisible(true)} /> diff --git a/src/pages/workspace/perDiem/WorkspacePerDiemDetailsPage.tsx b/src/pages/workspace/perDiem/WorkspacePerDiemDetailsPage.tsx index 3e53a1681274..be60e46c1564 100644 --- a/src/pages/workspace/perDiem/WorkspacePerDiemDetailsPage.tsx +++ b/src/pages/workspace/perDiem/WorkspacePerDiemDetailsPage.tsx @@ -10,14 +10,15 @@ import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import useLocalize from '@hooks/useLocalize'; +import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; -import {convertToFrontendAmountAsString, getCurrencySymbol} from '@libs/CurrencyUtils'; +import {convertToDisplayStringWithoutCurrency, getCurrencySymbol} from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import {getPerDiemCustomUnit} from '@libs/PolicyUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import * as PerDiem from '@userActions/Policy/PerDiem'; +import {deleteWorkspacePerDiemRates} from '@userActions/Policy/PerDiem'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -38,15 +39,18 @@ function WorkspacePerDiemDetailsPage({route}: WorkspacePerDiemDetailsPageProps) const customUnit = getPerDiemCustomUnit(policy); const selectedRate = customUnit?.rates?.[rateID]; - const selectedSubRate = selectedRate?.subRates?.find((subRate) => subRate.id === subRateID); + const fetchedSubRate = selectedRate?.subRates?.find((subRate) => subRate.id === subRateID); + const previousFetchedSubRate = usePrevious(fetchedSubRate); - const amountValue = selectedSubRate?.rate ? convertToFrontendAmountAsString(Number(selectedSubRate.rate)) : undefined; + const selectedSubRate = fetchedSubRate ?? previousFetchedSubRate; + + const amountValue = selectedSubRate?.rate ? convertToDisplayStringWithoutCurrency(Number(selectedSubRate.rate), selectedRate?.currency) : undefined; const currencyValue = selectedRate?.currency ? `${selectedRate.currency} - ${getCurrencySymbol(selectedRate.currency)}` : undefined; const FullPageBlockingView = isEmptyObject(selectedSubRate) ? FullPageOfflineBlockingView : View; const handleDeletePerDiemRate = () => { - PerDiem.deleteWorkspacePerDiemRates(policyID, customUnit, [ + deleteWorkspacePerDiemRates(policyID, customUnit, [ { destination: selectedRate?.name ?? '', subRateName: selectedSubRate?.name ?? '', diff --git a/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx b/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx index 3240e400a2eb..03212428f3e6 100644 --- a/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx +++ b/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx @@ -2,6 +2,7 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'; import lodashSortBy from 'lodash/sortBy'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {ActivityIndicator, View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; @@ -28,19 +29,21 @@ import useSearchBackPress from '@hooks/useSearchBackPress'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; -import {deleteWorkspacePerDiemRates, downloadPerDiemCSV, openPolicyPerDiemPage} from '@libs/actions/Policy/PerDiem'; import {convertAmountToDisplayString} from '@libs/CurrencyUtils'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import localeCompare from '@libs/LocaleCompare'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; +import {hasEnabledOptions} from '@libs/OptionsListUtils'; import {getPerDiemCustomUnit} from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import {openExternalLink} from '@userActions/Link'; +import {turnOffMobileSelectionMode} from '@userActions/MobileSelectionMode'; import {close} from '@userActions/Modal'; +import {deleteWorkspacePerDiemRates, downloadPerDiemCSV, openPolicyPerDiemPage} from '@userActions/Policy/PerDiem'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {PendingAction} from '@src/types/onyx/OnyxCommon'; @@ -124,6 +127,7 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { const policyID = route.params.policyID; const backTo = route.params?.backTo; const policy = usePolicy(policyID); + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); const {selectionMode} = useMobileSelectionMode(); const customUnit = getPerDiemCustomUnit(policy); @@ -268,6 +272,10 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { ); } + if (!policy?.areCategoriesEnabled || !hasEnabledOptions(policyCategories ?? {})) { + return null; + } + return (