Skip to content

Commit ba5ad38

Browse files
committed
Add Gusto connected actions
1 parent 0ace261 commit ba5ad38

4 files changed

Lines changed: 106 additions & 14 deletions

File tree

src/languages/en.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6340,6 +6340,12 @@ const translations = {
63406340
gusto: {
63416341
title: 'Gusto',
63426342
connect: 'Connect',
6343+
syncNow: 'Sync now',
6344+
disconnect: 'Disconnect',
6345+
lastSync: (relativeDate: string) => `Last synced ${relativeDate}`,
6346+
syncError: "Can't connect to Gusto",
6347+
disconnectTitle: 'Disconnect Gusto',
6348+
disconnectPrompt: 'Are you sure you want to disconnect Gusto?',
63436349
connectionDescription: 'Connect Gusto to keep employee approvals in sync with your workspace.',
63446350
approvalMode: 'Approval mode',
63456351
finalApprover: 'Final approver',

src/languages/es.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6200,6 +6200,12 @@ ${amount} para ${merchant} - ${date}`,
62006200
gusto: {
62016201
title: 'Gusto',
62026202
connect: 'Conectar',
6203+
syncNow: 'Sincronizar ahora',
6204+
disconnect: 'Desconectar',
6205+
lastSync: (relativeDate) => `Última sincronización ${relativeDate}`,
6206+
syncError: 'No se puede conectar con Gusto',
6207+
disconnectTitle: 'Desconectar Gusto',
6208+
disconnectPrompt: '¿Seguro que quieres desconectar Gusto?',
62036209
connectionDescription: 'Conecta Gusto para mantener sincronizadas las aprobaciones de empleados con tu espacio de trabajo.',
62046210
approvalMode: 'Modo de aprobación',
62056211
finalApprover: 'Aprobador final',

src/libs/actions/connections/index.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,7 @@ function removePolicyConnection(policy: Policy, connectionName: PolicyConnection
9898
}
9999

100100
/**
101-
* This method returns read command and stage in progress for a given accounting integration.
102-
*
103-
* @param policyID - ID of the policy for which the sync is needed
104-
* @param connectionName - Name of the connection, QBO/Xero
101+
* This method returns read command and stage in progress for a given policy connection.
105102
*/
106103
function getSyncConnectionParameters(connectionName: PolicyConnectionName) {
107104
switch (connectionName) {
@@ -120,16 +117,19 @@ function getSyncConnectionParameters(connectionName: PolicyConnectionName) {
120117
case CONST.POLICY.CONNECTIONS.NAME.QBD: {
121118
return {readCommand: READ_COMMANDS.SYNC_POLICY_TO_QUICKBOOKS_DESKTOP, stageInProgress: CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.STARTING_IMPORT_QBD};
122119
}
120+
case CONST.POLICY.CONNECTIONS.NAME.GUSTO: {
121+
return {readCommand: READ_COMMANDS.SYNC_GUSTO, stageInProgress: CONST.POLICY.CONNECTIONS.SYNC_STAGE_NAME.STARTING_IMPORT_GUSTO};
122+
}
123123
default:
124124
return undefined;
125125
}
126126
}
127127

128128
/**
129-
* This method helps in syncing policy to the connected accounting integration.
129+
* This method helps in syncing policy to the connected integration.
130130
*
131131
* @param policy - Policy for which the sync is needed
132-
* @param connectionName - Name of the connection, QBO/Xero
132+
* @param connectionName - Name of the connection
133133
* @param forceDataRefresh - If true, it will trigger a full data refresh
134134
*/
135135
function syncConnection(policy: Policy | undefined, connectionName: PolicyConnectionName | undefined, forceDataRefresh = false) {

src/pages/workspace/hr/WorkspaceHRPage.tsx

Lines changed: 88 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,35 @@
1-
import React, {useCallback, useEffect, useState} from 'react';
1+
import React, {useCallback, useEffect, useMemo, useState} from 'react';
22
import {View} from 'react-native';
3+
import ActivityIndicator from '@components/ActivityIndicator';
34
import Button from '@components/Button';
5+
import ConfirmModal from '@components/ConfirmModal';
46
import ConnectToGustoFlow from '@components/ConnectToGustoFlow';
57
import HeaderWithBackButton from '@components/HeaderWithBackButton';
68
import MenuItem from '@components/MenuItem';
79
import ScreenWrapper from '@components/ScreenWrapper';
810
import ScrollView from '@components/ScrollView';
911
import Section from '@components/Section';
1012
import Text from '@components/Text';
13+
import ThreeDotsMenu from '@components/ThreeDotsMenu';
14+
import type ThreeDotsMenuProps from '@components/ThreeDotsMenu/types';
1115
import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
1216
import useLocalize from '@hooks/useLocalize';
1317
import useNetwork from '@hooks/useNetwork';
18+
import useOnyx from '@hooks/useOnyx';
1419
import usePermissions from '@hooks/usePermissions';
1520
import usePolicy from '@hooks/usePolicy';
1621
import useResponsiveLayout from '@hooks/useResponsiveLayout';
1722
import useThemeStyles from '@hooks/useThemeStyles';
1823
import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle';
24+
import {isConnectionInProgress, removePolicyConnection, syncConnection} from '@libs/actions/connections';
1925
import {openPolicyHRPage} from '@libs/actions/PolicyConnections';
2026
import Navigation from '@libs/Navigation/Navigation';
2127
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
2228
import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types';
23-
import {isGustoConnected} from '@libs/PolicyUtils';
29+
import {getIntegrationLastSuccessfulDate, isGustoConnected} from '@libs/PolicyUtils';
2430
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
2531
import CONST from '@src/CONST';
32+
import ONYXKEYS from '@src/ONYXKEYS';
2633
import type SCREENS from '@src/SCREENS';
2734

2835
type WorkspaceHRPageProps = PlatformStackScreenProps<WorkspaceSplitNavigatorParamList, typeof SCREENS.WORKSPACE.HR>;
@@ -32,28 +39,68 @@ function WorkspaceHRPage({
3239
params: {policyID},
3340
},
3441
}: WorkspaceHRPageProps) {
35-
const {translate} = useLocalize();
42+
const {translate, datetimeToRelative, getLocalDateFromDatetime} = useLocalize();
3643
const {isBetaEnabled} = usePermissions();
3744
const styles = useThemeStyles();
3845
const {shouldUseNarrowLayout} = useResponsiveLayout();
3946
const policy = usePolicy(policyID);
4047
const [activeGustoFlowKey, setActiveGustoFlowKey] = useState<number>();
41-
const icons = useMemoizedLazyExpensifyIcons(['GustoSquare']);
48+
const [isDisconnectModalOpen, setIsDisconnectModalOpen] = useState(false);
49+
const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policyID}`);
50+
const icons = useMemoizedLazyExpensifyIcons(['GustoSquare', 'Sync', 'Trashcan']);
4251
const illustrations = useMemoizedLazyIllustrations(['NewUser']);
52+
const gustoConnection = policy?.connections?.gusto;
4353
const isConnected = isGustoConnected(policy);
54+
const isGustoSyncInProgress = connectionSyncProgress?.connectionName === CONST.POLICY.CONNECTIONS.NAME.GUSTO && isConnectionInProgress(connectionSyncProgress, policy);
55+
const successfulDate = getIntegrationLastSuccessfulDate(
56+
getLocalDateFromDatetime,
57+
gustoConnection,
58+
connectionSyncProgress?.connectionName === CONST.POLICY.CONNECTIONS.NAME.GUSTO ? connectionSyncProgress : undefined,
59+
);
60+
const hasGustoSyncError = !isGustoSyncInProgress && gustoConnection?.lastSync?.isSuccessful === false && !!gustoConnection?.lastSync?.errorDate;
61+
const lastSyncErrorMessage = hasGustoSyncError ? (gustoConnection?.lastSync?.errorMessage ?? translate('workspace.hr.gusto.syncError')) : undefined;
62+
const connectionDescription = useMemo(() => {
63+
if (isGustoSyncInProgress && connectionSyncProgress?.stageInProgress) {
64+
return translate('workspace.hr.syncStageName', {stage: connectionSyncProgress.stageInProgress});
65+
}
66+
67+
if (successfulDate && !lastSyncErrorMessage) {
68+
return translate('workspace.hr.gusto.lastSync', datetimeToRelative(successfulDate));
69+
}
70+
71+
return undefined;
72+
}, [connectionSyncProgress?.stageInProgress, datetimeToRelative, isGustoSyncInProgress, lastSyncErrorMessage, successfulDate, translate]);
4473

4574
useWorkspaceDocumentTitle(undefined, 'workspace.common.hr');
4675

4776
const fetchPolicyHRPage = useCallback(() => {
4877
openPolicyHRPage(policyID);
4978
}, [policyID]);
5079

51-
useNetwork({onReconnect: fetchPolicyHRPage});
80+
const {isOffline} = useNetwork({onReconnect: fetchPolicyHRPage});
5281

5382
useEffect(() => {
5483
fetchPolicyHRPage();
5584
}, [fetchPolicyHRPage]);
5685

86+
const overflowMenu: ThreeDotsMenuProps['menuItems'] = useMemo(
87+
() => [
88+
{
89+
icon: icons.Sync,
90+
text: translate('workspace.hr.gusto.syncNow'),
91+
onSelected: () => syncConnection(policy, CONST.POLICY.CONNECTIONS.NAME.GUSTO),
92+
disabled: isOffline,
93+
},
94+
{
95+
icon: icons.Trashcan,
96+
text: translate('workspace.hr.gusto.disconnect'),
97+
onSelected: () => setIsDisconnectModalOpen(true),
98+
shouldCallAfterModalHide: true,
99+
},
100+
],
101+
[icons.Sync, icons.Trashcan, isOffline, policy, translate],
102+
);
103+
57104
return (
58105
<AccessOrNotFoundWrapper
59106
accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.CONTROL]}
@@ -92,22 +139,55 @@ function WorkspaceHRPage({
92139
title={translate('workspace.hr.gusto.title')}
93140
icon={icons.GustoSquare}
94141
iconType={CONST.ICON_TYPE_AVATAR}
95-
wrapperStyle={[styles.ph0, styles.pv2, styles.mt4]}
142+
wrapperStyle={[styles.ph0, styles.pv2, styles.mt4, !!lastSyncErrorMessage && styles.pb0]}
96143
interactive={false}
97-
shouldShowRightComponent={!isConnected}
144+
description={connectionDescription}
145+
errorText={lastSyncErrorMessage}
146+
errorTextStyle={[styles.mt5]}
147+
shouldShowRedDotIndicator
148+
shouldShowRightComponent
98149
rightComponent={
99150
!isConnected ? (
100151
<Button
101152
small
102153
text={translate('workspace.hr.gusto.connect')}
103154
onPress={() => setActiveGustoFlowKey(Math.random())}
104155
/>
105-
) : undefined
156+
) : isGustoSyncInProgress ? (
157+
<ActivityIndicator
158+
style={[styles.popoverMenuIcon]}
159+
reasonAttributes={{context: 'WorkspaceHRPage.gustoSync'}}
160+
/>
161+
) : (
162+
<ThreeDotsMenu
163+
shouldSelfPosition
164+
menuItems={overflowMenu}
165+
anchorAlignment={{
166+
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT,
167+
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP,
168+
}}
169+
/>
170+
)
106171
}
107172
/>
108173
</Section>
109174
</View>
110175
</ScrollView>
176+
<ConfirmModal
177+
title={translate('workspace.hr.gusto.disconnectTitle')}
178+
isVisible={isDisconnectModalOpen}
179+
onConfirm={() => {
180+
if (policy) {
181+
removePolicyConnection(policy, CONST.POLICY.CONNECTIONS.NAME.GUSTO);
182+
}
183+
setIsDisconnectModalOpen(false);
184+
}}
185+
onCancel={() => setIsDisconnectModalOpen(false)}
186+
prompt={translate('workspace.hr.gusto.disconnectPrompt')}
187+
confirmText={translate('workspace.hr.gusto.disconnect')}
188+
cancelText={translate('common.cancel')}
189+
danger
190+
/>
111191
</ScreenWrapper>
112192
</AccessOrNotFoundWrapper>
113193
);

0 commit comments

Comments
 (0)