Compare commits
39 Commits
renovate/d
...
856f4a3f76
| Author | SHA1 | Date | |
|---|---|---|---|
| 856f4a3f76 | |||
| 3a218cd3fb | |||
| 7d64ea219f | |||
| f83cbe1386 | |||
| 9600acab0f | |||
| 3620587b3b | |||
| df6a9e8292 | |||
| e8b1b91202 | |||
| 0f0b5978b6 | |||
| 7626d91ece | |||
| e62f536c03 | |||
| 51f8aaccb6 | |||
| b34959df33 | |||
| e86c80310d | |||
| 09633458a0 | |||
| 2346c90be8 | |||
| 9f72cd9b9c | |||
| 5075e8843b | |||
| 238e17aed6 | |||
| 82dbf11b42 | |||
| f07a6a8923 | |||
| 33b03a4d74 | |||
| 70d8020610 | |||
| 5f25a516e9 | |||
| 829c2e8df4 | |||
| 5b9d82889c | |||
| 3efae7bfff | |||
| 1332b001c8 | |||
| 6e4b6a0499 | |||
| 936b095d46 | |||
| d0d1169c7d | |||
| 9a4da0462a | |||
| 49f3677081 | |||
| 16bc7eca6f | |||
| bc800e7cf6 | |||
| 18582fdff7 | |||
| c4fadce69f | |||
| 2f1df6c117 | |||
| 32d3793025 |
@@ -16,29 +16,31 @@ import { NewAccountRoute } from "./routes/auth/NewAccountRoute";
|
|||||||
import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute";
|
import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute";
|
||||||
import { PasswordForgottenRoute } from "./routes/auth/PasswordForgottenRoute";
|
import { PasswordForgottenRoute } from "./routes/auth/PasswordForgottenRoute";
|
||||||
import { ResetPasswordRoute } from "./routes/auth/ResetPasswordRoute";
|
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 { FamilySettingsRoute } from "./routes/family/FamilySettingsRoute";
|
||||||
import { FamilyUsersListRoute } from "./routes/family/FamilyUsersListRoute";
|
import { FamilyUsersListRoute } from "./routes/family/FamilyUsersListRoute";
|
||||||
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
|
import { AccommodationsSettingsRoute } from "./routes/family/accommodations/AccommodationsSettingsRoute";
|
||||||
import { BaseFamilyRoute } from "./widgets/BaseFamilyRoute";
|
|
||||||
import { BaseLoginPage } from "./widgets/BaseLoginpage";
|
|
||||||
import { FamilyMembersListRoute } from "./routes/family/genealogy/FamilyMembersListRoute";
|
|
||||||
import {
|
import {
|
||||||
FamilyCoupleRoute,
|
FamilyCoupleRoute,
|
||||||
FamilyCreateCoupleRoute,
|
FamilyCreateCoupleRoute,
|
||||||
FamilyEditCoupleRoute,
|
FamilyEditCoupleRoute,
|
||||||
} from "./routes/family/genealogy/FamilyCoupleRoute";
|
} from "./routes/family/genealogy/FamilyCoupleRoute";
|
||||||
import { FamilyCouplesListRoute } from "./routes/family/genealogy/FamilyCouplesListRoute";
|
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 { FamilyMemberTreeRoute } from "./routes/family/genealogy/FamilyMemberTreeRoute";
|
||||||
import { GenealogyHomeRoute } from "./routes/family/genealogy/GenealogyHomeRoute";
|
import { FamilyMembersListRoute } from "./routes/family/genealogy/FamilyMembersListRoute";
|
||||||
import { BaseGenealogyRoute } from "./widgets/genealogy/BaseGenealogyRoute";
|
import { FamilyTreeRoute } from "./routes/family/genealogy/FamilyTreeRoute";
|
||||||
import { GenalogySettingsRoute } from "./routes/family/genealogy/GenalogySettingsRoute";
|
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 {
|
interface AuthContext {
|
||||||
signedIn: boolean;
|
signedIn: boolean;
|
||||||
@@ -110,6 +112,17 @@ export function App(): React.ReactElement {
|
|||||||
<Route path="*" element={<NotFoundRoute />} />
|
<Route path="*" element={<NotFoundRoute />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="accommodations/*"
|
||||||
|
element={<BaseAccommodationsRoute />}
|
||||||
|
>
|
||||||
|
<Route
|
||||||
|
path="settings"
|
||||||
|
element={<AccommodationsSettingsRoute />}
|
||||||
|
/>
|
||||||
|
<Route path="*" element={<NotFoundRoute />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
<Route path="settings" element={<FamilySettingsRoute />} />
|
<Route path="settings" element={<FamilySettingsRoute />} />
|
||||||
<Route path="users" element={<FamilyUsersListRoute />} />
|
<Route path="users" element={<FamilyUsersListRoute />} />
|
||||||
<Route path="*" element={<NotFoundRoute />} />
|
<Route path="*" element={<NotFoundRoute />} />
|
||||||
|
|||||||
@@ -88,10 +88,12 @@ export class Family implements FamilyAPI {
|
|||||||
export class ExtendedFamilyInfo extends Family {
|
export class ExtendedFamilyInfo extends Family {
|
||||||
public disable_couple_photos: boolean;
|
public disable_couple_photos: boolean;
|
||||||
public enable_genealogy: boolean;
|
public enable_genealogy: boolean;
|
||||||
|
public enable_accommodations: boolean;
|
||||||
constructor(p: any) {
|
constructor(p: any) {
|
||||||
super(p);
|
super(p);
|
||||||
this.disable_couple_photos = p.disable_couple_photos;
|
this.disable_couple_photos = p.disable_couple_photos;
|
||||||
this.enable_genealogy = p.enable_genealogy;
|
this.enable_genealogy = p.enable_genealogy;
|
||||||
|
this.enable_accommodations = p.enable_accommodations;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,6 +237,7 @@ export class FamilyApi {
|
|||||||
id: number;
|
id: number;
|
||||||
name?: string;
|
name?: string;
|
||||||
enable_genealogy?: boolean;
|
enable_genealogy?: boolean;
|
||||||
|
enable_accommodations?: boolean;
|
||||||
disable_couple_photos?: boolean;
|
disable_couple_photos?: boolean;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
await APIClient.exec({
|
await APIClient.exec({
|
||||||
@@ -243,6 +246,7 @@ export class FamilyApi {
|
|||||||
jsonData: {
|
jsonData: {
|
||||||
name: settings.name,
|
name: settings.name,
|
||||||
enable_genealogy: settings.enable_genealogy,
|
enable_genealogy: settings.enable_genealogy,
|
||||||
|
enable_accommodations: settings.enable_accommodations,
|
||||||
disable_couple_photos: settings.disable_couple_photos,
|
disable_couple_photos: settings.disable_couple_photos,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ interface Constraints {
|
|||||||
member_country: LenConstraint;
|
member_country: LenConstraint;
|
||||||
member_sex: LenConstraint;
|
member_sex: LenConstraint;
|
||||||
member_note: LenConstraint;
|
member_note: LenConstraint;
|
||||||
|
accommodation_name_len: LenConstraint;
|
||||||
|
accommodation_description_len: LenConstraint;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OIDCProvider {
|
interface OIDCProvider {
|
||||||
|
|||||||
90
geneit_app/src/api/accommodations/AccommodationListApi.tsx
Normal file
90
geneit_app/src/api/accommodations/AccommodationListApi.tsx
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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)!;
|
||||||
|
}
|
||||||
@@ -71,6 +71,9 @@ function FamilySettingsCard(): React.ReactElement {
|
|||||||
const [enableGenealogy, setEnableGenealogy] = React.useState(
|
const [enableGenealogy, setEnableGenealogy] = React.useState(
|
||||||
family.family.enable_genealogy
|
family.family.enable_genealogy
|
||||||
);
|
);
|
||||||
|
const [enableAccommodations, setEnableAccommodations] = React.useState(
|
||||||
|
family.family.enable_accommodations
|
||||||
|
);
|
||||||
|
|
||||||
const canEdit = family.family.is_admin;
|
const canEdit = family.family.is_admin;
|
||||||
|
|
||||||
@@ -86,6 +89,7 @@ function FamilySettingsCard(): React.ReactElement {
|
|||||||
id: family.family.family_id,
|
id: family.family.family_id,
|
||||||
name: newName,
|
name: newName,
|
||||||
enable_genealogy: enableGenealogy,
|
enable_genealogy: enableGenealogy,
|
||||||
|
enable_accommodations: enableAccommodations,
|
||||||
});
|
});
|
||||||
|
|
||||||
family.reloadFamilyInfo();
|
family.reloadFamilyInfo();
|
||||||
@@ -118,14 +122,12 @@ function FamilySettingsCard(): React.ReactElement {
|
|||||||
label="Identifiant"
|
label="Identifiant"
|
||||||
value={family.family.family_id}
|
value={family.family.family_id}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
disabled
|
disabled
|
||||||
fullWidth
|
fullWidth
|
||||||
label="Création de la famille"
|
label="Création de la famille"
|
||||||
value={formatDate(family.family.time_create)}
|
value={formatDate(family.family.time_create)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
label="Nom de la famille"
|
label="Nom de la famille"
|
||||||
@@ -136,7 +138,6 @@ function FamilySettingsCard(): React.ReactElement {
|
|||||||
maxLength: ServerApi.Config.constraints.family_name_len.max,
|
maxLength: ServerApi.Config.constraints.family_name_len.max,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
disabled={!canEdit}
|
disabled={!canEdit}
|
||||||
control={
|
control={
|
||||||
@@ -147,6 +148,16 @@ function FamilySettingsCard(): React.ReactElement {
|
|||||||
}
|
}
|
||||||
label="Activer le module de généalogie"
|
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>
|
</Box>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardActions>
|
<CardActions>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
geneit_app/src/utils/from_utils.ts
Normal file
21
geneit_app/src/utils/from_utils.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
mdiCrowd,
|
mdiCrowd,
|
||||||
mdiFamilyTree,
|
mdiFamilyTree,
|
||||||
mdiFileTree,
|
mdiFileTree,
|
||||||
|
mdiHomeGroup,
|
||||||
mdiHumanMaleFemale,
|
mdiHumanMaleFemale,
|
||||||
mdiLockCheck,
|
mdiLockCheck,
|
||||||
mdiPlus,
|
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 */}
|
{/* Invitation code */}
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
|
|||||||
@@ -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)!;
|
||||||
|
}
|
||||||
@@ -5,16 +5,20 @@ export function PropCheckbox(p: {
|
|||||||
label: string;
|
label: string;
|
||||||
checked: boolean | undefined;
|
checked: boolean | undefined;
|
||||||
onValueChange: (v: boolean) => void;
|
onValueChange: (v: boolean) => void;
|
||||||
|
checkboxAlwaysVisible?: boolean;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
|
if (!p.checkboxAlwaysVisible) {
|
||||||
if (!p.editable && p.checked)
|
if (!p.editable && p.checked)
|
||||||
return <Typography variant="body2">{p.label}</Typography>;
|
return <Typography variant="body2">{p.label}</Typography>;
|
||||||
|
|
||||||
if (!p.editable) return <></>;
|
if (!p.editable) return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
disabled={!p.editable}
|
||||||
checked={p.checked}
|
checked={p.checked}
|
||||||
onChange={(e) => p.onValueChange(e.target.checked)}
|
onChange={(e) => p.onValueChange(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export function PropEdit(p: {
|
|||||||
multiline?: boolean;
|
multiline?: boolean;
|
||||||
minRows?: number;
|
minRows?: number;
|
||||||
maxRows?: number;
|
maxRows?: number;
|
||||||
|
helperText?: string;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
if (((!p.editable && p.value) ?? "") === "") return <></>;
|
if (((!p.editable && p.value) ?? "") === "") return <></>;
|
||||||
|
|
||||||
@@ -44,6 +45,7 @@ export function PropEdit(p: {
|
|||||||
!p.checkValue(p.value)) ||
|
!p.checkValue(p.value)) ||
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
helperText={p.helperText}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import { MemberApi, MembersList } from "../../api/genealogy/MemberApi";
|
|||||||
import { AsyncWidget } from "../AsyncWidget";
|
import { AsyncWidget } from "../AsyncWidget";
|
||||||
import { useFamily } from "../BaseFamilyRoute";
|
import { useFamily } from "../BaseFamilyRoute";
|
||||||
|
|
||||||
interface FamilyContext {
|
interface GenealogyContext {
|
||||||
members: MembersList;
|
members: MembersList;
|
||||||
couples: CouplesList;
|
couples: CouplesList;
|
||||||
reloadMembersList: () => Promise<void>;
|
reloadMembersList: () => Promise<void>;
|
||||||
reloadCouplesList: () => Promise<void>;
|
reloadCouplesList: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GenealogyContextK = React.createContext<FamilyContext | null>(null);
|
const GenealogyContextK = React.createContext<GenealogyContext | null>(null);
|
||||||
|
|
||||||
export function BaseGenealogyRoute(): React.ReactElement {
|
export function BaseGenealogyRoute(): React.ReactElement {
|
||||||
const family = useFamily();
|
const family = useFamily();
|
||||||
@@ -68,6 +68,6 @@ export function BaseGenealogyRoute(): React.ReactElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGenealogy(): FamilyContext {
|
export function useGenealogy(): GenealogyContext {
|
||||||
return React.useContext(GenealogyContextK)!;
|
return React.useContext(GenealogyContextK)!;
|
||||||
}
|
}
|
||||||
|
|||||||
13
geneit_backend/Cargo.lock
generated
13
geneit_backend/Cargo.lock
generated
@@ -728,8 +728,10 @@ checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"android-tzdata",
|
"android-tzdata",
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"serde",
|
"serde",
|
||||||
|
"wasm-bindgen",
|
||||||
"windows-targets 0.52.5",
|
"windows-targets 0.52.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1418,12 +1420,14 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bcrypt",
|
"bcrypt",
|
||||||
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"diesel",
|
"diesel",
|
||||||
"diesel_migrations",
|
"diesel_migrations",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"httpdate",
|
"httpdate",
|
||||||
|
"ical",
|
||||||
"image",
|
"image",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"lettre",
|
"lettre",
|
||||||
@@ -1777,6 +1781,15 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ical"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b7cab7543a8b7729a19e2c04309f902861293dcdae6558dfbeb634454d279f6"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ident_case"
|
name = "ident_case"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
|||||||
@@ -38,3 +38,5 @@ zip = "2.0.0"
|
|||||||
mime_guess = "2.0.4"
|
mime_guess = "2.0.4"
|
||||||
tempfile = "3.10.1"
|
tempfile = "3.10.1"
|
||||||
base64 = "0.22.0"
|
base64 = "0.22.0"
|
||||||
|
ical = { version = "0.11.0", features = ["generator", "ical", "vcard"] }
|
||||||
|
chrono = "0.4.38"
|
||||||
@@ -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;
|
||||||
@@ -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';
|
||||||
@@ -60,6 +60,10 @@ pub struct StaticConstraints {
|
|||||||
pub member_country: SizeConstraint,
|
pub member_country: SizeConstraint,
|
||||||
pub member_sex: SizeConstraint,
|
pub member_sex: SizeConstraint,
|
||||||
pub member_note: 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 {
|
impl Default for StaticConstraints {
|
||||||
@@ -91,6 +95,10 @@ impl Default for StaticConstraints {
|
|||||||
member_country: SizeConstraint::new(0, 2),
|
member_country: SizeConstraint::new(0, 2),
|
||||||
member_sex: SizeConstraint::new(0, 1),
|
member_sex: SizeConstraint::new(0, 1),
|
||||||
member_note: SizeConstraint::new(0, 35000),
|
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
|
/// Thumbnail height
|
||||||
pub const THUMB_HEIGHT: u32 = 350;
|
pub const THUMB_HEIGHT: u32 = 350;
|
||||||
|
|
||||||
|
/// Accommodations reservations calendars tokens len
|
||||||
|
pub const ACCOMMODATIONS_RESERVATIONS_CALENDARS_TOKENS_LEN: usize = 50;
|
||||||
|
|||||||
105
geneit_backend/src/controllers/accommodations_list_controller.rs
Normal file
105
geneit_backend/src/controllers/accommodations_list_controller.rs
Normal 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())
|
||||||
|
}
|
||||||
@@ -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()))
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
@@ -80,6 +80,7 @@ struct RichFamilyInfo {
|
|||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
membership: FamilyMembership,
|
membership: FamilyMembership,
|
||||||
enable_genealogy: bool,
|
enable_genealogy: bool,
|
||||||
|
enable_accommodations: bool,
|
||||||
disable_couple_photos: bool,
|
disable_couple_photos: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,6 +91,7 @@ pub async fn single_info(f: FamilyInPath) -> HttpResult {
|
|||||||
Ok(HttpResponse::Ok().json(RichFamilyInfo {
|
Ok(HttpResponse::Ok().json(RichFamilyInfo {
|
||||||
membership,
|
membership,
|
||||||
enable_genealogy: family.enable_genealogy,
|
enable_genealogy: family.enable_genealogy,
|
||||||
|
enable_accommodations: family.enable_accommodations,
|
||||||
disable_couple_photos: family.disable_couple_photos,
|
disable_couple_photos: family.disable_couple_photos,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -105,6 +107,7 @@ pub async fn leave(f: FamilyInPath) -> HttpResult {
|
|||||||
pub struct UpdateFamilyBody {
|
pub struct UpdateFamilyBody {
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
enable_genealogy: Option<bool>,
|
enable_genealogy: Option<bool>,
|
||||||
|
enable_accommodations: Option<bool>,
|
||||||
disable_couple_photos: Option<bool>,
|
disable_couple_photos: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,6 +130,10 @@ pub async fn update(
|
|||||||
family.enable_genealogy = enable_genealogy;
|
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 {
|
if let Some(disable_couple_photos) = req.disable_couple_photos {
|
||||||
family.disable_couple_photos = disable_couple_photos;
|
family.disable_couple_photos = disable_couple_photos;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ use actix_web::HttpResponse;
|
|||||||
use std::fmt::{Debug, Display, Formatter};
|
use std::fmt::{Debug, Display, Formatter};
|
||||||
use zip::result::ZipError;
|
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 auth_controller;
|
||||||
pub mod couples_controller;
|
pub mod couples_controller;
|
||||||
pub mod data_controller;
|
pub mod data_controller;
|
||||||
|
|||||||
83
geneit_backend/src/extractors/accommodation_extractor.rs
Normal file
83
geneit_backend/src/extractors/accommodation_extractor.rs
Normal 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!")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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!")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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!")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 couple_extractor;
|
||||||
pub mod family_extractor;
|
pub mod family_extractor;
|
||||||
pub mod member_extractor;
|
pub mod member_extractor;
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ use actix_web::{web, App, HttpServer};
|
|||||||
use geneit_backend::app_config::AppConfig;
|
use geneit_backend::app_config::AppConfig;
|
||||||
use geneit_backend::connections::{db_connection, s3_connection};
|
use geneit_backend::connections::{db_connection, s3_connection};
|
||||||
use geneit_backend::controllers::{
|
use geneit_backend::controllers::{
|
||||||
auth_controller, couples_controller, data_controller, families_controller, members_controller,
|
accommodations_list_controller, accommodations_reservations_calendars_controller,
|
||||||
photos_controller, server_controller, users_controller,
|
accommodations_reservations_controller, auth_controller, couples_controller, data_controller,
|
||||||
|
families_controller, members_controller, photos_controller, server_controller,
|
||||||
|
users_controller,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
@@ -204,6 +206,74 @@ async fn main() -> std::io::Result<()> {
|
|||||||
"/family/{id}/genealogy/data/import",
|
"/family/{id}/genealogy/data/import",
|
||||||
web::put().to(data_controller::import_family),
|
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
|
// Photos controller
|
||||||
.route(
|
.route(
|
||||||
"/photo/{id}",
|
"/photo/{id}",
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
use crate::app_config::AppConfig;
|
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 crate::utils::crypt_utils::sha256;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
||||||
@@ -66,6 +69,7 @@ pub struct Family {
|
|||||||
pub invitation_code: String,
|
pub invitation_code: String,
|
||||||
pub disable_couple_photos: bool,
|
pub disable_couple_photos: bool,
|
||||||
pub enable_genealogy: bool,
|
pub enable_genealogy: bool,
|
||||||
|
pub enable_accommodations: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Family {
|
impl Family {
|
||||||
@@ -308,7 +312,7 @@ pub struct NewMember {
|
|||||||
pub time_update: i64,
|
pub time_update: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Member ID holder
|
/// Couple ID holder
|
||||||
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)]
|
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)]
|
||||||
pub struct CoupleID(pub i32);
|
pub struct CoupleID(pub i32);
|
||||||
|
|
||||||
@@ -441,3 +445,145 @@ pub struct NewCouple {
|
|||||||
pub time_create: i64,
|
pub time_create: i64,
|
||||||
pub time_update: 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,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,48 @@
|
|||||||
// @generated automatically by Diesel CLI.
|
// @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! {
|
diesel::table! {
|
||||||
couples (id) {
|
couples (id) {
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
@@ -30,6 +73,7 @@ diesel::table! {
|
|||||||
invitation_code -> Varchar,
|
invitation_code -> Varchar,
|
||||||
disable_couple_photos -> Bool,
|
disable_couple_photos -> Bool,
|
||||||
enable_genealogy -> 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 -> families (family_id));
|
||||||
diesel::joinable!(couples -> photos (photo_id));
|
diesel::joinable!(couples -> photos (photo_id));
|
||||||
diesel::joinable!(members -> families (family_id));
|
diesel::joinable!(members -> families (family_id));
|
||||||
@@ -127,6 +178,9 @@ diesel::joinable!(memberships -> families (family_id));
|
|||||||
diesel::joinable!(memberships -> users (user_id));
|
diesel::joinable!(memberships -> users (user_id));
|
||||||
|
|
||||||
diesel::allow_tables_to_appear_in_same_query!(
|
diesel::allow_tables_to_appear_in_same_query!(
|
||||||
|
accommodations_list,
|
||||||
|
accommodations_reservations,
|
||||||
|
accommodations_reservations_cals_urls,
|
||||||
couples,
|
couples,
|
||||||
families,
|
families,
|
||||||
members,
|
members,
|
||||||
|
|||||||
102
geneit_backend/src/services/accommodations_list_service.rs
Normal file
102
geneit_backend/src/services/accommodations_list_service.rs
Normal 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(())
|
||||||
|
}
|
||||||
@@ -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(())
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -5,7 +5,9 @@ use crate::models::{
|
|||||||
Family, FamilyID, FamilyMembership, Membership, NewFamily, NewMembership, UserID,
|
Family, FamilyID, FamilyMembership, Membership, NewFamily, NewMembership, UserID,
|
||||||
};
|
};
|
||||||
use crate::schema::{families, memberships};
|
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::string_utils::rand_str;
|
||||||
use crate::utils::time_utils::time;
|
use crate::utils::time_utils::time;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
@@ -127,9 +129,9 @@ pub async fn update_membership(membership: &Membership) -> anyhow::Result<()> {
|
|||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
pub struct FamilyMember {
|
pub struct FamilyMember {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
membership: Membership,
|
pub membership: Membership,
|
||||||
user_name: String,
|
pub user_name: String,
|
||||||
user_mail: String,
|
pub user_mail: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get information about the users of a family
|
/// 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::name.eq(family.name.clone()),
|
||||||
families::dsl::invitation_code.eq(family.invitation_code.clone()),
|
families::dsl::invitation_code.eq(family.invitation_code.clone()),
|
||||||
families::dsl::enable_genealogy.eq(family.enable_genealogy),
|
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),
|
families::dsl::disable_couple_photos.eq(family.disable_couple_photos),
|
||||||
))
|
))
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
@@ -185,6 +188,9 @@ pub async fn update_family(family: &Family) -> anyhow::Result<()> {
|
|||||||
|
|
||||||
/// Delete a family
|
/// Delete a family
|
||||||
pub async fn delete_family(family_id: FamilyID) -> anyhow::Result<()> {
|
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
|
// Delete all family couples
|
||||||
couples_service::delete_all_family(family_id).await?;
|
couples_service::delete_all_family(family_id).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
//! # Backend services
|
//! # Backend services
|
||||||
|
|
||||||
|
pub mod accommodations_list_service;
|
||||||
|
pub mod accommodations_reservations_calendars_service;
|
||||||
|
pub mod accommodations_reservations_service;
|
||||||
pub mod couples_service;
|
pub mod couples_service;
|
||||||
pub mod families_service;
|
pub mod families_service;
|
||||||
pub mod login_token_service;
|
pub mod login_token_service;
|
||||||
|
|||||||
Reference in New Issue
Block a user