Skip to content

Commit aeab88b

Browse files
authored
Merge pull request #98 from yrjarv/dev-multiselect-workers
Allow selection of multiple workers when registering work
2 parents 9ad19ce + 1e5454b commit aeab88b

File tree

2 files changed

+211
-61
lines changed

2 files changed

+211
-61
lines changed

app/(pages)/(main)/volunteering/logs/workLogInput.js

Lines changed: 77 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,33 @@
1-
2-
import { Button, Fab, Skeleton, Stack, TextField, Tooltip, Typography } from "@mui/material";
1+
import {
2+
Button,
3+
Fab,
4+
Skeleton,
5+
Stack,
6+
TextField,
7+
Tooltip,
8+
Typography,
9+
} from "@mui/material";
310
import { useState } from "react";
411
import { DateTimePicker, LocalizationProvider } from "@mui/x-date-pickers";
512
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFnsV3";
613
import CustomAutoComplete from "@/app/components/input/CustomAutocomplete";
14+
import CustomMultiAutoComplete from "@/app/components/input/CustomMultiAutocomplete";
715
import CustomNumberInput from "@/app/components/input/CustomNumberInput";
816
import locale from "date-fns/locale/en-GB";
917
import { CalendarToday, PunchClock } from "@mui/icons-material";
1018
import TextFieldWithX from "@/app/components/input/TextFieldWithX";
1119

