Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0b909c6
Implement confirmation modal when user tries to create multiple empty…
ShridharGoel Oct 2, 2025
6457ce2
Update tests
ShridharGoel Oct 2, 2025
e2a419c
Lint and other fixes
ShridharGoel Oct 2, 2025
108edd0
Fixes
ShridharGoel Oct 2, 2025
16b50f8
Update based on comments
ShridharGoel Oct 6, 2025
f0ec673
Merge branch 'main' into confirmEmpty
ShridharGoel Oct 6, 2025
6bceb39
Prettier updates
ShridharGoel Oct 6, 2025
7da8e89
Update tests
ShridharGoel Oct 6, 2025
032385a
Lint fixes
ShridharGoel Oct 6, 2025
27d2b74
Add translations for other languages
ShridharGoel Oct 6, 2025
d03b744
Merge
ShridharGoel Oct 6, 2025
e056f50
Remove extra params from hasEmptyReportsForPolicy call in FloatingAct…
ShridharGoel Oct 7, 2025
1d6f017
Prettier and typecheck fixes
ShridharGoel Oct 7, 2025
3a25e52
Merge branch 'main' of https://github.com/Expensify/App into confirmE…
ShridharGoel Oct 7, 2025
ab5603c
Use selectors in Onyx
ShridharGoel Oct 9, 2025
6965de3
Merge
ShridharGoel Oct 11, 2025
bb03828
Update the selector logic
ShridharGoel Oct 13, 2025
99a2d95
Fixes
ShridharGoel Oct 13, 2025
53ae0ba
Merge
ShridharGoel Oct 13, 2025
be4b008
Update to remove unnecessary changes
ShridharGoel Oct 13, 2025
92f1cd5
Fix lint
ShridharGoel Oct 13, 2025
2f5426e
Check for transactions instead of money
ShridharGoel Oct 14, 2025
1ddc053
Merge
ShridharGoel Oct 14, 2025
9b6cae0
Fixes
ShridharGoel Oct 14, 2025
35908ab
Fix lint
ShridharGoel Oct 14, 2025
d099a8e
Fix lint
ShridharGoel Oct 14, 2025
8ec4256
Lint fix
ShridharGoel Oct 14, 2025
4eb17d9
Update inferred to defaultChatEnabled
ShridharGoel Oct 14, 2025
fa29236
Merge main
ShridharGoel Oct 14, 2025
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
106 changes: 106 additions & 0 deletions src/hooks/useCreateEmptyReportConfirmation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React, {useCallback, useMemo, useState} from 'react';
import type {ReactNode} from 'react';
import ConfirmModal from '@components/ConfirmModal';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import Navigation from '@libs/Navigation/Navigation';
import {buildCannedSearchQuery} from '@libs/SearchQueryUtils';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import useLocalize from './useLocalize';

type UseCreateEmptyReportConfirmationParams = {
/** The policy ID for which the report is being created */
policyID?: string;
/** The display name of the policy/workspace */
policyName?: string;
/** Callback function to execute when user confirms report creation */
onConfirm: () => void;
/** Optional callback function to execute when user cancels the confirmation */
onCancel?: () => void;
};

type UseCreateEmptyReportConfirmationResult = {
/** Function to open the confirmation modal */
openCreateReportConfirmation: () => void;
/** The confirmation modal React component to render */
CreateReportConfirmationModal: ReactNode;
};

