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:
parent
10ef342edf
commit
dc4151cfb0
@ -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]
|
||||||
|
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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" />
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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']));
|
||||||
|
@ -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>}
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
@ -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"
|
||||||
|
@ -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:
|
||||||
|
@ -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:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user