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> ); }