/**
* A React hook that provides a confirmation modal for creating empty reports.
* When a user attempts to create a new report in a workspace where they already have an empty report,
* this hook displays a confirmation modal to prevent accidental duplicate empty reports.
*
* @param params - Configuration object for the hook
* @param params.policyName - The display name of the policy/workspace
* @param params.onConfirm - Callback function to execute when user confirms report creation
* @returns An object containing:
* - openCreateReportConfirmation: Function to open the confirmation modal
* - CreateReportConfirmationModal: The confirmation modal React component to render
*
* @example
* const {openCreateReportConfirmation, CreateReportConfirmationModal} = useCreateEmptyReportConfirmation({
* policyID: 'policy123',
* policyName: 'Engineering Team',
* onConfirm: handleCreateReport,
* });
*
*/
export default function useCreateEmptyReportConfirmation({policyName, onConfirm, onCancel}: UseCreateEmptyReportConfirmationParams): UseCreateEmptyReportConfirmationResult {
const {translate} = useLocalize();
const [isVisible, setIsVisible] = useState(false);

const workspaceDisplayName = useMemo(() => (policyName?.trim().length ? policyName : translate('report.newReport.genericWorkspaceName')), [policyName, translate]);

const handleConfirm = useCallback(() => {
onConfirm();
setIsVisible(false);
}, [onConfirm]);

const handleCancel = useCallback(() => {
onCancel?.();
setIsVisible(false);
}, [onCancel]);

const handleReportsLinkPress = useCallback(() => {
setIsVisible(false);
Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query: buildCannedSearchQuery({groupBy: CONST.SEARCH.GROUP_BY.REPORTS})}));
}, []);

const openCreateReportConfirmation = useCallback(() => {
// The caller is responsible for determining if empty report confirmation
// should be shown. We simply open the modal when called.
setIsVisible(true);
}, []);

const prompt = useMemo(
() => (
<Text>
{translate('report.newReport.emptyReportConfirmationPrompt', {workspaceName: workspaceDisplayName})}{' '}
<TextLink onPress={handleReportsLinkPress}>{translate('report.newReport.emptyReportConfirmationPromptLink')}.</TextLink>
</Text>
),
[handleReportsLinkPress, translate, workspaceDisplayName],
);

const CreateReportConfirmationModal = useMemo(
() => (
<ConfirmModal
confirmText={translate('report.newReport.createReport')}
cancelText={translate('common.cancel')}
isVisible={isVisible}
onConfirm={handleConfirm}
onCancel={handleCancel}
prompt={prompt}
title={translate('report.newReport.emptyReportConfirmationTitle')}
/>
),
[handleCancel, handleConfirm, isVisible, prompt, translate],
);

