Can set the father and the mother of a member
This commit is contained in:
		@@ -31,6 +31,12 @@ export interface MemberDataApi {
 | 
			
		||||
  note?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface DateValue {
 | 
			
		||||
  year?: number;
 | 
			
		||||
  month?: number;
 | 
			
		||||
  day?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class Member implements MemberDataApi {
 | 
			
		||||
  id: number;
 | 
			
		||||
  family_id: number;
 | 
			
		||||
@@ -111,13 +117,43 @@ export class Member implements MemberDataApi {
 | 
			
		||||
    return this.photo_id !== null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get photoURL(): string {
 | 
			
		||||
  get photoURL(): string | null {
 | 
			
		||||
    if (!this.signed_photo_id) return null;
 | 
			
		||||
    return `${APIClient.backendURL()}/photo/${this.signed_photo_id}`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get thumbnailURL(): string {
 | 
			
		||||
  get thumbnailURL(): string | null {
 | 
			
		||||
    if (!this.signed_photo_id) return null;
 | 
			
		||||
    return `${APIClient.backendURL()}/photo/${this.signed_photo_id}/thumbnail`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get dateOfBirth(): DateValue | undefined {
 | 
			
		||||
    if (!this.birth_day && !this.birth_month && !this.birth_year)
 | 
			
		||||
      return undefined;
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      year: this.birth_year,
 | 
			
		||||
      month: this.birth_month,
 | 
			
		||||
      day: this.birth_day,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get dateOfDeath(): DateValue | undefined {
 | 
			
		||||
    if (!this.death_day && !this.death_month && !this.death_year)
 | 
			
		||||
      return undefined;
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      year: this.death_year,
 | 
			
		||||
      month: this.death_month,
 | 
			
		||||
      day: this.death_day,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function fmtDate(d?: DateValue): string {
 | 
			
		||||
  return `${d?.day?.toString().padStart(2, "0") ?? "__"}/${
 | 
			
		||||
    d?.month?.toString().padStart(2, "0") ?? "__"
 | 
			
		||||
  }/${d?.year?.toString().padStart(4, "0") ?? "__"}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class MembersList {
 | 
			
		||||
@@ -132,6 +168,14 @@ export class MembersList {
 | 
			
		||||
      this.map.set(m.id, m);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  filter(predicate: (m: Member) => boolean): Member[] {
 | 
			
		||||
    return this.list.filter(predicate);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get(id: number): Member | undefined {
 | 
			
		||||
    return this.map.get(id);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class MemberApi {
 | 
			
		||||
 
 | 
			
		||||
@@ -383,11 +383,7 @@ export function MemberPage(p: {
 | 
			
		||||
              label="Date de naissance"
 | 
			
		||||
              editable={p.editing}
 | 
			
		||||
              id="dob"
 | 
			
		||||
              value={{
 | 
			
		||||
                year: member.birth_year,
 | 
			
		||||
                month: member.birth_month,
 | 
			
		||||
                day: member.birth_day,
 | 
			
		||||
              }}
 | 
			
		||||
              value={member.dateOfBirth}
 | 
			
		||||
              onValueChange={(d) => {
 | 
			
		||||
                member.birth_year = d.year;
 | 
			
		||||
                member.birth_month = d.month;
 | 
			
		||||
@@ -412,11 +408,7 @@ export function MemberPage(p: {
 | 
			
		||||
              label="Date de décès"
 | 
			
		||||
              editable={p.editing}
 | 
			
		||||
              id="dod"
 | 
			
		||||
              value={{
 | 
			
		||||
                year: member.death_year,
 | 
			
		||||
                month: member.death_month,
 | 
			
		||||
                day: member.death_day,
 | 
			
		||||
              }}
 | 
			
		||||
              value={member.dateOfDeath}
 | 
			
		||||
              onValueChange={(d) => {
 | 
			
		||||
                member.death_year = d.year;
 | 
			
		||||
                member.death_month = d.month;
 | 
			
		||||
@@ -426,6 +418,7 @@ export function MemberPage(p: {
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
            {/* Father */}
 | 
			
		||||
            <br />
 | 
			
		||||
            <MemberInput
 | 
			
		||||
              editable={p.editing}
 | 
			
		||||
              label="Père"
 | 
			
		||||
@@ -433,20 +426,25 @@ export function MemberPage(p: {
 | 
			
		||||
                member.father = m;
 | 
			
		||||
                updatedMember();
 | 
			
		||||
              }}
 | 
			
		||||
              filter={(m) => m.sex === "M" || m.sex === undefined}
 | 
			
		||||
              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.father = m;
 | 
			
		||||
                member.mother = m;
 | 
			
		||||
                updatedMember();
 | 
			
		||||
              }}
 | 
			
		||||
              filter={(m) => m.sex === "F" || m.sex === undefined}
 | 
			
		||||
              current={member.father}
 | 
			
		||||
              filter={(m) =>
 | 
			
		||||
                (m.sex === "F" || m.sex === undefined) && m.id !== member.id
 | 
			
		||||
              }
 | 
			
		||||
              current={member.mother}
 | 
			
		||||
            />
 | 
			
		||||
          </PropertiesBox>
 | 
			
		||||
        </Grid>
 | 
			
		||||
@@ -469,7 +467,7 @@ export function MemberPage(p: {
 | 
			
		||||
                    onPhotoSelected={uploadNewPhoto}
 | 
			
		||||
                  />{" "}
 | 
			
		||||
                  {member.hasPhoto && (
 | 
			
		||||
                    <RouterLink to={member.photoURL} target="_blank">
 | 
			
		||||
                    <RouterLink to={member.photoURL!} target="_blank">
 | 
			
		||||
                      <Button
 | 
			
		||||
                        variant="outlined"
 | 
			
		||||
                        startIcon={<FileDownloadIcon />}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								geneit_app/src/utils/debug_utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								geneit_app/src/utils/debug_utils.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
export function isDebug(): boolean {
 | 
			
		||||
  return !process.env.NODE_ENV || process.env.NODE_ENV === "development";
 | 
			
		||||
}
 | 
			
		||||
@@ -3,13 +3,17 @@ import { Member } from "../api/MemberApi";
 | 
			
		||||
 | 
			
		||||
export function MemberPhoto(p: {
 | 
			
		||||
  member: Member;
 | 
			
		||||
  width: number;
 | 
			
		||||
  width?: number;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <Avatar
 | 
			
		||||
      sx={{ width: `${p.width}px`, height: "auto", display: "inline-block" }}
 | 
			
		||||
      sx={
 | 
			
		||||
        p.width
 | 
			
		||||
          ? { width: `${p.width}px`, height: "auto", display: "inline-block" }
 | 
			
		||||
          : undefined
 | 
			
		||||
      }
 | 
			
		||||
      variant="rounded"
 | 
			
		||||
      src={p.member.thumbnailURL}
 | 
			
		||||
      src={p.member.thumbnailURL ?? undefined}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,16 @@
 | 
			
		||||
import { Stack, TextField, Typography } from "@mui/material";
 | 
			
		||||
import { NumberConstraint, ServerApi } from "../../api/ServerApi";
 | 
			
		||||
 | 
			
		||||
export interface DateValue {
 | 
			
		||||
  year?: number;
 | 
			
		||||
  month?: number;
 | 
			
		||||
  day?: number;
 | 
			
		||||
}
 | 
			
		||||
import { DateValue, fmtDate } from "../../api/MemberApi";
 | 
			
		||||
 | 
			
		||||
export function DateInput(p: {
 | 
			
		||||
  id: string;
 | 
			
		||||
  label: string;
 | 
			
		||||
  editable: boolean;
 | 
			
		||||
  value: DateValue;
 | 
			
		||||
  value?: DateValue;
 | 
			
		||||
  onValueChange: (newVal: DateValue) => void;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  if (!p.editable) {
 | 
			
		||||
    if (!p.value.year && !p.value.month && !p.value.day) return <></>;
 | 
			
		||||
    if (!p.value) return <></>;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <Typography
 | 
			
		||||
@@ -23,8 +18,7 @@ export function DateInput(p: {
 | 
			
		||||
        display="block"
 | 
			
		||||
        style={{ marginBottom: "15px" }}
 | 
			
		||||
      >
 | 
			
		||||
        {p.label} : {p.value.day ?? "__"} / {p.value.month ?? "__"} /{" "}
 | 
			
		||||
        {p.value.year ?? "__"}
 | 
			
		||||
        {p.label} : {fmtDate(p.value!)}
 | 
			
		||||
      </Typography>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
@@ -41,8 +35,8 @@ export function DateInput(p: {
 | 
			
		||||
        required
 | 
			
		||||
        id={`${p.id}-day`}
 | 
			
		||||
        label="Jour"
 | 
			
		||||
        value={p.value.day}
 | 
			
		||||
        error={isValErr(p.value.day, ServerApi.Config.constraints.date_day)}
 | 
			
		||||
        value={p.value?.day}
 | 
			
		||||
        error={isValErr(p.value?.day, ServerApi.Config.constraints.date_day)}
 | 
			
		||||
        variant="filled"
 | 
			
		||||
        style={{ flex: 20 }}
 | 
			
		||||
        type="number"
 | 
			
		||||
@@ -50,8 +44,8 @@ export function DateInput(p: {
 | 
			
		||||
          const val = Number(e.target.value);
 | 
			
		||||
          p.onValueChange({
 | 
			
		||||
            day: val > 0 ? val : undefined,
 | 
			
		||||
            month: p.value.month,
 | 
			
		||||
            year: p.value.year,
 | 
			
		||||
            month: p.value?.month,
 | 
			
		||||
            year: p.value?.year,
 | 
			
		||||
          });
 | 
			
		||||
        }}
 | 
			
		||||
        inputProps={{
 | 
			
		||||
@@ -64,17 +58,20 @@ export function DateInput(p: {
 | 
			
		||||
        required
 | 
			
		||||
        id={`${p.id}-month`}
 | 
			
		||||
        label="Mois"
 | 
			
		||||
        value={p.value.month}
 | 
			
		||||
        error={isValErr(p.value.month, ServerApi.Config.constraints.date_month)}
 | 
			
		||||
        value={p.value?.month}
 | 
			
		||||
        error={isValErr(
 | 
			
		||||
          p.value?.month,
 | 
			
		||||
          ServerApi.Config.constraints.date_month
 | 
			
		||||
        )}
 | 
			
		||||
        variant="filled"
 | 
			
		||||
        style={{ flex: 20 }}
 | 
			
		||||
        type="number"
 | 
			
		||||
        onChange={(e) => {
 | 
			
		||||
          const val = Number(e.target.value);
 | 
			
		||||
          p.onValueChange({
 | 
			
		||||
            day: p.value.day,
 | 
			
		||||
            day: p.value?.day,
 | 
			
		||||
            month: val > 0 ? val : undefined,
 | 
			
		||||
            year: p.value.year,
 | 
			
		||||
            year: p.value?.year,
 | 
			
		||||
          });
 | 
			
		||||
        }}
 | 
			
		||||
        inputProps={{
 | 
			
		||||
@@ -87,16 +84,16 @@ export function DateInput(p: {
 | 
			
		||||
        required
 | 
			
		||||
        id={`${p.id}-year`}
 | 
			
		||||
        label="Année"
 | 
			
		||||
        value={p.value.year}
 | 
			
		||||
        value={p.value?.year}
 | 
			
		||||
        onChange={(e) => {
 | 
			
		||||
          const val = Number(e.target.value);
 | 
			
		||||
          p.onValueChange({
 | 
			
		||||
            day: p.value.day,
 | 
			
		||||
            month: p.value.month,
 | 
			
		||||
            day: p.value?.day,
 | 
			
		||||
            month: p.value?.month,
 | 
			
		||||
            year: val > 0 ? val : undefined,
 | 
			
		||||
          });
 | 
			
		||||
        }}
 | 
			
		||||
        error={isValErr(p.value.year, ServerApi.Config.constraints.date_year)}
 | 
			
		||||
        error={isValErr(p.value?.year, ServerApi.Config.constraints.date_year)}
 | 
			
		||||
        variant="filled"
 | 
			
		||||
        style={{ flex: 30 }}
 | 
			
		||||
        type="number"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,25 @@
 | 
			
		||||
import { Member } from "../../api/MemberApi";
 | 
			
		||||
import {
 | 
			
		||||
  Autocomplete,
 | 
			
		||||
  Avatar,
 | 
			
		||||
  Box,
 | 
			
		||||
  IconButton,
 | 
			
		||||
  ListItem,
 | 
			
		||||
  ListItemAvatar,
 | 
			
		||||
  ListItemButton,
 | 
			
		||||
  ListItemSecondaryAction,
 | 
			
		||||
  ListItemText,
 | 
			
		||||
  TextField,
 | 
			
		||||
  Typography,
 | 
			
		||||
  autocompleteClasses,
 | 
			
		||||
} from "@mui/material";
 | 
			
		||||
import ClearIcon from "@mui/icons-material/Clear";
 | 
			
		||||
import { Member, fmtDate } from "../../api/MemberApi";
 | 
			
		||||
import { useFamily } from "../BaseFamilyRoute";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { MemberPhoto } from "../MemberPhoto";
 | 
			
		||||
import Icon from "@mdi/react";
 | 
			
		||||
import { mdiCross } from "@mdi/js";
 | 
			
		||||
import { useNavigate } from "react-router-dom";
 | 
			
		||||
 | 
			
		||||
export function MemberInput(p: {
 | 
			
		||||
  editable: boolean;
 | 
			
		||||
@@ -7,5 +28,99 @@ export function MemberInput(p: {
 | 
			
		||||
  label: string;
 | 
			
		||||
  filter: (m: Member) => boolean;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  return <>CHOOSE</>;
 | 
			
		||||
  const n = useNavigate();
 | 
			
		||||
  const family = useFamily();
 | 
			
		||||
 | 
			
		||||
  const choices = family.members.filter(p.filter);
 | 
			
		||||
 | 
			
		||||
  const [inputValue, setInputValue] = React.useState("");
 | 
			
		||||
 | 
			
		||||
  if (p.current) {
 | 
			
		||||
    const member = family.members.get(p.current)!;
 | 
			
		||||
    return (
 | 
			
		||||
      <div style={{ display: "flex", alignItems: "center" }}>
 | 
			
		||||
        <Typography variant="body2">{p.label}</Typography>
 | 
			
		||||
        <MemberItem
 | 
			
		||||
          member={member}
 | 
			
		||||
          onClick={
 | 
			
		||||
            !p.editable
 | 
			
		||||
              ? () => {
 | 
			
		||||
                  n(family.family.URL(`member/${member.id}`));
 | 
			
		||||
                }
 | 
			
		||||
              : undefined
 | 
			
		||||
          }
 | 
			
		||||
          secondary={
 | 
			
		||||
            p.editable ? (
 | 
			
		||||
              <>
 | 
			
		||||
                <IconButton
 | 
			
		||||
                  edge="end"
 | 
			
		||||
                  onClick={() => p.onValueChange(undefined)}
 | 
			
		||||
                >
 | 
			
		||||
                  <ClearIcon />
 | 
			
		||||
                </IconButton>
 | 
			
		||||
              </>
 | 
			
		||||
            ) : undefined
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!p.editable) return <></>;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Autocomplete
 | 
			
		||||
      value={p.current ? family.members.get(p.current) : undefined}
 | 
			
		||||
      onChange={(_event: any, newValue: Member | null | undefined) => {
 | 
			
		||||
        p.onValueChange(newValue?.id);
 | 
			
		||||
      }}
 | 
			
		||||
      inputValue={inputValue}
 | 
			
		||||
      onInputChange={(_event, newInputValue) => {
 | 
			
		||||
        setInputValue(newInputValue);
 | 
			
		||||
      }}
 | 
			
		||||
      options={choices}
 | 
			
		||||
      sx={{ width: "100%" }}
 | 
			
		||||
      filterOptions={(options, state) =>
 | 
			
		||||
        options.filter((m) =>
 | 
			
		||||
          m?.fullName.toLowerCase().includes(state.inputValue)
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
      getOptionLabel={(o) => o?.fullName ?? ""}
 | 
			
		||||
      renderInput={(params) => <TextField {...params} label={p.label} />}
 | 
			
		||||
      renderOption={(_props, option, _state) => (
 | 
			
		||||
        <MemberItem
 | 
			
		||||
          member={option}
 | 
			
		||||
          onClick={() => p.onValueChange(option?.id)}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function MemberItem(p: {
 | 
			
		||||
  member?: Member;
 | 
			
		||||
  onClick?: () => void;
 | 
			
		||||
  secondary?: React.ReactElement;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <ListItemButton onClick={p.onClick}>
 | 
			
		||||
      <ListItemAvatar>
 | 
			
		||||
        <MemberPhoto member={p.member!} />
 | 
			
		||||
      </ListItemAvatar>
 | 
			
		||||
      <ListItemText
 | 
			
		||||
        primary={
 | 
			
		||||
          <>
 | 
			
		||||
            {p.member?.fullName}{" "}
 | 
			
		||||
            {p.member?.dead && <Icon path={mdiCross} size={"1rem"} />}
 | 
			
		||||
          </>
 | 
			
		||||
        }
 | 
			
		||||
        secondary={`${fmtDate(p.member?.dateOfBirth)} - ${fmtDate(
 | 
			
		||||
          p.member?.dateOfDeath
 | 
			
		||||
        )}`}
 | 
			
		||||
      />
 | 
			
		||||
      {p.secondary && (
 | 
			
		||||
        <ListItemSecondaryAction>{p.secondary}</ListItemSecondaryAction>
 | 
			
		||||
      )}
 | 
			
		||||
    </ListItemButton>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,8 @@ import { useAlert } from "../../hooks/context_providers/AlertDialogProvider";
 | 
			
		||||
import { Area } from "react-easy-crop";
 | 
			
		||||
import getCroppedImg from "../../utils/crop_image";
 | 
			
		||||
import UploadIcon from "@mui/icons-material/Upload";
 | 
			
		||||
import LinkIcon from "@mui/icons-material/Link";
 | 
			
		||||
import { isDebug } from "../../utils/debug_utils";
 | 
			
		||||
 | 
			
		||||
export function UploadPhotoButton(p: {
 | 
			
		||||
  label: string;
 | 
			
		||||
@@ -55,6 +57,12 @@ export function UploadPhotoButton(p: {
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const uploadPhotoFromURL = async () => {
 | 
			
		||||
    const URL = prompt("Image URL ?");
 | 
			
		||||
    if (URL === null || URL.length === 0) return;
 | 
			
		||||
    setImageURL(URL);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const cancelCrop = () => {
 | 
			
		||||
    setImageURL(undefined);
 | 
			
		||||
  };
 | 
			
		||||
@@ -90,7 +98,16 @@ export function UploadPhotoButton(p: {
 | 
			
		||||
      >
 | 
			
		||||
        {p.label}
 | 
			
		||||
      </Button>
 | 
			
		||||
 | 
			
		||||
      {/* Upload button (from URL) */}{" "}
 | 
			
		||||
      {isDebug() && (
 | 
			
		||||
        <Button
 | 
			
		||||
          onClick={uploadPhotoFromURL}
 | 
			
		||||
          variant="outlined"
 | 
			
		||||
          startIcon={<LinkIcon />}
 | 
			
		||||
        >
 | 
			
		||||
          {p.label} from URL
 | 
			
		||||
        </Button>
 | 
			
		||||
      )}{" "}
 | 
			
		||||
      {/* Crop image dialog */}
 | 
			
		||||
      {imageURL && (
 | 
			
		||||
        <ImageCropperDialog
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user