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 274 additions and 21 deletions
Showing only changes of commit 7525e78009 - Show all commits

View File

@ -34,6 +34,7 @@ interface Constraints {
member_note: LenConstraint; member_note: LenConstraint;
accommodation_name_len: LenConstraint; accommodation_name_len: LenConstraint;
accommodation_description_len: LenConstraint; accommodation_description_len: LenConstraint;
accommodation_calendar_name_len: LenConstraint;
} }
interface OIDCProvider { interface OIDCProvider {

View File

@ -0,0 +1,36 @@
import { APIClient } from "../ApiClient";
import { Family } from "../FamilyApi";
export interface NewCalendarURL {
accommodation_id?: number;
name: string;
}
export interface AccommodationCalendarURL {
id: number;
family_id: number;
accommodation_id: number;
user_id: number;
name: string;
token: string;
time_create: number;
time_used: number;
}
export class AccommodationsCalendarURLApi {
/**
* Create a new accommodation calendar URL
*/
static async Create(
family: Family,
calendar: NewCalendarURL
): Promise<AccommodationCalendarURL> {
return (
await APIClient.exec({
method: "POST",
uri: `/family/${family.family_id}/accommodations/reservations_calendars/create`,
jsonData: calendar,
})
).data;
}
}

View File

@ -0,0 +1,92 @@
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
} from "@mui/material";
import React from "react";
import { ServerApi } from "../../api/ServerApi";
import { NewCalendarURL } from "../../api/accommodations/AccommodationsCalendarURLApi";
import { checkConstraint } from "../../utils/from_utils";
import { PropEdit } from "../../widgets/forms/PropEdit";
import { PropSelect } from "../../widgets/forms/PropSelect";
import { useAccommodations } from "../../widgets/accommodations/BaseAccommodationsRoute";
export function CreateAccommodationCalendarURLDialog(p: {
open: boolean;
onClose: () => void;
onSubmitted: (c: NewCalendarURL) => void;
}): React.ReactElement {
const [calendar, setCalendar] = React.useState<NewCalendarURL>({ name: "" });
const accommodations = useAccommodations();
const nameErr = checkConstraint(
ServerApi.Config.constraints.accommodation_calendar_name_len,
calendar?.name
);
const clearForm = () => {
setCalendar({ name: "" });
};
const cancel = () => {
clearForm();
p.onClose();
};
const submit = async () => {
clearForm();
p.onSubmitted(calendar!);
};
return (
<Dialog open={p.open} onClose={cancel}>
<DialogTitle>Création d'un calendrier</DialogTitle>
<DialogContent style={{ display: "flex", flexDirection: "column" }}>
<PropEdit
editable
label="Nom"
value={calendar?.name}
onValueChange={(s) =>
setCalendar((a) => {
return {
...a!,
name: s!,
};
})
}
size={ServerApi.Config.constraints.accommodation_calendar_name_len}
helperText={nameErr}
/>
<PropSelect
editing
label="Logement ciblé"
onValueChange={(v) => {
setCalendar((a) => {
return {
...a!,
accommodation_id: v !== "A" && v ? Number(v) : undefined,
};
});
}}
options={[
{ label: "Tous les logements", value: "A" },
...accommodations.accommodations.fullList.map((a) => {
return { label: a.name, value: a.id.toString() };
}),
]}
value={calendar.accommodation_id?.toString() ?? "A"}
/>
</DialogContent>
<DialogActions>
<Button onClick={cancel}>Annuler</Button>
<Button onClick={submit} disabled={!!nameErr}>
Créer
</Button>
</DialogActions>
</Dialog>
);
}

View File

@ -0,0 +1,52 @@
import React, { PropsWithChildren } from "react";
import { NewCalendarURL } from "../../../api/accommodations/AccommodationsCalendarURLApi";
import { CreateAccommodationCalendarURLDialog } from "../../../dialogs/accommodations/CreateAccommodationCalendarURLDialog";
type DialogContext = () => Promise<NewCalendarURL | undefined>;
const DialogContextK = React.createContext<DialogContext | null>(null);
export function CreateAccommodationCalendarURLDialogProvider(
p: PropsWithChildren
): React.ReactElement {
const [open, setOpen] = React.useState(false);
const cb = React.useRef<null | ((a: NewCalendarURL | undefined) => void)>(
null
);
const handleClose = (res?: NewCalendarURL) => {
setOpen(false);
if (cb.current !== null) cb.current(res);
cb.current = null;
};
const hook: DialogContext = () => {
setOpen(true);
return new Promise((res) => {
cb.current = res;
});
};
return (
<>
<DialogContextK.Provider value={hook}>
{p.children}
</DialogContextK.Provider>
{open && (
<CreateAccommodationCalendarURLDialog
open={open}
onClose={handleClose}
onSubmitted={handleClose}
/>
)}
</>
);
}
export function useCreateAccommodationCalendarURL(): DialogContext {
return React.useContext(DialogContextK)!;
}

View File

@ -1,33 +1,35 @@
import AddIcon from "@mui/icons-material/Add";
import CheckIcon from "@mui/icons-material/Check";
import CloseIcon from "@mui/icons-material/Close";
import { import {
CardContent,
Typography,
Alert,
Button, Button,
Card, Card,
CardActions, CardActions,
CardContent,
Typography,
} from "@mui/material"; } from "@mui/material";
import React from "react"; import React from "react";
import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider";
import { useConfirm } from "../../../hooks/context_providers/ConfirmDialogProvider";
import { useLoadingMessage } from "../../../hooks/context_providers/LoadingMessageProvider";
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 { import {
Accommodation, Accommodation,
AccommodationListApi, AccommodationListApi,
} from "../../../api/accommodations/AccommodationListApi"; } from "../../../api/accommodations/AccommodationListApi";
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 { useSnackbar } from "../../../hooks/context_providers/SnackbarProvider";
import { useAccommodations } from "../../../widgets/accommodations/BaseAccommodationsRoute"; import { useUpdateAccommodation } from "../../../hooks/context_providers/accommodations/UpdateAccommodationDialogProvider";
import { useFamily } from "../../../widgets/BaseFamilyRoute";
import { FamilyCard } from "../../../widgets/FamilyCard";
import { TimeWidget } from "../../../widgets/TimeWidget"; import { TimeWidget } from "../../../widgets/TimeWidget";
import CheckIcon from "@mui/icons-material/Check"; import { useAccommodations } from "../../../widgets/accommodations/BaseAccommodationsRoute";
import CloseIcon from "@mui/icons-material/Close"; import { useCreateAccommodationCalendarURL } from "../../../hooks/context_providers/accommodations/CreateAccommodationCalendarURLDialogProvider";
import { AccommodationsCalendarURLApi } from "../../../api/accommodations/AccommodationsCalendarURLApi";
export function AccommodationsSettingsRoute(): React.ReactElement { export function AccommodationsSettingsRoute(): React.ReactElement {
return ( return (
<> <>
<AccommodationsListCard /> <AccommodationsListCard />
<AccommodationsCalURLsCard />
</> </>
); );
} }
@ -35,7 +37,6 @@ export function AccommodationsSettingsRoute(): React.ReactElement {
function AccommodationsListCard(): React.ReactElement { function AccommodationsListCard(): React.ReactElement {
const loading = useLoadingMessage(); const loading = useLoadingMessage();
const confirm = useConfirm(); const confirm = useConfirm();
const alert = useAlert();
const snackbar = useSnackbar(); const snackbar = useSnackbar();
const family = useFamily(); const family = useFamily();
@ -121,7 +122,7 @@ function AccommodationsListCard(): React.ReactElement {
}; };
return ( return (
<FamilyCard error={error} success={success}> <FamilyCard error={error} success={success} style={{ width: "350px" }}>
<CardContent> <CardContent>
<Typography gutterBottom variant="h5" component="div"> <Typography gutterBottom variant="h5" component="div">
Logements Logements
@ -150,7 +151,7 @@ function AccommodationsListCard(): React.ReactElement {
onClick={createAccommodation} onClick={createAccommodation}
size={"large"} size={"large"}
> >
Ajouter un nouveau logement Ajouter un logement
</Button> </Button>
)} )}
</CardContent> </CardContent>
@ -213,3 +214,66 @@ function BoolIcon(p: { checked?: boolean }): React.ReactElement {
<CloseIcon color="error" /> <CloseIcon color="error" />
); );
} }
function AccommodationsCalURLsCard(): React.ReactElement {
const loading = useLoadingMessage();
const [error, setError] = React.useState<string>();
const [success, setSuccess] = React.useState<string>();
const family = useFamily();
const createCalendarURLDialog = useCreateAccommodationCalendarURL();
const createCalendarURL = async () => {
try {
const newCal = await createCalendarURLDialog();
if (!newCal) return;
loading.show("Création du logement en cours...");
const cal = await AccommodationsCalendarURLApi.Create(
family.family,
newCal
);
setSuccess("Le calendrier a été créé avec succès !");
// TODO : reload URLS list
// TODO : show QrCode dialog
console.log(cal);
} catch (e) {
console.error("Failed to create new accommodation calendar URL!", e);
setError(`Échec de la création du calendrier! ${e}`);
} finally {
loading.hide();
}
};
return (
<FamilyCard error={error} success={success} style={{ width: "350px" }}>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
URL de calendriers
</Typography>
<Typography>
Vous pouvez, si vous le souhaitez, importer dans votre application de
calendrier le planning de réservation des logement. Pour ce faire, il
vous suffit de créer une URL de calendrier.
</Typography>
<Button
startIcon={<AddIcon />}
variant="outlined"
color="info"
fullWidth
onClick={createCalendarURL}
size={"large"}
>
Créer un calendrier
</Button>
</CardContent>
</FamilyCard>
);
}

