import { Modules, Tableplans, OpeningHours, ClosedDates } from 'meteor/mrwinston:data';
import { useTracker } from 'meteor/react-meteor-data';
import i18n from 'meteor/universe:i18n';
import { Meteor } from 'meteor/meteor';
import { makeStyles, FormControl, InputLabel, MenuItem, Select, Typography, TextField, Paper } from '@material-ui/core';
import { DatePicker, TimePicker } from '@material-ui/pickers';
import React, { useState, useMemo, useEffect } from 'react';
import { useHistory, useParams } from 'react-router';
import { isValidEmail, isValidPhoneNumber } from '/both/lib/form_utils';
import GlobalStore from '../../../../stores/GlobalStore';
import Loading from '../../../shared/Loading';
import Material from '/client/lib/material';
import DurationTimeSelector from './DurationTimeSelector';
import ReservationsStepper, { Step } from './Stepper';
const useStyles = makeStyles(({ spacing, breakpoints }) => ({
container: {
padding: spacing(2),
[breakpoints.down('sm')]: {
gridTemplateRows: '1fr auto',
display: 'grid',
minHeight: 350,
height: '80%',
},
},
stepTitle: {
marginBottom: spacing(2),
},
form: {
gridTemplateColumns: '1fr 1fr',
gap: `${spacing(2)}px`,
display: 'grid',
[breakpoints.down('sm')]: {
flexDirection: 'column',
display: 'flex',
},
},
fullWidth: {
gridColumn: 'span 2',
},
button: {
marginTop: spacing(2),
marginLeft: 'auto',
display: 'block',
},
closed: {
marginTop: spacing(2),
},
notVisible: {
display: 'none',
},
}));
export default function ReservationForm() {
const currentDate = useMemo(() => {
const date = new Date();
date.setMinutes(Math.ceil(date.getMinutes() / 15) * 15 + 30);
return date;
}, []);
const { tenantId } = useParams();
const history = useHistory();
const classes = useStyles();
const [ready, moduleSettings, tableplans, userProfile] = useTracker(() => {
const subs = [
Meteor.subscribe('modules', { tenantId, type: 'reservations' }),
Meteor.subscribe('tableplans'),
Meteor.subscribe(OpeningHours._name, { tenantId, type: OpeningHours.forTypes.DEFAULT }),
Meteor.subscribe(ClosedDates._name, { tenantId, type: OpeningHours.forTypes.DEFAULT }),
];
return [
subs.every((s) => s.ready()),
{
maxStartDate: 30,
minStartDate: 10,
maxDuration: 5 * 60,
minDuration: 120,
maxPeople: 5,
minPeople: 1,
durationGranularity: 30,
...Modules.reservations.get().settings,
},
Tableplans.find().fetch(),
Meteor.user()?.profile ?? {},
];
}, []);
const [tableplan, setTableplan] = useState('');
const [date, setDate] = useState(currentDate);
const [duration, setDuration] = useState('');
const [comment, setComment] = useState('');
const [people, setPeople] = useState(1);
const [phone, setPhone] = useState('');
const [email, setEmail] = useState('');
const [name, setName] = useState('');
const [step, setStep] = useState(0);
const [loading, setLoading] = useState(false);
const setStateFromEvent = (setState) => (event) => {
setState(event.target.value);
};
function createReservation() {
const formData = {
name,
email,
phone,
date,
duration,
comment,
tableplan,
};
const reservation = {
...formData,
people: parseInt(people, 10),
};
setLoading(true);
Meteor.call('createReservation', reservation, i18n.getLocale(), (err) => {
setLoading(false);
if (err) {
console.error(err);
if (err.reason.includes('Overlapping')) {
Material.toast(i18n.__(`reservation.create.${err.reason}`), {
autoHideDuration: 6000,
});
} else {
Material.toast(err.toString());
}
setStep(0);
} else {
GlobalStore.set('lastReservation', reservation);
history.push(`/${tenantId}#reservation-created`);
}
});
}
const shouldDisableDate = (momentDate, { onlyCheckDate = true } = {}) => {
const jsDate = momentDate instanceof Date ? momentDate : momentDate.toDate();
if (ClosedDates.getEntriesOn(OpeningHours.forTypes.DEFAULT, jsDate, onlyCheckDate, { direct: true }).length > 0) {
return true;
}
return OpeningHours.isClosedOn(OpeningHours.forTypes.DEFAULT, jsDate, { direct: true, onlyCheckDate });
};
const durationEntries = useMemo(() => {
const elements = [];
if (moduleSettings.allDuration > 0) {
setDuration(moduleSettings.allDuration);
return elements;
}
console.log(moduleSettings);
for (let i = moduleSettings.minDuration; i <= moduleSettings.maxDuration; i += moduleSettings.durationGranularity) {
elements.push(
<MenuItem key={i} value={i}>
{(i / 60).toLocaleString(i18n.getLocale(), { minimumFractionDigits: 1 })} {i18n.__('reservation.create.hours')}
</MenuItem>
);
}
return elements;
}, [moduleSettings]);
// Fix react-target div styling on this page
useEffect(() => {
const e = document.getElementById('react-target');
if (e) {
e.style.height = '100vh';
}
return () => {
if (e) {
e.style.height = '';
}
};
}, []);
useEffect(() => {
if (tableplans[0]?._id) {
setTableplan(tableplans[0]._id);
}
}, [tableplans]);
useEffect(() => {
setDuration(moduleSettings.minDuration);
setPhone(userProfile.telephone ?? '');
setPeople(moduleSettings.minPeople);
setEmail(userProfile.email ?? '');
setName(userProfile.name ?? '');
setComment('');
}, [ready]);
const maxDate = useMemo(() => new Date(currentDate.getTime() + moduleSettings.maxStartDate * 1000 * 60 * 60 * 24), [
currentDate,
moduleSettings.maxStartDate,
]);
const minDate = useMemo(() => new Date(currentDate.getTime() + moduleSettings.minStartDate * 1000 * 60), [
currentDate,
moduleSettings.minStartDate,
]);
const isClosed = useMemo(() => (date ? shouldDisableDate(date, { onlyCheckDate: false }) : false), [date]);
const isPhoneInvalid = useMemo(() => !isValidPhoneNumber(phone, { ignoreEmpty: true }), [phone]);
const isEmailInvalid = useMemo(() => !!email && !isValidEmail(email), [email]);
const steps = useMemo(
() => [
{
isOk: !!date && !!duration,
title: 'reservation.create.dateDetails',
content: (
<>
<DatePicker
value={date}
inputVariant="outlined"
required
onChange={(event) => setDate(event.toDate())}
label={i18n.__('reservation.create.date')}
shouldDisableDate={shouldDisableDate}
showTodayButton
maxDate={maxDate}
minDate={minDate}
disablePast
autoOk
/>
<TimePicker
value={date}
inputVariant="outlined"
required
onChange={(event) => setDate(event.toDate())}
ampm={false}
minutesStep={15}
disablePast
label={i18n.__('reservation.create.time')}
/>
{moduleSettings.minDuration % 30 === 0 && moduleSettings.durationGranularity % 30 === 0 && durationEntries.length < 15 ? (
<FormControl
variant="outlined"
color="secondary"
className={moduleSettings.allDuration > 0 ? classes.notVisible : classes.fullWidth}
required
>
<InputLabel shrink>{i18n.__('reservation.create.end.label')}</InputLabel>
<Select label={i18n.__('reservation.create.end.label')} value={duration} onChange={setStateFromEvent(setDuration)}>
{durationEntries}
</Select>
</FormControl>
) : (
<DurationTimeSelector
value={duration}
onChange={setDuration}
className={classes.fullWidth}
moduleSettings={moduleSettings}
label={i18n.__('reservation.create.end.label')}
/>
)}
</>
),
},
{
isOk: !!name && !!email && !isEmailInvalid && !!phone && !isPhoneInvalid && !!people,
title: 'reservation.create.contactDetails',
content: (
<>
<TextField
id="name"
variant="outlined"
color="secondary"
label={i18n.__('reservation.create.name')}
required
name="name"
value={name}
onChange={setStateFromEvent(setName)}
/>
<TextField
id="email"
variant="outlined"
color="secondary"
label={i18n.__('reservation.create.email')}
name="email"
required
error={isEmailInvalid}
helperText={isEmailInvalid && i18n.__('reservation.create.invalidEmail')}
type="email"
value={email}
onChange={setStateFromEvent(setEmail)}
/>
<TextField
id="phone"
variant="outlined"
color="secondary"
type="tel"
label={i18n.__('reservation.create.phone')}
name="phone"
error={isPhoneInvalid}
helperText={isPhoneInvalid && i18n.__('reservation.create.invalidPhoneNumber')}
required
value={phone}
onChange={setStateFromEvent(setPhone)}
/>
<TextField
type="number"
variant="outlined"
color="secondary"
id="people"
required
label={i18n.__('reservation.create.people')}
name="people"
value={people}
onChange={setStateFromEvent(setPeople)}
autoComplete="off"
inputProps={{
min: moduleSettings.minPeople,
max: moduleSettings.maxPeople,
}}
/>
</>
),
},
{
isOk: !!tableplan,
title: 'reservation.create.miscellaneousDetails',
content: (
<>
<FormControl variant="outlined" color="secondary" required>
<InputLabel shrink>{i18n.__('reservation.create.tableplan')}</InputLabel>
<Select label={i18n.__('reservation.create.tableplan')} value={tableplan} onChange={setStateFromEvent(setTableplan)}>
{tableplans.map((tp) => (
<MenuItem key={tp._id} value={tp._id}>
{tp.name}
</MenuItem>
))}
</Select>
</FormControl>
<TextField
id="comment"
variant="outlined"
color="secondary"
label={i18n.__('reservation.create.comment')}
name="comment"
value={comment}
onChange={setStateFromEvent(setComment)}
autoComplete="off"
/>
</>
),
},
],
[
classes.fullWidth,
comment,
date,
duration,
durationEntries,
email,
isEmailInvalid,
isPhoneInvalid,
maxDate,
minDate,
moduleSettings,
name,
people,
phone,
tableplan,
tableplans,
]
);
if (!ready) {
return <Loading />;
}
return (
<>
<Typography component="h2" variant="h4" gutterBottom>
{i18n.__('reservation.create.open')}
</Typography>
<Paper className={classes.container}>
<ReservationsStepper
step={step}
loading={loading}
onChange={setStep}
steps={steps.length}
onSubmit={createReservation}
nextAvailable={!loading && steps[step]?.isOk && !isClosed}
>
<form>
{steps.map(({ title, content }, index) => (
// eslint-disable-next-line react/no-array-index-key
<Step key={`${title}-${index}`} step={step} index={index}>
<Typography className={classes.stepTitle} component="h3" variant="h6">
{i18n.__(title)}
</Typography>
<div className={classes.form}>{content}</div>
</Step>
))}
{isClosed && <Typography className={classes.closed}>{i18n.__('reservation.create.closed')}</Typography>}
</form>
</ReservationsStepper>
</Paper>
</>
);
}