diff --git a/src/CONST/index.ts b/src/CONST/index.ts index f179535f6fd2..7c5660ad72c6 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1762,6 +1762,7 @@ const CONST = { UNABLE_TO_RETRY: 'unableToRetry', UPDATE_REQUIRED: 426, INCORRECT_MAGIC_CODE: 451, + POLICY_DIFF_WARNING: 305, }, HTTP_STATUS: { // When Cloudflare throttles diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index f9c5b2a1b15c..2cf9a40ea7b0 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -503,6 +503,9 @@ const ONYXKEYS = { /** Stores the information about the state of side panel */ NVP_SIDE_PANEL: 'nvp_sidePanel', + /** Information about vacation delegate */ + NVP_PRIVATE_VACATION_DELEGATE: 'nvp_private_vacationDelegate', + /** Stores draft information while user is scheduling the call. */ SCHEDULE_CALL_DRAFT: 'scheduleCallDraft', @@ -1183,6 +1186,7 @@ type OnyxValuesMapping = { [ONYXKEYS.SHOULD_BILL_WHEN_DOWNGRADING]: boolean | undefined; [ONYXKEYS.BILLING_RECEIPT_DETAILS]: OnyxTypes.BillingReceiptDetails; [ONYXKEYS.NVP_SIDE_PANEL]: OnyxTypes.SidePanel; + [ONYXKEYS.NVP_PRIVATE_VACATION_DELEGATE]: OnyxTypes.VacationDelegate; [ONYXKEYS.SCHEDULE_CALL_DRAFT]: OnyxTypes.ScheduleCallDraft; [ONYXKEYS.IS_FORCED_TO_CHANGE_CURRENCY]: boolean | undefined; [ONYXKEYS.IS_COMING_FROM_GLOBAL_REIMBURSEMENTS_FLOW]: boolean | undefined; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 154ee46d2662..04fffb256a44 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -346,6 +346,7 @@ const ROUTES = { SETTINGS_STATUS_CLEAR_AFTER: 'settings/profile/status/clear-after', SETTINGS_STATUS_CLEAR_AFTER_DATE: 'settings/profile/status/clear-after/date', SETTINGS_STATUS_CLEAR_AFTER_TIME: 'settings/profile/status/clear-after/time', + SETTINGS_VACATION_DELEGATE: 'settings/profile/status/vacation-delegate', SETTINGS_TROUBLESHOOT: 'settings/troubleshoot', SETTINGS_CONSOLE: { route: 'settings/troubleshoot/console', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index a924f5b016e6..906d8651e969 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -101,6 +101,7 @@ const SCREENS = { STATUS_CLEAR_AFTER: 'Settings_Status_Clear_After', STATUS_CLEAR_AFTER_DATE: 'Settings_Status_Clear_After_Date', STATUS_CLEAR_AFTER_TIME: 'Settings_Status_Clear_After_Time', + VACATION_DELEGATE: 'Settings_Status_Vacation_Delegate', STATUS: 'Settings_Status', PRONOUNS: 'Settings_Pronouns', TIMEZONE: 'Settings_Timezone', diff --git a/src/languages/de.ts b/src/languages/de.ts index 1cd30c1e02e0..1eb7bd00423c 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -194,6 +194,7 @@ import type { StepCounterParams, StripePaidParams, SubmitsToParams, + SubmittedToVacationDelegateParams, SubscriptionCommitmentParams, SubscriptionSettingsRenewsOnParams, SubscriptionSettingsSaveUpToParams, @@ -245,6 +246,7 @@ import type { UsePlusButtonParams, UserIsAlreadyMemberParams, UserSplitParams, + VacationDelegateParams, ViolationsAutoReportedRejectedExpenseParams, ViolationsCashExpenseWithNoReceiptParams, ViolationsConversionSurchargeParams, @@ -610,6 +612,7 @@ const translations = { workspacesTabTitle: 'Arbeitsbereiche', getTheApp: 'Hole dir die App', scanReceiptsOnTheGo: 'Scannen Sie Belege von Ihrem Telefon aus', + headsUp: 'Achtung!', }, supportalNoAccess: { title: 'Nicht so schnell', @@ -2377,6 +2380,13 @@ const translations = { untilTime: ({time}: UntilTimeParams) => `Bis ${time}`, date: 'Datum', time: 'Zeit', + vacationDelegate: 'Urlaubsvertretung', + setVacationDelegate: `Legen Sie eine Urlaubsvertretung fest, die Berichte in Ihrer Abwesenheit genehmigt.`, + vacationDelegateError: 'Beim Aktualisieren Ihrer Urlaubsvertretung ist ein Fehler aufgetreten.', + asVacationDelegate: ({nameOrEmail: managerName}: VacationDelegateParams) => `als Urlaubsvertretung von ${managerName}`, + toAsVacationDelegate: ({submittedToName, vacationDelegateName}: SubmittedToVacationDelegateParams) => `an ${submittedToName} als Urlaubsvertretung von ${vacationDelegateName}`, + vacationDelegateWarning: ({nameOrEmail}: VacationDelegateParams) => + `Sie weisen ${nameOrEmail} als Ihre Urlaubsvertretung zu. Diese Person ist noch nicht in all Ihren Workspaces. Wenn Sie fortfahren, wird eine E-Mail an alle Ihre Workspace-Administratoren gesendet, um sie hinzuzufügen.`, clearAfter: 'Nach dem Löschen', whenClearStatus: 'Wann sollten wir Ihren Status löschen?', }, diff --git a/src/languages/en.ts b/src/languages/en.ts index 3e457c764842..c88d0faa989d 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -182,6 +182,7 @@ import type { StepCounterParams, StripePaidParams, SubmitsToParams, + SubmittedToVacationDelegateParams, SubscriptionCommitmentParams, SubscriptionSettingsRenewsOnParams, SubscriptionSettingsSaveUpToParams, @@ -233,6 +234,7 @@ import type { UsePlusButtonParams, UserIsAlreadyMemberParams, UserSplitParams, + VacationDelegateParams, ViolationsAutoReportedRejectedExpenseParams, ViolationsCashExpenseWithNoReceiptParams, ViolationsConversionSurchargeParams, @@ -600,6 +602,7 @@ const translations = { workspacesTabTitle: 'Workspaces', getTheApp: 'Get the app', scanReceiptsOnTheGo: 'Scan receipts from your phone', + headsUp: 'Heads up!', }, supportalNoAccess: { title: 'Not so fast', @@ -2339,6 +2342,13 @@ const translations = { time: 'Time', clearAfter: 'Clear after', whenClearStatus: 'When should we clear your status?', + vacationDelegate: 'Vacation delegate', + setVacationDelegate: `Set a vacation delegate to approve reports on your behalf while you're out of office.`, + vacationDelegateError: 'There was an error updating your vacation delegate.', + asVacationDelegate: ({nameOrEmail}: VacationDelegateParams) => `as ${nameOrEmail}'s vacation delegate`, + toAsVacationDelegate: ({submittedToName, vacationDelegateName}: SubmittedToVacationDelegateParams) => `to ${submittedToName} as vacation delegate for ${vacationDelegateName}`, + vacationDelegateWarning: ({nameOrEmail}: VacationDelegateParams) => + `You're assigning ${nameOrEmail} as your vacation delegate. They're not on all your workspaces yet. If you choose to continue, an email will be sent to all your workspace admins to add them.`, }, stepCounter: ({step, total, text}: StepCounterParams) => { let result = `Step ${step}`; diff --git a/src/languages/es.ts b/src/languages/es.ts index 8e6d6679785b..6bf3b5280717 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -181,6 +181,7 @@ import type { StepCounterParams, StripePaidParams, SubmitsToParams, + SubmittedToVacationDelegateParams, SubscriptionCommitmentParams, SubscriptionSettingsRenewsOnParams, SubscriptionSettingsSaveUpToParams, @@ -232,6 +233,7 @@ import type { UsePlusButtonParams, UserIsAlreadyMemberParams, UserSplitParams, + VacationDelegateParams, ViolationsAutoReportedRejectedExpenseParams, ViolationsCashExpenseWithNoReceiptParams, ViolationsConversionSurchargeParams, @@ -591,6 +593,7 @@ const translations = { workspacesTabTitle: 'Espacios', getTheApp: 'Descarga la app', scanReceiptsOnTheGo: 'Escanea recibos desde tu teléfono', + headsUp: '¡Atención!', }, supportalNoAccess: { title: 'No tan rápido', @@ -2359,6 +2362,13 @@ const translations = { time: 'Hora', clearAfter: 'Borrar después', whenClearStatus: '¿Cuándo deberíamos borrar tu estado?', + vacationDelegate: 'Delegado de vacaciones', + setVacationDelegate: 'Configura un delegado de vacaciones para aprobar informes en tu nombre mientras estás fuera de la oficina.', + vacationDelegateError: 'Hubo un error al actualizar tu delegado de vacaciones.', + asVacationDelegate: ({nameOrEmail: managerName}: VacationDelegateParams) => `como delegado de vacaciones de ${managerName}`, + toAsVacationDelegate: ({submittedToName, vacationDelegateName}: SubmittedToVacationDelegateParams) => `a ${submittedToName} como delegado de vacaciones de ${vacationDelegateName}`, + vacationDelegateWarning: ({nameOrEmail}: VacationDelegateParams) => + `Está asignando a ${nameOrEmail} como su delegado de vacaciones. Aún no está en todos sus espacios de trabajo. Si decide continuar, se enviará un correo electrónico a todos los administradores de sus espacios de trabajo para agregarlo.`, }, stepCounter: ({step, total, text}: StepCounterParams) => { let result = `Paso ${step}`; diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 6015959ceb26..02c775fd7c0d 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -194,6 +194,7 @@ import type { StepCounterParams, StripePaidParams, SubmitsToParams, + SubmittedToVacationDelegateParams, SubscriptionCommitmentParams, SubscriptionSettingsRenewsOnParams, SubscriptionSettingsSaveUpToParams, @@ -245,6 +246,7 @@ import type { UsePlusButtonParams, UserIsAlreadyMemberParams, UserSplitParams, + VacationDelegateParams, ViolationsAutoReportedRejectedExpenseParams, ViolationsCashExpenseWithNoReceiptParams, ViolationsConversionSurchargeParams, @@ -610,6 +612,7 @@ const translations = { workspacesTabTitle: 'Espaces de travail', getTheApp: "Obtenez l'application", scanReceiptsOnTheGo: 'Numérisez les reçus depuis votre téléphone', + headsUp: 'Attention !', }, supportalNoAccess: { title: 'Pas si vite', @@ -2377,6 +2380,14 @@ const translations = { time: 'Temps', clearAfter: 'Effacer après', whenClearStatus: 'Quand devrions-nous effacer votre statut ?', + vacationDelegate: 'Délégué de vacances', + setVacationDelegate: `Définissez un délégué de vacances pour approuver les rapports en votre absence.`, + vacationDelegateError: 'Une erreur est survenue lors de la mise à jour de votre délégué de vacances.', + asVacationDelegate: ({nameOrEmail: managerName}: VacationDelegateParams) => `en tant que délégué de vacances de ${managerName}`, + toAsVacationDelegate: ({submittedToName, vacationDelegateName}: SubmittedToVacationDelegateParams) => + `à ${submittedToName} en tant que délégué de vacances de ${vacationDelegateName}`, + vacationDelegateWarning: ({nameOrEmail}: VacationDelegateParams) => + `Vous assignez ${nameOrEmail} en tant que délégué de vacances. Il/elle n'est pas encore présent(e) dans tous vos espaces de travail. Si vous choisissez de continuer, un e-mail sera envoyé à tous les administrateurs de vos espaces pour l’ajouter.`, }, stepCounter: ({step, total, text}: StepCounterParams) => { let result = `Étape ${step}`; diff --git a/src/languages/it.ts b/src/languages/it.ts index 52f2d778c010..9d6b8903c7b4 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -194,6 +194,7 @@ import type { StepCounterParams, StripePaidParams, SubmitsToParams, + SubmittedToVacationDelegateParams, SubscriptionCommitmentParams, SubscriptionSettingsRenewsOnParams, SubscriptionSettingsSaveUpToParams, @@ -245,6 +246,7 @@ import type { UsePlusButtonParams, UserIsAlreadyMemberParams, UserSplitParams, + VacationDelegateParams, ViolationsAutoReportedRejectedExpenseParams, ViolationsCashExpenseWithNoReceiptParams, ViolationsConversionSurchargeParams, @@ -610,6 +612,7 @@ const translations = { workspacesTabTitle: 'Spazi di lavoro', getTheApp: "Scarica l'app", scanReceiptsOnTheGo: 'Scansiona le ricevute dal tuo telefono', + headsUp: 'Attenzione!', }, supportalNoAccess: { title: 'Non così in fretta', @@ -2364,6 +2367,13 @@ const translations = { time: 'Tempo', clearAfter: 'Cancella dopo', whenClearStatus: 'Quando dovremmo cancellare il tuo stato?', + vacationDelegate: 'Delegato per le vacanze', + setVacationDelegate: `Imposta un delegato per le vacanze per approvare i report al tuo posto mentre sei fuori ufficio.`, + vacationDelegateError: 'Si è verificato un errore durante l’aggiornamento del delegato per le vacanze.', + asVacationDelegate: ({nameOrEmail: managerName}: VacationDelegateParams) => `come delegato per le vacanze di ${managerName}`, + toAsVacationDelegate: ({submittedToName, vacationDelegateName}: SubmittedToVacationDelegateParams) => `a ${submittedToName} come delegato per le vacanze di ${vacationDelegateName}`, + vacationDelegateWarning: ({nameOrEmail}: VacationDelegateParams) => + `Stai assegnando ${nameOrEmail} come tuo delegato per le vacanze. Non è ancora presente in tutti i tuoi workspace. Se scegli di continuare, verrà inviata un'e-mail a tutti gli amministratori dei tuoi workspace per aggiungerlo.`, }, stepCounter: ({step, total, text}: StepCounterParams) => { let result = `Passo ${step}`; diff --git a/src/languages/ja.ts b/src/languages/ja.ts index df2d96f6377d..72bc1eb8d0fb 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -194,6 +194,7 @@ import type { StepCounterParams, StripePaidParams, SubmitsToParams, + SubmittedToVacationDelegateParams, SubscriptionCommitmentParams, SubscriptionSettingsRenewsOnParams, SubscriptionSettingsSaveUpToParams, @@ -245,6 +246,7 @@ import type { UsePlusButtonParams, UserIsAlreadyMemberParams, UserSplitParams, + VacationDelegateParams, ViolationsAutoReportedRejectedExpenseParams, ViolationsCashExpenseWithNoReceiptParams, ViolationsConversionSurchargeParams, @@ -610,6 +612,7 @@ const translations = { workspacesTabTitle: 'ワークスペース', getTheApp: 'アプリを入手', scanReceiptsOnTheGo: '携帯電話から領収書をスキャンする', + headsUp: 'ご注意ください!', }, supportalNoAccess: { title: 'ちょっと待ってください', @@ -2350,6 +2353,13 @@ const translations = { afterWeek: '1週間', custom: 'カスタム', }, + vacationDelegate: '休暇代理人', + setVacationDelegate: `不在中にレポートを承認してもらうため、休暇代理人を設定してください。`, + vacationDelegateError: '休暇代理人の更新中にエラーが発生しました。', + asVacationDelegate: ({nameOrEmail: managerName}: VacationDelegateParams) => `${managerName}の休暇代理人として`, + toAsVacationDelegate: ({submittedToName, vacationDelegateName}: SubmittedToVacationDelegateParams) => `${submittedToName}へ、${vacationDelegateName}の休暇代理人として`, + vacationDelegateWarning: ({nameOrEmail}: VacationDelegateParams) => + `${nameOrEmail} を休暇代理人として設定しています。まだすべてのワークスペースに参加していません。続行すると、すべてのワークスペース管理者に追加を依頼するメールが送信されます。`, untilTomorrow: '明日まで', untilTime: ({time}: UntilTimeParams) => `${time}まで`, date: '日付', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index da9261972716..99e30a473c33 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -194,6 +194,7 @@ import type { StepCounterParams, StripePaidParams, SubmitsToParams, + SubmittedToVacationDelegateParams, SubscriptionCommitmentParams, SubscriptionSettingsRenewsOnParams, SubscriptionSettingsSaveUpToParams, @@ -245,6 +246,7 @@ import type { UsePlusButtonParams, UserIsAlreadyMemberParams, UserSplitParams, + VacationDelegateParams, ViolationsAutoReportedRejectedExpenseParams, ViolationsCashExpenseWithNoReceiptParams, ViolationsConversionSurchargeParams, @@ -609,6 +611,7 @@ const translations = { workspacesTabTitle: 'Werkruimtes', getTheApp: 'Download de app', scanReceiptsOnTheGo: 'Scan bonnetjes vanaf je telefoon', + headsUp: 'Let op!', }, supportalNoAccess: { title: 'Niet zo snel', @@ -2365,6 +2368,13 @@ const translations = { time: 'Tijd', clearAfter: 'Wissen na', whenClearStatus: 'Wanneer moeten we je status wissen?', + vacationDelegate: 'Vakantievervanger', + setVacationDelegate: `Stel een vakantievervanger in om rapporten namens jou goed te keuren terwijl je afwezig bent.`, + vacationDelegateError: 'Er is een fout opgetreden bij het bijwerken van je vakantievervanger.', + asVacationDelegate: ({nameOrEmail: managerName}: VacationDelegateParams) => `als vakantievervanger van ${managerName}`, + toAsVacationDelegate: ({submittedToName, vacationDelegateName}: SubmittedToVacationDelegateParams) => `aan ${submittedToName} als vakantievervanger van ${vacationDelegateName}`, + vacationDelegateWarning: ({nameOrEmail}: VacationDelegateParams) => + `Je wijst ${nameOrEmail} aan als je vakantievervanger. Deze persoon zit nog niet in al je werkruimtes. Als je doorgaat, wordt er een e-mail gestuurd naar alle beheerders van je werkruimtes om hem/haar toe te voegen.`, }, stepCounter: ({step, total, text}: StepCounterParams) => { let result = `Stap ${step}`; diff --git a/src/languages/params.ts b/src/languages/params.ts index 18291cb72588..d7eb4dd693f3 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -557,6 +557,10 @@ type DelegateRoleParams = {role: DelegateRole}; type DelegatorParams = {delegator: string}; +type VacationDelegateParams = {nameOrEmail: string}; + +type SubmittedToVacationDelegateParams = {submittedToName: string; vacationDelegateName: string}; + type RoleNamesParams = {role: string}; type AssignCardParams = { @@ -797,6 +801,7 @@ export type { SpreadCategoriesParams, DelegateRoleParams, DelegatorParams, + VacationDelegateParams, ReconciliationWorksParams, LastSyncAccountingParams, SyncStageNameConnectionsParams, @@ -865,6 +870,7 @@ export type { FormattedMaxLengthParams, GoBackMessageParams, GoToRoomParams, + SubmittedToVacationDelegateParams, HeldRequestParams, InstantSummaryParams, IssueVirtualCardParams, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 975f8a07d42c..2fee9b1043ee 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -194,6 +194,7 @@ import type { StepCounterParams, StripePaidParams, SubmitsToParams, + SubmittedToVacationDelegateParams, SubscriptionCommitmentParams, SubscriptionSettingsRenewsOnParams, SubscriptionSettingsSaveUpToParams, @@ -245,6 +246,7 @@ import type { UsePlusButtonParams, UserIsAlreadyMemberParams, UserSplitParams, + VacationDelegateParams, ViolationsAutoReportedRejectedExpenseParams, ViolationsCashExpenseWithNoReceiptParams, ViolationsConversionSurchargeParams, @@ -610,6 +612,7 @@ const translations = { workspacesTabTitle: 'Przestrzenie robocze', getTheApp: 'Pobierz aplikację', scanReceiptsOnTheGo: 'Skanuj paragony za pomocą telefonu', + headsUp: 'Uwaga!', }, supportalNoAccess: { title: 'Nie tak szybko', @@ -2360,6 +2363,13 @@ const translations = { time: 'Czas', clearAfter: 'Wyczyść po', whenClearStatus: 'Kiedy powinniśmy usunąć Twój status?', + vacationDelegate: 'Zastępca urlopowy', + setVacationDelegate: `Ustaw zastępcę urlopowego, który będzie zatwierdzał raporty w twoim imieniu podczas twojej nieobecności.`, + vacationDelegateError: 'Wystąpił błąd podczas aktualizacji twojego zastępcy urlopowego.', + asVacationDelegate: ({nameOrEmail: managerName}: VacationDelegateParams) => `jako zastępca urlopowy ${managerName}`, + toAsVacationDelegate: ({submittedToName, vacationDelegateName}: SubmittedToVacationDelegateParams) => `do ${submittedToName} jako zastępca urlopowy ${vacationDelegateName}`, + vacationDelegateWarning: ({nameOrEmail}: VacationDelegateParams) => + `Przydzielasz ${nameOrEmail} jako swojego zastępcę urlopowego. Osoba ta nie jest jeszcze członkiem wszystkich twoich przestrzeni roboczych. Jeśli zdecydujesz się kontynuować, zostanie wysłany e-mail do wszystkich administratorów twoich przestrzeni roboczych z prośbą o jej dodanie.`, }, stepCounter: ({step, total, text}: StepCounterParams) => { let result = `Krok ${step}`; diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index df2f4ac26110..583c5721b422 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -194,6 +194,7 @@ import type { StepCounterParams, StripePaidParams, SubmitsToParams, + SubmittedToVacationDelegateParams, SubscriptionCommitmentParams, SubscriptionSettingsRenewsOnParams, SubscriptionSettingsSaveUpToParams, @@ -245,6 +246,7 @@ import type { UsePlusButtonParams, UserIsAlreadyMemberParams, UserSplitParams, + VacationDelegateParams, ViolationsAutoReportedRejectedExpenseParams, ViolationsCashExpenseWithNoReceiptParams, ViolationsConversionSurchargeParams, @@ -609,6 +611,7 @@ const translations = { workspacesTabTitle: 'Workspaces', getTheApp: 'Obtenha o aplicativo', scanReceiptsOnTheGo: 'Digitalize recibos com seu celular', + headsUp: 'Atenção!', }, supportalNoAccess: { title: 'Não tão rápido', @@ -2362,6 +2365,13 @@ const translations = { time: 'Tempo', clearAfter: 'Limpar após', whenClearStatus: 'Quando devemos limpar seu status?', + vacationDelegate: 'Delegado de férias', + setVacationDelegate: `Defina um delegado de férias para aprovar relatórios em seu nome enquanto estiver fora do escritório.`, + vacationDelegateError: 'Ocorreu um erro ao atualizar seu delegado de férias.', + asVacationDelegate: ({nameOrEmail: managerName}: VacationDelegateParams) => `como delegado de férias de ${managerName}`, + toAsVacationDelegate: ({submittedToName, vacationDelegateName}: SubmittedToVacationDelegateParams) => `para ${submittedToName} como delegado de férias de ${vacationDelegateName}`, + vacationDelegateWarning: ({nameOrEmail}: VacationDelegateParams) => + `Você está designando ${nameOrEmail} como seu delegado de férias. Essa pessoa ainda não está em todos os seus espaços de trabalho. Se você continuar, um e-mail será enviado para todos os administradores dos seus espaços solicitando a inclusão dela.`, }, stepCounter: ({step, total, text}: StepCounterParams) => { let result = `Etapa ${step}`; diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 37a2d0d6f743..af4ae3342555 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -194,6 +194,7 @@ import type { StepCounterParams, StripePaidParams, SubmitsToParams, + SubmittedToVacationDelegateParams, SubscriptionCommitmentParams, SubscriptionSettingsRenewsOnParams, SubscriptionSettingsSaveUpToParams, @@ -245,6 +246,7 @@ import type { UsePlusButtonParams, UserIsAlreadyMemberParams, UserSplitParams, + VacationDelegateParams, ViolationsAutoReportedRejectedExpenseParams, ViolationsCashExpenseWithNoReceiptParams, ViolationsConversionSurchargeParams, @@ -609,6 +611,7 @@ const translations = { workspacesTabTitle: '工作区', getTheApp: '获取应用程序', scanReceiptsOnTheGo: '用手机扫描收据', + headsUp: '\u6CE8\u610F\uFF01', }, supportalNoAccess: { title: '慢一点', @@ -2335,6 +2338,14 @@ const translations = { time: '时间', clearAfter: '清除后', whenClearStatus: '我们应该何时清除您的状态?', + vacationDelegate: '\u4F11\u5047\u4EE3\u7406\u4EBA', + setVacationDelegate: '\u8BBE\u7F6E\u4E00\u4F4D\u4F11\u5047\u4EE3\u7406\u4EBA\uFF0C\u5728\u60A8\u5916\u51FA\u65F6\u4EE3\u60A8\u6279\u51C6\u62A5\u544A\u3002', + vacationDelegateError: '\u66F4\u65B0\u4F11\u5047\u4EE3\u7406\u4EBA\u65F6\u51FA\u9519\u3002', + asVacationDelegate: ({nameOrEmail: managerName}: VacationDelegateParams) => `\u4F5C\u4E3A ${managerName} \u7684\u4F11\u5047\u4EE3\u7406\u4EBA`, + toAsVacationDelegate: ({submittedToName, vacationDelegateName}: SubmittedToVacationDelegateParams) => + `\u53D1\u9001\u7ED9 ${submittedToName}\uFF0C\u4F5C\u4E3A ${vacationDelegateName} \u7684\u4F11\u5047\u4EE3\u7406\u4EBA`, + vacationDelegateWarning: ({nameOrEmail}: VacationDelegateParams) => + `\u60A8\u6B63\u5728\u6307\u5B9A ${nameOrEmail} \u4F5C\u4E3A\u60A8\u7684\u4F11\u5047\u4EE3\u7406\u4EBA\u3002\u4ED6/\u5979\u8FD8\u672A\u52A0\u5165\u60A8\u7684\u6240\u6709\u5DE5\u4F5C\u7A7A\u95F4\u3002\u5982\u679C\u60A8\u9009\u62E9\u7EE7\u7EED\uFF0C\u5C06\u5411\u6240\u6709\u5DE5\u4F5C\u7A7A\u95F4\u7BA1\u7406\u5458\u53D1\u9001\u90AE\u4EF6\uFF0C\u901A\u77E5\u4ED6\u4EEC\u6DFB\u52A0\u8BE5\u4EBA\u3002`, }, stepCounter: ({step, total, text}: StepCounterParams) => { let result = `步骤 ${step}`; diff --git a/src/libs/API/parameters/SetVacationDelegateParams.ts b/src/libs/API/parameters/SetVacationDelegateParams.ts new file mode 100644 index 000000000000..8e33e6b1a13b --- /dev/null +++ b/src/libs/API/parameters/SetVacationDelegateParams.ts @@ -0,0 +1,7 @@ +type SetVacationDelegateParams = { + creator: string; + vacationDelegateEmail: string; + overridePolicyDiffWarning: boolean; +}; + +export default SetVacationDelegateParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 2f355e1ecf90..7b47807ec1d8 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -402,6 +402,7 @@ export type {default as GetGuideCallAvailabilityScheduleParams} from './GetGuide export type {default as RetractReportParams} from './RetractReportParams'; export type {default as CompleteConciergeCallParams} from './CompleteConciergeCallParams'; export type {default as FinishCorpayBankAccountOnboardingParams} from './FinishCorpayBankAccountOnboardingParams'; +export type {default as SetVacationDelegateParams} from './SetVacationDelegateParams'; export type {default as LockAccountParams} from './LockAccountParams'; export type {default as ExportMultiLevelTagsSpreadSheetParams} from './ExportMultiLevelTagsSpreadSheetParams'; export type {default as ReopenReportParams} from './ReopenReportParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 10696912821b..c63644372db0 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -487,6 +487,7 @@ const WRITE_COMMANDS = { FINISH_CORPAY_BANK_ACCOUNT_ONBOARDING: 'FinishCorpayBankAccountOnboarding', REOPEN_REPORT: 'ReopenReport', GET_GUIDE_CALL_AVAILABILITY_SCHEDULE: 'GetGuideCallAvailabilitySchedule', + DELETE_VACATION_DELEGATE: 'DeleteVacationDelegate', } as const; type WriteCommand = ValueOf; @@ -838,6 +839,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.RETRACT_REPORT]: Parameters.RetractReportParams; [WRITE_COMMANDS.COMPLETE_CONCIERGE_CALL]: Parameters.CompleteConciergeCallParams; [WRITE_COMMANDS.FINISH_CORPAY_BANK_ACCOUNT_ONBOARDING]: Parameters.FinishCorpayBankAccountOnboardingParams; + [WRITE_COMMANDS.DELETE_VACATION_DELEGATE]: null; [WRITE_COMMANDS.REOPEN_REPORT]: Parameters.ReopenReportParams; [WRITE_COMMANDS.DELETE_MONEY_REQUEST_ON_SEARCH]: Parameters.DeleteMoneyRequestOnSearchParams; @@ -1165,6 +1167,7 @@ const SIDE_EFFECT_REQUEST_COMMANDS = { CREATE_DIGITAL_WALLET: 'CreateDigitalWallet', VERIFY_TEST_DRIVE_RECIPIENT: 'VerifyTestDriveRecipient', LOCK_ACCOUNT: 'LockAccount', + SET_VACATION_DELEGATE: 'SetVacationDelegate', } as const; type SideEffectRequestCommand = ValueOf; @@ -1188,6 +1191,7 @@ type SideEffectRequestCommandParameters = { [SIDE_EFFECT_REQUEST_COMMANDS.CREATE_DIGITAL_WALLET]: Parameters.CreateDigitalWalletParams; [SIDE_EFFECT_REQUEST_COMMANDS.VERIFY_TEST_DRIVE_RECIPIENT]: Parameters.VerifyTestDriveRecipientParams; [SIDE_EFFECT_REQUEST_COMMANDS.LOCK_ACCOUNT]: Parameters.LockAccountParams; + [SIDE_EFFECT_REQUEST_COMMANDS.SET_VACATION_DELEGATE]: Parameters.SetVacationDelegateParams; }; type ApiRequestCommandParameters = WriteCommandParameters & ReadCommandParameters & SideEffectRequestCommandParameters; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 949a6831aa13..9bb45acbb06d 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -300,6 +300,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Profile/CustomStatus/StatusClearAfterPage').default, [SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER_DATE]: () => require('../../../../pages/settings/Profile/CustomStatus/SetDatePage').default, [SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER_TIME]: () => require('../../../../pages/settings/Profile/CustomStatus/SetTimePage').default, + [SCREENS.SETTINGS.PROFILE.VACATION_DELEGATE]: () => require('../../../../pages/settings/Profile/CustomStatus/VacationDelegatePage').default, [SCREENS.SETTINGS.SUBSCRIPTION.SIZE]: () => require('../../../../pages/settings/Subscription/SubscriptionSize').default, [SCREENS.SETTINGS.SUBSCRIPTION.SETTINGS_DETAILS]: () => require('../../../../pages/settings/Subscription/SubscriptionSettings').default, [SCREENS.SETTINGS.SUBSCRIPTION.DISABLE_AUTO_RENEW_SURVEY]: () => require('../../../../pages/settings/Subscription/DisableAutoRenewSurveyPage').default, diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts index 8cbc615646a8..bb6d34928efc 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/SETTINGS_TO_RHP.ts @@ -11,6 +11,7 @@ const SETTINGS_TO_RHP: Partial['config'] = { [SCREENS.SETTINGS.SUBSCRIPTION.SETTINGS_DETAILS]: { path: ROUTES.SETTINGS_SUBSCRIPTION_SETTINGS_DETAILS, }, + [SCREENS.SETTINGS.PROFILE.VACATION_DELEGATE]: { + path: ROUTES.SETTINGS_VACATION_DELEGATE, + exact: true, + }, [SCREENS.SETTINGS.SUBSCRIPTION.SIZE]: { path: ROUTES.SETTINGS_SUBSCRIPTION_SIZE.route, parse: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 9f91696e8b52..bfc698749345 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -168,6 +168,7 @@ type SettingsNavigatorParamList = { [SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER]: undefined; [SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER_DATE]: undefined; [SCREENS.SETTINGS.PROFILE.STATUS_CLEAR_AFTER_TIME]: undefined; + [SCREENS.SETTINGS.PROFILE.VACATION_DELEGATE]: undefined; [SCREENS.WORKSPACE.CURRENCY]: undefined; [SCREENS.WORKSPACE.ADDRESS]: { policyID: string; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 01bbd0bdfbcc..c9d9aa9883fe 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -2927,6 +2927,30 @@ function getIntegrationSyncFailedMessage(action: OnyxEntry): strin return translateLocal('report.actions.type.integrationSyncFailed', {label, errorMessage}); } +function getManagerOnVacation(action: OnyxEntry): string | undefined { + if (!isApprovedAction(action)) { + return; + } + + return getOriginalMessage(action)?.managerOnVacation; +} + +function getVacationer(action: OnyxEntry): string | undefined { + if (!isSubmittedAction(action)) { + return; + } + + return getOriginalMessage(action)?.vacationer; +} + +function getSubmittedTo(action: OnyxEntry): string | undefined { + if (!isSubmittedAction(action)) { + return; + } + + return getOriginalMessage(action)?.to; +} + export { doesReportHaveVisibleActions, extractLinksFromMessageHtml, @@ -3091,6 +3115,9 @@ export { getReportActionFromExpensifyCard, isReopenedAction, getIntegrationSyncFailedMessage, + getManagerOnVacation, + getVacationer, + getSubmittedTo, getReceiptScanFailedMessage, }; diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts index 93e0bc83e7cf..83d4b036b112 100644 --- a/src/libs/UserUtils.ts +++ b/src/libs/UserUtils.ts @@ -6,8 +6,9 @@ import * as defaultAvatars from '@components/Icon/DefaultAvatars'; import {ConciergeAvatar, NotificationsAvatar} from '@components/Icon/Expensicons'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Account, LoginList, PrivatePersonalDetails, Session} from '@src/types/onyx'; +import type {Account, LoginList, PrivatePersonalDetails, Session, VacationDelegate} from '@src/types/onyx'; import type Login from '@src/types/onyx/Login'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; import hashCode from './hashCode'; @@ -97,9 +98,13 @@ function getLoginListBrickRoadIndicator(loginList: OnyxEntry): LoginL * Gets the appropriate brick road indicator status for the Profile section. * Error status is higher priority, so we check for that first. */ -function getProfilePageBrickRoadIndicator(loginList: OnyxEntry, privatePersonalDetails: OnyxEntry): LoginListIndicator { +function getProfilePageBrickRoadIndicator( + loginList: OnyxEntry, + privatePersonalDetails: OnyxEntry, + vacationDelegate: OnyxEntry, +): LoginListIndicator { const hasPhoneNumberError = !!privatePersonalDetails?.errorFields?.phoneNumber; - if (hasLoginListError(loginList) || hasPhoneNumberError) { + if (hasLoginListError(loginList) || hasPhoneNumberError || !isEmptyObject(vacationDelegate?.errors)) { return CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; } if (hasLoginListInfo(loginList)) { diff --git a/src/libs/actions/VacationDelegate.ts b/src/libs/actions/VacationDelegate.ts new file mode 100644 index 000000000000..1d5d9fb6a683 --- /dev/null +++ b/src/libs/actions/VacationDelegate.ts @@ -0,0 +1,112 @@ +import Onyx from 'react-native-onyx'; +import type {OnyxUpdate} from 'react-native-onyx'; +import * as API from '@libs/API'; +import type {SetVacationDelegateParams} from '@libs/API/parameters'; +import {SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {VacationDelegate} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; + +function setVacationDelegate(creator: string, delegate: string, shouldOverridePolicyDiffWarning = false) { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_PRIVATE_VACATION_DELEGATE, + value: { + creator, + delegate, + errors: null, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_PRIVATE_VACATION_DELEGATE, + value: { + errors: null, + pendingAction: null, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_PRIVATE_VACATION_DELEGATE, + value: { + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('statusPage.vacationDelegateError'), + }, + }, + ]; + + const parameters: SetVacationDelegateParams = { + creator, + vacationDelegateEmail: delegate, + overridePolicyDiffWarning: shouldOverridePolicyDiffWarning, + }; + + // We need to read the API response for showing a warning if there is a policy diff warning. + // eslint-disable-next-line rulesdir/no-api-side-effects-method + return API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.SET_VACATION_DELEGATE, parameters, {optimisticData, successData, failureData}); +} + +function deleteVacationDelegate(vacationDelegate?: VacationDelegate) { + if (isEmptyObject(vacationDelegate)) { + return; + } + + const {creator, delegate} = vacationDelegate; + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_PRIVATE_VACATION_DELEGATE, + value: { + creator: null, + delegate: null, + errors: null, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_PRIVATE_VACATION_DELEGATE, + value: { + errors: null, + pendingAction: null, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.NVP_PRIVATE_VACATION_DELEGATE, + value: { + creator, + delegate, + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('statusPage.vacationDelegateError'), + }, + }, + ]; + + API.write(WRITE_COMMANDS.DELETE_VACATION_DELEGATE, null, {optimisticData, successData, failureData}); +} + +function clearVacationDelegateError() { + Onyx.merge(ONYXKEYS.NVP_PRIVATE_VACATION_DELEGATE, { + errors: null, + pendingAction: null, + delegate: null, + creator: null, + }); +} + +export {setVacationDelegate, deleteVacationDelegate, clearVacationDelegateError}; diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index c74f44ad19ef..1abbe2ac0ff4 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -21,7 +21,7 @@ import ControlSelection from '@libs/ControlSelection'; import DateUtils from '@libs/DateUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils'; -import {getReportActionMessage} from '@libs/ReportActionsUtils'; +import {getManagerOnVacation, getReportActionMessage, getSubmittedTo, getVacationer} from '@libs/ReportActionsUtils'; import { getDefaultWorkspaceAvatar, getDisplayNameForParticipant, @@ -123,6 +123,17 @@ function ReportActionItemSingle({ let displayName = getDisplayNameForParticipant({accountID: actorAccountID, personalDetailsData: personalDetails}); const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails?.[actorAccountID ?? CONST.DEFAULT_NUMBER_ID] ?? {}; const accountOwnerDetails = getPersonalDetailByEmail(login ?? ''); + + // Vacation delegate details for submitted action + const vacationer = getVacationer(action); + const submittedTo = getSubmittedTo(action); + const vacationDelegateDetailsForSubmit = getPersonalDetailByEmail(vacationer ?? ''); + const submittedToDetails = getPersonalDetailByEmail(submittedTo ?? ''); + + // Vacation delegate details for approved action + const managerOnVacation = getManagerOnVacation(action); + const vacationDelegateDetailsForApprove = getPersonalDetailByEmail(managerOnVacation ?? ''); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing let actorHint = (login || (displayName ?? '')).replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); const isTripRoom = isTripRoomReportUtils(report); @@ -288,6 +299,7 @@ function ReportActionItemSingle({ ); }; + const hasEmojiStatus = !displayAllActors && status?.emojiCode; const formattedDate = DateUtils.getStatusUntilDate(status?.clearAfter ?? ''); const statusText = status?.text ?? ''; @@ -346,6 +358,19 @@ function ReportActionItemSingle({ {!!action?.delegateAccountID && ( {translate('delegate.onBehalfOfMessage', {delegator: accountOwnerDetails?.displayName ?? ''})} )} + {!!vacationer && !!submittedTo && ( + + {translate('statusPage.toAsVacationDelegate', { + submittedToName: submittedToDetails?.displayName ?? submittedTo ?? '', + vacationDelegateName: vacationDelegateDetailsForSubmit?.displayName ?? vacationer ?? '', + })} + + )} + {!!managerOnVacation && ( + + {translate('statusPage.asVacationDelegate', {nameOrEmail: vacationDelegateDetailsForApprove?.displayName ?? managerOnVacation ?? ''})} + + )} {children} diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx index e26a273f682f..721fc10dc6d2 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -89,6 +89,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr const [walletTerms] = useOnyx(ONYXKEYS.WALLET_TERMS, {canBeMissing: true}); const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST, {canBeMissing: true}); const [privatePersonalDetails] = useOnyx(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {canBeMissing: true}); + const [vacationDelegate] = useOnyx(ONYXKEYS.NVP_PRIVATE_VACATION_DELEGATE, {canBeMissing: true}); const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {canBeMissing: true}); const [allCards] = useOnyx(ONYXKEYS.CARD_LIST, {canBeMissing: true}); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); @@ -149,7 +150,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr * @returns object with translationKey, style and items for the account section */ const accountMenuItemsData: Menu = useMemo(() => { - const profileBrickRoadIndicator = getProfilePageBrickRoadIndicator(loginList, privatePersonalDetails); + const profileBrickRoadIndicator = getProfilePageBrickRoadIndicator(loginList, privatePersonalDetails, vacationDelegate); const items: MenuData[] = [ { translationKey: 'common.profile', @@ -200,6 +201,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr }, [ loginList, privatePersonalDetails, + vacationDelegate, walletBrickRoadIndicator, hasActivatedWallet, userWallet?.currentBalance, diff --git a/src/pages/settings/Profile/CustomStatus/StatusPage.tsx b/src/pages/settings/Profile/CustomStatus/StatusPage.tsx index 09b8f7357281..953f2250086c 100644 --- a/src/pages/settings/Profile/CustomStatus/StatusPage.tsx +++ b/src/pages/settings/Profile/CustomStatus/StatusPage.tsx @@ -11,6 +11,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; @@ -22,8 +23,11 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; import focusAfterModalClose from '@libs/focusAfterModalClose'; +import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; import Navigation from '@libs/Navigation/Navigation'; +import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils'; import {clearCustomStatus, clearDraftCustomStatus, updateCustomStatus, updateDraftCustomStatus} from '@userActions/User'; +import {clearVacationDelegateError} from '@userActions/VacationDelegate'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -41,6 +45,12 @@ function StatusPage() { const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const formRef = useRef(null); const [brickRoadIndicator, setBrickRoadIndicator] = useState>(); + + const [vacationDelegate] = useOnyx(ONYXKEYS.NVP_PRIVATE_VACATION_DELEGATE, {canBeMissing: true}); + const hasVacationDelegate = !!vacationDelegate?.delegate; + const vacationDelegatePersonalDetails = getPersonalDetailByEmail(vacationDelegate?.delegate ?? ''); + const formattedDelegateLogin = formatPhoneNumber(vacationDelegatePersonalDetails?.login ?? ''); + const currentUserEmojiCode = currentUserPersonalDetails?.status?.emojiCode ?? ''; const currentUserStatusText = currentUserPersonalDetails?.status?.text ?? ''; const currentUserClearAfter = currentUserPersonalDetails?.status?.clearAfter ?? ''; @@ -231,6 +241,39 @@ function StatusPage() { /> )} + + {translate('statusPage.setVacationDelegate')} + {hasVacationDelegate && {translate('statusPage.vacationDelegate')}} + {hasVacationDelegate ? ( + + Navigation.navigate(ROUTES.SETTINGS_VACATION_DELEGATE)} + containerStyle={styles.pr2} + /> + + ) : ( + + Navigation.navigate(ROUTES.SETTINGS_VACATION_DELEGATE)} + containerStyle={styles.pr2} + /> + + )} + ); diff --git a/src/pages/settings/Profile/CustomStatus/VacationDelegatePage.tsx b/src/pages/settings/Profile/CustomStatus/VacationDelegatePage.tsx new file mode 100644 index 000000000000..e04a676230fc --- /dev/null +++ b/src/pages/settings/Profile/CustomStatus/VacationDelegatePage.tsx @@ -0,0 +1,241 @@ +import React, {useCallback, useEffect, useMemo, useState} from 'react'; +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 {useBetas} from '@components/OnyxProvider'; +import {useOptionsList} from '@components/OptionListContextProvider'; +import ScreenWrapper from '@components/ScreenWrapper'; +import SelectionList from '@components/SelectionList'; +import UserListItem from '@components/SelectionList/UserListItem'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useDebouncedState from '@hooks/useDebouncedState'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {searchInServer} from '@libs/actions/Report'; +import {clearVacationDelegateError, deleteVacationDelegate, setVacationDelegate} from '@libs/actions/VacationDelegate'; +import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; +import Navigation from '@libs/Navigation/Navigation'; +import {filterAndOrderOptions, getHeaderMessage, getValidOptions} from '@libs/OptionsListUtils'; +import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type {Participant} from '@src/types/onyx/IOU'; + +function useOptions() { + const betas = useBetas(); + const [isLoading, setIsLoading] = useState(true); + const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); + const {options: optionsList, areOptionsInitialized} = useOptionsList(); + const [vacationDelegate] = useOnyx(ONYXKEYS.NVP_PRIVATE_VACATION_DELEGATE, {canBeMissing: true}); + const currentVacationDelegate = vacationDelegate?.delegate; + const delegatePersonalDetails = getPersonalDetailByEmail(currentVacationDelegate ?? ''); + + const excludeLogins = useMemo( + () => ({ + ...CONST.EXPENSIFY_EMAILS_OBJECT, + [currentVacationDelegate ?? '']: true, + }), + [currentVacationDelegate], + ); + + const defaultOptions = useMemo(() => { + const {recentReports, personalDetails, userToInvite, currentUserOption} = getValidOptions( + { + reports: optionsList.reports, + personalDetails: optionsList.personalDetails, + }, + { + betas, + excludeLogins, + }, + ); + + const headerMessage = getHeaderMessage((recentReports?.length || 0) + (personalDetails?.length || 0) !== 0, !!userToInvite, ''); + + if (isLoading) { + // eslint-disable-next-line react-compiler/react-compiler + setIsLoading(false); + } + + return { + userToInvite, + recentReports, + personalDetails, + currentUserOption, + headerMessage, + }; + }, [optionsList.reports, optionsList.personalDetails, betas, excludeLogins, isLoading]); + + const options = useMemo(() => { + const filteredOptions = filterAndOrderOptions(defaultOptions, debouncedSearchValue.trim(), { + excludeLogins, + maxRecentReportsToShow: CONST.IOU.MAX_RECENT_REPORTS_TO_SHOW, + }); + const headerMessage = getHeaderMessage( + (filteredOptions.recentReports?.length || 0) + (filteredOptions.personalDetails?.length || 0) !== 0, + !!filteredOptions.userToInvite, + debouncedSearchValue, + ); + + return { + ...filteredOptions, + headerMessage, + }; + }, [debouncedSearchValue, defaultOptions, excludeLogins]); + + return {...options, vacationDelegate, searchValue, debouncedSearchValue, setSearchValue, areOptionsInitialized, delegatePersonalDetails}; +} + +function VacationDelegatePage() { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const [isWarningModalVisible, setIsWarningModalVisible] = useState(false); + const [newVacationDelegate, setNewVacationDelegate] = useState(''); + const {login: currentUserLogin} = useCurrentUserPersonalDetails(); + + const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false, canBeMissing: false}); + const {vacationDelegate, userToInvite, recentReports, personalDetails, searchValue, debouncedSearchValue, setSearchValue, headerMessage, areOptionsInitialized, delegatePersonalDetails} = + useOptions(); + + const sections = useMemo(() => { + const sectionsList = []; + + if (vacationDelegate && delegatePersonalDetails) { + sectionsList.push({ + title: undefined, + data: [ + { + ...delegatePersonalDetails, + text: delegatePersonalDetails?.displayName ?? '', + alternateText: delegatePersonalDetails?.login ?? undefined, + login: delegatePersonalDetails.login ?? undefined, + keyForList: `vacationDelegate-${delegatePersonalDetails.login}`, + isDisabled: false, + isSelected: true, + shouldShowSubscript: undefined, + icons: [ + { + source: delegatePersonalDetails?.avatar ?? Expensicons.FallbackAvatar, + name: formatPhoneNumber(delegatePersonalDetails?.login ?? ''), + type: CONST.ICON_TYPE_AVATAR, + id: delegatePersonalDetails?.accountID, + }, + ], + }, + ], + shouldShow: true, + }); + } + + sectionsList.push({ + title: translate('common.recents'), + data: recentReports, + shouldShow: recentReports?.length > 0, + }); + + sectionsList.push({ + title: translate('common.contacts'), + data: personalDetails, + shouldShow: personalDetails?.length > 0, + }); + + if (userToInvite) { + sectionsList.push({ + title: undefined, + data: [userToInvite], + shouldShow: true, + }); + } + + return sectionsList.map((section) => ({ + ...section, + data: section.data.map((option) => ({ + ...option, + text: option?.text ?? option?.displayName ?? '', + alternateText: option?.alternateText ?? option?.login ?? undefined, + keyForList: option.keyForList ?? '', + isDisabled: option.isDisabled ?? undefined, + isSelected: option.isSelected ?? undefined, + login: option.login ?? undefined, + shouldShowSubscript: option.shouldShowSubscript ?? undefined, + })), + })); + }, [vacationDelegate, delegatePersonalDetails, personalDetails, recentReports, translate, userToInvite]); + + const onSelectRow = useCallback( + (option: Participant) => { + if (option?.login === vacationDelegate?.delegate) { + deleteVacationDelegate(vacationDelegate); + Navigation.goBack(ROUTES.SETTINGS_STATUS); + return; + } + + setVacationDelegate(currentUserLogin ?? '', option?.login ?? '').then((response) => { + if (!response?.jsonCode) { + Navigation.goBack(ROUTES.SETTINGS_STATUS); + return; + } + + if (response.jsonCode === CONST.JSON_CODE.POLICY_DIFF_WARNING) { + setIsWarningModalVisible(true); + setNewVacationDelegate(option?.login ?? ''); + } + }); + }, + [currentUserLogin, vacationDelegate], + ); + + useEffect(() => { + searchInServer(debouncedSearchValue); + }, [debouncedSearchValue]); + + return ( + <> + + Navigation.goBack(ROUTES.SETTINGS_STATUS)} + /> + + + + + { + setIsWarningModalVisible(false); + setVacationDelegate(currentUserLogin ?? '', newVacationDelegate, true).then(() => Navigation.goBack(ROUTES.SETTINGS_STATUS)); + }} + onCancel={() => { + setIsWarningModalVisible(false); + clearVacationDelegateError(); + }} + confirmText={translate('common.confirm')} + cancelText={translate('common.cancel')} + /> + + ); +} + +VacationDelegatePage.displayName = 'VacationDelegatePage'; + +export default VacationDelegatePage; diff --git a/src/pages/settings/Profile/ProfilePage.tsx b/src/pages/settings/Profile/ProfilePage.tsx index 681514cf10ca..3bae27c7a4d9 100755 --- a/src/pages/settings/Profile/ProfilePage.tsx +++ b/src/pages/settings/Profile/ProfilePage.tsx @@ -64,6 +64,7 @@ function ProfilePage() { const privateDetails = privatePersonalDetails ?? {}; const legalName = `${privateDetails.legalFirstName ?? ''} ${privateDetails.legalLastName ?? ''}`.trim(); + const [vacationDelegate] = useOnyx(ONYXKEYS.NVP_PRIVATE_VACATION_DELEGATE, {canBeMissing: true}); const {isActingAsDelegate, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); const publicOptions = [ { @@ -81,6 +82,7 @@ function ProfilePage() { description: translate('statusPage.status'), title: emojiCode ? `${emojiCode} ${currentUserPersonalDetails?.status?.text ?? ''}` : '', pageRoute: ROUTES.SETTINGS_STATUS, + brickRoadIndicator: isEmptyObject(vacationDelegate?.errors) ? undefined : CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR, }, { description: translate('pronounsPage.pronouns'), diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 805c57b9afc2..1600062bbd2f 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -148,6 +148,12 @@ type OriginalMessageSubmitted = { /** Was the report submitted via harvesting (delayed submit) */ harvesting?: boolean; + + /** The login the approver who is acting on behalf of the vacationer */ + to?: string; + + /** The login of the approver who is on a vacation */ + vacationer?: string; }; /** Model of `closed` report action */ @@ -700,6 +706,9 @@ type OriginalMessageApproved = { /** Report ID of the expense */ expenseReportID: string; + + /** The login of approver who is on vacation */ + managerOnVacation?: string; }; /** Model of `forwarded` report action */ diff --git a/src/types/onyx/VacationDelegate.ts b/src/types/onyx/VacationDelegate.ts new file mode 100644 index 000000000000..9b39ae5f31b1 --- /dev/null +++ b/src/types/onyx/VacationDelegate.ts @@ -0,0 +1,15 @@ +import type * as OnyxCommon from './OnyxCommon'; + +/** Information about vacation delegate */ +type VacationDelegate = OnyxCommon.OnyxValueWithOfflineFeedback<{ + /** Email of the user that set the vacation delegate */ + creator?: string; + + /** Email of the vacation delegate */ + delegate?: string; + + /** Error message */ + errors?: OnyxCommon.Errors; +}>; + +export default VacationDelegate; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index a736dc09f92f..b86343599f10 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -112,6 +112,7 @@ import type TryNewDot from './TryNewDot'; import type UserLocation from './UserLocation'; import type UserMetadata from './UserMetadata'; import type UserWallet from './UserWallet'; +import type VacationDelegate from './VacationDelegate'; import type ValidateMagicCodeAction from './ValidateMagicCodeAction'; import type ValidateUserAndGetAccessiblePolicies from './ValidateUserAndGetAccessiblePolicies'; import type WalletAdditionalDetails from './WalletAdditionalDetails'; @@ -264,5 +265,6 @@ export type { ScheduleCallDraft, ValidateUserAndGetAccessiblePolicies, BillingReceiptDetails, + VacationDelegate, HybridApp, };