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/Grid";
 | |
| 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>
 | |
|   );
 | |
| }
 |