9 Commits

Author SHA1 Message Date
1daca27ba0 Start to divide application
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-15 19:43:54 +02:00
b59db99d02 Change menu 2024-05-15 19:40:03 +02:00
cbdf2b1921 Fix bad path
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-05-15 19:37:57 +02:00
84ef4cac53 Change all application routes
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-05-15 19:36:57 +02:00
299d52f5d7 Fix error 2024-05-15 19:07:02 +02:00
d2a4bfb8e8 Add genealogy setting
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing
2024-05-15 19:05:45 +02:00
88e40fece6 Add a column in family table to toggle genealogy feature 2024-05-15 18:56:18 +02:00
c6b518d8de Fix icons alignment 2024-05-15 18:40:00 +02:00
cd7462ffb1 Refactor API routes
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2024-05-15 18:27:48 +02:00
30 changed files with 229 additions and 146 deletions

View File

@@ -36,6 +36,7 @@ import {
import { FamilyCouplesListRoute } from "./routes/family/FamilyCouplesListRoute"; import { FamilyCouplesListRoute } from "./routes/family/FamilyCouplesListRoute";
import { FamilyTreeRoute } from "./routes/family/FamilyTreeRoute"; import { FamilyTreeRoute } from "./routes/family/FamilyTreeRoute";
import { FamilyMemberTreeRoute } from "./routes/family/FamilyMemberTreeRoute"; import { FamilyMemberTreeRoute } from "./routes/family/FamilyMemberTreeRoute";
import { GenealogyHomeRoute } from "./routes/family/genealogy/GenealogyHomeRoute";
interface AuthContext { interface AuthContext {
signedIn: boolean; signedIn: boolean;
@@ -67,33 +68,44 @@ 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/*">
<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="*" 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;
} }
} }
@@ -230,6 +234,7 @@ export class FamilyApi {
static async UpdateFamily(settings: { static async UpdateFamily(settings: {
id: number; id: number;
name: string; name: string;
enable_genealogy: boolean;
disable_couple_photos: boolean; disable_couple_photos: boolean;
}): Promise<void> { }): Promise<void> {
await APIClient.exec({ await APIClient.exec({
@@ -237,6 +242,7 @@ export class FamilyApi {
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

@@ -6,8 +6,8 @@ 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 { Couple, CoupleApi } from "../../api/genealogy/CoupleApi";
import { Member } from "../../api/MemberApi"; import { Member } 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";
@@ -62,7 +62,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 (
@@ -115,7 +115,7 @@ 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 family.reloadCouplesList();
} catch (e) { } catch (e) {
@@ -486,7 +486,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,8 +6,8 @@ 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";
@@ -132,7 +132,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>
);
}, },
}, },

View File

@@ -2,7 +2,6 @@ import { useFamily } from "../../widgets/BaseFamilyRoute";
import { FamilyPageTitle } from "../../widgets/FamilyPageTitle"; import { FamilyPageTitle } from "../../widgets/FamilyPageTitle";
export function FamilyHomeRoute(): React.ReactElement { export function FamilyHomeRoute(): React.ReactElement {
const family = useFamily();
return ( return (
<> <>
<FamilyPageTitle title="Votre famille" /> <FamilyPageTitle title="Votre famille" />
@@ -12,12 +11,6 @@ export function FamilyHomeRoute(): React.ReactElement {
Veuillez utiliser le menu situé à gauche pour accéder aux différentes Veuillez utiliser le menu situé à gauche pour accéder aux différentes
sections de l'application. sections de l'application.
</p> </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> </div>
</> </>
); );

View File

@@ -14,8 +14,8 @@ 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 { Couple } from "../../api/genealogy/CoupleApi";
import { Member, MemberApi, fmtDate } from "../../api/MemberApi"; import { Member, MemberApi, 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";
@@ -63,7 +63,7 @@ export function FamilyCreateMemberRoute(): React.ReactElement {
await family.reloadMembersList(); await family.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 +73,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);
@@ -130,7 +130,7 @@ 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 family.reloadMembersList();
} catch (e) { } catch (e) {
@@ -155,9 +155,7 @@ export function FamilyMemberRoute(): React.ReactElement {
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}
/> />
)} )}
@@ -199,7 +197,7 @@ export function FamilyEditMemberRoute(): React.ReactElement {
await family.reloadMembersList(); await family.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 +660,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 +680,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 +689,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 +709,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 +719,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>

View File

@@ -87,7 +87,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,7 +8,12 @@ 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 {
Member,
MemberApi,
dateTimestamp,
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 { useSnackbar } from "../../hooks/context_providers/SnackbarProvider";
@@ -55,7 +60,7 @@ 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>
@@ -108,7 +113,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>
);
}, },
}, },

View File

@@ -14,9 +14,9 @@ import {
} 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 { DataApi } from "../../api/genealogy/DataApi";
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 { useLoadingMessage } from "../../hooks/context_providers/LoadingMessageProvider";
@@ -55,7 +55,7 @@ export function FamilySettingsRoute(): React.ReactElement {
return ( return (
<> <>
<FamilySettingsCard /> <FamilySettingsCard />
<FamilyExportCard /> {family.family.enable_genealogy && <GenealogyExportCard />}
<div style={{ textAlign: "center", marginTop: "50px" }}> <div style={{ textAlign: "center", marginTop: "50px" }}>
<Button <Button
size="small" size="small"
@@ -76,6 +76,9 @@ 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 [enableGenealogy, setEnableGenealogy] = React.useState(
family.family.enable_genealogy
);
const [disableCouplePhotos, setDisableCouplePhotos] = React.useState( const [disableCouplePhotos, setDisableCouplePhotos] = React.useState(
family.family.disable_couple_photos family.family.disable_couple_photos
); );
@@ -93,6 +96,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,
enable_genealogy: enableGenealogy,
disable_couple_photos: disableCouplePhotos, disable_couple_photos: disableCouplePhotos,
}); });
@@ -144,18 +148,34 @@ 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 <Checkbox
checked={disableCouplePhotos} checked={enableGenealogy}
onChange={(_e, c) => setDisableCouplePhotos(c)} onChange={(_e, c) => setEnableGenealogy(c)}
/> />
} }
label="Désactiver les photos de couple" label="Activer la généalogie"
/> />
</Tooltip> {enableGenealogy && (
<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={
<Checkbox
checked={disableCouplePhotos}
onChange={(_e, c) => setDisableCouplePhotos(c)}
/>
}
label="Désactiver les photos de couple"
/>
</Tooltip>
)}
</Box> </Box>
</CardContent> </CardContent>
<CardActions> <CardActions>
@@ -171,7 +191,7 @@ function FamilySettingsCard(): React.ReactElement {
); );
} }
function FamilyExportCard(): React.ReactElement { function GenealogyExportCard(): React.ReactElement {
const loading = useLoadingMessage(); const loading = useLoadingMessage();
const confirm = useConfirm(); const confirm = useConfirm();
const alert = useAlert(); const alert = useAlert();
@@ -234,7 +254,7 @@ function FamilyExportCard(): React.ReactElement {
<FamilyCard error={error} success={success}> <FamilyCard error={error} success={success}>
<CardContent> <CardContent>
<Typography gutterBottom variant="h5" component="div"> <Typography gutterBottom variant="h5" component="div">
Export / import des données de la famille Export / import des données de généalogie
</Typography> </Typography>
<p> <p>
Vous pouvez, à des fins de sauvegardes ou de transfert, exporter et Vous pouvez, à des fins de sauvegardes ou de transfert, exporter et

View File

@@ -0,0 +1,20 @@
import { useFamily } from "../../../widgets/BaseFamilyRoute";
import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle";
export function GenealogyHomeRoute(): React.ReactElement {
const family = useFamily();
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: {family.members.size}</p>
<p>Nombre de fiches de couples: {family.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

@@ -26,9 +26,9 @@ 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 { CoupleApi, CouplesList } from "../api/genealogy/CoupleApi";
import { MemberApi, MembersList } 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 { useSnackbar } from "../hooks/context_providers/SnackbarProvider";
@@ -147,13 +147,21 @@ export function BaseFamilyRoute(): React.ReactElement {
<FamilyLink icon={<HomeIcon />} label="Accueil" uri="" /> <FamilyLink icon={<HomeIcon />} label="Accueil" uri="" />
<Divider sx={{ my: 1 }} />
<ListSubheader component="div">Généalogie</ListSubheader>
<FamilyLink
icon={<HomeIcon />}
label="Accueil"
uri="genealogy"
/>
<FamilyLink <FamilyLink
icon={<Icon path={mdiCrowd} size={1} />} icon={<Icon path={mdiCrowd} size={1} />}
label="Membres" label="Membres"
uri="members" uri="genealogy/members"
secondaryAction={ secondaryAction={
<Tooltip title="Créer une nouvelle fiche de membre"> <Tooltip title="Créer une nouvelle fiche de membre">
<RouterLink to={family!.URL("member/create")}> <RouterLink to={family!.URL("genealogy/member/create")}>
<IconButton> <IconButton>
<Icon path={mdiPlus} size={0.75} /> <Icon path={mdiPlus} size={0.75} />
</IconButton> </IconButton>
@@ -165,10 +173,10 @@ export function BaseFamilyRoute(): React.ReactElement {
<FamilyLink <FamilyLink
icon={<Icon path={mdiHumanMaleFemale} size={1} />} icon={<Icon path={mdiHumanMaleFemale} size={1} />}
label="Couples" label="Couples"
uri="couples" uri="genealogy/couples"
secondaryAction={ secondaryAction={
<Tooltip title="Créer une nouvelle fiche de couple"> <Tooltip title="Créer une nouvelle fiche de couple">
<RouterLink to={family!.URL("couple/create")}> <RouterLink to={family!.URL("genealogy/couple/create")}>
<IconButton> <IconButton>
<Icon path={mdiPlus} size={0.75} /> <Icon path={mdiPlus} size={0.75} />
</IconButton> </IconButton>
@@ -180,7 +188,7 @@ export function BaseFamilyRoute(): React.ReactElement {
<FamilyLink <FamilyLink
icon={<Icon path={mdiFamilyTree} size={1} />} icon={<Icon path={mdiFamilyTree} size={1} />}
label="Arbre" label="Arbre"
uri="tree" uri="genealogy/tree"
/> />
<Divider sx={{ my: 1 }} /> <Divider sx={{ my: 1 }} />

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,7 +2,7 @@ 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";
@@ -30,7 +30,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
} }

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

@@ -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,
})) }))
} }
@@ -102,6 +104,7 @@ pub async fn leave(f: FamilyInPath) -> HttpResult {
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub struct UpdateFamilyBody { pub struct UpdateFamilyBody {
name: String, name: String,
enable_genealogy: bool,
disable_couple_photos: bool, disable_couple_photos: bool,
} }
@@ -119,6 +122,7 @@ pub async fn update(
let mut family = families_service::get_by_id(f.family_id()).await?; let mut family = families_service::get_by_id(f.family_id()).await?;
family.name = req.0.name; family.name = req.0.name;
family.enable_genealogy = req.0.enable_genealogy;
family.disable_couple_photos = req.0.disable_couple_photos; family.disable_couple_photos = req.0.disable_couple_photos;
families_service::update_family(&family).await?; families_service::update_family(&family).await?;

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)