Skip to content

Commit 07534df

Browse files
committed
Show Gusto sync results
1 parent 9d65949 commit 07534df

6 files changed

Lines changed: 199 additions & 0 deletions

File tree

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import React, {useState} from 'react';
2+
import {View} from 'react-native';
3+
import Button from '@components/Button';
4+
import FixedFooter from '@components/FixedFooter';
5+
import HeaderWithBackButton from '@components/HeaderWithBackButton';
6+
import Icon from '@components/Icon';
7+
import Modal from '@components/Modal';
8+
import type {ModalProps} from '@components/Modal/Global/ModalContext';
9+
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
10+
import ScreenWrapper from '@components/ScreenWrapper';
11+
import ScrollView from '@components/ScrollView';
12+
import Text from '@components/Text';
13+
import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
14+
import useLocalize from '@hooks/useLocalize';
15+
import useTheme from '@hooks/useTheme';
16+
import useThemeStyles from '@hooks/useThemeStyles';
17+
import type {GustoSyncResult, GustoSyncResultUser} from '@libs/API/GustoSyncResult';
18+
import CONST from '@src/CONST';
19+
20+
type GustoSyncResultsModalProps = ModalProps & {
21+
/** Sync result returned by the completed Gusto sync job */
22+
result: GustoSyncResult;
23+
};
24+
25+
function getResultCount(users?: GustoSyncResultUser[]) {
26+
return users?.length ?? 0;
27+
}
28+
29+
function GustoSyncResultsModal({result, closeModal}: GustoSyncResultsModalProps) {
30+
const {translate} = useLocalize();
31+
const styles = useThemeStyles();
32+
const theme = useTheme();
33+
const icons = useMemoizedLazyExpensifyIcons(['DownArrow', 'Sync']);
34+
const illustrations = useMemoizedLazyIllustrations(['NewUser']);
35+
const [isSkippedSectionExpanded, setIsSkippedSectionExpanded] = useState(false);
36+
37+
const addedCount = getResultCount(result.added);
38+
const removedCount = getResultCount(result.removed);
39+
const skippedCount = getResultCount(result.skipped);
40+
const closeResultsModal = () => closeModal();
41+
42+
const renderResultSummary = (label: string, count: number) => (
43+
<View style={[styles.mb6]}>
44+
<Text style={[styles.textSupporting, styles.mb1]}>{label}</Text>
45+
<Text style={[styles.textNormalThemeText, styles.textStrong]}>{translate('workspace.hr.gusto.syncResults.employeeCount', {count})}</Text>
46+
</View>
47+
);
48+
49+
return (
50+
<Modal
51+
type={CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED}
52+
isVisible
53+
onClose={closeResultsModal}
54+
shouldHandleNavigationBack
55+
enableEdgeToEdgeBottomSafeAreaPadding
56+
>
57+
<ScreenWrapper
58+
includePaddingTop={false}
59+
enableEdgeToEdgeBottomSafeAreaPadding
60+
testID="GustoSyncResultsModal"
61+
>
62+
<HeaderWithBackButton
63+
title={translate('workspace.hr.gusto.syncResults.title')}
64+
onBackButtonPress={closeResultsModal}
65+
/>
66+
<ScrollView contentContainerStyle={[styles.flexGrow1, styles.ph5]}>
67+
<View style={[styles.alignItemsCenter, styles.mt8, styles.mb8, styles.pRelative]}>
68+
<Icon
69+
src={illustrations.NewUser}
70+
width={88}
71+
height={88}
72+
/>
73+
<View style={[styles.pAbsolute, styles.rn3, styles.b0]}>
74+
<Icon
75+
src={icons.Sync}
76+
fill={theme.success}
77+
width={36}
78+
height={36}
79+
/>
80+
</View>
81+
</View>
82+
<Text style={[styles.textHeadlineH1, styles.mb8]}>{translate('workspace.hr.gusto.syncResults.successTitle')}</Text>
83+
{renderResultSummary(translate('workspace.hr.gusto.syncResults.added'), addedCount)}
84+
{renderResultSummary(translate('workspace.hr.gusto.syncResults.removed'), removedCount)}
85+
<PressableWithoutFeedback
86+
accessibilityLabel={translate('workspace.hr.gusto.syncResults.skipped')}
87+
role={CONST.ROLE.BUTTON}
88+
onPress={() => setIsSkippedSectionExpanded((isExpanded) => !isExpanded)}
89+
style={[styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter]}
90+
>
91+
<View>
92+
<Text style={[styles.textSupporting, styles.mb1]}>{translate('workspace.hr.gusto.syncResults.skipped')}</Text>
93+
<Text style={[styles.textNormalThemeText, styles.textStrong]}>{translate('workspace.hr.gusto.syncResults.employeeCount', {count: skippedCount})}</Text>
94+
</View>
95+
<Icon
96+
src={icons.DownArrow}
97+
fill={theme.icon}
98+
additionalStyles={isSkippedSectionExpanded ? {transform: [{rotate: '180deg'}]} : undefined}
99+
/>
100+
</PressableWithoutFeedback>
101+
{isSkippedSectionExpanded &&
102+
result.skipped?.map((user) => (
103+
<View
104+
key={user.email}
105+
style={[styles.mt4]}
106+
>
107+
<Text style={[styles.textNormalThemeText, styles.textStrong]}>{user.displayName ?? user.email}</Text>
108+
<Text style={[styles.textSupporting]}>{user.reason}</Text>
109+
</View>
110+
))}
111+
</ScrollView>
112+
<FixedFooter>
113+
<Button
114+
large
115+
success
116+
text={translate('common.buttonConfirm')}
117+
onPress={closeResultsModal}
118+
/>
119+
</FixedFooter>
120+
</ScreenWrapper>
121+
</Modal>
122+
);
123+
}
124+
125+
export default GustoSyncResultsModal;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {useEffect} from 'react';
2+
import type {OnyxEntry} from 'react-native-onyx';
3+
import GustoSyncResultsModal from '@components/GustoSyncResultsModal';
4+
import {useModal} from '@components/Modal/Global/ModalContext';
5+
import CONST from '@src/CONST';
6+
import type {PolicyConnectionSyncProgress} from '@src/types/onyx/Policy';
7+
import usePrevious from './usePrevious';
8+
9+
function useGustoSyncResultsModal(policyID: string, connectionSyncProgress: OnyxEntry<PolicyConnectionSyncProgress>, isFocused: boolean) {
10+
const {showModal} = useModal();
11+
const previousSyncProgress = usePrevious(connectionSyncProgress);
12+
13+
useEffect(() => {
14+
const isGustoSyncDoneWithResult =
15+
connectionSyncProgress?.connectionName === CONST.POLICY.CONNECTIONS.NAME.GUSTO &&
16+
connectionSyncProgress?.stageInProgress === CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.JOB_DONE &&
17+
!!connectionSyncProgress?.result;
18+
const wasSameGustoResultAlreadyHandled =
19+
previousSyncProgress?.connectionName === CONST.POLICY.CONNECTIONS.NAME.GUSTO &&
20+
previousSyncProgress?.stageInProgress === CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.JOB_DONE &&
21+
previousSyncProgress?.timestamp === connectionSyncProgress?.timestamp &&
22+
!!previousSyncProgress?.result;
23+
const didGustoSyncComplete =
24+
isFocused && isGustoSyncDoneWithResult && !wasSameGustoResultAlreadyHandled;
25+
26+
if (!didGustoSyncComplete) {
27+
return;
28+
}
29+
30+
showModal({
31+
component: GustoSyncResultsModal,
32+
props: {result: connectionSyncProgress.result},
33+
id: `gusto-sync-results-${policyID}`,
34+
});
35+
}, [
36+
connectionSyncProgress?.connectionName,
37+
connectionSyncProgress?.result,
38+
connectionSyncProgress?.stageInProgress,
39+
connectionSyncProgress?.timestamp,
40+
isFocused,
41+
policyID,
42+
previousSyncProgress?.connectionName,
43+
previousSyncProgress?.result,
44+
previousSyncProgress?.stageInProgress,
45+
previousSyncProgress?.timestamp,
46+
showModal,
47+
]);
48+
}
49+
50+
export default useGustoSyncResultsModal;

src/languages/en.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6431,6 +6431,14 @@ const translations = {
64316431
description: 'I’ll manually setup approval workflows in Expensify.',
64326432
},
64336433
},
6434+
syncResults: {
6435+
title: 'Gusto sync results',
6436+
successTitle: 'Successfully synced your Gusto connection!',
6437+
added: 'Added to Expensify',
6438+
removed: 'Removed from Expensify',
6439+
skipped: 'Skipped',
6440+
employeeCount: ({count}: {count: number}) => `${count} ${count === 1 ? 'employee' : 'employees'}`,
6441+
},
64346442
},
64356443
},
64366444
export: {

src/languages/es.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6268,6 +6268,14 @@ ${amount} para ${merchant} - ${date}`,
62686268
description: 'Configuraré manualmente los flujos de aprobación en Expensify.',
62696269
},
62706270
},
6271+
syncResults: {
6272+
title: 'Resultados de sincronización de Gusto',
6273+
successTitle: '¡Tu conexión de Gusto se sincronizó correctamente!',
6274+
added: 'Añadidos a Expensify',
6275+
removed: 'Eliminados de Expensify',
6276+
skipped: 'Omitidos',
6277+
employeeCount: ({count}) => `${count} ${count === 1 ? 'empleado' : 'empleados'}`,
6278+
},
62716279
},
62726280
},
62736281
export: {

src/pages/workspace/WorkspaceMembersPage.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'
2929
import useDebouncedAccessibilityAnnouncement from '@hooks/useDebouncedAccessibilityAnnouncement';
3030
import useDebouncedValue from '@hooks/useDebouncedValue';
3131
import useFilteredSelection from '@hooks/useFilteredSelection';
32+
import useGustoSyncResultsModal from '@hooks/useGustoSyncResultsModal';
3233
import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
3334
import useLocalize from '@hooks/useLocalize';
3435
import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
@@ -619,6 +620,8 @@ function WorkspaceMembersPage({personalDetails, route, policy}: WorkspaceMembers
619620
clearInviteDraft(route.params.policyID);
620621
}, [invitedEmailsToAccountIDsDraft, isFocused, accountIDs, prevAccountIDs, route.params.policyID]);
621622

623+
useGustoSyncResultsModal(policyID, connectionSyncProgress, isFocused);
624+
622625
const headerMessage = useMemo(() => {
623626
if (isOfflineAndNoMemberDataAvailable) {
624627
return translate('workspace.common.mustBeOnlineToViewMembers');

src/pages/workspace/hr/WorkspaceHRPage.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {useIsFocused} from '@react-navigation/native';
12
import React, {useCallback, useEffect, useMemo, useState} from 'react';
23
import {View} from 'react-native';
34
import type {ValueOf} from 'type-fest';
@@ -15,6 +16,7 @@ import Section from '@components/Section';
1516
import Text from '@components/Text';
1617
import ThreeDotsMenu from '@components/ThreeDotsMenu';
1718
import type ThreeDotsMenuProps from '@components/ThreeDotsMenu/types';
19+
import useGustoSyncResultsModal from '@hooks/useGustoSyncResultsModal';
1820
import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
1921
import useLocalize from '@hooks/useLocalize';
2022
import useNetwork from '@hooks/useNetwork';
@@ -46,6 +48,7 @@ function WorkspaceHRPage({
4648
},
4749
}: WorkspaceHRPageProps) {
4850
const {translate, datetimeToRelative, getLocalDateFromDatetime} = useLocalize();
51+
const isFocused = useIsFocused();
4952
const {isBetaEnabled} = usePermissions();
5053
const styles = useThemeStyles();
5154
const {shouldUseNarrowLayout} = useResponsiveLayout();
@@ -91,6 +94,8 @@ function WorkspaceHRPage({
9194
fetchPolicyHRPage();
9295
}, [fetchPolicyHRPage]);
9396

97+
useGustoSyncResultsModal(policyID, connectionSyncProgress, isFocused);
98+
9499
const overflowMenu: ThreeDotsMenuProps['menuItems'] = useMemo(
95100
() => [
96101
{

0 commit comments

Comments
 (0)