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