1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-18 07:52:23 +01:00

(feat) child validation

This commit is contained in:
Du Peng 2023-05-25 20:17:37 +02:00
parent 3d542ce6d4
commit ec1a736601
31 changed files with 237 additions and 35 deletions

View File

@ -4,7 +4,7 @@
# Children are used to provide a way to manage multiple users in the family account
class API::ChildrenController < API::APIController
before_action :authenticate_user!
before_action :set_child, only: %i[show update destroy]
before_action :set_child, only: %i[show update destroy validate]
def index
authorize Child
@ -43,6 +43,17 @@ class API::ChildrenController < API::APIController
head :no_content
end
def validate
authorize @child
cparams = params.require(:child).permit(:validated_at)
if @child.update(validated_at: cparams[:validated_at].present? ? Time.current : nil)
render :show, status: :ok, location: child_path(@child)
else
render json: @child.errors, status: :unprocessable_entity
end
end
private
def set_child

View File

@ -38,4 +38,9 @@ export default class ChildAPI {
const res: AxiosResponse<void> = await apiClient.delete(`/api/children/${childId}`);
return res?.data;
}
static async validate (child: Child): Promise<Child> {
const res: AxiosResponse<Child> = await apiClient.patch(`/api/children/${child.id}/validate`, { child });
return res?.data;
}
}

View File

