Add an accommodations reservations module #188
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -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)!;
|
||||||
|
}
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -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>}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user