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: {id: event.id, eventUpdate: {waitingList}},
49 });
50 addToEvent(event.id);
51 } catch (error) {
52 console.error(error);
53 addToast(t('passenger.errors.cant_add_passenger'));
54 }
55 },
56 [event]
57 );
58
59 const removePassenger = useCallback(
60 async passengerIndex => {
61 try {
62 const waitingList = event.waitingList
63 .filter((_, idx) => idx !== passengerIndex)
64 .map(({__typename, ...item}) => item);
65 await updateEvent({
66 variables: {id: event.id, eventUpdate: {waitingList}},
67 });
68 addToEvent(event.id);
69 } catch (error) {
70 console.error(error);
71 addToast(t('passenger.errors.cant_remove_passenger'));
72 }
73 },
74 [event]
75 );
76
77 const selectCar = useCallback(
78 async car => {
79 try {
80 const {id, ...passenger} = addingPassenger;
81 const carPassengers = [...(car.passengers || []), passenger].map(
82 ({__typename, ...item}) => item
83 );
84 await updateCar({
85 variables: {
86 id: car.id,
87 carUpdate: {
88 passengers: carPassengers,
89 },
90 },
91 });
92 const waitingList = event.waitingList
93 .filter(item => item.id !== id)
94 .map(({__typename, ...item}) => item);
95 await updateEvent({
96 variables: {
97 id: event.id,
98 eventUpdate: {
99 waitingList,
100 },
101 },
102 });
103 } catch (error) {
104 console.error(error);
105 addToast(t('passenger.errors.cant_select_car'));
106 }
107 setAddingPassenger(null);
108 },
109 [event, addingPassenger] // eslint-disable-line
110 );
111
112 const onPress = useCallback(
113 (passengerId: string) => {
114 const selectedPassenger = event.waitingList.find(
115 item => item.id === passengerId
116 );
117 if (isEditing) setRemovingPassenger(selectedPassenger);
118 else setAddingPassenger(selectedPassenger);
119 },
120 [isEditing, event]
121 );
122
123 return (
124 <>
125 <Paper className={classes.root}>
126 <div className={classes.header}>
127 <IconButton
128 size="small"
129 color="primary"
130 className={classes.editBtn}
131 disabled={!event.waitingList?.length}
132 onClick={toggleEditing}
133 >
134 {isEditing ? <Icon>check</Icon> : <Icon>edit</Icon>}
135 </IconButton>
136 <Typography variant="h5">{t('passenger.title')}</Typography>
137 <Typography variant="overline">
138 {t('passenger.availability.seats', {count: availability})}
139 </Typography>
140 </div>
141 <Divider />
142 <PassengersList
143 passengers={event.waitingList}
144 addPassenger={addPassenger}
145 onPress={onPress}
146 icon={isEditing ? 'close' : 'chevron_right'}
147 disabled={!isEditing && availability <= 0}
148 />
149 </Paper>
150 <RemoveDialog
151 text={
152 <Trans
153 i18nKey="passenger.actions.remove_alert"
154 values={{
155 name: removingPassenger?.name,
156 }}
157 components={{italic: <i />, bold: <strong />}}
158 />
159 }
160 open={!!removingPassenger}
161 onClose={() => setRemovingPassenger(null)}
162 onRemove={() => removePassenger(removingPassenger)}
163 />
164 <CarDialog
165 cars={cars}
166 open={!!addingPassenger}
167 onClose={() => setAddingPassenger(null)}
168 onSelect={selectCar}
169 />
170 </>
171 );
172};
173
174const sortCars = (a, b) => {
175 const dateA = new Date(a.departure).getTime();
176 const dateB = new Date(b.departure).getTime();
177 if (dateA === dateB) return new Date(a.createdAt) - new Date(b.createdAt);
178 else return dateA - dateB;
179};
180
181const useStyles = makeStyles(theme => ({
182 root: {
183 position: 'relative',
184 },
185 header: {
186 padding: theme.spacing(2),
187 },
188 editBtn: {
189 position: 'absolute',
190 top: 0,
191 right: 0,
192 margin: theme.spacing(1),
193 zIndex: theme.zIndex.speedDial,
194 },
195}));
196
197export default WaitingList;