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 { FamilyUsersListRoute } from "./routes/family/FamilyUsersListRoute";
|
||||
import { FamilySettingsRoute } from "./routes/family/FamilySettingsRoute";
|
||||
import {
|
||||
FamilyCreateMemberRoute,
|
||||
FamilyMemberRoute,
|
||||
} from "./routes/family/FamilyMemberRoute";
|
||||
|
||||
interface AuthContext {
|
||||
signedIn: boolean;
|
||||
@ -47,6 +51,11 @@ export function App(): React.ReactElement {
|
||||
<Route path="profile" element={<ProfileRoute />} />
|
||||
<Route path="family/:familyId/*" element={<BaseFamilyRoute />}>
|
||||
<Route path="" element={<FamilyHomeRoute />} />
|
||||
<Route
|
||||
path="member/create"
|
||||
element={<FamilyCreateMemberRoute />}
|
||||
/>
|
||||
<Route path="member/:memberId" element={<FamilyMemberRoute />} />
|
||||
<Route path="settings" element={<FamilySettingsRoute />} />
|
||||
<Route path="users" element={<FamilyUsersListRoute />} />
|
||||
<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";
|
||||
|
||||
interface LenConstraint {
|
||||
interface NumberConstraint {
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
export interface LenConstraint {
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
interface Constraints {
|
||||
date_year: NumberConstraint;
|
||||
date_month: NumberConstraint;
|
||||
date_day: NumberConstraint;
|
||||
photo_allowed_types: string[];
|
||||
photo_max_size: number;
|
||||
mail_len: LenConstraint;
|
||||
user_name_len: LenConstraint;
|
||||
password_len: LenConstraint;
|
||||
family_name_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 {
|
||||
@ -18,10 +39,24 @@ interface OIDCProvider {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Country {
|
||||
code: string;
|
||||
en: string;
|
||||
fr: string;
|
||||
}
|
||||
|
||||
export interface CouplesStates {
|
||||
code: string;
|
||||
en: string;
|
||||
fr: string;
|
||||
}
|
||||
|
||||
export interface ServerConfig {
|
||||
constraints: Constraints;
|
||||
mail: string;
|
||||
oidc_providers: OIDCProvider[];
|
||||
countries: Country[];
|
||||
couples_states: CouplesStates[];
|
||||
}
|
||||
|
||||
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