Start to build edit member form
This commit is contained in:
		@@ -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>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user