@ -8,9 +8,11 @@ import { FabButton } from '../base/fab-button';
import { FormFileUpload } from '../form/form-file-upload';
import { FileType } from '../../models/file';
import { SupportingDocumentType } from '../../models/supporting-document-type';
import { User } from '../../models/user';
interface ChildFormProps {
child: Child;
operator: User;
onSubmit: (data: Child) => void;
supportingDocumentsTypes: Array<SupportingDocumentType>;
}
@ -18,7 +20,7 @@ interface ChildFormProps {
/**
* A form for creating or editing a child.
*/
export const ChildForm: React.FC<ChildFormProps> = ({ child, onSubmit, supportingDocumentsTypes }) => {
export const ChildForm: React.FC<ChildFormProps> = ({ child, onSubmit, supportingDocumentsTypes, operator }) => {
const { t } = useTranslation('public');
const { register, formState, handleSubmit, setValue, control } = useForm<Child>({
@ -31,11 +33,20 @@ export const ChildForm: React.FC<ChildFormProps> = ({ child, onSubmit, supportin
return supportingDocumentType ? supportingDocumentType.name : '';
};
/**
* Check if the current operator has administrative rights or is a normal member
*/
const isPrivileged = (): boolean => {
return (operator?.role === 'admin' || operator?.role === 'manager');
};
return (
<div className="child-form">
<div className="info-area">
{t('app.public.child_form.child_form_info')}
</div>
{isPrivileged() &&
<div className="info-area">
{t('app.public.child_form.child_form_info')}
</div>
}
<form onSubmit={handleSubmit(onSubmit)}>
<FormInput id="first_name"
register={register}
@ -69,6 +80,22 @@ export const ChildForm: React.FC<ChildFormProps> = ({ child, onSubmit, supportin
label={t('app.public.child_form.email')}
/>
{output.supporting_document_files_attributes.map((sf, index) => {
if (isPrivileged()) {
return (
<div key={index} className="document-type">
<div className="type-name">{getSupportingDocumentsTypeName(sf.supporting_document_type_id)}</div>
{sf.attachment_url && (
<a href={sf.attachment_url} target="_blank" rel="noreferrer">
<span className="filename">{sf.attachment}</span>
<i className="fa fa-download"></i>
</a>
)}
{!sf.attachment_url && (
<div className="missing-file">{t('app.public.child_form.to_complete')}</div>
)}
</div>
);
}
return (
<FormFileUpload key={index}
defaultFile={sf as FileType}

View File

@ -5,12 +5,15 @@ import { Child } from '../../models/child';
import ChildAPI from '../../api/child';
import { ChildForm } from './child-form';
import { SupportingDocumentType } from '../../models/supporting-document-type';
import { ChildValidation } from './child-validation';
import { User } from '../../models/user';
interface ChildModalProps {
child?: Child;
operator: User;
isOpen: boolean;
toggleModal: () => void;
onSuccess: (child: Child) => void;
onSuccess: (child: Child, msg: string) => void;
onError: (error: string) => void;
supportingDocumentsTypes: Array<SupportingDocumentType>;
}
@ -18,7 +21,7 @@ interface ChildModalProps {
/**
* A modal for creating or editing a child.
*/
export const ChildModal: React.FC<ChildModalProps> = ({ child, isOpen, toggleModal, onSuccess, onError, supportingDocumentsTypes }) => {
export const ChildModal: React.FC<ChildModalProps> = ({ child, isOpen, toggleModal, onSuccess, onError, supportingDocumentsTypes, operator }) => {
const { t } = useTranslation('public');
/**
@ -32,7 +35,7 @@ export const ChildModal: React.FC<ChildModalProps> = ({ child, isOpen, toggleMod
await ChildAPI.create(data);
}
toggleModal();
onSuccess(data);
onSuccess(data, '');
} catch (error) {
onError(error);
}
@ -45,7 +48,10 @@ export const ChildModal: React.FC<ChildModalProps> = ({ child, isOpen, toggleMod
toggleModal={toggleModal}
closeButton={true}
confirmButton={false} >
<ChildForm child={child} onSubmit={handleSaveChild} supportingDocumentsTypes={supportingDocumentsTypes}/>
{(operator?.role === 'admin' || operator?.role === 'manager') &&
<ChildValidation child={child} onSuccess={onSuccess} onError={onError} />
}
<ChildForm child={child} onSubmit={handleSaveChild} supportingDocumentsTypes={supportingDocumentsTypes} operator={operator}/>
</FabModal>
);
};

View File

@ -0,0 +1,54 @@
import { useState, useEffect } from 'react';
import * as React from 'react';
import Switch from 'react-switch';
import _ from 'lodash';
import { useTranslation } from 'react-i18next';
import { Child } from '../../models/child';
import ChildAPI from '../../api/child';
import { TDateISO } from '../../typings/date-iso';
interface ChildValidationProps {
child: Child
onSuccess: (child: Child, message: string) => void,
onError: (message: string) => void,
}
/**
* This component allows to configure boolean value for a setting.
*/
export const ChildValidation: React.FC<ChildValidationProps> = ({ child, onSuccess, onError }) => {
const { t } = useTranslation('admin');
const [value, setValue] = useState<boolean>(!!(child?.validated_at));
useEffect(() => {
setValue(!!(child?.validated_at));
}, [child]);
/**
* Callback triggered when the 'switch' is changed.
*/
const handleChanged = (_value: boolean) => {
setValue(_value);
const _child = _.clone(child);
if (_value) {
_child.validated_at = new Date().toISOString() as TDateISO;
} else {
_child.validated_at = null;
}
ChildAPI.validate(_child)
.then((child: Child) => {
onSuccess(child, t(`app.admin.child_validation.${_value ? 'validate' : 'invalidate'}_child_success`));
}).catch(err => {
setValue(!_value);
onError(t(`app.admin.child_validation.${_value ? 'validate' : 'invalidate'}_child_error`) + err);
});
};
return (
<div className="child-validation">
<label htmlFor="child-validation-switch">{t('app.admin.child_validation.validate_child')}</label>
<Switch checked={value} id="child-validation-switch" onChange={handleChanged} className="switch"></Switch>
</div>
);
};

View File

@ -15,7 +15,8 @@ import SupportingDocumentTypeAPI from '../../api/supporting-document-type';
declare const Application: IApplication;
interface ChildrenListProps {
currentUser: User;
user: User;
operator: User;
onSuccess: (error: string) => void;
onError: (error: string) => void;
}
@ -23,7 +24,7 @@ interface ChildrenListProps {
/**
* A list of children belonging to the current user.
*/
export const ChildrenList: React.FC<ChildrenListProps> = ({ currentUser, onError }) => {
export const ChildrenList: React.FC<ChildrenListProps> = ({ user, operator, onError, onSuccess }) => {
const { t } = useTranslation('public');
const [children, setChildren] = useState<Array<Child>>([]);
@ -32,11 +33,11 @@ export const ChildrenList: React.FC<ChildrenListProps> = ({ currentUser, onError
const [supportingDocumentsTypes, setSupportingDocumentsTypes] = useState<Array<SupportingDocumentType>>([]);
useEffect(() => {
ChildAPI.index({ user_id: currentUser.id }).then(setChildren);
ChildAPI.index({ user_id: user.id }).then(setChildren);
SupportingDocumentTypeAPI.index({ document_type: 'Child' }).then(tData => {
setSupportingDocumentsTypes(tData);
});
}, [currentUser]);
}, [user]);
/**
* Open the add child modal
@ -44,7 +45,7 @@ export const ChildrenList: React.FC<ChildrenListProps> = ({ currentUser, onError
const addChild = () => {
setIsOpenChildModal(true);
setChild({
user_id: currentUser.id,
user_id: user.id,
supporting_document_files_attributes: supportingDocumentsTypes.map(t => {
return { supporting_document_type_id: t.id };
})
@ -70,24 +71,36 @@ export const ChildrenList: React.FC<ChildrenListProps> = ({ currentUser, onError
*/
const deleteChild = (child: Child) => {
ChildAPI.destroy(child.id).then(() => {
ChildAPI.index({ user_id: currentUser.id }).then(setChildren);
ChildAPI.index({ user_id: user.id }).then(setChildren);
});
};
/**
* Handle save child success from the API
*/
const handleSaveChildSuccess = () => {
ChildAPI.index({ user_id: currentUser.id }).then(setChildren);
const handleSaveChildSuccess = (_child: Child, msg: string) => {
ChildAPI.index({ user_id: user.id }).then(setChildren);
if (msg) {
onSuccess(msg);
}
};
/**
* Check if the current operator has administrative rights or is a normal member
*/
const isPrivileged = (): boolean => {
return (operator?.role === 'admin' || operator?.role === 'manager');
};
return (
<section>
<header>
<h2>{t('app.public.children_list.heading')}</h2>
<FabButton onClick={addChild}>
{t('app.public.children_list.add_child')}
</FabButton>
{!isPrivileged() && (
<FabButton onClick={addChild}>
{t('app.public.children_list.add_child')}
</FabButton>
)}
</header>
<div className="children-list">
@ -95,7 +108,7 @@ export const ChildrenList: React.FC<ChildrenListProps> = ({ currentUser, onError
<ChildItem key={child.id} child={child} onEdit={editChild} onDelete={deleteChild} />
))}
</div>
<ChildModal child={child} isOpen={isOpenChildModal} toggleModal={() => setIsOpenChildModal(false)} onSuccess={handleSaveChildSuccess} onError={onError} supportingDocumentsTypes={supportingDocumentsTypes} />
<ChildModal child={child} isOpen={isOpenChildModal} toggleModal={() => setIsOpenChildModal(false)} onSuccess={handleSaveChildSuccess} onError={onError} supportingDocumentsTypes={supportingDocumentsTypes} operator={operator} />
</section>
);
};
@ -108,4 +121,4 @@ const ChildrenListWrapper: React.FC<ChildrenListProps> = (props) => {
);
};
Application.Components.component('childrenList', react2angular(ChildrenListWrapper, ['currentUser', 'onSuccess', 'onError']));
Application.Components.component('childrenList', react2angular(ChildrenListWrapper, ['user', 'operator', 'onSuccess', 'onError']));

View File

@ -201,6 +201,9 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
// Global config: is the user validation required ?
$scope.enableUserValidationRequired = settingsPromise.user_validation_required === 'true';
// Global config: is the child validation required ?
$scope.enableChildValidationRequired = settingsPromise.child_validation_required === 'true';
// online payments (by card)
$scope.onlinePayment = {
showModal: false,
@ -635,6 +638,9 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
if (!user.booked) {
return false;
}
if ($scope.enableChildValidationRequired && user.booked.type === 'Child' && !user.booked.validatedAt) {
return false;
}
}
}
}
@ -731,7 +737,8 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
key,
name: child.first_name + ' ' + child.last_name,
id: child.id,
type: 'Child'
type: 'Child',
validatedAt: child.validated_at
});
}
}

View File

@ -13,6 +13,7 @@ export interface Child {
phone?: string,
birthday: TDateISODate,
user_id: number,
validated_at?: TDateISODate,
supporting_document_files_attributes?: Array<{
id?: number,
supportable_id?: number,

View File

@ -631,7 +631,7 @@ angular.module('application.router', ['ui.router'])
resolve: {
eventPromise: ['Event', '$transition$', function (Event, $transition$) { return Event.get({ id: $transition$.params().id }).$promise; }],
priceCategoriesPromise: ['PriceCategory', function (PriceCategory) { return PriceCategory.query().$promise; }],
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['booking_move_enable', 'booking_move_delay', 'booking_cancel_enable', 'booking_cancel_delay', 'event_explications_alert', 'online_payment_module', 'user_validation_required', 'user_validation_required_list']" }).$promise; }]
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['booking_move_enable', 'booking_move_delay', 'booking_cancel_enable', 'booking_cancel_delay', 'event_explications_alert', 'online_payment_module', 'user_validation_required', 'user_validation_required_list', 'child_validation_required']" }).$promise; }]
}
})

View File

@ -1,4 +1,4 @@
.user-validation {
.user-validation, .child-validation {
label {
margin-bottom: 0;
vertical-align: middle;

View File

@ -62,6 +62,10 @@
</uib-tab>
<uib-tab heading="{{ 'app.shared.user_admin.children' | translate }}" ng-if="$root.settings.familyAccount">
<children-list user="user" operator="currentUser" on-success="onSuccess" on-error="onError" />
</uib-tab>
<uib-tab heading="{{ 'app.admin.members_edit.supporting_documents' | translate }}" ng-show="hasProofOfIdentityTypes">
<supporting-documents-validation
operator="currentUser"

View File

@ -54,13 +54,20 @@
<div class="row">
<h3 class="m-l" translate>{{ 'app.admin.settings.family_account' }}</h3>
<p class="alert alert-warning m-h-md" ng-bind-html="'app.admin.settings.family_account_info_html' | translate"></p>
<div class="col-md-6">
<div class="col-md-10 col-md-offset-1">
<boolean-setting name="'family_account'"
settings="allSettings"
label="'app.admin.settings.enable_family_account' | translate"
on-success="onSuccess"
on-error="onError">
</div>
<div class="col-md-10 col-md-offset-1">
<boolean-setting name="'child_validation_required'"
settings="allSettings"
label="'app.admin.settings.child_validation_required_label' | translate"
on-success="onSuccess"
on-error="onError">
</div>
</div>
<div class="row">
<div class="col-md-12">

View File

@ -7,5 +7,5 @@
</section>
<children-list current-user="currentUser" on-success="onSuccess" on-error="onError" />
<children-list user="currentUser" operator="currentUser" on-success="onSuccess" on-error="onError" />
</div>

View File

@ -136,6 +136,12 @@
class="form-control">
<option value=""></option>
</select>
<uib-alert type="danger" ng-if="enableChildValidationRequired && user.booked && user.booked.type === 'Child' && !user.booked.validatedAt">
<p class="text-sm">
<i class="fa fa-warning"></i>
<span translate>{{ 'app.shared.cart.child_validation_required_alert' }}</span>
</p>
</uib-alert>
</div>
</div>
</div>
@ -162,6 +168,12 @@
class="form-control">
<option value=""></option>
</select>
<uib-alert type="danger" ng-if="enableChildValidationRequired && user.booked && user.booked.type === 'Child' && !user.booked.validatedAt">
<p class="text-sm">
<i class="fa fa-warning"></i>
<span translate>{{ 'app.shared.cart.child_validation_required_alert' }}</span>
</p>
</uib-alert>
</div>
</div>
</div>

View File

@ -168,6 +168,7 @@ module SettingsHelper
user_validation_required_list
show_username_in_admin_list
family_account
child_validation_required
store_module
store_withdrawal_instructions
store_hidden

View File

@ -11,14 +11,18 @@ class ChildPolicy < ApplicationPolicy
end
def show?
user.id == record.user_id
user.privileged? || user.id == record.user_id
end
def update?
user.id == record.user_id
user.privileged? || user.id == record.user_id
end
def destroy?
user.id == record.user_id
user.privileged? || user.id == record.user_id
end
def validate?
user.privileged?
end
end

View File

@ -47,7 +47,7 @@ class SettingPolicy < ApplicationPolicy
machines_banner_cta_url trainings_banner_active trainings_banner_text trainings_banner_cta_active trainings_banner_cta_label
trainings_banner_cta_url events_banner_active events_banner_text events_banner_cta_active events_banner_cta_label
events_banner_cta_url projects_list_member_filter_presence projects_list_date_filters_presence
project_categories_filter_placeholder project_categories_wording family_account]
project_categories_filter_placeholder project_categories_wording family_account child_validation_required]
end
##

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
json.extract! child, :id, :first_name, :last_name, :email, :birthday, :phone, :user_id
json.extract! child, :id, :first_name, :last_name, :email, :birthday, :phone, :user_id, :validated_at
json.supporting_document_files_attributes child.supporting_document_files do |f|
json.id f.id
json.supportable_id f.supportable_id
@ -8,5 +8,5 @@ json.supporting_document_files_attributes child.supporting_document_files do |f|
json.supporting_document_type_id f.supporting_document_type_id
json.attachment f.attachment.file.filename
json.attachment_name f.attachment_identifier
json.attachment_url f.attachment_url
json.attachment_url "/api/supporting_document_files/#{f.id}/download"
end

View File

@ -0,0 +1,3 @@
# forzen_string_literal: true
json.partial! 'child', child: @child

View File

@ -1242,6 +1242,12 @@ en:
validate_member_error: "An unexpected error occurred: unable to validate this member."
invalidate_member_error: "An unexpected error occurred: unable to invalidate this member."
validate_account: "Validate the account"
child_validation:
validate_child_success: "Child successfully validated"
invalidate_child_success: "Child successfully invalidated"
validate_child_error: "An unexpected error occurred: unable to validate this child."
invalidate_child_error: "An unexpected error occurred: unable to invalidate this child."
validate_child: "Validate the child"
supporting_documents_refusal_form:
refusal_comment: "Comment"
comment_placeholder: "Please type a comment here"
@ -1791,6 +1797,7 @@ en:
family_account: "family account"
family_account_info_html: "By activating this option, you offer your members the possibility to add their child(ren) to their own account. You can also request proof if you wish to validate them."
enable_family_account: "Enable the Family Account option"
child_validation_required_label: "Activate the account validation option for children"
overlapping_options:
training_reservations: "Trainings"
machine_reservations: "Machines"

View File

@ -1242,6 +1242,12 @@ fr:
validate_member_error: "Une erreur inattendue est survenue. Impossible de valider ce compte membre."
invalidate_member_error: "Une erreur inattendue est survenue. Impossible d'invalider ce compte membre."
validate_account: "Valider le compte"
child_validation:
validate_child_success: "Le compte enfant a bien été validé"
invalidate_child_success: "Le compte enfant a bien été invalidé"
validate_child_error: "Une erreur inattendue est survenue. Impossible de valider ce compte enfant."
invalidate_child_error: "Une erreur inattendue est survenue. Impossible d'invalider ce compte enfant."
validate_child: "Valider le compte enfant"
supporting_documents_refusal_form:
refusal_comment: "Commentaire"
comment_placeholder: "Veuillez saisir un commentaire ici"
@ -1791,6 +1797,7 @@ fr:
family_account: "Compte famille"
family_account_info_html: "En activant cette option, vous offrez à vos membres la possibilité d'ajouter sur leur propre compte leur(s) enfants. Vous pouvez aussi demander un justificatif si vous souhaitez les valider."
enable_family_account: "Activer l'option Compte Famille"
child_validation_required_label: "Activer l'option de validation des comptes enfants"
overlapping_options:
training_reservations: "Formations"
machine_reservations: "Machines"

View File

@ -497,6 +497,7 @@ en:
email: "Email"
phone: "Phone"
save: "Save"
to_complete: "To complete"
child_item:
first_name: "First name of the child"
last_name: "Last name of the child"

View File

@ -497,6 +497,7 @@ fr:
email: "Courriel"
phone: "Téléphone"
save: "Enregistrer"
to_complete: "À compléter"
child_item:
first_name: "Prénom de l'enfant"
last_name: "Nom de l'enfant"

View File

@ -199,6 +199,7 @@ en:
group_is_required: "Group is required."
trainings: "Trainings"
tags: "Tags"
children: "Children"
#machine/training slot modification modal
confirm_modify_slot_modal:
change_the_slot: "Change the slot"
@ -372,6 +373,7 @@ en:
user_tags: "User tags"
no_tags: "No tags"
user_validation_required_alert: "Warning!<br>Your administrator must validate your account. Then, you'll then be able to access all the booking features."
child_validation_required_alert: "Warning!<br>Your administrator must validate your child account. Then, you'll then be able to book the event."
# feature-tour modal
tour:
previous: "Previous"

View File

@ -199,6 +199,7 @@ fr:
group_is_required: "Le groupe est requis."
trainings: "Formations"
tags: "Étiquettes"
children: "Enfants"
#machine/training slot modification modal
confirm_modify_slot_modal:
change_the_slot: "Modifier le créneau"
@ -372,6 +373,7 @@ fr:
user_tags: "Étiquettes de l'utilisateur"
no_tags: "Aucune étiquette"
user_validation_required_alert: "Attention !<br>Votre administrateur doit valider votre compte. Vous pourrez alors accéder à l'ensemble des fonctionnalités de réservation."
child_validation_required_alert: "Attention !<br>Votre administrateur doit valider votre compte enfant. Vous pourrez alors réserver l'événement."
#feature-tour modal
tour:
previous: "Précédent"

View File

@ -185,7 +185,9 @@ Rails.application.routes.draw do
get 'withdrawal_instructions', on: :member
end
resources :children, only: %i[index show create update destroy]
resources :children do
patch ':id/validate', action: 'validate', on: :collection
end
# for admin
resources :trainings do

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
# add validated_at to child
class AddValidatedAtToChild < ActiveRecord::Migration[7.0]
def change
add_column :children, :validated_at, :datetime
end
end

View File

@ -734,3 +734,4 @@ Setting.set('projects_list_date_filters_presence', false) unless Setting.find_by
Setting.set('project_categories_filter_placeholder', 'Toutes les catégories') unless Setting.find_by(name: 'project_categories_filter_placeholder').try(:value)
Setting.set('project_categories_wording', 'Catégories') unless Setting.find_by(name: 'project_categories_wording').try(:value)
Setting.set('family_account', false) unless Setting.find_by(name: 'family_account').try(:value)
Setting.set('child_validation_required', false) unless Setting.find_by(name: 'child_validation_required').try(:value)

View File

@ -968,7 +968,8 @@ CREATE TABLE public.children (
phone character varying,
email character varying,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL
updated_at timestamp without time zone NOT NULL,
validated_at timestamp(6) without time zone
);
@ -9061,5 +9062,6 @@ INSERT INTO "schema_migrations" (version) VALUES
('20230524110215');
('20230626122844'),
('20230626122947');
('20230525101006');

View File

@ -890,3 +890,11 @@ history_value_105:
created_at: '2023-03-31 14:38:40.000421'
updated_at: '2023-03-31 14:38:40.000421'
invoicing_profile_id: 1
history_value_102:
id: 102
setting_id: 101
value: 'false'
created_at: '2023-03-31 14:38:40.000421'
updated_at: '2023-03-31 14:38:40.000421'
invoicing_profile_id: 1

View File

@ -615,3 +615,9 @@ setting_104:
name: family_account
created_at: 2023-03-31 14:38:40.000421500 Z
updated_at: 2023-03-31 14:38:40.000421500 Z
setting_101:
id: 101
name: child_validation_required
created_at: 2023-03-31 14:38:40.000421500 Z
updated_at: 2023-03-31 14:38:40.000421500 Z