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