Skip to content

Commit 14f10f9

Browse files
committed
Button and logic for sending automated emails
1 parent 9bdb6db commit 14f10f9

File tree

2 files changed

+181
-16
lines changed

2 files changed

+181
-16
lines changed

src/features/Dashboard/StaffDashboard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class AdminDashboardContainer extends React.Component<{}, IDashboardState> {
4848

4949
public render() {
5050
return (
51-
<DashboardView cards={this.generateCards()} title={'Staff Dashboard'} />
51+
<DashboardView cards={this.generateCards()} title={'Staff Dashboard'} />
5252
);
5353
}
5454

src/features/Search/Search.tsx

Lines changed: 180 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import fileDownload from 'js-file-download';
33
import * as React from 'react';
44
import Helmet from 'react-helmet';
55

6-
import { Account, Search, Sponsor } from '../../api';
6+
import { Account, Search, Sponsor, Emails } from '../../api';
77
import {
88
HACKATHON_NAME,
99
IAccount,
@@ -43,6 +43,9 @@ interface ISearchState {
4343
viewSaved: boolean;
4444
account?: IAccount;
4545
sponsor?: ISponsor;
46+
emailModalOpen: boolean;
47+
emailSending: boolean;
48+
emailStatus: string;
4649
}
4750

4851
class SearchContainer extends React.Component<{}, ISearchState> {
@@ -55,13 +58,54 @@ class SearchContainer extends React.Component<{}, ISearchState> {
5558
searchBar: this.getSearchBarFromQuery(),
5659
loading: false,
5760
viewSaved: false,
61+
emailModalOpen: false,
62+
emailSending: false,
63+
emailStatus: '',
5864
};
5965

6066
this.onFilterChange = this.onFilterChange.bind(this);
6167
this.triggerSearch = this.triggerSearch.bind(this);
6268
this.downloadData = this.downloadData.bind(this);
6369
this.onResetForm = this.onResetForm.bind(this);
6470
this.onSearchBarChanged = this.onSearchBarChanged.bind(this);
71+
this.openEmailModal = this.openEmailModal.bind(this);
72+
this.closeEmailModal = this.closeEmailModal.bind(this);
73+
this.handleSendEmails = this.handleSendEmails.bind(this);
74+
this.state = {
75+
...this.state,
76+
emailModalOpen: false,
77+
emailSending: false,
78+
emailStatus: '',
79+
};
80+
}
81+
openEmailModal() {
82+
this.setState({ emailModalOpen: true });
83+
}
84+
85+
closeEmailModal() {
86+
this.setState({ emailModalOpen: false, emailStatus: '' });
87+
}
88+
89+
async handleSendEmails(status: string) {
90+
this.setState({ emailSending: true, emailStatus: status });
91+
try {
92+
const resp = await Emails.sendAutomatedStatus(status);
93+
// Axios returns the response directly
94+
const { success, failed } = resp.data.data;
95+
alert(
96+
`Successfully sent ${success} ${status.toLowerCase()} emails` +
97+
(failed > 0 ? ` (${failed} failed)` : '')
98+
);
99+
} catch (err: any) {
100+
const message = err?.data?.message || err?.message || err;
101+
alert(`Failed to send ${status.toLowerCase()} emails: ${message}`);
102+
} finally {
103+
this.setState({
104+
emailSending: false,
105+
emailModalOpen: false,
106+
emailStatus: '',
107+
});
108+
}
65109
}
66110

67111
public render() {
@@ -126,6 +170,16 @@ class SearchContainer extends React.Component<{}, ISearchState> {
126170
>
127171
Export Hackers
128172
</Button>
173+
{account && account.accountType === UserType.STAFF && (
174+
<Button
175+
onClick={this.openEmailModal}
176+
variant={ButtonVariant.Secondary}
177+
isOutlined={true}
178+
style={{ marginLeft: '10px' }}
179+
>
180+
Send Emails
181+
</Button>
182+
)}
129183
</Box>
130184
</Flex>
131185
</Box>
@@ -139,6 +193,101 @@ class SearchContainer extends React.Component<{}, ISearchState> {
139193
</Box>
140194
</Flex>
141195
</Box>
196+
{/* Email Modal */}
197+
{this.state.emailModalOpen && (
198+
<div
199+
style={{
200+
position: 'fixed',
201+
top: 0,
202+
left: 0,
203+
width: '100vw',
204+
height: '100vh',
205+
background: 'rgba(0,0,0,0.3)',
206+
display: 'flex',
207+
alignItems: 'center',
208+
justifyContent: 'center',
209+
zIndex: 1000,
210+
}}
211+
onClick={this.closeEmailModal}
212+
>
213+
<div
214+
style={{
215+
background: 'white',
216+
padding: '16px 32px 32px',
217+
borderRadius: 8,
218+
minWidth: 320,
219+
boxShadow: '0 2px 16px rgba(0,0,0,0.2)',
220+
position: 'relative',
221+
}}
222+
onClick={(e) => e.stopPropagation()}
223+
>
224+
<h2>Send Decision Emails</h2>
225+
<div
226+
style={{ display: 'flex', flexDirection: 'column', gap: 12 }}
227+
>
228+
<Button
229+
disabled={this.state.emailSending}
230+
onClick={() => this.handleSendEmails('Accepted')}
231+
style={{
232+
backgroundColor: theme.colors.white,
233+
transition: 'background-color 0.2s',
234+
}}
235+
onMouseEnter={(e) => {
236+
e.currentTarget.style.backgroundColor =
237+
theme.colors.black10;
238+
}}
239+
onMouseLeave={(e) => {
240+
e.currentTarget.style.backgroundColor = theme.colors.white;
241+
}}
242+
>
243+
Send All Acceptance Emails
244+
</Button>
245+
<Button
246+
disabled={this.state.emailSending}
247+
onClick={() => this.handleSendEmails('Declined')}
248+
style={{
249+
backgroundColor: theme.colors.white,
250+
transition: 'background-color 0.2s',
251+
}}
252+
onMouseEnter={(e) => {
253+
e.currentTarget.style.backgroundColor =
254+
theme.colors.black10;
255+
}}
256+
onMouseLeave={(e) => {
257+
e.currentTarget.style.backgroundColor = theme.colors.white;
258+
}}
259+
>
260+
Send All Declined Emails
261+
</Button>
262+
</div>
263+
<div
264+
style={{
265+
display: 'flex',
266+
justifyContent: 'center',
267+
marginTop: 24,
268+
}}
269+
>
270+
<Button
271+
onClick={this.closeEmailModal}
272+
variant={ButtonVariant.Secondary}
273+
isOutlined={true}
274+
disabled={this.state.emailSending}
275+
onMouseEnter={(e) => {
276+
e.currentTarget.style.backgroundColor =
277+
theme.colors.black10;
278+
}}
279+
onMouseLeave={(e) => {
280+
e.currentTarget.style.backgroundColor = theme.colors.white;
281+
}}
282+
>
283+
Cancel
284+
</Button>
285+
</div>
286+
287+
{this.state.emailSending && <p>Sending emails...</p>}
288+
</div>
289+
</div>
290+
)}
142291
</Flex>
143292
);
144293
}
@@ -205,7 +354,10 @@ class SearchContainer extends React.Component<{}, ISearchState> {
205354
this.state.account.accountType === UserType.STAFF
206355
) {
207356
headers.push({ label: CONSTANTS.AGE_LABEL, key: 'accountId.age' });
208-
headers.push({ label: CONSTANTS.PHONE_NUMBER_LABEL, key: 'accountId.age' });
357+
headers.push({
358+
label: CONSTANTS.PHONE_NUMBER_LABEL,
359+
key: 'accountId.age',
360+
});
209361
headers.push({ label: 'Resume', key: 'application.general.URL.resume' });
210362
headers.push({ label: 'Github', key: 'application.general.URL.github' });
211363
headers.push({
@@ -221,9 +373,9 @@ class SearchContainer extends React.Component<{}, ISearchState> {
221373
key: 'application.general.URL.other',
222374
});
223375
headers.push({
224-
label: 'Number of previous hackathons',
225-
key: 'application.shortAnswer.previousHackathons'
226-
})
376+
label: 'Number of previous hackathons',
377+
key: 'application.shortAnswer.previousHackathons',
378+
});
227379
headers.push({
228380
label: CONSTANTS.SKILLS_LABEL,
229381
key: 'application.shortAnswer.skills',
@@ -273,14 +425,20 @@ class SearchContainer extends React.Component<{}, ISearchState> {
273425
label: CONSTANTS.PRONOUN_LABEL,
274426
key: 'accountId.pronoun',
275427
});
276-
headers.push({label: CONSTANTS.DIETARY_RESTRICTIONS_LABEL, key: 'accountId.dietaryRestrictions'});
277-
headers.push({label: 'Authorize MLH to send emails', key: 'application.other.sendEmail'})
428+
headers.push({
429+
label: CONSTANTS.DIETARY_RESTRICTIONS_LABEL,
430+
key: 'accountId.dietaryRestrictions',
431+
});
432+
headers.push({
433+
label: 'Authorize MLH to send emails',
434+
key: 'application.other.sendEmail',
435+
});
278436
}
279437
const tempHeaders: string[] = [];
280438
headers.forEach((header) => {
281439
tempHeaders.push(header.label);
282440
});
283-
const csvData: string[] = [tempHeaders.join(',')];
441+
const csvData: string[] = [tempHeaders.join(',')];
284442
this.filter().forEach((result) => {
285443
if (result.selected) {
286444
const row: string[] = [];
@@ -304,7 +462,11 @@ class SearchContainer extends React.Component<{}, ISearchState> {
304462
}
305463
});
306464

307-
fileDownload(csvData.join('\n'), 'hackerData.csv', 'text/csv;charset=utf-8');
465+
fileDownload(
466+
csvData.join('\n'),
467+
'hackerData.csv',
468+
'text/csv;charset=utf-8'
469+
);
308470
}
309471

310472
private async triggerSearch(): Promise<void> {
@@ -333,12 +495,15 @@ class SearchContainer extends React.Component<{}, ISearchState> {
333495
}
334496

335497
private onFilterChange(newFilters: ISearchParameter[]) {
336-
this.setState({
337-
query: newFilters,
338-
}, () => {
339-
this.updateQueryURL(newFilters, this.state.searchBar);
340-
this.triggerSearch();
341-
});
498+
this.setState(
499+
{
500+
query: newFilters,
501+
},
502+
() => {
503+
this.updateQueryURL(newFilters, this.state.searchBar);
504+
this.triggerSearch();
505+
}
506+
);
342507
}
343508

344509
private onSearchBarChanged(e: any) {

0 commit comments

Comments
 (0)