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 191 additions and 3 deletions
Showing only changes of commit 572117745a - Show all commits

View File

@ -33,6 +33,7 @@
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-easy-crop": "^5.0.7", "react-easy-crop": "^5.0.7",
"react-qr-code": "^2.0.14",
"react-router-dom": "^6.23.1", "react-router-dom": "^6.23.1",
"react-zoom-pan-pinch": "^3.4.4", "react-zoom-pan-pinch": "^3.4.4",
"svg2pdf.js": "^2.2.3", "svg2pdf.js": "^2.2.3",
@ -3483,6 +3484,11 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
}, },
"node_modules/qr.js": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz",
"integrity": "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ=="
},
"node_modules/raf": { "node_modules/raf": {
"version": "3.4.1", "version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
@ -3533,6 +3539,24 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="
}, },
"node_modules/react-qr-code": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.14.tgz",
"integrity": "sha512-xvAUqmXzFzf7X6aQAAKb6T02YYk9grBBFeqpp1MiVhUAKG3Rg9+hFiOKRYg4+rWc2MiXNxkri0ulAJgS12xh7Q==",
"dependencies": {
"prop-types": "^15.8.1",
"qr.js": "0.0.0"
},
"peerDependencies": {
"react": "*",
"react-native-svg": "*"
},
"peerDependenciesMeta": {
"react-native-svg": {
"optional": true
}
}
},
"node_modules/react-refresh": { "node_modules/react-refresh": {
"version": "0.14.2", "version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",

View File

@ -29,6 +29,7 @@
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-easy-crop": "^5.0.7", "react-easy-crop": "^5.0.7",
"react-qr-code": "^2.0.14",
"react-router-dom": "^6.23.1", "react-router-dom": "^6.23.1",
"react-zoom-pan-pinch": "^3.4.4", "react-zoom-pan-pinch": "^3.4.4",
"svg2pdf.js": "^2.2.3", "svg2pdf.js": "^2.2.3",

View File

@ -33,4 +33,11 @@ export class AccommodationsCalendarURLApi {
}) })
).data; ).data;
} }
/**
* Get accommodation calendar URL route
*/
static CalendarURL(c: AccommodationCalendarURL): string {
return `${APIClient.backendURL()}/acccommodations_calendar/${c.token}`;
}
} }

View File

@ -0,0 +1,76 @@
import {
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
Button,
Typography,
FormControl,
IconButton,
InputAdornment,
InputLabel,
OutlinedInput,
} from "@mui/material";
import {
AccommodationCalendarURL,
AccommodationsCalendarURLApi,
} from "../../api/accommodations/AccommodationsCalendarURLApi";
import { VisibilityOff, Visibility } from "@mui/icons-material";
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import { CopyToClipboard } from "../../widgets/CopyToClipboard";
import QRCode from "react-qr-code";
export function InstallCalendarDialog(p: {
cal?: AccommodationCalendarURL;
onClose: () => void;
}): React.ReactElement {
if (!p.cal) return <></>;
return (
<Dialog open={true} onClose={p.onClose}>
<DialogTitle>Installation du calendrier</DialogTitle>
<DialogContent>
<DialogContentText>
<Typography>
Afin d'installer le calendrier <i>{p.cal.name}</i> sur votre
appareil, veuillez utiliser l'URL suivante :
</Typography>
<br />
<FormControl fullWidth variant="outlined">
<InputLabel>URL</InputLabel>
<OutlinedInput
value={AccommodationsCalendarURLApi.CalendarURL(p.cal!)}
endAdornment={
<InputAdornment position="end">
<CopyToClipboard
content={AccommodationsCalendarURLApi.CalendarURL(p.cal!)}
>
<IconButton>
<ContentCopyIcon />
</IconButton>
</CopyToClipboard>
</InputAdornment>
}
label="Password"
/>
<div
style={{
margin: "20px auto",
padding: "20px",
backgroundColor: "white",
}}
>
<QRCode
value={AccommodationsCalendarURLApi.CalendarURL(p.cal!)}
/>
</div>
</FormControl>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={p.onClose}>Fermer</Button>
</DialogActions>
</Dialog>
);
}

View File

@ -0,0 +1,44 @@
import React, { PropsWithChildren } from "react";
import { AccommodationCalendarURL } from "../../../api/accommodations/AccommodationsCalendarURLApi";
import { InstallCalendarDialog } from "../../../dialogs/accommodations/InstallCalendarDialog";
type DialogContext = (cal: AccommodationCalendarURL) => Promise<void>;
const DialogContextK = React.createContext<DialogContext | null>(null);
export function InstallCalendarDialogProvider(
p: PropsWithChildren
): React.ReactElement {
const [cal, setCal] = React.useState<AccommodationCalendarURL | undefined>();
const cb = React.useRef<null | (() => void)>(null);
const handleClose = () => {
setCal(undefined);
if (cb.current !== null) cb.current();
cb.current = null;
};
const hook: DialogContext = (c) => {
setCal(c);
return new Promise((res) => {
cb.current = res;
});
};
return (
<>
<DialogContextK.Provider value={hook}>
{p.children}
</DialogContextK.Provider>
{cal && <InstallCalendarDialog cal={cal} onClose={handleClose} />}
</>
);
}
export function useInstallCalendarDialog(): DialogContext {
return React.useContext(DialogContextK)!;
}

