Start to build edit member form

This commit is contained in:
Pierre HUBERT 2023-08-08 13:55:51 +02:00
parent eacf3b0700
commit d07dfd6596
7 changed files with 322 additions and 1 deletions

View File

@ -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 />} />

View 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;
}
}

View File

@ -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;

View 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>
);
}

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

View 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>
);
}

View 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>
);
}