View File

@ -2,10 +2,14 @@ import { Alert, Card } from "@mui/material";
import { PropsWithChildren } from "react"; import { PropsWithChildren } from "react";
export function FamilyCard( export function FamilyCard(
p: PropsWithChildren<{ error?: string; success?: string }> p: PropsWithChildren<{
error?: string;
success?: string;
style?: React.CSSProperties | undefined;
}>
): React.ReactElement { ): React.ReactElement {
return ( return (
<Card style={{ margin: "10px auto", maxWidth: "450px" }}> <Card style={{ ...p.style, margin: "10px auto", maxWidth: "450px" }}>
{p.error && <Alert severity="error">{p.error}</Alert>} {p.error && <Alert severity="error">{p.error}</Alert>}
{p.success && <Alert severity="success">{p.success}</Alert>} {p.success && <Alert severity="success">{p.success}</Alert>}

View File

@ -4,9 +4,10 @@ import {
AccommodationListApi, AccommodationListApi,
AccommodationsList, AccommodationsList,
} from "../../api/accommodations/AccommodationListApi"; } from "../../api/accommodations/AccommodationListApi";
import { CreateAccommodationCalendarURLDialogProvider } from "../../hooks/context_providers/accommodations/CreateAccommodationCalendarURLDialogProvider";
import { UpdateAccommodationDialogProvider } from "../../hooks/context_providers/accommodations/UpdateAccommodationDialogProvider";
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;
@ -61,7 +62,9 @@ export function BaseAccommodationsRoute(): React.ReactElement {
}} }}
> >
<UpdateAccommodationDialogProvider> <UpdateAccommodationDialogProvider>
<Outlet /> <CreateAccommodationCalendarURLDialogProvider>
<Outlet />
</CreateAccommodationCalendarURLDialogProvider>
</UpdateAccommodationDialogProvider> </UpdateAccommodationDialogProvider>
</AccommodationsContextK.Provider> </AccommodationsContextK.Provider>
); );

View File

@ -2,7 +2,7 @@ import { FormControl, InputLabel, MenuItem, Select } from "@mui/material";
import { PropEdit } from "./PropEdit"; import { PropEdit } from "./PropEdit";
export interface SelectOption { export interface SelectOption {
value: string; value: string | undefined;
label: string; label: string;
} }
@ -19,6 +19,7 @@ export function PropSelect(p: {
const value = p.options.find((o) => o.value === p.value)?.label; const value = p.options.find((o) => o.value === p.value)?.label;
return <PropEdit label={p.label} editable={p.editing} value={value} />; return <PropEdit label={p.label} editable={p.editing} value={value} />;
} }
return ( return (
<FormControl fullWidth variant="filled" style={{ marginBottom: "15px" }}> <FormControl fullWidth variant="filled" style={{ marginBottom: "15px" }}>
<InputLabel>{p.label}</InputLabel> <InputLabel>{p.label}</InputLabel>