Skip to content
Open
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
70 changes: 70 additions & 0 deletions src/hooks/useConditionalCreateEmptyReportConfirmation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {accountIDSelector} from '@selectors/Session';
import {useCallback, useMemo} from 'react';
import type {ReactNode} from 'react';
import {hasEmptyReportsForPolicy, reportSummariesOnyxSelector} from '@libs/ReportUtils';
import ONYXKEYS from '@src/ONYXKEYS';
import getEmptyArray from '@src/types/utils/getEmptyArray';
import useCreateEmptyReportConfirmation from './useCreateEmptyReportConfirmation';
import useOnyx from './useOnyx';

type UseConditionalCreateEmptyReportConfirmationParams = {
/** The policy ID for which the report is being created */
policyID?: string;
/** The display name of the policy/workspace */
policyName?: string;
/** Callback executed after the user confirms report creation */
onCreateReport: () => void;
/** Optional callback executed when the confirmation modal is cancelled */
onCancel?: () => void;
};

type UseConditionalCreateEmptyReportConfirmationResult = {
/** Function that handles report creation with optional confirmation */
handleCreateReport: () => void;
/** Whether an empty report already exists for the provided policy */
hasEmptyReport: boolean;
/** The confirmation modal React component to render */
CreateReportConfirmationModal: ReactNode;
};

/**
* Hook that combines the empty report detection logic with the confirmation modal.
* It ensures the provided callback is only executed after the user confirms creation when necessary.
*/
export default function useConditionalCreateEmptyReportConfirmation({
policyID,
policyName,
onCreateReport,
onCancel,
}: UseConditionalCreateEmptyReportConfirmationParams): UseConditionalCreateEmptyReportConfirmationResult {
const [accountID] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector, canBeMissing: true});
type ReportSummary = ReturnType<typeof reportSummariesOnyxSelector>[number];
const [reportSummaries = getEmptyArray<ReportSummary>()] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {
canBeMissing: true,
selector: reportSummariesOnyxSelector,
});

const hasEmptyReport = useMemo(() => hasEmptyReportsForPolicy(reportSummaries, policyID, accountID), [accountID, policyID, reportSummaries]);

const {openCreateReportConfirmation, CreateReportConfirmationModal} = useCreateEmptyReportConfirmation({
policyID,
policyName,
onConfirm: onCreateReport,
onCancel,
});

const handleCreateReport = useCallback(() => {
if (hasEmptyReport) {
openCreateReportConfirmation();
return;
}

onCreateReport();
}, [hasEmptyReport, onCreateReport, openCreateReportConfirmation]);

return {
handleCreateReport,
hasEmptyReport,
CreateReportConfirmationModal,
};
}
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,
};
}
67 changes: 63 additions & 4 deletions src/hooks/useSearchTypeMenuSections.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import {createPoliciesSelector} from '@selectors/Policy';
import {useMemo} from 'react';
import {useCallback, useEffect, useMemo, useState} from 'react';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import memoize from '@libs/memoize';
import Permissions from '@libs/Permissions';
import {hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils';
import {getPersonalDetailsForAccountID, hasEmptyReportsForPolicy, hasViolations as hasViolationsReportUtils} from '@libs/ReportUtils';
import {createTypeMenuSections} from '@libs/SearchUIUtils';
import {createNewReport} from '@userActions/Report';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Policy, Session} from '@src/types/onyx';
import type {PersonalDetails, Policy, Session} from '@src/types/onyx';
import useCardFeedsForDisplay from './useCardFeedsForDisplay';
import useCreateEmptyReportConfirmation from './useCreateEmptyReportConfirmation';
import useNetwork from './useNetwork';
import useOnyx from './useOnyx';

Expand Down Expand Up @@ -59,6 +61,61 @@ const useSearchTypeMenuSections = () => {
const [allBetas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true});
const isASAPSubmitBetaEnabled = Permissions.isBetaEnabled(CONST.BETAS.ASAP_SUBMIT, allBetas);
const hasViolations = hasViolationsReportUtils(undefined, transactionViolations);
const [pendingReportCreation, setPendingReportCreation] = useState<{policyID: string; policyName?: string; onConfirm: () => void} | null>(null);

const handlePendingConfirm = useCallback(() => {
pendingReportCreation?.onConfirm();
setPendingReportCreation(null);
}, [pendingReportCreation, setPendingReportCreation]);

const handlePendingCancel = useCallback(() => {
setPendingReportCreation(null);
}, [setPendingReportCreation]);

const {openCreateReportConfirmation, CreateReportConfirmationModal} = useCreateEmptyReportConfirmation({
policyID: pendingReportCreation?.policyID,
policyName: pendingReportCreation?.policyName ?? '',
onConfirm: handlePendingConfirm,
onCancel: handlePendingCancel,
});

const createReportWithConfirmation = useCallback(
({policyID, policyName, onSuccess, personalDetails}: {policyID: string; policyName?: string; onSuccess: (reportID: string) => void; personalDetails?: PersonalDetails}) => {
const accountID = currentUserLoginAndAccountID?.accountID;
if (!accountID) {
return;
}

const personalDetailsForCreation = personalDetails ?? (getPersonalDetailsForAccountID(accountID) as PersonalDetails | undefined);
if (!personalDetailsForCreation) {
return;
}

const executeCreate = () => {
const createdReportID = createNewReport(personalDetailsForCreation, isASAPSubmitBetaEnabled, hasViolations, policyID);
onSuccess(createdReportID);
};

if (hasEmptyReportsForPolicy(reports, policyID, accountID)) {
setPendingReportCreation({
policyID,
policyName,
onConfirm: executeCreate,
});
return;
}

executeCreate();
},
[currentUserLoginAndAccountID?.accountID, hasViolations, isASAPSubmitBetaEnabled, reports, setPendingReportCreation],
);

useEffect(() => {
if (!pendingReportCreation) {
return;
}
openCreateReportConfirmation();
}, [pendingReportCreation, openCreateReportConfirmation]);

const typeMenuSections = useMemo(
() =>
Expand All @@ -75,6 +132,7 @@ const useSearchTypeMenuSections = () => {
isASAPSubmitBetaEnabled,
hasViolations,
reports,
createReportWithConfirmation,
),
[
currentUserLoginAndAccountID?.email,
Expand All @@ -89,10 +147,11 @@ const useSearchTypeMenuSections = () => {
isASAPSubmitBetaEnabled,
hasViolations,
reports,
createReportWithConfirmation,
],
);

return {typeMenuSections};
return {typeMenuSections, CreateReportConfirmationModal};
};

export default useSearchTypeMenuSections;
5 changes: 5 additions & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6399,6 +6399,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
Original file line number Diff line number Diff line change
Expand Up @@ -6352,6 +6352,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 @@ -6382,6 +6382,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 @@ -6406,6 +6406,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 @@ -6410,6 +6410,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 @@ -6346,6 +6346,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 @@ -6396,6 +6396,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 @@ -6381,6 +6381,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 @@ -6390,6 +6390,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
Loading
Loading