Genealogy as a feature (#175)
All checks were successful
continuous-integration/drone/push Build is passing

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: #175
This commit is contained in:
Pierre HUBERT 2024-05-16 19:15:15 +00:00
parent 0442538bd5
commit c8ee881b2c
34 changed files with 726 additions and 443 deletions

View File

@ -16,26 +16,29 @@ 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/FamilyHomeRoute"; import { FamilyHomeRoute } from "./routes/family/genealogy/FamilyHomeRoute";
import { import {
FamilyCreateMemberRoute, FamilyCreateMemberRoute,
FamilyEditMemberRoute, FamilyEditMemberRoute,
FamilyMemberRoute, FamilyMemberRoute,
} from "./routes/family/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 { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
import { BaseFamilyRoute } from "./widgets/BaseFamilyRoute"; import { BaseFamilyRoute } from "./widgets/BaseFamilyRoute";
import { BaseLoginPage } from "./widgets/BaseLoginpage"; import { BaseLoginPage } from "./widgets/BaseLoginpage";
import { FamilyMembersListRoute } from "./routes/family/FamilyMembersListRoute"; import { FamilyMembersListRoute } from "./routes/family/genealogy/FamilyMembersListRoute";
import { import {
FamilyCoupleRoute, FamilyCoupleRoute,
FamilyCreateCoupleRoute, FamilyCreateCoupleRoute,
FamilyEditCoupleRoute, FamilyEditCoupleRoute,
} from "./routes/family/FamilyCoupleRoute"; } from "./routes/family/genealogy/FamilyCoupleRoute";
import { FamilyCouplesListRoute } from "./routes/family/FamilyCouplesListRoute"; import { FamilyCouplesListRoute } from "./routes/family/genealogy/FamilyCouplesListRoute";
import { FamilyTreeRoute } from "./routes/family/FamilyTreeRoute"; import { FamilyTreeRoute } from "./routes/family/genealogy/FamilyTreeRoute";
import { FamilyMemberTreeRoute } from "./routes/family/FamilyMemberTreeRoute"; 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 { interface AuthContext {
signedIn: boolean; signedIn: boolean;
@ -67,33 +70,45 @@ export function App(): React.ReactElement {
<Route path="family/:familyId/*" element={<BaseFamilyRoute />}> <Route path="family/:familyId/*" element={<BaseFamilyRoute />}>
<Route path="" element={<FamilyHomeRoute />} /> <Route path="" element={<FamilyHomeRoute />} />
<Route path="members" element={<FamilyMembersListRoute />} /> <Route path="genealogy/*" element={<BaseGenealogyRoute />}>
<Route <Route path="" element={<GenealogyHomeRoute />} />
path="member/create"
element={<FamilyCreateMemberRoute />}
/>
<Route path="member/:memberId" element={<FamilyMemberRoute />} />
<Route
path="member/:memberId/edit"
element={<FamilyEditMemberRoute />}
/>
<Route path="couples" element={<FamilyCouplesListRoute />} /> <Route path="members" element={<FamilyMembersListRoute />} />
<Route <Route
path="couple/create" path="member/create"
element={<FamilyCreateCoupleRoute />} element={<FamilyCreateMemberRoute />}
/> />
<Route path="couple/:coupleId" element={<FamilyCoupleRoute />} /> <Route
<Route path="member/:memberId"
path="couple/:coupleId/edit" element={<FamilyMemberRoute />}
element={<FamilyEditCoupleRoute />} />
/> <Route
path="member/:memberId/edit"
element={<FamilyEditMemberRoute />}
/>
<Route path="tree" element={<FamilyTreeRoute />} /> <Route path="couples" element={<FamilyCouplesListRoute />} />
<Route <Route
path="tree/:memberId" path="couple/create"
element={<FamilyMemberTreeRoute />} element={<FamilyCreateCoupleRoute />}
/> />
<Route
path="couple/:coupleId"
element={<FamilyCoupleRoute />}
/>
<Route
path="couple/:coupleId/edit"
element={<FamilyEditCoupleRoute />}
/>
<Route path="tree" element={<FamilyTreeRoute />} />
<Route
path="tree/:memberId"
element={<FamilyMemberTreeRoute />}
/>
<Route path="settings" element={<GenalogySettingsRoute />} />
<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 />} />

View File

@ -1,6 +1,6 @@
import { APIClient } from "./ApiClient"; import { APIClient } from "./ApiClient";
import { Couple } from "./CoupleApi"; import { Couple } from "./genealogy/CoupleApi";
import { Member } from "./MemberApi"; import { Member } from "./genealogy/MemberApi";
interface FamilyAPI { interface FamilyAPI {
user_id: number; user_id: number;
@ -60,7 +60,8 @@ export class Family implements FamilyAPI {
*/ */
memberURL(member: Member, edit?: boolean): string { memberURL(member: Member, edit?: boolean): string {
return ( 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 * Get family tree URL for member
*/ */
familyTreeURL(member: Member | number): string { familyTreeURL(member: Member | number): string {
return `/family/${this.family_id}/tree/${ return `/family/${this.family_id}/genealogy/tree/${
typeof member === "number" ? member : member.id typeof member === "number" ? member : member.id
}`; }`;
} }
@ -78,16 +79,19 @@ export class Family implements FamilyAPI {
*/ */
coupleURL(member: Couple, edit?: boolean): string { coupleURL(member: Couple, edit?: boolean): string {
return ( 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 { export class ExtendedFamilyInfo extends Family {
public disable_couple_photos: boolean; public disable_couple_photos: boolean;
public enable_genealogy: 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;
} }
} }
@ -229,14 +233,16 @@ export class FamilyApi {
*/ */
static async UpdateFamily(settings: { static async UpdateFamily(settings: {
id: number; id: number;
name: string; name?: string;
disable_couple_photos: boolean; enable_genealogy?: boolean;
disable_couple_photos?: boolean;
}): Promise<void> { }): Promise<void> {
await APIClient.exec({ await APIClient.exec({
method: "PATCH", method: "PATCH",
uri: `/family/${settings.id}`, uri: `/family/${settings.id}`,
jsonData: { jsonData: {
name: settings.name, name: settings.name,
enable_genealogy: settings.enable_genealogy,
disable_couple_photos: settings.disable_couple_photos, disable_couple_photos: settings.disable_couple_photos,
}, },
}); });

View File

@ -1,6 +1,6 @@
import { APIClient } from "./ApiClient"; import { APIClient } from "../ApiClient";
import { DateValue, Member } from "./MemberApi"; import { DateValue, Member } from "./MemberApi";
import { ServerApi } from "./ServerApi"; import { ServerApi } from "../ServerApi";
interface CoupleApiInterface { interface CoupleApiInterface {
id: number; id: number;
@ -161,7 +161,7 @@ export class CoupleApi {
*/ */
static async Create(m: Couple): Promise<Couple> { static async Create(m: Couple): Promise<Couple> {
const res = await APIClient.exec({ const res = await APIClient.exec({
uri: `/family/${m.family_id}/couple/create`, uri: `/family/${m.family_id}/genealogy/couple/create`,
method: "POST", method: "POST",
jsonData: m, jsonData: m,
}); });
@ -177,7 +177,7 @@ export class CoupleApi {
couple_id: number couple_id: number
): Promise<Couple> { ): Promise<Couple> {
const res = await APIClient.exec({ const res = await APIClient.exec({
uri: `/family/${family_id}/couple/${couple_id}`, uri: `/family/${family_id}/genealogy/couple/${couple_id}`,
method: "GET", method: "GET",
}); });
@ -189,7 +189,7 @@ export class CoupleApi {
*/ */
static async GetEntireList(family_id: number): Promise<CouplesList> { static async GetEntireList(family_id: number): Promise<CouplesList> {
const res = await APIClient.exec({ const res = await APIClient.exec({
uri: `/family/${family_id}/couples`, uri: `/family/${family_id}/genealogy/couples`,
method: "GET", method: "GET",
}); });
@ -201,7 +201,7 @@ export class CoupleApi {
*/ */
static async Update(m: Couple): Promise<void> { static async Update(m: Couple): Promise<void> {
await APIClient.exec({ await APIClient.exec({
uri: `/family/${m.family_id}/couple/${m.id}`, uri: `/family/${m.family_id}/genealogy/couple/${m.id}`,
method: "PUT", method: "PUT",
jsonData: m, jsonData: m,
}); });
@ -214,7 +214,7 @@ export class CoupleApi {
const fd = new FormData(); const fd = new FormData();
fd.append("photo", b); fd.append("photo", b);
await APIClient.exec({ await APIClient.exec({
uri: `/family/${m.family_id}/couple/${m.id}/photo`, uri: `/family/${m.family_id}/genealogy/couple/${m.id}/photo`,
method: "PUT", method: "PUT",
formData: fd, formData: fd,
}); });
@ -225,7 +225,7 @@ export class CoupleApi {
*/ */
static async RemoveCouplePhoto(m: Couple): Promise<void> { static async RemoveCouplePhoto(m: Couple): Promise<void> {
await APIClient.exec({ await APIClient.exec({
uri: `/family/${m.family_id}/couple/${m.id}/photo`, uri: `/family/${m.family_id}/genealogy/couple/${m.id}/photo`,
method: "DELETE", method: "DELETE",
}); });
} }
@ -235,7 +235,7 @@ export class CoupleApi {
*/ */
static async Delete(m: Couple): Promise<void> { static async Delete(m: Couple): Promise<void> {
await APIClient.exec({ await APIClient.exec({
uri: `/family/${m.family_id}/couple/${m.id}`, uri: `/family/${m.family_id}/genealogy/couple/${m.id}`,
method: "DELETE", method: "DELETE",
}); });
} }

View File

@ -1,4 +1,4 @@
import { APIClient } from "./ApiClient"; import { APIClient } from "../ApiClient";
/** /**
* Data management api client * Data management api client
@ -9,7 +9,7 @@ export class DataApi {
*/ */
static async ExportData(family_id: number): Promise<Blob> { static async ExportData(family_id: number): Promise<Blob> {
const res = await APIClient.exec({ const res = await APIClient.exec({
uri: `/family/${family_id}/data/export`, uri: `/family/${family_id}/genealogy/data/export`,
method: "GET", method: "GET",
}); });
return res.data; return res.data;
@ -22,7 +22,7 @@ export class DataApi {
const fd = new FormData(); const fd = new FormData();
fd.append("archive", archive); fd.append("archive", archive);
const res = await APIClient.exec({ const res = await APIClient.exec({
uri: `/family/${family_id}/data/import`, uri: `/family/${family_id}/genealogy/data/import`,
method: "PUT", method: "PUT",
formData: fd, formData: fd,
}); });

View File

@ -1,4 +1,4 @@
import { APIClient } from "./ApiClient"; import { APIClient } from "../ApiClient";
import { Couple } from "./CoupleApi"; import { Couple } from "./CoupleApi";
export type Sex = "M" | "F"; export type Sex = "M" | "F";
@ -278,7 +278,7 @@ export class MemberApi {
*/ */
static async Create(m: Member): Promise<Member> { static async Create(m: Member): Promise<Member> {
const res = await APIClient.exec({ const res = await APIClient.exec({
uri: `/family/${m.family_id}/member/create`, uri: `/family/${m.family_id}/genealogy/member/create`,
method: "POST", method: "POST",
jsonData: m, jsonData: m,
}); });
@ -294,7 +294,7 @@ export class MemberApi {
member_id: number member_id: number
): Promise<Member> { ): Promise<Member> {
const res = await APIClient.exec({ const res = await APIClient.exec({
uri: `/family/${family_id}/member/${member_id}`, uri: `/family/${family_id}/genealogy/member/${member_id}`,
method: "GET", method: "GET",
}); });
@ -306,7 +306,7 @@ export class MemberApi {
*/ */
static async GetEntireList(family_id: number): Promise<MembersList> { static async GetEntireList(family_id: number): Promise<MembersList> {
const res = await APIClient.exec({ const res = await APIClient.exec({
uri: `/family/${family_id}/members`, uri: `/family/${family_id}/genealogy/members`,
method: "GET", method: "GET",
}); });
@ -318,7 +318,7 @@ export class MemberApi {
*/ */
static async Update(m: Member): Promise<void> { static async Update(m: Member): Promise<void> {
await APIClient.exec({ await APIClient.exec({
uri: `/family/${m.family_id}/member/${m.id}`, uri: `/family/${m.family_id}/genealogy/member/${m.id}`,
method: "PUT", method: "PUT",
jsonData: m, jsonData: m,
}); });
@ -331,7 +331,7 @@ export class MemberApi {
const fd = new FormData(); const fd = new FormData();
fd.append("photo", b); fd.append("photo", b);
await APIClient.exec({ await APIClient.exec({
uri: `/family/${m.family_id}/member/${m.id}/photo`, uri: `/family/${m.family_id}/genealogy/member/${m.id}/photo`,
method: "PUT", method: "PUT",
formData: fd, formData: fd,
}); });
@ -342,7 +342,7 @@ export class MemberApi {
*/ */
static async RemoveMemberPhoto(m: Member): Promise<void> { static async RemoveMemberPhoto(m: Member): Promise<void> {
await APIClient.exec({ await APIClient.exec({
uri: `/family/${m.family_id}/member/${m.id}/photo`, uri: `/family/${m.family_id}/genealogy/member/${m.id}/photo`,
method: "DELETE", method: "DELETE",
}); });
} }
@ -352,7 +352,7 @@ export class MemberApi {
*/ */
static async Delete(m: Member): Promise<void> { static async Delete(m: Member): Promise<void> {
await APIClient.exec({ await APIClient.exec({
uri: `/family/${m.family_id}/member/${m.id}`, uri: `/family/${m.family_id}/genealogy/member/${m.id}`,
method: "DELETE", method: "DELETE",
}); });
} }

View File

@ -1,24 +0,0 @@
import { useFamily } from "../../widgets/BaseFamilyRoute";
import { FamilyPageTitle } from "../../widgets/FamilyPageTitle";
export function FamilyHomeRoute(): React.ReactElement {
const family = useFamily();
return (
<>
<FamilyPageTitle title="Votre famille" />
<div style={{ margin: "20px" }}>
<p>
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.
</p>
<p>Nombre de fiches de membres: {family.members.size}</p>
<p>Nombre de fiches de couples: {family.couples.size}</p>
<p>
Vous pouvez inviter d'autres personnes à rejoindre cette famille en
leur donnant une copie du code d'invitation
</p>
</div>
</>
);
}

View File

@ -1,26 +1,19 @@
import DownloadIcon from "@mui/icons-material/Download";
import UploadIcon from "@mui/icons-material/Upload";
import { import {
Alert,
Box, Box,
Button, Button,
CardActions, CardActions,
CardContent, CardContent,
Checkbox,
FormControlLabel, FormControlLabel,
Switch,
TextField, TextField,
Tooltip,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import React from "react"; import React from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { DataApi } from "../../api/DataApi";
import { FamilyApi } from "../../api/FamilyApi"; import { FamilyApi } from "../../api/FamilyApi";
import { ServerApi } from "../../api/ServerApi"; import { ServerApi } from "../../api/ServerApi";
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider"; import { useAlert } from "../../hooks/context_providers/AlertDialogProvider";
import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider"; 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 { useFamily } from "../../widgets/BaseFamilyRoute";
import { FamilyCard } from "../../widgets/FamilyCard"; import { FamilyCard } from "../../widgets/FamilyCard";
import { formatDate } from "../../widgets/TimeWidget"; import { formatDate } from "../../widgets/TimeWidget";
@ -55,7 +48,6 @@ export function FamilySettingsRoute(): React.ReactElement {
return ( return (
<> <>
<FamilySettingsCard /> <FamilySettingsCard />
<FamilyExportCard />
<div style={{ textAlign: "center", marginTop: "50px" }}> <div style={{ textAlign: "center", marginTop: "50px" }}>
<Button <Button
size="small" size="small"
@ -76,8 +68,8 @@ function FamilySettingsCard(): React.ReactElement {
const family = useFamily(); const family = useFamily();
const [newName, setNewName] = React.useState(family.family.name); const [newName, setNewName] = React.useState(family.family.name);
const [disableCouplePhotos, setDisableCouplePhotos] = React.useState( const [enableGenealogy, setEnableGenealogy] = React.useState(
family.family.disable_couple_photos family.family.enable_genealogy
); );
const canEdit = family.family.is_admin; const canEdit = family.family.is_admin;
@ -93,7 +85,7 @@ function FamilySettingsCard(): React.ReactElement {
await FamilyApi.UpdateFamily({ await FamilyApi.UpdateFamily({
id: family.family.family_id, id: family.family.family_id,
name: newName, name: newName,
disable_couple_photos: disableCouplePhotos, enable_genealogy: enableGenealogy,
}); });
family.reloadFamilyInfo(); family.reloadFamilyInfo();
@ -144,18 +136,17 @@ function FamilySettingsCard(): React.ReactElement {
maxLength: ServerApi.Config.constraints.family_name_len.max, maxLength: ServerApi.Config.constraints.family_name_len.max,
}} }}
/> />
<Tooltip title="Les photos de couple ne sont pas utilisées en pratique dans les arbres généalogiques. Il est possible de masquer les formulaires d'édition de photos de couple pour limiter le risque de confusion.">
<FormControlLabel <FormControlLabel
disabled={!canEdit} disabled={!canEdit}
control={ control={
<Checkbox <Switch
checked={disableCouplePhotos} checked={enableGenealogy}
onChange={(_e, c) => setDisableCouplePhotos(c)} onChange={(_e, c) => setEnableGenealogy(c)}
/> />
} }
label="Désactiver les photos de couple" label="Activer le module de généalogie"
/> />
</Tooltip>
</Box> </Box>
</CardContent> </CardContent>
<CardActions> <CardActions>
@ -170,109 +161,3 @@ function FamilySettingsCard(): React.ReactElement {
</FamilyCard> </FamilyCard>
); );
} }
function FamilyExportCard(): React.ReactElement {
const loading = useLoadingMessage();
const confirm = useConfirm();
const alert = useAlert();
const family = useFamily();
const [error, setError] = React.useState<string>();
const [success, setSuccess] = React.useState<string>();
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 la famille en cours...");
await DataApi.ImportData(family.familyId, file);
family.reloadFamilyInfo();
alert("Import des données de la famille effectué avec succès !");
} catch (e) {
console.error(e);
setError(`Echec de l'import des données de la famille ! (${e})`);
}
loading.hide();
};
return (
<FamilyCard error={error} success={success}>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
Export / import des données de la famille
</Typography>
<p>
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.
</p>
<Alert severity="warning">
Attention ! La restauration des données 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 !
</Alert>
<p>&nbsp;</p>
<Button
startIcon={<DownloadIcon />}
variant="outlined"
fullWidth
onClick={exportData}
size={"large"}
style={{ marginBottom: "10px" }}
>
Exporter les données de la famille
</Button>
<Button
startIcon={<UploadIcon />}
variant="outlined"
color="warning"
fullWidth
onClick={importData}
disabled={!family.family.is_admin}
size={"large"}
>
Importer les données de la famille
</Button>
</CardContent>
</FamilyCard>
);
}

View File

@ -6,26 +6,27 @@ import SaveIcon from "@mui/icons-material/Save";
import { Button, Grid, Stack } from "@mui/material"; import { Button, Grid, Stack } from "@mui/material";
import React from "react"; import React from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { Couple, CoupleApi } from "../../api/CoupleApi"; import { ServerApi } from "../../../api/ServerApi";
import { Member } from "../../api/MemberApi"; import { Couple, CoupleApi } from "../../../api/genealogy/CoupleApi";
import { ServerApi } from "../../api/ServerApi"; import { Member } from "../../../api/genealogy/MemberApi";
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider"; import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider";
import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider"; import { useConfirm } from "../../../hooks/context_providers/ConfirmDialogProvider";
import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider"; import { useLoadingMessage } from "../../../hooks/context_providers/LoadingMessageProvider";
import { AsyncWidget } from "../../widgets/AsyncWidget"; import { useSnackbar } from "../../../hooks/context_providers/SnackbarProvider";
import { useFamily } from "../../widgets/BaseFamilyRoute"; import { useQuery } from "../../../hooks/useQuery";
import { ConfirmLeaveWithoutSaveDialog } from "../../widgets/ConfirmLeaveWithoutSaveDialog"; import { AsyncWidget } from "../../../widgets/AsyncWidget";
import { CouplePhoto } from "../../widgets/CouplePhoto"; import { useFamily } from "../../../widgets/BaseFamilyRoute";
import { FamilyPageTitle } from "../../widgets/FamilyPageTitle"; import { ConfirmLeaveWithoutSaveDialog } from "../../../widgets/ConfirmLeaveWithoutSaveDialog";
import { MemberItem } from "../../widgets/MemberItem"; import { CouplePhoto } from "../../../widgets/CouplePhoto";
import { PropertiesBox } from "../../widgets/PropertiesBox"; import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle";
import { RouterLink } from "../../widgets/RouterLink"; import { MemberItem } from "../../../widgets/MemberItem";
import { DateInput } from "../../widgets/forms/DateInput"; import { PropertiesBox } from "../../../widgets/PropertiesBox";
import { MemberInput } from "../../widgets/forms/MemberInput"; import { RouterLink } from "../../../widgets/RouterLink";
import { PropSelect } from "../../widgets/forms/PropSelect"; import { DateInput } from "../../../widgets/forms/DateInput";
import { UploadPhotoButton } from "../../widgets/forms/UploadPhotoButton"; import { MemberInput } from "../../../widgets/forms/MemberInput";
import { useQuery } from "../../hooks/useQuery"; import { PropSelect } from "../../../widgets/forms/PropSelect";
import { useLoadingMessage } from "../../hooks/context_providers/LoadingMessageProvider"; import { UploadPhotoButton } from "../../../widgets/forms/UploadPhotoButton";
import { useGenealogy } from "../../../widgets/genealogy/BaseGenealogyRoute";
/** /**
* Create a new couple route * Create a new couple route
@ -36,6 +37,7 @@ export function FamilyCreateCoupleRoute(): React.ReactElement {
const [shouldQuit, setShouldQuit] = React.useState(false); const [shouldQuit, setShouldQuit] = React.useState(false);
const n = useNavigate(); const n = useNavigate();
const genealogy = useGenealogy();
const family = useFamily(); const family = useFamily();
const params = useQuery(); const params = useQuery();
@ -49,7 +51,7 @@ export function FamilyCreateCoupleRoute(): React.ReactElement {
try { try {
const r = await CoupleApi.Create(m); const r = await CoupleApi.Create(m);
await family.reloadCouplesList(); await genealogy.reloadCouplesList();
setShouldQuit(true); setShouldQuit(true);
n(family.family.coupleURL(r)); n(family.family.coupleURL(r));
@ -62,7 +64,7 @@ export function FamilyCreateCoupleRoute(): React.ReactElement {
const cancel = () => { const cancel = () => {
setShouldQuit(true); setShouldQuit(true);
n(family.family.URL("couples")); n(family.family.URL("genealogy/couples"));
}; };
return ( return (
@ -89,6 +91,7 @@ export function FamilyCoupleRoute(): React.ReactElement {
const snackbar = useSnackbar(); const snackbar = useSnackbar();
const family = useFamily(); const family = useFamily();
const genealogy = useGenealogy();
const { coupleId } = useParams(); const { coupleId } = useParams();
const [couple, setCouple] = React.useState<Couple>(); const [couple, setCouple] = React.useState<Couple>();
@ -100,7 +103,7 @@ export function FamilyCoupleRoute(): React.ReactElement {
count.current += 1; count.current += 1;
setCouple(undefined); setCouple(undefined);
await family.reloadCouplesList(); await genealogy.reloadCouplesList();
}; };
const deleteCouple = async () => { const deleteCouple = async () => {
@ -115,9 +118,9 @@ export function FamilyCoupleRoute(): React.ReactElement {
await CoupleApi.Delete(couple!); await CoupleApi.Delete(couple!);
snackbar("La fiche du couple a été supprimée avec succès !"); 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) { } catch (e) {
console.error(e); console.error(e);
alert("Échec de la suppression du couple !"); alert("Échec de la suppression du couple !");
@ -133,7 +136,7 @@ export function FamilyCoupleRoute(): React.ReactElement {
build={() => ( build={() => (
<CouplePage <CouplePage
couple={couple!} couple={couple!}
children={family.members.childrenOfCouple(couple!)} children={genealogy.members.childrenOfCouple(couple!)}
creating={false} creating={false}
editing={false} editing={false}
onRequestDelete={deleteCouple} onRequestDelete={deleteCouple}
@ -157,6 +160,7 @@ export function FamilyEditCoupleRoute(): React.ReactElement {
const [shouldQuit, setShouldQuit] = React.useState(false); const [shouldQuit, setShouldQuit] = React.useState(false);
const genealogy = useGenealogy();
const family = useFamily(); const family = useFamily();
const [couple, setCouple] = React.useState<Couple>(); const [couple, setCouple] = React.useState<Couple>();
@ -176,7 +180,7 @@ export function FamilyEditCoupleRoute(): React.ReactElement {
snackbar("Les informations du couple ont été mises à jour avec succès !"); snackbar("Les informations du couple ont été mises à jour avec succès !");
await family.reloadCouplesList(); await genealogy.reloadCouplesList();
setShouldQuit(true); setShouldQuit(true);
n(family.family.coupleURL(c, false)); n(family.family.coupleURL(c, false));
@ -486,7 +490,7 @@ export function CouplePage(p: {
<div style={{ display: "flex", justifyContent: "end" }}> <div style={{ display: "flex", justifyContent: "end" }}>
<RouterLink <RouterLink
to={family.family.URL( to={family.family.URL(
`member/create?mother=${couple.wife}&father=${couple.husband}` `genealogy/member/create?mother=${couple.wife}&father=${couple.husband}`
)} )}
> >
<Button>Nouveau</Button> <Button>Nouveau</Button>

View File

@ -6,17 +6,18 @@ import { Button, TextField, Tooltip } from "@mui/material";
import { DataGrid, GridActionsCellItem, GridColDef } from "@mui/x-data-grid"; import { DataGrid, GridActionsCellItem, GridColDef } from "@mui/x-data-grid";
import React from "react"; import React from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Couple, CoupleApi } from "../../api/CoupleApi"; import { Couple, CoupleApi } from "../../../api/genealogy/CoupleApi";
import { dateTimestamp, fmtDate } from "../../api/MemberApi"; import { dateTimestamp, fmtDate } from "../../../api/genealogy/MemberApi";
import { ServerApi } from "../../api/ServerApi"; import { ServerApi } from "../../../api/ServerApi";
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider"; import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider";
import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider"; import { useConfirm } from "../../../hooks/context_providers/ConfirmDialogProvider";
import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider"; import { useSnackbar } from "../../../hooks/context_providers/SnackbarProvider";
import { useFamily } from "../../widgets/BaseFamilyRoute"; import { useFamily } from "../../../widgets/BaseFamilyRoute";
import { CouplePhoto } from "../../widgets/CouplePhoto"; import { CouplePhoto } from "../../../widgets/CouplePhoto";
import { FamilyPageTitle } from "../../widgets/FamilyPageTitle"; import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle";
import { MemberPhoto } from "../../widgets/MemberPhoto"; import { MemberPhoto } from "../../../widgets/MemberPhoto";
import { RouterLink } from "../../widgets/RouterLink"; import { RouterLink } from "../../../widgets/RouterLink";
import { useGenealogy } from "../../../widgets/genealogy/BaseGenealogyRoute";
export function FamilyCouplesListRoute(): React.ReactElement { export function FamilyCouplesListRoute(): React.ReactElement {
const alert = useAlert(); const alert = useAlert();
@ -24,6 +25,7 @@ export function FamilyCouplesListRoute(): React.ReactElement {
const snackbar = useSnackbar(); const snackbar = useSnackbar();
const family = useFamily(); const family = useFamily();
const genealogy = useGenealogy();
const [filter, setFilter] = React.useState(""); const [filter, setFilter] = React.useState("");
@ -37,7 +39,7 @@ export function FamilyCouplesListRoute(): React.ReactElement {
return; return;
await CoupleApi.Delete(c); await CoupleApi.Delete(c);
await family.reloadCouplesList(); await genealogy.reloadCouplesList();
snackbar("La fiche du couple a été supprimée avec succès !"); snackbar("La fiche du couple a été supprimée avec succès !");
} catch (e) { } catch (e) {
@ -63,7 +65,7 @@ export function FamilyCouplesListRoute(): React.ReactElement {
</RouterLink> </RouterLink>
</div> </div>
{family.couples.isEmpty ? ( {genealogy.couples.isEmpty ? (
<p> <p>
Votre famille n'a aucun couple enregistré pour le moment ! Utilisez le Votre famille n'a aucun couple enregistré pour le moment ! Utilisez le
bouton situé en haut à droite pour créer le premier ! bouton situé en haut à droite pour créer le premier !
@ -81,16 +83,16 @@ export function FamilyCouplesListRoute(): React.ReactElement {
<CouplesTable <CouplesTable
couples={ couples={
filter === "" filter === ""
? family.couples.fullList ? genealogy.couples.fullList
: family.couples.filter( : genealogy.couples.filter(
(m) => (m) =>
(m.wife && (m.wife &&
family.members genealogy.members
.get(m.wife)! .get(m.wife)!
.fullName.toLocaleLowerCase() .fullName.toLocaleLowerCase()
.includes(filter.toLocaleLowerCase())) || .includes(filter.toLocaleLowerCase())) ||
(m.husband && (m.husband &&
family.members genealogy.members
.get(m.husband)! .get(m.husband)!
.fullName.toLocaleLowerCase() .fullName.toLocaleLowerCase()
.includes(filter.toLocaleLowerCase())) === true .includes(filter.toLocaleLowerCase())) === true
@ -109,14 +111,18 @@ function CouplesTable(p: {
onDelete: (m: Couple) => void; onDelete: (m: Couple) => void;
}): React.ReactElement { }): React.ReactElement {
const family = useFamily(); const family = useFamily();
const genealogy = useGenealogy();
const n = useNavigate(); const n = useNavigate();
const compareInvertedMembersNames = ( const compareInvertedMembersNames = (
v1: number | undefined, v1: number | undefined,
v2: number | undefined v2: number | undefined
) => { ) => {
const n1 = ((v1 && family.members.get(v1)?.invertedFullName) ?? "") || ""; const n1 =
const n2 = ((v2 && family.members.get(v2)?.invertedFullName) ?? "") || ""; ((v1 && genealogy.members.get(v1)?.invertedFullName) ?? "") || "";
const n2 =
((v2 && genealogy.members.get(v2)?.invertedFullName) ?? "") || "";
return n1?.localeCompare(n2, undefined, { return n1?.localeCompare(n2, undefined, {
ignorePunctuation: true, ignorePunctuation: true,
@ -132,7 +138,13 @@ function CouplesTable(p: {
sortable: false, sortable: false,
width: 60, width: 60,
renderCell(params) { renderCell(params) {
return <CouplePhoto couple={params.row} />; return (
<div
style={{ display: "flex", alignItems: "center", height: "100%" }}
>
<CouplePhoto couple={params.row} />
</div>
);
}, },
}, },
@ -253,10 +265,10 @@ function CouplesTable(p: {
} }
function MemberCell(p: { id?: number }): React.ReactElement { function MemberCell(p: { id?: number }): React.ReactElement {
const family = useFamily(); const genealogy = useGenealogy();
if (!p.id) return <></>; if (!p.id) return <></>;
const member = family.members.get(p.id!)!; const member = genealogy.members.get(p.id!)!;
return ( return (
<Tooltip title="Double-cliquez ici pour accéder à la fiche du membre"> <Tooltip title="Double-cliquez ici pour accéder à la fiche du membre">

View File

@ -0,0 +1,17 @@
import { useFamily } from "../../../widgets/BaseFamilyRoute";
import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle";
export function FamilyHomeRoute(): React.ReactElement {
return (
<>
<FamilyPageTitle title="Votre famille" />
<div style={{ margin: "20px" }}>
<p>
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.
</p>
</div>
</>
);
}

View File

@ -1,3 +1,5 @@
import { mdiFamilyTree } from "@mdi/js";
import Icon from "@mdi/react";
import ClearIcon from "@mui/icons-material/Clear"; import ClearIcon from "@mui/icons-material/Clear";
import DeleteIcon from "@mui/icons-material/Delete"; import DeleteIcon from "@mui/icons-material/Delete";
import EditIcon from "@mui/icons-material/Edit"; import EditIcon from "@mui/icons-material/Edit";
@ -14,32 +16,31 @@ import {
import * as EmailValidator from "email-validator"; import * as EmailValidator from "email-validator";
import React from "react"; import React from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { Couple } from "../../api/CoupleApi"; import { ServerApi } from "../../../api/ServerApi";
import { Member, MemberApi, fmtDate } from "../../api/MemberApi"; import { Couple } from "../../../api/genealogy/CoupleApi";
import { ServerApi } from "../../api/ServerApi"; import { Member, MemberApi, fmtDate } from "../../../api/genealogy/MemberApi";
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider"; import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider";
import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider"; import { useConfirm } from "../../../hooks/context_providers/ConfirmDialogProvider";
import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider"; import { useLoadingMessage } from "../../../hooks/context_providers/LoadingMessageProvider";
import { AsyncWidget } from "../../widgets/AsyncWidget"; import { useSnackbar } from "../../../hooks/context_providers/SnackbarProvider";
import { useFamily } from "../../widgets/BaseFamilyRoute"; import { useQuery } from "../../../hooks/useQuery";
import { ConfirmLeaveWithoutSaveDialog } from "../../widgets/ConfirmLeaveWithoutSaveDialog"; import { AsyncWidget } from "../../../widgets/AsyncWidget";
import { CouplePhoto } from "../../widgets/CouplePhoto"; import { useFamily } from "../../../widgets/BaseFamilyRoute";
import { FamilyPageTitle } from "../../widgets/FamilyPageTitle"; import { ConfirmLeaveWithoutSaveDialog } from "../../../widgets/ConfirmLeaveWithoutSaveDialog";
import { MemberItem } from "../../widgets/MemberItem"; import { CouplePhoto } from "../../../widgets/CouplePhoto";
import { MemberPhoto } from "../../widgets/MemberPhoto"; import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle";
import { PropertiesBox } from "../../widgets/PropertiesBox"; import { MemberItem } from "../../../widgets/MemberItem";
import { RouterLink } from "../../widgets/RouterLink"; import { MemberPhoto } from "../../../widgets/MemberPhoto";
import { DateInput } from "../../widgets/forms/DateInput"; import { PropertiesBox } from "../../../widgets/PropertiesBox";
import { MemberInput } from "../../widgets/forms/MemberInput"; import { RouterLink } from "../../../widgets/RouterLink";
import { PropCheckbox } from "../../widgets/forms/PropCheckbox"; import { DateInput } from "../../../widgets/forms/DateInput";
import { PropEdit } from "../../widgets/forms/PropEdit"; import { MemberInput } from "../../../widgets/forms/MemberInput";
import { PropSelect } from "../../widgets/forms/PropSelect"; import { PropCheckbox } from "../../../widgets/forms/PropCheckbox";
import { SexSelection } from "../../widgets/forms/SexSelection"; import { PropEdit } from "../../../widgets/forms/PropEdit";
import { UploadPhotoButton } from "../../widgets/forms/UploadPhotoButton"; import { PropSelect } from "../../../widgets/forms/PropSelect";
import { useQuery } from "../../hooks/useQuery"; import { SexSelection } from "../../../widgets/forms/SexSelection";
import { mdiFamilyTree } from "@mdi/js"; import { UploadPhotoButton } from "../../../widgets/forms/UploadPhotoButton";
import Icon from "@mdi/react"; import { useGenealogy } from "../../../widgets/genealogy/BaseGenealogyRoute";
import { useLoadingMessage } from "../../hooks/context_providers/LoadingMessageProvider";
/** /**
* Create a new member route * Create a new member route
@ -50,6 +51,7 @@ export function FamilyCreateMemberRoute(): React.ReactElement {
const [shouldQuit, setShouldQuit] = React.useState(false); const [shouldQuit, setShouldQuit] = React.useState(false);
const n = useNavigate(); const n = useNavigate();
const genealogy = useGenealogy();
const family = useFamily(); const family = useFamily();
const parameters = useQuery(); const parameters = useQuery();
@ -60,10 +62,10 @@ export function FamilyCreateMemberRoute(): React.ReactElement {
try { try {
const r = await MemberApi.Create(m); const r = await MemberApi.Create(m);
await family.reloadMembersList(); await genealogy.reloadMembersList();
setShouldQuit(true); 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 !`); snackbar(`La fiche pour ${r.fullName} a été créée avec succès !`);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@ -73,7 +75,7 @@ export function FamilyCreateMemberRoute(): React.ReactElement {
const cancel = () => { const cancel = () => {
setShouldQuit(true); setShouldQuit(true);
n(family.family.URL("members")); n(family.family.URL("genealogy/members"));
}; };
const member = Member.New(family.family.family_id); const member = Member.New(family.family.family_id);
@ -104,6 +106,7 @@ export function FamilyMemberRoute(): React.ReactElement {
const snackbar = useSnackbar(); const snackbar = useSnackbar();
const family = useFamily(); const family = useFamily();
const genealogy = useGenealogy();
const { memberId } = useParams(); const { memberId } = useParams();
const [member, setMember] = React.useState<Member>(); const [member, setMember] = React.useState<Member>();
@ -115,7 +118,7 @@ export function FamilyMemberRoute(): React.ReactElement {
count.current += 1; count.current += 1;
setMember(undefined); setMember(undefined);
await family.reloadMembersList(); await genealogy.reloadMembersList();
}; };
const deleteMember = async () => { const deleteMember = async () => {
@ -130,9 +133,9 @@ export function FamilyMemberRoute(): React.ReactElement {
await MemberApi.Delete(member!); await MemberApi.Delete(member!);
snackbar("La fiche de membre a été supprimée avec succès !"); 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) { } catch (e) {
console.error(e); console.error(e);
alert("Échec de la suppression du membre !"); alert("Échec de la suppression du membre !");
@ -148,16 +151,14 @@ export function FamilyMemberRoute(): React.ReactElement {
build={() => ( build={() => (
<MemberPage <MemberPage
member={member!} member={member!}
children={family.members.children(member!.id)} children={genealogy.members.children(member!.id)}
siblings={family.members.siblings(member!.id)} siblings={genealogy.members.siblings(member!.id)}
couples={family.couples.getAllOf(member!)} couples={genealogy.couples.getAllOf(member!)}
creating={false} creating={false}
editing={false} editing={false}
onrequestOpenTree={() => n(family.family.familyTreeURL(member!))} onrequestOpenTree={() => n(family.family.familyTreeURL(member!))}
onRequestDelete={deleteMember} onRequestDelete={deleteMember}
onRequestEdit={() => onRequestEdit={() => n(family.family.memberURL(member!, true))}
n(family.family.URL(`member/${member!.id}/edit`))
}
onForceReload={forceReload} onForceReload={forceReload}
/> />
)} )}
@ -178,6 +179,7 @@ export function FamilyEditMemberRoute(): React.ReactElement {
const [shouldQuit, setShouldQuit] = React.useState(false); const [shouldQuit, setShouldQuit] = React.useState(false);
const family = useFamily(); const family = useFamily();
const genealogy = useGenealogy();
const [member, setMember] = React.useState<Member>(); const [member, setMember] = React.useState<Member>();
const load = async () => { const load = async () => {
@ -196,10 +198,10 @@ export function FamilyEditMemberRoute(): React.ReactElement {
snackbar("Les informations du membre ont été mises à jour avec succès !"); snackbar("Les informations du membre ont été mises à jour avec succès !");
await family.reloadMembersList(); await genealogy.reloadMembersList();
setShouldQuit(true); setShouldQuit(true);
n(family.family.URL(`member/${member!.id}`)); n(family.family.memberURL(member!));
} catch (e) { } catch (e) {
console.error(e); console.error(e);
alert("Échec de la mise à jour des informations du membre !"); alert("Échec de la mise à jour des informations du membre !");
@ -662,9 +664,9 @@ export function MemberPage(p: {
<div style={{ display: "flex", justifyContent: "end" }}> <div style={{ display: "flex", justifyContent: "end" }}>
<RouterLink <RouterLink
to={family.family.URL( to={family.family.URL(
`couple/create?${member.sex === "F" ? "wife" : "husband"}=${ `genealogy/couple/create?${
member.id member.sex === "F" ? "wife" : "husband"
}` }=${member.id}`
)} )}
> >
<Button>Nouveau</Button> <Button>Nouveau</Button>
@ -682,10 +684,7 @@ export function MemberPage(p: {
<>Aucun enfant</> <>Aucun enfant</>
) : ( ) : (
p.children.map((c) => ( p.children.map((c) => (
<RouterLink <RouterLink key={c.id} to={family.family.memberURL(c)}>
key={c.id}
to={family.family.URL(`member/${c.id}`)}
>
<MemberItem member={c} /> <MemberItem member={c} />
</RouterLink> </RouterLink>
)) ))
@ -694,7 +693,7 @@ export function MemberPage(p: {
<div style={{ display: "flex", justifyContent: "end" }}> <div style={{ display: "flex", justifyContent: "end" }}>
<RouterLink <RouterLink
to={family.family.URL( to={family.family.URL(
`member/create?${ `genealogy/member/create?${
member.sex === "F" ? "mother" : "father" member.sex === "F" ? "mother" : "father"
}=${member.id}` }=${member.id}`
)} )}
@ -714,10 +713,7 @@ export function MemberPage(p: {
<>Aucun frère ou sœur</> <>Aucun frère ou sœur</>
) : ( ) : (
p.siblings.map((c) => ( p.siblings.map((c) => (
<RouterLink <RouterLink key={c.id} to={family.family.memberURL(c)}>
key={c.id}
to={family.family.URL(`member/${c.id}`)}
>
<MemberItem member={c} /> <MemberItem member={c} />
</RouterLink> </RouterLink>
)) ))
@ -727,7 +723,7 @@ export function MemberPage(p: {
<div style={{ display: "flex", justifyContent: "end" }}> <div style={{ display: "flex", justifyContent: "end" }}>
<RouterLink <RouterLink
to={family.family.URL( to={family.family.URL(
`member/create?mother=${member.mother}&father=${member.father}` `genealogy/member/create?mother=${member.mother}&father=${member.father}`
)} )}
> >
<Button>Nouveau</Button> <Button>Nouveau</Button>
@ -749,6 +745,7 @@ function CoupleItem(p: {
const n = useNavigate(); const n = useNavigate();
const family = useFamily(); const family = useFamily();
const genealogy = useGenealogy();
const statusStr = ServerApi.Config.couples_states.find( const statusStr = ServerApi.Config.couples_states.find(
(c) => c.code === p.couple.state (c) => c.code === p.couple.state
@ -766,7 +763,7 @@ function CoupleItem(p: {
const otherSpouseID = const otherSpouseID =
p.couple.wife === p.currMemberId ? p.couple.husband : p.couple.wife; p.couple.wife === p.currMemberId ? p.couple.husband : p.couple.wife;
const otherSpouse = otherSpouseID const otherSpouse = otherSpouseID
? family.members.get(otherSpouseID) ? genealogy.members.get(otherSpouseID)
: undefined; : undefined;
return ( return (

View File

@ -21,12 +21,13 @@ import {
buildAscendingTree, buildAscendingTree,
buildDescendingTree, buildDescendingTree,
treeHeight, treeHeight,
} from "../../utils/family_tree"; } from "../../../utils/family_tree";
import { useFamily } from "../../widgets/BaseFamilyRoute"; import { useFamily } from "../../../widgets/BaseFamilyRoute";
import { BasicFamilyTree } from "../../widgets/BasicFamilyTree"; import { BasicFamilyTree } from "../../../widgets/BasicFamilyTree";
import { MemberItem } from "../../widgets/MemberItem"; import { MemberItem } from "../../../widgets/MemberItem";
import { RouterLink } from "../../widgets/RouterLink"; import { RouterLink } from "../../../widgets/RouterLink";
import { SimpleFamilyTree } from "../../widgets/simple_family_tree/SimpleFamilyTree"; import { useGenealogy } from "../../../widgets/genealogy/BaseGenealogyRoute";
import { SimpleFamilyTree } from "../../../widgets/simple_family_tree/SimpleFamilyTree";
enum CurrTab { enum CurrTab {
BasicTree, BasicTree,
@ -41,22 +42,23 @@ enum TreeMode {
export function FamilyMemberTreeRoute(): React.ReactElement { export function FamilyMemberTreeRoute(): React.ReactElement {
const { memberId } = useParams(); const { memberId } = useParams();
const genealogy = useGenealogy();
const family = useFamily(); const family = useFamily();
const [currTab, setCurrTab] = React.useState(CurrTab.SimpleTree); const [currTab, setCurrTab] = React.useState(CurrTab.SimpleTree);
const [currMode, setCurrMode] = React.useState(TreeMode.Descending); 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(() => { const memo: [FamilyTreeNode, number] | null = React.useMemo(() => {
if (!member) return null; if (!member) return null;
const tree = const tree =
currMode === TreeMode.Ascending currMode === TreeMode.Ascending
? buildAscendingTree(member.id, family.members, family.couples) ? buildAscendingTree(member.id, genealogy.members, genealogy.couples)
: buildDescendingTree(member.id, family.members, family.couples); : buildDescendingTree(member.id, genealogy.members, genealogy.couples);
return [tree, treeHeight(tree)]; return [tree, treeHeight(tree)];
}, [member, currMode, family.members, family.couples]); }, [member, currMode, genealogy.members, genealogy.couples]);
const [currDepth, setCurrDepth] = React.useState(0); const [currDepth, setCurrDepth] = React.useState(0);
@ -87,7 +89,7 @@ export function FamilyMemberTreeRoute(): React.ReactElement {
dense dense
member={member} member={member}
secondary={ secondary={
<RouterLink to={family.family.URL("tree")}> <RouterLink to={family.family.URL("genealogy/tree")}>
<IconButton> <IconButton>
<ClearIcon /> <ClearIcon />
</IconButton> </IconButton>

View File

@ -8,20 +8,27 @@ import { Button, TextField, Tooltip, Typography } from "@mui/material";
import { DataGrid, GridActionsCellItem, GridColDef } from "@mui/x-data-grid"; import { DataGrid, GridActionsCellItem, GridColDef } from "@mui/x-data-grid";
import React from "react"; import React from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Member, MemberApi, dateTimestamp, fmtDate } from "../../api/MemberApi"; import {
import { useAlert } from "../../hooks/context_providers/AlertDialogProvider"; Member,
import { useConfirm } from "../../hooks/context_providers/ConfirmDialogProvider"; MemberApi,
import { useSnackbar } from "../../hooks/context_providers/SnackbarProvider"; dateTimestamp,
import { useFamily } from "../../widgets/BaseFamilyRoute"; fmtDate,
import { FamilyPageTitle } from "../../widgets/FamilyPageTitle"; } from "../../../api/genealogy/MemberApi";
import { MemberPhoto } from "../../widgets/MemberPhoto"; import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider";
import { RouterLink } from "../../widgets/RouterLink"; 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 { export function FamilyMembersListRoute(): React.ReactElement {
const alert = useAlert(); const alert = useAlert();
const confirm = useConfirm(); const confirm = useConfirm();
const snackbar = useSnackbar(); const snackbar = useSnackbar();
const genealogy = useGenealogy();
const family = useFamily(); const family = useFamily();
const [filter, setFilter] = React.useState(""); const [filter, setFilter] = React.useState("");
@ -36,7 +43,7 @@ export function FamilyMembersListRoute(): React.ReactElement {
return; return;
await MemberApi.Delete(m); await MemberApi.Delete(m);
await family.reloadMembersList(); await genealogy.reloadMembersList();
snackbar("La fiche du membre a été supprimée avec succès !"); snackbar("La fiche du membre a été supprimée avec succès !");
} catch (e) { } catch (e) {
@ -55,14 +62,14 @@ export function FamilyMembersListRoute(): React.ReactElement {
}} }}
> >
<FamilyPageTitle title="Membres de la famille" /> <FamilyPageTitle title="Membres de la famille" />
<RouterLink to={family.family.URL("member/create")}> <RouterLink to={family.family.URL("genealogy/member/create")}>
<Tooltip title="Créer la fiche d'un nouveau membre"> <Tooltip title="Créer la fiche d'un nouveau membre">
<Button startIcon={<AddIcon />}>Nouveau</Button> <Button startIcon={<AddIcon />}>Nouveau</Button>
</Tooltip> </Tooltip>
</RouterLink> </RouterLink>
</div> </div>
{family.members.isEmpty ? ( {genealogy.members.isEmpty ? (
<p> <p>
Votre famille n'a aucun membre pour le moment ! Utilisez le bouton Votre famille n'a aucun membre pour le moment ! Utilisez le bouton
situé en haut à droite pour créer le premier ! situé en haut à droite pour créer le premier !
@ -80,8 +87,8 @@ export function FamilyMembersListRoute(): React.ReactElement {
<MembersTable <MembersTable
members={ members={
filter === "" filter === ""
? family.members.fullList ? genealogy.members.fullList
: family.members.filter((m) => : genealogy.members.filter((m) =>
m.fullName.toLowerCase().includes(filter.toLowerCase()) m.fullName.toLowerCase().includes(filter.toLowerCase())
) )
} }
@ -97,6 +104,7 @@ function MembersTable(p: {
members: Member[]; members: Member[];
onDelete: (m: Member) => void; onDelete: (m: Member) => void;
}): React.ReactElement { }): React.ReactElement {
const genealogy = useGenealogy();
const family = useFamily(); const family = useFamily();
const n = useNavigate(); const n = useNavigate();
@ -108,7 +116,13 @@ function MembersTable(p: {
sortable: false, sortable: false,
width: 60, width: 60,
renderCell(params) { renderCell(params) {
return <MemberPhoto member={params.row} />; return (
<div
style={{ display: "flex", alignItems: "center", height: "100%" }}
>
<MemberPhoto member={params.row} />
</div>
);
}, },
}, },
@ -166,8 +180,12 @@ function MembersTable(p: {
flex: 5, flex: 5,
renderCell(params) { renderCell(params) {
if (!params.row.father) if (!params.row.father)
return <Typography color="red">Non renseigné</Typography>; return (
return family.members.get(params.row.father)!.fullName; <Typography color="red" component="span" variant="body2">
Non renseigné
</Typography>
);
return genealogy.members.get(params.row.father)!.fullName;
}, },
}, },
{ {
@ -176,8 +194,12 @@ function MembersTable(p: {
flex: 5, flex: 5,
renderCell(params) { renderCell(params) {
if (!params.row.mother) if (!params.row.mother)
return <Typography color="red">Non renseignée</Typography>; return (
return family.members.get(params.row.mother)!.fullName; <Typography color="red" component="span" variant="body2">
Non renseignée
</Typography>
);
return genealogy.members.get(params.row.mother)!.fullName;
}, },
}, },
{ {

View File

@ -1,6 +1,6 @@
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useFamily } from "../../widgets/BaseFamilyRoute"; import { useFamily } from "../../../widgets/BaseFamilyRoute";
import { MemberInput } from "../../widgets/forms/MemberInput"; import { MemberInput } from "../../../widgets/forms/MemberInput";
export function FamilyTreeRoute(): React.ReactElement { export function FamilyTreeRoute(): React.ReactElement {
const n = useNavigate(); const n = useNavigate();

View File

@ -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 (
<>
<GenealogySettingsCard />
<GenealogyExportCard />
</>
);
}
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<string>();
const [success, setSuccess] = React.useState<string>();
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 (
<FamilyCard error={error} success={success}>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
Paramètres du module de généalogie
</Typography>
<Box
component="form"
sx={{
"& .MuiTextField-root": { my: 1 },
}}
noValidate
autoComplete="off"
>
<Tooltip
title="Les photos de couple ne sont pas utilisées en pratique dans les arbres généalogiques. Il est possible de masquer les formulaires d'édition de photos de couple pour limiter le risque de confusion."
arrow
>
<FormControlLabel
disabled={!canEdit}
control={
<Switch
checked={disableCouplePhotos}
onChange={(_e, c) => setDisableCouplePhotos(c)}
/>
}
label="Désactiver les photos de couple"
/>
</Tooltip>
</Box>
</CardContent>
<CardActions>
<Button
onClick={updateFamily}
disabled={!canEdit}
style={{ marginLeft: "auto" }}
>
Enregistrer
</Button>
</CardActions>
</FamilyCard>
);
}
function GenealogyExportCard(): React.ReactElement {
const loading = useLoadingMessage();
const confirm = useConfirm();
const alert = useAlert();
const family = useFamily();
const [error, setError] = React.useState<string>();
const [success, setSuccess] = React.useState<string>();
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 (
<FamilyCard error={error} success={success}>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
Export / import des données de généalogie
</Typography>
<p>
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.
</p>
<Alert severity="warning">
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 !
</Alert>
<p>&nbsp;</p>
<Button
startIcon={<DownloadIcon />}
variant="outlined"
fullWidth
onClick={exportData}
size={"large"}
style={{ marginBottom: "10px" }}
>
Exporter les données de généalogie
</Button>
<Button
startIcon={<UploadIcon />}
variant="outlined"
color="warning"
fullWidth
onClick={importData}
disabled={!family.family.is_admin}
size={"large"}
>
Importer les données de généalogie
</Button>
</CardContent>
</FamilyCard>
);
}

View File

@ -0,0 +1,20 @@
import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle";
import { useGenealogy } from "../../../widgets/genealogy/BaseGenealogyRoute";
export function GenealogyHomeRoute(): React.ReactElement {
const genealogy = useGenealogy();
return (
<>
<FamilyPageTitle title="Généalogie de votre famille" />
<div style={{ margin: "20px" }}>
<p>
Depuis cette section de l'application, vous pouvez afficher et
compléter l'abre généalogique de votre famille.
</p>
<p>&nbsp;</p>
<p>Nombre de fiches de membres: {genealogy.members.size}</p>
<p>Nombre de fiches de couples: {genealogy.couples.size}</p>
</div>
</>
);
}

View File

@ -1,5 +1,5 @@
import { Couple, CouplesList } from "../api/CoupleApi"; import { Couple, CouplesList } from "../api/genealogy/CoupleApi";
import { Member, MembersList, dateTimestamp } from "../api/MemberApi"; import { Member, MembersList, dateTimestamp } from "../api/genealogy/MemberApi";
export interface CoupleInformation { export interface CoupleInformation {
couple: Couple; couple: Couple;

View File

@ -4,6 +4,7 @@ import {
mdiContentCopy, mdiContentCopy,
mdiCrowd, mdiCrowd,
mdiFamilyTree, mdiFamilyTree,
mdiFileTree,
mdiHumanMaleFemale, mdiHumanMaleFemale,
mdiLockCheck, mdiLockCheck,
mdiPlus, mdiPlus,
@ -26,9 +27,7 @@ import {
} from "@mui/material"; } from "@mui/material";
import React from "react"; import React from "react";
import { Outlet, useLocation, useParams } from "react-router-dom"; import { Outlet, useLocation, useParams } from "react-router-dom";
import { CoupleApi, CouplesList } from "../api/CoupleApi";
import { ExtendedFamilyInfo, FamilyApi } from "../api/FamilyApi"; import { ExtendedFamilyInfo, FamilyApi } from "../api/FamilyApi";
import { MemberApi, MembersList } from "../api/MemberApi";
import { useAlert } from "../hooks/context_providers/AlertDialogProvider"; import { useAlert } from "../hooks/context_providers/AlertDialogProvider";
import { useConfirm } from "../hooks/context_providers/ConfirmDialogProvider"; import { useConfirm } from "../hooks/context_providers/ConfirmDialogProvider";
import { useSnackbar } from "../hooks/context_providers/SnackbarProvider"; import { useSnackbar } from "../hooks/context_providers/SnackbarProvider";
@ -37,12 +36,8 @@ import { RouterLink } from "./RouterLink";
interface FamilyContext { interface FamilyContext {
family: ExtendedFamilyInfo; family: ExtendedFamilyInfo;
members: MembersList;
couples: CouplesList;
familyId: number; familyId: number;
reloadFamilyInfo: () => void; reloadFamilyInfo: () => void;
reloadMembersList: () => Promise<void>;
reloadCouplesList: () => Promise<void>;
} }
const FamilyContextK = React.createContext<FamilyContext | null>(null); const FamilyContextK = React.createContext<FamilyContext | null>(null);
@ -54,8 +49,6 @@ export function BaseFamilyRoute(): React.ReactElement {
const confirm = useConfirm(); const confirm = useConfirm();
const [family, setFamily] = React.useState<null | ExtendedFamilyInfo>(null); const [family, setFamily] = React.useState<null | ExtendedFamilyInfo>(null);
const [members, setMembers] = React.useState<null | MembersList>(null);
const [couples, setCouples] = React.useState<null | CouplesList>(null);
const loadKey = React.useRef(1); const loadKey = React.useRef(1);
@ -64,15 +57,11 @@ export function BaseFamilyRoute(): React.ReactElement {
const load = async () => { const load = async () => {
const familyID = Number(familyId); const familyID = Number(familyId);
setFamily(await FamilyApi.GetSingle(familyID)); setFamily(await FamilyApi.GetSingle(familyID));
setMembers(await MemberApi.GetEntireList(familyID));
setCouples(await CoupleApi.GetEntireList(familyID));
}; };
const onReload = async () => { const onReload = async () => {
loadKey.current += 1; loadKey.current += 1;
setFamily(null); setFamily(null);
setMembers(null);
setCouples(null);
return new Promise<void>((res, _rej) => { return new Promise<void>((res, _rej) => {
loadPromise.current = () => res(); loadPromise.current = () => res();
@ -106,7 +95,7 @@ export function BaseFamilyRoute(): React.ReactElement {
return ( return (
<AsyncWidget <AsyncWidget
ready={family !== null && members !== null} ready={family !== null}
loadKey={`${familyId}-${loadKey.current}`} loadKey={`${familyId}-${loadKey.current}`}
load={load} load={load}
errMsg="Échec du chargement des informations de la famille !" errMsg="Échec du chargement des informations de la famille !"
@ -120,12 +109,8 @@ export function BaseFamilyRoute(): React.ReactElement {
<FamilyContextK.Provider <FamilyContextK.Provider
value={{ value={{
family: family!, family: family!,
members: members!,
couples: couples!,
familyId: family!.family_id, familyId: family!.family_id,
reloadFamilyInfo: onReload, reloadFamilyInfo: onReload,
reloadMembersList: onReload,
reloadCouplesList: onReload,
}} }}
> >
<Box <Box
@ -147,41 +132,57 @@ export function BaseFamilyRoute(): React.ReactElement {
<FamilyLink icon={<HomeIcon />} label="Accueil" uri="" /> <FamilyLink icon={<HomeIcon />} label="Accueil" uri="" />
<FamilyLink {family?.enable_genealogy && (
icon={<Icon path={mdiCrowd} size={1} />} <>
label="Membres" <Divider sx={{ my: 1 }} />
uri="members" <ListSubheader component="div">Généalogie</ListSubheader>
secondaryAction={
<Tooltip title="Créer une nouvelle fiche de membre">
<RouterLink to={family!.URL("member/create")}>
<IconButton>
<Icon path={mdiPlus} size={0.75} />
</IconButton>
</RouterLink>
</Tooltip>
}
/>
<FamilyLink <FamilyLink
icon={<Icon path={mdiHumanMaleFemale} size={1} />} icon={<HomeIcon />}
label="Couples" label="Accueil"
uri="couples" uri="genealogy"
secondaryAction={ />
<Tooltip title="Créer une nouvelle fiche de couple"> <FamilyLink
<RouterLink to={family!.URL("couple/create")}> icon={<Icon path={mdiCrowd} size={1} />}
<IconButton> label="Membres"
<Icon path={mdiPlus} size={0.75} /> uri="genealogy/members"
</IconButton> secondaryAction={
</RouterLink> <Tooltip title="Créer une nouvelle fiche de membre">
</Tooltip> <RouterLink
} to={family!.URL("genealogy/member/create")}
/> >
<IconButton>
<Icon path={mdiPlus} size={0.75} />
</IconButton>
</RouterLink>
</Tooltip>
}
/>
<FamilyLink <FamilyLink
icon={<Icon path={mdiFamilyTree} size={1} />} icon={<Icon path={mdiHumanMaleFemale} size={1} />}
label="Arbre" label="Couples"
uri="tree" uri="genealogy/couples"
/> secondaryAction={
<Tooltip title="Créer une nouvelle fiche de couple">
<RouterLink
to={family!.URL("genealogy/couple/create")}
>
<IconButton>
<Icon path={mdiPlus} size={0.75} />
</IconButton>
</RouterLink>
</Tooltip>
}
/>
<FamilyLink
icon={<Icon path={mdiFamilyTree} size={1} />}
label="Arbre"
uri="genealogy/tree"
/>
</>
)}
<Divider sx={{ my: 1 }} /> <Divider sx={{ my: 1 }} />
<ListSubheader component="div">Administration</ListSubheader> <ListSubheader component="div">Administration</ListSubheader>
@ -198,6 +199,14 @@ export function BaseFamilyRoute(): React.ReactElement {
uri="settings" uri="settings"
/> />
{family?.enable_genealogy && (
<FamilyLink
icon={<Icon path={mdiFileTree} size={1} />}
label="Généalogie"
uri="genealogy/settings"
/>
)}
{/* Invitation code */} {/* Invitation code */}
<ListItem <ListItem

View File

@ -5,8 +5,8 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import { TreeItem, SimpleTreeView } from "@mui/x-tree-view"; import { TreeItem, SimpleTreeView } from "@mui/x-tree-view";
import React from "react"; import React from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Couple } from "../api/CoupleApi"; import { Couple } from "../api/genealogy/CoupleApi";
import { Member, fmtDate } from "../api/MemberApi"; import { Member, fmtDate } from "../api/genealogy/MemberApi";
import { FamilyTreeNode } from "../utils/family_tree"; import { FamilyTreeNode } from "../utils/family_tree";
import { useFamily } from "./BaseFamilyRoute"; import { useFamily } from "./BaseFamilyRoute";
import { MemberPhoto } from "./MemberPhoto"; import { MemberPhoto } from "./MemberPhoto";

View File

@ -1,5 +1,5 @@
import { Avatar } from "@mui/material"; import { Avatar } from "@mui/material";
import { Couple } from "../api/CoupleApi"; import { Couple } from "../api/genealogy/CoupleApi";
export function CouplePhoto(p: { export function CouplePhoto(p: {
couple: Couple; couple: Couple;

View File

@ -5,7 +5,7 @@ import {
ListItemSecondaryAction, ListItemSecondaryAction,
ListItemText, ListItemText,
} from "@mui/material"; } from "@mui/material";
import { Member, fmtDate } from "../api/MemberApi"; import { Member, fmtDate } from "../api/genealogy/MemberApi";
import { MemberPhoto } from "./MemberPhoto"; import { MemberPhoto } from "./MemberPhoto";
import Icon from "@mdi/react"; import Icon from "@mdi/react";
import FemaleIcon from "@mui/icons-material/Female"; import FemaleIcon from "@mui/icons-material/Female";

View File

@ -1,5 +1,5 @@
import { Avatar } from "@mui/material"; import { Avatar } from "@mui/material";
import { Member } from "../api/MemberApi"; import { Member } from "../api/genealogy/MemberApi";
export function MemberPhoto(p: { export function MemberPhoto(p: {
member?: Member; member?: Member;

View File

@ -1,6 +1,6 @@
import { Stack, TextField, Typography } from "@mui/material"; import { Stack, TextField, Typography } from "@mui/material";
import { NumberConstraint, ServerApi } from "../../api/ServerApi"; import { NumberConstraint, ServerApi } from "../../api/ServerApi";
import { DateValue, fmtDate } from "../../api/MemberApi"; import { DateValue, fmtDate } from "../../api/genealogy/MemberApi";
import { PropEdit } from "./PropEdit"; import { PropEdit } from "./PropEdit";
export function DateInput(p: { export function DateInput(p: {

View File

@ -2,9 +2,10 @@ import ClearIcon from "@mui/icons-material/Clear";
import { Autocomplete, IconButton, TextField, Typography } from "@mui/material"; import { Autocomplete, IconButton, TextField, Typography } from "@mui/material";
import React from "react"; import React from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { Member } from "../../api/MemberApi"; import { Member } from "../../api/genealogy/MemberApi";
import { useFamily } from "../BaseFamilyRoute"; import { useFamily } from "../BaseFamilyRoute";
import { MemberItem } from "../MemberItem"; import { MemberItem } from "../MemberItem";
import { useGenealogy } from "../genealogy/BaseGenealogyRoute";
export function MemberInput(p: { export function MemberInput(p: {
editable: boolean; editable: boolean;
@ -15,13 +16,14 @@ export function MemberInput(p: {
}): React.ReactElement { }): React.ReactElement {
const n = useNavigate(); const n = useNavigate();
const family = useFamily(); const family = useFamily();
const genealogy = useGenealogy();
const choices = family.members.filter(p.filter); const choices = genealogy.members.filter(p.filter);
const [inputValue, setInputValue] = React.useState(""); const [inputValue, setInputValue] = React.useState("");
if (p.current) { if (p.current) {
const member = family.members.get(p.current)!; const member = genealogy.members.get(p.current)!;
return ( return (
<div style={{ display: "flex", alignItems: "center" }}> <div style={{ display: "flex", alignItems: "center" }}>
<Typography variant="body2">{p.label}</Typography> <Typography variant="body2">{p.label}</Typography>
@ -30,7 +32,7 @@ export function MemberInput(p: {
onClick={ onClick={
!p.editable !p.editable
? () => { ? () => {
n(family.family.URL(`member/${member.id}`)); n(family.family.memberURL(member));
} }
: undefined : undefined
} }
@ -55,7 +57,7 @@ export function MemberInput(p: {
return ( return (
<Autocomplete <Autocomplete
value={p.current ? family.members.get(p.current) : undefined} value={p.current ? genealogy.members.get(p.current) : undefined}
onChange={(_event: any, newValue: Member | null | undefined) => { onChange={(_event: any, newValue: Member | null | undefined) => {
p.onValueChange(newValue?.id); p.onValueChange(newValue?.id);
}} }}

View File

@ -6,7 +6,7 @@ import {
Radio, Radio,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import { Sex } from "../../api/MemberApi"; import { Sex } from "../../api/genealogy/MemberApi";
export function SexSelection(p: { export function SexSelection(p: {
readonly?: boolean; readonly?: boolean;

View File

@ -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<void>;
reloadCouplesList: () => Promise<void>;
}
const GenealogyContextK = React.createContext<FamilyContext | null>(null);
export function BaseGenealogyRoute(): React.ReactElement {
const family = useFamily();
const [members, setMembers] = React.useState<null | MembersList>(null);
const [couples, setCouples] = React.useState<null | CouplesList>(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<void>((res, _rej) => {
loadPromise.current = () => res();
});
};
return (
<AsyncWidget
ready={members !== null && couples !== null}
loadKey={`${family.familyId}-${loadKey.current}`}
load={load}
errMsg="Échec du chargement des informations de généalogie de la famille !"
build={() => {
if (loadPromise.current != null) {
loadPromise.current?.();
loadPromise.current = undefined;
}
return (
<GenealogyContextK.Provider
value={{
members: members!,
couples: couples!,
reloadMembersList: onReload,
reloadCouplesList: onReload,
}}
>
<Outlet />
</GenealogyContextK.Provider>
);
}}
/>
);
}
export function useGenealogy(): FamilyContext {
return React.useContext(GenealogyContextK)!;
}

View File

@ -5,8 +5,8 @@ import { IconButton, Tooltip } from "@mui/material";
import jsPDF from "jspdf"; import jsPDF from "jspdf";
import React from "react"; import React from "react";
import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch"; import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
import { Couple } from "../../api/CoupleApi"; import { Couple } from "../../api/genealogy/CoupleApi";
import { Member } from "../../api/MemberApi"; import { Member } from "../../api/genealogy/MemberApi";
import { useDarkTheme } from "../../hooks/context_providers/DarkThemeProvider"; import { useDarkTheme } from "../../hooks/context_providers/DarkThemeProvider";
import { FamilyTreeNode } from "../../utils/family_tree"; import { FamilyTreeNode } from "../../utils/family_tree";
import { downloadBlob } from "../../utils/files_utils"; import { downloadBlob } from "../../utils/files_utils";

View File

@ -0,0 +1,3 @@
-- Remove column to toggle genealogy
ALTER TABLE public.families
DROP COLUMN enable_genealogy;

View File

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

View File

@ -79,6 +79,7 @@ pub async fn list(token: LoginToken) -> HttpResult {
struct RichFamilyInfo { struct RichFamilyInfo {
#[serde(flatten)] #[serde(flatten)]
membership: FamilyMembership, membership: FamilyMembership,
enable_genealogy: bool,
disable_couple_photos: 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?; let family = families_service::get_by_id(f.family_id()).await?;
Ok(HttpResponse::Ok().json(RichFamilyInfo { Ok(HttpResponse::Ok().json(RichFamilyInfo {
membership, membership,
enable_genealogy: family.enable_genealogy,
disable_couple_photos: family.disable_couple_photos, disable_couple_photos: family.disable_couple_photos,
})) }))
} }
@ -101,8 +103,9 @@ pub async fn leave(f: FamilyInPath) -> HttpResult {
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub struct UpdateFamilyBody { pub struct UpdateFamilyBody {
name: String, name: Option<String>,
disable_couple_photos: bool, enable_genealogy: Option<bool>,
disable_couple_photos: Option<bool>,
} }
/// Update a family /// Update a family
@ -110,16 +113,24 @@ pub async fn update(
f: FamilyInPathWithAdminMembership, f: FamilyInPathWithAdminMembership,
req: web::Json<UpdateFamilyBody>, req: web::Json<UpdateFamilyBody>,
) -> HttpResult { ) -> HttpResult {
if !StaticConstraints::default() let mut family = families_service::get_by_id(f.family_id()).await?;
.family_name_len
.validate(&req.name) if let Some(name) = &req.name {
{ if !StaticConstraints::default().family_name_len.validate(name) {
return Ok(HttpResponse::BadRequest().body("Invalid family 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?; families_service::update_family(&family).await?;
log::info!("User {:?} updated family {:?}", f.user_id(), f.family_id()); log::info!("User {:?} updated family {:?}", f.user_id(), f.family_id());

View File

@ -137,71 +137,71 @@ async fn main() -> std::io::Result<()> {
"/family/{id}/user/{user_id}", "/family/{id}/user/{user_id}",
web::delete().to(families_controller::delete_membership), web::delete().to(families_controller::delete_membership),
) )
// Members controller // [GENEALOGY] Members controller
.route( .route(
"/family/{id}/member/create", "/family/{id}/genealogy/member/create",
web::post().to(members_controller::create), web::post().to(members_controller::create),
) )
.route( .route(
"/family/{id}/members", "/family/{id}/genealogy/members",
web::get().to(members_controller::get_all), web::get().to(members_controller::get_all),
) )
.route( .route(
"/family/{id}/member/{member_id}", "/family/{id}/genealogy/member/{member_id}",
web::get().to(members_controller::get_single), web::get().to(members_controller::get_single),
) )
.route( .route(
"/family/{id}/member/{member_id}", "/family/{id}/genealogy/member/{member_id}",
web::put().to(members_controller::update), web::put().to(members_controller::update),
) )
.route( .route(
"/family/{id}/member/{member_id}", "/family/{id}/genealogy/member/{member_id}",
web::delete().to(members_controller::delete), web::delete().to(members_controller::delete),
) )
.route( .route(
"/family/{id}/member/{member_id}/photo", "/family/{id}/genealogy/member/{member_id}/photo",
web::put().to(members_controller::set_photo), web::put().to(members_controller::set_photo),
) )
.route( .route(
"/family/{id}/member/{member_id}/photo", "/family/{id}/genealogy/member/{member_id}/photo",
web::delete().to(members_controller::remove_photo), web::delete().to(members_controller::remove_photo),
) )
// Couples controller // [GENEALOGY] Couples controller
.route( .route(
"/family/{id}/couple/create", "/family/{id}/genealogy/couple/create",
web::post().to(couples_controller::create), web::post().to(couples_controller::create),
) )
.route( .route(
"/family/{id}/couples", "/family/{id}/genealogy/couples",
web::get().to(couples_controller::get_all), web::get().to(couples_controller::get_all),
) )
.route( .route(
"/family/{id}/couple/{couple_id}", "/family/{id}/genealogy/couple/{couple_id}",
web::get().to(couples_controller::get_single), web::get().to(couples_controller::get_single),
) )
.route( .route(
"/family/{id}/couple/{couple_id}", "/family/{id}/genealogy/couple/{couple_id}",
web::put().to(couples_controller::update), web::put().to(couples_controller::update),
) )
.route( .route(
"/family/{id}/couple/{couple_id}", "/family/{id}/genealogy/couple/{couple_id}",
web::delete().to(couples_controller::delete), web::delete().to(couples_controller::delete),
) )
.route( .route(
"/family/{id}/couple/{couple_id}/photo", "/family/{id}/genealogy/couple/{couple_id}/photo",
web::put().to(couples_controller::set_photo), web::put().to(couples_controller::set_photo),
) )
.route( .route(
"/family/{id}/couple/{couple_id}/photo", "/family/{id}/genealogy/couple/{couple_id}/photo",
web::delete().to(couples_controller::remove_photo), web::delete().to(couples_controller::remove_photo),
) )
// Data controller // [GENEALOGY] Data controller
.route( .route(
"/family/{id}/data/export", "/family/{id}/genealogy/data/export",
web::get().to(data_controller::export_family), web::get().to(data_controller::export_family),
) )
.route( .route(
"/family/{id}/data/import", "/family/{id}/genealogy/data/import",
web::put().to(data_controller::import_family), web::put().to(data_controller::import_family),
) )
// Photos controller // Photos controller

View File

@ -65,6 +65,7 @@ pub struct Family {
pub name: String, pub name: String,
pub invitation_code: String, pub invitation_code: String,
pub disable_couple_photos: bool, pub disable_couple_photos: bool,
pub enable_genealogy: bool,
} }
impl Family { impl Family {

View File

@ -29,6 +29,7 @@ diesel::table! {
#[max_length = 7] #[max_length = 7]
invitation_code -> Varchar, invitation_code -> Varchar,
disable_couple_photos -> Bool, disable_couple_photos -> Bool,
enable_genealogy -> Bool,
} }
} }

View File

@ -174,6 +174,7 @@ pub async fn update_family(family: &Family) -> anyhow::Result<()> {
.set(( .set((
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::disable_couple_photos.eq(family.disable_couple_photos), families::dsl::disable_couple_photos.eq(family.disable_couple_photos),
)) ))
.execute(conn) .execute(conn)