Add an accommodations reservations module #188
@ -34,6 +34,7 @@ interface Constraints {
|
||||
member_note: LenConstraint;
|
||||
accommodation_name_len: LenConstraint;
|
||||
accommodation_description_len: LenConstraint;
|
||||
accommodation_calendar_name_len: LenConstraint;
|
||||
}
|
||||
|
||||
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 {
|
||||
CardContent,
|
||||
Typography,
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
CardActions,
|
||||
CardContent,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
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 {
|
||||
Accommodation,
|
||||
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 { 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 CheckIcon from "@mui/icons-material/Check";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import { useAccommodations } from "../../../widgets/accommodations/BaseAccommodationsRoute";
|
||||
import { useCreateAccommodationCalendarURL } from "../../../hooks/context_providers/accommodations/CreateAccommodationCalendarURLDialogProvider";
|
||||
import { AccommodationsCalendarURLApi } from "../../../api/accommodations/AccommodationsCalendarURLApi";
|
||||
|
||||
export function AccommodationsSettingsRoute(): React.ReactElement {
|
||||
return (
|
||||
<>
|
||||
<AccommodationsListCard />
|
||||
<AccommodationsCalURLsCard />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -35,7 +37,6 @@ export function AccommodationsSettingsRoute(): React.ReactElement {
|
||||
function AccommodationsListCard(): React.ReactElement {
|
||||
const loading = useLoadingMessage();
|
||||
const confirm = useConfirm();
|
||||
const alert = useAlert();
|
||||
const snackbar = useSnackbar();
|
||||
|
||||
const family = useFamily();
|
||||
@ -121,7 +122,7 @@ function AccommodationsListCard(): React.ReactElement {
|
||||
};
|
||||
|
||||
return (
|
||||
<FamilyCard error={error} success={success}>
|
||||
<FamilyCard error={error} success={success} style={{ width: "350px" }}>
|
||||
<CardContent>
|
||||
<Typography gutterBottom variant="h5" component="div">
|
||||
Logements
|
||||
@ -150,7 +151,7 @@ function AccommodationsListCard(): React.ReactElement {
|
||||
onClick={createAccommodation}
|
||||
size={"large"}
|
||||
>
|
||||
Ajouter un nouveau logement
|
||||
Ajouter un logement
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
@ -213,3 +214,66 @@ function BoolIcon(p: { checked?: boolean }): React.ReactElement {
|
||||
<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";
|
||||
|
||||
export function FamilyCard(
|
||||
p: PropsWithChildren<{ error?: string; success?: string }>
|
||||
p: PropsWithChildren<{
|
||||
error?: string;
|
||||
success?: string;
|
||||
style?: React.CSSProperties | undefined;
|
||||
}>
|
||||
): React.ReactElement {
|
||||
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.success && <Alert severity="success">{p.success}</Alert>}
|
||||
|
||||
|
@ -4,9 +4,10 @@ import {
|
||||
AccommodationListApi,
|
||||
AccommodationsList,
|
||||
} 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 { useFamily } from "../BaseFamilyRoute";
|
||||
import { UpdateAccommodationDialogProvider } from "../../hooks/context_providers/accommodations/UpdateAccommodationDialogProvider";
|
||||
|
||||
interface AccommodationsContext {
|
||||
accommodations: AccommodationsList;
|
||||
@ -61,7 +62,9 @@ export function BaseAccommodationsRoute(): React.ReactElement {
|
||||
}}
|
||||
>
|
||||
<UpdateAccommodationDialogProvider>
|
||||
<Outlet />
|
||||
<CreateAccommodationCalendarURLDialogProvider>
|
||||
<Outlet />
|
||||
</CreateAccommodationCalendarURLDialogProvider>
|
||||
</UpdateAccommodationDialogProvider>
|
||||
</AccommodationsContextK.Provider>
|
||||
);
|
||||
|
@ -2,7 +2,7 @@ import { FormControl, InputLabel, MenuItem, Select } from "@mui/material";
|
||||
import { PropEdit } from "./PropEdit";
|
||||
|
||||
export interface SelectOption {
|
||||
value: string;
|
||||
value: string | undefined;
|
||||
label: string;
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ export function PropSelect(p: {
|
||||
const value = p.options.find((o) => o.value === p.value)?.label;
|
||||
return <PropEdit label={p.label} editable={p.editing} value={value} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<FormControl fullWidth variant="filled" style={{ marginBottom: "15px" }}>
|
||||
<InputLabel>{p.label}</InputLabel>
|
||||
|
Loading…
Reference in New Issue
Block a user