Add an accommodations reservations module #188
@ -50,6 +50,13 @@ export class AccommodationsList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UpdateAccommodation {
|
||||||
|
name: string;
|
||||||
|
need_validation: boolean;
|
||||||
|
description?: string;
|
||||||
|
open_to_reservations: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export class AccommodationListApi {
|
export class AccommodationListApi {
|
||||||
/**
|
/**
|
||||||
* Get the list of accommodation of a family
|
* Get the list of accommodation of a family
|
||||||
@ -64,4 +71,20 @@ export class AccommodationListApi {
|
|||||||
|
|
||||||
return new AccommodationsList(data);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -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)!;
|
||||||
|
}
|
@ -6,6 +6,10 @@ import { useLoadingMessage } from "../../../hooks/context_providers/LoadingMessa
|
|||||||
import { useFamily } from "../../../widgets/BaseFamilyRoute";
|
import { useFamily } from "../../../widgets/BaseFamilyRoute";
|
||||||
import { FamilyCard } from "../../../widgets/FamilyCard";
|
import { FamilyCard } from "../../../widgets/FamilyCard";
|
||||||
import AddIcon from "@mui/icons-material/Add";
|
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 {
|
export function AccommodationsSettingsRoute(): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
@ -19,13 +23,43 @@ function AccommodationsListCard(): React.ReactElement {
|
|||||||
const loading = useLoadingMessage();
|
const loading = useLoadingMessage();
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
|
const snackbar = useSnackbar();
|
||||||
|
|
||||||
const family = useFamily();
|
const family = useFamily();
|
||||||
|
const accommodations = useAccommodations();
|
||||||
|
|
||||||
const [error, setError] = React.useState<string>();
|
const [error, setError] = React.useState<string>();
|
||||||
const [success, setSuccess] = 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 (
|
return (
|
||||||
<FamilyCard error={error} success={success}>
|
<FamilyCard error={error} success={success}>
|
||||||
|
21
geneit_app/src/utils/from_utils.ts
Normal file
21
geneit_app/src/utils/from_utils.ts
Normal 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;
|
||||||
|
}
|
@ -6,6 +6,7 @@ import {
|
|||||||
} from "../../api/accommodations/AccommodationListApi";
|
} from "../../api/accommodations/AccommodationListApi";
|
||||||
import { AsyncWidget } from "../AsyncWidget";
|
import { AsyncWidget } from "../AsyncWidget";
|
||||||
import { useFamily } from "../BaseFamilyRoute";
|
import { useFamily } from "../BaseFamilyRoute";
|
||||||
|
import { UpdateAccommodationDialogProvider } from "../../hooks/context_providers/accommodations/UpdateAccommodationDialogProvider";
|
||||||
|
|
||||||
interface AccommodationsContext {
|
interface AccommodationsContext {
|
||||||
accommodations: AccommodationsList;
|
accommodations: AccommodationsList;
|
||||||
@ -59,7 +60,9 @@ export function BaseAccommodationsRoute(): React.ReactElement {
|
|||||||
reloadAccommodationsList: onReload,
|
reloadAccommodationsList: onReload,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<UpdateAccommodationDialogProvider>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
</UpdateAccommodationDialogProvider>
|
||||||
</AccommodationsContextK.Provider>
|
</AccommodationsContextK.Provider>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
@ -5,16 +5,20 @@ export function PropCheckbox(p: {
|
|||||||
label: string;
|
label: string;
|
||||||
checked: boolean | undefined;
|
checked: boolean | undefined;
|
||||||
onValueChange: (v: boolean) => void;
|
onValueChange: (v: boolean) => void;
|
||||||
|
checkboxAlwaysVisible?: boolean;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
|
if (!p.checkboxAlwaysVisible) {
|
||||||
if (!p.editable && p.checked)
|
if (!p.editable && p.checked)
|
||||||
return <Typography variant="body2">{p.label}</Typography>;
|
return <Typography variant="body2">{p.label}</Typography>;
|
||||||
|
|
||||||
if (!p.editable) return <></>;
|
if (!p.editable) return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
disabled={!p.editable}
|
||||||
checked={p.checked}
|
checked={p.checked}
|
||||||
onChange={(e) => p.onValueChange(e.target.checked)}
|
onChange={(e) => p.onValueChange(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
|
@ -14,6 +14,7 @@ export function PropEdit(p: {
|
|||||||
multiline?: boolean;
|
multiline?: boolean;
|
||||||
minRows?: number;
|
minRows?: number;
|
||||||
maxRows?: number;
|
maxRows?: number;
|
||||||
|
helperText?: string;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
if (((!p.editable && p.value) ?? "") === "") return <></>;
|
if (((!p.editable && p.value) ?? "") === "") return <></>;
|
||||||
|
|
||||||
@ -44,6 +45,7 @@ export function PropEdit(p: {
|
|||||||
!p.checkValue(p.value)) ||
|
!p.checkValue(p.value)) ||
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
helperText={p.helperText}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user