1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-09 03:54:23 +01:00

(wip) create/update user children

This commit is contained in:
Du Peng 2023-04-04 18:09:17 +02:00
parent 10ef342edf
commit dc4151cfb0
11 changed files with 94 additions and 30 deletions

View File

@ -2,7 +2,7 @@
# API Controller for resources of type Child # API Controller for resources of type Child
# Children are used to provide a way to manage multiple users in the family account # Children are used to provide a way to manage multiple users in the family account
class API::ChildrenController < API::ApiController class API::ChildrenController < API::APIController
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_child, only: %i[show update destroy] before_action :set_child, only: %i[show update destroy]

View File

@ -1,22 +1,25 @@
import React from 'react'; import React from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import moment from 'moment';
import { Child } from '../../models/child'; import { Child } from '../../models/child';
import { TDateISODate } from '../../typings/date-iso'; import { TDateISODate } from '../../typings/date-iso';
import { FormInput } from '../form/form-input'; import { FormInput } from '../form/form-input';
import { FabButton } from '../base/fab-button';
interface ChildFormProps { interface ChildFormProps {
child: Child; child: Child;
onChange: (field: string, value: string | TDateISODate) => void; onChange: (field: string, value: string | TDateISODate) => void;
onSubmit: (data: Child) => void;
} }
/** /**
* A form for creating or editing a child. * A form for creating or editing a child.
*/ */
export const ChildForm: React.FC<ChildFormProps> = ({ child, onChange }) => { export const ChildForm: React.FC<ChildFormProps> = ({ child, onChange, onSubmit }) => {
const { t } = useTranslation('public'); const { t } = useTranslation('public');
const { register, formState } = useForm<Child>({ const { register, formState, handleSubmit } = useForm<Child>({
defaultValues: child defaultValues: child
}); });
@ -32,7 +35,7 @@ export const ChildForm: React.FC<ChildFormProps> = ({ child, onChange }) => {
<div className="info-area"> <div className="info-area">
{t('app.public.child_form.child_form_info')} {t('app.public.child_form.child_form_info')}
</div> </div>
<form> <form onSubmit={handleSubmit(onSubmit)}>
<FormInput id="first_name" <FormInput id="first_name"
register={register} register={register}
rules={{ required: true }} rules={{ required: true }}
@ -47,6 +50,35 @@ export const ChildForm: React.FC<ChildFormProps> = ({ child, onChange }) => {
label={t('app.public.child_form.last_name')} label={t('app.public.child_form.last_name')}
onChange={handleChange} onChange={handleChange}
/> />
<FormInput id="birthday"
register={register}
rules={{ required: true, validate: (value) => moment(value).isBefore(moment().subtract(18, 'year')) }}
formState={formState}
label={t('app.public.child_form.birthday')}
type="date"
max={moment().subtract(18, 'year').format('YYYY-MM-DD')}
onChange={handleChange}
/>
<FormInput id="phone"
register={register}
formState={formState}
label={t('app.public.child_form.phone')}
onChange={handleChange}
type="tel"
/>
<FormInput id="email"
register={register}
rules={{ required: true }}
formState={formState}
label={t('app.public.child_form.email')}
onChange={handleChange}
/>
<div className="actions">
<FabButton type="button" onClick={handleSubmit(onSubmit)}>
{t('app.public.child_form.save')}
</FabButton>
</div>
</form> </form>
</div> </div>
); );

View File

@ -2,6 +2,7 @@ import React from 'react';
import { Child } from '../../models/child'; import { Child } from '../../models/child';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FabButton } from '../base/fab-button'; import { FabButton } from '../base/fab-button';
import FormatLib from '../../lib/format';
interface ChildItemProps { interface ChildItemProps {
child: Child; child: Child;
@ -27,7 +28,7 @@ export const ChildItem: React.FC<ChildItemProps> = ({ child, onEdit, onDelete })
</div> </div>
<div> <div>
<div>{t('app.public.child_item.birthday')}</div> <div>{t('app.public.child_item.birthday')}</div>
<div>{child.birthday}</div> <div>{FormatLib.date(child.birthday)}</div>
</div> </div>
<div className="actions"> <div className="actions">
<FabButton icon={<i className="fa fa-edit" />} onClick={() => onEdit(child)} className="edit-button" /> <FabButton icon={<i className="fa fa-edit" />} onClick={() => onEdit(child)} className="edit-button" />

View File

@ -1,5 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { useState } from 'react'; import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FabModal, ModalSize } from '../base/fab-modal'; import { FabModal, ModalSize } from '../base/fab-modal';
import { Child } from '../../models/child'; import { Child } from '../../models/child';
@ -11,15 +11,20 @@ interface ChildModalProps {
child?: Child; child?: Child;
isOpen: boolean; isOpen: boolean;
toggleModal: () => void; toggleModal: () => void;
onSuccess: (child: Child) => void;
onError: (error: string) => void;
} }
/** /**
* A modal for creating or editing a child. * A modal for creating or editing a child.
*/ */
export const ChildModal: React.FC<ChildModalProps> = ({ child, isOpen, toggleModal }) => { export const ChildModal: React.FC<ChildModalProps> = ({ child, isOpen, toggleModal, onSuccess, onError }) => {
const { t } = useTranslation('public'); const { t } = useTranslation('public');
const [data, setData] = useState<Child>(child); const [data, setData] = useState<Child>(child);
console.log(child, data);
useEffect(() => {
setData(child);
}, [child]);
/** /**
* Save the child to the API * Save the child to the API
@ -32,18 +37,12 @@ export const ChildModal: React.FC<ChildModalProps> = ({ child, isOpen, toggleMod
await ChildAPI.create(data); await ChildAPI.create(data);
} }
toggleModal(); toggleModal();
onSuccess(data);
} catch (error) { } catch (error) {
console.error(error); onError(error);
} }
}; };
/**
* Check if the form is valid to save the child
*/
const isPreventedSaveChild = (): boolean => {
return !data?.first_name || !data?.last_name;
};
/** /**
* Handle the change of a child form field * Handle the change of a child form field
*/ */
@ -60,10 +59,9 @@ export const ChildModal: React.FC<ChildModalProps> = ({ child, isOpen, toggleMod
isOpen={isOpen} isOpen={isOpen}
toggleModal={toggleModal} toggleModal={toggleModal}
closeButton={true} closeButton={true}
confirmButton={t('app.public.child_modal.save')} confirmButton={false}
onConfirm={handleSaveChild} onConfirm={handleSaveChild} >
preventConfirm={isPreventedSaveChild()}> <ChildForm child={child} onChange={handleChildChanged} onSubmit={handleSaveChild} />
<ChildForm child={child} onChange={handleChildChanged} />
</FabModal> </FabModal>
); );
}; };

View File

@ -1,7 +1,6 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { react2angular } from 'react2angular'; import { react2angular } from 'react2angular';
import { Child } from '../../models/child'; import { Child } from '../../models/child';
// import { ChildListItem } from './child-list-item';
import ChildAPI from '../../api/child'; import ChildAPI from '../../api/child';
import { User } from '../../models/user'; import { User } from '../../models/user';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -15,12 +14,14 @@ declare const Application: IApplication;
interface ChildrenListProps { interface ChildrenListProps {
currentUser: User; currentUser: User;
onSuccess: (error: string) => void;
onError: (error: string) => void;
} }
/** /**
* A list of children belonging to the current user. * A list of children belonging to the current user.
*/ */
export const ChildrenList: React.FC<ChildrenListProps> = ({ currentUser }) => { export const ChildrenList: React.FC<ChildrenListProps> = ({ currentUser, onError }) => {
const { t } = useTranslation('public'); const { t } = useTranslation('public');
const [children, setChildren] = useState<Array<Child>>([]); const [children, setChildren] = useState<Array<Child>>([]);
@ -52,10 +53,17 @@ export const ChildrenList: React.FC<ChildrenListProps> = ({ currentUser }) => {
*/ */
const deleteChild = (child: Child) => { const deleteChild = (child: Child) => {
ChildAPI.destroy(child.id).then(() => { ChildAPI.destroy(child.id).then(() => {
setChildren(children.filter(c => c.id !== child.id)); ChildAPI.index({ user_id: currentUser.id }).then(setChildren);
}); });
}; };
/**
* Handle save child success from the API
*/
const handleSaveChildSuccess = () => {
ChildAPI.index({ user_id: currentUser.id }).then(setChildren);
};
return ( return (
<section> <section>
<header> <header>
@ -70,7 +78,7 @@ export const ChildrenList: React.FC<ChildrenListProps> = ({ currentUser }) => {
<ChildItem key={child.id} child={child} onEdit={editChild} onDelete={deleteChild} /> <ChildItem key={child.id} child={child} onEdit={editChild} onDelete={deleteChild} />
))} ))}
</div> </div>
<ChildModal child={child} isOpen={isOpenChildModal} toggleModal={() => setIsOpenChildModal(false)} /> <ChildModal child={child} isOpen={isOpenChildModal} toggleModal={() => setIsOpenChildModal(false)} onSuccess={handleSaveChildSuccess} onError={onError} />
</section> </section>
); );
}; };
@ -83,4 +91,4 @@ const ChildrenListWrapper: React.FC<ChildrenListProps> = (props) => {
); );
}; };
Application.Components.component('childrenList', react2angular(ChildrenListWrapper, ['currentUser'])); Application.Components.component('childrenList', react2angular(ChildrenListWrapper, ['currentUser', 'onSuccess', 'onError']));

