1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-30 19:52:20 +01:00

(wip) create/update user children

This commit is contained in:
Du Peng 2023-04-04 18:09:17 +02:00
parent 5365cbdaba
commit 39178b5d96
11 changed files with 94 additions and 30 deletions

View File

@ -2,7 +2,7 @@
# API Controller for resources of type Child
# 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 :set_child, only: %i[show update destroy]

View File

@ -1,22 +1,25 @@
import React from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import moment from 'moment';
import { Child } from '../../models/child';
import { TDateISODate } from '../../typings/date-iso';
import { FormInput } from '../form/form-input';
import { FabButton } from '../base/fab-button';
interface ChildFormProps {
child: Child;
onChange: (field: string, value: string | TDateISODate) => void;
onSubmit: (data: Child) => void;
}
/**
* 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 { register, formState } = useForm<Child>({
const { register, formState, handleSubmit } = useForm<Child>({
defaultValues: child
});
@ -32,7 +35,7 @@ export const ChildForm: React.FC<ChildFormProps> = ({ child, onChange }) => {
<div className="info-area">
{t('app.public.child_form.child_form_info')}
</div>
<form>
<form onSubmit={handleSubmit(onSubmit)}>
<FormInput id="first_name"
register={register}
rules={{ required: true }}
@ -47,6 +50,35 @@ export const ChildForm: React.FC<ChildFormProps> = ({ child, onChange }) => {
label={t('app.public.child_form.last_name')}
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>
</div>
);

View File

@ -2,6 +2,7 @@ import React from 'react';
import { Child } from '../../models/child';
import { useTranslation } from 'react-i18next';
import { FabButton } from '../base/fab-button';
import FormatLib from '../../lib/format';
interface ChildItemProps {
child: Child;
@ -27,7 +28,7 @@ export const ChildItem: React.FC<ChildItemProps> = ({ child, onEdit, onDelete })
</div>
<div>
<div>{t('app.public.child_item.birthday')}</div>
<div>{child.birthday}</div>
<div>{FormatLib.date(child.birthday)}</div>
</div>
<div className="actions">
<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 { useState } from 'react';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { FabModal, ModalSize } from '../base/fab-modal';
import { Child } from '../../models/child';
@ -11,15 +11,20 @@ interface ChildModalProps {
child?: Child;
isOpen: boolean;
toggleModal: () => void;
onSuccess: (child: Child) => void;
onError: (error: string) => void;
}
/**
* 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 [data, setData] = useState<Child>(child);
console.log(child, data);
useEffect(() => {
setData(child);
}, [child]);
/**
* Save the child to the API
@ -32,18 +37,12 @@ export const ChildModal: React.FC<ChildModalProps> = ({ child, isOpen, toggleMod
await ChildAPI.create(data);
}
toggleModal();
onSuccess(data);
} 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
*/
@ -60,10 +59,9 @@ export const ChildModal: React.FC<ChildModalProps> = ({ child, isOpen, toggleMod
isOpen={isOpen}
toggleModal={toggleModal}
closeButton={true}
confirmButton={t('app.public.child_modal.save')}
onConfirm={handleSaveChild}
preventConfirm={isPreventedSaveChild()}>
<ChildForm child={child} onChange={handleChildChanged} />
confirmButton={false}
onConfirm={handleSaveChild} >
<ChildForm child={child} onChange={handleChildChanged} onSubmit={handleSaveChild} />
</FabModal>
);
};

View File

@ -1,7 +1,6 @@
import React, { useState, useEffect } from 'react';
import { react2angular } from 'react2angular';
import { Child } from '../../models/child';
// import { ChildListItem } from './child-list-item';
import ChildAPI from '../../api/child';
import { User } from '../../models/user';
import { useTranslation } from 'react-i18next';
@ -15,12 +14,14 @@ declare const Application: IApplication;
interface ChildrenListProps {
currentUser: User;
onSuccess: (error: string) => void;
onError: (error: string) => void;
}
/**
* 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 [children, setChildren] = useState<Array<Child>>([]);
@ -52,10 +53,17 @@ export const ChildrenList: React.FC<ChildrenListProps> = ({ currentUser }) => {
*/
const deleteChild = (child: Child) => {
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 (
<section>
<header>
@ -70,7 +78,7 @@ export const ChildrenList: React.FC<ChildrenListProps> = ({ currentUser }) => {
<ChildItem key={child.id} child={child} onEdit={editChild} onDelete={deleteChild} />
))}
</div>
<ChildModal child={child} isOpen={isOpenChildModal} toggleModal={() => setIsOpenChildModal(false)} />
<ChildModal child={child} isOpen={isOpenChildModal} toggleModal={() => setIsOpenChildModal(false)} onSuccess={handleSaveChildSuccess} onError={onError} />
</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,
nullable?: boolean,
ariaLabel?: string,
maxLength?: number
maxLength?: number,
max?: number | string,
}
/**
* 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);
/**
@ -100,7 +101,8 @@ export const FormInput = <TFieldValues extends FieldValues, TInputType>({ id, re
disabled={typeof disabled === 'function' ? disabled(id) : disabled}
placeholder={placeholder}
accept={accept}
maxLength={maxLength} />
maxLength={maxLength}
max={max} />
{(type === 'file' && placeholder) && <span className='fab-button is-black file-placeholder'>{placeholder}</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>}

View File

@ -6,9 +6,11 @@ class Child < ApplicationRecord
validates :first_name, presence: true
validates :last_name, presence: true
validates :email, presence: true, format: { with: Devise.email_regexp }
validate :validate_age
# birthday should less than 18 years ago
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

View File

@ -484,6 +484,22 @@ en:
start_typing: "Start typing..."
children_list:
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:
conclusion:
title: "Thank you for your attention"

View File

@ -488,11 +488,14 @@ fr:
child_modal:
edit_child: "Modifier un enfant"
new_child: "Ajouter un enfant"
save: "Enregistrer"
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."
first_name: "Prénom"
last_name: "Nom"
birthday: "Date de naissance"
email: "Courriel"
phone: "Téléphone"
save: "Enregistrer"
child_item:
first_name: "Pré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."
product_in_use: "This product have already been ordered"
slug_already_used: "is already used"
birthday_less_than_18_years_ago: "Birthday must be under 18 years ago"
coupon:
code_format_error: "only caps letters, numbers, and dashes are allowed"
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."
product_in_use: "Ce produit a déjà été commandé"
slug_already_used: "est déjà utilisée"
birthday_less_than_18_years_ago: "l'age devez avoir au moins 18 ans."
coupon:
code_format_error: "seules les majuscules, chiffres et tirets sont autorisés"
apipie: