From c8ee881b2c028f4691b4c4dff922d02a6cb1bcc0 Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Thu, 16 May 2024 19:15:15 +0000 Subject: [PATCH] Genealogy as a feature (#175) Start our journey into turning GeneIT as afully featured family intranet by making genealogy a feature that can be disabled by family admins Reviewed-on: https://gitea.communiquons.org/pierre/GeneIT/pulls/175 --- geneit_app/src/App.tsx | 79 ++++--- geneit_app/src/api/FamilyApi.ts | 20 +- .../src/api/{ => genealogy}/CoupleApi.ts | 18 +- geneit_app/src/api/{ => genealogy}/DataApi.ts | 6 +- .../src/api/{ => genealogy}/MemberApi.ts | 16 +- .../src/routes/family/FamilyHomeRoute.tsx | 24 -- .../src/routes/family/FamilySettingsRoute.tsx | 145 ++---------- .../{ => genealogy}/FamilyCoupleRoute.tsx | 60 ++--- .../FamilyCouplesListRoute.tsx | 56 +++-- .../family/genealogy/FamilyHomeRoute.tsx | 17 ++ .../{ => genealogy}/FamilyMemberRoute.tsx | 105 ++++----- .../{ => genealogy}/FamilyMemberTreeRoute.tsx | 24 +- .../FamilyMembersListRoute.tsx | 58 +++-- .../{ => genealogy}/FamilyTreeRoute.tsx | 4 +- .../genealogy/GenalogySettingsRoute.tsx | 221 ++++++++++++++++++ .../family/genealogy/GenealogyHomeRoute.tsx | 20 ++ geneit_app/src/utils/family_tree.ts | 4 +- geneit_app/src/widgets/BaseFamilyRoute.tsx | 109 +++++---- geneit_app/src/widgets/BasicFamilyTree.tsx | 4 +- geneit_app/src/widgets/CouplePhoto.tsx | 2 +- geneit_app/src/widgets/MemberItem.tsx | 2 +- geneit_app/src/widgets/MemberPhoto.tsx | 2 +- geneit_app/src/widgets/forms/DateInput.tsx | 2 +- geneit_app/src/widgets/forms/MemberInput.tsx | 12 +- geneit_app/src/widgets/forms/SexSelection.tsx | 2 +- .../widgets/genealogy/BaseGenealogyRoute.tsx | 73 ++++++ .../simple_family_tree/SimpleFamilyTree.tsx | 4 +- .../down.sql | 3 + .../up.sql | 5 + .../src/controllers/families_controller.rs | 31 ++- geneit_backend/src/main.rs | 38 +-- geneit_backend/src/models.rs | 1 + geneit_backend/src/schema.rs | 1 + .../src/services/families_service.rs | 1 + 34 files changed, 726 insertions(+), 443 deletions(-) rename geneit_app/src/api/{ => genealogy}/CoupleApi.ts (90%) rename geneit_app/src/api/{ => genealogy}/DataApi.ts (78%) rename geneit_app/src/api/{ => genealogy}/MemberApi.ts (94%) delete mode 100644 geneit_app/src/routes/family/FamilyHomeRoute.tsx rename geneit_app/src/routes/family/{ => genealogy}/FamilyCoupleRoute.tsx (86%) rename geneit_app/src/routes/family/{ => genealogy}/FamilyCouplesListRoute.tsx (80%) create mode 100644 geneit_app/src/routes/family/genealogy/FamilyHomeRoute.tsx rename geneit_app/src/routes/family/{ => genealogy}/FamilyMemberRoute.tsx (87%) rename geneit_app/src/routes/family/{ => genealogy}/FamilyMemberTreeRoute.tsx (81%) rename geneit_app/src/routes/family/{ => genealogy}/FamilyMembersListRoute.tsx (76%) rename geneit_app/src/routes/family/{ => genealogy}/FamilyTreeRoute.tsx (87%) create mode 100644 geneit_app/src/routes/family/genealogy/GenalogySettingsRoute.tsx create mode 100644 geneit_app/src/routes/family/genealogy/GenealogyHomeRoute.tsx create mode 100644 geneit_app/src/widgets/genealogy/BaseGenealogyRoute.tsx create mode 100644 geneit_backend/migrations/2024-05-15-164434_genealogy_as_feature/down.sql create mode 100644 geneit_backend/migrations/2024-05-15-164434_genealogy_as_feature/up.sql diff --git a/geneit_app/src/App.tsx b/geneit_app/src/App.tsx index 2fdad99..a07dcd1 100644 --- a/geneit_app/src/App.tsx +++ b/geneit_app/src/App.tsx @@ -16,26 +16,29 @@ import { NewAccountRoute } from "./routes/auth/NewAccountRoute"; import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute"; import { PasswordForgottenRoute } from "./routes/auth/PasswordForgottenRoute"; import { ResetPasswordRoute } from "./routes/auth/ResetPasswordRoute"; -import { FamilyHomeRoute } from "./routes/family/FamilyHomeRoute"; +import { FamilyHomeRoute } from "./routes/family/genealogy/FamilyHomeRoute"; import { FamilyCreateMemberRoute, FamilyEditMemberRoute, FamilyMemberRoute, -} from "./routes/family/FamilyMemberRoute"; +} from "./routes/family/genealogy/FamilyMemberRoute"; import { FamilySettingsRoute } from "./routes/family/FamilySettingsRoute"; import { FamilyUsersListRoute } from "./routes/family/FamilyUsersListRoute"; import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage"; import { BaseFamilyRoute } from "./widgets/BaseFamilyRoute"; import { BaseLoginPage } from "./widgets/BaseLoginpage"; -import { FamilyMembersListRoute } from "./routes/family/FamilyMembersListRoute"; +import { FamilyMembersListRoute } from "./routes/family/genealogy/FamilyMembersListRoute"; import { FamilyCoupleRoute, FamilyCreateCoupleRoute, FamilyEditCoupleRoute, -} from "./routes/family/FamilyCoupleRoute"; -import { FamilyCouplesListRoute } from "./routes/family/FamilyCouplesListRoute"; -import { FamilyTreeRoute } from "./routes/family/FamilyTreeRoute"; -import { FamilyMemberTreeRoute } from "./routes/family/FamilyMemberTreeRoute"; +} from "./routes/family/genealogy/FamilyCoupleRoute"; +import { FamilyCouplesListRoute } from "./routes/family/genealogy/FamilyCouplesListRoute"; +import { FamilyTreeRoute } from "./routes/family/genealogy/FamilyTreeRoute"; +import { FamilyMemberTreeRoute } from "./routes/family/genealogy/FamilyMemberTreeRoute"; +import { GenealogyHomeRoute } from "./routes/family/genealogy/GenealogyHomeRoute"; +import { BaseGenealogyRoute } from "./widgets/genealogy/BaseGenealogyRoute"; +import { GenalogySettingsRoute } from "./routes/family/genealogy/GenalogySettingsRoute"; interface AuthContext { signedIn: boolean; @@ -67,33 +70,45 @@ export function App(): React.ReactElement { }> } /> - } /> - } - /> - } /> - } - /> + }> + } /> - } /> - } - /> - } /> - } - /> + } /> + } + /> + } + /> + } + /> - } /> - } - /> + } /> + } + /> + } + /> + } + /> + + } /> + } + /> + } /> + } /> + } /> } /> diff --git a/geneit_app/src/api/FamilyApi.ts b/geneit_app/src/api/FamilyApi.ts index 1518cac..31d97b6 100644 --- a/geneit_app/src/api/FamilyApi.ts +++ b/geneit_app/src/api/FamilyApi.ts @@ -1,6 +1,6 @@ import { APIClient } from "./ApiClient"; -import { Couple } from "./CoupleApi"; -import { Member } from "./MemberApi"; +import { Couple } from "./genealogy/CoupleApi"; +import { Member } from "./genealogy/MemberApi"; interface FamilyAPI { user_id: number; @@ -60,7 +60,8 @@ export class Family implements FamilyAPI { */ memberURL(member: Member, edit?: boolean): string { return ( - `/family/${this.family_id}/member/${member.id}` + (edit ? "/edit" : "") + `/family/${this.family_id}/genealogy/member/${member.id}` + + (edit ? "/edit" : "") ); } @@ -68,7 +69,7 @@ export class Family implements FamilyAPI { * Get family tree URL for member */ familyTreeURL(member: Member | number): string { - return `/family/${this.family_id}/tree/${ + return `/family/${this.family_id}/genealogy/tree/${ typeof member === "number" ? member : member.id }`; } @@ -78,16 +79,19 @@ export class Family implements FamilyAPI { */ coupleURL(member: Couple, edit?: boolean): string { return ( - `/family/${this.family_id}/couple/${member.id}` + (edit ? "/edit" : "") + `/family/${this.family_id}/genealogy/couple/${member.id}` + + (edit ? "/edit" : "") ); } } export class ExtendedFamilyInfo extends Family { public disable_couple_photos: boolean; + public enable_genealogy: boolean; constructor(p: any) { super(p); this.disable_couple_photos = p.disable_couple_photos; + this.enable_genealogy = p.enable_genealogy; } } @@ -229,14 +233,16 @@ export class FamilyApi { */ static async UpdateFamily(settings: { id: number; - name: string; - disable_couple_photos: boolean; + name?: string; + enable_genealogy?: boolean; + disable_couple_photos?: boolean; }): Promise { await APIClient.exec({ method: "PATCH", uri: `/family/${settings.id}`, jsonData: { name: settings.name, + enable_genealogy: settings.enable_genealogy, disable_couple_photos: settings.disable_couple_photos, }, }); diff --git a/geneit_app/src/api/CoupleApi.ts b/geneit_app/src/api/genealogy/CoupleApi.ts similarity index 90% rename from geneit_app/src/api/CoupleApi.ts rename to geneit_app/src/api/genealogy/CoupleApi.ts index 41234bd..bb12ae3 100644 --- a/geneit_app/src/api/CoupleApi.ts +++ b/geneit_app/src/api/genealogy/CoupleApi.ts @@ -1,6 +1,6 @@ -import { APIClient } from "./ApiClient"; +import { APIClient } from "../ApiClient"; import { DateValue, Member } from "./MemberApi"; -import { ServerApi } from "./ServerApi"; +import { ServerApi } from "../ServerApi"; interface CoupleApiInterface { id: number; @@ -161,7 +161,7 @@ export class CoupleApi { */ static async Create(m: Couple): Promise { const res = await APIClient.exec({ - uri: `/family/${m.family_id}/couple/create`, + uri: `/family/${m.family_id}/genealogy/couple/create`, method: "POST", jsonData: m, }); @@ -177,7 +177,7 @@ export class CoupleApi { couple_id: number ): Promise { const res = await APIClient.exec({ - uri: `/family/${family_id}/couple/${couple_id}`, + uri: `/family/${family_id}/genealogy/couple/${couple_id}`, method: "GET", }); @@ -189,7 +189,7 @@ export class CoupleApi { */ static async GetEntireList(family_id: number): Promise { const res = await APIClient.exec({ - uri: `/family/${family_id}/couples`, + uri: `/family/${family_id}/genealogy/couples`, method: "GET", }); @@ -201,7 +201,7 @@ export class CoupleApi { */ static async Update(m: Couple): Promise { await APIClient.exec({ - uri: `/family/${m.family_id}/couple/${m.id}`, + uri: `/family/${m.family_id}/genealogy/couple/${m.id}`, method: "PUT", jsonData: m, }); @@ -214,7 +214,7 @@ export class CoupleApi { const fd = new FormData(); fd.append("photo", b); await APIClient.exec({ - uri: `/family/${m.family_id}/couple/${m.id}/photo`, + uri: `/family/${m.family_id}/genealogy/couple/${m.id}/photo`, method: "PUT", formData: fd, }); @@ -225,7 +225,7 @@ export class CoupleApi { */ static async RemoveCouplePhoto(m: Couple): Promise { await APIClient.exec({ - uri: `/family/${m.family_id}/couple/${m.id}/photo`, + uri: `/family/${m.family_id}/genealogy/couple/${m.id}/photo`, method: "DELETE", }); } @@ -235,7 +235,7 @@ export class CoupleApi { */ static async Delete(m: Couple): Promise { await APIClient.exec({ - uri: `/family/${m.family_id}/couple/${m.id}`, + uri: `/family/${m.family_id}/genealogy/couple/${m.id}`, method: "DELETE", }); } diff --git a/geneit_app/src/api/DataApi.ts b/geneit_app/src/api/genealogy/DataApi.ts similarity index 78% rename from geneit_app/src/api/DataApi.ts rename to geneit_app/src/api/genealogy/DataApi.ts index 2b9a735..2c5281d 100644 --- a/geneit_app/src/api/DataApi.ts +++ b/geneit_app/src/api/genealogy/DataApi.ts @@ -1,4 +1,4 @@ -import { APIClient } from "./ApiClient"; +import { APIClient } from "../ApiClient"; /** * Data management api client @@ -9,7 +9,7 @@ export class DataApi { */ static async ExportData(family_id: number): Promise { const res = await APIClient.exec({ - uri: `/family/${family_id}/data/export`, + uri: `/family/${family_id}/genealogy/data/export`, method: "GET", }); return res.data; @@ -22,7 +22,7 @@ export class DataApi { const fd = new FormData(); fd.append("archive", archive); const res = await APIClient.exec({ - uri: `/family/${family_id}/data/import`, + uri: `/family/${family_id}/genealogy/data/import`, method: "PUT", formData: fd, }); diff --git a/geneit_app/src/api/MemberApi.ts b/geneit_app/src/api/genealogy/MemberApi.ts similarity index 94% rename from geneit_app/src/api/MemberApi.ts rename to geneit_app/src/api/genealogy/MemberApi.ts index 66b0d73..db989ab 100644 --- a/geneit_app/src/api/MemberApi.ts +++ b/geneit_app/src/api/genealogy/MemberApi.ts @@ -1,4 +1,4 @@ -import { APIClient } from "./ApiClient"; +import { APIClient } from "../ApiClient"; import { Couple } from "./CoupleApi"; export type Sex = "M" | "F"; @@ -278,7 +278,7 @@ export class MemberApi { */ static async Create(m: Member): Promise { const res = await APIClient.exec({ - uri: `/family/${m.family_id}/member/create`, + uri: `/family/${m.family_id}/genealogy/member/create`, method: "POST", jsonData: m, }); @@ -294,7 +294,7 @@ export class MemberApi { member_id: number ): Promise { const res = await APIClient.exec({ - uri: `/family/${family_id}/member/${member_id}`, + uri: `/family/${family_id}/genealogy/member/${member_id}`, method: "GET", }); @@ -306,7 +306,7 @@ export class MemberApi { */ static async GetEntireList(family_id: number): Promise { const res = await APIClient.exec({ - uri: `/family/${family_id}/members`, + uri: `/family/${family_id}/genealogy/members`, method: "GET", }); @@ -318,7 +318,7 @@ export class MemberApi { */ static async Update(m: Member): Promise { await APIClient.exec({ - uri: `/family/${m.family_id}/member/${m.id}`, + uri: `/family/${m.family_id}/genealogy/member/${m.id}`, method: "PUT", jsonData: m, }); @@ -331,7 +331,7 @@ export class MemberApi { const fd = new FormData(); fd.append("photo", b); await APIClient.exec({ - uri: `/family/${m.family_id}/member/${m.id}/photo`, + uri: `/family/${m.family_id}/genealogy/member/${m.id}/photo`, method: "PUT", formData: fd, }); @@ -342,7 +342,7 @@ export class MemberApi { */ static async RemoveMemberPhoto(m: Member): Promise { await APIClient.exec({ - uri: `/family/${m.family_id}/member/${m.id}/photo`, + uri: `/family/${m.family_id}/genealogy/member/${m.id}/photo`, method: "DELETE", }); } @@ -352,7 +352,7 @@ export class MemberApi { */ static async Delete(m: Member): Promise { await APIClient.exec({ - uri: `/family/${m.family_id}/member/${m.id}`, + uri: `/family/${m.family_id}/genealogy/member/${m.id}`, method: "DELETE", }); } diff --git a/geneit_app/src/routes/family/FamilyHomeRoute.tsx b/geneit_app/src/routes/family/FamilyHomeRoute.tsx deleted file mode 100644 index eaa5364..0000000 --- a/geneit_app/src/routes/family/FamilyHomeRoute.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { useFamily } from "../../widgets/BaseFamilyRoute"; -import { FamilyPageTitle } from "../../widgets/FamilyPageTitle"; - -export function FamilyHomeRoute(): React.ReactElement { - const family = useFamily(); - return ( - <> - -
-

- Bienvenue sur l'espace informatique dédié à la vie de votre famille ! - Veuillez utiliser le menu situé à gauche pour accéder aux différentes - sections de l'application. -

-

Nombre de fiches de membres: {family.members.size}

-

Nombre de fiches de couples: {family.couples.size}

-

- Vous pouvez inviter d'autres personnes à rejoindre cette famille en - leur donnant une copie du code d'invitation -

-
- - ); -} diff --git a/geneit_app/src/routes/family/FamilySettingsRoute.tsx b/geneit_app/src/routes/family/FamilySettingsRoute.tsx index 9f98d70..7c0d720 100644 --- a/geneit_app/src/routes/family/FamilySettingsRoute.tsx +++ b/geneit_app/src/routes/family/FamilySettingsRoute.tsx @@ -1,26 +1,19 @@ -import DownloadIcon from "@mui/icons-material/Download"; -import UploadIcon from "@mui/icons-material/Upload"; import { - Alert, Box, Button, CardActions, CardContent, - Checkbox, FormControlLabel, + Switch, TextField, - Tooltip, Typography, } from "@mui/material"; import React from "react"; import { useNavigate } from "react-router-dom"; -import { DataApi } from "../../api/DataApi"; import { FamilyApi } from "../../api/FamilyApi"; import { ServerApi } from "../../api/ServerApi"; import { useAlert } from "../../hooks/context_providers/AlertDialogProvider"; import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider"; -import { useLoadingMessage } from "../../hooks/context_providers/LoadingMessageProvider"; -import { downloadBlob, selectFileToUpload } from "../../utils/files_utils"; import { useFamily } from "../../widgets/BaseFamilyRoute"; import { FamilyCard } from "../../widgets/FamilyCard"; import { formatDate } from "../../widgets/TimeWidget"; @@ -55,7 +48,6 @@ export function FamilySettingsRoute(): React.ReactElement { return ( <> -
- - - - - ); -} diff --git a/geneit_app/src/routes/family/FamilyCoupleRoute.tsx b/geneit_app/src/routes/family/genealogy/FamilyCoupleRoute.tsx similarity index 86% rename from geneit_app/src/routes/family/FamilyCoupleRoute.tsx rename to geneit_app/src/routes/family/genealogy/FamilyCoupleRoute.tsx index 172e870..19e8f50 100644 --- a/geneit_app/src/routes/family/FamilyCoupleRoute.tsx +++ b/geneit_app/src/routes/family/genealogy/FamilyCoupleRoute.tsx @@ -6,26 +6,27 @@ import SaveIcon from "@mui/icons-material/Save"; import { Button, Grid, Stack } from "@mui/material"; import React from "react"; import { useNavigate, useParams } from "react-router-dom"; -import { Couple, CoupleApi } from "../../api/CoupleApi"; -import { Member } from "../../api/MemberApi"; -import { ServerApi } from "../../api/ServerApi"; -import { useAlert } from "../../hooks/context_providers/AlertDialogProvider"; -import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider"; -import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider"; -import { AsyncWidget } from "../../widgets/AsyncWidget"; -import { useFamily } from "../../widgets/BaseFamilyRoute"; -import { ConfirmLeaveWithoutSaveDialog } from "../../widgets/ConfirmLeaveWithoutSaveDialog"; -import { CouplePhoto } from "../../widgets/CouplePhoto"; -import { FamilyPageTitle } from "../../widgets/FamilyPageTitle"; -import { MemberItem } from "../../widgets/MemberItem"; -import { PropertiesBox } from "../../widgets/PropertiesBox"; -import { RouterLink } from "../../widgets/RouterLink"; -import { DateInput } from "../../widgets/forms/DateInput"; -import { MemberInput } from "../../widgets/forms/MemberInput"; -import { PropSelect } from "../../widgets/forms/PropSelect"; -import { UploadPhotoButton } from "../../widgets/forms/UploadPhotoButton"; -import { useQuery } from "../../hooks/useQuery"; -import { useLoadingMessage } from "../../hooks/context_providers/LoadingMessageProvider"; +import { ServerApi } from "../../../api/ServerApi"; +import { Couple, CoupleApi } from "../../../api/genealogy/CoupleApi"; +import { Member } from "../../../api/genealogy/MemberApi"; +import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider"; +import { useConfirm } from "../../../hooks/context_providers/ConfirmDialogProvider"; +import { useLoadingMessage } from "../../../hooks/context_providers/LoadingMessageProvider"; +import { useSnackbar } from "../../../hooks/context_providers/SnackbarProvider"; +import { useQuery } from "../../../hooks/useQuery"; +import { AsyncWidget } from "../../../widgets/AsyncWidget"; +import { useFamily } from "../../../widgets/BaseFamilyRoute"; +import { ConfirmLeaveWithoutSaveDialog } from "../../../widgets/ConfirmLeaveWithoutSaveDialog"; +import { CouplePhoto } from "../../../widgets/CouplePhoto"; +import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle"; +import { MemberItem } from "../../../widgets/MemberItem"; +import { PropertiesBox } from "../../../widgets/PropertiesBox"; +import { RouterLink } from "../../../widgets/RouterLink"; +import { DateInput } from "../../../widgets/forms/DateInput"; +import { MemberInput } from "../../../widgets/forms/MemberInput"; +import { PropSelect } from "../../../widgets/forms/PropSelect"; +import { UploadPhotoButton } from "../../../widgets/forms/UploadPhotoButton"; +import { useGenealogy } from "../../../widgets/genealogy/BaseGenealogyRoute"; /** * Create a new couple route @@ -36,6 +37,7 @@ export function FamilyCreateCoupleRoute(): React.ReactElement { const [shouldQuit, setShouldQuit] = React.useState(false); const n = useNavigate(); + const genealogy = useGenealogy(); const family = useFamily(); const params = useQuery(); @@ -49,7 +51,7 @@ export function FamilyCreateCoupleRoute(): React.ReactElement { try { const r = await CoupleApi.Create(m); - await family.reloadCouplesList(); + await genealogy.reloadCouplesList(); setShouldQuit(true); n(family.family.coupleURL(r)); @@ -62,7 +64,7 @@ export function FamilyCreateCoupleRoute(): React.ReactElement { const cancel = () => { setShouldQuit(true); - n(family.family.URL("couples")); + n(family.family.URL("genealogy/couples")); }; return ( @@ -89,6 +91,7 @@ export function FamilyCoupleRoute(): React.ReactElement { const snackbar = useSnackbar(); const family = useFamily(); + const genealogy = useGenealogy(); const { coupleId } = useParams(); const [couple, setCouple] = React.useState(); @@ -100,7 +103,7 @@ export function FamilyCoupleRoute(): React.ReactElement { count.current += 1; setCouple(undefined); - await family.reloadCouplesList(); + await genealogy.reloadCouplesList(); }; const deleteCouple = async () => { @@ -115,9 +118,9 @@ export function FamilyCoupleRoute(): React.ReactElement { await CoupleApi.Delete(couple!); snackbar("La fiche du couple a été supprimée avec succès !"); - n(family.family.URL("couples")); + n(family.family.URL("genealogy/couples")); - await family.reloadCouplesList(); + await genealogy.reloadCouplesList(); } catch (e) { console.error(e); alert("Échec de la suppression du couple !"); @@ -133,7 +136,7 @@ export function FamilyCoupleRoute(): React.ReactElement { build={() => ( (); @@ -176,7 +180,7 @@ export function FamilyEditCoupleRoute(): React.ReactElement { snackbar("Les informations du couple ont été mises à jour avec succès !"); - await family.reloadCouplesList(); + await genealogy.reloadCouplesList(); setShouldQuit(true); n(family.family.coupleURL(c, false)); @@ -486,7 +490,7 @@ export function CouplePage(p: {
diff --git a/geneit_app/src/routes/family/FamilyCouplesListRoute.tsx b/geneit_app/src/routes/family/genealogy/FamilyCouplesListRoute.tsx similarity index 80% rename from geneit_app/src/routes/family/FamilyCouplesListRoute.tsx rename to geneit_app/src/routes/family/genealogy/FamilyCouplesListRoute.tsx index c34c1f1..465f191 100644 --- a/geneit_app/src/routes/family/FamilyCouplesListRoute.tsx +++ b/geneit_app/src/routes/family/genealogy/FamilyCouplesListRoute.tsx @@ -6,17 +6,18 @@ import { Button, TextField, Tooltip } from "@mui/material"; import { DataGrid, GridActionsCellItem, GridColDef } from "@mui/x-data-grid"; import React from "react"; import { useNavigate } from "react-router-dom"; -import { Couple, CoupleApi } from "../../api/CoupleApi"; -import { dateTimestamp, fmtDate } from "../../api/MemberApi"; -import { ServerApi } from "../../api/ServerApi"; -import { useAlert } from "../../hooks/context_providers/AlertDialogProvider"; -import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider"; -import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider"; -import { useFamily } from "../../widgets/BaseFamilyRoute"; -import { CouplePhoto } from "../../widgets/CouplePhoto"; -import { FamilyPageTitle } from "../../widgets/FamilyPageTitle"; -import { MemberPhoto } from "../../widgets/MemberPhoto"; -import { RouterLink } from "../../widgets/RouterLink"; +import { Couple, CoupleApi } from "../../../api/genealogy/CoupleApi"; +import { dateTimestamp, fmtDate } from "../../../api/genealogy/MemberApi"; +import { ServerApi } from "../../../api/ServerApi"; +import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider"; +import { useConfirm } from "../../../hooks/context_providers/ConfirmDialogProvider"; +import { useSnackbar } from "../../../hooks/context_providers/SnackbarProvider"; +import { useFamily } from "../../../widgets/BaseFamilyRoute"; +import { CouplePhoto } from "../../../widgets/CouplePhoto"; +import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle"; +import { MemberPhoto } from "../../../widgets/MemberPhoto"; +import { RouterLink } from "../../../widgets/RouterLink"; +import { useGenealogy } from "../../../widgets/genealogy/BaseGenealogyRoute"; export function FamilyCouplesListRoute(): React.ReactElement { const alert = useAlert(); @@ -24,6 +25,7 @@ export function FamilyCouplesListRoute(): React.ReactElement { const snackbar = useSnackbar(); const family = useFamily(); + const genealogy = useGenealogy(); const [filter, setFilter] = React.useState(""); @@ -37,7 +39,7 @@ export function FamilyCouplesListRoute(): React.ReactElement { return; await CoupleApi.Delete(c); - await family.reloadCouplesList(); + await genealogy.reloadCouplesList(); snackbar("La fiche du couple a été supprimée avec succès !"); } catch (e) { @@ -63,7 +65,7 @@ export function FamilyCouplesListRoute(): React.ReactElement {
- {family.couples.isEmpty ? ( + {genealogy.couples.isEmpty ? (

Votre famille n'a aucun couple enregistré pour le moment ! Utilisez le bouton situé en haut à droite pour créer le premier ! @@ -81,16 +83,16 @@ export function FamilyCouplesListRoute(): React.ReactElement { (m.wife && - family.members + genealogy.members .get(m.wife)! .fullName.toLocaleLowerCase() .includes(filter.toLocaleLowerCase())) || (m.husband && - family.members + genealogy.members .get(m.husband)! .fullName.toLocaleLowerCase() .includes(filter.toLocaleLowerCase())) === true @@ -109,14 +111,18 @@ function CouplesTable(p: { onDelete: (m: Couple) => void; }): React.ReactElement { const family = useFamily(); + const genealogy = useGenealogy(); + const n = useNavigate(); const compareInvertedMembersNames = ( v1: number | undefined, v2: number | undefined ) => { - const n1 = ((v1 && family.members.get(v1)?.invertedFullName) ?? "") || ""; - const n2 = ((v2 && family.members.get(v2)?.invertedFullName) ?? "") || ""; + const n1 = + ((v1 && genealogy.members.get(v1)?.invertedFullName) ?? "") || ""; + const n2 = + ((v2 && genealogy.members.get(v2)?.invertedFullName) ?? "") || ""; return n1?.localeCompare(n2, undefined, { ignorePunctuation: true, @@ -132,7 +138,13 @@ function CouplesTable(p: { sortable: false, width: 60, renderCell(params) { - return ; + return ( +

+ +
+ ); }, }, @@ -253,10 +265,10 @@ function CouplesTable(p: { } function MemberCell(p: { id?: number }): React.ReactElement { - const family = useFamily(); + const genealogy = useGenealogy(); if (!p.id) return <>; - const member = family.members.get(p.id!)!; + const member = genealogy.members.get(p.id!)!; return ( diff --git a/geneit_app/src/routes/family/genealogy/FamilyHomeRoute.tsx b/geneit_app/src/routes/family/genealogy/FamilyHomeRoute.tsx new file mode 100644 index 0000000..4a66382 --- /dev/null +++ b/geneit_app/src/routes/family/genealogy/FamilyHomeRoute.tsx @@ -0,0 +1,17 @@ +import { useFamily } from "../../../widgets/BaseFamilyRoute"; +import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle"; + +export function FamilyHomeRoute(): React.ReactElement { + return ( + <> + +
+

+ Bienvenue sur l'espace informatique dédié à la vie de votre famille ! + Veuillez utiliser le menu situé à gauche pour accéder aux différentes + sections de l'application. +

+
+ + ); +} diff --git a/geneit_app/src/routes/family/FamilyMemberRoute.tsx b/geneit_app/src/routes/family/genealogy/FamilyMemberRoute.tsx similarity index 87% rename from geneit_app/src/routes/family/FamilyMemberRoute.tsx rename to geneit_app/src/routes/family/genealogy/FamilyMemberRoute.tsx index 7502189..5a5fa01 100644 --- a/geneit_app/src/routes/family/FamilyMemberRoute.tsx +++ b/geneit_app/src/routes/family/genealogy/FamilyMemberRoute.tsx @@ -1,3 +1,5 @@ +import { mdiFamilyTree } from "@mdi/js"; +import Icon from "@mdi/react"; import ClearIcon from "@mui/icons-material/Clear"; import DeleteIcon from "@mui/icons-material/Delete"; import EditIcon from "@mui/icons-material/Edit"; @@ -14,32 +16,31 @@ import { import * as EmailValidator from "email-validator"; import React from "react"; import { useNavigate, useParams } from "react-router-dom"; -import { Couple } from "../../api/CoupleApi"; -import { Member, MemberApi, fmtDate } from "../../api/MemberApi"; -import { ServerApi } from "../../api/ServerApi"; -import { useAlert } from "../../hooks/context_providers/AlertDialogProvider"; -import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider"; -import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider"; -import { AsyncWidget } from "../../widgets/AsyncWidget"; -import { useFamily } from "../../widgets/BaseFamilyRoute"; -import { ConfirmLeaveWithoutSaveDialog } from "../../widgets/ConfirmLeaveWithoutSaveDialog"; -import { CouplePhoto } from "../../widgets/CouplePhoto"; -import { FamilyPageTitle } from "../../widgets/FamilyPageTitle"; -import { MemberItem } from "../../widgets/MemberItem"; -import { MemberPhoto } from "../../widgets/MemberPhoto"; -import { PropertiesBox } from "../../widgets/PropertiesBox"; -import { RouterLink } from "../../widgets/RouterLink"; -import { DateInput } from "../../widgets/forms/DateInput"; -import { MemberInput } from "../../widgets/forms/MemberInput"; -import { PropCheckbox } from "../../widgets/forms/PropCheckbox"; -import { PropEdit } from "../../widgets/forms/PropEdit"; -import { PropSelect } from "../../widgets/forms/PropSelect"; -import { SexSelection } from "../../widgets/forms/SexSelection"; -import { UploadPhotoButton } from "../../widgets/forms/UploadPhotoButton"; -import { useQuery } from "../../hooks/useQuery"; -import { mdiFamilyTree } from "@mdi/js"; -import Icon from "@mdi/react"; -import { useLoadingMessage } from "../../hooks/context_providers/LoadingMessageProvider"; +import { ServerApi } from "../../../api/ServerApi"; +import { Couple } from "../../../api/genealogy/CoupleApi"; +import { Member, MemberApi, fmtDate } from "../../../api/genealogy/MemberApi"; +import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider"; +import { useConfirm } from "../../../hooks/context_providers/ConfirmDialogProvider"; +import { useLoadingMessage } from "../../../hooks/context_providers/LoadingMessageProvider"; +import { useSnackbar } from "../../../hooks/context_providers/SnackbarProvider"; +import { useQuery } from "../../../hooks/useQuery"; +import { AsyncWidget } from "../../../widgets/AsyncWidget"; +import { useFamily } from "../../../widgets/BaseFamilyRoute"; +import { ConfirmLeaveWithoutSaveDialog } from "../../../widgets/ConfirmLeaveWithoutSaveDialog"; +import { CouplePhoto } from "../../../widgets/CouplePhoto"; +import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle"; +import { MemberItem } from "../../../widgets/MemberItem"; +import { MemberPhoto } from "../../../widgets/MemberPhoto"; +import { PropertiesBox } from "../../../widgets/PropertiesBox"; +import { RouterLink } from "../../../widgets/RouterLink"; +import { DateInput } from "../../../widgets/forms/DateInput"; +import { MemberInput } from "../../../widgets/forms/MemberInput"; +import { PropCheckbox } from "../../../widgets/forms/PropCheckbox"; +import { PropEdit } from "../../../widgets/forms/PropEdit"; +import { PropSelect } from "../../../widgets/forms/PropSelect"; +import { SexSelection } from "../../../widgets/forms/SexSelection"; +import { UploadPhotoButton } from "../../../widgets/forms/UploadPhotoButton"; +import { useGenealogy } from "../../../widgets/genealogy/BaseGenealogyRoute"; /** * Create a new member route @@ -50,6 +51,7 @@ export function FamilyCreateMemberRoute(): React.ReactElement { const [shouldQuit, setShouldQuit] = React.useState(false); const n = useNavigate(); + const genealogy = useGenealogy(); const family = useFamily(); const parameters = useQuery(); @@ -60,10 +62,10 @@ export function FamilyCreateMemberRoute(): React.ReactElement { try { const r = await MemberApi.Create(m); - await family.reloadMembersList(); + await genealogy.reloadMembersList(); setShouldQuit(true); - n(family.family.URL(`member/${r.id}`)); + n(family.family.URL(`genealogy/member/${r.id}`)); snackbar(`La fiche pour ${r.fullName} a été créée avec succès !`); } catch (e) { console.error(e); @@ -73,7 +75,7 @@ export function FamilyCreateMemberRoute(): React.ReactElement { const cancel = () => { setShouldQuit(true); - n(family.family.URL("members")); + n(family.family.URL("genealogy/members")); }; const member = Member.New(family.family.family_id); @@ -104,6 +106,7 @@ export function FamilyMemberRoute(): React.ReactElement { const snackbar = useSnackbar(); const family = useFamily(); + const genealogy = useGenealogy(); const { memberId } = useParams(); const [member, setMember] = React.useState(); @@ -115,7 +118,7 @@ export function FamilyMemberRoute(): React.ReactElement { count.current += 1; setMember(undefined); - await family.reloadMembersList(); + await genealogy.reloadMembersList(); }; const deleteMember = async () => { @@ -130,9 +133,9 @@ export function FamilyMemberRoute(): React.ReactElement { await MemberApi.Delete(member!); snackbar("La fiche de membre a été supprimée avec succès !"); - n(family.family.URL("members")); + n(family.family.URL("genealogy/members")); - await family.reloadMembersList(); + await genealogy.reloadMembersList(); } catch (e) { console.error(e); alert("Échec de la suppression du membre !"); @@ -148,16 +151,14 @@ export function FamilyMemberRoute(): React.ReactElement { build={() => ( n(family.family.familyTreeURL(member!))} onRequestDelete={deleteMember} - onRequestEdit={() => - n(family.family.URL(`member/${member!.id}/edit`)) - } + onRequestEdit={() => n(family.family.memberURL(member!, true))} onForceReload={forceReload} /> )} @@ -178,6 +179,7 @@ export function FamilyEditMemberRoute(): React.ReactElement { const [shouldQuit, setShouldQuit] = React.useState(false); const family = useFamily(); + const genealogy = useGenealogy(); const [member, setMember] = React.useState(); const load = async () => { @@ -196,10 +198,10 @@ export function FamilyEditMemberRoute(): React.ReactElement { snackbar("Les informations du membre ont été mises à jour avec succès !"); - await family.reloadMembersList(); + await genealogy.reloadMembersList(); setShouldQuit(true); - n(family.family.URL(`member/${member!.id}`)); + n(family.family.memberURL(member!)); } catch (e) { console.error(e); alert("Échec de la mise à jour des informations du membre !"); @@ -662,9 +664,9 @@ export function MemberPage(p: {
@@ -682,10 +684,7 @@ export function MemberPage(p: { <>Aucun enfant ) : ( p.children.map((c) => ( - + )) @@ -694,7 +693,7 @@ export function MemberPage(p: {
Aucun frère ou sœur ) : ( p.siblings.map((c) => ( - + )) @@ -727,7 +723,7 @@ export function MemberPage(p: {
@@ -749,6 +745,7 @@ function CoupleItem(p: { const n = useNavigate(); const family = useFamily(); + const genealogy = useGenealogy(); const statusStr = ServerApi.Config.couples_states.find( (c) => c.code === p.couple.state @@ -766,7 +763,7 @@ function CoupleItem(p: { const otherSpouseID = p.couple.wife === p.currMemberId ? p.couple.husband : p.couple.wife; const otherSpouse = otherSpouseID - ? family.members.get(otherSpouseID) + ? genealogy.members.get(otherSpouseID) : undefined; return ( diff --git a/geneit_app/src/routes/family/FamilyMemberTreeRoute.tsx b/geneit_app/src/routes/family/genealogy/FamilyMemberTreeRoute.tsx similarity index 81% rename from geneit_app/src/routes/family/FamilyMemberTreeRoute.tsx rename to geneit_app/src/routes/family/genealogy/FamilyMemberTreeRoute.tsx index faa24b6..7996a83 100644 --- a/geneit_app/src/routes/family/FamilyMemberTreeRoute.tsx +++ b/geneit_app/src/routes/family/genealogy/FamilyMemberTreeRoute.tsx @@ -21,12 +21,13 @@ import { buildAscendingTree, buildDescendingTree, treeHeight, -} from "../../utils/family_tree"; -import { useFamily } from "../../widgets/BaseFamilyRoute"; -import { BasicFamilyTree } from "../../widgets/BasicFamilyTree"; -import { MemberItem } from "../../widgets/MemberItem"; -import { RouterLink } from "../../widgets/RouterLink"; -import { SimpleFamilyTree } from "../../widgets/simple_family_tree/SimpleFamilyTree"; +} from "../../../utils/family_tree"; +import { useFamily } from "../../../widgets/BaseFamilyRoute"; +import { BasicFamilyTree } from "../../../widgets/BasicFamilyTree"; +import { MemberItem } from "../../../widgets/MemberItem"; +import { RouterLink } from "../../../widgets/RouterLink"; +import { useGenealogy } from "../../../widgets/genealogy/BaseGenealogyRoute"; +import { SimpleFamilyTree } from "../../../widgets/simple_family_tree/SimpleFamilyTree"; enum CurrTab { BasicTree, @@ -41,22 +42,23 @@ enum TreeMode { export function FamilyMemberTreeRoute(): React.ReactElement { const { memberId } = useParams(); + const genealogy = useGenealogy(); const family = useFamily(); const [currTab, setCurrTab] = React.useState(CurrTab.SimpleTree); const [currMode, setCurrMode] = React.useState(TreeMode.Descending); - const member = family.members.get(Number(memberId)); + const member = genealogy.members.get(Number(memberId)); const memo: [FamilyTreeNode, number] | null = React.useMemo(() => { if (!member) return null; const tree = currMode === TreeMode.Ascending - ? buildAscendingTree(member.id, family.members, family.couples) - : buildDescendingTree(member.id, family.members, family.couples); + ? buildAscendingTree(member.id, genealogy.members, genealogy.couples) + : buildDescendingTree(member.id, genealogy.members, genealogy.couples); return [tree, treeHeight(tree)]; - }, [member, currMode, family.members, family.couples]); + }, [member, currMode, genealogy.members, genealogy.couples]); const [currDepth, setCurrDepth] = React.useState(0); @@ -87,7 +89,7 @@ export function FamilyMemberTreeRoute(): React.ReactElement { dense member={member} secondary={ - + diff --git a/geneit_app/src/routes/family/FamilyMembersListRoute.tsx b/geneit_app/src/routes/family/genealogy/FamilyMembersListRoute.tsx similarity index 76% rename from geneit_app/src/routes/family/FamilyMembersListRoute.tsx rename to geneit_app/src/routes/family/genealogy/FamilyMembersListRoute.tsx index 49cc89a..9313746 100644 --- a/geneit_app/src/routes/family/FamilyMembersListRoute.tsx +++ b/geneit_app/src/routes/family/genealogy/FamilyMembersListRoute.tsx @@ -8,20 +8,27 @@ import { Button, TextField, Tooltip, Typography } from "@mui/material"; import { DataGrid, GridActionsCellItem, GridColDef } from "@mui/x-data-grid"; import React from "react"; import { useNavigate } from "react-router-dom"; -import { Member, MemberApi, dateTimestamp, fmtDate } from "../../api/MemberApi"; -import { useAlert } from "../../hooks/context_providers/AlertDialogProvider"; -import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider"; -import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider"; -import { useFamily } from "../../widgets/BaseFamilyRoute"; -import { FamilyPageTitle } from "../../widgets/FamilyPageTitle"; -import { MemberPhoto } from "../../widgets/MemberPhoto"; -import { RouterLink } from "../../widgets/RouterLink"; +import { + Member, + MemberApi, + dateTimestamp, + fmtDate, +} from "../../../api/genealogy/MemberApi"; +import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider"; +import { useConfirm } from "../../../hooks/context_providers/ConfirmDialogProvider"; +import { useSnackbar } from "../../../hooks/context_providers/SnackbarProvider"; +import { useFamily } from "../../../widgets/BaseFamilyRoute"; +import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle"; +import { MemberPhoto } from "../../../widgets/MemberPhoto"; +import { RouterLink } from "../../../widgets/RouterLink"; +import { useGenealogy } from "../../../widgets/genealogy/BaseGenealogyRoute"; export function FamilyMembersListRoute(): React.ReactElement { const alert = useAlert(); const confirm = useConfirm(); const snackbar = useSnackbar(); + const genealogy = useGenealogy(); const family = useFamily(); const [filter, setFilter] = React.useState(""); @@ -36,7 +43,7 @@ export function FamilyMembersListRoute(): React.ReactElement { return; await MemberApi.Delete(m); - await family.reloadMembersList(); + await genealogy.reloadMembersList(); snackbar("La fiche du membre a été supprimée avec succès !"); } catch (e) { @@ -55,14 +62,14 @@ export function FamilyMembersListRoute(): React.ReactElement { }} > - +
- {family.members.isEmpty ? ( + {genealogy.members.isEmpty ? (

Votre famille n'a aucun membre pour le moment ! Utilisez le bouton situé en haut à droite pour créer le premier ! @@ -80,8 +87,8 @@ export function FamilyMembersListRoute(): React.ReactElement { + ? genealogy.members.fullList + : genealogy.members.filter((m) => m.fullName.toLowerCase().includes(filter.toLowerCase()) ) } @@ -97,6 +104,7 @@ function MembersTable(p: { members: Member[]; onDelete: (m: Member) => void; }): React.ReactElement { + const genealogy = useGenealogy(); const family = useFamily(); const n = useNavigate(); @@ -108,7 +116,13 @@ function MembersTable(p: { sortable: false, width: 60, renderCell(params) { - return ; + return ( +

+ +
+ ); }, }, @@ -166,8 +180,12 @@ function MembersTable(p: { flex: 5, renderCell(params) { if (!params.row.father) - return Non renseigné; - return family.members.get(params.row.father)!.fullName; + return ( + + Non renseigné + + ); + return genealogy.members.get(params.row.father)!.fullName; }, }, { @@ -176,8 +194,12 @@ function MembersTable(p: { flex: 5, renderCell(params) { if (!params.row.mother) - return Non renseignée; - return family.members.get(params.row.mother)!.fullName; + return ( + + Non renseignée + + ); + return genealogy.members.get(params.row.mother)!.fullName; }, }, { diff --git a/geneit_app/src/routes/family/FamilyTreeRoute.tsx b/geneit_app/src/routes/family/genealogy/FamilyTreeRoute.tsx similarity index 87% rename from geneit_app/src/routes/family/FamilyTreeRoute.tsx rename to geneit_app/src/routes/family/genealogy/FamilyTreeRoute.tsx index 86bda38..0e0a98e 100644 --- a/geneit_app/src/routes/family/FamilyTreeRoute.tsx +++ b/geneit_app/src/routes/family/genealogy/FamilyTreeRoute.tsx @@ -1,6 +1,6 @@ import { useNavigate } from "react-router-dom"; -import { useFamily } from "../../widgets/BaseFamilyRoute"; -import { MemberInput } from "../../widgets/forms/MemberInput"; +import { useFamily } from "../../../widgets/BaseFamilyRoute"; +import { MemberInput } from "../../../widgets/forms/MemberInput"; export function FamilyTreeRoute(): React.ReactElement { const n = useNavigate(); diff --git a/geneit_app/src/routes/family/genealogy/GenalogySettingsRoute.tsx b/geneit_app/src/routes/family/genealogy/GenalogySettingsRoute.tsx new file mode 100644 index 0000000..086ffca --- /dev/null +++ b/geneit_app/src/routes/family/genealogy/GenalogySettingsRoute.tsx @@ -0,0 +1,221 @@ +import DownloadIcon from "@mui/icons-material/Download"; +import UploadIcon from "@mui/icons-material/Upload"; +import { + Alert, + Box, + Button, + CardActions, + CardContent, + FormControlLabel, + Switch, + Tooltip, + Typography, +} from "@mui/material"; +import React from "react"; +import { FamilyApi } from "../../../api/FamilyApi"; +import { DataApi } from "../../../api/genealogy/DataApi"; +import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider"; +import { useConfirm } from "../../../hooks/context_providers/ConfirmDialogProvider"; +import { useLoadingMessage } from "../../../hooks/context_providers/LoadingMessageProvider"; +import { downloadBlob, selectFileToUpload } from "../../../utils/files_utils"; +import { useFamily } from "../../../widgets/BaseFamilyRoute"; +import { FamilyCard } from "../../../widgets/FamilyCard"; + +export function GenalogySettingsRoute(): React.ReactElement { + return ( + <> + + + + ); +} + +function GenealogySettingsCard(): React.ReactElement { + const alert = useAlert(); + + const family = useFamily(); + + const [disableCouplePhotos, setDisableCouplePhotos] = React.useState( + family.family.disable_couple_photos + ); + + const canEdit = family.family.is_admin; + + const [error, setError] = React.useState(); + const [success, setSuccess] = React.useState(); + + const updateFamily = async () => { + try { + setError(undefined); + setSuccess(undefined); + + await FamilyApi.UpdateFamily({ + id: family.family.family_id, + disable_couple_photos: disableCouplePhotos, + }); + + family.reloadFamilyInfo(); + + alert("Les paramètres de la famille ont été mis à jour avec succès !"); + } catch (e) { + console.error(e); + setError("Echec de la mise à jour des paramètres de la famille !"); + } + }; + + return ( + + + + Paramètres du module de généalogie + + + + + setDisableCouplePhotos(c)} + /> + } + label="Désactiver les photos de couple" + /> + + + + + + + + ); +} + +function GenealogyExportCard(): React.ReactElement { + const loading = useLoadingMessage(); + const confirm = useConfirm(); + const alert = useAlert(); + + const family = useFamily(); + + const [error, setError] = React.useState(); + const [success, setSuccess] = React.useState(); + + const exportData = async () => { + loading.show("Export des données"); + try { + setError(undefined); + setSuccess(undefined); + + const blob = await DataApi.ExportData(family.familyId); + downloadBlob(blob, `Export-${new Date().getTime()}.zip`); + + setSuccess("Export des données effectué avec succès !"); + } catch (e) { + console.error(e); + setError("Echec de l'export des données de la famille !"); + } + loading.hide(); + }; + + const importData = async () => { + try { + if ( + !(await confirm( + "Attention ! Cette opération a pour effet d'effacer toutes les données existantes en base ! Voulez-vous vraiment poursuivre l'opération ?" + )) + ) + return; + + const file = await selectFileToUpload({ + allowedTypes: ["application/zip"], + }); + if (file === null) return; + + setError(undefined); + setSuccess(undefined); + + loading.show( + "Restauration des données de généalogie de la famille en cours..." + ); + + await DataApi.ImportData(family.familyId, file); + + family.reloadFamilyInfo(); + + alert( + "Import des données de généalogie de la famille effectué avec succès !" + ); + } catch (e) { + console.error(e); + setError( + `Echec de l'import des données de généalogie de la famille ! (${e})` + ); + } + + loading.hide(); + }; + + return ( + + + + Export / import des données de généalogie + +

+ Vous pouvez, à des fins de sauvegardes ou de transfert, exporter et + importer l'ensemble des données des membres et des couples de cette + famille, sous format ZIP. +

+ + + Attention ! La restauration des données de généalogie de la famille + provoque préalablement l'effacement de toutes les données enregistrées + dans la famille ! Par ailleurs, la restauration n'est pas réversible ! + + +

 

+ + + + +
+
+ ); +} diff --git a/geneit_app/src/routes/family/genealogy/GenealogyHomeRoute.tsx b/geneit_app/src/routes/family/genealogy/GenealogyHomeRoute.tsx new file mode 100644 index 0000000..128ee7d --- /dev/null +++ b/geneit_app/src/routes/family/genealogy/GenealogyHomeRoute.tsx @@ -0,0 +1,20 @@ +import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle"; +import { useGenealogy } from "../../../widgets/genealogy/BaseGenealogyRoute"; + +export function GenealogyHomeRoute(): React.ReactElement { + const genealogy = useGenealogy(); + return ( + <> + +
+

+ Depuis cette section de l'application, vous pouvez afficher et + compléter l'abre généalogique de votre famille. +

+

 

+

Nombre de fiches de membres: {genealogy.members.size}

+

Nombre de fiches de couples: {genealogy.couples.size}

+
+ + ); +} diff --git a/geneit_app/src/utils/family_tree.ts b/geneit_app/src/utils/family_tree.ts index 5444540..62c9167 100644 --- a/geneit_app/src/utils/family_tree.ts +++ b/geneit_app/src/utils/family_tree.ts @@ -1,5 +1,5 @@ -import { Couple, CouplesList } from "../api/CoupleApi"; -import { Member, MembersList, dateTimestamp } from "../api/MemberApi"; +import { Couple, CouplesList } from "../api/genealogy/CoupleApi"; +import { Member, MembersList, dateTimestamp } from "../api/genealogy/MemberApi"; export interface CoupleInformation { couple: Couple; diff --git a/geneit_app/src/widgets/BaseFamilyRoute.tsx b/geneit_app/src/widgets/BaseFamilyRoute.tsx index 178b6d4..38e6830 100644 --- a/geneit_app/src/widgets/BaseFamilyRoute.tsx +++ b/geneit_app/src/widgets/BaseFamilyRoute.tsx @@ -4,6 +4,7 @@ import { mdiContentCopy, mdiCrowd, mdiFamilyTree, + mdiFileTree, mdiHumanMaleFemale, mdiLockCheck, mdiPlus, @@ -26,9 +27,7 @@ import { } from "@mui/material"; import React from "react"; import { Outlet, useLocation, useParams } from "react-router-dom"; -import { CoupleApi, CouplesList } from "../api/CoupleApi"; import { ExtendedFamilyInfo, FamilyApi } from "../api/FamilyApi"; -import { MemberApi, MembersList } from "../api/MemberApi"; import { useAlert } from "../hooks/context_providers/AlertDialogProvider"; import { useConfirm } from "../hooks/context_providers/ConfirmDialogProvider"; import { useSnackbar } from "../hooks/context_providers/SnackbarProvider"; @@ -37,12 +36,8 @@ import { RouterLink } from "./RouterLink"; interface FamilyContext { family: ExtendedFamilyInfo; - members: MembersList; - couples: CouplesList; familyId: number; reloadFamilyInfo: () => void; - reloadMembersList: () => Promise; - reloadCouplesList: () => Promise; } const FamilyContextK = React.createContext(null); @@ -54,8 +49,6 @@ export function BaseFamilyRoute(): React.ReactElement { const confirm = useConfirm(); const [family, setFamily] = React.useState(null); - const [members, setMembers] = React.useState(null); - const [couples, setCouples] = React.useState(null); const loadKey = React.useRef(1); @@ -64,15 +57,11 @@ export function BaseFamilyRoute(): React.ReactElement { const load = async () => { const familyID = Number(familyId); setFamily(await FamilyApi.GetSingle(familyID)); - setMembers(await MemberApi.GetEntireList(familyID)); - setCouples(await CoupleApi.GetEntireList(familyID)); }; const onReload = async () => { loadKey.current += 1; setFamily(null); - setMembers(null); - setCouples(null); return new Promise((res, _rej) => { loadPromise.current = () => res(); @@ -106,7 +95,7 @@ export function BaseFamilyRoute(): React.ReactElement { return ( } label="Accueil" uri="" /> - } - label="Membres" - uri="members" - secondaryAction={ - - - - - - - - } - /> + {family?.enable_genealogy && ( + <> + + Généalogie - } - label="Couples" - uri="couples" - secondaryAction={ - - - - - - - - } - /> + } + label="Accueil" + uri="genealogy" + /> + } + label="Membres" + uri="genealogy/members" + secondaryAction={ + + + + + + + + } + /> - } - label="Arbre" - uri="tree" - /> + } + label="Couples" + uri="genealogy/couples" + secondaryAction={ + + + + + + + + } + /> + + } + label="Arbre" + uri="genealogy/tree" + /> + + )} Administration @@ -198,6 +199,14 @@ export function BaseFamilyRoute(): React.ReactElement { uri="settings" /> + {family?.enable_genealogy && ( + } + label="Généalogie" + uri="genealogy/settings" + /> + )} + {/* Invitation code */} {p.label} @@ -30,7 +32,7 @@ export function MemberInput(p: { onClick={ !p.editable ? () => { - n(family.family.URL(`member/${member.id}`)); + n(family.family.memberURL(member)); } : undefined } @@ -55,7 +57,7 @@ export function MemberInput(p: { return ( { p.onValueChange(newValue?.id); }} diff --git a/geneit_app/src/widgets/forms/SexSelection.tsx b/geneit_app/src/widgets/forms/SexSelection.tsx index 49f53e5..941379f 100644 --- a/geneit_app/src/widgets/forms/SexSelection.tsx +++ b/geneit_app/src/widgets/forms/SexSelection.tsx @@ -6,7 +6,7 @@ import { Radio, Typography, } from "@mui/material"; -import { Sex } from "../../api/MemberApi"; +import { Sex } from "../../api/genealogy/MemberApi"; export function SexSelection(p: { readonly?: boolean; diff --git a/geneit_app/src/widgets/genealogy/BaseGenealogyRoute.tsx b/geneit_app/src/widgets/genealogy/BaseGenealogyRoute.tsx new file mode 100644 index 0000000..247663c --- /dev/null +++ b/geneit_app/src/widgets/genealogy/BaseGenealogyRoute.tsx @@ -0,0 +1,73 @@ +import React from "react"; +import { Outlet } from "react-router-dom"; +import { CoupleApi, CouplesList } from "../../api/genealogy/CoupleApi"; +import { MemberApi, MembersList } from "../../api/genealogy/MemberApi"; +import { AsyncWidget } from "../AsyncWidget"; +import { useFamily } from "../BaseFamilyRoute"; + +interface FamilyContext { + members: MembersList; + couples: CouplesList; + reloadMembersList: () => Promise; + reloadCouplesList: () => Promise; +} + +const GenealogyContextK = React.createContext(null); + +export function BaseGenealogyRoute(): React.ReactElement { + const family = useFamily(); + + const [members, setMembers] = React.useState(null); + const [couples, setCouples] = React.useState(null); + + const loadKey = React.useRef(1); + + const loadPromise = React.useRef<() => void>(); + + const load = async () => { + setMembers(await MemberApi.GetEntireList(family.familyId)); + setCouples(await CoupleApi.GetEntireList(family.familyId)); + }; + + const onReload = async () => { + loadKey.current += 1; + setMembers(null); + setCouples(null); + + return new Promise((res, _rej) => { + loadPromise.current = () => res(); + }); + }; + + return ( + { + if (loadPromise.current != null) { + loadPromise.current?.(); + loadPromise.current = undefined; + } + + return ( + + + + ); + }} + /> + ); +} + +export function useGenealogy(): FamilyContext { + return React.useContext(GenealogyContextK)!; +} diff --git a/geneit_app/src/widgets/simple_family_tree/SimpleFamilyTree.tsx b/geneit_app/src/widgets/simple_family_tree/SimpleFamilyTree.tsx index e13c041..c784dc2 100644 --- a/geneit_app/src/widgets/simple_family_tree/SimpleFamilyTree.tsx +++ b/geneit_app/src/widgets/simple_family_tree/SimpleFamilyTree.tsx @@ -5,8 +5,8 @@ import { IconButton, Tooltip } from "@mui/material"; import jsPDF from "jspdf"; import React from "react"; import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch"; -import { Couple } from "../../api/CoupleApi"; -import { Member } from "../../api/MemberApi"; +import { Couple } from "../../api/genealogy/CoupleApi"; +import { Member } from "../../api/genealogy/MemberApi"; import { useDarkTheme } from "../../hooks/context_providers/DarkThemeProvider"; import { FamilyTreeNode } from "../../utils/family_tree"; import { downloadBlob } from "../../utils/files_utils"; diff --git a/geneit_backend/migrations/2024-05-15-164434_genealogy_as_feature/down.sql b/geneit_backend/migrations/2024-05-15-164434_genealogy_as_feature/down.sql new file mode 100644 index 0000000..88da697 --- /dev/null +++ b/geneit_backend/migrations/2024-05-15-164434_genealogy_as_feature/down.sql @@ -0,0 +1,3 @@ +-- Remove column to toggle genealogy +ALTER TABLE public.families + DROP COLUMN enable_genealogy; \ No newline at end of file diff --git a/geneit_backend/migrations/2024-05-15-164434_genealogy_as_feature/up.sql b/geneit_backend/migrations/2024-05-15-164434_genealogy_as_feature/up.sql new file mode 100644 index 0000000..4238972 --- /dev/null +++ b/geneit_backend/migrations/2024-05-15-164434_genealogy_as_feature/up.sql @@ -0,0 +1,5 @@ +-- Add column to toggle genealogy +ALTER TABLE public.families + ADD enable_genealogy boolean NOT NULL DEFAULT false; +COMMENT +ON COLUMN public.families.enable_genealogy IS 'Specify whether genealogy feature is enabled for the family'; diff --git a/geneit_backend/src/controllers/families_controller.rs b/geneit_backend/src/controllers/families_controller.rs index d5c3f92..b5b4c3e 100644 --- a/geneit_backend/src/controllers/families_controller.rs +++ b/geneit_backend/src/controllers/families_controller.rs @@ -79,6 +79,7 @@ pub async fn list(token: LoginToken) -> HttpResult { struct RichFamilyInfo { #[serde(flatten)] membership: FamilyMembership, + enable_genealogy: bool, disable_couple_photos: bool, } @@ -88,6 +89,7 @@ pub async fn single_info(f: FamilyInPath) -> HttpResult { let family = families_service::get_by_id(f.family_id()).await?; Ok(HttpResponse::Ok().json(RichFamilyInfo { membership, + enable_genealogy: family.enable_genealogy, disable_couple_photos: family.disable_couple_photos, })) } @@ -101,8 +103,9 @@ pub async fn leave(f: FamilyInPath) -> HttpResult { #[derive(serde::Deserialize)] pub struct UpdateFamilyBody { - name: String, - disable_couple_photos: bool, + name: Option, + enable_genealogy: Option, + disable_couple_photos: Option, } /// Update a family @@ -110,16 +113,24 @@ pub async fn update( f: FamilyInPathWithAdminMembership, req: web::Json, ) -> HttpResult { - if !StaticConstraints::default() - .family_name_len - .validate(&req.name) - { - return Ok(HttpResponse::BadRequest().body("Invalid family name!")); + let mut family = families_service::get_by_id(f.family_id()).await?; + + if let Some(name) = &req.name { + if !StaticConstraints::default().family_name_len.validate(name) { + return Ok(HttpResponse::BadRequest().body("Invalid family name!")); + } + + family.name = name.to_string(); + } + + if let Some(enable_genealogy) = req.enable_genealogy { + family.enable_genealogy = enable_genealogy; + } + + if let Some(disable_couple_photos) = req.disable_couple_photos { + family.disable_couple_photos = disable_couple_photos; } - let mut family = families_service::get_by_id(f.family_id()).await?; - family.name = req.0.name; - family.disable_couple_photos = req.0.disable_couple_photos; families_service::update_family(&family).await?; log::info!("User {:?} updated family {:?}", f.user_id(), f.family_id()); diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index 711766b..683dfcb 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -137,71 +137,71 @@ async fn main() -> std::io::Result<()> { "/family/{id}/user/{user_id}", web::delete().to(families_controller::delete_membership), ) - // Members controller + // [GENEALOGY] Members controller .route( - "/family/{id}/member/create", + "/family/{id}/genealogy/member/create", web::post().to(members_controller::create), ) .route( - "/family/{id}/members", + "/family/{id}/genealogy/members", web::get().to(members_controller::get_all), ) .route( - "/family/{id}/member/{member_id}", + "/family/{id}/genealogy/member/{member_id}", web::get().to(members_controller::get_single), ) .route( - "/family/{id}/member/{member_id}", + "/family/{id}/genealogy/member/{member_id}", web::put().to(members_controller::update), ) .route( - "/family/{id}/member/{member_id}", + "/family/{id}/genealogy/member/{member_id}", web::delete().to(members_controller::delete), ) .route( - "/family/{id}/member/{member_id}/photo", + "/family/{id}/genealogy/member/{member_id}/photo", web::put().to(members_controller::set_photo), ) .route( - "/family/{id}/member/{member_id}/photo", + "/family/{id}/genealogy/member/{member_id}/photo", web::delete().to(members_controller::remove_photo), ) - // Couples controller + // [GENEALOGY] Couples controller .route( - "/family/{id}/couple/create", + "/family/{id}/genealogy/couple/create", web::post().to(couples_controller::create), ) .route( - "/family/{id}/couples", + "/family/{id}/genealogy/couples", web::get().to(couples_controller::get_all), ) .route( - "/family/{id}/couple/{couple_id}", + "/family/{id}/genealogy/couple/{couple_id}", web::get().to(couples_controller::get_single), ) .route( - "/family/{id}/couple/{couple_id}", + "/family/{id}/genealogy/couple/{couple_id}", web::put().to(couples_controller::update), ) .route( - "/family/{id}/couple/{couple_id}", + "/family/{id}/genealogy/couple/{couple_id}", web::delete().to(couples_controller::delete), ) .route( - "/family/{id}/couple/{couple_id}/photo", + "/family/{id}/genealogy/couple/{couple_id}/photo", web::put().to(couples_controller::set_photo), ) .route( - "/family/{id}/couple/{couple_id}/photo", + "/family/{id}/genealogy/couple/{couple_id}/photo", web::delete().to(couples_controller::remove_photo), ) - // Data controller + // [GENEALOGY] Data controller .route( - "/family/{id}/data/export", + "/family/{id}/genealogy/data/export", web::get().to(data_controller::export_family), ) .route( - "/family/{id}/data/import", + "/family/{id}/genealogy/data/import", web::put().to(data_controller::import_family), ) // Photos controller diff --git a/geneit_backend/src/models.rs b/geneit_backend/src/models.rs index e901fac..596d092 100644 --- a/geneit_backend/src/models.rs +++ b/geneit_backend/src/models.rs @@ -65,6 +65,7 @@ pub struct Family { pub name: String, pub invitation_code: String, pub disable_couple_photos: bool, + pub enable_genealogy: bool, } impl Family { diff --git a/geneit_backend/src/schema.rs b/geneit_backend/src/schema.rs index 708d30a..f681f84 100644 --- a/geneit_backend/src/schema.rs +++ b/geneit_backend/src/schema.rs @@ -29,6 +29,7 @@ diesel::table! { #[max_length = 7] invitation_code -> Varchar, disable_couple_photos -> Bool, + enable_genealogy -> Bool, } } diff --git a/geneit_backend/src/services/families_service.rs b/geneit_backend/src/services/families_service.rs index 77c6b0c..af6dd93 100644 --- a/geneit_backend/src/services/families_service.rs +++ b/geneit_backend/src/services/families_service.rs @@ -174,6 +174,7 @@ pub async fn update_family(family: &Family) -> anyhow::Result<()> { .set(( families::dsl::name.eq(family.name.clone()), families::dsl::invitation_code.eq(family.invitation_code.clone()), + families::dsl::enable_genealogy.eq(family.enable_genealogy), families::dsl::disable_couple_photos.eq(family.disable_couple_photos), )) .execute(conn)