From 7d64ea219ff5cf159e936cf7f25b81afb370da42 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 10 Jun 2024 22:00:30 +0200 Subject: [PATCH] Can create accommodation from WebUI --- .../accommodations/AccommodationListApi.tsx | 23 +++ .../UpdateAccommodationDialog.tsx | 140 ++++++++++++++++++ .../UpdateAccommodationDialogProvider.tsx | 64 ++++++++ .../AccommodationsSettingsRoute.tsx | 36 ++++- geneit_app/src/utils/from_utils.ts | 21 +++ .../BaseAccommodationsRoute.tsx | 5 +- geneit_app/src/widgets/forms/PropCheckbox.tsx | 10 +- geneit_app/src/widgets/forms/PropEdit.tsx | 2 + 8 files changed, 296 insertions(+), 5 deletions(-) create mode 100644 geneit_app/src/dialogs/accommodations/UpdateAccommodationDialog.tsx create mode 100644 geneit_app/src/hooks/context_providers/accommodations/UpdateAccommodationDialogProvider.tsx create mode 100644 geneit_app/src/utils/from_utils.ts diff --git a/geneit_app/src/api/accommodations/AccommodationListApi.tsx b/geneit_app/src/api/accommodations/AccommodationListApi.tsx index b343a09..f51d2e1 100644 --- a/geneit_app/src/api/accommodations/AccommodationListApi.tsx +++ b/geneit_app/src/api/accommodations/AccommodationListApi.tsx @@ -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 { + return ( + await APIClient.exec({ + method: "POST", + uri: `/family/${family.family_id}/accommodations/list/create`, + jsonData: accommodation, + }) + ).data; + } } diff --git a/geneit_app/src/dialogs/accommodations/UpdateAccommodationDialog.tsx b/geneit_app/src/dialogs/accommodations/UpdateAccommodationDialog.tsx new file mode 100644 index 0000000..a780f3c --- /dev/null +++ b/geneit_app/src/dialogs/accommodations/UpdateAccommodationDialog.tsx @@ -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 ( + + + {p.create ? "Création" : "Mise à jour"} d'un logement + + + + setAccommodation((a) => { + return { + ...a!, + name: s!, + }; + }) + } + size={ServerApi.Config.constraints.accommodation_name_len} + helperText={nameErr} + /> + + + setAccommodation((a) => { + return { + ...a!, + description: s!, + }; + }) + } + size={ServerApi.Config.constraints.accommodation_description_len} + helperText={descriptionErr} + /> + + + setAccommodation((a) => { + return { + ...a!, + open_to_reservations: c, + }; + }) + } + /> + + + + setAccommodation((a) => { + return { + ...a!, + need_validation: c, + }; + }) + } + /> + + + + + + + + ); +} diff --git a/geneit_app/src/hooks/context_providers/accommodations/UpdateAccommodationDialogProvider.tsx b/geneit_app/src/hooks/context_providers/accommodations/UpdateAccommodationDialogProvider.tsx new file mode 100644 index 0000000..63a6b77 --- /dev/null +++ b/geneit_app/src/hooks/context_providers/accommodations/UpdateAccommodationDialogProvider.tsx @@ -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; + +const DialogContextK = React.createContext(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 ( + <> + + {p.children} + + + {open && ( + + )} + + ); +} + +export function useUpdateAccommodation(): DialogContext { + return React.useContext(DialogContextK)!; +} diff --git a/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx b/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx index 76a7725..c26267f 100644 --- a/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx +++ b/geneit_app/src/routes/family/accommodations/AccommodationsSettingsRoute.tsx @@ -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(); const [success, setSuccess] = React.useState(); - 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 ( diff --git a/geneit_app/src/utils/from_utils.ts b/geneit_app/src/utils/from_utils.ts new file mode 100644 index 0000000..1be31df --- /dev/null +++ b/geneit_app/src/utils/from_utils.ts @@ -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; +} diff --git a/geneit_app/src/widgets/accommodations/BaseAccommodationsRoute.tsx b/geneit_app/src/widgets/accommodations/BaseAccommodationsRoute.tsx index 68f0d8e..c543ac9 100644 --- a/geneit_app/src/widgets/accommodations/BaseAccommodationsRoute.tsx +++ b/geneit_app/src/widgets/accommodations/BaseAccommodationsRoute.tsx @@ -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, }} > - + + + ); }} diff --git a/geneit_app/src/widgets/forms/PropCheckbox.tsx b/geneit_app/src/widgets/forms/PropCheckbox.tsx index 1316622..8c26c29 100644 --- a/geneit_app/src/widgets/forms/PropCheckbox.tsx +++ b/geneit_app/src/widgets/forms/PropCheckbox.tsx @@ -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 {p.label}; + if (!p.checkboxAlwaysVisible) { + if (!p.editable && p.checked) + return {p.label}; - if (!p.editable) return <>; + if (!p.editable) return <>; + } return ( p.onValueChange(e.target.checked)} /> diff --git a/geneit_app/src/widgets/forms/PropEdit.tsx b/geneit_app/src/widgets/forms/PropEdit.tsx index 217d359..f365bf2 100644 --- a/geneit_app/src/widgets/forms/PropEdit.tsx +++ b/geneit_app/src/widgets/forms/PropEdit.tsx @@ -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} /> ); }