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