return {
openCreateReportConfirmation,
CreateReportConfirmationModal,
};
}
5 changes: 5 additions & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6398,6 +6398,11 @@ ${amount} für ${merchant} - ${date}`,
newReport: {
createReport: 'Bericht erstellen',
chooseWorkspace: 'Wählen Sie einen Arbeitsbereich für diesen Bericht aus.',
emptyReportConfirmationTitle: 'Du hast bereits einen leeren Bericht',
emptyReportConfirmationPrompt: ({workspaceName}: {workspaceName: string}) =>
`Möchtest du wirklich einen weiteren Bericht in ${workspaceName} erstellen? Du kannst auf deine leeren Berichte zugreifen unter`,
emptyReportConfirmationPromptLink: 'Berichte',
genericWorkspaceName: 'diesem Arbeitsbereich',
},
genericCreateReportFailureMessage: 'Unerwarteter Fehler beim Erstellen dieses Chats. Bitte versuchen Sie es später erneut.',
genericAddCommentFailureMessage: 'Unerwarteter Fehler beim Posten des Kommentars. Bitte versuchen Sie es später noch einmal.',
Expand Down
5 changes: 5 additions & 0 deletions src/languages/en.ts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we confirm this translation elsewhere?
And I thought we should also generate other transaction files right? Not only es and en.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the other translations can be generated by a workflow which can be started by an internal eng.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added them.

Original file line number Diff line number Diff line change
Expand Up @@ -6351,6 +6351,11 @@ const translations = {
newReport: {
createReport: 'Create report',
chooseWorkspace: 'Choose a workspace for this report.',
emptyReportConfirmationTitle: 'You already have an empty report',
emptyReportConfirmationPrompt: ({workspaceName}: {workspaceName: string}) =>
`Are you sure you want to create another report in ${workspaceName}? You can access your empty reports in`,
emptyReportConfirmationPromptLink: 'Reports',
genericWorkspaceName: 'this workspace',
},
genericCreateReportFailureMessage: 'Unexpected error creating this chat. Please try again later.',
genericAddCommentFailureMessage: 'Unexpected error posting the comment. Please try again later.',
Expand Down
5 changes: 5 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6381,6 +6381,11 @@ ${amount} para ${merchant} - ${date}`,
newReport: {
createReport: 'Crear informe',
chooseWorkspace: 'Elige un espacio de trabajo para este informe.',
emptyReportConfirmationTitle: 'Ya tienes un informe vacío',
emptyReportConfirmationPrompt: ({workspaceName}: {workspaceName: string}) =>
`¿Estás seguro de que quieres crear otro informe en ${workspaceName}? Puedes acceder a tus informes vacíos en`,
emptyReportConfirmationPromptLink: 'Informes',
genericWorkspaceName: 'este espacio de trabajo',
},
genericCreateReportFailureMessage: 'Error inesperado al crear el chat. Por favor, inténtalo más tarde.',
genericAddCommentFailureMessage: 'Error inesperado al añadir el comentario. Por favor, inténtalo más tarde.',
Expand Down
5 changes: 5 additions & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6405,6 +6405,11 @@ ${amount} pour ${merchant} - ${date}`,
newReport: {
createReport: 'Créer un rapport',
chooseWorkspace: 'Choisissez un espace de travail pour ce rapport.',
emptyReportConfirmationTitle: 'Vous avez déjà un rapport vide',
emptyReportConfirmationPrompt: ({workspaceName}: {workspaceName: string}) =>
`Êtes-vous sûr de vouloir créer un autre rapport dans ${workspaceName} ? Vous pouvez accéder à vos rapports vides dans`,
emptyReportConfirmationPromptLink: 'Rapports',
genericWorkspaceName: 'cet espace de travail',
},
genericCreateReportFailureMessage: 'Erreur inattendue lors de la création de ce chat. Veuillez réessayer plus tard.',
genericAddCommentFailureMessage: 'Erreur inattendue lors de la publication du commentaire. Veuillez réessayer plus tard.',
Expand Down
5 changes: 5 additions & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6409,6 +6409,11 @@ ${amount} per ${merchant} - ${date}`,
newReport: {
createReport: 'Crea rapporto',
chooseWorkspace: "Scegli un'area di lavoro per questo report.",
emptyReportConfirmationTitle: 'Hai già un rapporto vuoto',
emptyReportConfirmationPrompt: ({workspaceName}: {workspaceName: string}) =>
`Sei sicuro di voler creare un altro rapporto in ${workspaceName}? Puoi accedere ai tuoi rapporti vuoti in`,
emptyReportConfirmationPromptLink: 'Rapporti',
genericWorkspaceName: 'questo spazio di lavoro',
},
genericCreateReportFailureMessage: 'Errore imprevisto durante la creazione di questa chat. Si prega di riprovare più tardi.',
genericAddCommentFailureMessage: 'Errore imprevisto durante la pubblicazione del commento. Per favore riprova più tardi.',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6345,6 +6345,10 @@ ${date} - ${merchant}に${amount}`,
newReport: {
createReport: 'レポートを作成',
chooseWorkspace: 'このレポートのワークスペースを選択してください。',
emptyReportConfirmationTitle: '空のレポートがすでにあります',
emptyReportConfirmationPrompt: ({workspaceName}: {workspaceName: string}) => `${workspaceName} で別のレポートを作成しますか? 空のレポートには次からアクセスできます`,
emptyReportConfirmationPromptLink: 'レポート',
genericWorkspaceName: 'このワークスペース',
},
genericCreateReportFailureMessage: 'このチャットの作成中に予期しないエラーが発生しました。後でもう一度お試しください。',
genericAddCommentFailureMessage: 'コメントの投稿中に予期しないエラーが発生しました。後でもう一度お試しください。',
Expand Down
5 changes: 5 additions & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6395,6 +6395,11 @@ ${amount} voor ${merchant} - ${date}`,
newReport: {
createReport: 'Rapport maken',
chooseWorkspace: 'Kies een werkruimte voor dit rapport.',
emptyReportConfirmationTitle: 'Je hebt al een leeg rapport',
emptyReportConfirmationPrompt: ({workspaceName}: {workspaceName: string}) =>
`Weet je zeker dat je nog een rapport wilt maken in ${workspaceName}? Je kunt je lege rapporten vinden onder`,
emptyReportConfirmationPromptLink: 'Rapporten',
genericWorkspaceName: 'deze werkruimte',
},
genericCreateReportFailureMessage: 'Onverwachte fout bij het maken van deze chat. Probeer het later opnieuw.',
genericAddCommentFailureMessage: 'Onverwachte fout bij het plaatsen van de opmerking. Probeer het later opnieuw.',
Expand Down
5 changes: 5 additions & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6380,6 +6380,11 @@ ${amount} dla ${merchant} - ${date}`,
newReport: {
createReport: 'Utwórz raport',
chooseWorkspace: 'Wybierz przestrzeń roboczą dla tego raportu.',
emptyReportConfirmationTitle: 'Masz już pusty raport',
emptyReportConfirmationPrompt: ({workspaceName}: {workspaceName: string}) =>
`Czy na pewno chcesz utworzyć kolejny raport w ${workspaceName}? Do pustych raportów możesz przejść w`,
emptyReportConfirmationPromptLink: 'Raporty',
genericWorkspaceName: 'tej przestrzeni roboczej',
},
genericCreateReportFailureMessage: 'Nieoczekiwany błąd podczas tworzenia tego czatu. Proszę spróbować ponownie później.',
genericAddCommentFailureMessage: 'Nieoczekiwany błąd podczas publikowania komentarza. Spróbuj ponownie później.',
Expand Down
5 changes: 5 additions & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6389,6 +6389,11 @@ ${amount} para ${merchant} - ${date}`,
newReport: {
createReport: 'Criar relatório',
chooseWorkspace: 'Escolha um espaço de trabalho para este relatório.',
emptyReportConfirmationTitle: 'Você já tem um relatório vazio',
emptyReportConfirmationPrompt: ({workspaceName}: {workspaceName: string}) =>
`Tem certeza de que deseja criar outro relatório em ${workspaceName}? Você pode acessar seus relatórios vazios em`,
emptyReportConfirmationPromptLink: 'Relatórios',
genericWorkspaceName: 'este espaço de trabalho',
},
genericCreateReportFailureMessage: 'Erro inesperado ao criar este chat. Por favor, tente novamente mais tarde.',
genericAddCommentFailureMessage: 'Erro inesperado ao postar o comentário. Por favor, tente novamente mais tarde.',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6265,6 +6265,10 @@ ${merchant}的${amount} - ${date}`,
newReport: {
createReport: '创建报告',
chooseWorkspace: '为此报告选择一个工作区。',
emptyReportConfirmationTitle: '你已经有一个空报告',
emptyReportConfirmationPrompt: ({workspaceName}: {workspaceName: string}) => `确定要在 ${workspaceName} 中再创建一个报告吗?你可以在以下位置访问你的空报告`,
emptyReportConfirmationPromptLink: '报告',
genericWorkspaceName: '此工作区',
},
genericCreateReportFailureMessage: '创建此聊天时出现意外错误。请稍后再试。',
genericAddCommentFailureMessage: '发表评论时出现意外错误。请稍后再试。',
Expand Down
20 changes: 20 additions & 0 deletions src/libs/PolicyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@

let allPolicies: OnyxCollection<Policy>;

Onyx.connect({

Check warning on line 60 in src/libs/PolicyUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.POLICY,
waitForCollectionCallback: true,
callback: (value) => (allPolicies = value),
Expand Down Expand Up @@ -1422,6 +1422,25 @@
return Object.values(policies).filter((policy) => isPaidGroupPolicy(policy) && policy?.isPolicyExpenseChatEnabled);
}

/**
* This method checks if the active policy has expense chat enabled and is a paid group policy.
* If true, it returns the active policy itself, else it returns the first policy from groupPoliciesWithChatEnabled.
*
* Further, if groupPoliciesWithChatEnabled is empty, then it returns undefined
* and the user would be taken to the workspace selection page.
*/
function getDefaultChatEnabledPolicy(groupPoliciesWithChatEnabled: Array<OnyxInputOrEntry<Policy>>, activePolicy?: OnyxInputOrEntry<Policy> | null): OnyxInputOrEntry<Policy> | undefined {
if (activePolicy && activePolicy.isPolicyExpenseChatEnabled && isPaidGroupPolicy(activePolicy)) {
return activePolicy;
}

if (groupPoliciesWithChatEnabled.length === 1) {
return groupPoliciesWithChatEnabled.at(0);
}

return undefined;
}

function hasOtherControlWorkspaces(currentPolicyID: string) {
const otherControlWorkspaces = Object.values(allPolicies ?? {}).filter((policy) => policy?.id !== currentPolicyID && isPolicyAdmin(policy) && isControlPolicy(policy));
return otherControlWorkspaces.length > 0;
Expand Down Expand Up @@ -1671,6 +1690,7 @@
areSettingsInErrorFields,
settingsPendingAction,
getGroupPaidPoliciesWithExpenseChatEnabled,
getDefaultChatEnabledPolicy,
getForwardsToAccount,
getSubmitToAccountID,
getWorkspaceAccountID,
Expand Down
114 changes: 114 additions & 0 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -931,7 +931,7 @@
const parsedReportActionMessageCache: Record<string, string> = {};

let conciergeReportID: OnyxEntry<string>;
Onyx.connect({

Check warning on line 934 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.CONCIERGE_REPORT_ID,
callback: (value) => {
conciergeReportID = value;
Expand All @@ -939,7 +939,7 @@
});

const defaultAvatarBuildingIconTestID = 'SvgDefaultAvatarBuilding Icon';
Onyx.connect({

Check warning on line 942 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.SESSION,
callback: (value) => {
// When signed out, val is undefined
Expand All @@ -957,7 +957,7 @@
let allPersonalDetails: OnyxEntry<PersonalDetailsList>;
let allPersonalDetailLogins: string[];
let currentUserPersonalDetails: OnyxEntry<PersonalDetails>;
Onyx.connect({

Check warning on line 960 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (value) => {
if (currentUserAccountID) {
Expand All @@ -969,14 +969,14 @@
});

let allReportsDraft: OnyxCollection<Report>;
Onyx.connect({

Check warning on line 972 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_DRAFT,
waitForCollectionCallback: true,
callback: (value) => (allReportsDraft = value),
});

let allPolicies: OnyxCollection<Policy>;
Onyx.connect({

Check warning on line 979 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.POLICY,
waitForCollectionCallback: true,
callback: (value) => (allPolicies = value),
Expand All @@ -984,7 +984,7 @@

let allReports: OnyxCollection<Report>;
let reportsByPolicyID: ReportByPolicyMap;
Onyx.connect({

Check warning on line 987 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => {
Expand Down Expand Up @@ -1027,14 +1027,14 @@
});

let allBetas: OnyxEntry<Beta[]>;
Onyx.connect({

Check warning on line 1030 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.BETAS,
callback: (value) => (allBetas = value),
});

let allTransactions: OnyxCollection<Transaction> = {};
let reportsTransactions: Record<string, Transaction[]> = {};
Onyx.connect({

Check warning on line 1037 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.TRANSACTION,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -1060,7 +1060,7 @@
});

let allReportActions: OnyxCollection<ReportActions>;
Onyx.connect({

Check warning on line 1063 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
waitForCollectionCallback: true,
callback: (actions) => {
Expand Down Expand Up @@ -8287,6 +8287,116 @@
return generateIsEmptyReport(report, isReportArchived);
}

type ReportEmptyStateSummary = Pick<Report, 'policyID' | 'ownerAccountID' | 'type' | 'stateNum' | 'statusNum' | 'total' | 'nonReimbursableTotal' | 'pendingAction' | 'errors'> &
Pick<Report, 'reportID'>;

function toReportEmptyStateSummary(report: Report | ReportEmptyStateSummary | undefined): ReportEmptyStateSummary | undefined {
if (!report) {
return undefined;
}

return {
reportID: report.reportID,
policyID: report.policyID ?? undefined,
ownerAccountID: report.ownerAccountID ?? undefined,
type: report.type ?? undefined,
stateNum: report.stateNum ?? undefined,
statusNum: report.statusNum ?? undefined,
total: report.total ?? undefined,
nonReimbursableTotal: report.nonReimbursableTotal ?? undefined,
pendingAction: report.pendingAction ?? undefined,
errors: report.errors ?? undefined,
};
}

function getReportSummariesForEmptyCheck(reports: OnyxCollection<Report> | Array<Report | ReportEmptyStateSummary | null | undefined> | undefined): ReportEmptyStateSummary[] {
if (!reports) {
return [];
}

const reportsArray = Array.isArray(reports) ? reports : Object.values(reports);
return reportsArray.map((report) => toReportEmptyStateSummary(report as Report | ReportEmptyStateSummary | undefined)).filter((summary): summary is ReportEmptyStateSummary => !!summary);
}

const reportSummariesOnyxSelector = (reports: Parameters<typeof getReportSummariesForEmptyCheck>[0]) => getReportSummariesForEmptyCheck(reports);

/**
* Checks if there are any empty (no transactions) open expense reports for a specific policy and user.
* An empty report is defined as having zero transactions.
* This excludes reports that are being deleted or have errors.
*/
function hasEmptyReportsForPolicy(
reports: OnyxCollection<Report> | Array<Report | ReportEmptyStateSummary | null | undefined> | undefined,
policyID: string | undefined,
accountID?: number,
reportsTransactionsParam: Record<string, Transaction[]> = reportsTransactions,
): boolean {
if (!accountID || !policyID) {
return false;
}

const summaries = getReportSummariesForEmptyCheck(reports);

return summaries.some((report) => {
if (!report.reportID || !report.policyID || report.policyID !== policyID || report.ownerAccountID !== accountID) {
return false;
}

// Exclude reports that are being deleted or have errors
if (report.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || report.errors) {
return false;
}

const isOpenExpense = report.type === CONST.REPORT.TYPE.EXPENSE && report.stateNum === CONST.REPORT.STATE_NUM.OPEN && report.statusNum === CONST.REPORT.STATUS_NUM.OPEN;
if (!isOpenExpense) {
return false;
}

const transactions = getReportTransactions(report.reportID, reportsTransactionsParam);
return transactions.length === 0;
});
}

/**
* Returns a lookup object containing the policy IDs that have empty (no transactions) open expense reports for a specific user.
* An empty report is defined as having zero transactions.
* This excludes reports that are being deleted or have errors.
*/
function getPolicyIDsWithEmptyReportsForAccount(
reports: OnyxCollection<Report> | Array<Report | ReportEmptyStateSummary | null | undefined> | undefined,
accountID?: number,
reportsTransactionsParam: Record<string, Transaction[]> = reportsTransactions,
): Record<string, boolean> {
if (!accountID) {
return {};
}

const summaries = getReportSummariesForEmptyCheck(reports);
const policyLookup: Record<string, boolean> = {};

summaries.forEach((report) => {
if (!report.reportID || !report.policyID || report.ownerAccountID !== accountID) {
return;
}

if (report.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || report.errors) {
return;
}

const isOpenExpense = report.type === CONST.REPORT.TYPE.EXPENSE && report.stateNum === CONST.REPORT.STATE_NUM.OPEN && report.statusNum === CONST.REPORT.STATUS_NUM.OPEN;
if (!isOpenExpense) {
return;
}

const transactions = getReportTransactions(report.reportID, reportsTransactionsParam);
if (transactions.length === 0) {
policyLookup[report.policyID] = true;
}
});

return policyLookup;
}

/**
* Check if the report is empty, meaning it has no visible messages (i.e. only a "created" report action).
* No cache implementation which bypasses derived value check.
Expand Down Expand Up @@ -12185,8 +12295,12 @@
getInvoicePayerName,
getInvoicesChatName,
getPayeeName,
getReportSummariesForEmptyCheck,
reportSummariesOnyxSelector,
getPolicyIDsWithEmptyReportsForAccount,
hasActionWithErrorsForTransaction,
hasAutomatedExpensifyAccountIDs,
hasEmptyReportsForPolicy,
hasExpensifyGuidesEmails,
hasHeldExpenses,
hasIOUWaitingOnCurrentUserBankAccount,
Expand Down
Loading
Loading