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