From e85e2d7b472ba3cf11fecd3ed9359d5a6a873b9c Mon Sep 17 00:00:00 2001 From: Du Peng Date: Mon, 15 May 2023 16:42:01 +0200 Subject: [PATCH] (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 @@ -