Add an accommodations reservations module #188

Merged
pierre merged 81 commits from accomodation_module into master 2024-06-22 21:30:26 +00:00
2 changed files with 143 additions and 5 deletions
Showing only changes of commit ac8ff918b1 - Show all commits

View File

@ -117,4 +117,14 @@ export class AccommodationsReservationsApi {
return new AccommodationsReservationsList(data);
}
/**
* Delete a reservation
*/
static async Delete(r: AccommodationReservation): Promise<void> {
await APIClient.exec({
method: "DELETE",
uri: `/family/${r.family_id}/accommodations/reservation/${r.id}`,
});
}
}

View File

@ -4,29 +4,44 @@ import dayGridPlugin from "@fullcalendar/daygrid";
import interactionPlugin from "@fullcalendar/interaction";
import listPlugin from "@fullcalendar/list";
import FullCalendar from "@fullcalendar/react";
import DeleteIcon from "@mui/icons-material/Delete";
import {
Alert,
Avatar,
Card,
CardActions,
CardContent,
CardHeader,
Checkbox,
FormControl,
FormControlLabel,
FormGroup,
FormLabel,
IconButton,
Popover,
Tooltip,
Typography,
fabClasses,
} from "@mui/material";
import { red } from "@mui/material/colors";
import React from "react";
import { FamilyApi, FamilyUser } from "../../../api/FamilyApi";
import { Accommodation } from "../../../api/accommodations/AccommodationListApi";
import {
AccommodationReservation,
AccommodationsReservationsApi,
AccommodationsReservationsList,
} from "../../../api/accommodations/AccommodationsReservationsApi";
import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider";
import { useConfirm } from "../../../hooks/context_providers/ConfirmDialogProvider";
import { useLoadingMessage } from "../../../hooks/context_providers/LoadingMessageProvider";
import { useSnackbar } from "../../../hooks/context_providers/SnackbarProvider";
import { useUpdateAccommodationReservation } from "../../../hooks/context_providers/accommodations/UpdateReservationDialogProvider";
import { fmtUnixDateFullCalendar } from "../../../utils/time_utils";
import {
fmtUnixDate,
fmtUnixDateFullCalendar,
} from "../../../utils/time_utils";
import { AsyncWidget } from "../../../widgets/AsyncWidget";
import { useUser } from "../../../widgets/BaseAuthenticatedPage";
import { useFamily } from "../../../widgets/BaseFamilyRoute";
import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle";
import { useAccommodations } from "../../../widgets/accommodations/BaseAccommodationsRoute";
@ -34,10 +49,12 @@ import { useAccommodations } from "../../../widgets/accommodations/BaseAccommoda
export function AccommodationsReservationsRoute(): React.ReactElement {
const snackbar = useSnackbar();
const alert = useAlert();
const confirm = useConfirm();
const loadingMessage = useLoadingMessage();
const loadKey = React.useRef(1);
const user = useUser();
const family = useFamily();
const accommodations = useAccommodations();
const updateReservation = useUpdateAccommodationReservation();
@ -62,6 +79,10 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
const [activeEvent, setActiveEvent] = React.useState<
| undefined
| {
user: FamilyUser;
accommodation: Accommodation;
reservation: AccommodationReservation;
x: number;
y: number;
w: number;
@ -127,9 +148,52 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
};
const onEventClick = (ev: EventClickArg) => {
const id: number = ev.event.extendedProps.id;
const resa = reservations?.get(id)!;
const acc = accommodations.accommodations.get(resa.accommodation_id)!;
const user = users?.find((u) => u.user_id === resa.user_id);
if (!user) {
console.error(`User ${resa.user_id} not found!`);
return;
}
const loc = ev.el.getBoundingClientRect();
setActiveEvent({ x: loc.left, y: loc.top, w: loc.width, h: loc.height });
console.log(ev);
setActiveEvent({
reservation: resa,
accommodation: acc,
user: user,
x: loc.left,
y: loc.top,
w: loc.width,
h: loc.height,
});
};
const deleteReservation = async (r: AccommodationReservation) => {
try {
if (
!(await confirm(
"Voulez-vous vraiment supprimer cette réservation ? L'opération n'est pas réversible !"
))
)
return;
setActiveEvent(undefined);
loadingMessage.show("Suppression de la réservation en cours...");
await AccommodationsReservationsApi.Delete(r);
reload();
snackbar("La réservation a été supprimée avec succès !");
} catch (e) {
console.error("Failed to delete a reservation!", e);
alert("Échec de la suppression de la réservation!");
} finally {
loadingMessage.hide();
}
};
return (
@ -283,6 +347,9 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
: r.validated === false
? "red dotted"
: "grey dotted",
extendedProps: {
id: r.id,
},
};
})}
/>
@ -299,6 +366,7 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
width: activeEvent?.w + "px",
height: activeEvent?.h + "px",
backgroundColor: "pink",
zIndex: 0,
}}
></div>
<Popover
@ -312,7 +380,67 @@ export function AccommodationsReservationsRoute(): React.ReactElement {
horizontal: "left",
}}
>
<Typography sx={{ p: 2 }}>The content of the Popover.</Typography>
<Card sx={{ maxWidth: 345 }} elevation={6}>
<CardHeader
avatar={
<Avatar sx={{ bgcolor: red[500] }}>
{activeEvent?.user.user_name
.substring(0, 1)
.toLocaleUpperCase()}
</Avatar>
}
title={activeEvent?.user.user_name}
subheader={activeEvent?.user.user_mail}
/>
<CardContent>
<Typography variant="body2" color="text.secondary">
<p>
Réservation de {activeEvent?.accommodation.name}
<br />
<em>{activeEvent?.accommodation.description}</em>
</p>
<p>
Du{" "}
{fmtUnixDate(
activeEvent?.reservation.reservation_start ?? 0
)}{" "}
<br />
Au{" "}
{fmtUnixDate(
activeEvent?.reservation.reservation_end ?? 0
)}
</p>
<p>
<strong>
{activeEvent?.reservation.validated === false ? (
<span style={{ color: "#f44336" }}>Refusée</span>
) : activeEvent?.reservation.validated === true ? (
<span style={{ color: "#66bb6a" }}>Validée</span>
) : (
<span style={{ color: "#29b6f6" }}>
En attente de validation
</span>
)}
</strong>
</p>
</Typography>
</CardContent>
<CardActions disableSpacing>
{user.user.id === activeEvent?.reservation.user_id && (
<Tooltip title="Supprimer la réservation" arrow>
<IconButton
color="error"
onClick={() =>
deleteReservation(activeEvent?.reservation)
}
>
<DeleteIcon />
</IconButton>
</Tooltip>
)}
</CardActions>
</Card>
</Popover>
</div>
)}