All checks were successful
continuous-integration/drone/push Build is passing
508 lines
15 KiB
TypeScript
508 lines
15 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, Stack } from "@mui/material";
|
|
import Grid from "@mui/material/Grid2";
|
|
import React from "react";
|
|
import { useNavigate, useParams } from "react-router-dom";
|
|
import { ServerApi } from "../../../api/ServerApi";
|
|
import { Couple, CoupleApi } from "../../../api/genealogy/CoupleApi";
|
|
import { Member } from "../../../api/genealogy/MemberApi";
|
|
import { useAlert } from "../../../hooks/context_providers/AlertDialogProvider";
|
|
import { useConfirm } from "../../../hooks/context_providers/ConfirmDialogProvider";
|
|
import { useLoadingMessage } from "../../../hooks/context_providers/LoadingMessageProvider";
|
|
import { useSnackbar } from "../../../hooks/context_providers/SnackbarProvider";
|
|
import { useQuery } from "../../../hooks/useQuery";
|
|
import { AsyncWidget } from "../../../widgets/AsyncWidget";
|
|
import { useFamily } from "../../../widgets/BaseFamilyRoute";
|
|
import { ConfirmLeaveWithoutSaveDialog } from "../../../widgets/ConfirmLeaveWithoutSaveDialog";
|
|
import { CouplePhoto } from "../../../widgets/CouplePhoto";
|
|
import { FamilyPageTitle } from "../../../widgets/FamilyPageTitle";
|
|
import { MemberItem } from "../../../widgets/MemberItem";
|
|
import { PropertiesBox } from "../../../widgets/PropertiesBox";
|
|
import { RouterLink } from "../../../widgets/RouterLink";
|
|
import { DateInput } from "../../../widgets/forms/DateInput";
|
|
import { MemberInput } from "../../../widgets/forms/MemberInput";
|
|
import { PropSelect } from "../../../widgets/forms/PropSelect";
|
|
import { UploadPhotoButton } from "../../../widgets/forms/UploadPhotoButton";
|
|
import { useGenealogy } from "../../../widgets/genealogy/BaseGenealogyRoute";
|
|
|
|
/**
|
|
* Create a new couple route
|
|
*/
|
|
export function FamilyCreateCoupleRoute(): React.ReactElement {
|
|
const alert = useAlert();
|
|
const snackbar = useSnackbar();
|
|
|
|
const [shouldQuit, setShouldQuit] = React.useState(false);
|
|
const n = useNavigate();
|
|
const genealogy = useGenealogy();
|
|
const family = useFamily();
|
|
|
|
const params = useQuery();
|
|
const couple = Couple.New(family.family.family_id);
|
|
const wife = Number(params.get("wife"));
|
|
const husband = Number(params.get("husband"));
|
|
if (wife) couple.wife = wife;
|
|
if (husband) couple.husband = husband;
|
|
|
|
const create = async (m: Couple) => {
|
|
try {
|
|
const r = await CoupleApi.Create(m);
|
|
|
|
await genealogy.reloadCouplesList();
|
|
|
|
setShouldQuit(true);
|
|
n(family.family.coupleURL(r));
|
|
snackbar(`La fiche pour le couple a été créée avec succès !`);
|
|
} catch (e) {
|
|
console.error(e);
|
|
alert("Echec de la création du couple !");
|
|
}
|
|
};
|
|
|
|
const cancel = () => {
|
|
setShouldQuit(true);
|
|
n(family.family.URL("genealogy/couples"));
|
|
};
|
|
|
|
return (
|
|
<CouplePage
|
|
couple={couple}
|
|
creating={true}
|
|
editing={true}
|
|
onCancel={cancel}
|
|
onSave={create}
|
|
shouldAllowLeaving={shouldQuit}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get existing couple route
|
|
*/
|
|
export function FamilyCoupleRoute(): 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 { coupleId } = useParams();
|
|
|
|
const [couple, setCouple] = React.useState<Couple>();
|
|
const load = async () => {
|
|
setCouple(await CoupleApi.GetSingle(family.familyId, Number(coupleId)));
|
|
};
|
|
|
|
const forceReload = async () => {
|
|
count.current += 1;
|
|
setCouple(undefined);
|
|
|
|
await genealogy.reloadCouplesList();
|
|
};
|
|
|
|
const deleteCouple = async () => {
|
|
try {
|
|
if (
|
|
!(await confirm(
|
|
"Voulez-vous vraiment supprimer cette fiche de couple ? L'opération n'est pas réversible !"
|
|
))
|
|
)
|
|
return;
|
|
|
|
await CoupleApi.Delete(couple!);
|
|
|
|
snackbar("La fiche du couple a été supprimée avec succès !");
|
|
n(family.family.URL("genealogy/couples"));
|
|
|
|
await genealogy.reloadCouplesList();
|
|
} catch (e) {
|
|
console.error(e);
|
|
alert("Échec de la suppression du couple !");
|
|
}
|
|
};
|
|
|
|
return (
|
|
<AsyncWidget
|
|
loadKey={`${coupleId}-${count.current}`}
|
|
load={load}
|
|
ready={couple !== undefined}
|
|
errMsg="Echec du chargement des informations du couple !"
|
|
build={() => (
|
|
<CouplePage
|
|
couple={couple!}
|
|
children={genealogy.members.childrenOfCouple(couple!)}
|
|
creating={false}
|
|
editing={false}
|
|
onRequestDelete={deleteCouple}
|
|
onRequestEdit={() => n(family.family.coupleURL(couple!, true))}
|
|
onForceReload={forceReload}
|
|
/>
|
|
)}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Edit existing couple route
|
|
*/
|
|
export function FamilyEditCoupleRoute(): React.ReactElement {
|
|
const n = useNavigate();
|
|
const { coupleId } = useParams();
|
|
|
|
const alert = useAlert();
|
|
const snackbar = useSnackbar();
|
|
|
|
const [shouldQuit, setShouldQuit] = React.useState(false);
|
|
|
|
const genealogy = useGenealogy();
|
|
const family = useFamily();
|
|
|
|
const [couple, setCouple] = React.useState<Couple>();
|
|
const load = async () => {
|
|
setCouple(await CoupleApi.GetSingle(family.familyId, Number(coupleId)));
|
|
};
|
|
|
|
const cancel = () => {
|
|
setShouldQuit(true);
|
|
n(family.family.coupleURL(couple!));
|
|
//n(-1);
|
|
};
|
|
|
|
const save = async (c: Couple) => {
|
|
try {
|
|
await CoupleApi.Update(c);
|
|
|
|
snackbar("Les informations du couple ont été mises à jour avec succès !");
|
|
|
|
await genealogy.reloadCouplesList();
|
|
|
|
setShouldQuit(true);
|
|
n(family.family.coupleURL(c, false));
|
|
} catch (e) {
|
|
console.error(e);
|
|
alert("Échec de la mise à jour des informations du couple !");
|
|
}
|
|
};
|
|
|
|
return (
|
|
<AsyncWidget
|
|
loadKey={coupleId}
|
|
load={load}
|
|
errMsg="Échec du chargement des informations du couple !"
|
|
build={() => (
|
|
<CouplePage
|
|
couple={couple!}
|
|
creating={false}
|
|
editing={true}
|
|
onCancel={cancel}
|
|
onSave={save}
|
|
shouldAllowLeaving={shouldQuit}
|
|
/>
|
|
)}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function CouplePage(p: {
|
|
couple: Couple;
|
|
editing: boolean;
|
|
creating: boolean;
|
|
shouldAllowLeaving?: boolean;
|
|
children?: Member[];
|
|
onCancel?: () => void;
|
|
onSave?: (m: Couple) => Promise<void>;
|
|
onRequestEdit?: () => void;
|
|
onRequestDelete?: () => void;
|
|
onForceReload?: () => void;
|
|
}): React.ReactElement {
|
|
const confirm = useConfirm();
|
|
const snackbar = useSnackbar();
|
|
const loadingMessage = useLoadingMessage();
|
|
|
|
const family = useFamily();
|
|
|
|
const [changed, setChanged] = React.useState(false);
|
|
const [couple, setCouple] = React.useState(
|
|
new Couple(structuredClone(p.couple))
|
|
);
|
|
|
|
const updatedCouple = () => {
|
|
setChanged(true);
|
|
setCouple(new Couple(structuredClone(couple)));
|
|
};
|
|
|
|
const save = async () => {
|
|
loadingMessage.show(
|
|
"Enregistrement des informations du couple en cours..."
|
|
);
|
|
await p.onSave!(couple);
|
|
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 CoupleApi.SetCouplePhoto(couple, b);
|
|
snackbar("La photo du couple a été mise à jour avec succès !");
|
|
p.onForceReload?.();
|
|
};
|
|
|
|
const deletePhoto = async () => {
|
|
try {
|
|
if (!(await confirm("Voulez-vous supprimer cette photo ?"))) return;
|
|
|
|
await CoupleApi.RemoveCouplePhoto(couple);
|
|
|
|
snackbar("La photo du couple 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 couple"
|
|
}
|
|
/>
|
|
<Stack direction="row" spacing={1}>
|
|
{/* 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 size={{ sm: 12, md: 6 }}>
|
|
<PropertiesBox title="Informations générales">
|
|
{/* Husband */}
|
|
<br />
|
|
<MemberInput
|
|
editable={p.editing}
|
|
label="Époux"
|
|
onValueChange={(m) => {
|
|
couple.husband = m;
|
|
updatedCouple();
|
|
}}
|
|
filter={(m) => m.sex === "M" || m.sex === undefined}
|
|
current={couple.husband}
|
|
/>
|
|
|
|
{/* Wife */}
|
|
<br />
|
|
<MemberInput
|
|
editable={p.editing}
|
|
label="Épouse"
|
|
onValueChange={(m) => {
|
|
couple.wife = m;
|
|
updatedCouple();
|
|
}}
|
|
filter={(m) => m.sex === "F" || m.sex === undefined}
|
|
current={couple.wife}
|
|
/>
|
|
<br />
|
|
|
|
{/* State */}
|
|
<PropSelect
|
|
editing={p.editing}
|
|
label="Status"
|
|
value={couple.state}
|
|
onValueChange={(s) => {
|
|
couple.state = s;
|
|
updatedCouple();
|
|
}}
|
|
options={ServerApi.Config.couples_states.map((s) => {
|
|
return { label: s.fr, value: s.code };
|
|
})}
|
|
/>
|
|
|
|
{/* Wedding day */}
|
|
<DateInput
|
|
label="Date du mariage"
|
|
editable={p.editing}
|
|
id="dow"
|
|
value={couple.dateOfWedding}
|
|
onValueChange={(d) => {
|
|
couple.wedding_year = d.year;
|
|
couple.wedding_month = d.month;
|
|
couple.wedding_day = d.day;
|
|
updatedCouple();
|
|
}}
|
|
/>
|
|
|
|
{/* Divorce day */}
|
|
<DateInput
|
|
label="Date du divorce"
|
|
editable={p.editing}
|
|
id="dod"
|
|
value={couple.dateOfDivorce}
|
|
onValueChange={(d) => {
|
|
couple.divorce_year = d.year;
|
|
couple.divorce_month = d.month;
|
|
couple.divorce_day = d.day;
|
|
updatedCouple();
|
|
}}
|
|
/>
|
|
</PropertiesBox>
|
|
</Grid>
|
|
|
|
{
|
|
/* Photo */ !family.family.disable_couple_photos && (
|
|
<Grid size={{ sm: 12, md: 6 }}>
|
|
<PropertiesBox title="Photo">
|
|
<div style={{ textAlign: "center" }}>
|
|
<CouplePhoto couple={couple} width={150} />
|
|
<br />
|
|
{p.editing ? (
|
|
<p>
|
|
Veuillez enregistrer / annuler les modifications apportées
|
|
à la fiche avant de changer la photo du couple.
|
|
</p>
|
|
) : (
|
|
<>
|
|
<UploadPhotoButton
|
|
label={couple.hasPhoto ? "Remplacer" : "Ajouter"}
|
|
onPhotoSelected={uploadNewPhoto}
|
|
aspect={5 / 4}
|
|
/>{" "}
|
|
{couple.hasPhoto && (
|
|
<RouterLink to={couple.photoURL!} target="_blank">
|
|
<Button
|
|
variant="outlined"
|
|
startIcon={<FileDownloadIcon />}
|
|
>
|
|
Télécharger
|
|
</Button>
|
|
</RouterLink>
|
|
)}{" "}
|
|
{couple.hasPhoto && (
|
|
<Button
|
|
variant="outlined"
|
|
startIcon={<DeleteIcon />}
|
|
color="error"
|
|
onClick={deletePhoto}
|
|
>
|
|
Supprimer
|
|
</Button>
|
|
)}
|
|
</>
|
|
)}{" "}
|
|
</div>
|
|
</PropertiesBox>
|
|
</Grid>
|
|
)
|
|
}
|
|
|
|
{/* Children */}
|
|
{p.children && (
|
|
<Grid size={{ 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>
|
|
))
|
|
)}
|
|
|
|
{couple.wife && couple.husband && (
|
|
<div style={{ display: "flex", justifyContent: "end" }}>
|
|
<RouterLink
|
|
to={family.family.URL(
|
|
`genealogy/member/create?mother=${couple.wife}&father=${couple.husband}`
|
|
)}
|
|
>
|
|
<Button>Nouveau</Button>
|
|
</RouterLink>
|
|
</div>
|
|
)}
|
|
</PropertiesBox>
|
|
</Grid>
|
|
)}
|
|
</Grid>
|
|
</div>
|
|
);
|
|
}
|