Check accommodation availability directly in create reservation dialog
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing

This commit is contained in:
Pierre HUBERT 2024-06-19 22:09:29 +02:00
parent b66a8a8ac9
commit 9929c5db48
6 changed files with 144 additions and 23 deletions

View File

@ -1,5 +1,6 @@
import { APIClient } from "../ApiClient"; import { APIClient } from "../ApiClient";
import { Family } from "../FamilyApi"; import { Family } from "../FamilyApi";
import { Accommodation } from "./AccommodationListApi";
export interface AccommodationReservation { export interface AccommodationReservation {
id: number; id: number;
@ -59,6 +60,7 @@ export interface UpdateAccommodationReservation {
start: number; start: number;
end: number; end: number;
accommodation_id: number; accommodation_id: number;
reservation_id?: number;
} }
export class AccommodationsReservationsApi { export class AccommodationsReservationsApi {
@ -77,4 +79,23 @@ export class AccommodationsReservationsApi {
return new AccommodationsReservationsList(data); return new AccommodationsReservationsList(data);
} }
/**
* Get the reservations of a given time interval for an accommodation
*/
static async ReservationsForInterval(
family: Family,
accommodation: Accommodation,
start: number,
end: number
): Promise<AccommodationsReservationsList> {
const data = (
await APIClient.exec({
method: "GET",
uri: `/family/${family.family_id}/accommodations/reservations/accommodation/${accommodation.id}/for_interval?start=${start}&end=${end}`,
})
).data;
return new AccommodationsReservationsList(data);
}
} }

View File

@ -1,15 +1,24 @@
import { import {
Alert,
Button, Button,
Dialog, Dialog,
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
Typography,
} from "@mui/material"; } from "@mui/material";
import React from "react"; import React from "react";
import { UpdateAccommodationReservation } from "../../api/accommodations/AccommodationsReservationsApi"; import {
AccommodationReservation,
AccommodationsReservationsApi,
UpdateAccommodationReservation,
} from "../../api/accommodations/AccommodationsReservationsApi";
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider";
import { useFamily } from "../../widgets/BaseFamilyRoute";
import { useAccommodations } from "../../widgets/accommodations/BaseAccommodationsRoute"; import { useAccommodations } from "../../widgets/accommodations/BaseAccommodationsRoute";
import { PropDateInput } from "../../widgets/forms/PropDateInput"; import { PropDateInput } from "../../widgets/forms/PropDateInput";
import { PropSelect } from "../../widgets/forms/PropSelect"; import { PropSelect } from "../../widgets/forms/PropSelect";
import { fmtUnixDate } from "../../utils/time_utils";
export function UpdateReservationDialog(p: { export function UpdateReservationDialog(p: {
open: boolean; open: boolean;
@ -18,12 +27,19 @@ export function UpdateReservationDialog(p: {
onClose: () => void; onClose: () => void;
onSubmitted: (c: UpdateAccommodationReservation) => void; onSubmitted: (c: UpdateAccommodationReservation) => void;
}): React.ReactElement { }): React.ReactElement {
const alert = useAlert();
const family = useFamily();
const accommodations = useAccommodations(); const accommodations = useAccommodations();
const [reservation, setReservation] = React.useState< const [reservation, setReservation] = React.useState<
UpdateAccommodationReservation | undefined UpdateAccommodationReservation | undefined
>(); >();
const [conflicts, setConflicts] = React.useState<
AccommodationReservation[] | undefined
>(undefined);
const clearForm = () => { const clearForm = () => {
setReservation(undefined); setReservation(undefined);
}; };
@ -42,7 +58,46 @@ export function UpdateReservationDialog(p: {
if (!reservation) setReservation(p.reservation); if (!reservation) setReservation(p.reservation);
}, [p.open, p.reservation]); }, [p.open, p.reservation]);
// TODO : check availability 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 ( return (
<Dialog open={p.open} onClose={cancel}> <Dialog open={p.open} onClose={cancel}>
@ -94,7 +149,22 @@ export function UpdateReservationDialog(p: {
minDate={reservation?.start} minDate={reservation?.start}
/> />
{/* TODO : la suite */} {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> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={cancel}>Annuler</Button> <Button onClick={cancel}>Annuler</Button>
@ -104,7 +174,8 @@ export function UpdateReservationDialog(p: {
!( !(
(reservation?.accommodation_id ?? -1) > 0 && (reservation?.accommodation_id ?? -1) > 0 &&
(reservation?.start ?? -1) > 0 && (reservation?.start ?? -1) > 0 &&
(reservation?.end ?? -1) > (reservation?.start ?? 0) (reservation?.end ?? -1) > (reservation?.start ?? 0) &&
(conflicts?.length ?? 0) === 0
) )
} }
> >

View File

@ -74,7 +74,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
return ( return (
<> <>
<FamilyPageTitle title="Réservation" /> <FamilyPageTitle title="Réservations" />
<AsyncWidget <AsyncWidget
loadKey={loadKey.current} loadKey={loadKey.current}
load={load} load={load}

View File

@ -39,7 +39,6 @@ export function PropDateInput(p: {
const maxDate = p.maxDate ? dayjs(new Date(p.maxDate * 1000)) : undefined; const maxDate = p.maxDate ? dayjs(new Date(p.maxDate * 1000)) : undefined;
return ( return (
<div style={{ margin: "10px auto" }}>
<LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="fr"> <LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="fr">
<DatePicker <DatePicker
label={p.label} label={p.label}
@ -55,7 +54,7 @@ export function PropDateInput(p: {
minDate={minDate} minDate={minDate}
maxDate={maxDate} maxDate={maxDate}
/> />
<div style={{ height: "10px" }}></div>
</LocalizationProvider> </LocalizationProvider>
</div>
); );
} }

View File

@ -93,6 +93,31 @@ pub async fn get_accommodation_reservations(a: FamilyAndAccommodationInPath) ->
.json(accommodations_reservations_service::get_all_of_accommodation(a.id()).await?)) .json(accommodations_reservations_service::get_all_of_accommodation(a.id()).await?))
} }
#[derive(serde::Deserialize)]
pub struct CheckAvailabilityQuery {
start: usize,
end: usize,
}
/// Check reservation availability
pub async fn get_accommodation_reservations_for_interval(
a: FamilyAndAccommodationInPath,
req: web::Query<CheckAvailabilityQuery>,
) -> HttpResult {
if req.start > req.end {
return Ok(HttpResponse::BadRequest().json("start should be smaller than end!"));
}
let res = accommodations_reservations_service::get_reservations_for_time_interval(
a.id(),
req.start,
req.end,
)
.await?;
Ok(HttpResponse::Ok().json(res))
}
/// Get the full list of accommodations reservations for a family /// Get the full list of accommodations reservations for a family
pub async fn full_list(m: FamilyInPath) -> HttpResult { pub async fn full_list(m: FamilyInPath) -> HttpResult {
Ok(HttpResponse::Ok() Ok(HttpResponse::Ok()

View File

@ -233,6 +233,11 @@ async fn main() -> std::io::Result<()> {
web::get() web::get()
.to(accommodations_reservations_controller::get_accommodation_reservations), .to(accommodations_reservations_controller::get_accommodation_reservations),
) )
.route(
"/family/{id}/accommodations/reservations/accommodation/{accommodation_id}/for_interval",
web::get()
.to(accommodations_reservations_controller::get_accommodation_reservations_for_interval),
)
.route( .route(
"/family/{id}/accommodations/reservations/full_list", "/family/{id}/accommodations/reservations/full_list",
web::get().to(accommodations_reservations_controller::full_list), web::get().to(accommodations_reservations_controller::full_list),