Genealogy as a feature (#175)
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Start our journey into turning GeneIT as afully featured family intranet by making genealogy a feature that can be disabled by family admins Reviewed-on: #175
This commit is contained in:
784
geneit_app/src/routes/family/genealogy/FamilyMemberRoute.tsx
Normal file
784
geneit_app/src/routes/family/genealogy/FamilyMemberRoute.tsx
Normal file
@ -0,0 +1,784 @@
|
||||
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";
|
||||
import FileDownloadIcon from "@mui/icons-material/FileDownload";
|
||||
import SaveIcon from "@mui/icons-material/Save";
|
||||
import {
|
||||
Button,
|
||||
Grid,
|
||||
ListItemAvatar,
|
||||
ListItemButton,
|
||||
ListItemText,
|
||||
Stack,
|
||||
} from "@mui/material";
|
||||
import * as EmailValidator from "email-validator";
|
||||
import React from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
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
|
||||
*/
|
||||
export function FamilyCreateMemberRoute(): React.ReactElement {
|
||||
const alert = useAlert();
|
||||
const snackbar = useSnackbar();
|
||||
|
||||
const [shouldQuit, setShouldQuit] = React.useState(false);
|
||||
const n = useNavigate();
|
||||
const genealogy = useGenealogy();
|
||||
const family = useFamily();
|
||||
|
||||
const parameters = useQuery();
|
||||
const mother = Number(parameters.get("mother"));
|
||||
const father = Number(parameters.get("father"));
|
||||
|
||||
const create = async (m: Member) => {
|
||||
try {
|
||||
const r = await MemberApi.Create(m);
|
||||
|
||||
await genealogy.reloadMembersList();
|
||||
|
||||
setShouldQuit(true);
|
||||
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);
|
||||
alert("Echec de la création de la personne !");
|
||||
}
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
setShouldQuit(true);
|
||||
n(family.family.URL("genealogy/members"));
|
||||
};
|
||||
|
||||
const member = Member.New(family.family.family_id);
|
||||
if (mother) member.mother = mother;
|
||||
if (father) member.father = father;
|
||||
|
||||
return (
|
||||
<MemberPage
|
||||
member={member}
|
||||
creating={true}
|
||||
editing={true}
|
||||
onCancel={cancel}
|
||||
onSave={create}
|
||||
shouldAllowLeaving={shouldQuit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get existing member route
|
||||
*/
|
||||
export function FamilyMemberRoute(): React.ReactElement {
|
||||
const count = React.useRef(1);
|
||||
|
||||
const n = useNavigate();
|
||||
const alert = useAlert();
|
||||
const confirm = useConfirm();
|
||||
const snackbar = useSnackbar();
|
||||
|
||||
const family = useFamily();
|
||||
const genealogy = useGenealogy();
|
||||
const { memberId } = useParams();
|
||||
|
||||
const [member, setMember] = React.useState<Member>();
|
||||
const load = async () => {
|
||||
setMember(await MemberApi.GetSingle(family.familyId, Number(memberId)));
|
||||
};
|
||||
|
||||
const forceReload = async () => {
|
||||
count.current += 1;
|
||||
setMember(undefined);
|
||||
|
||||
await genealogy.reloadMembersList();
|
||||
};
|
||||
|
||||
const deleteMember = async () => {
|
||||
try {
|
||||
if (
|
||||
!(await confirm(
|
||||
"Voulez-vous vraiment supprimer cette fiche membre ? L'opération n'est pas réversible !"
|
||||
))
|
||||
)
|
||||
return;
|
||||
|
||||
await MemberApi.Delete(member!);
|
||||
|
||||
snackbar("La fiche de membre a été supprimée avec succès !");
|
||||
n(family.family.URL("genealogy/members"));
|
||||
|
||||
await genealogy.reloadMembersList();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert("Échec de la suppression du membre !");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AsyncWidget
|
||||
loadKey={`${memberId}-${count.current}`}
|
||||
load={load}
|
||||
ready={member !== undefined}
|
||||
errMsg="Echec du chargement des informations du membre"
|
||||
build={() => (
|
||||
<MemberPage
|
||||
member={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.memberURL(member!, true))}
|
||||
onForceReload={forceReload}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit existing member route
|
||||
*/
|
||||
export function FamilyEditMemberRoute(): React.ReactElement {
|
||||
const n = useNavigate();
|
||||
const { memberId } = useParams();
|
||||
|
||||
const alert = useAlert();
|
||||
const snackbar = useSnackbar();
|
||||
|
||||
const [shouldQuit, setShouldQuit] = React.useState(false);
|
||||
|
||||
const family = useFamily();
|
||||
const genealogy = useGenealogy();
|
||||
|
||||
const [member, setMember] = React.useState<Member>();
|
||||
const load = async () => {
|
||||
setMember(await MemberApi.GetSingle(family.familyId, Number(memberId)));
|
||||
};
|
||||
|
||||
const cancel = () => {
|
||||
setShouldQuit(true);
|
||||
n(family.family.memberURL(member!));
|
||||
//n(-1);
|
||||
};
|
||||
|
||||
const save = async (m: Member) => {
|
||||
try {
|
||||
await MemberApi.Update(m);
|
||||
|
||||
snackbar("Les informations du membre ont été mises à jour avec succès !");
|
||||
|
||||
await genealogy.reloadMembersList();
|
||||
|
||||
setShouldQuit(true);
|
||||
n(family.family.memberURL(member!));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert("Échec de la mise à jour des informations du membre !");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AsyncWidget
|
||||
loadKey={memberId}
|
||||
load={load}
|
||||
errMsg="Échec du chargement des informations du membre"
|
||||
build={() => (
|
||||
<MemberPage
|
||||
member={member!}
|
||||
creating={false}
|
||||
editing={true}
|
||||
onCancel={cancel}
|
||||
onSave={save}
|
||||
shouldAllowLeaving={shouldQuit}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function MemberPage(p: {
|
||||
member: Member;
|
||||
editing: boolean;
|
||||
creating: boolean;
|
||||
shouldAllowLeaving?: boolean;
|
||||
children?: Member[];
|
||||
siblings?: Member[];
|
||||
couples?: Couple[];
|
||||
onCancel?: () => void;
|
||||
onSave?: (m: Member) => Promise<void>;
|
||||
onRequestEdit?: () => void;
|
||||
onRequestDelete?: () => void;
|
||||
onForceReload?: () => void;
|
||||
onrequestOpenTree?: () => void;
|
||||
}): React.ReactElement {
|
||||
const confirm = useConfirm();
|
||||
const snackbar = useSnackbar();
|
||||
const loadingMessage = useLoadingMessage();
|
||||
|
||||
const family = useFamily();
|
||||
|
||||
const [changed, setChanged] = React.useState(false);
|
||||
const [member, setMember] = React.useState(
|
||||
new Member(structuredClone(p.member))
|
||||
);
|
||||
|
||||
const updatedMember = () => {
|
||||
setChanged(true);
|
||||
setMember(new Member(structuredClone(member)));
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
loadingMessage.show(
|
||||
"Enregistrement des informations du membre en cours..."
|
||||
);
|
||||
await p.onSave!(member);
|
||||
loadingMessage.hide();
|
||||
};
|
||||
|
||||
const cancel = async () => {
|
||||
if (
|
||||
changed &&
|
||||
!(await confirm(
|
||||
"Voulez-vous vraiment retirer les modifications apportées ? Celles-ci seront perdues !"
|
||||
))
|
||||
)
|
||||
return;
|
||||
|
||||
p.onCancel!();
|
||||
};
|
||||
|
||||
const uploadNewPhoto = async (b: Blob) => {
|
||||
await MemberApi.SetMemberPhoto(member, b);
|
||||
snackbar("La photo du membre a été mise à jour avec succès !");
|
||||
p.onForceReload?.();
|
||||
};
|
||||
|
||||
const deletePhoto = async () => {
|
||||
try {
|
||||
if (!(await confirm("Voulez-vous supprimer cette photo ?"))) return;
|
||||
|
||||
await MemberApi.RemoveMemberPhoto(member);
|
||||
|
||||
snackbar("La photo du membre a été supprimée avec succès !");
|
||||
p.onForceReload?.();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert("Échec de la suppresion de la photo !");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: "2000px", margin: "auto" }}>
|
||||
<ConfirmLeaveWithoutSaveDialog
|
||||
shouldBlock={changed && p.shouldAllowLeaving !== true}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<FamilyPageTitle
|
||||
title={
|
||||
(p.editing
|
||||
? p.creating
|
||||
? "Création"
|
||||
: "Édition"
|
||||
: "Visualisation") + " d'une fiche de membre"
|
||||
}
|
||||
/>
|
||||
<Stack direction="row" spacing={1}>
|
||||
{/* Family tree button */}
|
||||
{p.onrequestOpenTree && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<Icon path={mdiFamilyTree} size={1} />}
|
||||
onClick={p.onrequestOpenTree}
|
||||
size="large"
|
||||
>
|
||||
Arbre
|
||||
</Button>
|
||||
)}
|
||||
{/* Edit button */}
|
||||
{p.onRequestEdit && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<EditIcon />}
|
||||
onClick={p.onRequestEdit}
|
||||
size="large"
|
||||
>
|
||||
Editer
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Delete button */}
|
||||
{p.onRequestDelete && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<DeleteIcon />}
|
||||
onClick={p.onRequestDelete}
|
||||
size="large"
|
||||
color="error"
|
||||
>
|
||||
Supprimer
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Save button */}
|
||||
{p.editing && changed && (
|
||||
<Button
|
||||
variant={"contained"}
|
||||
startIcon={<SaveIcon />}
|
||||
onClick={save}
|
||||
size="large"
|
||||
>
|
||||
Enregistrer
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Cancel button */}
|
||||
{p.editing && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<ClearIcon />}
|
||||
onClick={cancel}
|
||||
size="small"
|
||||
>
|
||||
Annuler les modifications
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
{/* General info */}
|
||||
<Grid item sm={12} md={6}>
|
||||
<PropertiesBox title="Informations générales">
|
||||
{/* Sex */}
|
||||
<SexSelection
|
||||
readonly={!p.editing}
|
||||
current={member.sex}
|
||||
onChange={(v) => {
|
||||
member.sex = v;
|
||||
updatedMember();
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* First name */}
|
||||
<PropEdit
|
||||
label="Prénom"
|
||||
editable={p.editing}
|
||||
value={member.first_name}
|
||||
onValueChange={(v) => {
|
||||
member.first_name = v;
|
||||
updatedMember();
|
||||
}}
|
||||
size={ServerApi.Config.constraints.member_first_name}
|
||||
/>
|
||||
|
||||
{/* Last name */}
|
||||
<PropEdit
|
||||
label="Nom"
|
||||
editable={p.editing}
|
||||
value={member.last_name}
|
||||
onValueChange={(v) => {
|
||||
member.last_name = v;
|
||||
updatedMember();
|
||||
}}
|
||||
size={ServerApi.Config.constraints.member_last_name}
|
||||
/>
|
||||
|
||||
{/* Birth last name */}
|
||||
<PropEdit
|
||||
label="Nom de naissance"
|
||||
editable={p.editing}
|
||||
value={member.birth_last_name}
|
||||
onValueChange={(v) => {
|
||||
member.birth_last_name = v;
|
||||
updatedMember();
|
||||
}}
|
||||
size={ServerApi.Config.constraints.member_birth_last_name}
|
||||
/>
|
||||
|
||||
{/* Birth day */}
|
||||
<DateInput
|
||||
label="Date de naissance"
|
||||
editable={p.editing}
|
||||
id="dob"
|
||||
value={member.dateOfBirth}
|
||||
onValueChange={(d) => {
|
||||
member.birth_year = d.year;
|
||||
member.birth_month = d.month;
|
||||
member.birth_day = d.day;
|
||||
updatedMember();
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Is dead */}
|
||||
<PropCheckbox
|
||||
checked={member.dead}
|
||||
editable={p.editing}
|
||||
label={member.sex === "F" ? "Décédée" : "Décédé"}
|
||||
onValueChange={(v) => {
|
||||
member.dead = v;
|
||||
updatedMember();
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Death day */}
|
||||
<DateInput
|
||||
label="Date de décès"
|
||||
editable={p.editing}
|
||||
id="dod"
|
||||
value={member.dateOfDeath}
|
||||
onValueChange={(d) => {
|
||||
member.death_year = d.year;
|
||||
member.death_month = d.month;
|
||||
member.death_day = d.day;
|
||||
updatedMember();
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Father */}
|
||||
<br />
|
||||
<MemberInput
|
||||
editable={p.editing}
|
||||
label="Père"
|
||||
onValueChange={(m) => {
|
||||
member.father = m;
|
||||
updatedMember();
|
||||
}}
|
||||
filter={(m) =>
|
||||
(m.sex === "M" || m.sex === undefined) && m.id !== member.id
|
||||
}
|
||||
current={member.father}
|
||||
/>
|
||||
|
||||
{/* Mother */}
|
||||
<br />
|
||||
<MemberInput
|
||||
editable={p.editing}
|
||||
label="Mère"
|
||||
onValueChange={(m) => {
|
||||
member.mother = m;
|
||||
updatedMember();
|
||||
}}
|
||||
filter={(m) =>
|
||||
(m.sex === "F" || m.sex === undefined) && m.id !== member.id
|
||||
}
|
||||
current={member.mother}
|
||||
/>
|
||||
</PropertiesBox>
|
||||
</Grid>
|
||||
|
||||
{/* Photo */}
|
||||
<Grid item sm={12} md={6}>
|
||||
<PropertiesBox title="Photo">
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<MemberPhoto member={member} width={150} />
|
||||
<br />
|
||||
{p.editing ? (
|
||||
<p>
|
||||
Veuillez enregistrer / annuler les modifications apportées à
|
||||
la fiche avant de changer la photo du membre.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<UploadPhotoButton
|
||||
label={member.hasPhoto ? "Remplacer" : "Ajouter"}
|
||||
onPhotoSelected={uploadNewPhoto}
|
||||
aspect={4 / 5}
|
||||
/>{" "}
|
||||
{member.hasPhoto && (
|
||||
<RouterLink to={member.photoURL!} target="_blank">
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<FileDownloadIcon />}
|
||||
>
|
||||
Télécharger
|
||||
</Button>
|
||||
</RouterLink>
|
||||
)}{" "}
|
||||
{member.hasPhoto && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<DeleteIcon />}
|
||||
color="error"
|
||||
onClick={deletePhoto}
|
||||
>
|
||||
Supprimer
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}{" "}
|
||||
</div>
|
||||
</PropertiesBox>
|
||||
</Grid>
|
||||
|
||||
{/* Contact */}
|
||||
{(p.editing || member.hasContactInfo) && (
|
||||
<Grid item sm={12} md={6}>
|
||||
<PropertiesBox title="Contact">
|
||||
{/* Email */}
|
||||
<PropEdit
|
||||
label="Adresse mail"
|
||||
editable={p.editing}
|
||||
value={member.email}
|
||||
onValueChange={(v) => {
|
||||
member.email = v;
|
||||
updatedMember();
|
||||
}}
|
||||
size={ServerApi.Config.constraints.member_email}
|
||||
checkValue={(e) => EmailValidator.validate(e)}
|
||||
/>
|
||||
|
||||
{/* Phone number */}
|
||||
<PropEdit
|
||||
label="Téléphone"
|
||||
editable={p.editing}
|
||||
value={member.phone}
|
||||
onValueChange={(v) => {
|
||||
member.phone = v;
|
||||
updatedMember();
|
||||
}}
|
||||
size={ServerApi.Config.constraints.member_phone}
|
||||
/>
|
||||
|
||||
{/* Country */}
|
||||
<PropSelect
|
||||
label="Pays"
|
||||
editing={p.editing}
|
||||
onValueChange={(o) => {
|
||||
member.country = o;
|
||||
updatedMember();
|
||||
}}
|
||||
value={member.country}
|
||||
options={ServerApi.Config.countries.map((c) => {
|
||||
return { label: c.fr, value: c.code };
|
||||
})}
|
||||
/>
|
||||
{/* Address */}
|
||||
<PropEdit
|
||||
label="Adresse"
|
||||
editable={p.editing}
|
||||
value={member.address}
|
||||
onValueChange={(v) => {
|
||||
member.address = v;
|
||||
updatedMember();
|
||||
}}
|
||||
size={ServerApi.Config.constraints.member_address}
|
||||
/>
|
||||
|
||||
{/* Postal code */}
|
||||
<PropEdit
|
||||
label="Code postal"
|
||||
editable={p.editing}
|
||||
value={member.postal_code}
|
||||
onValueChange={(v) => {
|
||||
member.postal_code = v;
|
||||
updatedMember();
|
||||
}}
|
||||
size={ServerApi.Config.constraints.member_postal_code}
|
||||
/>
|
||||
|
||||
{/* City */}
|
||||
<PropEdit
|
||||
label="Ville"
|
||||
editable={p.editing}
|
||||
value={member.city}
|
||||
onValueChange={(v) => {
|
||||
member.city = v;
|
||||
updatedMember();
|
||||
}}
|
||||
size={ServerApi.Config.constraints.member_city}
|
||||
/>
|
||||
</PropertiesBox>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* Bio */}
|
||||
{(p.editing || member.hasNote) && (
|
||||
<Grid item sm={12} md={6}>
|
||||
<PropertiesBox title="Biographie">
|
||||
<PropEdit
|
||||
label="Biographie"
|
||||
editable={p.editing}
|
||||
multiline={true}
|
||||
minRows={5}
|
||||
maxRows={20}
|
||||
value={member.note}
|
||||
onValueChange={(v) => {
|
||||
member.note = v;
|
||||
updatedMember();
|
||||
}}
|
||||
size={ServerApi.Config.constraints.member_note}
|
||||
/>
|
||||
</PropertiesBox>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* Couples */}
|
||||
{p.couples && (
|
||||
<Grid item sm={12} md={6}>
|
||||
<PropertiesBox title={member.sex === "F" ? "Époux" : "Épouse"}>
|
||||
{p.couples!.length === 0 ? (
|
||||
<>{member.sex === "F" ? "Aucun époux" : "Aucune épouse"}</>
|
||||
) : (
|
||||
p.couples.map((c) => (
|
||||
<CoupleItem key={c.id} currMemberId={member.id} couple={c} />
|
||||
))
|
||||
)}
|
||||
|
||||
<div style={{ display: "flex", justifyContent: "end" }}>
|
||||
<RouterLink
|
||||
to={family.family.URL(
|
||||
`genealogy/couple/create?${
|
||||
member.sex === "F" ? "wife" : "husband"
|
||||
}=${member.id}`
|
||||
)}
|
||||
>
|
||||
<Button>Nouveau</Button>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</PropertiesBox>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* Children */}
|
||||
{p.children && (
|
||||
<Grid item sm={12} md={6}>
|
||||
<PropertiesBox title="Enfants">
|
||||
{p.children.length === 0 ? (
|
||||
<>Aucun enfant</>
|
||||
) : (
|
||||
p.children.map((c) => (
|
||||
<RouterLink key={c.id} to={family.family.memberURL(c)}>
|
||||
<MemberItem member={c} />
|
||||
</RouterLink>
|
||||
))
|
||||
)}
|
||||
|
||||
<div style={{ display: "flex", justifyContent: "end" }}>
|
||||
<RouterLink
|
||||
to={family.family.URL(
|
||||
`genealogy/member/create?${
|
||||
member.sex === "F" ? "mother" : "father"
|
||||
}=${member.id}`
|
||||
)}
|
||||
>
|
||||
<Button>Nouveau</Button>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</PropertiesBox>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* Siblings */}
|
||||
{p.siblings && (
|
||||
<Grid item sm={12} md={6}>
|
||||
<PropertiesBox title="Frères et sœurs">
|
||||
{p.siblings.length === 0 ? (
|
||||
<>Aucun frère ou sœur</>
|
||||
) : (
|
||||
p.siblings.map((c) => (
|
||||
<RouterLink key={c.id} to={family.family.memberURL(c)}>
|
||||
<MemberItem member={c} />
|
||||
</RouterLink>
|
||||
))
|
||||
)}
|
||||
|
||||
{(member.mother || member.father) && (
|
||||
<div style={{ display: "flex", justifyContent: "end" }}>
|
||||
<RouterLink
|
||||
to={family.family.URL(
|
||||
`genealogy/member/create?mother=${member.mother}&father=${member.father}`
|
||||
)}
|
||||
>
|
||||
<Button>Nouveau</Button>
|
||||
</RouterLink>
|
||||
</div>
|
||||
)}
|
||||
</PropertiesBox>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CoupleItem(p: {
|
||||
currMemberId: number;
|
||||
couple: Couple;
|
||||
}): React.ReactElement {
|
||||
const n = useNavigate();
|
||||
|
||||
const family = useFamily();
|
||||
const genealogy = useGenealogy();
|
||||
|
||||
const statusStr = ServerApi.Config.couples_states.find(
|
||||
(c) => c.code === p.couple.state
|
||||
)?.fr;
|
||||
|
||||
const status = [];
|
||||
if (statusStr) status.push(statusStr);
|
||||
|
||||
if (p.couple.dateOfWedding)
|
||||
status.push(`Mariage : ${fmtDate(p.couple.dateOfWedding)}`);
|
||||
|
||||
if (p.couple.dateOfDivorce)
|
||||
status.push(`Divorce : ${fmtDate(p.couple.dateOfDivorce)}`);
|
||||
|
||||
const otherSpouseID =
|
||||
p.couple.wife === p.currMemberId ? p.couple.husband : p.couple.wife;
|
||||
const otherSpouse = otherSpouseID
|
||||
? genealogy.members.get(otherSpouseID)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<ListItemButton onClick={() => n(family.family.coupleURL(p.couple))}>
|
||||
<ListItemAvatar>
|
||||
{p.couple.hasPhoto ? (
|
||||
<CouplePhoto couple={p.couple!} />
|
||||
) : (
|
||||
<MemberPhoto member={otherSpouse} />
|
||||
)}
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={otherSpouse ? otherSpouse.fullName : "___ ___"}
|
||||
secondary={status.join(" - ")}
|
||||
></ListItemText>
|
||||
</ListItemButton>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user