Add an accommodations reservations module #188

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

View File

@ -50,6 +50,13 @@ export class AccommodationsList {
}
}
export interface UpdateAccommodation {
name: string;
need_validation: boolean;
description?: string;
open_to_reservations: boolean;
}
export class AccommodationListApi {
/**
* Get the list of accommodation of a family
@ -64,4 +71,20 @@ export class AccommodationListApi {
return new AccommodationsList(data);
}
/**
* Create a new accommodation
*/
static async Create(
family: Family,
accommodation: UpdateAccommodation
): Promise<Accommodation> {
return (
await APIClient.exec({
method: "POST",
uri: `/family/${family.family_id}/accommodations/list/create`,
jsonData: accommodation,
})
).data;
}
}

View File

@ -0,0 +1,140 @@
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/from_utils";
import { PropCheckbox } from "../../widgets/forms/PropCheckbox";
import { PropEdit } from "../../widgets/forms/PropEdit";
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}
/>
<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 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>
);
}

View File

@ -0,0 +1,64 @@
import React, { PropsWithChildren } from "react";
import { UpdateAccommodation } from "../../../api/accommodations/AccommodationListApi";
import { UpdateAccommodationDialog } from "../../../dialogs/accommodations/UpdateAccommodationDialog";
type DialogContext = (
accommodation: UpdateAccommodation,
create: boolean
) => Promise<UpdateAccommodation | undefined>;
const DialogContextK = React.createContext<DialogContext | null>(null);
export function UpdateAccommodationDialogProvider(
p: PropsWithChildren
): React.ReactElement {
const [open, setOpen] = React.useState(false);
const [accommodation, setAccommodation] = React.useState<
UpdateAccommodation | undefined
>(undefined);
const [create, setCreate] = React.useState(false);
const cb = React.useRef<
null | ((a: UpdateAccommodation | undefined) => void)
>(null);
const handleClose = (res?: UpdateAccommodation) => {
setOpen(false);
if (cb.current !== null) cb.current(res);
cb.current = null;
};
const hook: DialogContext = (accommodation, create) => {
setAccommodation(accommodation);
setCreate(create);
setOpen(true);
return new Promise((res) => {
cb.current = res;
});
};
return (
<>
<DialogContextK.Provider value={hook}>
{p.children}
</DialogContextK.Provider>
{open && (
<UpdateAccommodationDialog
open={open}
accommodation={accommodation}
create={create}
onClose={handleClose}
onSubmitted={handleClose}
/>
)}
</>
);
}
export function useUpdateAccommodation(): DialogContext {
return React.useContext(DialogContextK)!;
}

View File

@ -6,6 +6,10 @@ import { useLoadingMessage } from "../../../hooks/context_providers/LoadingMessa
import { useFamily } from "../../../widgets/BaseFamilyRoute";
import { FamilyCard } from "../../../widgets/FamilyCard";
import AddIcon from "@mui/icons-material/Add";
import { useUpdateAccommodation } from "../../../hooks/context_providers/accommodations/UpdateAccommodationDialogProvider";
import { AccommodationListApi } from "../../../api/accommodations/AccommodationListApi";
import { useSnackbar } from "../../../hooks/context_providers/SnackbarProvider";
import { useAccommodations } from "../../../widgets/accommodations/BaseAccommodationsRoute";
export function AccommodationsSettingsRoute(): React.ReactElement {
return (
@ -19,13 +23,43 @@ function AccommodationsListCard(): React.ReactElement {
const loading = useLoadingMessage();
const confirm = useConfirm();
const alert = useAlert();
const snackbar = useSnackbar();
const family = useFamily();
const accommodations = useAccommodations();
const [error, setError] = React.useState<string>();
const [success, setSuccess] = React.useState<string>();
const createAccommodation = () => {};
const updateAccommodation = useUpdateAccommodation();
const createAccommodation = async () => {
try {
const accommodation = await updateAccommodation(
{
name: "",
open_to_reservations: true,
need_validation: false,
},
true
);
if (!accommodation) return;
loading.show("Création du logement en cours...");
await AccommodationListApi.Create(family.family, accommodation);
snackbar("Le logement a été créé avec succès !");
await accommodations.reloadAccommodationsList();
} catch (e) {
console.error("Failed to create accommodation!", e);
alert(`Echec de la création du logement! ${e}`);
} finally {
loading.hide();
}
};
return (
<FamilyCard error={error} success={success}>

View File

@ -0,0 +1,21 @@
import { LenConstraint } from "../api/ServerApi";
/**
* Check if a constraint was respected or not
*
* @returns An error message appropriate for the constraint
* violation, if any, or undefined otherwise
*/
export function checkConstraint(
constraint: LenConstraint,
value: string | undefined
): string | undefined {
value = value ?? "";
if (value.length < constraint.min)
return `Veuillez indiquer au moins ${constraint.min} caractères !`;
if (value.length > constraint.max)
return `Veuillez indiquer au maximum ${constraint.min} caractères !`;
return undefined;
}

View File

@ -6,6 +6,7 @@ import {
} from "../../api/accommodations/AccommodationListApi";
import { AsyncWidget } from "../AsyncWidget";
import { useFamily } from "../BaseFamilyRoute";
import { UpdateAccommodationDialogProvider } from "../../hooks/context_providers/accommodations/UpdateAccommodationDialogProvider";
interface AccommodationsContext {
accommodations: AccommodationsList;
@ -59,7 +60,9 @@ export function BaseAccommodationsRoute(): React.ReactElement {
reloadAccommodationsList: onReload,
}}
>
<Outlet />
<UpdateAccommodationDialogProvider>
<Outlet />
</UpdateAccommodationDialogProvider>
</AccommodationsContextK.Provider>
);
}}

View File

@ -5,16 +5,20 @@ export function PropCheckbox(p: {
label: string;
checked: boolean | undefined;
onValueChange: (v: boolean) => void;
checkboxAlwaysVisible?: boolean;
}): React.ReactElement {
if (!p.editable && p.checked)
return <Typography variant="body2">{p.label}</Typography>;
if (!p.checkboxAlwaysVisible) {
if (!p.editable && p.checked)
return <Typography variant="body2">{p.label}</Typography>;
if (!p.editable) return <></>;
if (!p.editable) return <></>;
}
return (
<FormControlLabel
control={
<Checkbox
disabled={!p.editable}
checked={p.checked}
onChange={(e) => p.onValueChange(e.target.checked)}
/>

View File

@ -14,6 +14,7 @@ export function PropEdit(p: {
multiline?: boolean;
minRows?: number;
maxRows?: number;
helperText?: string;
}): React.ReactElement {
if (((!p.editable && p.value) ?? "") === "") return <></>;
@ -44,6 +45,7 @@ export function PropEdit(p: {
!p.checkValue(p.value)) ||
false
}
helperText={p.helperText}
/>
);
}