Skip to content

Commit b80244f

Browse files
committed
Added user invite emails
1 parent 0bba862 commit b80244f

File tree

4 files changed

+115
-11
lines changed

4 files changed

+115
-11
lines changed
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import React, { useState } from "react";
2+
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography, CircularProgress, Alert } from "@mui/material";
3+
import { ApiHelper } from "@churchapps/apphelper";
4+
5+
interface Props {
6+
open: boolean;
7+
personName: string;
8+
personEmail: string;
9+
contextName: string;
10+
onClose: () => void;
11+
}
12+
13+
export const SendInviteDialog: React.FC<Props> = (props) => {
14+
const [sending, setSending] = useState(false);
15+
const [sent, setSent] = useState(false);
16+
const [error, setError] = useState("");
17+
18+
const handleSend = async () => {
19+
setSending(true);
20+
setError("");
21+
try {
22+
await ApiHelper.post("/users/sendInviteEmail", {
23+
email: props.personEmail,
24+
personName: props.personName,
25+
contextName: props.contextName
26+
}, "MembershipApi");
27+
setSent(true);
28+
setTimeout(() => props.onClose(), 1500);
29+
} catch (err: any) {
30+
setError(err?.message || "Failed to send invite email.");
31+
} finally {
32+
setSending(false);
33+
}
34+
};
35+
36+
const handleClose = () => {
37+
if (!sending) props.onClose();
38+
};
39+
40+
return (
41+
<Dialog open={props.open} onClose={handleClose} maxWidth="xs" fullWidth>
42+
<DialogTitle>Send Invite Email?</DialogTitle>
43+
<DialogContent>
44+
{sent ? (
45+
<Alert severity="success">Invite email sent to {props.personEmail}.</Alert>
46+
) : (
47+
<>
48+
{error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
49+
<Typography>
50+
<strong>{props.personName}</strong> has been added to <strong>{props.contextName}</strong>.
51+
Would you like to send them an email invitation at <strong>{props.personEmail}</strong>?
52+
</Typography>
53+
</>
54+
)}
55+
</DialogContent>
56+
{!sent && (
57+
<DialogActions>
58+
<Button onClick={handleClose} disabled={sending}>No Thanks</Button>
59+
<Button variant="contained" onClick={handleSend} disabled={sending} startIcon={sending ? <CircularProgress size={16} /> : null}>
60+
Send Invite
61+
</Button>
62+
</DialogActions>
63+
)}
64+
</Dialog>
65+
);
66+
};

src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export { DocChatWidget } from "./docChat";
1818
// Person Management Components (moved from AppHelper)
1919
export { PersonAdd } from "./PersonAdd";
2020
export { CreatePerson } from "./CreatePerson";
21+
export { SendInviteDialog } from "./SendInviteDialog";
2122

2223
// UI Components
2324
export * from "./ui";

src/groups/components/GroupMembers.tsx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { Link } from "react-router-dom";
2020
import { Button, FormControl, InputLabel, MenuItem, Select, Table, TableBody, TableCell, TableHead, TableRow, TextField } from "@mui/material";
2121
import { Send as SendIcon } from "@mui/icons-material";
2222
import { SmallButton } from "@churchapps/apphelper";
23+
import { SendInviteDialog } from "../../components";
2324

2425
interface Props {
2526
group: GroupInterface;
@@ -33,6 +34,7 @@ export const GroupMembers: React.FC<Props> = memo((props) => {
3334
const [selectedTemplate, setSelectedTemplate] = useState<string>("");
3435
const [message, setMessage] = useState<string>("");
3536
const [count, setCount] = useState<number>(0);
37+
const [showInviteDialog, setShowInviteDialog] = useState<boolean>(false);
3638

3739
const groupMembers = useQuery<GroupMemberInterface[]>({
3840
queryKey: [`/groupmembers?groupId=${props.group?.id}`, "MembershipApi"],
@@ -68,13 +70,16 @@ export const GroupMembers: React.FC<Props> = memo((props) => {
6870
[groupMembers.data]
6971
);
7072

71-
const handleAdd = useCallback(() => {
73+
const handleAdd = useCallback(async () => {
7274
if (getMemberByPersonId(props.addedPerson.id) === null) {
7375
const gm = { groupId: props.group.id, personId: props.addedPerson.id, person: props.addedPerson } as GroupMemberInterface;
74-
ApiHelper.post("/groupmembers", [gm], "MembershipApi").then(() => {
75-
groupMembers.refetch();
76-
});
77-
props.addedCallback();
76+
await ApiHelper.post("/groupmembers", [gm], "MembershipApi");
77+
groupMembers.refetch();
78+
if (props.addedPerson.contactInfo?.email) {
79+
setShowInviteDialog(true);
80+
} else {
81+
props.addedCallback();
82+
}
7883
}
7984
}, [props, getMemberByPersonId, groupMembers]);
8085

@@ -287,6 +292,15 @@ export const GroupMembers: React.FC<Props> = memo((props) => {
287292
</div>
288293
)}
289294
{getTable()}
295+
{showInviteDialog && props.addedPerson && (
296+
<SendInviteDialog
297+
open={showInviteDialog}
298+
personName={props.addedPerson.name?.display || `${props.addedPerson.name?.first || ""} ${props.addedPerson.name?.last || ""}`.trim()}
299+
personEmail={props.addedPerson.contactInfo.email}
300+
contextName={props.group.name}
301+
onClose={() => { setShowInviteDialog(false); props.addedCallback(); }}
302+
/>
303+
)}
290304
</DisplayBox>
291305
);
292306
});

src/settings/components/UserAdd.tsx

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from "@churchapps/apphelper";
1616
import { AssociatePerson } from "./";
1717
import { TextField } from "@mui/material";
18+
import { SendInviteDialog } from "../../components";
1819

1920
interface Props {
2021
role: RoleInterface;
@@ -37,6 +38,19 @@ export const UserAdd = (props: Props) => {
3738
const [showNameFields, setShowNameFields] = useState<boolean>(false);
3839
const [editMode, setEditMode] = useState<boolean>(false);
3940
const [hasSearched, setHasSearched] = useState<boolean>(false);
41+
const [showInviteDialog, setShowInviteDialog] = useState<boolean>(false);
42+
const [inviteEmail, setInviteEmail] = useState<string>("");
43+
const [invitePersonName, setInvitePersonName] = useState<string>("");
44+
45+
const showInviteOrFinish = (emailAddr: string, personName: string, isNewUser: boolean) => {
46+
if (isNewUser || !emailAddr) {
47+
props.updatedFunction();
48+
} else {
49+
setInviteEmail(emailAddr);
50+
setInvitePersonName(personName);
51+
setShowInviteDialog(true);
52+
}
53+
};
4054

4155
const saveExistingUser = async () => {
4256
if (validate()) {
@@ -48,7 +62,7 @@ export const UserAdd = (props: Props) => {
4862
person.name.first = firstName;
4963
person.name.last = lastName;
5064
await ApiHelper.post("/people", [person], "MembershipApi");
51-
props.updatedFunction();
65+
showInviteOrFinish(email, firstName, false);
5266
} catch {
5367
setErrors([Locale.label("settings.userAdd.errAnother")]);
5468
}
@@ -60,7 +74,7 @@ export const UserAdd = (props: Props) => {
6074
const user = await createUserAndToGroup(firstName, lastName, email);
6175
const person = await createPerson(user.id);
6276
await linkUserAndPerson(user.id, person.id);
63-
props.updatedFunction();
77+
showInviteOrFinish(email, firstName, user.isNewUser === true);
6478
}
6579
};
6680

@@ -85,7 +99,7 @@ export const UserAdd = (props: Props) => {
8599
setErrors([Locale.label("settings.userAdd.errDiff")]);
86100
}
87101

88-
props.updatedFunction();
102+
showInviteOrFinish(userEmail, first, user.isNewUser === true);
89103
};
90104

91105
const handleSave = async () => {
@@ -107,9 +121,9 @@ export const UserAdd = (props: Props) => {
107121
await ApiHelper.post(`/userchurch?userId=${userId}`, { personId }, "MembershipApi");
108122
};
109123

110-
const createUserAndToGroup = async (firstName: string, lastName: string, userEmail: string) => {
124+
const createUserAndToGroup = async (firstName: string, lastName: string, userEmail: string): Promise<UserInterface & { isNewUser?: boolean }> => {
111125
const userPayload: LoadCreateUserRequestInterface = { firstName, lastName, userEmail };
112-
const user: UserInterface = await ApiHelper.post("/users/loadOrCreate", userPayload, "MembershipApi");
126+
const user: UserInterface & { isNewUser?: boolean } = await ApiHelper.post("/users/loadOrCreate", userPayload, "MembershipApi");
113127
const roleMember: RoleMemberInterface = { userId: user.id, roleId: props.role.id, churchId: UserHelper.currentUserChurch.church.id };
114128
await ApiHelper.post("/rolemembers/", [roleMember], "MembershipApi");
115129

@@ -157,7 +171,7 @@ export const UserAdd = (props: Props) => {
157171
setErrors([Locale.label("settings.userAdd.errDiff")]);
158172
return;
159173
}
160-
props.updatedFunction();
174+
showInviteOrFinish(userEmail, first, user.isNewUser === true);
161175
}
162176
};
163177

@@ -225,6 +239,15 @@ export const UserAdd = (props: Props) => {
225239
{nameField}
226240
{emailField}
227241
{message}
242+
{showInviteDialog && (
243+
<SendInviteDialog
244+
open={showInviteDialog}
245+
personName={invitePersonName}
246+
personEmail={inviteEmail}
247+
contextName={props.role.name}
248+
onClose={() => { setShowInviteDialog(false); props.updatedFunction(); }}
249+
/>
250+
)}
228251
</InputBox>
229252
);
230253
};

0 commit comments

Comments
 (0)