Skip to content

Commit 94a5fe7

Browse files
authored
feat: deactivate partner form (#1581)
1 parent 4e3c8ab commit 94a5fe7

File tree

16 files changed

+325
-48
lines changed

16 files changed

+325
-48
lines changed

components/forms/CreatePartnerAdminForm.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ const CreatePartnerAdminForm = () => {
2626

2727
const [loading, setLoading] = useState<boolean>(false);
2828
const [selectedPartner, setSelectedPartner] = useState<string>('');
29-
const [email, setEmail] = useState<string | null>(null);
30-
const [name, setName] = useState<string | null>(null);
29+
const [email, setEmail] = useState<string | null>('');
30+
const [name, setName] = useState<string | null>('');
3131

3232
const [formSubmitSuccess, setFormSubmitSuccess] = useState<boolean>(false);
3333
const [addPartnerAdmin] = useAddPartnerAdminMutation();
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
'use client';
2+
3+
import { useGetPartnersQuery, useUpdatePartnerActiveMutation } from '@/lib/api';
4+
import {
5+
UPDATE_PARTNER_ACTIVE_ERROR,
6+
UPDATE_PARTNER_ACTIVE_REQUEST,
7+
UPDATE_PARTNER_ACTIVE_SUCCESS,
8+
} from '@/lib/constants/events';
9+
import { useTypedSelector } from '@/lib/hooks/store';
10+
import { getErrorMessage } from '@/lib/utils/errorMessage';
11+
import logEvent from '@/lib/utils/logEvent';
12+
import LoadingButton from '@mui/lab/LoadingButton';
13+
import { Box, Button, MenuItem, TextField, Typography } from '@mui/material';
14+
import { useRollbar } from '@rollbar/react';
15+
import { useTranslations } from 'next-intl';
16+
import * as React from 'react';
17+
import { useState } from 'react';
18+
19+
const UpdatePartnerActiveForm = () => {
20+
const partners = useTypedSelector((state) => state.partners);
21+
const rollbar = useRollbar();
22+
23+
useGetPartnersQuery(undefined);
24+
25+
const t = useTranslations('Admin.updatePartner');
26+
27+
const [loading, setLoading] = useState<boolean>(false);
28+
const [selectedPartner, setSelectedPartner] = useState<string>('');
29+
const [selectedPartnerName, setSelectedPartnerName] = useState<string>('');
30+
const [isActive, setIsActive] = useState<boolean | null>(null);
31+
32+
const [updatePartnerActive] = useUpdatePartnerActiveMutation();
33+
const [formError, setFormError] = useState<
34+
string | React.ReactNode[] | React.ReactElement<any, string | React.JSXElementConstructor<any>>
35+
>();
36+
const [isSuccess, setIsSuccess] = useState<boolean>(false);
37+
38+
const selectPartner = (event: React.ChangeEvent<HTMLInputElement>) => {
39+
setLoading(true);
40+
setSelectedPartner(event.target.value);
41+
const partner = partners.find((partner) => partner.id === event.target.value);
42+
if (partner) {
43+
setIsActive(partner.isActive);
44+
setSelectedPartnerName(partner.name);
45+
}
46+
setIsSuccess(false);
47+
setLoading(false);
48+
};
49+
50+
const submitHandler = async (event: React.FormEvent<HTMLFormElement>) => {
51+
event.preventDefault();
52+
setLoading(true);
53+
54+
logEvent(UPDATE_PARTNER_ACTIVE_REQUEST, { active: !isActive });
55+
56+
const partnerResponse = await updatePartnerActive({
57+
id: selectedPartner,
58+
active: !isActive as boolean,
59+
});
60+
61+
if (partnerResponse.data) {
62+
logEvent(UPDATE_PARTNER_ACTIVE_SUCCESS, { active: !isActive });
63+
}
64+
65+
if (partnerResponse.error) {
66+
const error = partnerResponse.error;
67+
const errorMessage = getErrorMessage(error);
68+
69+
logEvent(UPDATE_PARTNER_ACTIVE_ERROR, {
70+
active: !isActive,
71+
error: errorMessage,
72+
});
73+
rollbar.error(t('error') + errorMessage);
74+
75+
setFormError(t('error') + errorMessage);
76+
setLoading(false);
77+
return;
78+
}
79+
80+
setLoading(false);
81+
setIsSuccess(true);
82+
};
83+
84+
const resetForm = (event: React.MouseEvent<HTMLButtonElement>) => {
85+
event.preventDefault();
86+
setIsSuccess(false);
87+
setSelectedPartner('');
88+
setIsActive(null);
89+
};
90+
91+
const FormResetButton = () => (
92+
<Box>
93+
<Button sx={{ mt: 3 }} variant="contained" color="secondary" onClick={resetForm}>
94+
{t('reset')}
95+
</Button>
96+
</Box>
97+
);
98+
99+
const FormSuccess = () => (
100+
<Box>
101+
<Typography fontWeight={500} mb={1}>
102+
{t.rich('successDescription', {
103+
status: isActive ? 'inactive' : 'active',
104+
partner: selectedPartnerName || 'partner',
105+
})}
106+
</Typography>
107+
<Typography>
108+
{t.rich(isActive ? 'inactiveSuccess' : 'activeSuccess', { partner: selectedPartnerName })}
109+
</Typography>
110+
<FormResetButton />
111+
</Box>
112+
);
113+
114+
if (isSuccess) {
115+
return <FormSuccess />;
116+
}
117+
118+
return (
119+
<form autoComplete="off" onSubmit={submitHandler}>
120+
<TextField
121+
id="selectPartner"
122+
name="selectPartner"
123+
key="select-partner"
124+
fullWidth
125+
select
126+
label={t('partnerNameLabel')}
127+
onChange={selectPartner}
128+
variant="standard"
129+
required
130+
value={selectedPartner}
131+
>
132+
{partners.map((option, index) => (
133+
<MenuItem key={`partner-name-${index}`} value={option.id}>
134+
{option.name}
135+
</MenuItem>
136+
))}
137+
</TextField>
138+
139+
{formError && <Typography color="error.main">{formError}</Typography>}
140+
141+
{selectedPartner && (
142+
<Typography>
143+
{t.rich(isActive ? 'activeDescription' : 'inactiveDescription', {
144+
partner: selectedPartnerName,
145+
})}
146+
</Typography>
147+
)}
148+
{selectedPartner && isActive !== null && (
149+
<LoadingButton
150+
variant="contained"
151+
color={isActive ? 'error' : 'secondary'}
152+
type="submit"
153+
loading={loading}
154+
sx={{ mt: 2 }}
155+
>
156+
{t.rich(isActive ? 'activeLabel' : 'inactiveLabel', { partner: selectedPartnerName })}
157+
</LoadingButton>
158+
)}
159+
</form>
160+
);
161+
};
162+
163+
export default UpdatePartnerActiveForm;

components/forms/UpdatePartnerAdminForm.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { SyntheticEvent, useEffect, useState } from 'react';
2525
const UpdatePartnerAdminForm = () => {
2626
const rollbar = useRollbar();
2727

28-
const t = useTranslations('Admin.updatePartner');
28+
const t = useTranslations('Admin.updatePartnerAdmin');
2929
const dispatch: any = useAppDispatch();
3030

3131
const [loading, setLoading] = useState<boolean>(false);
@@ -45,14 +45,16 @@ const UpdatePartnerAdminForm = () => {
4545
setAutocompleteSearchQueryIsLoading(true);
4646
const searchCriteria = {
4747
email: autocompleteSearchQuery,
48-
partnerAdmin: true,
48+
partnerAdmin: { partnerAdminId: 'IS NOT NULL' },
4949
include: ['partnerAdmin'],
5050
limit: 10,
5151
};
5252

5353
const result = await dispatch(
5454
api.endpoints.getUsers.initiate(
55-
{ searchCriteria: JSON.stringify(searchCriteria) },
55+
{
56+
searchCriteria: JSON.stringify(searchCriteria),
57+
},
5658
// We don't want this request cached as a user might use this request to check their updates have worked on the form
5759
{ forceRefetch: true },
5860
),

components/pages/AdminDashboardPage.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import CreatePartnerAdminForm from '@/components/forms/CreatePartnerAdminForm';
44
import UpdatePartnerAdminForm from '@/components/forms/UpdatePartnerAdminForm';
55
import UpdateTherapyAdminForm from '@/components/forms/UpdateTherapyAdminForm';
66
import AdminHeader from '@/components/layout/PartnerAdminHeader';
7-
import { CREATE_PARTNER_ACCESS_VIEWED } from '@/lib/constants/events';
7+
import { ADMIN_DASHBOARD_VIEWED } from '@/lib/constants/events';
88
import logEvent from '@/lib/utils/logEvent';
99
import { rowStyle } from '@/styles/common';
1010
import { Box, Card, CardContent, Container, Typography } from '@mui/material';
1111
import { useTranslations } from 'next-intl';
1212
import { useEffect } from 'react';
13+
import UpdatePartnerActiveForm from '../forms/UpdatePartnerActiveForm';
1314

1415
const containerStyle = {
1516
backgroundColor: 'secondary.light',
@@ -29,7 +30,7 @@ export default function AdminDashboardPage() {
2930
};
3031

3132
useEffect(() => {
32-
logEvent(CREATE_PARTNER_ACCESS_VIEWED);
33+
logEvent(ADMIN_DASHBOARD_VIEWED);
3334
}, []);
3435

3536
return (
@@ -71,11 +72,19 @@ export default function AdminDashboardPage() {
7172
<Card sx={cardStyle}>
7273
<CardContent>
7374
<Typography variant="h2" component="h2">
74-
{t('updatePartner.title')}
75+
{t('updatePartnerAdmin.title')}
7576
</Typography>
7677
<UpdatePartnerAdminForm />
7778
</CardContent>
7879
</Card>
80+
<Card sx={cardStyle}>
81+
<CardContent>
82+
<Typography variant="h2" component="h2">
83+
{t('updatePartner.title')}
84+
</Typography>
85+
<UpdatePartnerActiveForm />
86+
</CardContent>
87+
</Card>
7988
</Container>
8089
</Box>
8190
);

components/pages/CreateAccessCodePage.tsx

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,11 @@
22

33
import CreateAccessCodeForm from '@/components/forms/CreateAccessCodeForm';
44
import AdminHeader from '@/components/layout/PartnerAdminHeader';
5-
import { CREATE_PARTNER_ACCESS_VIEWED } from '@/lib/constants/events';
65
import { useTypedSelector } from '@/lib/hooks/store';
7-
import logEvent from '@/lib/utils/logEvent';
86
import bloomLogo from '@/public/bloom_logo.svg';
97
import { rowStyle } from '@/styles/common';
108
import { Box, Card, CardContent, Container, Typography } from '@mui/material';
119
import { useTranslations } from 'next-intl';
12-
import { useEffect } from 'react';
1310

1411
const containerStyle = {
1512
backgroundColor: 'secondary.light',
@@ -31,10 +28,6 @@ export default function CreateAccessCodePage() {
3128
partnerLogoAlt: partnerAdmin.partner?.logoAlt || 'alt.bloomLogo',
3229
};
3330

34-
useEffect(() => {
35-
logEvent(CREATE_PARTNER_ACCESS_VIEWED);
36-
}, []);
37-
3831
return (
3932
<Box>
4033
<AdminHeader

cypress/integration/tests/admin-dashboard.cy.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe.skip('Admin dashboard page should display', () => {
2020
cy.get('h2').should('contain', 'Create an admin account');
2121
cy.get('p').should('contain', 'Admin accounts are able to generate therapy codes');
2222

23-
cy.get('label.Mui-required').contains('Select the partner');
23+
cy.get('label.Mui-required').contains('Select partner');
2424
cy.get('input[name="selectPartner"]').should('exist');
2525

2626
cy.get('label.Mui-required').contains('Email address');
@@ -35,7 +35,7 @@ describe.skip('Admin dashboard page should display', () => {
3535
it('update therapy sessions panel', () => {
3636
cy.get('h2').should('contain', 'Update therapy sessions');
3737

38-
cy.get('label').contains(`Type a user's email address`);
38+
cy.get('label').contains(`Search by user email`);
3939
cy.get('input[id="user-email-address-search"]').should('exist');
4040

4141
cy.get('button').contains('Update therapy sessions');
@@ -44,7 +44,7 @@ describe.skip('Admin dashboard page should display', () => {
4444
it('update partner admin panel', () => {
4545
cy.get('h2').should('contain', 'Update partner admin');
4646

47-
cy.get('label').contains('Type at least 4 letters');
47+
cy.get('label').contains('Search by admin email');
4848
cy.get('input[id="partnerAdmin"]').should('exist');
4949

5050
cy.get('button').contains('Update partner admin');

i18n/messages/admin/de.json

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"createPartnerAdmin": {
99
"title": "Create an admin account",
1010
"description": "Admin accounts are able to generate therapy codes",
11-
"partnerNameLabel": "Select the partner",
11+
"partnerNameLabel": "Select partner",
1212
"submit": "Create admin account",
1313
"successDescription": "Partner Admin created! Email them to say they have an account and will need to use the 'Forgot password' flow to choose a password and be able to login for the first time.",
1414
"emailAddressLabel": "Email address",
@@ -18,22 +18,37 @@
1818
},
1919
"updateTherapy": {
2020
"title": "Update therapy sessions",
21-
"emailLabel": "Type a user's email address",
21+
"emailLabel": "Search by user email",
2222
"therapySessionsRedeemed": "Number of therapy sessions redeemed",
2323
"therapySessionsTotal": "Total number of therapy sessions",
2424
"submit": "Update therapy sessions",
2525
"successDescription": "User therapy sessions updated!",
2626
"error": "Error updating therapy sessions: ",
2727
"reset": "Update another user"
2828
},
29-
"updatePartner": {
29+
"updatePartnerAdmin": {
3030
"title": "Update partner admin",
31-
"emailLabel": "Type at least 4 letters",
31+
"emailLabel": "Search by admin email",
3232
"activeAdminLabel": "Partner admin is currently active",
3333
"successDescription": "Partner admin updated!",
3434
"formError": "Make sure you have selected a partner user",
3535
"error": "Error updating partner admin: ",
3636
"reset": "Update another partner admin"
37+
},
38+
"updatePartner": {
39+
"title": "Update partner active status",
40+
"activeLabel": "Deactivate {partner}",
41+
"inactiveLabel": "Reactivate {partner}",
42+
"partnerNameLabel": "Select partner",
43+
"partnerActiveLabel": "Partner is active",
44+
"successDescription": "{partner} successfully updated to {status}",
45+
"inactiveDescription": "{partner} is currently inactive. Reactivating {partner} will restore the partner and all previous partner admins and users",
46+
"activeDescription": "{partner} is currently active. Deactivating {partner} will also deactivate partner admins and users, making them public users. All partner data and user data will be retained and the partner can be reactivated later.",
47+
"inactiveSuccess": "{partner} admins and partner users have also been updated and will now access Bloom as public users",
48+
"activeSuccess": "Previous {partner} admins and partner users have also been updated and will now have access to Bloom as partner users",
49+
"formError": "Make sure you have selected a partner",
50+
"error": "Error updating partner active status: ",
51+
"reset": "Reset form"
3752
}
3853
}
3954
}

0 commit comments

Comments
 (0)