Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,8 @@ const translations = {
scanMultipleReceiptsDescription: 'Snap photos of all your receipts at once, then confirm details yourself or let SmartScan handle it.',
receiptScanInProgress: 'Receipt scan in progress',
receiptScanInProgressDescription: 'Receipt scan in progress. Check back later or enter the details now.',
removeFromReport: 'Remove from report',
moveToPersonalSpace: 'Move expenses to your personal space',
duplicateTransaction: ({isSubmitted}: DuplicateTransactionParams) =>
!isSubmitted
? 'Potential duplicate expenses identified. Review duplicates to enable submission.'
Expand Down
2 changes: 2 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,8 @@ const translations = {
scanMultipleReceiptsDescription: 'Tome fotos de todos sus recibos a la vez y confirme los detalles usted mismo o deje que SmartScan se encargue.',
receiptScanInProgress: 'Escaneado de recibo en proceso',
receiptScanInProgressDescription: 'Escaneado de recibo en proceso. Vuelve a comprobarlo más tarde o introduce los detalles ahora.',
removeFromReport: 'Eliminar del informe',
moveToPersonalSpace: 'Mover gastos a tu espacio personal',
duplicateTransaction: ({isSubmitted}: DuplicateTransactionParams) =>
!isSubmitted
? 'Se han identificado posibles gastos duplicados. Revisa los duplicados para habilitar el envío.'
Expand Down
2 changes: 2 additions & 0 deletions src/libs/API/parameters/ChangeTransactionsReportParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ type TransactionThreadInfo = {
moneyRequestPreviewReportActionID: string;
transactionThreadReportID?: string;
transactionThreadCreatedReportActionID?: string;
selfDMReportID?: string;
selfDMCreatedReportActionID?: string;
};

type ChangeTransactionsReportParams = {
Expand Down
3 changes: 2 additions & 1 deletion src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3956,9 +3956,10 @@ function canEditFieldOfMoneyRequest(reportAction: OnyxInputOrEntry<ReportAction>

if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.REPORT) {
const isUnreported = !moneyRequestReport || isEmptyObject(moneyRequestReport);
const isOwner = moneyRequestReport?.ownerAccountID === currentUserAccountID;
return isUnreported
? Object.values(allPolicies ?? {}).flatMap((currentPolicy) => getOutstandingReportsForUser(currentPolicy?.id, currentUserAccountID, allReports ?? {})).length > 0
: getOutstandingReportsForUser(moneyRequestReport?.policyID, moneyRequestReport?.ownerAccountID, allReports ?? {}).length > 1;
: getOutstandingReportsForUser(moneyRequestReport?.policyID, moneyRequestReport?.ownerAccountID, allReports ?? {}).length > 1 || isOwner;
}

return true;
Expand Down
133 changes: 126 additions & 7 deletions src/libs/actions/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ import {
buildOptimisticCreatedReportAction,
buildOptimisticDismissedViolationReportAction,
buildOptimisticMovedTransactionAction,
buildOptimisticSelfDMReport,
buildOptimisticUnreportedTransactionAction,
buildTransactionThread,
findSelfDMReportID,
} from '@libs/ReportUtils';
import {getAmount, waypointHasValidAddress} from '@libs/TransactionUtils';
import CONST from '@src/CONST';
Expand Down Expand Up @@ -599,18 +601,103 @@ function setTransactionReport(transactionID: string, reportID: string, isDraft:

function changeTransactionsReport(transactionIDs: string[], reportID: string) {
const newReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
if (!newReport) {
return;
}

const transactions = transactionIDs.map((id) => allTransactions?.[id]).filter((t): t is NonNullable<typeof t> => t !== undefined);
const transactionIDToReportActionAndThreadData: Record<string, TransactionThreadInfo> = {};
const updatedReportTotals: Record<string, number> = {};

// Store current violations for each transaction to restore on failure
const currentTransactionViolations: Record<string, TransactionViolation[]> = {};
transactionIDs.forEach((id) => {
currentTransactionViolations[id] = allTransactionViolation?.[id] ?? [];
});

const optimisticData: OnyxUpdate[] = [];
const failureData: OnyxUpdate[] = [];
const successData: OnyxUpdate[] = [];

const existingSelfDMReportID = findSelfDMReportID();
let selfDMReport: Report;
let selfDMCreatedReportAction: ReportAction;

if (!existingSelfDMReportID && reportID === CONST.REPORT.UNREPORTED_REPORT_ID) {
const currentTime = DateUtils.getDBTime();
selfDMReport = buildOptimisticSelfDMReport(currentTime);
selfDMCreatedReportAction = buildOptimisticCreatedReportAction(currentUserEmail ?? '', currentTime);

// Add optimistic updates for self DM report
optimisticData.push(
{
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.REPORT}${selfDMReport.reportID}`,
value: {
...selfDMReport,
pendingFields: {
createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
},
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${selfDMReport.reportID}`,
value: {isOptimisticReport: true},
},
{
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${selfDMReport.reportID}`,
value: {
[selfDMCreatedReportAction.reportActionID]: selfDMCreatedReportAction,
},
},
);

// Add success data for self DM report
successData.push(
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${selfDMReport.reportID}`,
value: {
pendingFields: {
createChat: null,
},
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${selfDMReport.reportID}`,
value: {isOptimisticReport: false},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${selfDMReport.reportID}`,
value: {
[selfDMCreatedReportAction.reportActionID]: {
pendingAction: null,
},
},
},
);

// Add failure data for self DM report
failureData.push(
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${selfDMReport.reportID}`,
value: null,
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${selfDMReport.reportID}`,
value: null,
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${selfDMReport.reportID}`,
value: null,
},
);
}

let transactionsMoved = false;

transactions.forEach((transaction) => {
Expand Down Expand Up @@ -649,12 +736,31 @@ function changeTransactionsReport(transactionIDs: string[], reportID: string) {
},
});

// Optimistically clear all violations for the transaction
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`,
value: [],
});

successData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`,
value: [],
});

failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`,
value: currentTransactionViolations[transaction.transactionID],
});

// 2. Keep track of the new report totals
const transactionAmount = getAmount(transaction);
if (oldReportID) {
updatedReportTotals[oldReportID] = (updatedReportTotals[oldReportID] ? updatedReportTotals[oldReportID] : (oldReport?.total ?? 0)) + transactionAmount;
}
if (reportID) {
if (reportID && newReport) {
updatedReportTotals[reportID] = (updatedReportTotals[reportID] ? updatedReportTotals[reportID] : (newReport.total ?? 0)) - transactionAmount;
}

Expand All @@ -670,9 +776,10 @@ function changeTransactionsReport(transactionIDs: string[], reportID: string) {
};

if (oldIOUAction) {
const targetReportID = reportID === CONST.REPORT.UNREPORTED_REPORT_ID ? (existingSelfDMReportID ?? selfDMReport.reportID) : reportID;
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${targetReportID}`,
value: {
[newIOUAction.reportActionID]: newIOUAction,
},
Expand Down Expand Up @@ -733,7 +840,7 @@ function changeTransactionsReport(transactionIDs: string[], reportID: string) {
value: {
parentReportID: reportID,
parentReportActionID: optimisticMoneyRequestReportActionID,
policyID: reportID !== CONST.REPORT.UNREPORTED_REPORT_ID ? newReport.policyID : CONST.POLICY.ID_FAKE,
policyID: reportID !== CONST.REPORT.UNREPORTED_REPORT_ID && newReport ? newReport.policyID : CONST.POLICY.ID_FAKE,
},
});

Expand Down Expand Up @@ -833,7 +940,8 @@ function changeTransactionsReport(transactionIDs: string[], reportID: string) {
value: {[movedAction?.reportActionID]: null},
});

transactionIDToReportActionAndThreadData[transaction.transactionID] = {
// Create base transaction data object
const baseTransactionData = {
movedReportActionID: movedAction.reportActionID,
moneyRequestPreviewReportActionID: newIOUAction.reportActionID,
...(oldIOUAction && !oldIOUAction.childReportID
Expand All @@ -843,6 +951,17 @@ function changeTransactionsReport(transactionIDs: string[], reportID: string) {
}
: {}),
};

if (!existingSelfDMReportID && reportID === CONST.REPORT.UNREPORTED_REPORT_ID) {
// Add self DM data to transaction data
transactionIDToReportActionAndThreadData[transaction.transactionID] = {
...baseTransactionData,
selfDMReportID: selfDMReport.reportID,
selfDMCreatedReportActionID: selfDMCreatedReportAction.reportActionID,
};
} else {
transactionIDToReportActionAndThreadData[transaction.transactionID] = baseTransactionData;
}
});

if (!transactionsMoved) {
Expand Down
1 change: 1 addition & 0 deletions src/pages/Search/SearchTransactionsChangeReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ function SearchTransactionsChangeReport() {
return (
<IOURequestEditReportCommon
backTo={undefined}
isEditing={false}
transactionsReports={transactionsReports}
selectReport={selectReport}
/>
Expand Down
11 changes: 11 additions & 0 deletions src/pages/iou/request/step/IOURequestEditReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {useSearchContext} from '@components/Search/SearchContext';
import type {ListItem} from '@components/SelectionList/types';
import {changeTransactionsReport} from '@libs/actions/Transaction';
import Navigation from '@libs/Navigation/Navigation';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
import IOURequestEditReportCommon from './IOURequestEditReportCommon';
Expand Down Expand Up @@ -35,11 +36,21 @@ function IOURequestEditReport({route}: IOURequestEditReportProps) {
Navigation.dismissModalWithReport({reportID: item.value});
};

const removeFromReport = () => {
if (!transactionReport || selectedTransactionIDs.length === 0) {
return;
}
changeTransactionsReport(selectedTransactionIDs, CONST.REPORT.UNREPORTED_REPORT_ID);
Navigation.dismissModal();
};

return (
<IOURequestEditReportCommon
backTo={backTo}
transactionsReports={transactionReport ? [transactionReport] : []}
selectReport={selectReport}
removeFromReport={removeFromReport}
isEditing
/>
);
}
Expand Down
46 changes: 33 additions & 13 deletions src/pages/iou/request/step/IOURequestEditReportCommon.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import React, {useMemo} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
import * as Expensicons from '@components/Icon/Expensicons';
import MenuItem from '@components/MenuItem';
import {useOptionsList} from '@components/OptionListContextProvider';
import SelectionList from '@components/SelectionList';
import InviteMemberListItem from '@components/SelectionList/InviteMemberListItem';
import type {ListItem} from '@components/SelectionList/types';
import UserListItem from '@components/SelectionList/UserListItem';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useDebouncedState from '@hooks/useDebouncedState';
import useLocalize from '@hooks/useLocalize';
import Navigation from '@libs/Navigation/Navigation';
import {getOutstandingReportsForUser} from '@libs/ReportUtils';
import {getOutstandingReportsForUser, getPolicyName} from '@libs/ReportUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Route} from '@src/ROUTES';
Expand Down Expand Up @@ -40,17 +43,23 @@ type Props = {
backTo: Route | undefined;
transactionsReports: Report[];
selectReport: (item: ReportListItem) => void;
removeFromReport?: () => void;
isEditing: boolean;
};

function IOURequestEditReportCommon({backTo, transactionsReports, selectReport}: Props) {
function IOURequestEditReportCommon({backTo, transactionsReports, selectReport, removeFromReport, isEditing}: Props) {
const {translate} = useLocalize();
const {options} = useOptionsList();
const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: (reports) => mapOnyxCollectionItems(reports, reportSelector), canBeMissing: true});
const [reportNameValuePairs] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, {canBeMissing: true});
const [allPoliciesID] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: (policies) => mapOnyxCollectionItems(policies, (policy) => policy?.id), canBeMissing: false});
const onlyReport = transactionsReports.length === 1 ? transactionsReports.at(0) : undefined;

const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState('');

const isOwner = onlyReport ? onlyReport.ownerAccountID === currentUserPersonalDetails.accountID : false;

const expenseReports = useMemo(
() =>
Object.values(allPoliciesID ?? {}).flatMap((policyID) => {
Expand All @@ -73,19 +82,20 @@ function IOURequestEditReportCommon({backTo, transactionsReports, selectReport}:
return [];
}

const onlyReport = transactionsReports.length === 1 ? transactionsReports.at(0) : undefined;

return expenseReports
.sort((a, b) => a?.reportName?.localeCompare(b?.reportName?.toLowerCase() ?? '') ?? 0)
.filter((report) => !debouncedSearchValue || report?.reportName?.toLowerCase().includes(debouncedSearchValue.toLowerCase()))
.filter((report): report is NonNullable<typeof report> => report !== undefined)
.map((report) => ({
text: report.reportName,
value: report.reportID,
keyForList: report.reportID,
isSelected: onlyReport && report.reportID === onlyReport?.reportID,
}));
}, [allReports, debouncedSearchValue, expenseReports, transactionsReports]);
.map((report) => {
const matchingOption = options.reports.find((option) => option.reportID === report.reportID);
return {
...matchingOption,
alternateText: getPolicyName({report}) ?? matchingOption?.alternateText,
value: report.reportID,
isSelected: onlyReport && report.reportID === onlyReport?.reportID,
};
});
}, [allReports, expenseReports, debouncedSearchValue, options.reports, onlyReport]);

const navigateBack = () => {
Navigation.goBack(backTo);
Expand All @@ -111,7 +121,17 @@ function IOURequestEditReportCommon({backTo, transactionsReports, selectReport}:
shouldSingleExecuteRowSelect
headerMessage={headerMessage}
initiallyFocusedOptionKey={transactionsReports.length === 1 ? transactionsReports.at(0)?.reportID : undefined}
ListItem={UserListItem}
ListItem={InviteMemberListItem}
listFooterContent={
isEditing && isOwner ? (
<MenuItem
onPress={removeFromReport}
title={translate('iou.removeFromReport')}
description={translate('iou.moveToPersonalSpace')}
icon={Expensicons.Close}
/>
) : undefined
}
/>
</StepScreenWrapper>
);
Expand Down
Loading