Add an accommodations reservations module (#188)
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Add a new module to enable accommodations reservation  Reviewed-on: #188
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
} from "@mui/material";
|
||||
import React from "react";
|
||||
import { ServerApi } from "../../api/ServerApi";
|
||||
import { NewCalendarURL } from "../../api/accommodations/AccommodationsCalendarURLApi";
|
||||
import { checkConstraint } from "../../utils/form_utils";
|
||||
import { useAccommodations } from "../../widgets/accommodations/BaseAccommodationsRoute";
|
||||
import { PropEdit } from "../../widgets/forms/PropEdit";
|
||||
import { PropSelect } from "../../widgets/forms/PropSelect";
|
||||
|
||||
export function CreateAccommodationCalendarURLDialog(p: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onSubmitted: (c: NewCalendarURL) => void;
|
||||
}): React.ReactElement {
|
||||
const [calendar, setCalendar] = React.useState<NewCalendarURL>({ name: "" });
|
||||
|
||||
const accommodations = useAccommodations();
|
||||
|
||||
const nameErr = checkConstraint(
|
||||
ServerApi.Config.constraints.accommodation_calendar_name_len,
|
||||
calendar?.name
|
||||
);
|
||||
|
||||
const clearForm = () => {
|
||||
setCalendar({ name: "" });
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
clearForm();
|
||||
p.onClose();
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
clearForm();
|
||||
p.onSubmitted(calendar!);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={p.open} onClose={cancel}>
|
||||
<DialogTitle>Création d'un calendrier</DialogTitle>
|
||||
<DialogContent style={{ display: "flex", flexDirection: "column" }}>
|
||||
<PropEdit
|
||||
editable
|
||||
label="Nom"
|
||||
value={calendar?.name}
|
||||
onValueChange={(s) =>
|
||||
setCalendar((a) => {
|
||||
return {
|
||||
...a!,
|
||||
name: s!,
|
||||
};
|
||||
})
|
||||
}
|
||||
size={ServerApi.Config.constraints.accommodation_calendar_name_len}
|
||||
helperText={nameErr}
|
||||
/>
|
||||
|
||||
<PropSelect
|
||||
editing
|
||||
label="Logement ciblé"
|
||||
onValueChange={(v) => {
|
||||
setCalendar((a) => {
|
||||
return {
|
||||
...a!,
|
||||
accommodation_id: v !== "A" && v ? Number(v) : undefined,
|
||||
};
|
||||
});
|
||||
}}
|
||||
options={[
|
||||
{ label: "Tous les logements", value: "A" },
|
||||
...accommodations.accommodations.fullList.map((a) => {
|
||||
return { label: a.name, value: a.id.toString() };
|
||||
}),
|
||||
]}
|
||||
value={calendar.accommodation_id?.toString() ?? "A"}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={cancel}>Annuler</Button>
|
||||
<Button onClick={submit} disabled={!!nameErr}>
|
||||
Créer
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
@@ -0,0 +1,72 @@
|
||||
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
FormControl,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
OutlinedInput,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import QRCode from "react-qr-code";
|
||||
import {
|
||||
AccommodationCalendarURL,
|
||||
AccommodationsCalendarURLApi,
|
||||
} from "../../api/accommodations/AccommodationsCalendarURLApi";
|
||||
import { CopyToClipboard } from "../../widgets/CopyToClipboard";
|
||||
|
||||
export function InstallCalendarDialog(p: {
|
||||
cal?: AccommodationCalendarURL;
|
||||
onClose: () => void;
|
||||
}): React.ReactElement {
|
||||
if (!p.cal) return <></>;
|
||||
|
||||
return (
|
||||
<Dialog open={true} onClose={p.onClose}>
|
||||
<DialogTitle>Installation du calendrier</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
<Typography>
|
||||
Afin d'installer le calendrier <i>{p.cal.name}</i> sur votre
|
||||
appareil, veuillez utiliser l'URL suivante :
|
||||
</Typography>
|
||||
<br />
|
||||
<FormControl fullWidth variant="outlined">
|
||||
<OutlinedInput
|
||||
value={AccommodationsCalendarURLApi.CalendarURL(p.cal!)}
|
||||
endAdornment={
|
||||
<InputAdornment position="end">
|
||||
<CopyToClipboard
|
||||
content={AccommodationsCalendarURLApi.CalendarURL(p.cal!)}
|
||||
>
|
||||
<IconButton>
|
||||
<ContentCopyIcon />
|
||||
</IconButton>
|
||||
</CopyToClipboard>
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
margin: "20px auto",
|
||||
padding: "20px",
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
>
|
||||
<QRCode
|
||||
value={AccommodationsCalendarURLApi.CalendarURL(p.cal!)}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={p.onClose}>Fermer</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
@@ -0,0 +1,155 @@
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import React from "react";
|
||||
import { ServerApi } from "../../api/ServerApi";
|
||||
import { UpdateAccommodation } from "../../api/accommodations/AccommodationListApi";
|
||||
import { checkConstraint } from "../../utils/form_utils";
|
||||
import { PropCheckbox } from "../../widgets/forms/PropCheckbox";
|
||||
import { PropEdit } from "../../widgets/forms/PropEdit";
|
||||
import { PropColorPicker } from "../../widgets/forms/PropColorPicker";
|
||||
|
||||
export function UpdateAccommodationDialog(p: {
|
||||
open: boolean;
|
||||
create: boolean;
|
||||
onClose: () => void;
|
||||
onSubmitted: (c: UpdateAccommodation) => void;
|
||||
accommodation: UpdateAccommodation | undefined;
|
||||
}): React.ReactElement {
|
||||
const [accommodation, setAccommodation] = React.useState<
|
||||
UpdateAccommodation | undefined
|
||||
>();
|
||||
|
||||
const nameErr = checkConstraint(
|
||||
ServerApi.Config.constraints.accommodation_name_len,
|
||||
accommodation?.name
|
||||
);
|
||||
const descriptionErr = checkConstraint(
|
||||
ServerApi.Config.constraints.accommodation_description_len,
|
||||
accommodation?.description
|
||||
);
|
||||
|
||||
const clearForm = () => {
|
||||
setAccommodation(undefined);
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
clearForm();
|
||||
p.onClose();
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
clearForm();
|
||||
p.onSubmitted(accommodation!);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!accommodation) setAccommodation(p.accommodation);
|
||||
}, [p.open, p.accommodation]);
|
||||
|
||||
return (
|
||||
<Dialog open={p.open} onClose={cancel}>
|
||||
<DialogTitle>
|
||||
{p.create ? "Création" : "Mise à jour"} d'un logement
|
||||
</DialogTitle>
|
||||
<DialogContent style={{ display: "flex", flexDirection: "column" }}>
|
||||
<PropEdit
|
||||
editable
|
||||
label="Nom"
|
||||
value={accommodation?.name}
|
||||
onValueChange={(s) =>
|
||||
setAccommodation((a) => {
|
||||
return {
|
||||
...a!,
|
||||
name: s!,
|
||||
};
|
||||
})
|
||||
}
|
||||
size={ServerApi.Config.constraints.accommodation_name_len}
|
||||
helperText={nameErr}
|
||||
/>
|
||||
|
||||
<PropEdit
|
||||
editable
|
||||
label="Description"
|
||||
value={accommodation?.description}
|
||||
onValueChange={(s) =>
|
||||
setAccommodation((a) => {
|
||||
return {
|
||||
...a!,
|
||||
description: s!,
|
||||
};
|
||||
})
|
||||
}
|
||||
size={ServerApi.Config.constraints.accommodation_description_len}
|
||||
helperText={descriptionErr}
|
||||
/>
|
||||
|
||||
<PropColorPicker
|
||||
editable
|
||||
label="Couleur"
|
||||
value={accommodation?.color}
|
||||
onChange={(s) =>
|
||||
setAccommodation((a) => {
|
||||
return {
|
||||
...a!,
|
||||
color: s!,
|
||||
};
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
<PropCheckbox
|
||||
editable
|
||||
label="Ouvert aux réservations"
|
||||
checked={accommodation?.open_to_reservations === true}
|
||||
onValueChange={(c) =>
|
||||
setAccommodation((a) => {
|
||||
return {
|
||||
...a!,
|
||||
open_to_reservations: c,
|
||||
};
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
<Tooltip
|
||||
title={
|
||||
"Permet de spécifier si un administrateur de la famille doit valider manuellement les demandes de réservation pour qu'elles soient validées"
|
||||
}
|
||||
>
|
||||
<PropCheckbox
|
||||
checkboxAlwaysVisible
|
||||
editable={accommodation?.open_to_reservations === true}
|
||||
label="Validation des réservations par un administrateur requise"
|
||||
checked={accommodation?.need_validation === true}
|
||||
onValueChange={(c) =>
|
||||
setAccommodation((a) => {
|
||||
return {
|
||||
...a!,
|
||||
need_validation: c,
|
||||
};
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Tooltip>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={cancel}>Annuler</Button>
|
||||
<Button
|
||||
onClick={submit}
|
||||
disabled={
|
||||
!!nameErr || (!!accommodation?.description && !!descriptionErr)
|
||||
}
|
||||
>
|
||||
{p.create ? "Créer" : "Mettre à jour"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
@@ -0,0 +1,192 @@
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
} from "@mui/material";
|
||||
import React from "react";
|
||||
import {
|
||||
AccommodationReservation,
|
||||
AccommodationsReservationsApi,
|
||||
UpdateAccommodationReservation,
|
||||
} from "../../api/accommodations/AccommodationsReservationsApi";
|
||||
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider";
|
||||
import { fmtUnixDate } from "../../utils/time_utils";
|
||||
import { useFamily } from "../../widgets/BaseFamilyRoute";
|
||||
import { useAccommodations } from "../../widgets/accommodations/BaseAccommodationsRoute";
|
||||
import { PropDateInput } from "../../widgets/forms/PropDateInput";
|
||||
import { PropSelect } from "../../widgets/forms/PropSelect";
|
||||
|
||||
export function UpdateReservationDialog(p: {
|
||||
open: boolean;
|
||||
create: boolean;
|
||||
reservation?: UpdateAccommodationReservation;
|
||||
onClose: () => void;
|
||||
onSubmitted: (c: UpdateAccommodationReservation) => void;
|
||||
}): React.ReactElement {
|
||||
const alert = useAlert();
|
||||
|
||||
const family = useFamily();
|
||||
const accommodations = useAccommodations();
|
||||
|
||||
const [reservation, setReservation] = React.useState<
|
||||
UpdateAccommodationReservation | undefined
|
||||
>();
|
||||
|
||||
const [conflicts, setConflicts] = React.useState<
|
||||
AccommodationReservation[] | undefined
|
||||
>(undefined);
|
||||
|
||||
const clearForm = () => {
|
||||
setReservation(undefined);
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
clearForm();
|
||||
p.onClose();
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
clearForm();
|
||||
p.onSubmitted(reservation!);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!reservation) setReservation(p.reservation);
|
||||
}, [p.open, p.reservation]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setConflicts(undefined);
|
||||
(async () => {
|
||||
try {
|
||||
if (
|
||||
!reservation ||
|
||||
reservation.accommodation_id < 1 ||
|
||||
reservation.start < 1 ||
|
||||
reservation.start > reservation.end
|
||||
) {
|
||||
setConflicts([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setConflicts(
|
||||
(
|
||||
await AccommodationsReservationsApi.ReservationsForInterval(
|
||||
family.family,
|
||||
accommodations.accommodations.get(reservation.accommodation_id)!,
|
||||
reservation.start,
|
||||
reservation.end
|
||||
)
|
||||
).filter(
|
||||
(r) =>
|
||||
r.id !== p.reservation?.reservation_id && r.validated !== false
|
||||
)
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(
|
||||
"Echec de la vérification de la présence de conflits de calendrier !"
|
||||
);
|
||||
}
|
||||
})();
|
||||
}, [
|
||||
p.open,
|
||||
reservation?.accommodation_id,
|
||||
reservation?.start,
|
||||
reservation?.end,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Dialog open={p.open} onClose={cancel}>
|
||||
<DialogTitle>
|
||||
{p.create ? "Création" : "Mise à jour"} d'une réservation
|
||||
</DialogTitle>
|
||||
<DialogContent style={{ display: "flex", flexDirection: "column" }}>
|
||||
<PropSelect
|
||||
editing={p.create}
|
||||
label="Logement ciblé"
|
||||
onValueChange={(v) => {
|
||||
setReservation((a) => {
|
||||
return {
|
||||
...a!,
|
||||
accommodation_id: Number(v),
|
||||
};
|
||||
});
|
||||
}}
|
||||
options={accommodations.accommodations.openToReservationList.map(
|
||||
(a) => {
|
||||
return { label: a.name, value: a.id.toString() };
|
||||
}
|
||||
)}
|
||||
value={
|
||||
reservation?.accommodation_id === -1
|
||||
? ""
|
||||
: reservation?.accommodation_id?.toString()
|
||||
}
|
||||
/>
|
||||
|
||||
<PropDateInput
|
||||
editable
|
||||
label="Date de début"
|
||||
value={reservation?.start}
|
||||
onChange={(s) => {
|
||||
setReservation((r) => {
|
||||
return { ...r!, start: s ?? -1 };
|
||||
});
|
||||
}}
|
||||
minDate={Math.floor(new Date().getTime() / 1000) - 3600 * 24 * 60}
|
||||
canSetMiddleDay
|
||||
/>
|
||||
|
||||
<PropDateInput
|
||||
editable
|
||||
label="Date de fin"
|
||||
value={reservation?.end}
|
||||
lastSecOfDay={true}
|
||||
onChange={(s) => {
|
||||
setReservation((r) => {
|
||||
return { ...r!, end: s ?? -1 };
|
||||
});
|
||||
}}
|
||||
minDate={reservation?.start}
|
||||
canSetMiddleDay
|
||||
/>
|
||||
|
||||
{conflicts && conflicts.length > 0 && (
|
||||
<Alert severity="error">
|
||||
<p>
|
||||
Cette réservation est en conflit avec d'autres réservations sur
|
||||
les intervalles suivants :
|
||||
</p>
|
||||
<ul>
|
||||
{conflicts.map((c, num) => (
|
||||
<li key={num}>
|
||||
Réservation du {fmtUnixDate(c.reservation_start)} au{" "}
|
||||
{fmtUnixDate(c.reservation_end)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Alert>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={cancel}>Annuler</Button>
|
||||
<Button
|
||||
onClick={submit}
|
||||
disabled={
|
||||
!(
|
||||
(reservation?.accommodation_id ?? -1) > 0 &&
|
||||
(reservation?.start ?? -1) > 0 &&
|
||||
(reservation?.end ?? -1) > (reservation?.start ?? 0) &&
|
||||
(conflicts?.length ?? 0) === 0
|
||||
)
|
||||
}
|
||||
>
|
||||
{p.create ? "Créer" : "Mettre à jour"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user