Can set the father and the mother of a member
This commit is contained in:
parent
e237abe4e1
commit
335ff0f178
@ -31,6 +31,12 @@ export interface MemberDataApi {
|
|||||||
note?: string;
|
note?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DateValue {
|
||||||
|
year?: number;
|
||||||
|
month?: number;
|
||||||
|
day?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export class Member implements MemberDataApi {
|
export class Member implements MemberDataApi {
|
||||||
id: number;
|
id: number;
|
||||||
family_id: number;
|
family_id: number;
|
||||||
@ -111,13 +117,43 @@ export class Member implements MemberDataApi {
|
|||||||
return this.photo_id !== null;
|
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}`;
|
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`;
|
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 {
|
export class MembersList {
|
||||||
@ -132,6 +168,14 @@ export class MembersList {
|
|||||||
this.map.set(m.id, m);
|
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 {
|
export class MemberApi {
|
||||||
|
@ -383,11 +383,7 @@ export function MemberPage(p: {
|
|||||||
label="Date de naissance"
|
label="Date de naissance"
|
||||||
editable={p.editing}
|
editable={p.editing}
|
||||||
id="dob"
|
id="dob"
|
||||||
value={{
|
value={member.dateOfBirth}
|
||||||
year: member.birth_year,
|
|
||||||
month: member.birth_month,
|
|
||||||
day: member.birth_day,
|
|
||||||
}}
|
|
||||||
onValueChange={(d) => {
|
onValueChange={(d) => {
|
||||||
member.birth_year = d.year;
|
member.birth_year = d.year;
|
||||||
member.birth_month = d.month;
|
member.birth_month = d.month;
|
||||||
@ -412,11 +408,7 @@ export function MemberPage(p: {
|
|||||||
label="Date de décès"
|
label="Date de décès"
|
||||||
editable={p.editing}
|
editable={p.editing}
|
||||||
id="dod"
|
id="dod"
|
||||||
value={{
|
value={member.dateOfDeath}
|
||||||
year: member.death_year,
|
|
||||||
month: member.death_month,
|
|
||||||
day: member.death_day,
|
|
||||||
}}
|
|
||||||
onValueChange={(d) => {
|
onValueChange={(d) => {
|
||||||
member.death_year = d.year;
|
member.death_year = d.year;
|
||||||
member.death_month = d.month;
|
member.death_month = d.month;
|
||||||
@ -426,6 +418,7 @@ export function MemberPage(p: {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Father */}
|
{/* Father */}
|
||||||
|
<br />
|
||||||
<MemberInput
|
<MemberInput
|
||||||
editable={p.editing}
|
editable={p.editing}
|
||||||
label="Père"
|
label="Père"
|
||||||
@ -433,20 +426,25 @@ export function MemberPage(p: {
|
|||||||
member.father = m;
|
member.father = m;
|
||||||
updatedMember();
|
updatedMember();
|
||||||
}}
|
}}
|
||||||
filter={(m) => m.sex === "M" || m.sex === undefined}
|
filter={(m) =>
|
||||||
|
(m.sex === "M" || m.sex === undefined) && m.id !== member.id
|
||||||
|
}
|
||||||
current={member.father}
|
current={member.father}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Mother */}
|
{/* Mother */}
|
||||||
|
<br />
|
||||||
<MemberInput
|
<MemberInput
|
||||||
editable={p.editing}
|
editable={p.editing}
|
||||||
label="Mère"
|
label="Mère"
|
||||||
onValueChange={(m) => {
|
onValueChange={(m) => {
|
||||||
member.father = m;
|
member.mother = m;
|
||||||
updatedMember();
|
updatedMember();
|
||||||
}}
|
}}
|
||||||
filter={(m) => m.sex === "F" || m.sex === undefined}
|
filter={(m) =>
|
||||||
current={member.father}
|
(m.sex === "F" || m.sex === undefined) && m.id !== member.id
|
||||||
|
}
|
||||||
|
current={member.mother}
|
||||||
/>
|
/>
|
||||||
</PropertiesBox>
|
</PropertiesBox>
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -469,7 +467,7 @@ export function MemberPage(p: {
|
|||||||
onPhotoSelected={uploadNewPhoto}
|
onPhotoSelected={uploadNewPhoto}
|
||||||
/>{" "}
|
/>{" "}
|
||||||
{member.hasPhoto && (
|
{member.hasPhoto && (
|
||||||
<RouterLink to={member.photoURL} target="_blank">
|
<RouterLink to={member.photoURL!} target="_blank">
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
startIcon={<FileDownloadIcon />}
|
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: {
|
export function MemberPhoto(p: {
|
||||||
member: Member;
|
member: Member;
|
||||||
width: number;
|
width?: number;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
<Avatar
|
<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"
|
variant="rounded"
|
||||||
src={p.member.thumbnailURL}
|
src={p.member.thumbnailURL ?? undefined}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,16 @@
|
|||||||
import { Stack, TextField, Typography } from "@mui/material";
|
import { Stack, TextField, Typography } from "@mui/material";
|
||||||
import { NumberConstraint, ServerApi } from "../../api/ServerApi";
|
import { NumberConstraint, ServerApi } from "../../api/ServerApi";
|
||||||
|
import { DateValue, fmtDate } from "../../api/MemberApi";
|
||||||
export interface DateValue {
|
|
||||||
year?: number;
|
|
||||||
month?: number;
|
|
||||||
day?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DateInput(p: {
|
export function DateInput(p: {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
value: DateValue;
|
value?: DateValue;
|
||||||
onValueChange: (newVal: DateValue) => void;
|
onValueChange: (newVal: DateValue) => void;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
if (!p.editable) {
|
if (!p.editable) {
|
||||||
if (!p.value.year && !p.value.month && !p.value.day) return <></>;
|
if (!p.value) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Typography
|
<Typography
|
||||||
@ -23,8 +18,7 @@ export function DateInput(p: {
|
|||||||
display="block"
|
display="block"
|
||||||
style={{ marginBottom: "15px" }}
|
style={{ marginBottom: "15px" }}
|
||||||
>
|
>
|
||||||
{p.label} : {p.value.day ?? "__"} / {p.value.month ?? "__"} /{" "}
|
{p.label} : {fmtDate(p.value!)}
|
||||||
{p.value.year ?? "__"}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -41,8 +35,8 @@ export function DateInput(p: {
|
|||||||
required
|
required
|
||||||
id={`${p.id}-day`}
|
id={`${p.id}-day`}
|
||||||
label="Jour"
|
label="Jour"
|
||||||
value={p.value.day}
|
value={p.value?.day}
|
||||||
error={isValErr(p.value.day, ServerApi.Config.constraints.date_day)}
|
error={isValErr(p.value?.day, ServerApi.Config.constraints.date_day)}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
style={{ flex: 20 }}
|
style={{ flex: 20 }}
|
||||||
type="number"
|
type="number"
|
||||||
@ -50,8 +44,8 @@ export function DateInput(p: {
|
|||||||
const val = Number(e.target.value);
|
const val = Number(e.target.value);
|
||||||
p.onValueChange({
|
p.onValueChange({
|
||||||
day: val > 0 ? val : undefined,
|
day: val > 0 ? val : undefined,
|
||||||
month: p.value.month,
|
month: p.value?.month,
|
||||||
year: p.value.year,
|
year: p.value?.year,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
inputProps={{
|
inputProps={{
|
||||||
@ -64,17 +58,20 @@ export function DateInput(p: {
|
|||||||
required
|
required
|
||||||
id={`${p.id}-month`}
|
id={`${p.id}-month`}
|
||||||
label="Mois"
|
label="Mois"
|
||||||
value={p.value.month}
|
value={p.value?.month}
|
||||||
error={isValErr(p.value.month, ServerApi.Config.constraints.date_month)}
|
error={isValErr(
|
||||||
|
p.value?.month,
|
||||||
|
ServerApi.Config.constraints.date_month
|
||||||
|
)}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
style={{ flex: 20 }}
|
style={{ flex: 20 }}
|
||||||
type="number"
|
type="number"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const val = Number(e.target.value);
|
const val = Number(e.target.value);
|
||||||
p.onValueChange({
|
p.onValueChange({
|
||||||
day: p.value.day,
|
day: p.value?.day,
|
||||||
month: val > 0 ? val : undefined,
|
month: val > 0 ? val : undefined,
|
||||||
year: p.value.year,
|
year: p.value?.year,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
inputProps={{
|
inputProps={{
|
||||||
@ -87,16 +84,16 @@ export function DateInput(p: {
|
|||||||
required
|
required
|
||||||
id={`${p.id}-year`}
|
id={`${p.id}-year`}
|
||||||
label="Année"
|
label="Année"
|
||||||
value={p.value.year}
|
value={p.value?.year}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const val = Number(e.target.value);
|
const val = Number(e.target.value);
|
||||||
p.onValueChange({
|
p.onValueChange({
|
||||||
day: p.value.day,
|
day: p.value?.day,
|
||||||
month: p.value.month,
|
month: p.value?.month,
|
||||||
year: val > 0 ? val : undefined,
|
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"
|
variant="filled"
|
||||||
style={{ flex: 30 }}
|
style={{ flex: 30 }}
|
||||||
type="number"
|
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: {
|
export function MemberInput(p: {
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
@ -7,5 +28,99 @@ export function MemberInput(p: {
|
|||||||
label: string;
|
label: string;
|
||||||
filter: (m: Member) => boolean;
|
filter: (m: Member) => boolean;
|
||||||
}): React.ReactElement {
|
}): 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 { Area } from "react-easy-crop";
|
||||||
import getCroppedImg from "../../utils/crop_image";
|
import getCroppedImg from "../../utils/crop_image";
|
||||||
import UploadIcon from "@mui/icons-material/Upload";
|
import UploadIcon from "@mui/icons-material/Upload";
|
||||||
|
import LinkIcon from "@mui/icons-material/Link";
|
||||||
|
import { isDebug } from "../../utils/debug_utils";
|
||||||
|
|
||||||
export function UploadPhotoButton(p: {
|
export function UploadPhotoButton(p: {
|
||||||
label: string;
|
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 = () => {
|
const cancelCrop = () => {
|
||||||
setImageURL(undefined);
|
setImageURL(undefined);
|
||||||
};
|
};
|
||||||
@ -90,7 +98,16 @@ export function UploadPhotoButton(p: {
|
|||||||
>
|
>
|
||||||
{p.label}
|
{p.label}
|
||||||
</Button>
|
</Button>
|
||||||
|
{/* Upload button (from URL) */}{" "}
|
||||||
|
{isDebug() && (
|
||||||
|
<Button
|
||||||
|
onClick={uploadPhotoFromURL}
|
||||||
|
variant="outlined"
|
||||||
|
startIcon={<LinkIcon />}
|
||||||
|
>
|
||||||
|
{p.label} from URL
|
||||||
|
</Button>
|
||||||
|
)}{" "}
|
||||||
{/* Crop image dialog */}
|
{/* Crop image dialog */}
|
||||||
{imageURL && (
|
{imageURL && (
|
||||||
<ImageCropperDialog
|
<ImageCropperDialog
|
||||||
|
Loading…
Reference in New Issue
Block a user