12-
export default function workLogInput(
13-
session,
14-
users,
15-
workGroups,
16-
setRefresh
17-
) {
18-
const [registeredFor, setRegisteredFor] = useState(null);
20+
export default function workLogInput(session, users, workGroups, setRefresh) {
21+
const [registeredFor, setRegisteredFor] = useState([]);
1922
const [selectedGroup, setSelectedGroup] = useState(null);
2023
const [selectedDateTime, setSelectedDateTime] = useState(new Date());
2124
const [endDateTime, setEndDateTime] = useState(new Date());
2225
const [hours, setHours] = useState(0);
2326
const [description, setDescription] = useState("");
2427
const [shouldInputHours, setShouldInputHours] = useState(false);
25-
const [endInputMethodTooltip, setEndInputMethodTooltip] = useState("Change to 'End of work'");
28+
const [endInputMethodTooltip, setEndInputMethodTooltip] = useState(
29+
"Change to 'End of work'"
30+
);
2631

2732
const [registeredForError, setRegisteredForError] = useState(false);
2833
const [selectedGroupError, setSelectedGroupError] = useState(false);
@@ -48,39 +53,39 @@ export default function workLogInput(
4853

4954
if (isInvalid) return;
5055

51-
fetch("/api/v2/workLogs", {
52-
method: "POST",
53-
headers: {
54-
"content-type": "application/json"
55-
},
56-
body: JSON.stringify({
57-
loggedBy: session.data.user.id,
58-
loggedFor: registeredFor.id,
59-
workedAt: selectedDateTime.toISOString(),
60-
duration: hours,
61-
description: description,
62-
semesterId: session.data.semester.id,
63-
}),
64-
}).then(res => {
65-
setRegisteredFor(null);
66-
setSelectedGroup(null);
67-
setHours(0);
68-
setDescription("");
69-
setRefresh();
56+
for (let user of registeredFor) {
57+
fetch("/api/v2/workLogs", {
58+
method: "POST",
59+
headers: {
60+
"content-type": "application/json",
61+
},
62+
body: JSON.stringify({
63+
loggedBy: session.data.user.id,
64+
loggedFor: user.id,
65+
workedAt: selectedDateTime.toISOString(),
66+
duration: hours,
67+
description: description,
68+
semesterId: session.data.semester.id,
69+
}),
70+
});
71+
72+
fetch("/api/v2/workGroups", {
73+
method: "POST",
74+
headers: {
75+
"content-type": "application/json",
76+
},
77+
body: JSON.stringify({
78+
userId: user.id,
79+
workGroupId: selectedGroup.id,
80+
}),
7081
});
82+
}
83+
setRegisteredFor([]);
84+
setSelectedGroup(null);
85+
setHours(0);
86+
setDescription("");
87+
setRefresh("");
7188

72-
fetch("/api/v2/workGroups", {
73-
method: "POST",
74-
headers: {
75-
"content-type": "application/json"
76-
},
77-
body: JSON.stringify({
78-
userId: registeredFor.id,
79-
workGroupId: selectedGroup.id
80-
})
81-
})
82-
83-
setRegisteredFor(null);
8489
setRequestResponse("Work registered.");
8590
setTimeout(() => {
8691
setRequestResponse("");
@@ -90,22 +95,25 @@ export default function workLogInput(
9095
const switchEndInputMethod = () => {
9196
if (!shouldInputHours) {
9297
setShouldInputHours(true);
93-
document.querySelector(".endDateTime").setAttribute("style", "display: inline");
98+
document
99+
.querySelector(".endDateTime")
100+
.setAttribute("style", "display: inline");
94101
document.querySelector(".hours").setAttribute("style", "display: none");
95102
setEndInputMethodTooltip("Change to 'Hours worked'");
96-
97103
} else {
98104
setShouldInputHours(false);
99-
document.querySelector(".endDateTime").setAttribute("style", "display: none");
105+
document
106+
.querySelector(".endDateTime")
107+
.setAttribute("style", "display: none");
100108
document.querySelector(".hours").setAttribute("style", "display: inline");
101109
setEndInputMethodTooltip("Change to 'End of work'");
102110
}
103111
};
104112

105113
return (
106114
<Stack direction="column" spacing={1}>
107-
<Stack direction="column" spacing={2}>
108-
<CustomAutoComplete
115+
<Stack direction="column" spacing={2}>
116+
<CustomMultiAutoComplete
109117
label="Registered for"
110118
dataLabel="name"
111119
subDataLabel="email"
@@ -122,21 +130,24 @@ export default function workLogInput(
122130
callback={setSelectedGroup}
123131
error={selectedGroupError}
124132
/>
125-
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={locale}>
133+
<LocalizationProvider
134+
dateAdapter={AdapterDateFns}
135+
adapterLocale={locale}
136+
>
126137
<DateTimePicker
127138
label="Start of work"
128139
defaultValue={selectedDateTime}
129140
slotProps={{ textField: { fullWidth: true, size: "small" } }}
130141
ampm={false}
131142
disableOpenPicker
132143
onChange={(e) => setSelectedDateTime(e)}
133-
/>
144+
/>
134145
</LocalizationProvider>
135-
<Stack
136-
direction="row"
137-
alignItems={"stretch"}
138-
>
139-
<LocalizationProvider dateAdapter={AdapterDateFns} adapterLocale={locale}>
146+
<Stack direction="row" alignItems={"stretch"}>
147+
<LocalizationProvider
148+
dateAdapter={AdapterDateFns}
149+
adapterLocale={locale}
150+
>
140151
<DateTimePicker
141152
className="endDateTime"
142153
sx={{ display: "none" }}
@@ -148,21 +159,25 @@ export default function workLogInput(
148159
onChange={(e) => {
149160
if (e < selectedDateTime) return; // Prevent endDateTime from being before startDateTime
150161
setEndDateTime(e);
151-
setHours(Math.round(((e - selectedDateTime) / 3600000) * 10) / 10) // Update hours
162+
setHours(
163+
Math.round(((e - selectedDateTime) / 3600000) * 10) / 10
164+
); // Update hours
152165
}}
153166
/>
154167
</LocalizationProvider>
155168
<CustomNumberInput
156169
className="hours"
157170
label="Hours worked"
158171
value={hours}
159-
setValue={(value) => {setHours(value);}}
172+
setValue={(value) => {
173+
setHours(value);
174+
}}
160175
check={(data) => data.match(/[^0-9.]/) || data.match(/[.]{2,}/g)}
161176
error={hoursError}
162177
/>
163178
<Tooltip
164179
className="endInputMethodTooltip"
165-
title={ endInputMethodTooltip }
180+
title={endInputMethodTooltip}
166181
>
167182
<Fab
168183
className="endInputMethodChangeButton"
@@ -171,10 +186,12 @@ export default function workLogInput(
171186
style={{
172187
marginLeft: "10px",
173188
padding: "10px",
174-
}}
175-
onClick={() => {switchEndInputMethod();}}
189+
}}
190+
onClick={() => {
191+
switchEndInputMethod();
192+
}}
176193
>
177-
{ shouldInputHours ? <PunchClock /> : <CalendarToday /> }
194+
{shouldInputHours ? <PunchClock /> : <CalendarToday />}
178195
</Fab>
179196
</Tooltip>
180197
</Stack>
@@ -220,10 +237,9 @@ function validateWorkLogRequest(
220237
setHoursError,
221238
setDescriptionError
222239
) {
223-
224240
// Define an object to store the errors
225241
const errors = {
226-
registeredForError: registeredFor == null,
242+
registeredForError: registeredFor.length == 0,
227243
selectedGroupError: selectedGroup == null,
228244
selectedDateTimeError: false, // TODO: add semester validation
229245
hoursError: Number.isNaN(hours) || hours <= 0 || hours > 24,
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
2+
import { Autocomplete, Box, createFilterOptions, Paper, Stack, TextField, Typography } from "@mui/material";
3+
import { Component } from "react";
4+
5+
export default class CustomMultiAutoComplete extends Component {
6+
render() {
7+
const {
8+
label,
9+
value,
10+
data,
11+
dataLabel,
12+
subDataLabel,
13+
allowAdding,
14+
callback,
15+
defaultValue,
16+
error,
17+
disable,
18+
} = this.props;
19+
20+
let displayValue = [];
21+
if (defaultValue != undefined) {
22+
displayValue = defaultValue;
23+
} else if (value != undefined && value) {
24+
displayValue = value
25+
}
26+
27+
const maxSuggestions = data ? Math.min(data.length, 10) : 10;
28+
const filterOptions = createFilterOptions();
29+
30+
return (
31+
<Autocomplete
32+
multiple
33+
disabled={disable || defaultValue != undefined}
34+
size="small"
35+
fullWidth
36+
disablePortal
37+
value={displayValue}
38+
options={data}
39+
40+
// when a dropdown item is selected
41+
onChange={(e, v) => {
42+
// console.log(v);
43+
callback(v);
44+
// if (typeof v == "string") {
45+
// } else {
46+
// }
47+
}}
48+
49+
// when looking for an item
50+
filterOptions={(options, state) => {
51+
let newOptions = filterOptions(options, state).slice(
52+
0,
53+
maxSuggestions
54+
);
55+
56+
if (allowAdding && state.inputValue != "") {
57+
let item = {
58+
[dataLabel]: `Add "${state.inputValue}"`,
59+
inputValue: state.inputValue,
60+
newOption: true,
61+
};
62+
63+
newOptions.push(item);
64+
}
65+
66+
return newOptions;
67+
}}
68+
69+
// when matching value with dropdown item
70+
isOptionEqualToValue={(option, value) => {
71+
if (subDataLabel) {
72+
return option[subDataLabel] == value[subDataLabel];
73+
}
74+
return option[dataLabel] == value[dataLabel];
75+
}}
76+
77+
// setting text for dropdown item
78+
getOptionLabel={(option) => {
79+
if (option.newOption) {
80+
return option.inputValue;
81+
}
82+
return option[dataLabel];
83+
}}
84+
85+
// dropdown item element
86+
renderOption={(props, option) => {
87+
// console.log(props, option);
88+
return (
89+
<Box
90+
{...props}
91+
key={props.id}
92+
component="li"
93+
color="InfoBackground"
94+
// flexDirection="column"
95+
// alignContent="start"
96+
// alignItems="flex-start"
97+
// onClick={}
98+
// key={`option_box_${props.key}`}
99+
>
100+
<Stack direction="column" alignItems="start">
101+
<Typography
102+
key={`option_box_name_${props.id}`}
103+
// color="MenuText"
104+
>
105+
{option[dataLabel]}
106+
</Typography>
107+
{subDataLabel ? (
108+
<Typography
109+
key={`option_box_email_${props.id}`}
110+
variant="caption"
111+
color="GrayText"
112+
>
113+
{option[subDataLabel]}
114+
</Typography>
115+
) : (
116+
<></>
117+
)}
118+
</Stack>
119+
</Box>
120+
);
121+
}}
122+
renderInput={(params) => (
123+
<TextField
124+
{...params}
125+
error={error}
126+
InputLabelProps={{ shrink: true }}
127+
label={label}
128+
/>
129+
)}
130+
PaperComponent={(props) => <Paper elevation={3} {...props} />}
131+
/>
132+
);
133+
}
134+
}

0 commit comments

Comments
 (0)