View File

@ -22,13 +22,14 @@ type FormInputProps<TFieldValues, TInputType> = FormComponent<TFieldValues> & Ab
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void, onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void,
nullable?: boolean, nullable?: boolean,
ariaLabel?: string, ariaLabel?: string,
maxLength?: number maxLength?: number,
max?: number | string,
} }
/** /**
* This component is a template for an input component to use within React Hook Form * This component is a template for an input component to use within React Hook Form
*/ */
export const FormInput = <TFieldValues extends FieldValues, TInputType>({ id, register, label, tooltip, defaultValue, icon, className, rules, disabled, type, addOn, addOnAction, addOnClassName, addOnAriaLabel, placeholder, error, warning, formState, step, onChange, debounce, accept, nullable = false, ariaLabel, maxLength }: FormInputProps<TFieldValues, TInputType>) => { export const FormInput = <TFieldValues extends FieldValues, TInputType>({ id, register, label, tooltip, defaultValue, icon, className, rules, disabled, type, addOn, addOnAction, addOnClassName, addOnAriaLabel, placeholder, error, warning, formState, step, onChange, debounce, accept, nullable = false, ariaLabel, maxLength, max }: FormInputProps<TFieldValues, TInputType>) => {
const [characterCount, setCharacterCount] = useState<number>(0); const [characterCount, setCharacterCount] = useState<number>(0);
/** /**
@ -100,7 +101,8 @@ export const FormInput = <TFieldValues extends FieldValues, TInputType>({ id, re
disabled={typeof disabled === 'function' ? disabled(id) : disabled} disabled={typeof disabled === 'function' ? disabled(id) : disabled}
placeholder={placeholder} placeholder={placeholder}
accept={accept} accept={accept}
maxLength={maxLength} /> maxLength={maxLength}
max={max} />
{(type === 'file' && placeholder) && <span className='fab-button is-black file-placeholder'>{placeholder}</span>} {(type === 'file' && placeholder) && <span className='fab-button is-black file-placeholder'>{placeholder}</span>}
{maxLength && <span className='countdown'>{characterCount} / {maxLength}</span>} {maxLength && <span className='countdown'>{characterCount} / {maxLength}</span>}
{addOn && addOnAction && <button aria-label={addOnAriaLabel} type="button" onClick={addOnAction} className={`addon ${addOnClassName || ''} is-btn`}>{addOn}</button>} {addOn && addOnAction && <button aria-label={addOnAriaLabel} type="button" onClick={addOnAction} className={`addon ${addOnClassName || ''} is-btn`}>{addOn}</button>}

View File

@ -6,9 +6,11 @@ class Child < ApplicationRecord
validates :first_name, presence: true validates :first_name, presence: true
validates :last_name, presence: true validates :last_name, presence: true
validates :email, presence: true, format: { with: Devise.email_regexp }
validate :validate_age validate :validate_age
# birthday should less than 18 years ago
def validate_age def validate_age
errors.add(:birthday, 'You should be over 18 years old.') if birthday.blank? && birthday < 18.years.ago errors.add(:birthday, I18n.t('.errors.messages.birthday_less_than_18_years_ago')) if birthday.blank? || birthday > 18.years.ago
end end
end end

View File

@ -478,6 +478,22 @@ en:
start_typing: "Start typing..." start_typing: "Start typing..."
children_list: children_list:
heading: "My children" heading: "My children"
add_child: "Add a child"
child_modal:
edit_child: "Edit child"
new_child: "New child"
child_form:
child_form_info: "Note that you can only add your children under 18 years old. Supporting documents are requested by your administrator, they will be useful to validate your child's account and authorize the reservation of events."
first_name: "First name"
last_name: "Last name"
birthday: "Birthday"
email: "Email"
phone: "Phone"
save: "Save"
child_item:
first_name: "First name of the child"
last_name: "Last name of the child"
birthday: "Birthday"
tour: tour:
conclusion: conclusion:
title: "Thank you for your attention" title: "Thank you for your attention"

View File

@ -482,11 +482,14 @@ fr:
child_modal: child_modal:
edit_child: "Modifier un enfant" edit_child: "Modifier un enfant"
new_child: "Ajouter un enfant" new_child: "Ajouter un enfant"
save: "Enregistrer"
child_form: child_form:
child_form_info: "Notez que vous ne pouvez ajouter que vos enfants de moins de 18 ans. Des pièces justificatives sont demandés par votre administrateur, elles lui seront utiles pour valider le compte de votre enfant et ainsi autoriser la réservation d'événements." child_form_info: "Notez que vous ne pouvez ajouter que vos enfants de moins de 18 ans. Des pièces justificatives sont demandés par votre administrateur, elles lui seront utiles pour valider le compte de votre enfant et ainsi autoriser la réservation d'événements."
first_name: "Prénom" first_name: "Prénom"
last_name: "Nom" last_name: "Nom"
birthday: "Date de naissance"
email: "Courriel"
phone: "Téléphone"
save: "Enregistrer"
child_item: child_item:
first_name: "Prénom de l'enfant" first_name: "Prénom de l'enfant"
last_name: "Nom de l'enfant" last_name: "Nom de l'enfant"

View File

@ -49,6 +49,7 @@ en:
gateway_amount_too_large: "Payments above %{AMOUNT} are not supported. Please order directly at the reception." gateway_amount_too_large: "Payments above %{AMOUNT} are not supported. Please order directly at the reception."
product_in_use: "This product have already been ordered" product_in_use: "This product have already been ordered"
slug_already_used: "is already used" slug_already_used: "is already used"
birthday_less_than_18_years_ago: "Birthday must be under 18 years ago"
coupon: coupon:
code_format_error: "only caps letters, numbers, and dashes are allowed" code_format_error: "only caps letters, numbers, and dashes are allowed"
apipie: apipie:

View File

@ -49,6 +49,7 @@ fr:
gateway_amount_too_large: "Les paiements supérieurs à %{AMOUNT} ne sont pas pris en charge. Merci de passer commande directement à l'accueil." gateway_amount_too_large: "Les paiements supérieurs à %{AMOUNT} ne sont pas pris en charge. Merci de passer commande directement à l'accueil."
product_in_use: "Ce produit a déjà été commandé" product_in_use: "Ce produit a déjà été commandé"
slug_already_used: "est déjà utilisée" slug_already_used: "est déjà utilisée"
birthday_less_than_18_years_ago: "l'age devez avoir au moins 18 ans."
coupon: coupon:
code_format_error: "seules les majuscules, chiffres et tirets sont autorisés" code_format_error: "seules les majuscules, chiffres et tirets sont autorisés"
apipie: apipie: