38 Commits

Author SHA1 Message Date
3a218cd3fb Minor fixes
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-10 22:03:18 +02:00
7d64ea219f Can create accommodation from WebUI 2024-06-10 22:00:30 +02:00
f83cbe1386 Start to create accommodations UI
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-06-10 21:20:26 +02:00
9600acab0f Merge branch 'master' into accomodation_module
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-06-10 18:17:17 +00:00
3620587b3b Merge branch 'master' into accomodation_module
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-06-06 13:16:53 +00:00
df6a9e8292 Can generate calendars
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-06-03 22:47:39 +02:00
e8b1b91202 Merge branch 'master' into accomodation_module
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-06-03 19:23:42 +00:00
0f0b5978b6 Fix unused warning
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-31 23:07:56 +02:00
7626d91ece Remove comment
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2024-05-31 22:35:22 +02:00
e62f536c03 Can delete a reservation 2024-05-31 22:31:23 +02:00
51f8aaccb6 Can get accommodations reservations of a family 2024-05-31 22:09:33 +02:00
b34959df33 Can create calendars
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-31 21:41:58 +02:00
e86c80310d Merge branch 'master' into accomodation_module
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-31 06:22:22 +00:00
09633458a0 Merge branch 'master' into accomodation_module
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-30 19:59:31 +00:00
2346c90be8 Add new table to define iCal calendars URLs
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-30 21:59:00 +02:00
9f72cd9b9c Fix bad assumption
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-30 21:46:55 +02:00
5075e8843b Can validate or reject reservation
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-30 21:43:27 +02:00
238e17aed6 Merge branch 'master' into accomodation_module
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-30 17:59:00 +00:00
82dbf11b42 Fix typo
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-30 19:56:35 +02:00
f07a6a8923 Can change a reservation time 2024-05-30 19:54:21 +02:00
33b03a4d74 Can delete a reservation
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-29 22:28:38 +02:00
70d8020610 Prohibit similar start time and end time
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-29 22:18:35 +02:00
5f25a516e9 Can create reservation requests 2024-05-29 22:16:50 +02:00
829c2e8df4 Merge branch 'master' into accomodation_module
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-29 19:55:37 +00:00
5b9d82889c Update TODO list
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-28 22:14:34 +02:00
3efae7bfff Can get single accommodation reservation information
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-28 22:12:17 +02:00
1332b001c8 Can get all the reservations of a given accommodation
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-28 21:43:24 +02:00
6e4b6a0499 Merge branch 'master' into accomodation_module
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-28 19:30:53 +00:00
936b095d46 Can get the full list of accommodation reservations for a family
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-25 08:48:13 +02:00
d0d1169c7d Can update accommodation information
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-25 07:57:50 +02:00
9a4da0462a Get single accommodation information
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-25 07:54:34 +02:00
49f3677081 Get full list of accommodations
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-25 07:50:11 +02:00
16bc7eca6f Merge branch 'master' into accomodation_module
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-24 13:13:37 +00:00
bc800e7cf6 Only an admin can delete an accommodation
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-23 21:34:28 +02:00
18582fdff7 Can delete an accommodation 2024-05-23 21:30:51 +02:00
c4fadce69f Can create new accommodations using the API
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-23 21:20:14 +02:00
2f1df6c117 Can toggle accommodations module
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-23 19:28:29 +02:00
32d3793025 Update database structure
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-23 18:45:56 +02:00
36 changed files with 1950 additions and 30 deletions

View File

@@ -16,29 +16,31 @@ import { NewAccountRoute } from "./routes/auth/NewAccountRoute";
import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute";
import { PasswordForgottenRoute } from "./routes/auth/PasswordForgottenRoute";
import { ResetPasswordRoute } from "./routes/auth/ResetPasswordRoute";
import { FamilyHomeRoute } from "./routes/family/genealogy/FamilyHomeRoute";
import {
FamilyCreateMemberRoute,
FamilyEditMemberRoute,
FamilyMemberRoute,
} from "./routes/family/genealogy/FamilyMemberRoute";
import { FamilySettingsRoute } from "./routes/family/FamilySettingsRoute";
import { FamilyUsersListRoute } from "./routes/family/FamilyUsersListRoute";
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
import { BaseFamilyRoute } from "./widgets/BaseFamilyRoute";
import { BaseLoginPage } from "./widgets/BaseLoginpage";
import { FamilyMembersListRoute } from "./routes/family/genealogy/FamilyMembersListRoute";
import { AccommodationsSettingsRoute } from "./routes/family/accommodations/AccommodationsSettingsRoute";
import {
FamilyCoupleRoute,
FamilyCreateCoupleRoute,
FamilyEditCoupleRoute,
} from "./routes/family/genealogy/FamilyCoupleRoute";
import { FamilyCouplesListRoute } from "./routes/family/genealogy/FamilyCouplesListRoute";
import { FamilyTreeRoute } from "./routes/family/genealogy/FamilyTreeRoute";
import { FamilyHomeRoute } from "./routes/family/genealogy/FamilyHomeRoute";
import {
FamilyCreateMemberRoute,
FamilyEditMemberRoute,
FamilyMemberRoute,
} from "./routes/family/genealogy/FamilyMemberRoute";
import { FamilyMemberTreeRoute } from "./routes/family/genealogy/FamilyMemberTreeRoute";
import { GenealogyHomeRoute } from "./routes/family/genealogy/GenealogyHomeRoute";
import { BaseGenealogyRoute } from "./widgets/genealogy/BaseGenealogyRoute";
import { FamilyMembersListRoute } from "./routes/family/genealogy/FamilyMembersListRoute";
import { FamilyTreeRoute } from "./routes/family/genealogy/FamilyTreeRoute";
import { GenalogySettingsRoute } from "./routes/family/genealogy/GenalogySettingsRoute";
import { GenealogyHomeRoute } from "./routes/family/genealogy/GenealogyHomeRoute";
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
import { BaseFamilyRoute } from "./widgets/BaseFamilyRoute";
import { BaseLoginPage } from "./widgets/BaseLoginpage";
import { BaseAccommodationsRoute } from "./widgets/accommodations/BaseAccommodationsRoute";
import { BaseGenealogyRoute } from "./widgets/genealogy/BaseGenealogyRoute";
interface AuthContext {
signedIn: boolean;
@@ -110,6 +112,17 @@ export function App(): React.ReactElement {
<Route path="*" element={<NotFoundRoute />} />
</Route>
<Route
path="accommodations/*"
element={<BaseAccommodationsRoute />}
>
<Route
path="settings"
element={<AccommodationsSettingsRoute />}
/>
<Route path="*" element={<NotFoundRoute />} />
</Route>
<Route path="settings" element={<FamilySettingsRoute />} />
<Route path="users" element={<FamilyUsersListRoute />} />
<Route path="*" element={<NotFoundRoute />} />

View File

@@ -88,10 +88,12 @@ export class Family implements FamilyAPI {
export class ExtendedFamilyInfo extends Family {
public disable_couple_photos: boolean;
public enable_genealogy: boolean;
public enable_accommodations: boolean;
constructor(p: any) {
super(p);
this.disable_couple_photos = p.disable_couple_photos;
this.enable_genealogy = p.enable_genealogy;
this.enable_accommodations = p.enable_accommodations;
}
}
@@ -235,6 +237,7 @@ export class FamilyApi {
id: number;
name?: string;
enable_genealogy?: boolean;
enable_accommodations?: boolean;
disable_couple_photos?: boolean;
}): Promise<void> {
await APIClient.exec({
@@ -243,6 +246,7 @@ export class FamilyApi {
jsonData: {
name: settings.name,
enable_genealogy: settings.enable_genealogy,
enable_accommodations: settings.enable_accommodations,
disable_couple_photos: settings.disable_couple_photos,
},
});

View File

@@ -32,6 +32,8 @@ interface Constraints {
member_country: LenConstraint;
member_sex: LenConstraint;
member_note: LenConstraint;
accommodation_name_len: LenConstraint;
accommodation_description_len: LenConstraint;
}
interface OIDCProvider {

View File

@@ -0,0 +1,90 @@
import { APIClient } from "../ApiClient";
import { Family } from "../FamilyApi";
export interface Accommodation {
id: number;
family_id: number;
time_create: number;
time_update: number;
name: string;
need_validation: boolean;
description: string;
open_to_reservations: boolean;
}
export class AccommodationsList {
private list: Accommodation[];
private map: Map<number, Accommodation>;
constructor(list: Accommodation[]) {
this.list = list;
this.map = new Map();
for (const m of list) {
this.map.set(m.id, m);
}
this.list.sort((a, b) =>
a.name.toLowerCase().localeCompare(b.name.toLocaleLowerCase())
);
}
public get isEmpty(): boolean {
return this.list.length === 0;
}
public get size(): number {
return this.list.length;
}
public get fullList(): Accommodation[] {
return this.list;
}
filter(predicate: (m: Accommodation) => boolean): Accommodation[] {
return this.list.filter(predicate);
}
get(id: number): Accommodation | undefined {
return this.map.get(id);
}
}
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
*/
static async GetListOfFamily(family: Family): Promise<AccommodationsList> {
const data = (
await APIClient.exec({
method: "GET",
uri: `/family/${family.family_id}/accommodations/list/list`,
})
).data;
return new AccommodationsList(data);
}
/**
* Create a new accommodation
*/
static async Create(
family: Family,
accommodation: UpdateAccommodation
): Promise<Accommodation> {
return (
await APIClient.exec({
method: "POST",
uri: `/family/${family.family_id}/accommodations/list/create`,
jsonData: accommodation,
})
).data;
}
}

View File

@@ -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 (
<Dialog open={p.open} onClose={cancel}>
<DialogTitle>
{p.create ? "Création" : "Mise à jour"} d'un logement
</DialogTitle>
<DialogContent style={{ display: "flex", flexDirection: "column" }}>
<PropEdit
editable
label="Nom"
value={accommodation?.name}
onValueChange={(s) =>
setAccommodation((a) => {
return {
...a!,
name: s!,
};
})
}
size={ServerApi.Config.constraints.accommodation_name_len}
helperText={nameErr}
/>
<PropEdit
editable
label="Description"
value={accommodation?.description}
onValueChange={(s) =>
setAccommodation((a) => {
return {
...a!,
description: s!,
};
})
}
size={ServerApi.Config.constraints.accommodation_description_len}
helperText={descriptionErr}
/>
<PropCheckbox
editable
label="Ouvert aux réservations"
checked={accommodation?.open_to_reservations === true}
onValueChange={(c) =>
setAccommodation((a) => {
return {
...a!,
open_to_reservations: c,
};
})
}
/>
<Tooltip
title={
"Permet de spécifier si un administrateur de la famille doit valider manuellement les demandes de réservation pour qu'elles soient validées"
}
>
<PropCheckbox
checkboxAlwaysVisible
editable={accommodation?.open_to_reservations === true}
label="Validation des réservations par un administrateur requise"
checked={accommodation?.need_validation === true}
onValueChange={(c) =>
setAccommodation((a) => {
return {
...a!,
need_validation: c,
};
})
}
/>
</Tooltip>
</DialogContent>
<DialogActions>
<Button onClick={cancel}>Annuler</Button>
<Button
onClick={submit}
disabled={
!!nameErr || (!!accommodation?.description && !!descriptionErr)
}
>
{p.create ? "Créer" : "Mettre à jour"}
</Button>
</DialogActions>
</Dialog>
);
}

View File

@@ -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<UpdateAccommodation | undefined>;
const DialogContextK = React.createContext<DialogContext | null>(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 (
<>
<DialogContextK.Provider value={hook}>
{p.children}
</DialogContextK.Provider>
{open && (
<UpdateAccommodationDialog
open={open}
accommodation={accommodation}
create={create}
onClose={handleClose}
onSubmitted={handleClose}
/>
)}
</>
);
}
export function useUpdateAccommodation(): DialogContext {
return React.useContext(DialogContextK)!;
}

View File

@@ -71,6 +71,9 @@ function FamilySettingsCard(): React.ReactElement {
const [enableGenealogy, setEnableGenealogy] = React.useState(
family.family.enable_genealogy
);
const [enableAccommodations, setEnableAccommodations] = React.useState(
family.family.enable_accommodations
);
const canEdit = family.family.is_admin;
@@ -86,6 +89,7 @@ function FamilySettingsCard(): React.ReactElement {
id: family.family.family_id,
name: newName,
enable_genealogy: enableGenealogy,
enable_accommodations: enableAccommodations,
});
family.reloadFamilyInfo();
@@ -118,14 +122,12 @@ function FamilySettingsCard(): React.ReactElement {
label="Identifiant"
value={family.family.family_id}
/>
<TextField
disabled
fullWidth
label="Création de la famille"
value={formatDate(family.family.time_create)}
/>
<TextField
fullWidth
label="Nom de la famille"
@@ -136,7 +138,6 @@ function FamilySettingsCard(): React.ReactElement {
maxLength: ServerApi.Config.constraints.family_name_len.max,
}}
/>
<FormControlLabel
disabled={!canEdit}
control={
@@ -147,6 +148,16 @@ function FamilySettingsCard(): React.ReactElement {
}
label="Activer le module de généalogie"
/>
<FormControlLabel
disabled={!canEdit}
control={
<Switch
checked={enableAccommodations}
onChange={(_e, c) => setEnableAccommodations(c)}
/>
}
label="Activer le module de réservation de logements"
/>
</Box>
</CardContent>
<CardActions>

View File

@@ -0,0 +1,84 @@
import { CardContent, Typography, Alert, Button } 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 { AccommodationListApi } from "../../../api/accommodations/AccommodationListApi";
import { useSnackbar } from "../../../hooks/context_providers/SnackbarProvider";
import { useAccommodations } from "../../../widgets/accommodations/BaseAccommodationsRoute";
export function AccommodationsSettingsRoute(): React.ReactElement {
return (
<>
<AccommodationsListCard />
</>
);
}
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<string>();
const [success, setSuccess] = React.useState<string>();
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(`Échec de la création du logement! ${e}`);
} finally {
loading.hide();
}
};
return (
<FamilyCard error={error} success={success}>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
Logements
</Typography>
<Button
startIcon={<AddIcon />}
variant="outlined"
color="info"
fullWidth
onClick={createAccommodation}
disabled={!family.family.is_admin}
size={"large"}
>
Ajouter un nouveau logement
</Button>
</CardContent>
</FamilyCard>
);
}

View File

@@ -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;
}

View File

@@ -5,6 +5,7 @@ import {
mdiCrowd,
mdiFamilyTree,
mdiFileTree,
mdiHomeGroup,
mdiHumanMaleFemale,
mdiLockCheck,
mdiPlus,
@@ -207,6 +208,14 @@ export function BaseFamilyRoute(): React.ReactElement {
/>
)}
{family?.enable_accommodations && (
<FamilyLink
icon={<Icon path={mdiHomeGroup} size={1} />}
label="Logements"
uri="accommodations/settings"
/>
)}
{/* Invitation code */}
<ListItem

View File

@@ -0,0 +1,75 @@
import React from "react";
import { Outlet } from "react-router-dom";
import {
AccommodationListApi,
AccommodationsList,
} from "../../api/accommodations/AccommodationListApi";
import { AsyncWidget } from "../AsyncWidget";
import { useFamily } from "../BaseFamilyRoute";
import { UpdateAccommodationDialogProvider } from "../../hooks/context_providers/accommodations/UpdateAccommodationDialogProvider";
interface AccommodationsContext {
accommodations: AccommodationsList;
reloadAccommodationsList: () => Promise<void>;
}
const AccommodationsContextK =
React.createContext<AccommodationsContext | null>(null);
export function BaseAccommodationsRoute(): React.ReactElement {
const family = useFamily();
const [accommodations, setAccommodations] =
React.useState<null | AccommodationsList>(null);
const loadKey = React.useRef(1);
const loadPromise = React.useRef<() => void>();
const load = async () => {
setAccommodations(
await AccommodationListApi.GetListOfFamily(family.family)
);
};
const onReload = async () => {
loadKey.current += 1;
setAccommodations(null);
return new Promise<void>((res, _rej) => {
loadPromise.current = () => res();
});
};
return (
<AsyncWidget
ready={accommodations !== null}
loadKey={`${family.familyId}-${loadKey.current}`}
load={load}
errMsg="Échec du chargement des informations sur les logements de la famille !"
build={() => {
if (loadPromise.current != null) {
loadPromise.current?.();
loadPromise.current = undefined;
}
return (
<AccommodationsContextK.Provider
value={{
accommodations: accommodations!,
reloadAccommodationsList: onReload,
}}
>
<UpdateAccommodationDialogProvider>
<Outlet />
</UpdateAccommodationDialogProvider>
</AccommodationsContextK.Provider>
);
}}
/>
);
}
export function useAccommodations(): AccommodationsContext {
return React.useContext(AccommodationsContextK)!;
}

View File

@@ -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 <Typography variant="body2">{p.label}</Typography>;
if (!p.checkboxAlwaysVisible) {
if (!p.editable && p.checked)
return <Typography variant="body2">{p.label}</Typography>;
if (!p.editable) return <></>;
if (!p.editable) return <></>;
}
return (
<FormControlLabel
control={
<Checkbox
disabled={!p.editable}
checked={p.checked}
onChange={(e) => p.onValueChange(e.target.checked)}
/>

View File

@@ -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}
/>
);
}

View File

@@ -5,14 +5,14 @@ import { MemberApi, MembersList } from "../../api/genealogy/MemberApi";
import { AsyncWidget } from "../AsyncWidget";
import { useFamily } from "../BaseFamilyRoute";
interface FamilyContext {
interface GenealogyContext {
members: MembersList;
couples: CouplesList;
reloadMembersList: () => Promise<void>;
reloadCouplesList: () => Promise<void>;
}
const GenealogyContextK = React.createContext<FamilyContext | null>(null);
const GenealogyContextK = React.createContext<GenealogyContext | null>(null);
export function BaseGenealogyRoute(): React.ReactElement {
const family = useFamily();
@@ -68,6 +68,6 @@ export function BaseGenealogyRoute(): React.ReactElement {
);
}
export function useGenealogy(): FamilyContext {
export function useGenealogy(): GenealogyContext {
return React.useContext(GenealogyContextK)!;
}

View File

@@ -728,8 +728,10 @@ checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-targets 0.52.5",
]
@@ -1418,12 +1420,14 @@ dependencies = [
"anyhow",
"base64 0.22.1",
"bcrypt",
"chrono",
"clap",
"diesel",
"diesel_migrations",
"env_logger",
"futures-util",
"httpdate",
"ical",
"image",
"lazy_static",
"lettre",
@@ -1777,6 +1781,15 @@ dependencies = [
"cc",
]
[[package]]
name = "ical"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b7cab7543a8b7729a19e2c04309f902861293dcdae6558dfbeb634454d279f6"
dependencies = [
"thiserror",
]
[[package]]
name = "ident_case"
version = "1.0.1"

View File

@@ -38,3 +38,5 @@ zip = "2.0.0"
mime_guess = "2.0.4"
tempfile = "3.10.1"
base64 = "0.22.0"
ical = { version = "0.11.0", features = ["generator", "ical", "vcard"] }
chrono = "0.4.38"

View File

@@ -0,0 +1,6 @@
ALTER TABLE public.families
DROP COLUMN enable_accommodations;
DROP TABLE IF EXISTS accommodations_reservations_cals_urls;
DROP TABLE IF EXISTS accommodations_reservations;
DROP TABLE IF EXISTS accommodations_list;

View File

@@ -0,0 +1,51 @@
-- Add column to toggle accommodations module
ALTER TABLE public.families
ADD enable_accommodations boolean NOT NULL DEFAULT false;
COMMENT
ON COLUMN public.families.enable_accommodations IS 'Specify whether accommodations feature is enabled for the family';
-- Create tables
CREATE TABLE IF NOT EXISTS accommodations_list
(
id SERIAL PRIMARY KEY,
family_id integer NOT NULL REFERENCES families,
time_create BIGINT NOT NULL,
time_update BIGINT NOT NULL,
name VARCHAR(50) NOT NULL,
need_validation BOOLEAN NOT NULL DEFAULT true,
description text NULL,
open_to_reservations BOOLEAN NOT NULL DEFAULT false
);
COMMENT ON COLUMN accommodations_list.need_validation is 'true if family admin review is required for validation. False otherwise';
COMMENT ON COLUMN accommodations_list.open_to_reservations is 'true if reservations can be created / updated. False otherwise';
CREATE TABLE IF NOT EXISTS accommodations_reservations
(
id SERIAL PRIMARY KEY,
family_id integer NOT NULL REFERENCES families ON DELETE CASCADE,
accommodation_id integer NOT NULL REFERENCES accommodations_list ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
time_create BIGINT NOT NULL,
time_update BIGINT NOT NULL,
reservation_start BIGINT NOT NULL,
reservation_end BIGINT NOT NULL,
validated BOOLEAN NULL
);
COMMENT ON COLUMN accommodations_reservations.validated is 'null if not reviewed yet. true if reservation is accepted. false if reservation is rejected';
CREATE TABLE IF NOT EXISTS accommodations_reservations_cals_urls
(
id SERIAL PRIMARY KEY,
family_id integer NOT NULL REFERENCES families ON DELETE CASCADE,
accommodation_id integer NULL REFERENCES accommodations_list ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
name VARCHAR(50) NOT NULL,
token VARCHAR(50) NOT NULL,
time_create BIGINT NOT NULL,
time_used BIGINT NOT NULL
);
COMMENT ON COLUMN accommodations_reservations_cals_urls.accommodation_id is 'null to get reservations of all accommodations. otherwise get the reservations of the specified accommodation only';

View File

@@ -60,6 +60,10 @@ pub struct StaticConstraints {
pub member_country: SizeConstraint,
pub member_sex: SizeConstraint,
pub member_note: SizeConstraint,
pub accommodation_name_len: SizeConstraint,
pub accommodation_description_len: SizeConstraint,
pub accommodation_calendar_name_len: SizeConstraint,
}
impl Default for StaticConstraints {
@@ -91,6 +95,10 @@ impl Default for StaticConstraints {
member_country: SizeConstraint::new(0, 2),
member_sex: SizeConstraint::new(0, 1),
member_note: SizeConstraint::new(0, 35000),
accommodation_name_len: SizeConstraint::new(1, 50),
accommodation_description_len: SizeConstraint::new(0, 500),
accommodation_calendar_name_len: SizeConstraint::new(2, 50),
}
}
}
@@ -134,3 +142,6 @@ pub const THUMB_WIDTH: u32 = 350;
/// Thumbnail height
pub const THUMB_HEIGHT: u32 = 350;
/// Accommodations reservations calendars tokens len
pub const ACCOMMODATIONS_RESERVATIONS_CALENDARS_TOKENS_LEN: usize = 50;

View File

@@ -0,0 +1,105 @@
use crate::constants::StaticConstraints;
use crate::controllers::HttpResult;
use crate::extractors::accommodation_extractor::FamilyAndAccommodationInPath;
use crate::extractors::family_extractor::{FamilyInPath, FamilyInPathWithAdminMembership};
use crate::models::Accommodation;
use crate::services::accommodations_list_service;
use actix_web::{web, HttpResponse};
#[derive(thiserror::Error, Debug)]
enum AccommodationListControllerErr {
#[error("Malformed name!")]
MalformedName,
#[error("Malformed description!")]
MalformedDescription,
}
#[derive(serde::Deserialize, Clone)]
pub struct AccommodationRequest {
pub name: String,
pub need_validation: bool,
pub description: Option<String>,
pub open_to_reservations: bool,
}
impl AccommodationRequest {
pub async fn to_accommodation(self, accommodation: &mut Accommodation) -> anyhow::Result<()> {
let c = StaticConstraints::default();
if !c.accommodation_name_len.validate(&self.name) {
return Err(AccommodationListControllerErr::MalformedName.into());
}
accommodation.name = self.name;
if let Some(d) = &self.description {
if !c.accommodation_description_len.validate(d) {
return Err(AccommodationListControllerErr::MalformedDescription.into());
}
}
accommodation.description.clone_from(&self.description);
accommodation.need_validation = self.need_validation;
accommodation.open_to_reservations = self.open_to_reservations;
Ok(())
}
}
/// Create a new accommodation
pub async fn create(
m: FamilyInPathWithAdminMembership,
req: web::Json<AccommodationRequest>,
) -> HttpResult {
let mut accommodation = accommodations_list_service::create(m.family_id()).await?;
if let Err(e) = req.0.to_accommodation(&mut accommodation).await {
log::error!("Failed to apply accommodation information! {e}");
accommodations_list_service::delete(&mut accommodation).await?;
return Ok(HttpResponse::BadRequest().body(e.to_string()));
}
if let Err(e) = accommodations_list_service::update(&mut accommodation).await {
log::error!("Failed to update accommodation information! {e}");
accommodations_list_service::delete(&mut accommodation).await?;
return Ok(HttpResponse::InternalServerError().finish());
}
Ok(HttpResponse::Ok().json(accommodation))
}
/// Get the full list of accommodations
pub async fn get_full_list(m: FamilyInPath) -> HttpResult {
Ok(HttpResponse::Ok()
.json(accommodations_list_service::get_all_of_family(m.family_id()).await?))
}
/// Get the information of a single accommodation
pub async fn get_single(m: FamilyAndAccommodationInPath) -> HttpResult {
Ok(HttpResponse::Ok().json(&m.to_accommodation()))
}
/// Update an accommodation
pub async fn update(
m: FamilyAndAccommodationInPath,
req: web::Json<AccommodationRequest>,
_admin: FamilyInPathWithAdminMembership,
) -> HttpResult {
let mut accommodation = m.to_accommodation();
if let Err(e) = req.0.to_accommodation(&mut accommodation).await {
log::error!("Failed to parse accommodation information! {e}");
return Ok(HttpResponse::BadRequest().body(e.to_string()));
}
accommodations_list_service::update(&mut accommodation).await?;
Ok(HttpResponse::Accepted().finish())
}
/// Delete an accommodation
pub async fn delete(
m: FamilyAndAccommodationInPath,
_admin: FamilyInPathWithAdminMembership,
) -> HttpResult {
accommodations_list_service::delete(&mut m.to_accommodation()).await?;
Ok(HttpResponse::Ok().finish())
}

View File

@@ -0,0 +1,157 @@
use ical::{generator::*, *};
use actix_web::{web, HttpResponse};
use chrono::DateTime;
use crate::constants::StaticConstraints;
use crate::controllers::HttpResult;
use crate::extractors::accommodation_reservation_calendar_extractor::FamilyAndAccommodationReservationCalendarInPath;
use crate::extractors::family_extractor::FamilyInPath;
use crate::models::{AccommodationID, ReservationStatus};
use crate::services::{
accommodations_list_service, accommodations_reservations_calendars_service,
accommodations_reservations_service, families_service,
};
#[derive(serde::Deserialize)]
pub struct CreateCalendarQuery {
accommodation_id: Option<AccommodationID>,
name: String,
}
/// Create a calendar
pub async fn create(a: FamilyInPath, req: web::Json<CreateCalendarQuery>) -> HttpResult {
let accommodation_id = match req.accommodation_id {
Some(i) => {
let accommodation = match accommodations_list_service::get_by_id(i).await {
Ok(a) => a,
Err(e) => {
log::error!("Failed to get accommodation information! {e}");
return Ok(HttpResponse::NotFound()
.json("The accommodation was not found in the family!"));
}
};
if accommodation.family_id() != a.family_id() {
return Ok(
HttpResponse::NotFound().json("The accommodation was not found in the family!")
);
}
Some(accommodation.id())
}
None => None,
};
let conf = StaticConstraints::default();
if !conf.accommodation_calendar_name_len.validate(&req.name) {
return Ok(HttpResponse::BadRequest().json("Invalid accommodation name!"));
}
let calendar = accommodations_reservations_calendars_service::create(
a.user_id(),
a.family_id(),
accommodation_id,
&req.name,
)
.await?;
Ok(HttpResponse::Ok().json(calendar))
}
/// Get the list of calendars of a user
pub async fn get_list(a: FamilyInPath) -> HttpResult {
let users =
accommodations_reservations_calendars_service::get_all_of_user(a.user_id(), a.family_id())
.await?;
Ok(HttpResponse::Ok().json(users))
}
/// Delete a calendar
pub async fn delete(resa: FamilyAndAccommodationReservationCalendarInPath) -> HttpResult {
accommodations_reservations_calendars_service::delete(resa.to_reservation()).await?;
Ok(HttpResponse::Ok().json("Calendar successfully deleted"))
}
fn fmt_date(time: i64) -> String {
let res = DateTime::from_timestamp(time, 0).expect("Failed to parse date");
/*format!(
"{:0>4}{:0>2}{:0>2}T{:0>2}{:0>2}",
res.year(),
res.month(),
res.day(),
res.minute(),
res.second()
)*/
res.format("%Y%m%dT%H%M%S").to_string()
}
#[derive(serde::Deserialize)]
pub struct AnonymousAccessURL {
token: String,
}
/// Get the content of the calendar
pub async fn anonymous_access(req: web::Path<AnonymousAccessURL>) -> HttpResult {
let calendar =
match accommodations_reservations_calendars_service::get_by_token(&req.token).await {
Ok(c) => c,
Err(e) => {
log::error!("Calendar information could not be retrieved: {e}");
return Ok(HttpResponse::NotFound().body("Calendar not found!"));
}
};
let accommodations =
accommodations_list_service::get_all_of_family(calendar.family_id()).await?;
let members = families_service::get_memberships_of_family(calendar.family_id()).await?;
// Get calendar associated events
let events = match calendar.accommodation_id() {
None => {
accommodations_reservations_service::get_all_of_family(calendar.family_id()).await?
}
Some(a) => accommodations_reservations_service::get_all_of_accommodation(a).await?,
};
let mut cal = IcalCalendarBuilder::version("2.0")
.gregorian()
.prodid("-//geneit//")
.build();
for ev in events {
let accommodation = accommodations
.iter()
.find(|a| a.id() == ev.accommodation_id())
.unwrap();
let member_name = members
.iter()
.find(|a| a.membership.user_id() == ev.user_id())
.map(|m| m.user_name.as_str())
.unwrap_or("other user");
let event = IcalEventBuilder::tzid("Europe/Paris")
.uid(format!("resa-{}", ev.id().0))
.changed(fmt_date(ev.time_update))
.start(fmt_date(ev.reservation_start))
.end(fmt_date(ev.reservation_end))
.set(ical_property!("SUMMARY", member_name))
.set(ical_property!("LOCATION", &accommodation.name))
.set(ical_property!(
"STATUS",
match ev.status() {
ReservationStatus::Pending => "TENTATIVE",
ReservationStatus::Accepted => "CONFIRMED",
ReservationStatus::Rejected => "CANCELLED",
}
))
.build();
cal.events.push(event);
}
Ok(HttpResponse::Ok()
.content_type("text/calendar")
.body(cal.generate()))
}

View File

@@ -0,0 +1,198 @@
use crate::controllers::HttpResult;
use crate::extractors::accommodation_extractor::FamilyAndAccommodationInPath;
use crate::extractors::accommodation_reservation_extractor::FamilyAndAccommodationReservationInPath;
use crate::extractors::family_extractor::FamilyInPath;
use crate::models::{Accommodation, AccommodationReservationID, NewAccommodationReservation};
use crate::services::accommodations_reservations_service;
use crate::utils::time_utils::time;
use actix_web::{web, HttpResponse};
#[derive(serde::Deserialize)]
pub struct UpdateReservationQuery {
start: usize,
end: usize,
}
impl UpdateReservationQuery {
/// Check whether a reservation request is valid or not
async fn validate(
&self,
a: &Accommodation,
resa_id: Option<AccommodationReservationID>,
) -> anyhow::Result<Option<&str>> {
if !a.open_to_reservations {
return Ok(Some(
"The accommodation is not open to reservations creation / update!",
));
}
if (self.start as i64) < (time() as i64 - 3600 * 24 * 30) {
return Ok(Some("Start time is too far in the past!"));
}
if self.start == self.end {
return Ok(Some("Start and end time must be different!"));
}
if self.start > self.end {
return Ok(Some("End time happens before start time!"));
}
let existing = accommodations_reservations_service::get_reservations_for_time_interval(
a.id(),
self.start,
self.end,
)
.await?;
if existing
.iter()
.any(|r| r.validated != Some(false) && resa_id != Some(r.id()))
{
return Ok(Some("This reservation is in conflict with another one!"));
}
Ok(None)
}
}
/// Create a reservation
pub async fn create_reservation(
a: FamilyAndAccommodationInPath,
req: web::Json<UpdateReservationQuery>,
) -> HttpResult {
if let Some(err) = req.validate(&a, None).await? {
return Ok(HttpResponse::BadRequest().json(err));
}
let mut reservation =
accommodations_reservations_service::create(&NewAccommodationReservation {
family_id: a.family_id().0,
accommodation_id: a.id().0,
user_id: a.membership().user_id().0,
time_create: time() as i64,
time_update: time() as i64,
reservation_start: req.start as i64,
reservation_end: req.end as i64,
})
.await?;
// Auto validate reservation if requested
if !a.need_validation {
reservation.validated = Some(true);
accommodations_reservations_service::update(&mut reservation).await?;
}
Ok(HttpResponse::Ok().json(reservation))
}
/// Get the reservations for a given accommodation
pub async fn get_accommodation_reservations(a: FamilyAndAccommodationInPath) -> HttpResult {
Ok(HttpResponse::Ok()
.json(accommodations_reservations_service::get_all_of_accommodation(a.id()).await?))
}
/// Get the full list of accommodations reservations for a family
pub async fn full_list(m: FamilyInPath) -> HttpResult {
Ok(HttpResponse::Ok()
.json(accommodations_reservations_service::get_all_of_family(m.family_id()).await?))
}
/// Get a single accommodation reservation
pub async fn get_single(m: FamilyAndAccommodationReservationInPath) -> HttpResult {
Ok(HttpResponse::Ok().json(m.to_reservation()))
}
/// Update a reservation
pub async fn update_single(
m: FamilyAndAccommodationReservationInPath,
req: web::Json<UpdateReservationQuery>,
) -> HttpResult {
if let Some(err) = req.validate(m.as_accommodation(), Some(m.id())).await? {
return Ok(HttpResponse::BadRequest().json(err));
}
if m.membership().user_id() != m.user_id() {
return Ok(
HttpResponse::BadRequest().json("Only the owner of a reservation can change it!")
);
}
let need_validation = m.as_accommodation().need_validation;
let mut reservation = m.to_reservation();
reservation.reservation_start = req.start as i64;
reservation.reservation_end = req.end as i64;
if need_validation {
reservation.validated = None;
}
accommodations_reservations_service::update(&mut reservation).await?;
Ok(HttpResponse::Accepted().finish())
}
/// Delete a reservation
pub async fn delete(m: FamilyAndAccommodationReservationInPath) -> HttpResult {
if m.membership().user_id() != m.user_id() {
return Ok(
HttpResponse::BadRequest().json("Only the owner of a reservation can delete it!")
);
}
accommodations_reservations_service::delete(m.to_reservation()).await?;
Ok(HttpResponse::Accepted().finish())
}
#[derive(serde::Deserialize)]
pub struct ValidateQuery {
validate: bool,
}
/// Validate or reject a reservation
pub async fn validate_or_reject(
m: FamilyAndAccommodationReservationInPath,
q: web::Json<ValidateQuery>,
) -> HttpResult {
if !m.membership().is_admin {
return Ok(
HttpResponse::BadRequest().json("Only a family admin can validate a reservation!")
);
}
if m.validated == Some(q.validate) {
return Ok(
HttpResponse::AlreadyReported().json("This reservation has already been processed!")
);
}
// In case of re-validation, check that the time is still available
if m.validated == Some(false) && q.validate {
let potential_conflicts =
accommodations_reservations_service::get_reservations_for_time_interval(
m.accommodation_id(),
m.reservation_start as usize,
m.reservation_end as usize,
)
.await?;
if potential_conflicts
.iter()
.any(|a| a.validated != Some(false))
{
return Ok(HttpResponse::Conflict().json(
"This cannot be accepted as it would create a conflict with another reservation!",
));
}
}
// Update reservation validation status
let mut reservation = m.to_reservation();
reservation.validated = Some(q.validate);
accommodations_reservations_service::update(&mut reservation).await?;
Ok(HttpResponse::Accepted().finish())
}

View File

@@ -80,6 +80,7 @@ struct RichFamilyInfo {
#[serde(flatten)]
membership: FamilyMembership,
enable_genealogy: bool,
enable_accommodations: bool,
disable_couple_photos: bool,
}
@@ -90,6 +91,7 @@ pub async fn single_info(f: FamilyInPath) -> HttpResult {
Ok(HttpResponse::Ok().json(RichFamilyInfo {
membership,
enable_genealogy: family.enable_genealogy,
enable_accommodations: family.enable_accommodations,
disable_couple_photos: family.disable_couple_photos,
}))
}
@@ -105,6 +107,7 @@ pub async fn leave(f: FamilyInPath) -> HttpResult {
pub struct UpdateFamilyBody {
name: Option<String>,
enable_genealogy: Option<bool>,
enable_accommodations: Option<bool>,
disable_couple_photos: Option<bool>,
}
@@ -127,6 +130,10 @@ pub async fn update(
family.enable_genealogy = enable_genealogy;
}
if let Some(enable_accommodations) = req.enable_accommodations {
family.enable_accommodations = enable_accommodations;
}
if let Some(disable_couple_photos) = req.disable_couple_photos {
family.disable_couple_photos = disable_couple_photos;
}

View File

@@ -5,6 +5,9 @@ use actix_web::HttpResponse;
use std::fmt::{Debug, Display, Formatter};
use zip::result::ZipError;
pub mod accommodations_list_controller;
pub mod accommodations_reservations_calendars_controller;
pub mod accommodations_reservations_controller;
pub mod auth_controller;
pub mod couples_controller;
pub mod data_controller;

View File

@@ -0,0 +1,83 @@
use crate::extractors::family_extractor::FamilyInPath;
use crate::models::{Accommodation, AccommodationID, FamilyID, Membership};
use crate::services::accommodations_list_service;
use actix_web::dev::Payload;
use actix_web::{FromRequest, HttpRequest};
use serde::Deserialize;
use std::ops::Deref;
#[derive(thiserror::Error, Debug)]
enum AccommodationExtractorErr {
#[error("Accommodation {0:?} does not belong to family {1:?}!")]
AccommodationNotInFamily(AccommodationID, FamilyID),
}
#[derive(Debug)]
pub struct FamilyAndAccommodationInPath(Membership, Accommodation);
impl FamilyAndAccommodationInPath {
async fn load_accommodation_from_path(
family: FamilyInPath,
accommodation_id: AccommodationID,
) -> anyhow::Result<Self> {
let accommodation = accommodations_list_service::get_by_id(accommodation_id).await?;
if accommodation.family_id() != family.family_id() {
return Err(AccommodationExtractorErr::AccommodationNotInFamily(
accommodation.id(),
family.family_id(),
)
.into());
}
Ok(Self(family.into(), accommodation))
}
}
impl Deref for FamilyAndAccommodationInPath {
type Target = Accommodation;
fn deref(&self) -> &Self::Target {
&self.1
}
}
impl FamilyAndAccommodationInPath {
pub fn membership(&self) -> &Membership {
&self.0
}
pub fn to_accommodation(self) -> Accommodation {
self.1
}
}
#[derive(Deserialize)]
struct AccommodationIDInPath {
accommodation_id: AccommodationID,
}
impl FromRequest for FamilyAndAccommodationInPath {
type Error = actix_web::Error;
type Future = futures_util::future::LocalBoxFuture<'static, Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
let req = req.clone();
Box::pin(async move {
let family = FamilyInPath::extract(&req).await?;
let accommodation_id = actix_web::web::Path::<AccommodationIDInPath>::from_request(
&req,
&mut Payload::None,
)
.await?
.accommodation_id;
Self::load_accommodation_from_path(family, accommodation_id)
.await
.map_err(|e| {
log::error!("Failed to extract accommodation ID from URL! {}", e);
actix_web::error::ErrorNotFound("Could not fetch accommodation information!")
})
})
}
}

View File

@@ -0,0 +1,93 @@
use crate::extractors::family_extractor::FamilyInPath;
use crate::models::{
AccommodationReservationCalendar, AccommodationReservationCalendarID, FamilyID, Membership,
};
use crate::services::accommodations_reservations_calendars_service;
use actix_web::dev::Payload;
use actix_web::{FromRequest, HttpRequest};
use serde::Deserialize;
use std::ops::Deref;
#[derive(thiserror::Error, Debug)]
enum AccommodationCalendarExtractorErr {
#[error("Calendar {0:?} does not belong to user or family {1:?}!")]
CalendarNotOfUserOrFamily(AccommodationReservationCalendarID, FamilyID),
}
#[derive(Debug)]
pub struct FamilyAndAccommodationReservationCalendarInPath(
Membership,
AccommodationReservationCalendar,
);
impl FamilyAndAccommodationReservationCalendarInPath {
async fn load_calendar_from_path(
family: FamilyInPath,
calendar_id: AccommodationReservationCalendarID,
) -> anyhow::Result<Self> {
let accommodation =
accommodations_reservations_calendars_service::get_by_id(calendar_id).await?;
if accommodation.family_id() != family.family_id()
|| accommodation.user_id() != family.user_id()
{
return Err(
AccommodationCalendarExtractorErr::CalendarNotOfUserOrFamily(
accommodation.id(),
family.family_id(),
)
.into(),
);
}
Ok(Self(family.into(), accommodation))
}
}
impl Deref for FamilyAndAccommodationReservationCalendarInPath {
type Target = AccommodationReservationCalendar;
fn deref(&self) -> &Self::Target {
&self.1
}
}
impl FamilyAndAccommodationReservationCalendarInPath {
pub fn membership(&self) -> &Membership {
&self.0
}
pub fn to_reservation(self) -> AccommodationReservationCalendar {
self.1
}
}
#[derive(Deserialize)]
struct AccommodationIDInPath {
cal_id: AccommodationReservationCalendarID,
}
impl FromRequest for FamilyAndAccommodationReservationCalendarInPath {
type Error = actix_web::Error;
type Future = futures_util::future::LocalBoxFuture<'static, Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
let req = req.clone();
Box::pin(async move {
let family = FamilyInPath::extract(&req).await?;
let accommodation_id = actix_web::web::Path::<AccommodationIDInPath>::from_request(
&req,
&mut Payload::None,
)
.await?
.cal_id;
Self::load_calendar_from_path(family, accommodation_id)
.await
.map_err(|e| {
log::error!("Failed to extract calendar ID from URL! {}", e);
actix_web::error::ErrorNotFound("Could not fetch calendar information!")
})
})
}
}

View File

@@ -0,0 +1,103 @@
use crate::extractors::family_extractor::FamilyInPath;
use crate::models::{
Accommodation, AccommodationReservation, AccommodationReservationID, FamilyID, Membership,
};
use crate::services::{accommodations_list_service, accommodations_reservations_service};
use actix_web::dev::Payload;
use actix_web::{FromRequest, HttpRequest};
use serde::Deserialize;
use std::fmt::Debug;
use std::ops::Deref;
#[derive(thiserror::Error, Debug)]
enum AccommodationReservationExtractorErr {
#[error("Accommodation reservation {0:?} does not belong to family {1:?}!")]
AccommodationNotInFamily(AccommodationReservationID, FamilyID),
}
#[derive(Debug)]
pub struct FamilyAndAccommodationReservationInPath(
Membership,
Accommodation,
AccommodationReservation,
);
impl FamilyAndAccommodationReservationInPath {
async fn load_accommodation_reservation_from_path(
family: FamilyInPath,
reservation_id: AccommodationReservationID,
) -> anyhow::Result<Self> {
let reservation = accommodations_reservations_service::get_by_id(reservation_id).await?;
let accommodation =
accommodations_list_service::get_by_id(reservation.accommodation_id()).await?;
if accommodation.family_id() != family.family_id()
|| reservation.family_id() != family.family_id()
{
return Err(
AccommodationReservationExtractorErr::AccommodationNotInFamily(
reservation.id(),
family.family_id(),
)
.into(),
);
}
Ok(Self(family.into(), accommodation, reservation))
}
}
impl Deref for FamilyAndAccommodationReservationInPath {
type Target = AccommodationReservation;
fn deref(&self) -> &Self::Target {
&self.2
}
}
impl FamilyAndAccommodationReservationInPath {
pub fn membership(&self) -> &Membership {
&self.0
}
pub fn as_accommodation(&self) -> &Accommodation {
&self.1
}
pub fn to_accommodation(self) -> Accommodation {
self.1
}
pub fn to_reservation(self) -> AccommodationReservation {
self.2
}
}
#[derive(Deserialize)]
struct ReservationIDInPath {
reservation_id: AccommodationReservationID,
}
impl FromRequest for FamilyAndAccommodationReservationInPath {
type Error = actix_web::Error;
type Future = futures_util::future::LocalBoxFuture<'static, Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
let req = req.clone();
Box::pin(async move {
let family = FamilyInPath::extract(&req).await?;
let reservation_id =
actix_web::web::Path::<ReservationIDInPath>::from_request(&req, &mut Payload::None)
.await?
.reservation_id;
Self::load_accommodation_reservation_from_path(family, reservation_id)
.await
.map_err(|e| {
log::error!("Failed to extract accommodation ID from URL! {}", e);
actix_web::error::ErrorNotFound("Could not fetch accommodation information!")
})
})
}
}

View File

@@ -1,3 +1,6 @@
pub mod accommodation_extractor;
pub mod accommodation_reservation_calendar_extractor;
pub mod accommodation_reservation_extractor;
pub mod couple_extractor;
pub mod family_extractor;
pub mod member_extractor;

View File

@@ -6,8 +6,10 @@ use actix_web::{web, App, HttpServer};
use geneit_backend::app_config::AppConfig;
use geneit_backend::connections::{db_connection, s3_connection};
use geneit_backend::controllers::{
auth_controller, couples_controller, data_controller, families_controller, members_controller,
photos_controller, server_controller, users_controller,
accommodations_list_controller, accommodations_reservations_calendars_controller,
accommodations_reservations_controller, auth_controller, couples_controller, data_controller,
families_controller, members_controller, photos_controller, server_controller,
users_controller,
};
#[actix_web::main]
@@ -204,6 +206,74 @@ async fn main() -> std::io::Result<()> {
"/family/{id}/genealogy/data/import",
web::put().to(data_controller::import_family),
)
// [ACCOMODATIONS] List controller
.route(
"/family/{id}/accommodations/list/create",
web::post().to(accommodations_list_controller::create),
)
.route(
"/family/{id}/accommodations/list/list",
web::get().to(accommodations_list_controller::get_full_list),
)
.route(
"/family/{id}/accommodations/list/{accommodation_id}",
web::get().to(accommodations_list_controller::get_single),
)
.route(
"/family/{id}/accommodations/list/{accommodation_id}",
web::put().to(accommodations_list_controller::update),
)
.route(
"/family/{id}/accommodations/list/{accommodation_id}",
web::delete().to(accommodations_list_controller::delete),
)
// [ACCOMODATIONS] Reservations controller
.route(
"/family/{id}/accommodations/reservations/accommodation/{accommodation_id}",
web::get()
.to(accommodations_reservations_controller::get_accommodation_reservations),
)
.route(
"/family/{id}/accommodations/reservations/full_list",
web::get().to(accommodations_reservations_controller::full_list),
)
.route(
"/family/{id}/accommodations/reservations/accommodation/{accommodation_id}/create",
web::post().to(accommodations_reservations_controller::create_reservation),
)
.route(
"/family/{id}/accommodations/reservation/{reservation_id}",
web::get().to(accommodations_reservations_controller::get_single),
)
.route(
"/family/{id}/accommodations/reservation/{reservation_id}",
web::patch().to(accommodations_reservations_controller::update_single),
)
.route(
"/family/{id}/accommodations/reservation/{reservation_id}",
web::delete().to(accommodations_reservations_controller::delete),
)
.route(
"/family/{id}/accommodations/reservation/{reservation_id}/validate",
web::post().to(accommodations_reservations_controller::validate_or_reject),
)
// [ACCOMMODATIONS] Calendars controller
.route(
"/family/{id}/accommodations/reservations_calendars/create",
web::post().to(accommodations_reservations_calendars_controller::create),
)
.route(
"/family/{id}/accommodations/reservations_calendars/list",
web::get().to(accommodations_reservations_calendars_controller::get_list),
)
.route(
"/family/{id}/accommodations/reservations_calendars/{cal_id}",
web::delete().to(accommodations_reservations_calendars_controller::delete),
)
.route(
"/acccommodations_calendar/{token}",
web::get().to(accommodations_reservations_calendars_controller::anonymous_access),
)
// Photos controller
.route(
"/photo/{id}",

View File

@@ -1,5 +1,8 @@
use crate::app_config::AppConfig;
use crate::schema::{couples, families, members, memberships, photos, users};
use crate::schema::{
accommodations_list, accommodations_reservations, accommodations_reservations_cals_urls,
couples, families, members, memberships, photos, users,
};
use crate::utils::crypt_utils::sha256;
use diesel::prelude::*;
@@ -66,6 +69,7 @@ pub struct Family {
pub invitation_code: String,
pub disable_couple_photos: bool,
pub enable_genealogy: bool,
pub enable_accommodations: bool,
}
impl Family {
@@ -308,7 +312,7 @@ pub struct NewMember {
pub time_update: i64,
}
/// Member ID holder
/// Couple ID holder
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)]
pub struct CoupleID(pub i32);
@@ -441,3 +445,145 @@ pub struct NewCouple {
pub time_create: i64,
pub time_update: i64,
}
/// Accommodation ID holder
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)]
pub struct AccommodationID(pub i32);
#[derive(Queryable, Debug, serde::Serialize)]
pub struct Accommodation {
id: i32,
family_id: i32,
time_create: i64,
pub time_update: i64,
pub name: String,
pub need_validation: bool,
pub description: Option<String>,
pub open_to_reservations: bool,
}
impl Accommodation {
pub fn id(&self) -> AccommodationID {
AccommodationID(self.id)
}
pub fn family_id(&self) -> FamilyID {
FamilyID(self.family_id)
}
}
#[derive(Insertable)]
#[diesel(table_name = accommodations_list)]
pub struct NewAccommodation {
pub family_id: i32,
pub name: String,
pub time_create: i64,
pub time_update: i64,
}
/// Accommodation reservation ID holder
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)]
pub struct AccommodationReservationID(pub i32);
pub enum ReservationStatus {
Pending,
Accepted,
Rejected,
}
#[derive(Queryable, Debug, serde::Serialize)]
pub struct AccommodationReservation {
id: i32,
family_id: i32,
accommodation_id: i32,
user_id: i32,
time_create: i64,
pub time_update: i64,
pub reservation_start: i64,
pub reservation_end: i64,
pub validated: Option<bool>,
}
impl AccommodationReservation {
pub fn id(&self) -> AccommodationReservationID {
AccommodationReservationID(self.id)
}
pub fn accommodation_id(&self) -> AccommodationID {
AccommodationID(self.accommodation_id)
}
pub fn family_id(&self) -> FamilyID {
FamilyID(self.family_id)
}
pub fn user_id(&self) -> UserID {
UserID(self.user_id)
}
pub fn status(&self) -> ReservationStatus {
match self.validated {
None => ReservationStatus::Pending,
Some(true) => ReservationStatus::Accepted,
Some(false) => ReservationStatus::Rejected,
}
}
}
#[derive(Insertable)]
#[diesel(table_name = accommodations_reservations)]
pub struct NewAccommodationReservation {
pub family_id: i32,
pub accommodation_id: i32,
pub user_id: i32,
pub time_create: i64,
pub time_update: i64,
pub reservation_start: i64,
pub reservation_end: i64,
}
/// Accommodation reservation calendar ID holder
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)]
pub struct AccommodationReservationCalendarID(pub i32);
#[derive(Queryable, Debug, serde::Serialize)]
pub struct AccommodationReservationCalendar {
id: i32,
family_id: i32,
accommodation_id: Option<i32>,
user_id: i32,
name: String,
token: String,
pub time_create: i64,
pub time_used: i64,
}
impl AccommodationReservationCalendar {
pub fn id(&self) -> AccommodationReservationCalendarID {
AccommodationReservationCalendarID(self.id)
}
pub fn accommodation_id(&self) -> Option<AccommodationID> {
self.accommodation_id.map(AccommodationID)
}
pub fn family_id(&self) -> FamilyID {
FamilyID(self.family_id)
}
pub fn user_id(&self) -> UserID {
UserID(self.user_id)
}
}
#[derive(Insertable)]
#[diesel(table_name = accommodations_reservations_cals_urls)]
pub struct NewAccommodationReservationCalendar {
pub family_id: i32,
pub accommodation_id: Option<i32>,
pub user_id: i32,
pub name: String,
pub token: String,
pub time_create: i64,
pub time_used: i64,
}

View File

@@ -1,5 +1,48 @@
// @generated automatically by Diesel CLI.
diesel::table! {
accommodations_list (id) {
id -> Int4,
family_id -> Int4,
time_create -> Int8,
time_update -> Int8,
#[max_length = 50]
name -> Varchar,
need_validation -> Bool,
description -> Nullable<Text>,
open_to_reservations -> Bool,
}
}
diesel::table! {
accommodations_reservations (id) {
id -> Int4,
family_id -> Int4,
accommodation_id -> Int4,
user_id -> Int4,
time_create -> Int8,
time_update -> Int8,
reservation_start -> Int8,
reservation_end -> Int8,
validated -> Nullable<Bool>,
}
}
diesel::table! {
accommodations_reservations_cals_urls (id) {
id -> Int4,
family_id -> Int4,
accommodation_id -> Nullable<Int4>,
user_id -> Int4,
#[max_length = 50]
name -> Varchar,
#[max_length = 50]
token -> Varchar,
time_create -> Int8,
time_used -> Int8,
}
}
diesel::table! {
couples (id) {
id -> Int4,
@@ -30,6 +73,7 @@ diesel::table! {
invitation_code -> Varchar,
disable_couple_photos -> Bool,
enable_genealogy -> Bool,
enable_accommodations -> Bool,
}
}
@@ -119,6 +163,13 @@ diesel::table! {
}
}
diesel::joinable!(accommodations_list -> families (family_id));
diesel::joinable!(accommodations_reservations -> accommodations_list (accommodation_id));
diesel::joinable!(accommodations_reservations -> families (family_id));
diesel::joinable!(accommodations_reservations -> users (user_id));
diesel::joinable!(accommodations_reservations_cals_urls -> accommodations_list (accommodation_id));
diesel::joinable!(accommodations_reservations_cals_urls -> families (family_id));
diesel::joinable!(accommodations_reservations_cals_urls -> users (user_id));
diesel::joinable!(couples -> families (family_id));
diesel::joinable!(couples -> photos (photo_id));
diesel::joinable!(members -> families (family_id));
@@ -127,6 +178,9 @@ diesel::joinable!(memberships -> families (family_id));
diesel::joinable!(memberships -> users (user_id));
diesel::allow_tables_to_appear_in_same_query!(
accommodations_list,
accommodations_reservations,
accommodations_reservations_cals_urls,
couples,
families,
members,

View File

@@ -0,0 +1,102 @@
use crate::connections::db_connection;
use crate::models::{Accommodation, AccommodationID, FamilyID, NewAccommodation};
use crate::schema::accommodations_list;
use crate::utils::time_utils::time;
use diesel::prelude::*;
/// Create a new accommodation
pub async fn create(family_id: FamilyID) -> anyhow::Result<Accommodation> {
db_connection::execute(|conn| {
let res: Accommodation = diesel::insert_into(accommodations_list::table)
.values(&NewAccommodation {
family_id: family_id.0,
name: "".to_string(),
time_create: time() as i64,
time_update: time() as i64,
})
.get_result(conn)?;
Ok(res)
})
}
/// Get the information of an accommodation
pub async fn get_by_id(id: AccommodationID) -> anyhow::Result<Accommodation> {
db_connection::execute(|conn| {
accommodations_list::table
.filter(accommodations_list::dsl::id.eq(id.0))
.first(conn)
})
}
/// Get all the accommodations of a family
pub async fn get_all_of_family(id: FamilyID) -> anyhow::Result<Vec<Accommodation>> {
db_connection::execute(|conn| {
accommodations_list::table
.filter(accommodations_list::dsl::family_id.eq(id.0))
.get_results(conn)
})
}
/// Check whether accommodation with a given id exists or not
pub async fn exists(
family_id: FamilyID,
accommodation_id: AccommodationID,
) -> anyhow::Result<bool> {
db_connection::execute(|conn| {
let count: i64 = accommodations_list::table
.filter(
accommodations_list::id
.eq(accommodation_id.0)
.and(accommodations_list::family_id.eq(family_id.0)),
)
.count()
.get_result(conn)?;
Ok(count != 0)
})
}
/// Update the information of an accommodation
pub async fn update(accommodation: &mut Accommodation) -> anyhow::Result<()> {
accommodation.time_update = time() as i64;
db_connection::execute(|conn| {
diesel::update(
accommodations_list::dsl::accommodations_list
.filter(accommodations_list::dsl::id.eq(accommodation.id().0)),
)
.set((
accommodations_list::dsl::time_update.eq(accommodation.time_update),
accommodations_list::dsl::name.eq(accommodation.name.to_string()),
accommodations_list::dsl::need_validation.eq(accommodation.need_validation),
accommodations_list::dsl::description.eq(accommodation.description.clone()),
accommodations_list::dsl::open_to_reservations.eq(accommodation.open_to_reservations),
))
.execute(conn)
})?;
Ok(())
}
/// Delete an accommodation
pub async fn delete(accommodation: &mut Accommodation) -> anyhow::Result<()> {
// Remove the accommodation
db_connection::execute(|conn| {
diesel::delete(
accommodations_list::dsl::accommodations_list
.filter(accommodations_list::dsl::id.eq(accommodation.id().0)),
)
.execute(conn)
})?;
Ok(())
}
/// Delete all the accommodations of a family
pub async fn delete_all_family(family_id: FamilyID) -> anyhow::Result<()> {
for mut m in get_all_of_family(family_id).await? {
delete(&mut m).await?;
}
Ok(())
}

View File

@@ -0,0 +1,84 @@
use crate::connections::db_connection;
use crate::constants;
use crate::models::{
AccommodationID, AccommodationReservationCalendar, AccommodationReservationCalendarID,
FamilyID, NewAccommodationReservationCalendar, UserID,
};
use crate::schema::accommodations_reservations_cals_urls;
use crate::utils::string_utils::rand_str;
use crate::utils::time_utils::time;
use diesel::prelude::*;
/// Create a new reservation calendar entry
pub async fn create(
user_id: UserID,
family_id: FamilyID,
accommodation_id: Option<AccommodationID>,
name: &str,
) -> anyhow::Result<AccommodationReservationCalendar> {
db_connection::execute(|conn| {
let res: AccommodationReservationCalendar =
diesel::insert_into(accommodations_reservations_cals_urls::table)
.values(&NewAccommodationReservationCalendar {
family_id: family_id.0,
accommodation_id: accommodation_id.map(|i| i.0),
user_id: user_id.0,
name: name.to_string(),
token: rand_str(constants::ACCOMMODATIONS_RESERVATIONS_CALENDARS_TOKENS_LEN),
time_create: time() as i64,
time_used: time() as i64,
})
.get_result(conn)?;
Ok(res)
})
}
/// Get all the calendars of a user
pub async fn get_all_of_user(
user: UserID,
family: FamilyID,
) -> anyhow::Result<Vec<AccommodationReservationCalendar>> {
db_connection::execute(|conn| {
accommodations_reservations_cals_urls::table
.filter(
accommodations_reservations_cals_urls::dsl::family_id
.eq(family.0)
.and(accommodations_reservations_cals_urls::dsl::user_id.eq(user.0)),
)
.get_results(conn)
})
}
/// Get a single calendar by its id
pub async fn get_by_id(
id: AccommodationReservationCalendarID,
) -> anyhow::Result<AccommodationReservationCalendar> {
db_connection::execute(|conn| {
accommodations_reservations_cals_urls::table
.filter(accommodations_reservations_cals_urls::dsl::id.eq(id.0))
.get_result(conn)
})
}
/// Get a single calendar by its token
pub async fn get_by_token(token: &str) -> anyhow::Result<AccommodationReservationCalendar> {
db_connection::execute(|conn| {
accommodations_reservations_cals_urls::table
.filter(accommodations_reservations_cals_urls::dsl::token.eq(token))
.get_result(conn)
})
}
/// Delete a calendar
pub async fn delete(r: AccommodationReservationCalendar) -> anyhow::Result<()> {
db_connection::execute(|conn| {
diesel::delete(
accommodations_reservations_cals_urls::dsl::accommodations_reservations_cals_urls
.filter(accommodations_reservations_cals_urls::dsl::id.eq(r.id().0)),
)
.execute(conn)
})?;
Ok(())
}

View File

@@ -0,0 +1,101 @@
use crate::connections::db_connection;
use crate::models::{
AccommodationID, AccommodationReservation, AccommodationReservationID, FamilyID,
NewAccommodationReservation,
};
use crate::schema::accommodations_reservations;
use crate::utils::time_utils::time;
use diesel::prelude::*;
/// Create a new reservation
pub async fn create(new: &NewAccommodationReservation) -> anyhow::Result<AccommodationReservation> {
db_connection::execute(|conn| {
let res: AccommodationReservation = diesel::insert_into(accommodations_reservations::table)
.values(new)
.get_result(conn)?;
Ok(res)
})
}
/// Update a reservation
pub async fn update(r: &mut AccommodationReservation) -> anyhow::Result<()> {
r.time_update = time() as i64;
db_connection::execute(|conn| {
diesel::update(
accommodations_reservations::dsl::accommodations_reservations
.filter(accommodations_reservations::dsl::id.eq(r.id().0)),
)
.set((
accommodations_reservations::dsl::time_update.eq(r.time_update),
accommodations_reservations::dsl::validated.eq(r.validated),
accommodations_reservations::dsl::reservation_start.eq(r.reservation_start),
accommodations_reservations::dsl::reservation_end.eq(r.reservation_end),
))
.execute(conn)
})?;
Ok(())
}
/// Delete a reservation
pub async fn delete(r: AccommodationReservation) -> anyhow::Result<()> {
// Remove the reservation
db_connection::execute(|conn| {
diesel::delete(
accommodations_reservations::dsl::accommodations_reservations
.filter(accommodations_reservations::dsl::id.eq(r.id().0)),
)
.execute(conn)
})?;
Ok(())
}
/// Get all the reservations of an accommodation
pub async fn get_all_of_accommodation(
id: AccommodationID,
) -> anyhow::Result<Vec<AccommodationReservation>> {
db_connection::execute(|conn| {
accommodations_reservations::table
.filter(accommodations_reservations::dsl::accommodation_id.eq(id.0))
.get_results(conn)
})
}
/// Get all the reservations of a family
pub async fn get_all_of_family(id: FamilyID) -> anyhow::Result<Vec<AccommodationReservation>> {
db_connection::execute(|conn| {
accommodations_reservations::table
.filter(accommodations_reservations::dsl::family_id.eq(id.0))
.get_results(conn)
})
}
/// Get a single accommodation reservation by its id
pub async fn get_by_id(id: AccommodationReservationID) -> anyhow::Result<AccommodationReservation> {
db_connection::execute(|conn| {
accommodations_reservations::table
.filter(accommodations_reservations::dsl::id.eq(id.0))
.get_result(conn)
})
}
/// Get the reservations that are between a given interval of time for a given accommodation
pub async fn get_reservations_for_time_interval(
id: AccommodationID,
start: usize,
end: usize,
) -> anyhow::Result<Vec<AccommodationReservation>> {
db_connection::execute(|conn| {
accommodations_reservations::table
.filter(
accommodations_reservations::dsl::accommodation_id
.eq(id.0)
.and(accommodations_reservations::dsl::reservation_start.lt((end) as i64))
.and(accommodations_reservations::dsl::reservation_end.gt((start) as i64)),
)
.get_results(conn)
})
}

View File

@@ -5,7 +5,9 @@ use crate::models::{
Family, FamilyID, FamilyMembership, Membership, NewFamily, NewMembership, UserID,
};
use crate::schema::{families, memberships};
use crate::services::{couples_service, members_service, users_service};
use crate::services::{
accommodations_list_service, couples_service, members_service, users_service,
};
use crate::utils::string_utils::rand_str;
use crate::utils::time_utils::time;
use diesel::prelude::*;
@@ -127,9 +129,9 @@ pub async fn update_membership(membership: &Membership) -> anyhow::Result<()> {
#[derive(serde::Serialize)]
pub struct FamilyMember {
#[serde(flatten)]
membership: Membership,
user_name: String,
user_mail: String,
pub membership: Membership,
pub user_name: String,
pub user_mail: String,
}
/// Get information about the users of a family
@@ -175,6 +177,7 @@ pub async fn update_family(family: &Family) -> anyhow::Result<()> {
families::dsl::name.eq(family.name.clone()),
families::dsl::invitation_code.eq(family.invitation_code.clone()),
families::dsl::enable_genealogy.eq(family.enable_genealogy),
families::dsl::enable_accommodations.eq(family.enable_accommodations),
families::dsl::disable_couple_photos.eq(family.disable_couple_photos),
))
.execute(conn)
@@ -185,6 +188,9 @@ pub async fn update_family(family: &Family) -> anyhow::Result<()> {
/// Delete a family
pub async fn delete_family(family_id: FamilyID) -> anyhow::Result<()> {
// Delete all family accommodations
accommodations_list_service::delete_all_family(family_id).await?;
// Delete all family couples
couples_service::delete_all_family(family_id).await?;

View File

@@ -1,5 +1,8 @@
//! # Backend services
pub mod accommodations_list_service;
pub mod accommodations_reservations_calendars_service;
pub mod accommodations_reservations_service;
pub mod couples_service;
pub mod families_service;
pub mod login_token_service;