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 useToastStore from '../../stores/useToastStore';
11import useEventStore from '../../stores/useEventStore';
12import useAddToEvents from '../../hooks/useAddToEvents';
13import PassengersList from '../PassengersList';
14import RemoveDialog from '../RemoveDialog';
15import AddPassengerButtons from '../AddPassengerButtons';
16import TravelDialog from './TravelDialog';
17import ClearButton from '../ClearButton';
18import AssignButton from './AssignButton';
19import usePassengersActions from '../../hooks/usePassengersActions';
20
21interface Props {
22 getToggleNewPassengerDialogFunction: (addSelf: boolean) => () => void;
23 canAddSelf: boolean;
24 slideToTravel: (travelId: string) => void;
25}
26
27const WaitingList = ({
28 getToggleNewPassengerDialogFunction,
29 canAddSelf,
30 slideToTravel,
31}: Props) => {
32 const classes = useStyles();
33 const {t} = useTranslation();
34 const event = useEventStore(s => s.event);
35 const addToast = useToastStore(s => s.addToast);
36 const {addToEvent} = useAddToEvents();
37 const [isEditing, toggleEditing] = useReducer(i => !i, false);
38 const [removingPassenger, setRemovingPassenger] = useState(null);
39 const [addingPassenger, setAddingPassenger] = useState(null);
40 const travels =
41 event?.travels?.length > 0 ? event.travels.slice().sort(sortTravels) : [];
42 const {addPassengerToTravel, removePassengerFromWaitingList} =
43 usePassengersActions();
44
45 const availability = useMemo(() => {
46 if (!travels) return;
47 return travels.reduce((count, {vehicle, passengers = []}) => {
48 if (!vehicle) return 0;
49 else if (!passengers) return count + vehicle.seats;
50 return count + vehicle.seats - passengers.length;
51 }, 0);
52 }, [travels]);
53
54 const removePassengerFromWaitingListFallBack = useCallback(
55 removePassengerFromWaitingList,
56 [event]
57 );
58
59 const selectTravel = useCallback(
60 async travel => {
61 const {id, ...passenger} = addingPassenger;
62
63 try {
64 await addPassengerToTravel({
65 travel,
66 passenger,
67 });
68 await removePassengerFromWaitingListFallBack({
69 passenger: addingPassenger,
70 event: {
71 ...event,
72 waitingList: event.waitingList.filter(
73 item => item.id !== addingPassenger.id
74 ),
75 },
76 });
77 setAddingPassenger(null);
78 slideToTravel(travel.id);
79 addToast(
80 t('passenger.success.added_to_car', {
81 name: addingPassenger.name,
82 })
83 );
84 } catch (error) {
85 console.error(error);
86 addToast(t('passenger.errors.cant_select_travel'));
87 }
88 },
89 [event, addingPassenger] // eslint-disable-line
90 );
91
92 const onPress = useCallback(
93 (passengerId: string) => {
94 const selectedPassenger = event.waitingList.find(
95 item => item.id === passengerId
96 );
97 if (isEditing) setRemovingPassenger(selectedPassenger);
98 else setAddingPassenger(selectedPassenger);
99 },
100 [isEditing, event]
101 );
102
103 const onRemove = async () => {
104 try {
105 await removePassengerFromWaitingListFallBack({
106 passenger: removingPassenger,
107 event,
108 });
109 addToEvent(event.id);
110 } catch (error) {
111 console.error(error);
112 addToast(t('passenger.errors.cant_remove_passenger'));
113 }
114 };
115
116 const ListButton = isEditing
117 ? ({onClick}: {onClick: () => void}) => (
118 <ClearButton icon="close" onClick={onClick} tabIndex={-1} />
119 )
120 : ({onClick, disabled}: {onClick: () => void; disabled: boolean}) => (
121 <AssignButton onClick={onClick} tabIndex={-1} disabled={disabled} />
122 );
123
124 return (
125 <>
126 <Paper className={classes.root}>
127 <div className={clsx(classes.header, 'tour_waiting_list')}>
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 <AddPassengerButtons
144 getOnClickFunction={getToggleNewPassengerDialogFunction}
145 canAddSelf={canAddSelf}
146 variant="waitingList"
147 />
148 <Divider />
149 <PassengersList
150 passengers={event.waitingList}
151 onPress={onPress}
152 Button={ListButton}
153 />
154 </Paper>
155 <RemoveDialog
156 text={
157 <Trans
158 i18nKey="passenger.actions.remove_alert"
159 values={{
160 name: removingPassenger?.name,
161 }}
162 components={{italic: <i />, bold: <strong />}}
163 />
164 }
165 open={!!removingPassenger}
166 onClose={() => setRemovingPassenger(null)}
167 onRemove={onRemove}
168 />
169 <TravelDialog
170 eventName={event.name}
171 travels={travels}
172 passenger={addingPassenger}
173 open={!!addingPassenger}
174 onClose={() => setAddingPassenger(null)}
175 onSelect={selectTravel}
176 />
177 </>
178 );
179};
180
181const sortTravels = (a, b) => {
182 const dateA = new Date(a.departure).getTime();
183 const dateB = new Date(b.departure).getTime();
184 if (dateA === dateB)
185 return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
186 else return dateA - dateB;
187};
188
189const useStyles = makeStyles(theme => ({
190 root: {
191 position: 'relative',
192 },
193 header: {
194 padding: theme.spacing(2),
195 },
196 editBtn: {
197 position: 'absolute',
198 top: 0,
199 right: 0,
200 margin: theme.spacing(1),
201 zIndex: theme.zIndex.speedDial,
202 },
203}));
204
205export default WaitingList;