View File

@ -24,6 +24,8 @@ import { TimeWidget } from "../../../widgets/TimeWidget";
import { useAccommodations } from "../../../widgets/accommodations/BaseAccommodationsRoute"; import { useAccommodations } from "../../../widgets/accommodations/BaseAccommodationsRoute";
import { useCreateAccommodationCalendarURL } from "../../../hooks/context_providers/accommodations/CreateAccommodationCalendarURLDialogProvider"; import { useCreateAccommodationCalendarURL } from "../../../hooks/context_providers/accommodations/CreateAccommodationCalendarURLDialogProvider";
import { AccommodationsCalendarURLApi } from "../../../api/accommodations/AccommodationsCalendarURLApi"; import { AccommodationsCalendarURLApi } from "../../../api/accommodations/AccommodationsCalendarURLApi";
import { useInstallCalendarDialog } from "../../../hooks/context_providers/accommodations/InstallCalendarDialogProvider";
import { InstallCalendarDialog } from "../../../dialogs/accommodations/InstallCalendarDialog";
export function AccommodationsSettingsRoute(): React.ReactElement { export function AccommodationsSettingsRoute(): React.ReactElement {
return ( return (
@ -224,6 +226,7 @@ function AccommodationsCalURLsCard(): React.ReactElement {
const family = useFamily(); const family = useFamily();
const createCalendarURLDialog = useCreateAccommodationCalendarURL(); const createCalendarURLDialog = useCreateAccommodationCalendarURL();
const calendarURLDialog = useInstallCalendarDialog();
const createCalendarURL = async () => { const createCalendarURL = async () => {
try { try {
@ -241,8 +244,8 @@ function AccommodationsCalURLsCard(): React.ReactElement {
setSuccess("Le calendrier a été créé avec succès !"); setSuccess("Le calendrier a été créé avec succès !");
// TODO : reload URLS list // TODO : reload URLS list
// TODO : show QrCode dialog
console.log(cal); calendarURLDialog(cal);
} catch (e) { } catch (e) {
console.error("Failed to create new accommodation calendar URL!", e); console.error("Failed to create new accommodation calendar URL!", e);
setError(`Échec de la création du calendrier! ${e}`); setError(`Échec de la création du calendrier! ${e}`);

View File

@ -0,0 +1,30 @@
import { ButtonBase } from "@mui/material";
import { PropsWithChildren } from "react";
import { useSnackbar } from "../hooks/context_providers/SnackbarProvider";
export function CopyToClipboard(
p: PropsWithChildren<{ content: string }>
): React.ReactElement {
const snackbar = useSnackbar();
const copy = () => {
navigator.clipboard.writeText(p.content);
snackbar(`${p.content} copied to clipboard.`);
};
return (
<ButtonBase
onClick={copy}
style={{
display: "inline-block",
alignItems: "unset",
textAlign: "unset",
position: "relative",
padding: "0px",
}}
disableRipple
>
{p.children}
</ButtonBase>
);
}

View File

@ -8,6 +8,7 @@ import { CreateAccommodationCalendarURLDialogProvider } from "../../hooks/contex
import { UpdateAccommodationDialogProvider } from "../../hooks/context_providers/accommodations/UpdateAccommodationDialogProvider"; 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 { InstallCalendarDialogProvider } from "../../hooks/context_providers/accommodations/InstallCalendarDialogProvider";
interface AccommodationsContext { interface AccommodationsContext {
accommodations: AccommodationsList; accommodations: AccommodationsList;
@ -63,7 +64,9 @@ export function BaseAccommodationsRoute(): React.ReactElement {
> >
<UpdateAccommodationDialogProvider> <UpdateAccommodationDialogProvider>
<CreateAccommodationCalendarURLDialogProvider> <CreateAccommodationCalendarURLDialogProvider>
<InstallCalendarDialogProvider>
<Outlet /> <Outlet />
</InstallCalendarDialogProvider>
</CreateAccommodationCalendarURLDialogProvider> </CreateAccommodationCalendarURLDialogProvider>
</UpdateAccommodationDialogProvider> </UpdateAccommodationDialogProvider>
</AccommodationsContextK.Provider> </AccommodationsContextK.Provider>