From 7d10132953b3556c6913939209f8260f0edcca33 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Fri, 31 Mar 2023 14:44:37 +0200 Subject: [PATCH 01/75] (feat) add Family account setting --- Gemfile.lock | 1 + app/frontend/src/javascript/models/setting.ts | 3 ++- app/frontend/templates/admin/settings/compte.html | 11 +++++++++++ app/helpers/settings_helper.rb | 1 + app/policies/setting_policy.rb | 2 +- config/locales/app.admin.en.yml | 3 +++ config/locales/app.admin.fr.yml | 3 +++ config/locales/en.yml | 1 + db/seeds/settings.rb | 1 + test/fixtures/history_values.yml | 10 ++++++++++ test/fixtures/settings.yml | 5 +++++ test/frontend/__fixtures__/settings.ts | 6 ++++++ 12 files changed, 45 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index cd14fbf34..6c6c012ae 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -528,6 +528,7 @@ GEM zeitwerk (2.6.7) PLATFORMS + x86_64-darwin-20 x86_64-darwin-21 x86_64-linux diff --git a/app/frontend/src/javascript/models/setting.ts b/app/frontend/src/javascript/models/setting.ts index ef4f535f1..e075ae1e3 100644 --- a/app/frontend/src/javascript/models/setting.ts +++ b/app/frontend/src/javascript/models/setting.ts @@ -178,7 +178,8 @@ export const accountSettings = [ 'external_id', 'user_change_group', 'user_validation_required', - 'user_validation_required_list' + 'user_validation_required_list', + 'family_account' ] as const; export const analyticsSettings = [ diff --git a/app/frontend/templates/admin/settings/compte.html b/app/frontend/templates/admin/settings/compte.html index 0ed115706..3935b9c98 100644 --- a/app/frontend/templates/admin/settings/compte.html +++ b/app/frontend/templates/admin/settings/compte.html @@ -51,6 +51,17 @@ +
+

{{ 'app.admin.settings.family_account' }}

+

+
+ +
+

{{ 'app.admin.settings.captcha' }}

diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 0f268a7e9..8e60fe4e1 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -167,6 +167,7 @@ module SettingsHelper user_validation_required user_validation_required_list show_username_in_admin_list + family_account store_module store_withdrawal_instructions store_hidden diff --git a/app/policies/setting_policy.rb b/app/policies/setting_policy.rb index 1a74868be..7c8001bef 100644 --- a/app/policies/setting_policy.rb +++ b/app/policies/setting_policy.rb @@ -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] + project_categories_filter_placeholder project_categories_wording family_account] end ## diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index c91c20d25..dae87a978 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -1783,6 +1783,9 @@ en: projects_list_date_filters_presence: "Presence of date filters on projects list" project_categories_filter_placeholder: "Placeholder for categories filter in project gallery" project_categories_wording: "Wording used to replace \"Categories\" on public pages" + 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" overlapping_options: training_reservations: "Trainings" machine_reservations: "Machines" diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml index 4bc83e3aa..d3c4941a1 100644 --- a/config/locales/app.admin.fr.yml +++ b/config/locales/app.admin.fr.yml @@ -1783,6 +1783,9 @@ fr: projects_list_date_filters_presence: "Presence of date filters on projects list" project_categories_filter_placeholder: "Placeholder for categories filter in project gallery" project_categories_wording: "Wording used to replace \"Categories\" on public pages" + 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" overlapping_options: training_reservations: "Formations" machine_reservations: "Machines" diff --git a/config/locales/en.yml b/config/locales/en.yml index 248a18e50..cec01d3e2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -701,6 +701,7 @@ en: projects_list_date_filters_presence: "Presence of dates filter on projects list" project_categories_filter_placeholder: "Placeholder for categories filter in project gallery" project_categories_wording: "Wording used to replace \"Categories\" on public pages" + family_account: "Family account" #statuses of projects statuses: new: "New" diff --git a/db/seeds/settings.rb b/db/seeds/settings.rb index 7e455834e..92d932a82 100644 --- a/db/seeds/settings.rb +++ b/db/seeds/settings.rb @@ -733,3 +733,4 @@ Setting.set('projects_list_member_filter_presence', false) unless Setting.find_b Setting.set('projects_list_date_filters_presence', false) unless Setting.find_by(name: 'projects_list_date_filters_presence') 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) diff --git a/test/fixtures/history_values.yml b/test/fixtures/history_values.yml index e8ac8f534..645e05f9a 100644 --- a/test/fixtures/history_values.yml +++ b/test/fixtures/history_values.yml @@ -853,6 +853,7 @@ history_value_100: invoicing_profile_id: 1 history_value_101: + id: 101 setting_id: 100 value: 'Toutes les catégories' created_at: 2023-04-05 09:16:08.000511500 Z @@ -860,6 +861,7 @@ history_value_101: invoicing_profile_id: 1 history_value_102: + id: 102 setting_id: 101 value: 'Catégories' created_at: 2023-04-05 09:16:08.000511500 Z @@ -880,4 +882,12 @@ history_value_104: value: 'false' created_at: 2023-04-05 09:16:08.000511500 Z updated_at: 2023-04-05 09:16:08.000511500 Z + +history_value_105: + id: 105 + setting_id: 104 + value: 'false' + created_at: '2023-03-31 14:38:40.000421' + updated_at: '2023-03-31 14:38:40.000421' + footprint: invoicing_profile_id: 1 diff --git a/test/fixtures/settings.yml b/test/fixtures/settings.yml index e0ce32209..8e8326214 100644 --- a/test/fixtures/settings.yml +++ b/test/fixtures/settings.yml @@ -610,3 +610,8 @@ setting_103: name: projects_list_date_filters_presence created_at: 2023-04-05 09:16:08.000511500 Z updated_at: 2023-04-05 09:16:08.000511500 Z + +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 diff --git a/test/frontend/__fixtures__/settings.ts b/test/frontend/__fixtures__/settings.ts index 262b47f67..824e16533 100644 --- a/test/frontend/__fixtures__/settings.ts +++ b/test/frontend/__fixtures__/settings.ts @@ -849,6 +849,12 @@ export const settings: Array = [ value: 'Catégories', last_update: '2022-12-23T14:39:12+0100', localized: 'Project categories overridden name' + }, + { + name: 'family_account', + value: 'false', + last_update: '2023-03-31T14:39:12+0100', + localized: 'Family account' } ]; From 5365cbdaba9531bba01304803a498c0ecd1882e1 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Mon, 3 Apr 2023 18:23:49 +0200 Subject: [PATCH 02/75] (wip) add/edit user children --- app/controllers/api/children_controller.rb | 52 +++++++++++ app/frontend/src/javascript/api/child.ts | 31 +++++++ .../components/family-account/child-form.tsx | 53 ++++++++++++ .../components/family-account/child-item.tsx | 38 ++++++++ .../components/family-account/child-modal.tsx | 69 +++++++++++++++ .../family-account/children-list.tsx | 86 +++++++++++++++++++ .../src/javascript/controllers/children.js | 23 +++++ app/frontend/src/javascript/models/child.ts | 16 ++++ app/frontend/src/javascript/router.js | 16 +++- .../templates/dashboard/children.html | 11 +++ app/frontend/templates/dashboard/nav.html | 1 + app/models/child.rb | 14 +++ app/models/user.rb | 3 + app/policies/child_policy.rb | 31 +++++++ app/views/api/children/_child.json.jbuilder | 3 + app/views/api/children/create.json.jbuilder | 3 + app/views/api/children/index.json.jbuilder | 5 ++ app/views/api/children/update.json.jbuilder | 3 + config/locales/app.public.en.yml | 3 + config/locales/app.public.fr.yml | 16 ++++ config/routes.rb | 2 + db/migrate/20230331132506_create_children.rb | 17 ++++ db/schema.rb | 17 +++- 23 files changed, 509 insertions(+), 4 deletions(-) create mode 100644 app/controllers/api/children_controller.rb create mode 100644 app/frontend/src/javascript/api/child.ts create mode 100644 app/frontend/src/javascript/components/family-account/child-form.tsx create mode 100644 app/frontend/src/javascript/components/family-account/child-item.tsx create mode 100644 app/frontend/src/javascript/components/family-account/child-modal.tsx create mode 100644 app/frontend/src/javascript/components/family-account/children-list.tsx create mode 100644 app/frontend/src/javascript/controllers/children.js create mode 100644 app/frontend/src/javascript/models/child.ts create mode 100644 app/frontend/templates/dashboard/children.html create mode 100644 app/models/child.rb create mode 100644 app/policies/child_policy.rb create mode 100644 app/views/api/children/_child.json.jbuilder create mode 100644 app/views/api/children/create.json.jbuilder create mode 100644 app/views/api/children/index.json.jbuilder create mode 100644 app/views/api/children/update.json.jbuilder create mode 100644 db/migrate/20230331132506_create_children.rb diff --git a/app/controllers/api/children_controller.rb b/app/controllers/api/children_controller.rb new file mode 100644 index 000000000..7c5a1003a --- /dev/null +++ b/app/controllers/api/children_controller.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +# 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 + before_action :authenticate_user! + before_action :set_child, only: %i[show update destroy] + + def index + @children = policy_scope(Child) + end + + def show + authorize @child + end + + def create + @child = Child.new(child_params) + authorize @child + if @child.save + render status: :created + else + render json: @child.errors.full_messages, status: :unprocessable_entity + end + end + + def update + authorize @child + + if @child.update(child_params) + render status: :ok + else + render json: @child.errors.full_messages, status: :unprocessable_entity + end + end + + def destroy + authorize @child + @child.destroy + head :no_content + end + + private + + def set_child + @child = Child.find(params[:id]) + end + + def child_params + params.require(:child).permit(:first_name, :last_name, :email, :phone, :birthday, :user_id) + end +end diff --git a/app/frontend/src/javascript/api/child.ts b/app/frontend/src/javascript/api/child.ts new file mode 100644 index 000000000..1ff948219 --- /dev/null +++ b/app/frontend/src/javascript/api/child.ts @@ -0,0 +1,31 @@ +import apiClient from './clients/api-client'; +import { AxiosResponse } from 'axios'; +import { Child, ChildIndexFilter } from '../models/child'; +import ApiLib from '../lib/api'; + +export default class ChildAPI { + static async index (filters: ChildIndexFilter): Promise> { + const res: AxiosResponse> = await apiClient.get(`/api/children${ApiLib.filtersToQuery(filters)}`); + return res?.data; + } + + static async get (id: number): Promise { + const res: AxiosResponse = await apiClient.get(`/api/children/${id}`); + return res?.data; + } + + static async create (child: Child): Promise { + const res: AxiosResponse = await apiClient.post('/api/children', { child }); + return res?.data; + } + + static async update (child: Child): Promise { + const res: AxiosResponse = await apiClient.patch(`/api/children/${child.id}`, { child }); + return res?.data; + } + + static async destroy (childId: number): Promise { + const res: AxiosResponse = await apiClient.delete(`/api/children/${childId}`); + return res?.data; + } +} diff --git a/app/frontend/src/javascript/components/family-account/child-form.tsx b/app/frontend/src/javascript/components/family-account/child-form.tsx new file mode 100644 index 000000000..fee281548 --- /dev/null +++ b/app/frontend/src/javascript/components/family-account/child-form.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { Child } from '../../models/child'; +import { TDateISODate } from '../../typings/date-iso'; +import { FormInput } from '../form/form-input'; + +interface ChildFormProps { + child: Child; + onChange: (field: string, value: string | TDateISODate) => void; +} + +/** + * A form for creating or editing a child. + */ +export const ChildForm: React.FC = ({ child, onChange }) => { + const { t } = useTranslation('public'); + + const { register, formState } = useForm({ + defaultValues: child + }); + + /** + * Handle the change of a child form field + */ + const handleChange = (event: React.ChangeEvent): void => { + onChange(event.target.id, event.target.value); + }; + + return ( +
+
+ {t('app.public.child_form.child_form_info')} +
+
+ + + +
+ ); +}; diff --git a/app/frontend/src/javascript/components/family-account/child-item.tsx b/app/frontend/src/javascript/components/family-account/child-item.tsx new file mode 100644 index 000000000..747d8212c --- /dev/null +++ b/app/frontend/src/javascript/components/family-account/child-item.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { Child } from '../../models/child'; +import { useTranslation } from 'react-i18next'; +import { FabButton } from '../base/fab-button'; + +interface ChildItemProps { + child: Child; + onEdit: (child: Child) => void; + onDelete: (child: Child) => void; +} + +/** + * A child item. + */ +export const ChildItem: React.FC = ({ child, onEdit, onDelete }) => { + const { t } = useTranslation('public'); + + return ( +
+
+
{t('app.public.child_item.last_name')}
+
{child.last_name}
+
+
+
{t('app.public.child_item.first_name')}
+
{child.first_name}
+
+
+
{t('app.public.child_item.birthday')}
+
{child.birthday}
+
+
+ } onClick={() => onEdit(child)} className="edit-button" /> + } onClick={() => onDelete(child)} className="delete-button" /> +
+
+ ); +}; diff --git a/app/frontend/src/javascript/components/family-account/child-modal.tsx b/app/frontend/src/javascript/components/family-account/child-modal.tsx new file mode 100644 index 000000000..03f1486c4 --- /dev/null +++ b/app/frontend/src/javascript/components/family-account/child-modal.tsx @@ -0,0 +1,69 @@ +import * as React from 'react'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { FabModal, ModalSize } from '../base/fab-modal'; +import { Child } from '../../models/child'; +import { TDateISODate } from '../../typings/date-iso'; +import ChildAPI from '../../api/child'; +import { ChildForm } from './child-form'; + +interface ChildModalProps { + child?: Child; + isOpen: boolean; + toggleModal: () => void; +} + +/** + * A modal for creating or editing a child. + */ +export const ChildModal: React.FC = ({ child, isOpen, toggleModal }) => { + const { t } = useTranslation('public'); + const [data, setData] = useState(child); + console.log(child, data); + + /** + * Save the child to the API + */ + const handleSaveChild = async (): Promise => { + try { + if (child?.id) { + await ChildAPI.update(data); + } else { + await ChildAPI.create(data); + } + toggleModal(); + } catch (error) { + console.error(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 + */ + const handleChildChanged = (field: string, value: string | TDateISODate): void => { + setData({ + ...data, + [field]: value + }); + }; + + return ( + + + + ); +}; diff --git a/app/frontend/src/javascript/components/family-account/children-list.tsx b/app/frontend/src/javascript/components/family-account/children-list.tsx new file mode 100644 index 000000000..c737e2d98 --- /dev/null +++ b/app/frontend/src/javascript/components/family-account/children-list.tsx @@ -0,0 +1,86 @@ +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'; +import { Loader } from '../base/loader'; +import { IApplication } from '../../models/application'; +import { ChildModal } from './child-modal'; +import { ChildItem } from './child-item'; +import { FabButton } from '../base/fab-button'; + +declare const Application: IApplication; + +interface ChildrenListProps { + currentUser: User; +} + +/** + * A list of children belonging to the current user. + */ +export const ChildrenList: React.FC = ({ currentUser }) => { + const { t } = useTranslation('public'); + + const [children, setChildren] = useState>([]); + const [isOpenChildModal, setIsOpenChildModal] = useState(false); + const [child, setChild] = useState(); + + useEffect(() => { + ChildAPI.index({ user_id: currentUser.id }).then(setChildren); + }, [currentUser]); + + /** + * Open the add child modal + */ + const addChild = () => { + setIsOpenChildModal(true); + setChild({ user_id: currentUser.id } as Child); + }; + + /** + * Open the edit child modal + */ + const editChild = (child: Child) => { + setIsOpenChildModal(true); + setChild(child); + }; + + /** + * Delete a child + */ + const deleteChild = (child: Child) => { + ChildAPI.destroy(child.id).then(() => { + setChildren(children.filter(c => c.id !== child.id)); + }); + }; + + return ( +
+
+

{t('app.public.children_list.heading')}

+ + {t('app.public.children_list.add_child')} + +
+ +
+ {children.map(child => ( + + ))} +
+ setIsOpenChildModal(false)} /> +
+ ); +}; + +const ChildrenListWrapper: React.FC = (props) => { + return ( + + + + ); +}; + +Application.Components.component('childrenList', react2angular(ChildrenListWrapper, ['currentUser'])); diff --git a/app/frontend/src/javascript/controllers/children.js b/app/frontend/src/javascript/controllers/children.js new file mode 100644 index 000000000..1fc180294 --- /dev/null +++ b/app/frontend/src/javascript/controllers/children.js @@ -0,0 +1,23 @@ +'use strict'; + +Application.Controllers.controller('ChildrenController', ['$scope', 'memberPromise', 'growl', + function ($scope, memberPromise, growl) { + // Current user's profile + $scope.user = memberPromise; + + /** + * Callback used to display a error message + */ + $scope.onError = function (message) { + console.error(message); + growl.error(message); + }; + + /** + * Callback used to display a success message + */ + $scope.onSuccess = function (message) { + growl.success(message); + }; + } +]); diff --git a/app/frontend/src/javascript/models/child.ts b/app/frontend/src/javascript/models/child.ts new file mode 100644 index 000000000..cea24bf1d --- /dev/null +++ b/app/frontend/src/javascript/models/child.ts @@ -0,0 +1,16 @@ +import { TDateISODate } from '../typings/date-iso'; +import { ApiFilter } from './api'; + +export interface ChildIndexFilter extends ApiFilter { + user_id: number, +} + +export interface Child { + id?: number, + last_name: string, + first_name: string, + email?: string, + phone?: string, + birthday: TDateISODate, + user_id: number +} diff --git a/app/frontend/src/javascript/router.js b/app/frontend/src/javascript/router.js index 5adfd6e40..8c8db892d 100644 --- a/app/frontend/src/javascript/router.js +++ b/app/frontend/src/javascript/router.js @@ -28,9 +28,9 @@ angular.module('application.router', ['ui.router']) logoBlackFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'logo-black-file' }).$promise; }], sharedTranslations: ['Translations', function (Translations) { return Translations.query(['app.shared', 'app.public.common']).$promise; }], modulesPromise: ['Setting', function (Setting) { return Setting.query({ names: "['machines_module', 'spaces_module', 'plans_module', 'invoicing_module', 'wallet_module', 'statistics_module', 'trainings_module', 'public_agenda_module', 'store_module']" }).$promise; }], - settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['public_registrations', 'store_hidden']" }).$promise; }] + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['public_registrations', 'store_hidden', 'family_account']" }).$promise; }] }, - onEnter: ['$rootScope', 'logoFile', 'logoBlackFile', 'modulesPromise', 'CSRF', function ($rootScope, logoFile, logoBlackFile, modulesPromise, CSRF) { + onEnter: ['$rootScope', 'logoFile', 'logoBlackFile', 'modulesPromise', 'settingsPromise', 'CSRF', function ($rootScope, logoFile, logoBlackFile, modulesPromise, settingsPromise, CSRF) { // Retrieve Anti-CSRF tokens from cookies CSRF.setMetaTags(); // Application logo @@ -47,6 +47,9 @@ angular.module('application.router', ['ui.router']) publicAgenda: (modulesPromise.public_agenda_module === 'true'), statistics: (modulesPromise.statistics_module === 'true') }; + $rootScope.settings = { + familyAccount: (settingsPromise.family_account === 'true') + }; }] }) .state('app.public', { @@ -151,6 +154,15 @@ angular.module('application.router', ['ui.router']) } } }) + .state('app.logged.dashboard.children', { + url: '/children', + views: { + 'main@': { + templateUrl: '/dashboard/children.html', + controller: 'ChildrenController' + } + } + }) .state('app.logged.dashboard.settings', { url: '/settings', views: { diff --git a/app/frontend/templates/dashboard/children.html b/app/frontend/templates/dashboard/children.html new file mode 100644 index 000000000..8f5c2c17d --- /dev/null +++ b/app/frontend/templates/dashboard/children.html @@ -0,0 +1,11 @@ +
+
+
+ +
+ +
+ + + +
diff --git a/app/frontend/templates/dashboard/nav.html b/app/frontend/templates/dashboard/nav.html index 8da97d01a..f0dc4bd10 100644 --- a/app/frontend/templates/dashboard/nav.html +++ b/app/frontend/templates/dashboard/nav.html @@ -11,6 +11,7 @@

{{ 'app.public.common.dashboard' }}

); diff --git a/app/frontend/src/javascript/components/family-account/child-item.tsx b/app/frontend/src/javascript/components/family-account/child-item.tsx index 747d8212c..44ca25aea 100644 --- a/app/frontend/src/javascript/components/family-account/child-item.tsx +++ b/app/frontend/src/javascript/components/family-account/child-item.tsx @@ -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 = ({ child, onEdit, onDelete })
{t('app.public.child_item.birthday')}
-
{child.birthday}
+
{FormatLib.date(child.birthday)}
} onClick={() => onEdit(child)} className="edit-button" /> diff --git a/app/frontend/src/javascript/components/family-account/child-modal.tsx b/app/frontend/src/javascript/components/family-account/child-modal.tsx index 03f1486c4..baa9af408 100644 --- a/app/frontend/src/javascript/components/family-account/child-modal.tsx +++ b/app/frontend/src/javascript/components/family-account/child-modal.tsx @@ -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 = ({ child, isOpen, toggleModal }) => { +export const ChildModal: React.FC = ({ child, isOpen, toggleModal, onSuccess, onError }) => { const { t } = useTranslation('public'); const [data, setData] = useState(child); - console.log(child, data); + + useEffect(() => { + setData(child); + }, [child]); /** * Save the child to the API @@ -32,18 +37,12 @@ export const ChildModal: React.FC = ({ 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 = ({ child, isOpen, toggleMod isOpen={isOpen} toggleModal={toggleModal} closeButton={true} - confirmButton={t('app.public.child_modal.save')} - onConfirm={handleSaveChild} - preventConfirm={isPreventedSaveChild()}> - + confirmButton={false} + onConfirm={handleSaveChild} > + ); }; diff --git a/app/frontend/src/javascript/components/family-account/children-list.tsx b/app/frontend/src/javascript/components/family-account/children-list.tsx index c737e2d98..287f69802 100644 --- a/app/frontend/src/javascript/components/family-account/children-list.tsx +++ b/app/frontend/src/javascript/components/family-account/children-list.tsx @@ -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 = ({ currentUser }) => { +export const ChildrenList: React.FC = ({ currentUser, onError }) => { const { t } = useTranslation('public'); const [children, setChildren] = useState>([]); @@ -52,10 +53,17 @@ export const ChildrenList: React.FC = ({ 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 (
@@ -70,7 +78,7 @@ export const ChildrenList: React.FC = ({ currentUser }) => { ))}
- setIsOpenChildModal(false)} /> + setIsOpenChildModal(false)} onSuccess={handleSaveChildSuccess} onError={onError} /> ); }; @@ -83,4 +91,4 @@ const ChildrenListWrapper: React.FC = (props) => { ); }; -Application.Components.component('childrenList', react2angular(ChildrenListWrapper, ['currentUser'])); +Application.Components.component('childrenList', react2angular(ChildrenListWrapper, ['currentUser', 'onSuccess', 'onError'])); diff --git a/app/frontend/src/javascript/components/form/form-input.tsx b/app/frontend/src/javascript/components/form/form-input.tsx index d4a4331e9..b19d42ccc 100644 --- a/app/frontend/src/javascript/components/form/form-input.tsx +++ b/app/frontend/src/javascript/components/form/form-input.tsx @@ -22,13 +22,14 @@ type FormInputProps = FormComponent & Ab onChange?: (event: React.ChangeEvent) => 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 = ({ 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) => { +export const FormInput = ({ 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) => { const [characterCount, setCharacterCount] = useState(0); /** @@ -100,7 +101,8 @@ export const FormInput = ({ id, re disabled={typeof disabled === 'function' ? disabled(id) : disabled} placeholder={placeholder} accept={accept} - maxLength={maxLength} /> + maxLength={maxLength} + max={max} /> {(type === 'file' && placeholder) && {placeholder}} {maxLength && {characterCount} / {maxLength}} {addOn && addOnAction && } diff --git a/app/models/child.rb b/app/models/child.rb index 49ebdaf9e..93332bde8 100644 --- a/app/models/child.rb +++ b/app/models/child.rb @@ -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 diff --git a/config/locales/app.public.en.yml b/config/locales/app.public.en.yml index 1ec5f80f6..dee4e8ced 100644 --- a/config/locales/app.public.en.yml +++ b/config/locales/app.public.en.yml @@ -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" diff --git a/config/locales/app.public.fr.yml b/config/locales/app.public.fr.yml index f430d82b1..781ee06d2 100644 --- a/config/locales/app.public.fr.yml +++ b/config/locales/app.public.fr.yml @@ -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" diff --git a/config/locales/en.yml b/config/locales/en.yml index cec01d3e2..6cdd8c50f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 6d8a4588f..17b01ef07 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -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: From 9373f507060fb603734f45fe0178f3ab7a6726ca Mon Sep 17 00:00:00 2001 From: Du Peng Date: Tue, 9 May 2023 18:54:16 +0200 Subject: [PATCH 04/75] (wip) event reservation naminative --- Gemfile.lock | 1 - app/controllers/api/events_controller.rb | 2 +- .../components/events/event-form.tsx | 5 + .../src/javascript/controllers/events.js.erb | 51 ++++- app/frontend/src/javascript/models/event.ts | 3 +- app/frontend/templates/events/show.html | 16 +- app/models/booking_user.rb | 9 + app/models/reservation.rb | 3 + app/views/api/events/_event.json.jbuilder | 2 +- config/locales/app.admin.en.yml | 2 + config/locales/app.admin.fr.yml | 2 + ...9121907_add_booking_nominative_to_event.rb | 8 + .../20230509161557_create_booking_users.rb | 15 ++ db/structure.sql | 208 +++++++++++++++--- .../components/events/event-form.test.tsx | 1 + 15 files changed, 287 insertions(+), 41 deletions(-) create mode 100644 app/models/booking_user.rb create mode 100644 db/migrate/20230509121907_add_booking_nominative_to_event.rb create mode 100644 db/migrate/20230509161557_create_booking_users.rb diff --git a/Gemfile.lock b/Gemfile.lock index 6c6c012ae..1d1f8f54d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -529,7 +529,6 @@ GEM PLATFORMS x86_64-darwin-20 - x86_64-darwin-21 x86_64-linux DEPENDENCIES diff --git a/app/controllers/api/events_controller.rb b/app/controllers/api/events_controller.rb index ce4b42bd9..1b8abdc77 100644 --- a/app/controllers/api/events_controller.rb +++ b/app/controllers/api/events_controller.rb @@ -96,7 +96,7 @@ class API::EventsController < API::APIController # handle general properties event_preparams = params.required(:event).permit(:title, :description, :start_date, :start_time, :end_date, :end_time, :amount, :nb_total_places, :availability_id, :all_day, :recurrence, - :recurrence_end_at, :category_id, :event_theme_ids, :age_range_id, + :recurrence_end_at, :category_id, :event_theme_ids, :age_range_id, :booking_nominative, event_theme_ids: [], event_image_attributes: %i[id attachment], event_files_attributes: %i[id attachment _destroy], diff --git a/app/frontend/src/javascript/components/events/event-form.tsx b/app/frontend/src/javascript/components/events/event-form.tsx index e8a89c29f..033dff894 100644 --- a/app/frontend/src/javascript/components/events/event-form.tsx +++ b/app/frontend/src/javascript/components/events/event-form.tsx @@ -290,6 +290,11 @@ export const EventForm: React.FC = ({ action, event, onError, on label={t('app.admin.event_form.seats_available')} type="number" tooltip={t('app.admin.event_form.seats_help')} /> + u.booked_id === $scope.ctrl.member.id && u.booked_type === 'User')) { + return true; + } + } + return false; + }; + /** * Callback to call when the number of tickets to book changes in the current booking */ - $scope.changeNbPlaces = function () { + $scope.changeNbPlaces = function (priceType) { // compute the total remaining places let remain = $scope.event.nb_free_places - $scope.reserve.nbReservePlaces; for (let ticket in $scope.reserve.tickets) { @@ -247,6 +260,22 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' } } + const nbBookingUsers = $scope.reserve.bookingUsers[priceType].length; + const nbReservePlaces = priceType === 'normal' ? $scope.reserve.nbReservePlaces : $scope.reserve.tickets[priceType]; + if (nbReservePlaces > nbBookingUsers) { + _.times(nbReservePlaces - nbBookingUsers, () => { + if (!hasMemberInBookingUsers()) { + $scope.reserve.bookingUsers[priceType].push({ event_price_category_id: priceType === 'normal' ? null : priceType, booked_id: $scope.ctrl.member.id, booked_type: 'User', name: $scope.ctrl.member.name }); + } else { + $scope.reserve.bookingUsers[priceType].push({ event_price_category_id: priceType === 'normal' ? null : priceType }); + } + }); + } else { + _.times(nbBookingUsers - nbReservePlaces, () => { + $scope.reserve.bookingUsers[priceType].pop(); + }); + } + // recompute the total price return $scope.computeEventAmount(); }; @@ -638,7 +667,8 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' reservable_type: 'Event', slots_reservations_attributes: [], nb_reserve_places: reserve.nbReservePlaces, - tickets_attributes: [] + tickets_attributes: [], + booking_users_attributes: [] }; reservation.slots_reservations_attributes.push({ @@ -656,6 +686,15 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' } } + if (event.booking_nominative) { + for (const key of Object.keys($scope.reserve.bookingUsers)) { + for (const user of $scope.reserve.bookingUsers[key]) { + reservation.booking_users_attributes.push(user); + } + } + console.log(reservation); + } + return { reservation }; }; @@ -688,11 +727,15 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' tickets: {}, toReserve: false, amountTotal: 0, - totalSeats: 0 + totalSeats: 0, + bookingUsers: { + normal: [], + }, }; for (let evt_px_cat of Array.from($scope.event.event_price_categories_attributes)) { $scope.reserve.nbPlaces[evt_px_cat.id] = __range__(0, $scope.event.nb_free_places, true); + $scope.reserve.bookingUsers[evt_px_cat.id] = []; $scope.reserve.tickets[evt_px_cat.id] = 0; } diff --git a/app/frontend/src/javascript/models/event.ts b/app/frontend/src/javascript/models/event.ts index 5cca1d9e8..52bdc1496 100644 --- a/app/frontend/src/javascript/models/event.ts +++ b/app/frontend/src/javascript/models/event.ts @@ -63,7 +63,8 @@ export interface Event { }>, recurrence: RecurrenceOption, recurrence_end_at: Date, - advanced_accounting_attributes?: AdvancedAccounting + advanced_accounting_attributes?: AdvancedAccounting, + booking_nominative: boolean, } export interface EventDecoration { diff --git a/app/frontend/templates/events/show.html b/app/frontend/templates/events/show.html index ec362a3d5..c8d86cd02 100644 --- a/app/frontend/templates/events/show.html +++ b/app/frontend/templates/events/show.html @@ -116,16 +116,28 @@
- {{ 'app.public.events_show.ticket' | translate:{NUMBER:reserve.nbReservePlaces} }}
+
+
+ + +
+
- {{ 'app.public.events_show.ticket' | translate:{NUMBER:reserve.tickets[price.id]} }}
+
+
+ + +
+
diff --git a/app/models/booking_user.rb b/app/models/booking_user.rb new file mode 100644 index 000000000..78c6fe6b5 --- /dev/null +++ b/app/models/booking_user.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +# BookingUser is a class for save the booking info of reservation +# booked can be a User or a Child (polymorphic) +class BookingUser < ApplicationRecord + belongs_to :reservation + belongs_to :booked, polymorphic: true + belongs_to :event_price_category +end diff --git a/app/models/reservation.rb b/app/models/reservation.rb index d0472354b..9b9d0ff4f 100644 --- a/app/models/reservation.rb +++ b/app/models/reservation.rb @@ -23,6 +23,9 @@ class Reservation < ApplicationRecord has_many :prepaid_pack_reservations, dependent: :destroy + has_many :booking_users, dependent: :destroy + accepts_nested_attributes_for :booking_users, allow_destroy: true + validates :reservable_id, :reservable_type, presence: true validate :machine_not_already_reserved, if: -> { reservable.is_a?(Machine) } validate :training_not_fully_reserved, if: -> { reservable.is_a?(Training) } diff --git a/app/views/api/events/_event.json.jbuilder b/app/views/api/events/_event.json.jbuilder index 09a19e0c0..11be0748e 100644 --- a/app/views/api/events/_event.json.jbuilder +++ b/app/views/api/events/_event.json.jbuilder @@ -1,6 +1,6 @@ # frozen_string_literal: true -json.extract! event, :id, :title, :description +json.extract! event, :id, :title, :description, :booking_nominative if event.event_image json.event_image_attributes do json.id event.event_image.id diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index dae87a978..74d26c121 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -139,6 +139,8 @@ en: event_themes: "Event themes" age_range: "Age range" add_price: "Add a price" + booking_nominative: "Nominative booking" + booking_nominative_help: "If you check this option, the members will have to enter the names of the participants when booking." save: "Save" create_success: "The event was created successfully" events_updated: "{COUNT, plural, =1{One event was} other{{COUNT} Events were}} successfully updated" diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml index d3c4941a1..34bbaed54 100644 --- a/config/locales/app.admin.fr.yml +++ b/config/locales/app.admin.fr.yml @@ -139,6 +139,8 @@ fr: event_themes: "Thèmes de l'événement" age_range: "Tranche d'âge" add_price: "Ajouter un tarif" + booking_nominative: "Réservation nominative" + booking_nominative_help: "Si cette option est activée, les réservations seront nominatives. Les participants devront s'identifier pour réserver." save: "Enregistrer" create_success: "L'événement a bien été créé" events_updated: "{COUNT, plural, one {}=1{Un événement à été} other{{COUNT} événements ont été}} mis à jour avec succès" diff --git a/db/migrate/20230509121907_add_booking_nominative_to_event.rb b/db/migrate/20230509121907_add_booking_nominative_to_event.rb new file mode 100644 index 000000000..1a709170d --- /dev/null +++ b/db/migrate/20230509121907_add_booking_nominative_to_event.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# add booking_nominative to event +class AddBookingNominativeToEvent < ActiveRecord::Migration[7.0] + def change + add_column :events, :booking_nominative, :boolean, default: false + end +end diff --git a/db/migrate/20230509161557_create_booking_users.rb b/db/migrate/20230509161557_create_booking_users.rb new file mode 100644 index 000000000..f1ba65728 --- /dev/null +++ b/db/migrate/20230509161557_create_booking_users.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# create booking_users table +class CreateBookingUsers < ActiveRecord::Migration[7.0] + def change + create_table :booking_users do |t| + t.string :name + t.belongs_to :reservation, foreign_key: true + t.references :booked, polymorphic: true + t.references :event_price_category, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/structure.sql b/db/structure.sql index a9aa1f96d..54505685e 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -108,8 +108,8 @@ SET default_tablespace = ''; CREATE TABLE public.abuses ( id integer NOT NULL, - signaled_id integer, signaled_type character varying, + signaled_id integer, first_name character varying, last_name character varying, email character varying, @@ -229,8 +229,8 @@ CREATE TABLE public.addresses ( locality character varying, country character varying, postal_code character varying, - placeable_id integer, placeable_type character varying, + placeable_id integer, created_at timestamp without time zone, updated_at timestamp without time zone ); @@ -339,8 +339,8 @@ CREATE TABLE public.ar_internal_metadata ( CREATE TABLE public.assets ( id integer NOT NULL, - viewable_id integer, viewable_type character varying, + viewable_id integer, attachment character varying, type character varying, created_at timestamp without time zone, @@ -513,6 +513,41 @@ CREATE SEQUENCE public.availability_tags_id_seq ALTER SEQUENCE public.availability_tags_id_seq OWNED BY public.availability_tags.id; +-- +-- Name: booking_users; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.booking_users ( + id bigint NOT NULL, + name character varying, + reservation_id bigint, + booked_type character varying, + booked_id bigint, + event_price_category_id bigint, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: booking_users_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.booking_users_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: booking_users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.booking_users_id_seq OWNED BY public.booking_users.id; + + -- -- Name: cart_item_coupons; Type: TABLE; Schema: public; Owner: - -- @@ -885,6 +920,42 @@ CREATE SEQUENCE public.chained_elements_id_seq ALTER SEQUENCE public.chained_elements_id_seq OWNED BY public.chained_elements.id; +-- +-- Name: children; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.children ( + id bigint NOT NULL, + user_id bigint, + first_name character varying, + last_name character varying, + birthday date, + phone character varying, + email character varying, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: children_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.children_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: children_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.children_id_seq OWNED BY public.children.id; + + -- -- Name: components; Type: TABLE; Schema: public; Owner: - -- @@ -958,8 +1029,8 @@ ALTER SEQUENCE public.coupons_id_seq OWNED BY public.coupons.id; CREATE TABLE public.credits ( id integer NOT NULL, - creditable_id integer, creditable_type character varying, + creditable_id integer, plan_id integer, hours integer, created_at timestamp without time zone, @@ -1129,7 +1200,8 @@ CREATE TABLE public.events ( recurrence_id integer, age_range_id integer, category_id integer, - deleted_at timestamp without time zone + deleted_at timestamp without time zone, + booking_nominative boolean DEFAULT false ); @@ -1755,15 +1827,15 @@ ALTER SEQUENCE public.notification_types_id_seq OWNED BY public.notification_typ CREATE TABLE public.notifications ( id integer NOT NULL, receiver_id integer, - attached_object_id integer, attached_object_type character varying, + attached_object_id integer, notification_type_id integer, is_read boolean DEFAULT false, created_at timestamp without time zone, updated_at timestamp without time zone, receiver_type character varying, is_send boolean DEFAULT false, - meta_data jsonb DEFAULT '{}'::jsonb + meta_data jsonb DEFAULT '"{}"'::jsonb ); @@ -2491,8 +2563,8 @@ CREATE TABLE public.prices ( id integer NOT NULL, group_id integer, plan_id integer, - priceable_id integer, priceable_type character varying, + priceable_id integer, amount integer, created_at timestamp without time zone NOT NULL, updated_at timestamp without time zone NOT NULL, @@ -3018,8 +3090,8 @@ CREATE TABLE public.reservations ( message text, created_at timestamp without time zone, updated_at timestamp without time zone, - reservable_id integer, reservable_type character varying, + reservable_id integer, nb_reserve_places integer, statistic_profile_id integer ); @@ -3051,8 +3123,8 @@ ALTER SEQUENCE public.reservations_id_seq OWNED BY public.reservations.id; CREATE TABLE public.roles ( id integer NOT NULL, name character varying, - resource_id integer, resource_type character varying, + resource_id integer, created_at timestamp without time zone, updated_at timestamp without time zone ); @@ -4158,8 +4230,8 @@ CREATE TABLE public.users ( is_allow_newsletter boolean, current_sign_in_ip inet, last_sign_in_ip inet, - mapped_from_sso character varying, - validated_at timestamp without time zone + validated_at timestamp without time zone, + mapped_from_sso character varying ); @@ -4368,6 +4440,13 @@ ALTER TABLE ONLY public.availabilities ALTER COLUMN id SET DEFAULT nextval('publ ALTER TABLE ONLY public.availability_tags ALTER COLUMN id SET DEFAULT nextval('public.availability_tags_id_seq'::regclass); +-- +-- Name: booking_users id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.booking_users ALTER COLUMN id SET DEFAULT nextval('public.booking_users_id_seq'::regclass); + + -- -- Name: cart_item_coupons id; Type: DEFAULT; Schema: public; Owner: - -- @@ -4445,6 +4524,13 @@ ALTER TABLE ONLY public.categories ALTER COLUMN id SET DEFAULT nextval('public.c ALTER TABLE ONLY public.chained_elements ALTER COLUMN id SET DEFAULT nextval('public.chained_elements_id_seq'::regclass); +-- +-- Name: children id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.children ALTER COLUMN id SET DEFAULT nextval('public.children_id_seq'::regclass); + + -- -- Name: components id; Type: DEFAULT; Schema: public; Owner: - -- @@ -5220,6 +5306,14 @@ ALTER TABLE ONLY public.availability_tags ADD CONSTRAINT availability_tags_pkey PRIMARY KEY (id); +-- +-- Name: booking_users booking_users_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.booking_users + ADD CONSTRAINT booking_users_pkey PRIMARY KEY (id); + + -- -- Name: cart_item_coupons cart_item_coupons_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -5308,6 +5402,14 @@ ALTER TABLE ONLY public.chained_elements ADD CONSTRAINT chained_elements_pkey PRIMARY KEY (id); +-- +-- Name: children children_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.children + ADD CONSTRAINT children_pkey PRIMARY KEY (id); + + -- -- Name: components components_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -5804,6 +5906,14 @@ ALTER TABLE ONLY public.roles ADD CONSTRAINT roles_pkey PRIMARY KEY (id); +-- +-- Name: schema_migrations schema_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.schema_migrations + ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version); + + -- -- Name: settings settings_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -6154,6 +6264,27 @@ CREATE INDEX index_availability_tags_on_availability_id ON public.availability_t CREATE INDEX index_availability_tags_on_tag_id ON public.availability_tags USING btree (tag_id); +-- +-- Name: index_booking_users_on_booked; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_booking_users_on_booked ON public.booking_users USING btree (booked_type, booked_id); + + +-- +-- Name: index_booking_users_on_event_price_category_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_booking_users_on_event_price_category_id ON public.booking_users USING btree (event_price_category_id); + + +-- +-- Name: index_booking_users_on_reservation_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_booking_users_on_reservation_id ON public.booking_users USING btree (reservation_id); + + -- -- Name: index_cart_item_coupons_on_coupon_id; Type: INDEX; Schema: public; Owner: - -- @@ -6336,6 +6467,13 @@ CREATE UNIQUE INDEX index_categories_on_slug ON public.categories USING btree (s CREATE INDEX index_chained_elements_on_element ON public.chained_elements USING btree (element_type, element_id); +-- +-- Name: index_children_on_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_children_on_user_id ON public.children USING btree (user_id); + + -- -- Name: index_coupons_on_code; Type: INDEX; Schema: public; Owner: - -- @@ -7484,21 +7622,6 @@ CREATE INDEX proof_of_identity_type_id_and_proof_of_identity_refusal_id ON publi CREATE UNIQUE INDEX unique_not_null_external_id ON public.invoicing_profiles USING btree (external_id) WHERE (external_id IS NOT NULL); --- --- Name: unique_schema_migrations; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX unique_schema_migrations ON public.schema_migrations USING btree (version); - - --- --- Name: accounting_periods accounting_periods_del_protect; Type: RULE; Schema: public; Owner: - --- - -CREATE RULE accounting_periods_del_protect AS - ON DELETE TO public.accounting_periods DO INSTEAD NOTHING; - - -- -- Name: accounting_periods accounting_periods_upd_protect; Type: RULE; Schema: public; Owner: - -- @@ -7732,6 +7855,14 @@ ALTER TABLE ONLY public.subscriptions ADD CONSTRAINT fk_rails_358a71f738 FOREIGN KEY (statistic_profile_id) REFERENCES public.statistic_profiles(id); +-- +-- Name: booking_users fk_rails_38ad1ae7e8; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.booking_users + ADD CONSTRAINT fk_rails_38ad1ae7e8 FOREIGN KEY (reservation_id) REFERENCES public.reservations(id); + + -- -- Name: invoices fk_rails_40d78f8cf6; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -8132,6 +8263,14 @@ ALTER TABLE ONLY public.cart_item_coupons ADD CONSTRAINT fk_rails_a44bac1e45 FOREIGN KEY (operator_profile_id) REFERENCES public.invoicing_profiles(id); +-- +-- Name: children fk_rails_a51d7cfb22; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.children + ADD CONSTRAINT fk_rails_a51d7cfb22 FOREIGN KEY (user_id) REFERENCES public.users(id); + + -- -- Name: projects_themes fk_rails_b021a22658; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -8380,6 +8519,14 @@ ALTER TABLE ONLY public.projects ADD CONSTRAINT fk_rails_e812590204 FOREIGN KEY (author_statistic_profile_id) REFERENCES public.statistic_profiles(id); +-- +-- Name: booking_users fk_rails_e88263229e; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.booking_users + ADD CONSTRAINT fk_rails_e88263229e FOREIGN KEY (event_price_category_id) REFERENCES public.event_price_categories(id); + + -- -- Name: user_tags fk_rails_ea0382482a; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -8491,7 +8638,6 @@ INSERT INTO "schema_migrations" (version) VALUES ('20140605125131'), ('20140605142133'), ('20140605151442'), -('20140606133116'), ('20140609092700'), ('20140609092827'), ('20140610153123'), @@ -8560,14 +8706,12 @@ INSERT INTO "schema_migrations" (version) VALUES ('20150507075620'), ('20150512123546'), ('20150520132030'), -('20150520133409'), ('20150526130729'), ('20150527153312'), ('20150529113555'), ('20150601125944'), ('20150603104502'), ('20150603104658'), -('20150603133050'), ('20150604081757'), ('20150604131525'), ('20150608142234'), @@ -8649,7 +8793,6 @@ INSERT INTO "schema_migrations" (version) VALUES ('20160905142700'), ('20160906094739'), ('20160906094847'), -('20160906145713'), ('20160915105234'), ('20161123104604'), ('20170109085345'), @@ -8817,6 +8960,9 @@ INSERT INTO "schema_migrations" (version) VALUES ('20230328094807'), ('20230328094808'), ('20230328094809'), +('20230331132506'), +('20230509121907'), +('20230509161557'); ('20230626122844'), ('20230626122947'); diff --git a/test/frontend/components/events/event-form.test.tsx b/test/frontend/components/events/event-form.test.tsx index 7b3d53b6c..49f50ce79 100644 --- a/test/frontend/components/events/event-form.test.tsx +++ b/test/frontend/components/events/event-form.test.tsx @@ -27,6 +27,7 @@ describe('EventForm', () => { expect(screen.getByLabelText(/app.admin.event_form._and_ends_on/)).toBeInTheDocument(); expect(screen.getByLabelText(/app.admin.event_form.seats_available/)).toBeInTheDocument(); expect(screen.getByLabelText(/app.admin.event_form.standard_rate/)).toBeInTheDocument(); + expect(screen.getByLabelText(/app.admin.event_form.booking_nominative/)).toBeInTheDocument(); expect(screen.getByRole('button', { name: /app.admin.event_form.add_price/ })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /app.admin.event_form.add_a_new_file/ })).toBeInTheDocument(); expect(screen.getByLabelText(/app.admin.advanced_accounting_form.code/)).toBeInTheDocument(); From 177748169acec970a185a4c29ee32eaf9b644c5a Mon Sep 17 00:00:00 2001 From: Du Peng Date: Wed, 10 May 2023 18:47:13 +0200 Subject: [PATCH 05/75] (wip) save booking user for event nominatif --- .../src/javascript/controllers/events.js.erb | 11 ++- app/frontend/templates/events/show.html | 10 +-- app/models/cart_item.rb | 5 ++ app/models/cart_item/event_reservation.rb | 13 +++ .../event_reservation_booking_user.rb | 10 +++ app/services/cart_service.rb | 3 +- config/locales/app.public.en.yml | 1 + config/locales/app.public.fr.yml | 1 + ...rt_item_event_reservation_booking_users.rb | 15 ++++ db/structure.sql | 88 +++++++++++++++++++ 10 files changed, 149 insertions(+), 8 deletions(-) create mode 100644 app/models/cart_item.rb create mode 100644 app/models/cart_item/event_reservation_booking_user.rb create mode 100644 db/migrate/20230510141305_create_cart_item_event_reservation_booking_users.rb diff --git a/app/frontend/src/javascript/controllers/events.js.erb b/app/frontend/src/javascript/controllers/events.js.erb index 63e2bd25d..335f97580 100644 --- a/app/frontend/src/javascript/controllers/events.js.erb +++ b/app/frontend/src/javascript/controllers/events.js.erb @@ -264,11 +264,14 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' const nbReservePlaces = priceType === 'normal' ? $scope.reserve.nbReservePlaces : $scope.reserve.tickets[priceType]; if (nbReservePlaces > nbBookingUsers) { _.times(nbReservePlaces - nbBookingUsers, () => { + /* if (!hasMemberInBookingUsers()) { $scope.reserve.bookingUsers[priceType].push({ event_price_category_id: priceType === 'normal' ? null : priceType, booked_id: $scope.ctrl.member.id, booked_type: 'User', name: $scope.ctrl.member.name }); } else { $scope.reserve.bookingUsers[priceType].push({ event_price_category_id: priceType === 'normal' ? null : priceType }); } + */ + $scope.reserve.bookingUsers[priceType].push({ event_price_category_id: priceType === 'normal' ? null : priceType }); }); } else { _.times(nbBookingUsers - nbReservePlaces, () => { @@ -689,10 +692,14 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' if (event.booking_nominative) { for (const key of Object.keys($scope.reserve.bookingUsers)) { for (const user of $scope.reserve.bookingUsers[key]) { - reservation.booking_users_attributes.push(user); + reservation.booking_users_attributes.push({ + event_price_category_id: user.event_price_category_id, + name: user.name, + booked_id: user.booked_id, + booked_type: user.booked_type + }); } } - console.log(reservation); } return { reservation }; diff --git a/app/frontend/templates/events/show.html b/app/frontend/templates/events/show.html index c8d86cd02..a75de228b 100644 --- a/app/frontend/templates/events/show.html +++ b/app/frontend/templates/events/show.html @@ -119,9 +119,9 @@ {{ 'app.public.events_show.ticket' | translate:{NUMBER:reserve.nbReservePlaces} }}
-
+
- +
@@ -132,9 +132,9 @@ {{ 'app.public.events_show.ticket' | translate:{NUMBER:reserve.tickets[price.id]} }}
-
+
- +
@@ -169,7 +169,7 @@
{{ 'app.public.events_show.you_booked_DATE' | translate:{DATE:(reservation.created_at | amDateFormat:'L LT')} }}
{{ 'app.public.events_show.full_price_' | translate }} {{reservation.nb_reserve_places}} {{ 'app.public.events_show.ticket' | translate:{NUMBER:reservation.nb_reserve_places} }}
-
+
{{ticket.event_price_category.price_category.name}} : {{ticket.booked}} {{ 'app.public.events_show.ticket' | translate:{NUMBER:ticket.booked} }}
diff --git a/app/models/cart_item.rb b/app/models/cart_item.rb new file mode 100644 index 000000000..4397e866e --- /dev/null +++ b/app/models/cart_item.rb @@ -0,0 +1,5 @@ +module CartItem + def self.table_name_prefix + "cart_item_" + end +end diff --git a/app/models/cart_item/event_reservation.rb b/app/models/cart_item/event_reservation.rb index 9ae7be11a..cbb90a934 100644 --- a/app/models/cart_item/event_reservation.rb +++ b/app/models/cart_item/event_reservation.rb @@ -13,6 +13,11 @@ class CartItem::EventReservation < CartItem::Reservation foreign_type: 'cart_item_type', as: :cart_item accepts_nested_attributes_for :cart_item_reservation_slots + has_many :cart_item_event_reservation_booking_users, class_name: 'CartItem::EventReservationBookingUser', dependent: :destroy, + inverse_of: :cart_item_event_reservation, + foreign_key: 'cart_item_event_reservation_id' + accepts_nested_attributes_for :cart_item_event_reservation_booking_users + belongs_to :operator_profile, class_name: 'InvoicingProfile' belongs_to :customer_profile, class_name: 'InvoicingProfile' @@ -63,6 +68,14 @@ class CartItem::EventReservation < CartItem::Reservation booked: t.booked } end, + booking_users_attributes: cart_item_event_reservation_booking_users.map do |b| + { + event_price_category_id: b.event_price_category_id, + booked_type: b.booked_type, + booked_id: b.booked_id, + name: b.name + } + end, nb_reserve_places: normal_tickets, statistic_profile_id: StatisticProfile.find_by(user: customer).id ) diff --git a/app/models/cart_item/event_reservation_booking_user.rb b/app/models/cart_item/event_reservation_booking_user.rb new file mode 100644 index 000000000..82df9ac9c --- /dev/null +++ b/app/models/cart_item/event_reservation_booking_user.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# A relation table between a pending event reservation and reservation users for this event +class CartItem::EventReservationBookingUser < ApplicationRecord + self.table_name = 'cart_item_event_reservation_booking_users' + + belongs_to :cart_item_event_reservation, class_name: 'CartItem::EventReservation', inverse_of: :cart_item_event_reservation_booking_users + belongs_to :event_price_category, inverse_of: :cart_item_event_reservation_tickets + belongs_to :booked, polymorphic: true +end diff --git a/app/services/cart_service.rb b/app/services/cart_service.rb index 6c6588e4b..52210c40d 100644 --- a/app/services/cart_service.rb +++ b/app/services/cart_service.rb @@ -171,7 +171,8 @@ class CartService event: reservable, cart_item_reservation_slots_attributes: cart_item[:slots_reservations_attributes], normal_tickets: cart_item[:nb_reserve_places], - cart_item_event_reservation_tickets_attributes: cart_item[:tickets_attributes] || {}) + cart_item_event_reservation_tickets_attributes: cart_item[:tickets_attributes] || {}, + cart_item_event_reservation_booking_users_attributes: cart_item[:booking_users_attributes] || {}) when Space CartItem::SpaceReservation.new(customer_profile: @customer.invoicing_profile, operator_profile: @operator.invoicing_profile, diff --git a/config/locales/app.public.en.yml b/config/locales/app.public.en.yml index dee4e8ced..6d7f2b66d 100644 --- a/config/locales/app.public.en.yml +++ b/config/locales/app.public.en.yml @@ -363,6 +363,7 @@ en: view_event_list: "View events to come" share_on_facebook: "Share on Facebook" share_on_twitter: "Share on Twitter" + last_name_and_first_name: "Last name and first name" #public calendar calendar: calendar: "Calendar" diff --git a/config/locales/app.public.fr.yml b/config/locales/app.public.fr.yml index 781ee06d2..f7ddb0f08 100644 --- a/config/locales/app.public.fr.yml +++ b/config/locales/app.public.fr.yml @@ -363,6 +363,7 @@ fr: view_event_list: "Voir les événements à venir" share_on_facebook: "Partager sur Facebook" share_on_twitter: "Partager sur Twitter" + last_name_and_first_name: "Nom et prénom" #public calendar calendar: calendar: "Calendrier" diff --git a/db/migrate/20230510141305_create_cart_item_event_reservation_booking_users.rb b/db/migrate/20230510141305_create_cart_item_event_reservation_booking_users.rb new file mode 100644 index 000000000..bbaae5626 --- /dev/null +++ b/db/migrate/20230510141305_create_cart_item_event_reservation_booking_users.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# A relation table between a pending event reservation and reservation users for this event +class CreateCartItemEventReservationBookingUsers < ActiveRecord::Migration[7.0] + def change + create_table :cart_item_event_reservation_booking_users do |t| + t.string :name + t.belongs_to :cart_item_event_reservation, foreign_key: true, index: { name: 'index_cart_item_booking_users_on_cart_item_event_reservation' } + t.references :event_price_category, foreign_key: true, index: { name: 'index_cart_item_booking_users_on_event_price_category' } + t.references :booked, polymorphic: true + + t.timestamps + end + end +end diff --git a/db/structure.sql b/db/structure.sql index 54505685e..b77f0a07c 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -581,6 +581,41 @@ CREATE SEQUENCE public.cart_item_coupons_id_seq ALTER SEQUENCE public.cart_item_coupons_id_seq OWNED BY public.cart_item_coupons.id; +-- +-- Name: cart_item_event_reservation_booking_users; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.cart_item_event_reservation_booking_users ( + id bigint NOT NULL, + name character varying, + cart_item_event_reservation_id bigint, + event_price_category_id bigint, + booked_type character varying, + booked_id bigint, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: cart_item_event_reservation_booking_users_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.cart_item_event_reservation_booking_users_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: cart_item_event_reservation_booking_users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.cart_item_event_reservation_booking_users_id_seq OWNED BY public.cart_item_event_reservation_booking_users.id; + + -- -- Name: cart_item_event_reservation_tickets; Type: TABLE; Schema: public; Owner: - -- @@ -4454,6 +4489,13 @@ ALTER TABLE ONLY public.booking_users ALTER COLUMN id SET DEFAULT nextval('publi ALTER TABLE ONLY public.cart_item_coupons ALTER COLUMN id SET DEFAULT nextval('public.cart_item_coupons_id_seq'::regclass); +-- +-- Name: cart_item_event_reservation_booking_users id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.cart_item_event_reservation_booking_users ALTER COLUMN id SET DEFAULT nextval('public.cart_item_event_reservation_booking_users_id_seq'::regclass); + + -- -- Name: cart_item_event_reservation_tickets id; Type: DEFAULT; Schema: public; Owner: - -- @@ -5322,6 +5364,14 @@ ALTER TABLE ONLY public.cart_item_coupons ADD CONSTRAINT cart_item_coupons_pkey PRIMARY KEY (id); +-- +-- Name: cart_item_event_reservation_booking_users cart_item_event_reservation_booking_users_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.cart_item_event_reservation_booking_users + ADD CONSTRAINT cart_item_event_reservation_booking_users_pkey PRIMARY KEY (id); + + -- -- Name: cart_item_event_reservation_tickets cart_item_event_reservation_tickets_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -6285,6 +6335,20 @@ CREATE INDEX index_booking_users_on_event_price_category_id ON public.booking_us CREATE INDEX index_booking_users_on_reservation_id ON public.booking_users USING btree (reservation_id); +-- +-- Name: index_cart_item_booking_users_on_cart_item_event_reservation; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_cart_item_booking_users_on_cart_item_event_reservation ON public.cart_item_event_reservation_booking_users USING btree (cart_item_event_reservation_id); + + +-- +-- Name: index_cart_item_booking_users_on_event_price_category; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_cart_item_booking_users_on_event_price_category ON public.cart_item_event_reservation_booking_users USING btree (event_price_category_id); + + -- -- Name: index_cart_item_coupons_on_coupon_id; Type: INDEX; Schema: public; Owner: - -- @@ -6306,6 +6370,13 @@ CREATE INDEX index_cart_item_coupons_on_customer_profile_id ON public.cart_item_ CREATE INDEX index_cart_item_coupons_on_operator_profile_id ON public.cart_item_coupons USING btree (operator_profile_id); +-- +-- Name: index_cart_item_event_reservation_booking_users_on_booked; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_cart_item_event_reservation_booking_users_on_booked ON public.cart_item_event_reservation_booking_users USING btree (booked_type, booked_id); + + -- -- Name: index_cart_item_event_reservations_on_customer_profile_id; Type: INDEX; Schema: public; Owner: - -- @@ -7655,6 +7726,14 @@ ALTER TABLE ONLY public.payment_schedules ADD CONSTRAINT fk_rails_00308dc223 FOREIGN KEY (wallet_transaction_id) REFERENCES public.wallet_transactions(id); +-- +-- Name: cart_item_event_reservation_booking_users fk_rails_0964335a37; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.cart_item_event_reservation_booking_users + ADD CONSTRAINT fk_rails_0964335a37 FOREIGN KEY (event_price_category_id) REFERENCES public.event_price_categories(id); + + -- -- Name: cart_item_free_extensions fk_rails_0d11862969; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -7927,6 +8006,14 @@ ALTER TABLE ONLY public.chained_elements ADD CONSTRAINT fk_rails_4fad806cca FOREIGN KEY (previous_id) REFERENCES public.chained_elements(id); +-- +-- Name: cart_item_event_reservation_booking_users fk_rails_5206c6ca4a; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.cart_item_event_reservation_booking_users + ADD CONSTRAINT fk_rails_5206c6ca4a FOREIGN KEY (cart_item_event_reservation_id) REFERENCES public.cart_item_event_reservations(id); + + -- -- Name: cart_item_event_reservation_tickets fk_rails_5307e8aab8; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -8963,6 +9050,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20230331132506'), ('20230509121907'), ('20230509161557'); +('20230510141305'); ('20230626122844'), ('20230626122947'); From 07b47d79563ac6e77f31de2948059aaf7316226e Mon Sep 17 00:00:00 2001 From: Du Peng Date: Thu, 11 May 2023 11:22:23 +0200 Subject: [PATCH 06/75] (wip) add event type --- app/controllers/api/events_controller.rb | 2 +- .../components/events/event-form.tsx | 26 ++++++++++++++----- .../src/javascript/controllers/events.js.erb | 2 +- app/frontend/src/javascript/models/event.ts | 3 ++- app/frontend/templates/events/show.html | 4 +-- app/models/event.rb | 2 ++ app/views/api/events/_event.json.jbuilder | 2 +- config/locales/app.admin.en.yml | 7 +++-- config/locales/app.admin.fr.yml | 7 +++-- ...9121907_add_booking_nominative_to_event.rb | 8 ------ ...rt_item_event_reservation_booking_users.rb | 3 ++- .../20230511081018_add_event_type_to_event.rb | 10 +++++++ db/structure.sql | 4 ++- test/fixtures/history_values.yml | 1 - .../components/events/event-form.test.tsx | 2 +- test/integration/events/as_admin_test.rb | 1 + 16 files changed, 55 insertions(+), 29 deletions(-) delete mode 100644 db/migrate/20230509121907_add_booking_nominative_to_event.rb create mode 100644 db/migrate/20230511081018_add_event_type_to_event.rb diff --git a/app/controllers/api/events_controller.rb b/app/controllers/api/events_controller.rb index 1b8abdc77..4d05f6b56 100644 --- a/app/controllers/api/events_controller.rb +++ b/app/controllers/api/events_controller.rb @@ -96,7 +96,7 @@ class API::EventsController < API::APIController # handle general properties event_preparams = params.required(:event).permit(:title, :description, :start_date, :start_time, :end_date, :end_time, :amount, :nb_total_places, :availability_id, :all_day, :recurrence, - :recurrence_end_at, :category_id, :event_theme_ids, :age_range_id, :booking_nominative, + :recurrence_end_at, :category_id, :event_theme_ids, :age_range_id, :event_type, event_theme_ids: [], event_image_attributes: %i[id attachment], event_files_attributes: %i[id attachment _destroy], diff --git a/app/frontend/src/javascript/components/events/event-form.tsx b/app/frontend/src/javascript/components/events/event-form.tsx index 033dff894..0a2ce1d8c 100644 --- a/app/frontend/src/javascript/components/events/event-form.tsx +++ b/app/frontend/src/javascript/components/events/event-form.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import * as React from 'react'; import { SubmitHandler, useFieldArray, useForm, useWatch } from 'react-hook-form'; -import { Event, EventDecoration, EventPriceCategoryAttributes, RecurrenceOption } from '../../models/event'; +import { Event, EventDecoration, EventPriceCategoryAttributes, RecurrenceOption, EventType } from '../../models/event'; import EventAPI from '../../api/event'; import { useTranslation } from 'react-i18next'; import { FormInput } from '../form/form-input'; @@ -40,7 +40,7 @@ interface EventFormProps { * Form to edit or create events */ export const EventForm: React.FC = ({ action, event, onError, onSuccess }) => { - const { handleSubmit, register, control, setValue, formState } = useForm({ defaultValues: { ...event } }); + const { handleSubmit, register, control, setValue, formState } = useForm({ defaultValues: Object.assign({ event_type: 'standard' }, event) }); const output = useWatch({ control }); const { fields, append, remove } = useFieldArray({ control, name: 'event_price_categories_attributes' }); @@ -168,6 +168,17 @@ export const EventForm: React.FC = ({ action, event, onError, on ]; }; + /** + * This method provides event type options + */ + const buildEventTypeOptions = (): Array> => { + return [ + { label: t('app.admin.event_form.event_types.standard'), value: 'standard' }, + { label: t('app.admin.event_form.event_types.nominative'), value: 'nominative' }, + { label: t('app.admin.event_form.event_types.family'), value: 'family' } + ]; + }; + return (
@@ -203,6 +214,12 @@ export const EventForm: React.FC = ({ action, event, onError, on label={t('app.admin.event_form.description')} limit={null} heading bulletList blockquote link video image /> + = ({ action, event, onError, on label={t('app.admin.event_form.seats_available')} type="number" tooltip={t('app.admin.event_form.seats_help')} /> - {{ 'app.public.events_show.ticket' | translate:{NUMBER:reserve.nbReservePlaces} }}
-
+
@@ -132,7 +132,7 @@ {{ 'app.public.events_show.ticket' | translate:{NUMBER:reserve.tickets[price.id]} }}
-
+
diff --git a/app/models/event.rb b/app/models/event.rb index 60bb3f70b..96ab48199 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -33,6 +33,8 @@ class Event < ApplicationRecord has_many :cart_item_event_reservations, class_name: 'CartItem::EventReservation', dependent: :destroy + validates :event_type, inclusion: { in: %w[standard nominative family] }, presence: true + attr_accessor :recurrence, :recurrence_end_at before_save :update_nb_free_places diff --git a/app/views/api/events/_event.json.jbuilder b/app/views/api/events/_event.json.jbuilder index 11be0748e..885a29d28 100644 --- a/app/views/api/events/_event.json.jbuilder +++ b/app/views/api/events/_event.json.jbuilder @@ -1,6 +1,6 @@ # frozen_string_literal: true -json.extract! event, :id, :title, :description, :booking_nominative +json.extract! event, :id, :title, :description, :event_type if event.event_image json.event_image_attributes do json.id event.event_image.id diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index 74d26c121..eca3e8e05 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -139,8 +139,6 @@ en: event_themes: "Event themes" age_range: "Age range" add_price: "Add a price" - booking_nominative: "Nominative booking" - booking_nominative_help: "If you check this option, the members will have to enter the names of the participants when booking." save: "Save" create_success: "The event was created successfully" events_updated: "{COUNT, plural, =1{One event was} other{{COUNT} Events were}} successfully updated" @@ -153,6 +151,11 @@ en: every_week: "Every week" every_month: "Every month" every_year: "Every year" + event_type: "Event type" + event_types: + standard: "Event standard" + nominative: "Event nominative" + family: "Event family" plan_form: ACTION_title: "{ACTION, select, create{New} other{Update the}} plan" tab_settings: "Settings" diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml index 34bbaed54..f477117d5 100644 --- a/config/locales/app.admin.fr.yml +++ b/config/locales/app.admin.fr.yml @@ -139,8 +139,6 @@ fr: event_themes: "Thèmes de l'événement" age_range: "Tranche d'âge" add_price: "Ajouter un tarif" - booking_nominative: "Réservation nominative" - booking_nominative_help: "Si cette option est activée, les réservations seront nominatives. Les participants devront s'identifier pour réserver." save: "Enregistrer" create_success: "L'événement a bien été créé" events_updated: "{COUNT, plural, one {}=1{Un événement à été} other{{COUNT} événements ont été}} mis à jour avec succès" @@ -153,6 +151,11 @@ fr: every_week: "Chaque semaine" every_month: "Chaque mois" every_year: "Chaque année" + event_type: "Type d'événement" + event_types: + standard: "Evénement standard" + nominative: "Evénement nominatif" + family: "Evénement famille" plan_form: ACTION_title: "{ACTION, select, create{Nouvelle} other{Mettre à jour la}} formule d'abonnement" tab_settings: "Paramètres" diff --git a/db/migrate/20230509121907_add_booking_nominative_to_event.rb b/db/migrate/20230509121907_add_booking_nominative_to_event.rb deleted file mode 100644 index 1a709170d..000000000 --- a/db/migrate/20230509121907_add_booking_nominative_to_event.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -# add booking_nominative to event -class AddBookingNominativeToEvent < ActiveRecord::Migration[7.0] - def change - add_column :events, :booking_nominative, :boolean, default: false - end -end diff --git a/db/migrate/20230510141305_create_cart_item_event_reservation_booking_users.rb b/db/migrate/20230510141305_create_cart_item_event_reservation_booking_users.rb index bbaae5626..e5d1a3465 100644 --- a/db/migrate/20230510141305_create_cart_item_event_reservation_booking_users.rb +++ b/db/migrate/20230510141305_create_cart_item_event_reservation_booking_users.rb @@ -5,7 +5,8 @@ class CreateCartItemEventReservationBookingUsers < ActiveRecord::Migration[7.0] def change create_table :cart_item_event_reservation_booking_users do |t| t.string :name - t.belongs_to :cart_item_event_reservation, foreign_key: true, index: { name: 'index_cart_item_booking_users_on_cart_item_event_reservation' } + t.belongs_to :cart_item_event_reservation, foreign_key: true, + index: { name: 'index_cart_item_booking_users_on_cart_item_event_reservation' } t.references :event_price_category, foreign_key: true, index: { name: 'index_cart_item_booking_users_on_event_price_category' } t.references :booked, polymorphic: true diff --git a/db/migrate/20230511081018_add_event_type_to_event.rb b/db/migrate/20230511081018_add_event_type_to_event.rb new file mode 100644 index 000000000..4f1f832a9 --- /dev/null +++ b/db/migrate/20230511081018_add_event_type_to_event.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# Add event_type to event model, to be able to create standard/nominative/family events +class AddEventTypeToEvent < ActiveRecord::Migration[7.0] + def change + add_column :events, :event_type, :string, default: 'standard' + Event.reset_column_information + Event.update_all(event_type: 'standard') + end +end diff --git a/db/structure.sql b/db/structure.sql index b77f0a07c..d1215001f 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -1236,7 +1236,7 @@ CREATE TABLE public.events ( age_range_id integer, category_id integer, deleted_at timestamp without time zone, - booking_nominative boolean DEFAULT false + event_type character varying DEFAULT 'standard'::character varying ); @@ -9051,6 +9051,8 @@ INSERT INTO "schema_migrations" (version) VALUES ('20230509121907'), ('20230509161557'); ('20230510141305'); +('20230511080650'), +('20230511081018'); ('20230626122844'), ('20230626122947'); diff --git a/test/fixtures/history_values.yml b/test/fixtures/history_values.yml index 645e05f9a..5738b7399 100644 --- a/test/fixtures/history_values.yml +++ b/test/fixtures/history_values.yml @@ -889,5 +889,4 @@ history_value_105: value: 'false' created_at: '2023-03-31 14:38:40.000421' updated_at: '2023-03-31 14:38:40.000421' - footprint: invoicing_profile_id: 1 diff --git a/test/frontend/components/events/event-form.test.tsx b/test/frontend/components/events/event-form.test.tsx index 49f50ce79..219542a23 100644 --- a/test/frontend/components/events/event-form.test.tsx +++ b/test/frontend/components/events/event-form.test.tsx @@ -15,6 +15,7 @@ describe('EventForm', () => { expect(screen.getByLabelText(/app.admin.event_form.title/)).toBeInTheDocument(); expect(screen.getByLabelText(/app.admin.event_form.matching_visual/)).toBeInTheDocument(); expect(screen.getByLabelText(/app.admin.event_form.description/)).toBeInTheDocument(); + expect(screen.getByLabelText(/app.admin.event_form.event_type/)).toBeInTheDocument(); expect(screen.getByLabelText(/app.admin.event_form.event_category/)).toBeInTheDocument(); expect(screen.getByLabelText(/app.admin.event_form.event_themes/)).toBeInTheDocument(); expect(screen.getByLabelText(/app.admin.event_form.age_range/)).toBeInTheDocument(); @@ -27,7 +28,6 @@ describe('EventForm', () => { expect(screen.getByLabelText(/app.admin.event_form._and_ends_on/)).toBeInTheDocument(); expect(screen.getByLabelText(/app.admin.event_form.seats_available/)).toBeInTheDocument(); expect(screen.getByLabelText(/app.admin.event_form.standard_rate/)).toBeInTheDocument(); - expect(screen.getByLabelText(/app.admin.event_form.booking_nominative/)).toBeInTheDocument(); expect(screen.getByRole('button', { name: /app.admin.event_form.add_price/ })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /app.admin.event_form.add_a_new_file/ })).toBeInTheDocument(); expect(screen.getByLabelText(/app.admin.advanced_accounting_form.code/)).toBeInTheDocument(); diff --git a/test/integration/events/as_admin_test.rb b/test/integration/events/as_admin_test.rb index e4d721ed4..5d59eef64 100644 --- a/test/integration/events/as_admin_test.rb +++ b/test/integration/events/as_admin_test.rb @@ -22,6 +22,7 @@ class Events::AsAdminTest < ActionDispatch::IntegrationTest end_date: 1.week.from_now.utc, end_time: 1.week.from_now.utc.change(hour: 20), category_id: Category.first.id, + event_type: 'standard', amount: 0 } }.to_json, From e85e2d7b472ba3cf11fecd3ed9359d5a6a873b9c Mon Sep 17 00:00:00 2001 From: Du Peng Date: Mon, 15 May 2023 16:42:01 +0200 Subject: [PATCH 07/75] (wip) pay family and nominative event --- app/controllers/api/children_controller.rb | 5 +- .../components/events/event-form.tsx | 13 +- .../src/javascript/controllers/events.js.erb | 164 ++++++++++++++---- app/frontend/src/javascript/services/child.js | 11 ++ app/frontend/templates/events/show.html | 34 +++- app/policies/child_policy.rb | 7 - 6 files changed, 184 insertions(+), 50 deletions(-) create mode 100644 app/frontend/src/javascript/services/child.js diff --git a/app/controllers/api/children_controller.rb b/app/controllers/api/children_controller.rb index f923f2bb3..618d859fb 100644 --- a/app/controllers/api/children_controller.rb +++ b/app/controllers/api/children_controller.rb @@ -7,7 +7,10 @@ class API::ChildrenController < API::APIController before_action :set_child, only: %i[show update destroy] def index - @children = policy_scope(Child) + authorize Child + user_id = current_user.id + user_id = params[:user_id] if current_user.privileged? && params[:user_id] + @children = Child.where(user_id: user_id) end def show diff --git a/app/frontend/src/javascript/components/events/event-form.tsx b/app/frontend/src/javascript/components/events/event-form.tsx index 0a2ce1d8c..5ed6a7036 100644 --- a/app/frontend/src/javascript/components/events/event-form.tsx +++ b/app/frontend/src/javascript/components/events/event-form.tsx @@ -54,6 +54,7 @@ export const EventForm: React.FC = ({ action, event, onError, on const [isOpenRecurrentModal, setIsOpenRecurrentModal] = useState(false); const [updatingEvent, setUpdatingEvent] = useState(null); const [isActiveAccounting, setIsActiveAccounting] = useState(false); + const [isActiveFamilyAccount, setIsActiveFamilyAccount] = useState(false); useEffect(() => { EventCategoryAPI.index() @@ -69,6 +70,7 @@ export const EventForm: React.FC = ({ action, event, onError, on .then(data => setPriceCategoriesOptions(data.map(c => decorationToOption(c)))) .catch(onError); SettingAPI.get('advanced_accounting').then(res => setIsActiveAccounting(res.value === 'true')).catch(onError); + SettingAPI.get('family_account').then(res => setIsActiveFamilyAccount(res.value === 'true')).catch(onError); }, []); useEffect(() => { @@ -172,11 +174,14 @@ export const EventForm: React.FC = ({ action, event, onError, on * This method provides event type options */ const buildEventTypeOptions = (): Array> => { - return [ - { label: t('app.admin.event_form.event_types.standard'), value: 'standard' }, - { label: t('app.admin.event_form.event_types.nominative'), value: 'nominative' }, - { label: t('app.admin.event_form.event_types.family'), value: 'family' } + const options = [ + { label: t('app.admin.event_form.event_types.standard'), value: 'standard' as EventType }, + { label: t('app.admin.event_form.event_types.nominative'), value: 'nominative' as EventType } ]; + if (isActiveFamilyAccount) { + options.push({ label: t('app.admin.event_form.event_types.family'), value: 'family' as EventType }); + } + return options; }; return ( diff --git a/app/frontend/src/javascript/controllers/events.js.erb b/app/frontend/src/javascript/controllers/events.js.erb index 32e266bfe..5782994be 100644 --- a/app/frontend/src/javascript/controllers/events.js.erb +++ b/app/frontend/src/javascript/controllers/events.js.erb @@ -136,8 +136,8 @@ Application.Controllers.controller('EventsController', ['$scope', '$state', 'Eve } ]); -Application.Controllers.controller('ShowEventController', ['$scope', '$state', '$rootScope', 'Event', '$uibModal', 'Member', 'Reservation', 'Price', 'CustomAsset', 'SlotsReservation', 'eventPromise', 'growl', '_t', 'Wallet', 'AuthService', 'helpers', 'dialogs', 'priceCategoriesPromise', 'settingsPromise', 'LocalPayment', - function ($scope, $state,$rootScope, Event, $uibModal, Member, Reservation, Price, CustomAsset, SlotsReservation, eventPromise, growl, _t, Wallet, AuthService, helpers, dialogs, priceCategoriesPromise, settingsPromise, LocalPayment) { +Application.Controllers.controller('ShowEventController', ['$scope', '$state', '$rootScope', 'Event', '$uibModal', 'Member', 'Reservation', 'Price', 'CustomAsset', 'SlotsReservation', 'eventPromise', 'growl', '_t', 'Wallet', 'AuthService', 'helpers', 'dialogs', 'priceCategoriesPromise', 'settingsPromise', 'LocalPayment', 'Child', + function ($scope, $state,$rootScope, Event, $uibModal, Member, Reservation, Price, CustomAsset, SlotsReservation, eventPromise, growl, _t, Wallet, AuthService, helpers, dialogs, priceCategoriesPromise, settingsPromise, LocalPayment, Child) { /* PUBLIC SCOPE */ // reservations for the currently shown event @@ -150,6 +150,9 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' $scope.ctrl = { member: {} }; + // children for the member + $scope.children = []; + // parameters for a new reservation $scope.reserve = { nbPlaces: { @@ -226,22 +229,12 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' }); }; - const hasMemberInBookingUsers = function () { - const keys = Object.keys($scope.reserve.bookingUsers); - for (const key of keys) { - if ($scope.reserve.bookingUsers[key].find(u => u.booked_id === $scope.ctrl.member.id && u.booked_type === 'User')) { - return true; - } - } - return false; - }; - /** * Callback to call when the number of tickets to book changes in the current booking */ $scope.changeNbPlaces = function (priceType) { // compute the total remaining places - let remain = $scope.event.nb_free_places - $scope.reserve.nbReservePlaces; + let remain = ($scope.event.event_type === 'family' ? ($scope.children.length + 1) : $scope.event.nb_free_places) - $scope.reserve.nbReservePlaces; for (let ticket in $scope.reserve.tickets) { remain -= $scope.reserve.tickets[ticket]; } @@ -260,36 +253,41 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' } } - const nbBookingUsers = $scope.reserve.bookingUsers[priceType].length; - const nbReservePlaces = priceType === 'normal' ? $scope.reserve.nbReservePlaces : $scope.reserve.tickets[priceType]; - if (nbReservePlaces > nbBookingUsers) { - _.times(nbReservePlaces - nbBookingUsers, () => { - /* - if (!hasMemberInBookingUsers()) { - $scope.reserve.bookingUsers[priceType].push({ event_price_category_id: priceType === 'normal' ? null : priceType, booked_id: $scope.ctrl.member.id, booked_type: 'User', name: $scope.ctrl.member.name }); - } else { - $scope.reserve.bookingUsers[priceType].push({ event_price_category_id: priceType === 'normal' ? null : priceType }); - } - */ - $scope.reserve.bookingUsers[priceType].push({ event_price_category_id: priceType === 'normal' ? null : priceType }); - }); - } else { - _.times(nbBookingUsers - nbReservePlaces, () => { - $scope.reserve.bookingUsers[priceType].pop(); - }); + if ($scope.event.event_type === 'nominative' || $scope.event.event_type === 'family') { + const nbBookingUsers = $scope.reserve.bookingUsers[priceType].length; + const nbReservePlaces = priceType === 'normal' ? $scope.reserve.nbReservePlaces : $scope.reserve.tickets[priceType]; + if (nbReservePlaces > nbBookingUsers) { + _.times(nbReservePlaces - nbBookingUsers, () => { + $scope.reserve.bookingUsers[priceType].push({ event_price_category_id: priceType === 'normal' ? null : priceType, bookedUsers: buildBookedUsersOptions() }); + }); + } else { + _.times(nbBookingUsers - nbReservePlaces, () => { + $scope.reserve.bookingUsers[priceType].pop(); + }); + } } // recompute the total price return $scope.computeEventAmount(); }; + $scope.changeBookedUser = function () { + for (const key of Object.keys($scope.reserve.bookingUsers)) { + for (const user of $scope.reserve.bookingUsers[key]) { + user.bookedUsers = buildBookedUsersOptions(user.booked); + } + } + } + /** * Callback to reset the current reservation parameters * @param e {Object} see https://docs.angularjs.org/guide/expression#-event- */ $scope.cancelReserve = function (e) { e.preventDefault(); - return resetEventReserve(); + resetEventReserve(); + updateNbReservePlaces(); + return; }; $scope.isUserValidatedByType = () => { @@ -354,6 +352,9 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' Member.get({ id: $scope.ctrl.member.id }, function (member) { $scope.ctrl.member = member; getReservations($scope.event.id, 'Event', $scope.ctrl.member.id); + getChildren($scope.ctrl.member.id).then(() => { + updateNbReservePlaces(); + }); }); } }; @@ -615,6 +616,31 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' growl.error(message); }; + /** + * Checks if the reservation of current event is valid + */ + $scope.reservationIsValid = () => { + if ($scope.event.event_type === 'nominative') { + for (const key of Object.keys($scope.reserve.bookingUsers)) { + for (const user of $scope.reserve.bookingUsers[key]) { + if (!_.trim(user.name)) { + return false; + } + } + } + } + if ($scope.event.event_type === 'family') { + for (const key of Object.keys($scope.reserve.bookingUsers)) { + for (const user of $scope.reserve.bookingUsers[key]) { + if (!user.booked) { + return false; + } + } + } + } + return true; + } + /* PRIVATE SCOPE */ /** @@ -634,6 +660,9 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' // get the current user's reservations into $scope.reservations if ($scope.currentUser) { getReservations($scope.event.id, 'Event', $scope.currentUser.id); + getChildren($scope.currentUser.id).then(function (children) { + updateNbReservePlaces(); + }); } // watch when a coupon is applied to re-compute the total price @@ -658,6 +687,72 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' }).$promise.then(function (reservations) { $scope.reservations = reservations; }); }; + /** + * Retrieve the children for the user + * @param user_id {number} the user's id (current or managed) + */ + const getChildren = function (user_id) { + return Child.query({ + user_id + }).$promise.then(function (children) { + $scope.children = children; + return $scope.children; + }); + }; + + /** + * Update the number of places reserved by the current user + */ + const hasBookedUser = function (userKey) { + for (const key of Object.keys($scope.reserve.bookingUsers)) { + for (const user of $scope.reserve.bookingUsers[key]) { + if (user.booked && user.booked.key === userKey) { + return true; + } + } + } + return false; + }; + + /** + * Build the list of options for the select box of the booked users + * @param booked {object} the booked user + */ + const buildBookedUsersOptions = function (booked) { + const options = []; + const userKey = `user_${$scope.ctrl.member.id}`; + if ((booked && booked.key === userKey) || !hasBookedUser(userKey)) { + options.push({ key: userKey, name: $scope.ctrl.member.name, type: 'User', id: $scope.ctrl.member.id }); + } + for (const child of $scope.children) { + const key = `child_${child.id}`; + if ((booked && booked.key === key) || !hasBookedUser(key)) { + options.push({ + key, + name: child.first_name + ' ' + child.last_name, + id: child.id, + type: 'Child' + }); + } + } + return options; + }; + + /** + * update number of places available for each price category for the family event + */ + const updateNbReservePlaces = function () { + if ($scope.event.event_type === 'family') { + const maxPlaces = $scope.children.length + 1; + if ($scope.event.nb_free_places > maxPlaces) { + $scope.reserve.nbPlaces.normal = __range__(0, maxPlaces, true); + for (let evt_px_cat of Array.from($scope.event.event_price_categories_attributes)) { + $scope.reserve.nbPlaces[evt_px_cat.id] = __range__(0, maxPlaces, true); + } + } + } + }; + /** * Create a hash map implementing the Reservation specs * @param reserve {Object} Reservation parameters (places...) @@ -694,9 +789,9 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' for (const user of $scope.reserve.bookingUsers[key]) { reservation.booking_users_attributes.push({ event_price_category_id: user.event_price_category_id, - name: user.name, - booked_id: user.booked_id, - booked_type: user.booked_type + name: user.booked ? user.booked.name : _.trim(user.name), + booked_id: user.booked ? user.booked.id : undefined, + booked_type: user.booked ? user.booked.type : undefined, }); } } @@ -865,6 +960,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' $scope.reservations.push(reservation); }); resetEventReserve(); + updateNbReservePlaces(); $scope.reserveSuccess = true; $scope.coupon.applied = null; if ($scope.currentUser.role === 'admin') { diff --git a/app/frontend/src/javascript/services/child.js b/app/frontend/src/javascript/services/child.js new file mode 100644 index 000000000..07d0d916c --- /dev/null +++ b/app/frontend/src/javascript/services/child.js @@ -0,0 +1,11 @@ +'use strict'; + +Application.Services.factory('Child', ['$resource', function ($resource) { + return $resource('/api/children/:id', + { id: '@id' }, { + update: { + method: 'PUT' + } + } + ); +}]); diff --git a/app/frontend/templates/events/show.html b/app/frontend/templates/events/show.html index 1da00b830..72cd5aa07 100644 --- a/app/frontend/templates/events/show.html +++ b/app/frontend/templates/events/show.html @@ -122,7 +122,20 @@
- + +
+
+
+
+ +
@@ -135,7 +148,20 @@
- + +
+
+
+
+ +
@@ -202,11 +228,11 @@
-
@@ -174,6 +180,12 @@ {{ 'app.shared.cart.child_validation_required_alert' }}

+ +

+ + {{ 'app.shared.cart.child_birthday_must_be_under_18_years_ago_alert' }} +

+
diff --git a/config/locales/app.shared.en.yml b/config/locales/app.shared.en.yml index be26b070d..8872fea25 100644 --- a/config/locales/app.shared.en.yml +++ b/config/locales/app.shared.en.yml @@ -374,6 +374,7 @@ en: no_tags: "No tags" user_validation_required_alert: "Warning!
Your administrator must validate your account. Then, you'll then be able to access all the booking features." child_validation_required_alert: "Warning!
Your administrator must validate your child account. Then, you'll then be able to book the event." + child_birthday_must_be_under_18_years_ago_alert: "Warning!
Your child must be under 18 years ago. Then, you'll then be able to book the event." # feature-tour modal tour: previous: "Previous" diff --git a/config/locales/app.shared.fr.yml b/config/locales/app.shared.fr.yml index 2338a1e1e..573f1cfe5 100644 --- a/config/locales/app.shared.fr.yml +++ b/config/locales/app.shared.fr.yml @@ -374,6 +374,7 @@ fr: no_tags: "Aucune étiquette" user_validation_required_alert: "Attention !
Votre administrateur doit valider votre compte. Vous pourrez alors accéder à l'ensemble des fonctionnalités de réservation." child_validation_required_alert: "Attention !
Votre administrateur doit valider votre compte enfant. Vous pourrez alors réserver l'événement." + child_birthday_must_be_under_18_years_ago_alert: "Attention !
La date de naissance de l'enfant doit être inférieure à 18 ans. Vous pourrez alors réserver l'événement." #feature-tour modal tour: previous: "Précédent" From 78d8b3bfe6d71b0e9c89762d57b101691273ef84 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Mon, 29 May 2023 15:15:32 +0200 Subject: [PATCH 20/75] (feat) show event reservation booking users in events of member dashbaord --- app/frontend/templates/dashboard/events.html | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/frontend/templates/dashboard/events.html b/app/frontend/templates/dashboard/events.html index 1783502d7..9af91bd8f 100644 --- a/app/frontend/templates/dashboard/events.html +++ b/app/frontend/templates/dashboard/events.html @@ -26,12 +26,20 @@ translate-values="{NUMBER: r.nb_reserve_places}"> {{ 'app.logged.dashboard.events.NUMBER_normal_places_reserved' }} + +
+ {{bu.name}} +

{{ 'app.logged.dashboard.events.NUMBER_of_NAME_places_reserved' }} + +
+ {{bu.name}} +
From e7bac208a790d8f55b98eda8c8b160668f13dc1c Mon Sep 17 00:00:00 2001 From: Du Peng Date: Mon, 29 May 2023 19:32:51 +0200 Subject: [PATCH 21/75] (feat) child's supporting document file created/updated notification --- app/services/child_service.rb | 27 ++++++++++++++++++- ...rting_document_files_created.json.jbuilder | 3 +++ ...rting_document_files_updated.json.jbuilder | 5 ++++ ...supporting_document_files_created.html.erb | 18 +++++++++++++ ...supporting_document_files_updated.html.erb | 19 +++++++++++++ config/locales/app.logged.en.yml | 2 ++ config/locales/app.logged.fr.yml | 2 ++ config/locales/en.yml | 4 +++ config/locales/fr.yml | 4 +++ config/locales/mails.en.yml | 10 +++++++ config/locales/mails.fr.yml | 10 +++++++ db/seeds/notification_types.rb | 16 +++++++++++ 12 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 app/views/api/notifications/_notify_admin_user_child_supporting_document_files_created.json.jbuilder create mode 100644 app/views/api/notifications/_notify_admin_user_child_supporting_document_files_updated.json.jbuilder create mode 100644 app/views/notifications_mailer/notify_admin_user_child_supporting_document_files_created.html.erb create mode 100644 app/views/notifications_mailer/notify_admin_user_child_supporting_document_files_updated.html.erb diff --git a/app/services/child_service.rb b/app/services/child_service.rb index e589a9f54..45ebd1a4c 100644 --- a/app/services/child_service.rb +++ b/app/services/child_service.rb @@ -7,13 +7,38 @@ class ChildService NotificationCenter.call type: 'notify_admin_child_created', receiver: User.admins_and_managers, attached_object: child + all_files_are_upload = true + SupportingDocumentType.where(document_type: 'Child').each do |sdt| + file = sdt.supporting_document_files.find_by(supportable: child) + all_files_are_upload = false if file.nil? || file.attachment_identifier.nil? + end + if all_files_are_upload + NotificationCenter.call type: 'notify_admin_user_child_supporting_document_files_created', + receiver: User.admins_and_managers, + attached_object: child + end + return true end false end def self.update(child, child_params) - child.update(child_params) + if child.update(child_params) + all_files_are_upload = true + SupportingDocumentType.where(document_type: 'Child').each do |sdt| + file = sdt.supporting_document_files.find_by(supportable: child) + all_files_are_upload = false if file.nil? || file.attachment_identifier.nil? + end + if all_files_are_upload + NotificationCenter.call type: 'notify_admin_user_child_supporting_document_files_updated', + receiver: User.admins_and_managers, + attached_object: child + end + + return true + end + false end def self.validate(child, is_valid) diff --git a/app/views/api/notifications/_notify_admin_user_child_supporting_document_files_created.json.jbuilder b/app/views/api/notifications/_notify_admin_user_child_supporting_document_files_created.json.jbuilder new file mode 100644 index 000000000..4a16e4502 --- /dev/null +++ b/app/views/api/notifications/_notify_admin_user_child_supporting_document_files_created.json.jbuilder @@ -0,0 +1,3 @@ +json.title notification.notification_type +json.description t('.supporting_document_files_uploaded', + NAME: notification.attached_object&.full_name || t('api.notifications.deleted_user')) diff --git a/app/views/api/notifications/_notify_admin_user_child_supporting_document_files_updated.json.jbuilder b/app/views/api/notifications/_notify_admin_user_child_supporting_document_files_updated.json.jbuilder new file mode 100644 index 000000000..5b97b34ea --- /dev/null +++ b/app/views/api/notifications/_notify_admin_user_child_supporting_document_files_updated.json.jbuilder @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +json.title notification.notification_type +json.description t('.supporting_document_files_uploaded', + NAME: notification.attached_object&.full_name || t('api.notifications.deleted_user')) diff --git a/app/views/notifications_mailer/notify_admin_user_child_supporting_document_files_created.html.erb b/app/views/notifications_mailer/notify_admin_user_child_supporting_document_files_created.html.erb new file mode 100644 index 000000000..239915785 --- /dev/null +++ b/app/views/notifications_mailer/notify_admin_user_child_supporting_document_files_created.html.erb @@ -0,0 +1,18 @@ +<%= render 'notifications_mailer/shared/hello', recipient: @recipient %> + +

+ <%= t('.body.supporting_document_files_uploaded_below', + NAME: @attached_object&.full_name || t('api.notifications.deleted_user')) %> +

+
    +<% if @attached_object %> + <% SupportingDocumentType.where(document_type: 'Child').each do |type| %> +
  • <%= type.name %>
  • + <% end %> +<% end %> +
+<% if Setting.get('child_validation_required') %> +

+ <%= t('.body.validate_child') %> +

+<% end %> diff --git a/app/views/notifications_mailer/notify_admin_user_child_supporting_document_files_updated.html.erb b/app/views/notifications_mailer/notify_admin_user_child_supporting_document_files_updated.html.erb new file mode 100644 index 000000000..00355c465 --- /dev/null +++ b/app/views/notifications_mailer/notify_admin_user_child_supporting_document_files_updated.html.erb @@ -0,0 +1,19 @@ +<%= render 'notifications_mailer/shared/hello', recipient: @recipient %> + +

+ <%= t('.body.child_update_supporting_document_file', + NAME: @attached_object&.full_name || t('api.notifications.deleted_user')) %> +

+
    +<% if @attached_object %> + <% SupportingDocumentType.where(document_type: 'Child').each do |type| %> +
  • <%= type.name %>
  • + <% end %> +<% end %> +
+ +<% if Setting.get('child_validation_required') %> +

+ <%= t('.body.validate_child') %> +

+<% end %> diff --git a/config/locales/app.logged.en.yml b/config/locales/app.logged.en.yml index 69afbfbd7..f284fbc5d 100644 --- a/config/locales/app.logged.en.yml +++ b/config/locales/app.logged.en.yml @@ -295,6 +295,8 @@ en: notify_admin_user_child_supporting_document_refusal: "A supporting document of child has been rejected" notify_admin_user_supporting_document_files_created: "A user has uploaded a supporting document" notify_admin_user_supporting_document_files_updated: "A user has updated a supporting document" + notify_admin_user_child_supporting_document_files_created: "A child has uploaded a supporting document" + notify_admin_user_child_supporting_document_files_updated: "A child has updated a supporting document" notify_admin_member_create_reservation: "A member books a reservation" notify_admin_slot_is_modified: "A reservation slot has been modified" notify_admin_slot_is_canceled: "A reservation has been cancelled" diff --git a/config/locales/app.logged.fr.yml b/config/locales/app.logged.fr.yml index 0dc9c03f0..24550f9d1 100644 --- a/config/locales/app.logged.fr.yml +++ b/config/locales/app.logged.fr.yml @@ -295,6 +295,8 @@ fr: notify_admin_user_child_supporting_document_refusal: "Un justificatif de l'enfant a été refusé" notify_admin_user_supporting_document_files_created: "Un utilisateur a téléversé un justificatif" notify_admin_user_supporting_document_files_updated: "Un utilisateur a mis à jour un justificatif" + notify_admin_user_child_supporting_document_files_created: "Un enfant a téléversé un justificatif" + notify_admin_user_child_supporting_document_files_updated: "Un enfant a mis à jour un justificatif" notify_admin_member_create_reservation: "Un membre fait une réservation" notify_admin_slot_is_modified: "Un créneau de réservation a été modifié" notify_admin_slot_is_canceled: "Une réservation a été annulée" diff --git a/config/locales/en.yml b/config/locales/en.yml index ff00e33d7..7646770b6 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -443,6 +443,10 @@ en: supporting_document_files_uploaded: "Supporting document uploaded by member %{NAME}." notify_admin_user_supporting_document_files_updated: supporting_document_files_uploaded: "Supporting document changed by member %{NAME}." + notify_admin_user_child_supporting_document_files_created: + supporting_document_files_uploaded: "Child's %{NAME} supporting document uploaded." + notify_admin_user_child_supporting_document_files_updated: + supporting_document_files_uploaded: "Supporting document changed by child %{NAME}." notify_user_is_validated: account_validated: "Your account is valid." notify_user_is_invalidated: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 03d0aa8e5..ec3c61adc 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -443,6 +443,10 @@ fr: supporting_document_files_uploaded: "Le membre %{NAME} a téléversé un nouveau justificatif." notify_admin_user_supporting_document_files_updated: supporting_document_files_uploaded: "Le membre %{NAME} a modifié un justificatif." + notify_admin_user_child_supporting_document_files_created: + supporting_document_files_uploaded: "L'enfant %{NAME} a téléversé un nouveau justificatif." + notify_admin_user_child_supporting_document_files_updated: + supporting_document_files_uploaded: "L'enfant %{NAME} a modifié un justificatif." notify_user_is_validated: account_validated: "Votre compte est valide." notify_user_is_invalidated: diff --git a/config/locales/mails.en.yml b/config/locales/mails.en.yml index bf2c74d02..0d6ca6a84 100644 --- a/config/locales/mails.en.yml +++ b/config/locales/mails.en.yml @@ -393,6 +393,16 @@ en: body: user_update_supporting_document_file: "Member %{NAME} has modified the supporting documents below:" validate_user: "Please validate this account" + notify_admin_user_child_supporting_document_files_created: + subject: "Supporting documents of child uploaded by a member" + body: + supporting_document_files_uploaded_below: "Child %{NAME} has uploaded the following supporting documents:" + validate_child: "Please validate this child account" + notify_admin_user_child_supporting_document_files_updated: + subject: "Child's supporting documents have changed" + body: + child_update_supporting_document_file: "Child %{NAME} has modified the supporting documents below:" + validate_child: "Please validate this child account" notify_user_is_validated: subject: "Account validated" body: diff --git a/config/locales/mails.fr.yml b/config/locales/mails.fr.yml index 69e10ab97..60bfe3be1 100644 --- a/config/locales/mails.fr.yml +++ b/config/locales/mails.fr.yml @@ -393,6 +393,16 @@ fr: body: user_update_supporting_document_file: "Le membre %{NAME} a modifié le justificatif ci-dessous :" validate_user: "Veuillez valider son compte" + notify_admin_user_child_supporting_document_files_created: + subject: "Justificatif de l'enfant téléversé par un membre" + body: + supporting_document_files_uploaded_below: "L'enfant %{NAME} a téléversé le justificatif suivant :" + validate_child: "Veuillez valider son compte enfant" + notify_admin_user_child_supporting_document_files_updated: + subject: "Le justificatif d'un enfant a changé" + body: + child_update_supporting_document_file: "L'enfant %{NAME} a modifié le justificatif ci-dessous :" + validate_child: "Veuillez valider son compte enfant" notify_user_is_validated: subject: "Compte validé" body: diff --git a/db/seeds/notification_types.rb b/db/seeds/notification_types.rb index 635b6cd6f..2ef8f21ae 100644 --- a/db/seeds/notification_types.rb +++ b/db/seeds/notification_types.rb @@ -127,3 +127,19 @@ unless NotificationType.find_by(name: 'notify_user_child_is_invalidated') is_configurable: false ) end + +unless NotificationType.find_by(name: 'notify_admin_user_child_supporting_document_files_updated') + NotificationType.create!( + name: 'notify_admin_user_child_supporting_document_files_updated', + category: 'supporting_documents', + is_configurable: true + ) +end + +unless NotificationType.find_by(name: 'notify_admin_user_child_supporting_document_files_created') + NotificationType.create!( + name: 'notify_admin_user_child_supporting_document_files_created', + category: 'supporting_documents', + is_configurable: true + ) +end From f64e76a4631c9af20870817115dc4d7e45c68f45 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Tue, 30 May 2023 10:43:27 +0200 Subject: [PATCH 22/75] (bug) child validated_at type --- app/frontend/src/javascript/models/child.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/frontend/src/javascript/models/child.ts b/app/frontend/src/javascript/models/child.ts index bc00f1fb1..b9fcaca9a 100644 --- a/app/frontend/src/javascript/models/child.ts +++ b/app/frontend/src/javascript/models/child.ts @@ -1,4 +1,4 @@ -import { TDateISODate } from '../typings/date-iso'; +import { TDateISODate, TDateISO } from '../typings/date-iso'; import { ApiFilter } from './api'; export interface ChildIndexFilter extends ApiFilter { @@ -13,7 +13,7 @@ export interface Child { phone?: string, birthday: TDateISODate, user_id: number, - validated_at?: TDateISODate, + validated_at?: TDateISO, supporting_document_files_attributes?: Array<{ id?: number, supportable_id?: number, From d4b2b16425210ac764b75e5d8935862da0aa1469 Mon Sep 17 00:00:00 2001 From: Vincent Date: Wed, 31 May 2023 11:29:10 +0200 Subject: [PATCH 23/75] (ui) integration --- .../components/family-account/child-form.tsx | 167 ++++++++++-------- .../components/family-account/child-item.tsx | 31 ++-- ...ildren-list.tsx => children-dashboard.tsx} | 28 +-- .../components/form/form-file-upload.tsx | 7 +- .../supporting-documents-types-list.tsx | 59 +++---- .../components/user/members-list-item.tsx | 102 +++++++++++ .../components/user/members-list.tsx | 37 ++++ app/frontend/src/javascript/models/member.ts | 47 +++++ app/frontend/src/stylesheets/application.scss | 5 +- .../modules/base/edit-destroy-buttons.scss | 3 + .../stylesheets/modules/base/fab-modal.scss | 1 + .../modules/family-account/child-form.scss | 43 +++++ .../modules/family-account/child-item.scss | 62 ++++++- .../family-account/children-dashboard.scss | 20 +++ .../modules/form/form-file-upload.scss | 2 + .../src/stylesheets/modules/members.scss | 105 +++++++++++ .../supporting-documents-types-list.scss | 35 ++-- .../modules/user/user-validation.scss | 4 + .../templates/admin/members/edit.html | 2 +- .../templates/admin/members/members.html | 47 +---- .../templates/dashboard/children.html | 2 +- app/frontend/templates/events/show.html | 19 +- .../templates/shared/_member_select.html | 9 +- config/locales/app.admin.en.yml | 10 +- config/locales/app.public.en.yml | 18 +- config/locales/app.public.fr.yml | 2 +- config/locales/app.shared.en.yml | 9 +- 27 files changed, 654 insertions(+), 222 deletions(-) rename app/frontend/src/javascript/components/family-account/{children-list.tsx => children-dashboard.tsx} (76%) create mode 100644 app/frontend/src/javascript/components/user/members-list-item.tsx create mode 100644 app/frontend/src/javascript/components/user/members-list.tsx create mode 100644 app/frontend/src/javascript/models/member.ts create mode 100644 app/frontend/src/stylesheets/modules/family-account/child-form.scss create mode 100644 app/frontend/src/stylesheets/modules/family-account/children-dashboard.scss diff --git a/app/frontend/src/javascript/components/family-account/child-form.tsx b/app/frontend/src/javascript/components/family-account/child-form.tsx index 02234f827..bc7ab29c1 100644 --- a/app/frontend/src/javascript/components/family-account/child-form.tsx +++ b/app/frontend/src/javascript/components/family-account/child-form.tsx @@ -10,6 +10,7 @@ import { FileType } from '../../models/file'; import { SupportingDocumentType } from '../../models/supporting-document-type'; import { User } from '../../models/user'; import { SupportingDocumentsRefusalModal } from '../supporting-documents/supporting-documents-refusal-modal'; +import { FabAlert } from '../base/fab-alert'; interface ChildFormProps { child: Child; @@ -65,92 +66,114 @@ export const ChildForm: React.FC = ({ child, onSubmit, supportin return (
{!isPrivileged() && -
- {t('app.public.child_form.child_form_info')} -
+ +

{t('app.public.child_form.child_form_info')}

+
}
- - - moment(value).isAfter(moment().subtract(18, 'year')) }} - formState={formState} - label={t('app.public.child_form.birthday')} - type="date" - max={moment().format('YYYY-MM-DD')} - min={moment().subtract(18, 'year').format('YYYY-MM-DD')} - /> - +
+ + +
+
+ moment(value).isAfter(moment().subtract(18, 'year')) }} + formState={formState} + label={t('app.public.child_form.birthday')} + type="date" + max={moment().format('YYYY-MM-DD')} + min={moment().subtract(18, 'year').format('YYYY-MM-DD')} + /> + +
- {output.supporting_document_files_attributes.map((sf, index) => { - if (isPrivileged()) { + + {!isPrivileged() && <> +

{t('app.public.child_form.supporting_documents')}

+ {output.supporting_document_files_attributes.map((sf, index) => { return ( -
-
{getSupportingDocumentsTypeName(sf.supporting_document_type_id)}
- {sf.attachment_url && ( - - {sf.attachment} - - - )} - {!sf.attachment_url && ( -
{t('app.public.child_form.to_complete')}
- )} -
+ ); - } - return ( - - ); - })} + })} + }
- + {t('app.public.child_form.save')} - {isPrivileged() && -
- {t('app.public.child_form.refuse_documents')} - -
- }
+ + {isPrivileged() && <> +

{t('app.public.child_form.supporting_documents')}

+
+ {output.supporting_document_files_attributes.map((sf, index) => { + return ( +
+ {getSupportingDocumentsTypeName(sf.supporting_document_type_id)} + {sf.attachment_url && ( +
+

{sf.attachment}

+ + + +
+ )} + {!sf.attachment_url && ( +
+

{t('app.public.child_form.to_complete')}

+
+ )} +
+ ); + })} +
+ } + + {isPrivileged() && <> + +

{t('app.public.child_form.refuse_documents_info')}

+
+
+ {t('app.public.child_form.refuse_documents')} + +
+ }
); diff --git a/app/frontend/src/javascript/components/family-account/child-item.tsx b/app/frontend/src/javascript/components/family-account/child-item.tsx index 1f0134808..62de02e9e 100644 --- a/app/frontend/src/javascript/components/family-account/child-item.tsx +++ b/app/frontend/src/javascript/components/family-account/child-item.tsx @@ -5,9 +5,11 @@ import { FabButton } from '../base/fab-button'; import FormatLib from '../../lib/format'; import { DeleteChildModal } from './delete-child-modal'; import ChildAPI from '../../api/child'; +import { PencilSimple, Trash, UserSquare } from 'phosphor-react'; interface ChildItemProps { child: Child; + size: 'sm' | 'lg'; onEdit: (child: Child) => void; onDelete: (error: string) => void; onError: (error: string) => void; @@ -16,7 +18,7 @@ interface ChildItemProps { /** * A child item. */ -export const ChildItem: React.FC = ({ child, onEdit, onDelete, onError }) => { +export const ChildItem: React.FC = ({ child, size, onEdit, onDelete, onError }) => { const { t } = useTranslation('public'); const [isOpenDeleteChildModal, setIsOpenDeleteChildModal] = React.useState(false); @@ -38,22 +40,29 @@ export const ChildItem: React.FC = ({ child, onEdit, onDelete, o }; return ( -
-
+
+
+ +
+
{t('app.public.child_item.last_name')} -
{child.last_name}
+

{child.last_name}

-
+
{t('app.public.child_item.first_name')} -
{child.first_name}
+

{child.first_name}

-
+
{t('app.public.child_item.birthday')} -
{FormatLib.date(child.birthday)}
+

{FormatLib.date(child.birthday)}

-
- } onClick={() => onEdit(child)} className="edit-button" /> - } onClick={toggleDeleteChildModal} className="delete-button" /> +
+ onEdit(child)} className="edit-btn"> + + + + +
diff --git a/app/frontend/src/javascript/components/family-account/children-list.tsx b/app/frontend/src/javascript/components/family-account/children-dashboard.tsx similarity index 76% rename from app/frontend/src/javascript/components/family-account/children-list.tsx rename to app/frontend/src/javascript/components/family-account/children-dashboard.tsx index 875555ddd..30d950068 100644 --- a/app/frontend/src/javascript/components/family-account/children-list.tsx +++ b/app/frontend/src/javascript/components/family-account/children-dashboard.tsx @@ -14,9 +14,10 @@ import SupportingDocumentTypeAPI from '../../api/supporting-document-type'; declare const Application: IApplication; -interface ChildrenListProps { +interface ChildrenDashboardProps { user: User; operator: User; + adminPanel?: boolean; onSuccess: (error: string) => void; onError: (error: string) => void; } @@ -24,7 +25,7 @@ interface ChildrenListProps { /** * A list of children belonging to the current user. */ -export const ChildrenList: React.FC = ({ user, operator, onError, onSuccess }) => { +export const ChildrenDashboard: React.FC = ({ user, operator, adminPanel, onError, onSuccess }) => { const { t } = useTranslation('public'); const [children, setChildren] = useState>([]); @@ -92,19 +93,24 @@ export const ChildrenList: React.FC = ({ user, operator, onEr }; return ( -
+
-

{t('app.public.children_list.heading')}

+ {adminPanel + ?

{t('app.public.children_dashboard.heading')}

+ :

{t('app.public.children_dashboard.member_heading')}

+ } {!isPrivileged() && ( - - {t('app.public.children_list.add_child')} - +
+ + {t('app.public.children_dashboard.add_child')} + +
)}
{children.map(child => ( - + ))}
setIsOpenChildModal(false)} onSuccess={handleSaveChildSuccess} onError={onError} supportingDocumentsTypes={supportingDocumentsTypes} operator={operator} /> @@ -112,12 +118,12 @@ export const ChildrenList: React.FC = ({ user, operator, onEr ); }; -const ChildrenListWrapper: React.FC = (props) => { +const ChildrenDashboardWrapper: React.FC = (props) => { return ( - + ); }; -Application.Components.component('childrenList', react2angular(ChildrenListWrapper, ['user', 'operator', 'onSuccess', 'onError'])); +Application.Components.component('childrenDashboard', react2angular(ChildrenDashboardWrapper, ['user', 'operator', 'adminPanel', 'onSuccess', 'onError'])); diff --git a/app/frontend/src/javascript/components/form/form-file-upload.tsx b/app/frontend/src/javascript/components/form/form-file-upload.tsx index 18d25fbae..22b58a252 100644 --- a/app/frontend/src/javascript/components/form/form-file-upload.tsx +++ b/app/frontend/src/javascript/components/form/form-file-upload.tsx @@ -75,9 +75,10 @@ export const FormFileUpload = ({ id, label, re return (
- {hasFile() && ( - {file.attachment_name} - )} + {hasFile() + ? {file.attachment_name} + : {t('app.shared.form_file_upload.placeholder')} + }
{file?.id && file?.attachment_url && ( {getGroupsNames(poit.group_ids)} {poit.name} -
+
- + - +
@@ -292,38 +293,26 @@ const SupportingDocumentsTypesList: React.FC onSuccess={onDestroySuccess} onError={onError}/> - - - - - - - - - {supportingDocumentsTypes.map(poit => { - return ( - - - - - ); - })} - -
- - {t('app.admin.settings.account.supporting_documents_types_list.name')} - - -
{poit.name} -
- - - - - - -
-
+
+ {supportingDocumentsTypes.map(poit => { + return ( +
+
+

{poit.name}

+
+ + + + + + +
+
+
+ ); + })} +
+ {!hasTypes() && (

diff --git a/app/frontend/src/javascript/components/user/members-list-item.tsx b/app/frontend/src/javascript/components/user/members-list-item.tsx new file mode 100644 index 000000000..19041b4e9 --- /dev/null +++ b/app/frontend/src/javascript/components/user/members-list-item.tsx @@ -0,0 +1,102 @@ +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Member } from '../../models/member'; +import { Child } from '../../models/child'; +import ChildAPI from '../../api/child'; +import { FabButton } from '../base/fab-button'; +import { CaretDown, User, Users } from 'phosphor-react'; +import { ChildItem } from '../family-account/child-item'; +import { EditDestroyButtons } from '../base/edit-destroy-buttons'; + +interface MembersListItemProps { + member: Member, + onError: (message: string) => void, + onSuccess: (message: string) => void +} + +/** + * Members list + */ +export const MembersListItem: React.FC = ({ member, onError, onSuccess }) => { + const { t } = useTranslation('admin'); + + const [children, setChildren] = useState>([]); + const [childrenList, setChildrenList] = useState(false); + + useEffect(() => { + ChildAPI.index({ user_id: member.id }).then(setChildren); + }, [member]); + + /** + * Redirect to the given user edition page + */ + const toMemberEdit = (memberId: number): void => { + window.location.href = `/#!/admin/members/${memberId}/edit`; + }; + + return ( +

+
+
+ {(children.length > 0) + ? + : + } +
+ {(children.length > 0) && + setChildrenList(!childrenList)} className={`toggle ${childrenList ? 'open' : ''}`}> + + + } +
+ +
+
+
+ {t('app.admin.members_list_item.surname')} +

{member.profile.last_name}

+
+
+ {t('app.admin.members_list_item.first_name')} +

{member.profile.first_name}

+
+
+ {t('app.admin.members_list_item.phone')} +

{member.profile.phone || '---'}

+
+
+ {t('app.admin.members_list_item.email')} +

{member.email}

+
+
+ {t('app.admin.members_list_item.group')} +

{member.group.name}

+
+
+ {t('app.admin.members_list_item.subscription')} +

{member.subscribed_plan?.name || '---'}

+
+
+ +
+ {/* TODO: */} + toMemberEdit(member.id)} + onDeleteSuccess={() => onSuccess} + itemId={member.id} + itemType={t('app.admin.members_list_item.item_type')} + destroy={() => new Promise(() => console.log(`Delete member ${member.id}`))} /> +
+
+ + { (children.length > 0) && +
+
+ {children.map(child => ( + console.log('edit child')} onDelete={() => console.log('delete child')} onError={onError} /> + ))} +
+ } +
+ ); +}; diff --git a/app/frontend/src/javascript/components/user/members-list.tsx b/app/frontend/src/javascript/components/user/members-list.tsx new file mode 100644 index 000000000..939db4632 --- /dev/null +++ b/app/frontend/src/javascript/components/user/members-list.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { IApplication } from '../../models/application'; +import { Loader } from '../base/loader'; +import { react2angular } from 'react2angular'; +import { Member } from '../../models/member'; +import { MembersListItem } from './members-list-item'; + +declare const Application: IApplication; + +interface MembersListProps { + members: Member[], + onError: (message: string) => void, + onSuccess: (message: string) => void +} + +/** + * Members list + */ +export const MembersList: React.FC = ({ members, onError, onSuccess }) => { + return ( +
+ {members.map(member => ( + + ))} +
+ ); +}; + +const MembersListWrapper: React.FC = (props) => { + return ( + + + + ); +}; + +Application.Components.component('membersList', react2angular(MembersListWrapper, ['members', 'onError', 'onSuccess'])); diff --git a/app/frontend/src/javascript/models/member.ts b/app/frontend/src/javascript/models/member.ts new file mode 100644 index 000000000..b163c3814 --- /dev/null +++ b/app/frontend/src/javascript/models/member.ts @@ -0,0 +1,47 @@ +import { TDateISO } from '../typings/date-iso'; + +export interface Member { + maxMembers: number + id: number + username: string + email: string + profile: { + first_name: string + last_name: string + phone: string + } + need_completion?: boolean + group: { + name: string + } + subscribed_plan?: Plan + validated_at: TDateISO +} + +interface Plan { + id: number + base_name: string + name: string + amount: number + interval: string + interval_count: number + training_credit_nb: number + training_credits: [ + { + training_id: number + }, + { + training_id: number + } + ] + machine_credits: [ + { + machine_id: number + hours: number + }, + { + machine_id: number + hours: number + } + ] +} diff --git a/app/frontend/src/stylesheets/application.scss b/app/frontend/src/stylesheets/application.scss index e9416affb..cf9ff38aa 100644 --- a/app/frontend/src/stylesheets/application.scss +++ b/app/frontend/src/stylesheets/application.scss @@ -52,6 +52,9 @@ @import "modules/events/event-form"; @import "modules/events/update-recurrent-modal"; @import "modules/events/events-settings.scss"; +@import "modules/family-account/child-form"; +@import "modules/family-account/child-item"; +@import "modules/family-account/children-dashboard"; @import "modules/form/abstract-form-item"; @import "modules/form/form-input"; @import "modules/form/form-multi-file-upload"; @@ -181,8 +184,6 @@ @import "modules/tour"; @import "modules/wallet-info"; -@import "modules/family-account/child-item"; - @import "app.responsive"; @import "overrides"; diff --git a/app/frontend/src/stylesheets/modules/base/edit-destroy-buttons.scss b/app/frontend/src/stylesheets/modules/base/edit-destroy-buttons.scss index 6adaece60..2fa61210a 100644 --- a/app/frontend/src/stylesheets/modules/base/edit-destroy-buttons.scss +++ b/app/frontend/src/stylesheets/modules/base/edit-destroy-buttons.scss @@ -1,6 +1,9 @@ .edit-destroy-buttons { + width: fit-content; + flex-shrink: 0; border-radius: var(--border-radius-sm); overflow: hidden; + button { @include btn; border-radius: 0; diff --git a/app/frontend/src/stylesheets/modules/base/fab-modal.scss b/app/frontend/src/stylesheets/modules/base/fab-modal.scss index fb34b42c8..777b29999 100644 --- a/app/frontend/src/stylesheets/modules/base/fab-modal.scss +++ b/app/frontend/src/stylesheets/modules/base/fab-modal.scss @@ -30,6 +30,7 @@ animation: 0.3s ease-out slideInFromTop; position: relative; top: 90px; + max-width: 100vw; margin: auto; opacity: 1; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); diff --git a/app/frontend/src/stylesheets/modules/family-account/child-form.scss b/app/frontend/src/stylesheets/modules/family-account/child-form.scss new file mode 100644 index 000000000..b1ed219d4 --- /dev/null +++ b/app/frontend/src/stylesheets/modules/family-account/child-form.scss @@ -0,0 +1,43 @@ +.child-form { + .grp { + display: flex; + flex-direction: column; + @media (min-width: 640px) {flex-direction: row; } + + .form-item:first-child { margin-right: 2.4rem; } + } + + hr { width: 100%; } + .actions { + align-self: flex-end; + } + + .document-list { + margin-bottom: 1.6rem; + display: flex; + flex-direction: column; + gap: 1.6rem; + + &-item { + display: flex; + flex-direction: column; + gap: 0.8rem; + .type { + @include text-sm; + } + .file, + .missing { + padding: 0.8rem 0.8rem 0.8rem 1.6rem; + display: flex; + justify-content: space-between; + align-items: center; + border: 1px solid var(--gray-soft-dark); + border-radius: var(--border-radius); + p { margin: 0; } + } + .missing { + background-color: var(--gray-soft-light); + } + } + } +} \ No newline at end of file diff --git a/app/frontend/src/stylesheets/modules/family-account/child-item.scss b/app/frontend/src/stylesheets/modules/family-account/child-item.scss index 40706e12b..f505a73bd 100644 --- a/app/frontend/src/stylesheets/modules/family-account/child-item.scss +++ b/app/frontend/src/stylesheets/modules/family-account/child-item.scss @@ -1,12 +1,62 @@ .child-item { width: 100%; display: grid; - grid-template-rows: repeat(3, min-content); - grid-template-columns: 1fr 1fr; + grid-template-columns: min-content 1fr; + align-items: flex-start; gap: 1.6rem 2.4rem; - align-items: center; - padding: 1.6rem; - border: 1px solid var(--gray-soft-dark); - border-radius: var(--border-radius); background-color: var(--gray-soft-lightest); + &.lg { + padding: 1.6rem; + border: 1px solid var(--gray-soft-dark); + border-radius: var(--border-radius); + } + &.sm { + .actions button { + height: 3rem !important; + min-height: auto; + } + } + + & > div:not(.actions) { + display: flex; + flex-direction: column; + span { + @include text-xs; + color: var(--gray-hard-light); + } + } + p { + margin: 0; + @include text-base(600); + } + &.sm p { + @include text-sm(500); + } + + .status { + grid-row: 1/5; + align-self: stretch; + display: flex; + align-items: center; + } + &.is-validated .status svg { + color: var(--success-dark); + } + + .actions { + align-self: center; + justify-self: flex-end; + } + + @media (min-width: 768px) { + grid-template-columns: min-content repeat(3, 1fr); + .status { grid-row: auto; } + .actions { + grid-column-end: -1; + display: flex; + } + } + @media (min-width: 1024px) { + grid-template-columns: min-content repeat(3, 1fr) max-content; + } } diff --git a/app/frontend/src/stylesheets/modules/family-account/children-dashboard.scss b/app/frontend/src/stylesheets/modules/family-account/children-dashboard.scss new file mode 100644 index 000000000..bed5aae61 --- /dev/null +++ b/app/frontend/src/stylesheets/modules/family-account/children-dashboard.scss @@ -0,0 +1,20 @@ +.children-dashboard { + max-width: 1600px; + margin: 0 auto; + padding-bottom: 6rem; + @include grid-col(12); + gap: 3.2rem; + align-items: flex-start; + + header { + @include header(); + padding-bottom: 0; + grid-column: 2 / -2; + } + .children-list { + grid-column: 2 / -2; + display: flex; + flex-direction: column; + gap: 1.6rem; + } +} \ No newline at end of file diff --git a/app/frontend/src/stylesheets/modules/form/form-file-upload.scss b/app/frontend/src/stylesheets/modules/form/form-file-upload.scss index 5707bbc45..b1bad6758 100644 --- a/app/frontend/src/stylesheets/modules/form/form-file-upload.scss +++ b/app/frontend/src/stylesheets/modules/form/form-file-upload.scss @@ -13,6 +13,8 @@ margin-bottom: 1.6rem; } + .placeholder { color: var(--gray-soft-darkest); } + .actions { margin-left: auto; display: flex; diff --git a/app/frontend/src/stylesheets/modules/members.scss b/app/frontend/src/stylesheets/modules/members.scss index 34a9209f9..dc30f4c6f 100644 --- a/app/frontend/src/stylesheets/modules/members.scss +++ b/app/frontend/src/stylesheets/modules/members.scss @@ -1,4 +1,109 @@ .promote-member img { width: 16px; height: 21px; +} + +.members-list { + width: 100%; + margin: 2.4rem 0; + display: flex; + flex-direction: column; + gap: 2.4rem; + + &-item { + width: 100%; + padding: 1.6rem; + display: grid; + grid-template-columns: 48px 1fr; + gap: 1.6rem 2.4rem; + border: 1px solid var(--gray-soft-dark); + border-radius: var(--border-radius); + background-color: var(--gray-soft-lightest); + &.is-validated .left-col .status svg { color: var(--success-dark); } + &.is-incomplet .left-col .status svg { color: var(--alert); } + + .left-col { + grid-row: span 2; + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-between; + .status { + display: flex; + align-items: center; + } + + .toggle { + height: fit-content; + background-color: var(--gray-soft); + border: none; + svg { transition: transform 0.5s ease-in-out; } + &.open svg { transform: rotate(-180deg); } + } + } + + .member { + display: flex; + flex-direction: column; + gap: 2.4rem; + &-infos { + flex: 1; + display: grid; + gap: 1.6rem; + + & > div:not(.actions) { + display: flex; + flex-direction: column; + span { + @include text-xs; + color: var(--gray-hard-light); + } + } + p { + margin: 0; + @include text-base(600); + line-height: 1.5; + } + + } + &-actions { + align-self: flex-end; + } + } + + .member-children { + max-height: 0; + display: flex; + flex-direction: column; + gap: 1.6rem; + overflow-y: hidden; + transition: max-height 0.5s ease-in-out; + &.open { + max-height: 17rem; + overflow-y: auto; + } + + hr { margin: 0; } + .child-item:last-of-type { padding-bottom: 0; } + } + + @media (min-width: 768px) { + .member-infos { + grid-template-columns: repeat(2, 1fr); + } + } + @media (min-width: 1024px) { + .member { + flex-direction: row; + &-actions { + align-self: center; + } + } + } + @media (min-width: 1220px) { + .member-infos { + grid-template-columns: repeat(3, 1fr); + } + } + } } \ No newline at end of file diff --git a/app/frontend/src/stylesheets/modules/supporting-documents/supporting-documents-types-list.scss b/app/frontend/src/stylesheets/modules/supporting-documents/supporting-documents-types-list.scss index b9fca3534..2448c0dc3 100644 --- a/app/frontend/src/stylesheets/modules/supporting-documents/supporting-documents-types-list.scss +++ b/app/frontend/src/stylesheets/modules/supporting-documents/supporting-documents-types-list.scss @@ -37,6 +37,7 @@ } .title { + margin-bottom: 1.6rem; display: flex; flex-direction: row; justify-content: space-between; @@ -54,22 +55,26 @@ } } - table { - thead > tr { - th.group-name, - th.name { - width: 40% - } - th.actions { - width: 20%; - } - } + .document-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(min-content, 50rem)); + gap: 1.6rem; - tbody { - .buttons { - .edit-btn { - margin-right: 5px; - } + &-item { + display: flex; + flex-direction: column; + gap: 0.8rem; + .type { + @include text-sm; + } + .file { + padding: 0.8rem 0.8rem 0.8rem 1.6rem; + display: flex; + justify-content: space-between; + align-items: center; + border: 1px solid var(--gray-soft-dark); + border-radius: var(--border-radius); + p { margin: 0; } } } } diff --git a/app/frontend/src/stylesheets/modules/user/user-validation.scss b/app/frontend/src/stylesheets/modules/user/user-validation.scss index 2c160f181..3c9e1b2b6 100644 --- a/app/frontend/src/stylesheets/modules/user/user-validation.scss +++ b/app/frontend/src/stylesheets/modules/user/user-validation.scss @@ -9,3 +9,7 @@ vertical-align: middle; } } +.child-validation { + margin: 0 0 2rem; + text-align: center; +} \ No newline at end of file diff --git a/app/frontend/templates/admin/members/edit.html b/app/frontend/templates/admin/members/edit.html index 8dbbffcdb..752bffe3a 100644 --- a/app/frontend/templates/admin/members/edit.html +++ b/app/frontend/templates/admin/members/edit.html @@ -63,7 +63,7 @@ - + diff --git a/app/frontend/templates/admin/members/members.html b/app/frontend/templates/admin/members/members.html index b0be69229..36d1e43a5 100644 --- a/app/frontend/templates/admin/members/members.html +++ b/app/frontend/templates/admin/members/members.html @@ -17,11 +17,12 @@
+
- -
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'app.admin.members.username' | translate }} {{ 'app.admin.members.surname' | translate }} {{ 'app.admin.members.first_name' | translate }}
- - {{ m.username }}{{ m.profile.last_name }}{{ m.profile.first_name }} -
- - - {{ 'app.shared.user_admin.incomplete_profile' }} -
-
+ +
diff --git a/app/frontend/templates/dashboard/children.html b/app/frontend/templates/dashboard/children.html index 9f81ef21c..022cfa3a0 100644 --- a/app/frontend/templates/dashboard/children.html +++ b/app/frontend/templates/dashboard/children.html @@ -7,5 +7,5 @@
- +
diff --git a/app/frontend/templates/events/show.html b/app/frontend/templates/events/show.html index 0b4aeb048..cf44f2333 100644 --- a/app/frontend/templates/events/show.html +++ b/app/frontend/templates/events/show.html @@ -49,7 +49,7 @@
-
+
{{event.event_files_attributes.length}}

{{ 'app.public.events_show.downloadable_documents' }}

@@ -72,8 +72,11 @@
+
+ {{ 'app.public.events_show.event_type.nominative' }} + {{ 'app.public.events_show.event_type.family' }} +
-
{{event.category.name}}
{{theme.name}} @@ -136,17 +139,17 @@ class="form-control"> - -

+ + {{ 'app.shared.cart.child_validation_required_alert' }} -

+
- -

+ + {{ 'app.shared.cart.child_birthday_must_be_under_18_years_ago_alert' }} -

+
diff --git a/app/frontend/templates/shared/_member_select.html b/app/frontend/templates/shared/_member_select.html index b508c96fe..730f5de4a 100644 --- a/app/frontend/templates/shared/_member_select.html +++ b/app/frontend/templates/shared/_member_select.html @@ -12,8 +12,11 @@ {{member}} -
- {{ 'app.shared.member_select.member_not_validated' }} -
+ + + + {{ 'app.shared.member_select.member_not_validated' }} + +
diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index 140eb587e..27332ca33 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -1195,6 +1195,14 @@ en: member_filter_all: "All" member_filter_not_confirmed: "Unconfirmed" member_filter_inactive_for_3_years: "Inactive for 3 years" + members_list_item: + item_type: "member" + surname: "Surname" + first_name: "First name" + phone: "Phone" + email: "Email" + group: "Group" + subscription: "Subscription" #add a member members_new: add_a_member: "Add a member" @@ -1895,7 +1903,7 @@ en: no_groups_info: "Supporting documents are necessarily applied to groups.
If you do not have any group yet, you can create one from the \"Users/Groups\" page (button on the right)." create_groups: "Create groups" supporting_documents_type_title: "Supporting documents requests" - add_type: "New supporting documents request" + add_type: "Add new document" group_name: "Group" name: "Supporting documents" no_types: "You do not have any supporting documents requests.
Make sure you have created at least one group in order to add a request." diff --git a/config/locales/app.public.en.yml b/config/locales/app.public.en.yml index 6cd2b7157..99d6d0f84 100644 --- a/config/locales/app.public.en.yml +++ b/config/locales/app.public.en.yml @@ -318,6 +318,9 @@ en: event_description: "Event description" downloadable_documents: "Downloadable documents" information_and_booking: "Information and booking" + event_type: + family: "Family event" + nominative: "Nominative event" dates: "Dates" beginning: "Beginning:" ending: "Ending:" @@ -483,25 +486,28 @@ en: member_select: select_a_member: "Select a member" start_typing: "Start typing..." - children_list: - heading: "My children" + children_dashboard: + heading: "Children" + member_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." + child_form_info: "Please note that you can only add a child under the age of 18. 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" + supporting_documents: "Supporting documents" to_complete: "To complete" - refuse_documents: "Refusing the documents" + refuse_documents_info: "You can refuse a selection of documents by clicking on the following button." + refuse_documents: "Refuse documents" child_item: - first_name: "First name of the child" - last_name: "Last name of the child" + first_name: "Child first name" + last_name: "Child last name" birthday: "Birthday" deleted: "The child has been deleted." unable_to_delete: "Unable to delete the child." diff --git a/config/locales/app.public.fr.yml b/config/locales/app.public.fr.yml index 3ec715aba..4f89f4df1 100644 --- a/config/locales/app.public.fr.yml +++ b/config/locales/app.public.fr.yml @@ -483,7 +483,7 @@ fr: member_select: select_a_member: "Sélectionnez un membre" start_typing: "Commencez à écrire..." - children_list: + children_dashboard: heading: "Mes enfants" add_child: "Ajouter un enfant" child_modal: diff --git a/config/locales/app.shared.en.yml b/config/locales/app.shared.en.yml index 8872fea25..e2ff2a219 100644 --- a/config/locales/app.shared.en.yml +++ b/config/locales/app.shared.en.yml @@ -169,7 +169,7 @@ en: member_select: select_a_member: "Select a member" start_typing: "Start typing..." - member_not_validated: "Warning:
The member was not validated." + member_not_validated: "This member has not yet been validated." #payment modal abstract_payment_modal: online_payment: "Online payment" @@ -372,9 +372,9 @@ en: slot_tags: "Slot tags" user_tags: "User tags" no_tags: "No tags" - user_validation_required_alert: "Warning!
Your administrator must validate your account. Then, you'll then be able to access all the booking features." - child_validation_required_alert: "Warning!
Your administrator must validate your child account. Then, you'll then be able to book the event." - child_birthday_must_be_under_18_years_ago_alert: "Warning!
Your child must be under 18 years ago. Then, you'll then be able to book the event." + user_validation_required_alert: "Your administrator must validate your account. Then, you'll then be able to access all the booking features." + child_validation_required_alert: "Your administrator must validate your child account. Then, you'll then be able to book the event." + child_birthday_must_be_under_18_years_ago_alert: "Your child must be under 18. Then, you'll then be able to book the event." # feature-tour modal tour: previous: "Previous" @@ -447,6 +447,7 @@ en: select_all: "Select all" unselect_all: "Unselect all" form_file_upload: + placeholder: "Add a file" browse: "Browse" edit: "Edit" form_image_upload: From e392cc1803ae6f16af22b8caee6676886210fc07 Mon Sep 17 00:00:00 2001 From: Vincent Date: Wed, 31 May 2023 14:55:09 +0200 Subject: [PATCH 24/75] (ui) restor deleted table's style --- .../modules/base/edit-destroy-buttons.scss | 2 +- .../supporting-documents-types-list.scss | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/frontend/src/stylesheets/modules/base/edit-destroy-buttons.scss b/app/frontend/src/stylesheets/modules/base/edit-destroy-buttons.scss index 2fa61210a..949b4b028 100644 --- a/app/frontend/src/stylesheets/modules/base/edit-destroy-buttons.scss +++ b/app/frontend/src/stylesheets/modules/base/edit-destroy-buttons.scss @@ -1,5 +1,5 @@ .edit-destroy-buttons { - width: fit-content; + width: max-content; flex-shrink: 0; border-radius: var(--border-radius-sm); overflow: hidden; diff --git a/app/frontend/src/stylesheets/modules/supporting-documents/supporting-documents-types-list.scss b/app/frontend/src/stylesheets/modules/supporting-documents/supporting-documents-types-list.scss index 2448c0dc3..30837f400 100644 --- a/app/frontend/src/stylesheets/modules/supporting-documents/supporting-documents-types-list.scss +++ b/app/frontend/src/stylesheets/modules/supporting-documents/supporting-documents-types-list.scss @@ -55,6 +55,18 @@ } } + table { + thead > tr { + th.group-name, + th.name { + width: 40% + } + th.actions { + width: 20%; + } + } + } + .document-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(min-content, 50rem)); @@ -83,4 +95,4 @@ text-align: center; } } -} +} \ No newline at end of file From 050ae9be6714fd4408ecfa51e23af0f16e1c4cab Mon Sep 17 00:00:00 2001 From: Vincent Date: Wed, 31 May 2023 15:18:39 +0200 Subject: [PATCH 25/75] (ui) fix gap --- app/frontend/src/stylesheets/modules/members.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/frontend/src/stylesheets/modules/members.scss b/app/frontend/src/stylesheets/modules/members.scss index dc30f4c6f..b54e92a35 100644 --- a/app/frontend/src/stylesheets/modules/members.scss +++ b/app/frontend/src/stylesheets/modules/members.scss @@ -15,7 +15,7 @@ padding: 1.6rem; display: grid; grid-template-columns: 48px 1fr; - gap: 1.6rem 2.4rem; + gap: 0 2.4rem; border: 1px solid var(--gray-soft-dark); border-radius: var(--border-radius); background-color: var(--gray-soft-lightest); @@ -83,7 +83,7 @@ overflow-y: auto; } - hr { margin: 0; } + hr { margin: 1.6rem 0 0; } .child-item:last-of-type { padding-bottom: 0; } } From a4d4107cb18a551c9328e76ff5482fe65618880a Mon Sep 17 00:00:00 2001 From: Du Peng Date: Wed, 31 May 2023 14:42:12 +0200 Subject: [PATCH 26/75] (i18n) update translation --- config/locales/app.public.fr.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/config/locales/app.public.fr.yml b/config/locales/app.public.fr.yml index 4f89f4df1..9a4c93af9 100644 --- a/config/locales/app.public.fr.yml +++ b/config/locales/app.public.fr.yml @@ -318,6 +318,9 @@ fr: event_description: "Description de l’événement" downloadable_documents: "Documents à télécharger" information_and_booking: "Informations et réservation" + event_type: + family: "Evénement famille" + nominative: "Evénement nominatif" dates: "Dates" beginning: "Début :" ending: "Fin :" @@ -497,8 +500,10 @@ fr: email: "Courriel" phone: "Téléphone" save: "Enregistrer" + supporting_documents: "Documents justificatifs" to_complete: "À compléter" refuse_documents: "Refuser les documents" + refuse_documents_info: "Vous pouvez refuser une sélection de documents en cliquant sur le bouton suivant." child_item: first_name: "Prénom de l'enfant" last_name: "Nom de l'enfant" From 26f1c939cd4e88008fe653c6fe3b7dd291adad61 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Wed, 31 May 2023 18:20:14 +0200 Subject: [PATCH 27/75] (feat) edit/delete child in members list --- app/controllers/api/children_controller.rb | 2 +- .../components/family-account/child-item.tsx | 7 ++- .../components/family-account/child-modal.tsx | 4 +- .../family-account/children-dashboard.tsx | 4 +- .../components/user/members-list-item.tsx | 41 ++++++------- .../components/user/members-list.tsx | 60 +++++++++++++++++-- .../javascript/controllers/admin/members.js | 42 ++++++++++++- app/frontend/src/javascript/models/member.ts | 2 + .../templates/admin/members/members.html | 4 +- app/views/api/members/list.json.jbuilder | 12 ++++ 10 files changed, 141 insertions(+), 37 deletions(-) diff --git a/app/controllers/api/children_controller.rb b/app/controllers/api/children_controller.rb index bbd1d8e04..ac1d98b78 100644 --- a/app/controllers/api/children_controller.rb +++ b/app/controllers/api/children_controller.rb @@ -10,7 +10,7 @@ class API::ChildrenController < API::APIController authorize Child user_id = current_user.id user_id = params[:user_id] if current_user.privileged? && params[:user_id] - @children = Child.where(user_id: user_id) + @children = Child.where(user_id: user_id).includes(:supporting_document_files).order(:created_at) end def show diff --git a/app/frontend/src/javascript/components/family-account/child-item.tsx b/app/frontend/src/javascript/components/family-account/child-item.tsx index 62de02e9e..4192d2c04 100644 --- a/app/frontend/src/javascript/components/family-account/child-item.tsx +++ b/app/frontend/src/javascript/components/family-account/child-item.tsx @@ -11,7 +11,7 @@ interface ChildItemProps { child: Child; size: 'sm' | 'lg'; onEdit: (child: Child) => void; - onDelete: (error: string) => void; + onDelete: (child: Child, error: string) => void; onError: (error: string) => void; } @@ -33,8 +33,9 @@ export const ChildItem: React.FC = ({ child, size, onEdit, onDel const deleteChild = () => { ChildAPI.destroy(child.id).then(() => { toggleDeleteChildModal(); - onDelete(t('app.public.child_item.deleted')); - }).catch(() => { + onDelete(child, t('app.public.child_item.deleted')); + }).catch((e) => { + console.error(e); onError(t('app.public.child_item.unable_to_delete')); }); }; diff --git a/app/frontend/src/javascript/components/family-account/child-modal.tsx b/app/frontend/src/javascript/components/family-account/child-modal.tsx index acc75e9f4..f950e207e 100644 --- a/app/frontend/src/javascript/components/family-account/child-modal.tsx +++ b/app/frontend/src/javascript/components/family-account/child-modal.tsx @@ -13,7 +13,7 @@ interface ChildModalProps { operator: User; isOpen: boolean; toggleModal: () => void; - onSuccess: (msg: string) => void; + onSuccess: (child: Child, msg: string) => void; onError: (error: string) => void; supportingDocumentsTypes: Array; } @@ -35,7 +35,7 @@ export const ChildModal: React.FC = ({ child, isOpen, toggleMod await ChildAPI.create(data); } toggleModal(); - onSuccess(''); + onSuccess(data, ''); } catch (error) { onError(error); } diff --git a/app/frontend/src/javascript/components/family-account/children-dashboard.tsx b/app/frontend/src/javascript/components/family-account/children-dashboard.tsx index 30d950068..c9f856d58 100644 --- a/app/frontend/src/javascript/components/family-account/children-dashboard.tsx +++ b/app/frontend/src/javascript/components/family-account/children-dashboard.tsx @@ -70,7 +70,7 @@ export const ChildrenDashboard: React.FC = ({ user, oper /** * Delete a child */ - const handleDeleteChildSuccess = (msg: string) => { + const handleDeleteChildSuccess = (_child: Child, msg: string) => { ChildAPI.index({ user_id: user.id }).then(setChildren); onSuccess(msg); }; @@ -78,7 +78,7 @@ export const ChildrenDashboard: React.FC = ({ user, oper /** * Handle save child success from the API */ - const handleSaveChildSuccess = (msg: string) => { + const handleSaveChildSuccess = (_data: Child, msg: string) => { ChildAPI.index({ user_id: user.id }).then(setChildren); if (msg) { onSuccess(msg); diff --git a/app/frontend/src/javascript/components/user/members-list-item.tsx b/app/frontend/src/javascript/components/user/members-list-item.tsx index 19041b4e9..4d0bea4f0 100644 --- a/app/frontend/src/javascript/components/user/members-list-item.tsx +++ b/app/frontend/src/javascript/components/user/members-list-item.tsx @@ -1,32 +1,28 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Member } from '../../models/member'; import { Child } from '../../models/child'; -import ChildAPI from '../../api/child'; import { FabButton } from '../base/fab-button'; -import { CaretDown, User, Users } from 'phosphor-react'; +import { CaretDown, User, Users, PencilSimple, Trash } from 'phosphor-react'; import { ChildItem } from '../family-account/child-item'; -import { EditDestroyButtons } from '../base/edit-destroy-buttons'; interface MembersListItemProps { member: Member, onError: (message: string) => void, onSuccess: (message: string) => void + onEditChild: (child: Child) => void; + onDeleteChild: (child: Child, error: string) => void; + onDeleteMember: (memberId: number) => void; } /** * Members list */ -export const MembersListItem: React.FC = ({ member, onError, onSuccess }) => { +export const MembersListItem: React.FC = ({ member, onError, onEditChild, onDeleteChild, onDeleteMember }) => { const { t } = useTranslation('admin'); - const [children, setChildren] = useState>([]); const [childrenList, setChildrenList] = useState(false); - useEffect(() => { - ChildAPI.index({ user_id: member.id }).then(setChildren); - }, [member]); - /** * Redirect to the given user edition page */ @@ -38,12 +34,12 @@ export const MembersListItem: React.FC = ({ member, onErro
- {(children.length > 0) + {(member.children.length > 0) ? : }
- {(children.length > 0) && + {(member.children.length > 0) && setChildrenList(!childrenList)} className={`toggle ${childrenList ? 'open' : ''}`}> @@ -78,22 +74,21 @@ export const MembersListItem: React.FC = ({ member, onErro
-
- {/* TODO: */} - toMemberEdit(member.id)} - onDeleteSuccess={() => onSuccess} - itemId={member.id} - itemType={t('app.admin.members_list_item.item_type')} - destroy={() => new Promise(() => console.log(`Delete member ${member.id}`))} /> +
+ toMemberEdit(member.id)} className="edit-btn"> + + + onDeleteMember(member.id)} className="delete-btn"> + +
- { (children.length > 0) && + { (member.children.length > 0) &&

- {children.map(child => ( - console.log('edit child')} onDelete={() => console.log('delete child')} onError={onError} /> + {member.children.map((child: Child) => ( + ))}
} diff --git a/app/frontend/src/javascript/components/user/members-list.tsx b/app/frontend/src/javascript/components/user/members-list.tsx index 939db4632..ad911c0a0 100644 --- a/app/frontend/src/javascript/components/user/members-list.tsx +++ b/app/frontend/src/javascript/components/user/members-list.tsx @@ -1,27 +1,79 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { IApplication } from '../../models/application'; import { Loader } from '../base/loader'; import { react2angular } from 'react2angular'; import { Member } from '../../models/member'; import { MembersListItem } from './members-list-item'; +import { SupportingDocumentType } from '../../models/supporting-document-type'; +import SupportingDocumentTypeAPI from '../../api/supporting-document-type'; +import { Child } from '../../models/child'; +import { ChildModal } from '../family-account/child-modal'; +import { User } from '../../models/user'; declare const Application: IApplication; interface MembersListProps { members: Member[], + operator: User, onError: (message: string) => void, onSuccess: (message: string) => void + onDeleteMember: (memberId: number) => void; + onDeletedChild: (memberId: number, childId: number) => void; + onUpdatedChild: (memberId: number, child: Child) => void; } /** * Members list */ -export const MembersList: React.FC = ({ members, onError, onSuccess }) => { +export const MembersList: React.FC = ({ members, onError, onSuccess, operator, onDeleteMember, onDeletedChild, onUpdatedChild }) => { + const [supportingDocumentsTypes, setSupportingDocumentsTypes] = useState>([]); + const [child, setChild] = useState(); + const [isOpenChildModal, setIsOpenChildModal] = useState(false); + + useEffect(() => { + SupportingDocumentTypeAPI.index({ document_type: 'Child' }).then(tData => { + setSupportingDocumentsTypes(tData); + }); + }, []); + + /** + * Open the edit child modal + */ + const editChild = (child: Child) => { + setIsOpenChildModal(true); + setChild({ + ...child, + supporting_document_files_attributes: supportingDocumentsTypes.map(t => { + const file = child.supporting_document_files_attributes.find(f => f.supporting_document_type_id === t.id); + return file || { supporting_document_type_id: t.id }; + }) + } as Child); + }; + + /** + * Delete a child + */ + const handleDeleteChildSuccess = (c: Child, msg: string) => { + onDeletedChild(c.user_id, c.id); + onSuccess(msg); + }; + + /** + * Handle save child success from the API + */ + const handleSaveChildSuccess = (c: Child, msg: string) => { + onUpdatedChild(c.user_id, c); + if (msg) { + onSuccess(msg); + } + }; + return (
{members.map(member => ( - + ))} + setIsOpenChildModal(false)} onSuccess={handleSaveChildSuccess} onError={onError} supportingDocumentsTypes={supportingDocumentsTypes} operator={operator} />
); }; @@ -34,4 +86,4 @@ const MembersListWrapper: React.FC = (props) => { ); }; -Application.Components.component('membersList', react2angular(MembersListWrapper, ['members', 'onError', 'onSuccess'])); +Application.Components.component('membersList', react2angular(MembersListWrapper, ['members', 'onError', 'onSuccess', 'operator', 'onDeleteMember', 'onDeletedChild', 'onUpdatedChild'])); diff --git a/app/frontend/src/javascript/controllers/admin/members.js b/app/frontend/src/javascript/controllers/admin/members.js index e26a3da48..e7b388827 100644 --- a/app/frontend/src/javascript/controllers/admin/members.js +++ b/app/frontend/src/javascript/controllers/admin/members.js @@ -291,7 +291,7 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', Member.delete( { id: memberId }, function () { - $scope.members.splice(findItemIdxById($scope.members, memberId), 1); + $scope.members = _.filter($scope.members, function (m) { return m.id !== memberId; }); return growl.success(_t('app.admin.members.member_successfully_deleted')); }, function (error) { @@ -303,6 +303,32 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', ); }; + $scope.onDeletedChild = function (memberId, childId) { + $scope.members = $scope.members.map(function (member) { + if (member.id === memberId) { + member.children = _.filter(member.children, function (c) { return c.id !== childId; }); + return member; + } + return member; + }); + }; + + $scope.onUpdatedChild = function (memberId, child) { + $scope.members = $scope.members.map(function (member) { + if (member.id === memberId) { + member.children = member.children.map(function (c) { + if (c.id === child.id) { + return child; + } + return c; + }); + console.log(member.children); + return member; + } + return member; + }); + }; + /** * Ask for confirmation then delete the specified administrator * @param admins {Array} full list of administrators @@ -588,6 +614,20 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', } }; + /** + * Callback triggered in case of error + */ + $scope.onError = (message) => { + growl.error(message); + }; + + /** + * Callback triggered in case of success + */ + $scope.onSuccess = (message) => { + growl.success(message); + }; + /* PRIVATE SCOPE */ /** diff --git a/app/frontend/src/javascript/models/member.ts b/app/frontend/src/javascript/models/member.ts index b163c3814..3c9e8cad2 100644 --- a/app/frontend/src/javascript/models/member.ts +++ b/app/frontend/src/javascript/models/member.ts @@ -1,4 +1,5 @@ import { TDateISO } from '../typings/date-iso'; +import { Child } from './child'; export interface Member { maxMembers: number @@ -16,6 +17,7 @@ export interface Member { } subscribed_plan?: Plan validated_at: TDateISO + children: Child[] } interface Plan { diff --git a/app/frontend/templates/admin/members/members.html b/app/frontend/templates/admin/members/members.html index 36d1e43a5..22fe77590 100644 --- a/app/frontend/templates/admin/members/members.html +++ b/app/frontend/templates/admin/members/members.html @@ -35,7 +35,9 @@
- +
+ +
diff --git a/app/views/api/members/list.json.jbuilder b/app/views/api/members/list.json.jbuilder index 38dd1b1a2..97840a4b8 100644 --- a/app/views/api/members/list.json.jbuilder +++ b/app/views/api/members/list.json.jbuilder @@ -18,4 +18,16 @@ json.array!(@members) do |member| end end json.validated_at member.validated_at + json.children member.children.order(:created_at) do |child| + 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 + json.supportable_type f.supportable_type + 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_identifier ? "/api/supporting_document_files/#{f.id}/download" : nil + end + end end From b7bcce75f3b4ebd52c2881ef36b64dfc12b2c943 Mon Sep 17 00:00:00 2001 From: Du Peng Date: Wed, 31 May 2023 18:26:08 +0200 Subject: [PATCH 28/75] (bug) hide member and child select if admin dont select a member before reserve event --- app/frontend/templates/events/show.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/frontend/templates/events/show.html b/app/frontend/templates/events/show.html index cf44f2333..66f217f80 100644 --- a/app/frontend/templates/events/show.html +++ b/app/frontend/templates/events/show.html @@ -128,7 +128,7 @@
-
+
Date: Tue, 13 Jun 2023 11:38:06 +0200 Subject: [PATCH 29/75] (wip) add pre-registration to event --- app/controllers/api/events_controller.rb | 1 + .../javascript/components/events/event-form.tsx | 14 ++++++++++++++ app/frontend/src/javascript/models/event.ts | 2 ++ app/frontend/templates/events/show.html | 4 ++-- app/policies/local_payment_policy.rb | 3 ++- app/views/api/events/_event.json.jbuilder | 2 +- config/locales/app.admin.en.yml | 3 +++ config/locales/app.admin.fr.yml | 3 +++ ...20230612123250_add_pre_registration_to_event.rb | 9 +++++++++ db/structure.sql | 7 +++++-- 10 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 db/migrate/20230612123250_add_pre_registration_to_event.rb diff --git a/app/controllers/api/events_controller.rb b/app/controllers/api/events_controller.rb index 4d05f6b56..781e170ba 100644 --- a/app/controllers/api/events_controller.rb +++ b/app/controllers/api/events_controller.rb @@ -97,6 +97,7 @@ class API::EventsController < API::APIController event_preparams = params.required(:event).permit(:title, :description, :start_date, :start_time, :end_date, :end_time, :amount, :nb_total_places, :availability_id, :all_day, :recurrence, :recurrence_end_at, :category_id, :event_theme_ids, :age_range_id, :event_type, + :pre_registration, :pre_registration_end_date, event_theme_ids: [], event_image_attributes: %i[id attachment], event_files_attributes: %i[id attachment _destroy], diff --git a/app/frontend/src/javascript/components/events/event-form.tsx b/app/frontend/src/javascript/components/events/event-form.tsx index 5ed6a7036..0d8c0b81b 100644 --- a/app/frontend/src/javascript/components/events/event-form.tsx +++ b/app/frontend/src/javascript/components/events/event-form.tsx @@ -55,6 +55,7 @@ export const EventForm: React.FC = ({ action, event, onError, on const [updatingEvent, setUpdatingEvent] = useState(null); const [isActiveAccounting, setIsActiveAccounting] = useState(false); const [isActiveFamilyAccount, setIsActiveFamilyAccount] = useState(false); + const [isAcitvePreRegistration, setIsActivePreRegistration] = useState(event?.pre_registration); useEffect(() => { EventCategoryAPI.index() @@ -241,6 +242,19 @@ export const EventForm: React.FC = ({ action, event, onError, on formState={formState} options={ageRangeOptions} label={t('app.admin.event_form.age_range')} />} + + {isAcitvePreRegistration && + + }
diff --git a/app/frontend/src/javascript/models/event.ts b/app/frontend/src/javascript/models/event.ts index 892ca95dc..7f72b1974 100644 --- a/app/frontend/src/javascript/models/event.ts +++ b/app/frontend/src/javascript/models/event.ts @@ -66,6 +66,8 @@ export interface Event { recurrence_end_at: Date, advanced_accounting_attributes?: AdvancedAccounting, event_type: EventType, + pre_registration?: boolean, + pre_registration_end_date?: TDateISODate | Date, } export interface EventDecoration { diff --git a/app/frontend/templates/events/show.html b/app/frontend/templates/events/show.html index 66f217f80..6a3c9652e 100644 --- a/app/frontend/templates/events/show.html +++ b/app/frontend/templates/events/show.html @@ -255,11 +255,11 @@
-