GeneIT/geneit_app/src/routes/family/FamilyMemberRoute.tsx

788 lines
23 KiB
TypeScript

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 { 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";
/**
* 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 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 family.reloadMembersList();
setShouldQuit(true);
n(family.family.URL(`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("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 { 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 family.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("members"));
await family.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={family.members.children(member!.id)}
siblings={family.members.siblings(member!.id)}
couples={family.couples.getAllOf(member!)}
creating={false}
editing={false}
onrequestOpenTree={() => n(family.family.familyTreeURL(member!))}
onRequestDelete={deleteMember}
onRequestEdit={() =>
n(family.family.URL(`member/${member!.id}/edit`))
}
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 [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 family.reloadMembersList();
setShouldQuit(true);
n(family.family.URL(`member/${member!.id}`));
} 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(
`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.URL(`member/${c.id}`)}
>
<MemberItem member={c} />
</RouterLink>
))
)}
<div style={{ display: "flex", justifyContent: "end" }}>
<RouterLink
to={family.family.URL(
`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.URL(`member/${c.id}`)}
>
<MemberItem member={c} />
</RouterLink>
))
)}
{(member.mother || member.father) && (
<div style={{ display: "flex", justifyContent: "end" }}>
<RouterLink
to={family.family.URL(
`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 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
? family.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>
);
}