frontend/containers/WaitingList/index.tsx (view raw)
1import {useReducer, useState, useMemo, useCallback} from 'react';
2import Typography from '@material-ui/core/Typography';
3import IconButton from '@material-ui/core/IconButton';
4import Icon from '@material-ui/core/Icon';
5import Paper from '@material-ui/core/Paper';
6import Divider from '@material-ui/core/Divider';
7import {makeStyles} from '@material-ui/core/styles';
8import {Trans, useTranslation} from 'react-i18next';
9import useAddToEvents from '../../hooks/useAddToEvents';
10import PassengersList from '../PassengersList';
11import RemoveDialog from '../RemoveDialog';
12import CarDialog from './CarDialog';
13import useToastStore from '../../stores/useToastStore';
14import useEventStore from '../../stores/useEventStore';
15import {
16 useUpdateEventMutation,
17 useUpdateCarMutation,
18} from '../../generated/graphql';
19
20const WaitingList = () => {
21 const classes = useStyles();
22 const {t} = useTranslation();
23 const event = useEventStore(s => s.event);
24 const addToast = useToastStore(s => s.addToast);
25 const {addToEvent} = useAddToEvents();
26 const [isEditing, toggleEditing] = useReducer(i => !i, false);
27 const [removingPassenger, setRemovingPassenger] = useState(null);
28 const [addingPassenger, setAddingPassenger] = useState(null);
29 const cars = event?.cars?.length > 0 ? event.cars.slice().sort(sortCars) : [];
30 const [updateEvent] = useUpdateEventMutation();
31 const [updateCar] = useUpdateCarMutation();
32
33 const availability = useMemo(() => {
34 if (!cars) return;
35 return cars.reduce((count, {seats, passengers = []}) => {
36 if (!passengers) return count + seats;
37 return count + seats - passengers.length;
38 }, 0);
39 }, [cars]);
40
41 const addPassenger = useCallback(
42 async passenger => {
43 try {
44 const waitingList = [...event.waitingList, passenger].map(
45 ({__typename, ...item}) => item
46 );
47 await updateEvent({
48 variables: {uuid: event.uuid, eventUpdate: {waitingList}},
49 refetchQueries: ['eventByUUID'],
50 });
51 addToEvent(event.id);
52 } catch (error) {
53 console.error(error);
54 addToast(t('passenger.errors.cant_add_passenger'));
55 }
56 },
57 [event]
58 );
59
60 const removePassenger = useCallback(
61 async passengerIndex => {
62 try {
63 const waitingList = event.waitingList
64 .filter((_, idx) => idx !== passengerIndex)
65 .map(({__typename, ...item}) => item);
66 await updateEvent({
67 variables: {uuid: event.uuid, eventUpdate: {waitingList}},
68 });
69 addToEvent(event.id);
70 } catch (error) {
71 console.error(error);
72 addToast(t('passenger.errors.cant_remove_passenger'));
73 }
74 },
75 [event]
76 );
77
78 const selectCar = useCallback(
79 async car => {
80 try {
81 const {id, ...passenger} = addingPassenger;
82 const carPassengers = [...(car.passengers || []), passenger].map(
83 ({__typename, ...item}) => item
84 );
85 await updateCar({
86 variables: {
87 id: car.id,
88 carUpdate: {
89 passengers: carPassengers,
90 },
91 },
92 });
93 const waitingList = event.waitingList
94 .filter(item => item.id !== id)
95 .map(({__typename, ...item}) => item);
96 await updateEvent({
97 variables: {
98 uuid: event.uuid,
99 eventUpdate: {
100 waitingList,
101 },
102 },
103 });
104 } catch (error) {
105 console.error(error);
106 addToast(t('passenger.errors.cant_select_car'));
107 }
108 setAddingPassenger(null);
109 },
110 [event, addingPassenger] // eslint-disable-line
111 );
112
113 const onPress = useCallback(
114 (passengerId: string) => {
115 const selectedPassenger = event.waitingList.find(
116 item => item.id === passengerId
117 );
118 if (isEditing) setRemovingPassenger(selectedPassenger);
119 else setAddingPassenger(selectedPassenger);
120 },
121 [isEditing, event]
122 );
123
124 return (
125 <>
126 <Paper className={classes.root}>
127 <div className={classes.header}>
128 <IconButton
129 size="small"
130 color="primary"
131 className={classes.editBtn}
132 disabled={!event.waitingList?.length}
133 onClick={toggleEditing}
134 >
135 {isEditing ? <Icon>check</Icon> : <Icon>edit</Icon>}
136 </IconButton>
137 <Typography variant="h5">{t('passenger.title')}</Typography>
138 <Typography variant="overline">
139 {t('passenger.availability.seats', {count: availability})}
140 </Typography>
141 </div>
142 <Divider />
143 <PassengersList
144 passengers={event.waitingList}
145 addPassenger={addPassenger}
146 onPress={onPress}
147 icon={isEditing ? 'close' : 'chevron_right'}
148 disabled={!isEditing && availability <= 0}
149 />
150 </Paper>
151 <RemoveDialog
152 text={
153 <Trans
154 i18nKey="passenger.actions.remove_alert"
155 values={{
156 name: removingPassenger?.name,
157 }}
158 components={{italic: <i />, bold: <strong />}}
159 />
160 }
161 open={!!removingPassenger}
162 onClose={() => setRemovingPassenger(null)}
163 onRemove={() => removePassenger(removingPassenger)}
164 />
165 <CarDialog
166 cars={cars}
167 open={!!addingPassenger}
168 onClose={() => setAddingPassenger(null)}
169 onSelect={selectCar}
170 />
171 </>
172 );
173};
174
175const sortCars = (a, b) => {
176 const dateA = new Date(a.departure).getTime();
177 const dateB = new Date(b.departure).getTime();
178 if (dateA === dateB) return new Date(a.createdAt) - new Date(b.createdAt);
179 else return dateA - dateB;
180};
181
182const useStyles = makeStyles(theme => ({
183 root: {
184 position: 'relative',
185 },
186 header: {
187 padding: theme.spacing(2),
188 },
189 editBtn: {
190 position: 'absolute',
191 top: 0,
192 right: 0,
193 margin: theme.spacing(1),
194 zIndex: theme.zIndex.speedDial,
195 },
196}));
197
198export default WaitingList;