Genealogy as a feature #175
@ -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,12 +70,18 @@ export function App(): React.ReactElement {
|
||||
<Route path="family/:familyId/*" element={<BaseFamilyRoute />}>
|
||||
<Route path="" element={<FamilyHomeRoute />} />
|
||||
|
||||
<Route path="genealogy/*" element={<BaseGenealogyRoute />}>
|
||||
<Route path="" element={<GenealogyHomeRoute />} />
|
||||
|
||||
<Route path="members" element={<FamilyMembersListRoute />} />
|
||||
<Route
|
||||
path="member/create"
|
||||
element={<FamilyCreateMemberRoute />}
|
||||
/>
|
||||
<Route path="member/:memberId" element={<FamilyMemberRoute />} />
|
||||
<Route
|
||||
path="member/:memberId"
|
||||
element={<FamilyMemberRoute />}
|
||||
/>
|
||||
<Route
|
||||
path="member/:memberId/edit"
|
||||
element={<FamilyEditMemberRoute />}
|
||||
@ -83,7 +92,10 @@ export function App(): React.ReactElement {
|
||||
path="couple/create"
|
||||
element={<FamilyCreateCoupleRoute />}
|
||||
/>
|
||||
<Route path="couple/:coupleId" element={<FamilyCoupleRoute />} />
|
||||
<Route
|
||||
path="couple/:coupleId"
|
||||
element={<FamilyCoupleRoute />}
|
||||
/>
|
||||
<Route
|
||||
path="couple/:coupleId/edit"
|
||||
element={<FamilyEditCoupleRoute />}
|
||||
@ -94,6 +106,9 @@ export function App(): React.ReactElement {
|
||||
path="tree/:memberId"
|
||||
element={<FamilyMemberTreeRoute />}
|
||||
/>
|
||||
<Route path="settings" element={<GenalogySettingsRoute />} />
|
||||
<Route path="*" element={<NotFoundRoute />} />
|
||||
</Route>
|
||||
|
||||
<Route path="settings" element={<FamilySettingsRoute />} />
|
||||
<Route path="users" element={<FamilyUsersListRoute />} />
|
||||
|
@ -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<void> {
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
@ -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<Couple> {
|
||||
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<Couple> {
|
||||
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<CouplesList> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
await APIClient.exec({
|
||||
uri: `/family/${m.family_id}/couple/${m.id}`,
|
||||
uri: `/family/${m.family_id}/genealogy/couple/${m.id}`,
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
@ -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<Blob> {
|
||||
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,
|
||||
});
|
@ -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<Member> {
|
||||
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<Member> {
|
||||
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<MembersList> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
await APIClient.exec({
|
||||
uri: `/family/${m.family_id}/member/${m.id}`,
|
||||
uri: `/family/${m.family_id}/genealogy/member/${m.id}`,
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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 (
|
||||
<>
|
||||
<FamilySettingsCard />
|
||||
<FamilyExportCard />
|
||||
<div style={{ textAlign: "center", marginTop: "50px" }}>
|
||||
<Button
|
||||
size="small"
|
||||
@ -76,8 +68,8 @@ function FamilySettingsCard(): React.ReactElement {
|
||||
const family = useFamily();
|
||||
|
||||
const [newName, setNewName] = React.useState(family.family.name);
|
||||
const [disableCouplePhotos, setDisableCouplePhotos] = React.useState(
|
||||
family.family.disable_couple_photos
|
||||
const [enableGenealogy, setEnableGenealogy] = React.useState(
|
||||
family.family.enable_genealogy
|
||||
);
|
||||
|
||||
const canEdit = family.family.is_admin;
|
||||
@ -93,7 +85,7 @@ function FamilySettingsCard(): React.ReactElement {
|
||||
await FamilyApi.UpdateFamily({
|
||||
id: family.family.family_id,
|
||||
name: newName,
|
||||
disable_couple_photos: disableCouplePhotos,
|
||||
enable_genealogy: enableGenealogy,
|
||||
});
|
||||
|
||||
family.reloadFamilyInfo();
|
||||
@ -144,18 +136,17 @@ function FamilySettingsCard(): React.ReactElement {
|
||||
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
|
||||
disabled={!canEdit}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={disableCouplePhotos}
|
||||
onChange={(_e, c) => setDisableCouplePhotos(c)}
|
||||
<Switch
|
||||
checked={enableGenealogy}
|
||||
onChange={(_e, c) => setEnableGenealogy(c)}
|
||||
/>
|
||||
}
|
||||
label="Désactiver les photos de couple"
|
||||
label="Activer le module de généalogie"
|
||||
/>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
@ -170,109 +161,3 @@ function FamilySettingsCard(): React.ReactElement {
|
||||
</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> </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>
|
||||
);
|
||||
}
|
||||
|
@ -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<Couple>();
|
||||
@ -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={() => (
|
||||
<CouplePage
|
||||
couple={couple!}
|
||||
children={family.members.childrenOfCouple(couple!)}
|
||||
children={genealogy.members.childrenOfCouple(couple!)}
|
||||
creating={false}
|
||||
editing={false}
|
||||
onRequestDelete={deleteCouple}
|
||||
@ -157,6 +160,7 @@ export function FamilyEditCoupleRoute(): React.ReactElement {
|
||||
|
||||
const [shouldQuit, setShouldQuit] = React.useState(false);
|
||||
|
||||
const genealogy = useGenealogy();
|
||||
const family = useFamily();
|
||||
|
||||
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 !");
|
||||
|
||||
await family.reloadCouplesList();
|
||||
await genealogy.reloadCouplesList();
|
||||
|
||||
setShouldQuit(true);
|
||||
n(family.family.coupleURL(c, false));
|
||||
@ -486,7 +490,7 @@ export function CouplePage(p: {
|
||||
<div style={{ display: "flex", justifyContent: "end" }}>
|
||||
<RouterLink
|
||||
to={family.family.URL(
|
||||
`member/create?mother=${couple.wife}&father=${couple.husband}`
|
||||
`genealogy/member/create?mother=${couple.wife}&father=${couple.husband}`
|
||||
)}
|
||||
>
|
||||
<Button>Nouveau</Button>
|
@ -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 {
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
{family.couples.isEmpty ? (
|
||||
{genealogy.couples.isEmpty ? (
|
||||
<p>
|
||||
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 {
|
||||
<CouplesTable
|
||||
couples={
|
||||
filter === ""
|
||||
? family.couples.fullList
|
||||
: family.couples.filter(
|
||||
? genealogy.couples.fullList
|
||||
: genealogy.couples.filter(
|
||||
(m) =>
|
||||
(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 <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 {
|
||||
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 (
|
||||
<Tooltip title="Double-cliquez ici pour accéder à la fiche du membre">
|
17
geneit_app/src/routes/family/genealogy/FamilyHomeRoute.tsx
Normal file
17
geneit_app/src/routes/family/genealogy/FamilyHomeRoute.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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<Member>();
|
||||
@ -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={() => (
|
||||
<MemberPage
|
||||
member={member!}
|
||||
children={family.members.children(member!.id)}
|
||||
siblings={family.members.siblings(member!.id)}
|
||||
couples={family.couples.getAllOf(member!)}
|
||||
children={genealogy.members.children(member!.id)}
|
||||
siblings={genealogy.members.siblings(member!.id)}
|
||||
couples={genealogy.couples.getAllOf(member!)}
|
||||
creating={false}
|
||||
editing={false}
|
||||
onrequestOpenTree={() => 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<Member>();
|
||||
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: {
|
||||
<div style={{ display: "flex", justifyContent: "end" }}>
|
||||
<RouterLink
|
||||
to={family.family.URL(
|
||||
`couple/create?${member.sex === "F" ? "wife" : "husband"}=${
|
||||
member.id
|
||||
}`
|
||||
`genealogy/couple/create?${
|
||||
member.sex === "F" ? "wife" : "husband"
|
||||
}=${member.id}`
|
||||
)}
|
||||
>
|
||||
<Button>Nouveau</Button>
|
||||
@ -682,10 +684,7 @@ export function MemberPage(p: {
|
||||
<>Aucun enfant</>
|
||||
) : (
|
||||
p.children.map((c) => (
|
||||
<RouterLink
|
||||
key={c.id}
|
||||
to={family.family.URL(`member/${c.id}`)}
|
||||
>
|
||||
<RouterLink key={c.id} to={family.family.memberURL(c)}>
|
||||
<MemberItem member={c} />
|
||||
</RouterLink>
|
||||
))
|
||||
@ -694,7 +693,7 @@ export function MemberPage(p: {
|
||||
<div style={{ display: "flex", justifyContent: "end" }}>
|
||||
<RouterLink
|
||||
to={family.family.URL(
|
||||
`member/create?${
|
||||
`genealogy/member/create?${
|
||||
member.sex === "F" ? "mother" : "father"
|
||||
}=${member.id}`
|
||||
)}
|
||||
@ -714,10 +713,7 @@ export function MemberPage(p: {
|
||||
<>Aucun frère ou sœur</>
|
||||
) : (
|
||||
p.siblings.map((c) => (
|
||||
<RouterLink
|
||||
key={c.id}
|
||||
to={family.family.URL(`member/${c.id}`)}
|
||||
>
|
||||
<RouterLink key={c.id} to={family.family.memberURL(c)}>
|
||||
<MemberItem member={c} />
|
||||
</RouterLink>
|
||||
))
|
||||
@ -727,7 +723,7 @@ export function MemberPage(p: {
|
||||
<div style={{ display: "flex", justifyContent: "end" }}>
|
||||
<RouterLink
|
||||
to={family.family.URL(
|
||||
`member/create?mother=${member.mother}&father=${member.father}`
|
||||
`genealogy/member/create?mother=${member.mother}&father=${member.father}`
|
||||
)}
|
||||
>
|
||||
<Button>Nouveau</Button>
|
||||
@ -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 (
|
@ -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={
|
||||
<RouterLink to={family.family.URL("tree")}>
|
||||
<RouterLink to={family.family.URL("genealogy/tree")}>
|
||||
<IconButton>
|
||||
<ClearIcon />
|
||||
</IconButton>
|
@ -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 {
|
||||
}}
|
||||
>
|
||||
<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">
|
||||
<Button startIcon={<AddIcon />}>Nouveau</Button>
|
||||
</Tooltip>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
{family.members.isEmpty ? (
|
||||
{genealogy.members.isEmpty ? (
|
||||
<p>
|
||||
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 {
|
||||
<MembersTable
|
||||
members={
|
||||
filter === ""
|
||||
? family.members.fullList
|
||||
: family.members.filter((m) =>
|
||||
? 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 <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,
|
||||
renderCell(params) {
|
||||
if (!params.row.father)
|
||||
return <Typography color="red">Non renseigné</Typography>;
|
||||
return family.members.get(params.row.father)!.fullName;
|
||||
return (
|
||||
<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,
|
||||
renderCell(params) {
|
||||
if (!params.row.mother)
|
||||
return <Typography color="red">Non renseignée</Typography>;
|
||||
return family.members.get(params.row.mother)!.fullName;
|
||||
return (
|
||||
<Typography color="red" component="span" variant="body2">
|
||||
Non renseignée
|
||||
</Typography>
|
||||
);
|
||||
return genealogy.members.get(params.row.mother)!.fullName;
|
||||
},
|
||||
},
|
||||
{
|
@ -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();
|
221
geneit_app/src/routes/family/genealogy/GenalogySettingsRoute.tsx
Normal file
221
geneit_app/src/routes/family/genealogy/GenalogySettingsRoute.tsx
Normal 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> </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>
|
||||
);
|
||||
}
|
@ -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> </p>
|
||||
<p>Nombre de fiches de membres: {genealogy.members.size}</p>
|
||||
<p>Nombre de fiches de couples: {genealogy.couples.size}</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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;
|
||||
|
@ -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<void>;
|
||||
reloadCouplesList: () => Promise<void>;
|
||||
}
|
||||
|
||||
const FamilyContextK = React.createContext<FamilyContext | null>(null);
|
||||
@ -54,8 +49,6 @@ export function BaseFamilyRoute(): React.ReactElement {
|
||||
const confirm = useConfirm();
|
||||
|
||||
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);
|
||||
|
||||
@ -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<void>((res, _rej) => {
|
||||
loadPromise.current = () => res();
|
||||
@ -106,7 +95,7 @@ export function BaseFamilyRoute(): React.ReactElement {
|
||||
|
||||
return (
|
||||
<AsyncWidget
|
||||
ready={family !== null && members !== null}
|
||||
ready={family !== null}
|
||||
loadKey={`${familyId}-${loadKey.current}`}
|
||||
load={load}
|
||||
errMsg="Échec du chargement des informations de la famille !"
|
||||
@ -120,12 +109,8 @@ export function BaseFamilyRoute(): React.ReactElement {
|
||||
<FamilyContextK.Provider
|
||||
value={{
|
||||
family: family!,
|
||||
members: members!,
|
||||
couples: couples!,
|
||||
familyId: family!.family_id,
|
||||
reloadFamilyInfo: onReload,
|
||||
reloadMembersList: onReload,
|
||||
reloadCouplesList: onReload,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
@ -147,13 +132,25 @@ export function BaseFamilyRoute(): React.ReactElement {
|
||||
|
||||
<FamilyLink icon={<HomeIcon />} label="Accueil" uri="" />
|
||||
|
||||
{family?.enable_genealogy && (
|
||||
<>
|
||||
<Divider sx={{ my: 1 }} />
|
||||
<ListSubheader component="div">Généalogie</ListSubheader>
|
||||
|
||||
<FamilyLink
|
||||
icon={<HomeIcon />}
|
||||
label="Accueil"
|
||||
uri="genealogy"
|
||||
/>
|
||||
<FamilyLink
|
||||
icon={<Icon path={mdiCrowd} size={1} />}
|
||||
label="Membres"
|
||||
uri="members"
|
||||
uri="genealogy/members"
|
||||
secondaryAction={
|
||||
<Tooltip title="Créer une nouvelle fiche de membre">
|
||||
<RouterLink to={family!.URL("member/create")}>
|
||||
<RouterLink
|
||||
to={family!.URL("genealogy/member/create")}
|
||||
>
|
||||
<IconButton>
|
||||
<Icon path={mdiPlus} size={0.75} />
|
||||
</IconButton>
|
||||
@ -165,10 +162,12 @@ export function BaseFamilyRoute(): React.ReactElement {
|
||||
<FamilyLink
|
||||
icon={<Icon path={mdiHumanMaleFemale} size={1} />}
|
||||
label="Couples"
|
||||
uri="couples"
|
||||
uri="genealogy/couples"
|
||||
secondaryAction={
|
||||
<Tooltip title="Créer une nouvelle fiche de couple">
|
||||
<RouterLink to={family!.URL("couple/create")}>
|
||||
<RouterLink
|
||||
to={family!.URL("genealogy/couple/create")}
|
||||
>
|
||||
<IconButton>
|
||||
<Icon path={mdiPlus} size={0.75} />
|
||||
</IconButton>
|
||||
@ -180,8 +179,10 @@ export function BaseFamilyRoute(): React.ReactElement {
|
||||
<FamilyLink
|
||||
icon={<Icon path={mdiFamilyTree} size={1} />}
|
||||
label="Arbre"
|
||||
uri="tree"
|
||||
uri="genealogy/tree"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Divider sx={{ my: 1 }} />
|
||||
<ListSubheader component="div">Administration</ListSubheader>
|
||||
@ -198,6 +199,14 @@ export function BaseFamilyRoute(): React.ReactElement {
|
||||
uri="settings"
|
||||
/>
|
||||
|
||||
{family?.enable_genealogy && (
|
||||
<FamilyLink
|
||||
icon={<Icon path={mdiFileTree} size={1} />}
|
||||
label="Généalogie"
|
||||
uri="genealogy/settings"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Invitation code */}
|
||||
|
||||
<ListItem
|
||||
|
@ -5,8 +5,8 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import { TreeItem, SimpleTreeView } from "@mui/x-tree-view";
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Couple } from "../api/CoupleApi";
|
||||
import { Member, fmtDate } from "../api/MemberApi";
|
||||
import { Couple } from "../api/genealogy/CoupleApi";
|
||||
import { Member, fmtDate } from "../api/genealogy/MemberApi";
|
||||
import { FamilyTreeNode } from "../utils/family_tree";
|
||||
import { useFamily } from "./BaseFamilyRoute";
|
||||
import { MemberPhoto } from "./MemberPhoto";
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Avatar } from "@mui/material";
|
||||
import { Couple } from "../api/CoupleApi";
|
||||
import { Couple } from "../api/genealogy/CoupleApi";
|
||||
|
||||
export function CouplePhoto(p: {
|
||||
couple: Couple;
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
ListItemSecondaryAction,
|
||||
ListItemText,
|
||||
} from "@mui/material";
|
||||
import { Member, fmtDate } from "../api/MemberApi";
|
||||
import { Member, fmtDate } from "../api/genealogy/MemberApi";
|
||||
import { MemberPhoto } from "./MemberPhoto";
|
||||
import Icon from "@mdi/react";
|
||||
import FemaleIcon from "@mui/icons-material/Female";
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Avatar } from "@mui/material";
|
||||
import { Member } from "../api/MemberApi";
|
||||
import { Member } from "../api/genealogy/MemberApi";
|
||||
|
||||
export function MemberPhoto(p: {
|
||||
member?: Member;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Stack, TextField, Typography } from "@mui/material";
|
||||
import { NumberConstraint, ServerApi } from "../../api/ServerApi";
|
||||
import { DateValue, fmtDate } from "../../api/MemberApi";
|
||||
import { DateValue, fmtDate } from "../../api/genealogy/MemberApi";
|
||||
import { PropEdit } from "./PropEdit";
|
||||
|
||||
export function DateInput(p: {
|
||||
|
@ -2,9 +2,10 @@ import ClearIcon from "@mui/icons-material/Clear";
|
||||
import { Autocomplete, IconButton, TextField, Typography } from "@mui/material";
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Member } from "../../api/MemberApi";
|
||||
import { Member } from "../../api/genealogy/MemberApi";
|
||||
import { useFamily } from "../BaseFamilyRoute";
|
||||
import { MemberItem } from "../MemberItem";
|
||||
import { useGenealogy } from "../genealogy/BaseGenealogyRoute";
|
||||
|
||||
export function MemberInput(p: {
|
||||
editable: boolean;
|
||||
@ -15,13 +16,14 @@ export function MemberInput(p: {
|
||||
}): React.ReactElement {
|
||||
const n = useNavigate();
|
||||
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("");
|
||||
|
||||
if (p.current) {
|
||||
const member = family.members.get(p.current)!;
|
||||
const member = genealogy.members.get(p.current)!;
|
||||
return (
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<Typography variant="body2">{p.label}</Typography>
|
||||
@ -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 (
|
||||
<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) => {
|
||||
p.onValueChange(newValue?.id);
|
||||
}}
|
||||
|
@ -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;
|
||||
|
73
geneit_app/src/widgets/genealogy/BaseGenealogyRoute.tsx
Normal file
73
geneit_app/src/widgets/genealogy/BaseGenealogyRoute.tsx
Normal 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)!;
|
||||
}
|
@ -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";
|
||||
|
@ -0,0 +1,3 @@
|
||||
-- Remove column to toggle genealogy
|
||||
ALTER TABLE public.families
|
||||
DROP COLUMN enable_genealogy;
|
@ -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';
|
@ -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<String>,
|
||||
enable_genealogy: Option<bool>,
|
||||
disable_couple_photos: Option<bool>,
|
||||
}
|
||||
|
||||
/// Update a family
|
||||
@ -110,16 +113,24 @@ pub async fn update(
|
||||
f: FamilyInPathWithAdminMembership,
|
||||
req: web::Json<UpdateFamilyBody>,
|
||||
) -> HttpResult {
|
||||
if !StaticConstraints::default()
|
||||
.family_name_len
|
||||
.validate(&req.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!"));
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
families_service::update_family(&family).await?;
|
||||
|
||||
log::info!("User {:?} updated family {:?}", f.user_id(), f.family_id());
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -29,6 +29,7 @@ diesel::table! {
|
||||
#[max_length = 7]
|
||||
invitation_code -> Varchar,
|
||||
disable_couple_photos -> Bool,
|
||||
enable_genealogy -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user