Start to build edit member form
This commit is contained in:
parent
eacf3b0700
commit
d07dfd6596
@ -17,6 +17,10 @@ import { BaseFamilyRoute } from "./widgets/BaseFamilyRoute";
|
|||||||
import { BaseLoginPage } from "./widgets/BaseLoginpage";
|
import { BaseLoginPage } from "./widgets/BaseLoginpage";
|
||||||
import { FamilyUsersListRoute } from "./routes/family/FamilyUsersListRoute";
|
import { FamilyUsersListRoute } from "./routes/family/FamilyUsersListRoute";
|
||||||
import { FamilySettingsRoute } from "./routes/family/FamilySettingsRoute";
|
import { FamilySettingsRoute } from "./routes/family/FamilySettingsRoute";
|
||||||
|
import {
|
||||||
|
FamilyCreateMemberRoute,
|
||||||
|
FamilyMemberRoute,
|
||||||
|
} from "./routes/family/FamilyMemberRoute";
|
||||||
|
|
||||||
interface AuthContext {
|
interface AuthContext {
|
||||||
signedIn: boolean;
|
signedIn: boolean;
|
||||||
@ -47,6 +51,11 @@ export function App(): React.ReactElement {
|
|||||||
<Route path="profile" element={<ProfileRoute />} />
|
<Route path="profile" element={<ProfileRoute />} />
|
||||||
<Route path="family/:familyId/*" element={<BaseFamilyRoute />}>
|
<Route path="family/:familyId/*" element={<BaseFamilyRoute />}>
|
||||||
<Route path="" element={<FamilyHomeRoute />} />
|
<Route path="" element={<FamilyHomeRoute />} />
|
||||||
|
<Route
|
||||||
|
path="member/create"
|
||||||
|
element={<FamilyCreateMemberRoute />}
|
||||||
|
/>
|
||||||
|
<Route path="member/:memberId" element={<FamilyMemberRoute />} />
|
||||||
<Route path="settings" element={<FamilySettingsRoute />} />
|
<Route path="settings" element={<FamilySettingsRoute />} />
|
||||||
<Route path="users" element={<FamilyUsersListRoute />} />
|
<Route path="users" element={<FamilyUsersListRoute />} />
|
||||||
<Route path="*" element={<NotFoundRoute />} />
|
<Route path="*" element={<NotFoundRoute />} />
|
||||||
|
88
geneit_app/src/api/MemberApi.ts
Normal file
88
geneit_app/src/api/MemberApi.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
export type Sex = "M" | "F";
|
||||||
|
|
||||||
|
export interface MemberApi {
|
||||||
|
id: number;
|
||||||
|
family_id: number;
|
||||||
|
first_name?: string;
|
||||||
|
last_name?: string;
|
||||||
|
birth_last_name?: string;
|
||||||
|
photo_id?: number;
|
||||||
|
signed_photo_id?: number;
|
||||||
|
email?: string;
|
||||||
|
phone?: string;
|
||||||
|
address?: string;
|
||||||
|
city?: string;
|
||||||
|
postal_code?: string;
|
||||||
|
country?: string;
|
||||||
|
sex?: Sex;
|
||||||
|
time_create?: number;
|
||||||
|
time_update?: number;
|
||||||
|
mother?: number;
|
||||||
|
father?: number;
|
||||||
|
birth_year?: number;
|
||||||
|
birth_month?: number;
|
||||||
|
birth_day?: number;
|
||||||
|
dead: boolean;
|
||||||
|
death_year?: number;
|
||||||
|
death_month?: number;
|
||||||
|
death_day?: number;
|
||||||
|
note?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Member implements MemberApi {
|
||||||
|
id: number;
|
||||||
|
family_id: number;
|
||||||
|
first_name?: string;
|
||||||
|
last_name?: string;
|
||||||
|
birth_last_name?: string;
|
||||||
|
photo_id?: number;
|
||||||
|
signed_photo_id?: number;
|
||||||
|
email?: string;
|
||||||
|
phone?: string;
|
||||||
|
address?: string;
|
||||||
|
city?: string;
|
||||||
|
postal_code?: string;
|
||||||
|
country?: string;
|
||||||
|
sex?: Sex;
|
||||||
|
time_create?: number;
|
||||||
|
time_update?: number;
|
||||||
|
mother?: number;
|
||||||
|
father?: number;
|
||||||
|
birth_year?: number;
|
||||||
|
birth_month?: number;
|
||||||
|
birth_day?: number;
|
||||||
|
dead!: boolean;
|
||||||
|
death_year?: number;
|
||||||
|
death_month?: number;
|
||||||
|
death_day?: number;
|
||||||
|
note?: string;
|
||||||
|
|
||||||
|
constructor(m: MemberApi) {
|
||||||
|
this.id = m.id;
|
||||||
|
this.family_id = m.family_id;
|
||||||
|
this.first_name = m.first_name;
|
||||||
|
this.last_name = m.last_name;
|
||||||
|
this.birth_last_name = m.birth_last_name;
|
||||||
|
this.photo_id = m.photo_id;
|
||||||
|
this.signed_photo_id = m.signed_photo_id;
|
||||||
|
this.email = m.email;
|
||||||
|
this.phone = m.phone;
|
||||||
|
this.address = m.address;
|
||||||
|
this.city = m.city;
|
||||||
|
this.postal_code = m.postal_code;
|
||||||
|
this.country = m.country;
|
||||||
|
this.sex = m.sex;
|
||||||
|
this.time_create = m.time_create;
|
||||||
|
this.time_update = m.time_update;
|
||||||
|
this.mother = m.mother;
|
||||||
|
this.father = m.father;
|
||||||
|
this.birth_year = m.birth_year;
|
||||||
|
this.birth_month = m.birth_month;
|
||||||
|
this.birth_day = m.birth_day;
|
||||||
|
this.dead = m.dead;
|
||||||
|
this.death_year = m.death_year;
|
||||||
|
this.death_month = m.death_month;
|
||||||
|
this.death_day = m.death_day;
|
||||||
|
this.note = m.note;
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +1,37 @@
|
|||||||
import { APIClient } from "./ApiClient";
|
import { APIClient } from "./ApiClient";
|
||||||
|
|
||||||
interface LenConstraint {
|
interface NumberConstraint {
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LenConstraint {
|
||||||
min: number;
|
min: number;
|
||||||
max: number;
|
max: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Constraints {
|
interface Constraints {
|
||||||
|
date_year: NumberConstraint;
|
||||||
|
date_month: NumberConstraint;
|
||||||
|
date_day: NumberConstraint;
|
||||||
|
photo_allowed_types: string[];
|
||||||
|
photo_max_size: number;
|
||||||
mail_len: LenConstraint;
|
mail_len: LenConstraint;
|
||||||
user_name_len: LenConstraint;
|
user_name_len: LenConstraint;
|
||||||
password_len: LenConstraint;
|
password_len: LenConstraint;
|
||||||
family_name_len: LenConstraint;
|
family_name_len: LenConstraint;
|
||||||
invitation_code_len: LenConstraint;
|
invitation_code_len: LenConstraint;
|
||||||
|
member_first_name: LenConstraint;
|
||||||
|
member_last_name: LenConstraint;
|
||||||
|
member_birth_last_name: LenConstraint;
|
||||||
|
member_email: LenConstraint;
|
||||||
|
member_phone: LenConstraint;
|
||||||
|
member_address: LenConstraint;
|
||||||
|
member_city: LenConstraint;
|
||||||
|
member_postal_code: LenConstraint;
|
||||||
|
member_country: LenConstraint;
|
||||||
|
member_sex: LenConstraint;
|
||||||
|
member_note: LenConstraint;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OIDCProvider {
|
interface OIDCProvider {
|
||||||
@ -18,10 +39,24 @@ interface OIDCProvider {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Country {
|
||||||
|
code: string;
|
||||||
|
en: string;
|
||||||
|
fr: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CouplesStates {
|
||||||
|
code: string;
|
||||||
|
en: string;
|
||||||
|
fr: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ServerConfig {
|
export interface ServerConfig {
|
||||||
constraints: Constraints;
|
constraints: Constraints;
|
||||||
mail: string;
|
mail: string;
|
||||||
oidc_providers: OIDCProvider[];
|
oidc_providers: OIDCProvider[];
|
||||||
|
countries: Country[];
|
||||||
|
couples_states: CouplesStates[];
|
||||||
}
|
}
|
||||||
|
|
||||||
let config: ServerConfig | null = null;
|
let config: ServerConfig | null = null;
|
||||||
|
112
geneit_app/src/routes/family/FamilyMemberRoute.tsx
Normal file
112
geneit_app/src/routes/family/FamilyMemberRoute.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Member } from "../../api/MemberApi";
|
||||||
|
import { useFamily } from "../../widgets/BaseFamilyRoute";
|
||||||
|
import { Grid, Typography } from "@mui/material";
|
||||||
|
import { PropertiesBox } from "../../widgets/PropertiesBox";
|
||||||
|
import { PropEdit } from "../../widgets/PropEdit";
|
||||||
|
import { ServerApi } from "../../api/ServerApi";
|
||||||
|
import { SexSelection } from "../../widgets/SexSelection";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new member route
|
||||||
|
*/
|
||||||
|
export function FamilyCreateMemberRoute(): React.ReactElement {
|
||||||
|
const family = useFamily();
|
||||||
|
const member = new Member({
|
||||||
|
id: 0,
|
||||||
|
dead: false,
|
||||||
|
family_id: family.family.family_id,
|
||||||
|
});
|
||||||
|
return <MemberPage member={member} creating={true} forceEdit={true} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit existing member route
|
||||||
|
*/
|
||||||
|
export function FamilyMemberRoute(): React.ReactElement {
|
||||||
|
return <p>TODO</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MemberPage(p: {
|
||||||
|
member: Member;
|
||||||
|
forceEdit: boolean;
|
||||||
|
creating: boolean;
|
||||||
|
}): React.ReactElement {
|
||||||
|
// TODO : add confirmation when leaving page https://dev.to/bangash1996/detecting-user-leaving-page-with-react-router-dom-v602-33ni
|
||||||
|
const [editing, setEditing] = React.useState(p.forceEdit);
|
||||||
|
const [member, setMember] = React.useState(structuredClone(p.member));
|
||||||
|
|
||||||
|
const updatedMember = () => {
|
||||||
|
// TODO : add confirmation
|
||||||
|
setMember(structuredClone(member));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ maxWidth: "2000px", margin: "auto" }}>
|
||||||
|
<Typography variant="h3">Fiche de membre</Typography>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item sm={12} md={6}>
|
||||||
|
<PropertiesBox title="Informations générales">
|
||||||
|
{/* Sex */}
|
||||||
|
<SexSelection
|
||||||
|
current={member.sex}
|
||||||
|
onChange={(v) => {
|
||||||
|
member.sex = v;
|
||||||
|
updatedMember();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* First name */}
|
||||||
|
<PropEdit
|
||||||
|
label="Prénom"
|
||||||
|
editable={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={editing}
|
||||||
|
value={member.last_name}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
member.last_name = v;
|
||||||
|
updatedMember();
|
||||||
|
}}
|
||||||
|
size={ServerApi.Config.constraints.member_last_name}
|
||||||
|
/>
|
||||||
|
<PropEdit
|
||||||
|
label="Nom de naissance"
|
||||||
|
editable={editing}
|
||||||
|
value={member.birth_last_name}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
member.birth_last_name = v;
|
||||||
|
updatedMember();
|
||||||
|
}}
|
||||||
|
size={ServerApi.Config.constraints.member_birth_last_name}
|
||||||
|
/>
|
||||||
|
</PropertiesBox>
|
||||||
|
</Grid>
|
||||||
|
<Grid item sm={12} md={6}>
|
||||||
|
<PropertiesBox title="Contact"></PropertiesBox>
|
||||||
|
</Grid>
|
||||||
|
<Grid item sm={12} md={6}>
|
||||||
|
<PropertiesBox title="Photo"></PropertiesBox>
|
||||||
|
</Grid>
|
||||||
|
<Grid item sm={12} md={6}>
|
||||||
|
<PropertiesBox title="Biographie"></PropertiesBox>
|
||||||
|
</Grid>
|
||||||
|
<Grid item sm={12} md={6}>
|
||||||
|
<PropertiesBox title="Époux / Épouse">TODO</PropertiesBox>
|
||||||
|
</Grid>
|
||||||
|
<Grid item sm={12} md={6}>
|
||||||
|
<PropertiesBox title="Enfants">TODO</PropertiesBox>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
35
geneit_app/src/widgets/PropEdit.tsx
Normal file
35
geneit_app/src/widgets/PropEdit.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { TextField } from "@mui/material";
|
||||||
|
import { LenConstraint } from "../api/ServerApi";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Couple / Member property edition
|
||||||
|
*/
|
||||||
|
export function PropEdit(p: {
|
||||||
|
label: string;
|
||||||
|
editable: boolean;
|
||||||
|
value?: string;
|
||||||
|
onValueChange: (newVal: string | undefined) => void;
|
||||||
|
size: LenConstraint;
|
||||||
|
}): React.ReactElement {
|
||||||
|
if (((!p.editable && p.value) ?? "") === "") return <></>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
label={p.label}
|
||||||
|
value={p.value}
|
||||||
|
onChange={(e) =>
|
||||||
|
p.onValueChange(
|
||||||
|
e.target.value.length === 0 ? undefined : e.target.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
inputProps={{
|
||||||
|
maxLength: p.size.max,
|
||||||
|
}}
|
||||||
|
InputProps={{
|
||||||
|
readOnly: !p.editable,
|
||||||
|
}}
|
||||||
|
variant={p.editable ? "filled" : "standard"}
|
||||||
|
style={{ width: "100%", marginBottom: "15px" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
14
geneit_app/src/widgets/PropertiesBox.tsx
Normal file
14
geneit_app/src/widgets/PropertiesBox.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { Paper, Typography } from "@mui/material";
|
||||||
|
|
||||||
|
export function PropertiesBox(
|
||||||
|
p: React.PropsWithChildren<{ title: string }>
|
||||||
|
): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<Paper elevation={3} style={{ padding: "15px" }}>
|
||||||
|
<Typography variant="h5" style={{ marginBottom: "15px" }}>
|
||||||
|
{p.title}
|
||||||
|
</Typography>
|
||||||
|
{p.children}
|
||||||
|
</Paper>
|
||||||
|
);
|
||||||
|
}
|
28
geneit_app/src/widgets/SexSelection.tsx
Normal file
28
geneit_app/src/widgets/SexSelection.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
RadioGroup,
|
||||||
|
FormControlLabel,
|
||||||
|
Radio,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { Sex } from "../api/MemberApi";
|
||||||
|
|
||||||
|
export function SexSelection(p: {
|
||||||
|
current?: Sex;
|
||||||
|
onChange: (s: Sex) => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<FormControl style={{ marginBottom: "15px" }}>
|
||||||
|
<FormLabel id="sex-label">Sexe</FormLabel>
|
||||||
|
<RadioGroup
|
||||||
|
row
|
||||||
|
aria-labelledby="sex-label"
|
||||||
|
value={p.current}
|
||||||
|
onChange={(e) => p.onChange(e.target.value as Sex)}
|
||||||
|
>
|
||||||
|
<FormControlLabel value="M" control={<Radio />} label="Homme" />
|
||||||
|
<FormControlLabel value="F" control={<Radio />} label="Femme" />
|
||||||
|
</RadioGroup>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user