From 658549517d35a3335166c006b79cd9df7ebf268c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Musia=C5=82?= Date: Wed, 20 May 2026 16:28:49 +0200 Subject: [PATCH 01/10] Add MergeHRApprovalModePage --- src/languages/de.ts | 1 + src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/languages/fr.ts | 1 + src/languages/it.ts | 1 + src/languages/ja.ts | 1 + src/languages/nl.ts | 1 + src/languages/pl.ts | 1 + src/languages/pt-BR.ts | 1 + src/languages/zh-hans.ts | 1 + .../workspace/hr/HRApprovalModePageBase.tsx | 157 ++++++++++++++++++ .../hr/gusto/GustoApprovalModePage.tsx | 135 ++------------- .../hr/merge/MergeHRApprovalModePage.tsx | 40 +++-- 13 files changed, 202 insertions(+), 140 deletions(-) create mode 100644 src/pages/workspace/hr/HRApprovalModePageBase.tsx diff --git a/src/languages/de.ts b/src/languages/de.ts index c426efeb3ca7..8628161efb11 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7090,6 +7090,7 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc syncError: (providerName: string) => `Verbindung zu ${providerName} nicht möglich`, connectionDescription: (providerName: string) => `Verbinden Sie ${providerName}, um Mitarbeitergenehmigungen mit Ihrem Workspace zu synchronisieren.`, approvalMode: 'Genehmigungsmodus', + providerApprovalMode: (providerName: string) => `${providerName}-Genehmigungsmodus`, finalApprover: 'Endgültige:r Genehmiger:in', notSet: 'Nicht festgelegt', approvalModeDescription: (providerName: string) => `Mitglieder und Manager sind für die Synchronisation mit ${providerName} eingerichtet.`, diff --git a/src/languages/en.ts b/src/languages/en.ts index dcd723575c4e..9ecede88a77d 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -6445,6 +6445,7 @@ const translations = { syncError: (providerName: string) => `Can't connect to ${providerName}`, connectionDescription: (providerName: string) => `Connect ${providerName} to keep employee approvals in sync with your workspace.`, approvalMode: 'Approval mode', + providerApprovalMode: (providerName: string) => `${providerName} approval mode`, finalApprover: 'Final approver', notSet: 'Not set', approvalModeDescription: (providerName: string) => `Members and managers are set up to sync with ${providerName}.`, diff --git a/src/languages/es.ts b/src/languages/es.ts index f57b98681fa3..c536ee107ae1 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -6257,6 +6257,7 @@ ${amount} para ${merchant} - ${date}`, syncError: (providerName: string) => `No se puede conectar con ${providerName}`, connectionDescription: (providerName: string) => `Conecta ${providerName} para mantener sincronizadas las aprobaciones de empleados con tu espacio de trabajo.`, approvalMode: 'Modo de aprobación', + providerApprovalMode: (providerName: string) => `Modo de aprobación de ${providerName}`, finalApprover: 'Aprobador final', notSet: 'No configurado', approvalModeDescription: (providerName: string) => `Los miembros y gerentes están configurados para sincronizarse con ${providerName}.`, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 78e6cd917c12..efcb18fcd15c 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7112,6 +7112,7 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e syncError: (providerName: string) => `Impossible de se connecter à ${providerName}`, connectionDescription: (providerName: string) => `Connectez ${providerName} pour synchroniser les approbations des employés avec votre espace de travail.`, approvalMode: "Mode d'approbation", + providerApprovalMode: (providerName: string) => `Mode d'approbation ${providerName}`, finalApprover: 'Approbateur final', notSet: 'Non défini', approvalModeDescription: (providerName: string) => `Les membres et les responsables sont configurés pour se synchroniser avec ${providerName}.`, diff --git a/src/languages/it.ts b/src/languages/it.ts index 6e04ab1376fa..0501f8ea74a6 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7076,6 +7076,7 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, syncError: (providerName: string) => `Impossibile connettersi a ${providerName}`, connectionDescription: (providerName: string) => `Collega ${providerName} per mantenere sincronizzate le approvazioni dei dipendenti con il tuo spazio di lavoro.`, approvalMode: 'Modalità di approvazione', + providerApprovalMode: (providerName: string) => `Modalità di approvazione ${providerName}`, finalApprover: 'Approvatore finale', notSet: 'Non impostato', approvalModeDescription: (providerName: string) => `I membri e i responsabili sono configurati per la sincronizzazione con ${providerName}.`, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index c8b40f50c116..608d5cff4963 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -6998,6 +6998,7 @@ ${reportName} syncError: (providerName: string) => `${providerName}に接続できません`, connectionDescription: (providerName: string) => `${providerName}を接続して、従業員の承認をワークスペースと同期させましょう。`, approvalMode: '承認モード', + providerApprovalMode: (providerName: string) => `${providerName}の承認モード`, finalApprover: '最終承認者', notSet: '未設定', approvalModeDescription: (providerName: string) => `メンバーとマネージャーは ${providerName} と同期するように設定されています。`, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index ce2a913de53c..c477af313c4e 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7053,6 +7053,7 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, syncError: (providerName: string) => `Kan geen verbinding maken met ${providerName}`, connectionDescription: (providerName: string) => `Verbind ${providerName} om goedkeuringen van werknemers gesynchroniseerd te houden met je werkruimte.`, approvalMode: 'Goedkeuringsmodus', + providerApprovalMode: (providerName: string) => `${providerName}-goedkeuringsmodus`, finalApprover: 'Eindgoedkeurder', notSet: 'Niet ingesteld', approvalModeDescription: (providerName: string) => `Leden en managers zijn ingesteld om te synchroniseren met ${providerName}.`, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 821b725e378d..d04c2f953af8 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7047,6 +7047,7 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, syncError: (providerName: string) => `Nie można połączyć z ${providerName}`, connectionDescription: (providerName: string) => `Połącz ${providerName}, aby synchronizować akceptacje pracowników z Twoim miejscem pracy.`, approvalMode: 'Tryb zatwierdzania', + providerApprovalMode: (providerName: string) => `Tryb zatwierdzania ${providerName}`, finalApprover: 'Ostateczny zatwierdzający', notSet: 'Nie ustawiono', approvalModeDescription: (providerName: string) => `Członkowie i menedżerowie są skonfigurowani do synchronizacji z ${providerName}.`, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 9662a9caa1f1..5da647f8159a 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7053,6 +7053,7 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, syncError: (providerName: string) => `Não é possível conectar ao ${providerName}`, connectionDescription: (providerName: string) => `Conecte ${providerName} para manter as aprovações de funcionários sincronizadas com seu workspace.`, approvalMode: 'Modo de aprovação', + providerApprovalMode: (providerName: string) => `Modo de aprovação do ${providerName}`, finalApprover: 'Aprovador final', notSet: 'Não definido', approvalModeDescription: (providerName: string) => `Membros e gerentes estão configurados para sincronizar com ${providerName}.`, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 8eb560866b18..8ecf312a3ed5 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -6878,6 +6878,7 @@ ${reportName} syncError: (providerName: string) => `无法连接到 ${providerName}`, connectionDescription: (providerName: string) => `连接 ${providerName},以在您的工作区中同步员工审批。`, approvalMode: '审批模式', + providerApprovalMode: (providerName: string) => `${providerName}审批模式`, finalApprover: '最终审批人', notSet: '未设置', approvalModeDescription: (providerName: string) => `成员和管理员已设置为与 ${providerName} 同步。`, diff --git a/src/pages/workspace/hr/HRApprovalModePageBase.tsx b/src/pages/workspace/hr/HRApprovalModePageBase.tsx new file mode 100644 index 000000000000..910e6006692f --- /dev/null +++ b/src/pages/workspace/hr/HRApprovalModePageBase.tsx @@ -0,0 +1,157 @@ +import React, {useState} from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +import Button from '@components/Button'; +import FixedFooter from '@components/FixedFooter'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import type {LocaleContextProps} from '@components/LocaleContextProvider'; +import {ModalActions} from '@components/Modal/Global/ModalContext'; +import RenderHTML from '@components/RenderHTML'; +import ScreenWrapper from '@components/ScreenWrapper'; +import SelectionList from '@components/SelectionList'; +import SingleSelectListItem from '@components/SelectionList/ListItem/SingleSelectListItem'; +import type {ListItem} from '@components/SelectionList/types'; +import Text from '@components/Text'; +import useConfirmModal from '@hooks/useConfirmModal'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import usePermissions from '@hooks/usePermissions'; +import usePolicy from '@hooks/usePolicy'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type Beta from '@src/types/onyx/Beta'; +import type Policy from '@src/types/onyx/Policy'; +import type {PolicyConnectionSyncProgress} from '@src/types/onyx/Policy'; + +type ApprovalModeValue = ValueOf | ValueOf; + +type HRApprovalModeProviderConfig = { + testID: string; + beta: Beta; + isConnected: (policy: OnyxEntry) => boolean; + approvalModes: {BASIC: T; MANAGER: T; CUSTOM: T}; + getCurrentApprovalMode: (policy: OnyxEntry) => T | null; + getProviderName: (policy: OnyxEntry, translate: LocaleContextProps['translate']) => string; + getHeaderTitle: (providerName: string, translate: LocaleContextProps['translate']) => string; + onSave: (params: {policyID: string; draftApprovalMode: T; currentApprovalMode: T | null; connectionSyncProgress?: OnyxEntry}) => void; +}; + +type ApprovalModeListItem = ListItem & { + value: T; +}; + +type HRApprovalModePageBaseProps = { + policyID: string; + config: HRApprovalModeProviderConfig; +}; + +function HRApprovalModePageBase({policyID, config}: HRApprovalModePageBaseProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const {showConfirmModal} = useConfirmModal(); + const {isBetaEnabled} = usePermissions(); + const policy = usePolicy(policyID); + const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policyID}`); + + const providerName = config.getProviderName(policy, translate); + const currentApprovalMode = config.getCurrentApprovalMode(policy); + const [draftApprovalMode, setDraftApprovalMode] = useState(); + const selectedApprovalMode = draftApprovalMode ?? currentApprovalMode; + const isSaveDisabled = !draftApprovalMode || draftApprovalMode === currentApprovalMode; + + const approvalModeOptions: Array> = [ + { + text: translate('workspace.hr.approvalModes.basic.label'), + alternateText: translate('workspace.hr.approvalModes.basic.description'), + keyForList: config.approvalModes.BASIC, + value: config.approvalModes.BASIC, + isSelected: selectedApprovalMode === config.approvalModes.BASIC, + }, + { + text: translate('workspace.hr.approvalModes.manager.label'), + alternateText: translate('workspace.hr.approvalModes.manager.description', providerName), + keyForList: config.approvalModes.MANAGER, + value: config.approvalModes.MANAGER, + isSelected: selectedApprovalMode === config.approvalModes.MANAGER, + }, + { + text: translate('workspace.hr.approvalModes.custom.label'), + alternateText: translate('workspace.hr.approvalModes.custom.description'), + keyForList: config.approvalModes.CUSTOM, + value: config.approvalModes.CUSTOM, + isSelected: selectedApprovalMode === config.approvalModes.CUSTOM, + }, + ]; + const selectedApprovalModeKey = approvalModeOptions.find((option) => option.isSelected)?.keyForList; + + const saveApprovalMode = () => { + if (!draftApprovalMode) { + return; + } + + config.onSave({policyID, draftApprovalMode, currentApprovalMode, connectionSyncProgress}); + Navigation.goBack(); + }; + + const confirmSaveApprovalMode = () => { + showConfirmModal({ + title: translate('workspace.hr.approvalModeWarningTitle'), + prompt: , + confirmText: translate('workspace.hr.approvalModeWarningConfirm'), + cancelText: translate('common.cancel'), + }).then((result) => { + if (result?.action !== ModalActions.CONFIRM) { + return; + } + saveApprovalMode(); + }); + }; + + return ( + + + Navigation.goBack()} + /> + + {translate('workspace.hr.approvalModeDescription', providerName)} + setDraftApprovalMode(option.value)} + shouldSingleExecuteRowSelect + initiallyFocusedItemKey={selectedApprovalModeKey} + alternateNumberOfSupportedLines={3} + showScrollIndicator={false} + /> + +