1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2024-12-01 12:24:28 +01:00

(wip) pay family and nominative event

This commit is contained in:
Du Peng 2023-05-15 16:42:01 +02:00
parent 07b47d7956
commit e85e2d7b47
6 changed files with 184 additions and 50 deletions

View File

@ -7,7 +7,10 @@ class API::ChildrenController < API::APIController
before_action :set_child, only: %i[show update destroy] before_action :set_child, only: %i[show update destroy]
def index 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 end
def show def show

View File

@ -54,6 +54,7 @@ export const EventForm: React.FC<EventFormProps> = ({ action, event, onError, on
const [isOpenRecurrentModal, setIsOpenRecurrentModal] = useState<boolean>(false); const [isOpenRecurrentModal, setIsOpenRecurrentModal] = useState<boolean>(false);
const [updatingEvent, setUpdatingEvent] = useState<Event>(null); const [updatingEvent, setUpdatingEvent] = useState<Event>(null);
const [isActiveAccounting, setIsActiveAccounting] = useState<boolean>(false); const [isActiveAccounting, setIsActiveAccounting] = useState<boolean>(false);
const [isActiveFamilyAccount, setIsActiveFamilyAccount] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
EventCategoryAPI.index() EventCategoryAPI.index()
@ -69,6 +70,7 @@ export const EventForm: React.FC<EventFormProps> = ({ action, event, onError, on
.then(data => setPriceCategoriesOptions(data.map(c => decorationToOption(c)))) .then(data => setPriceCategoriesOptions(data.map(c => decorationToOption(c))))
.catch(onError); .catch(onError);
SettingAPI.get('advanced_accounting').then(res => setIsActiveAccounting(res.value === 'true')).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(() => { useEffect(() => {
@ -172,11 +174,14 @@ export const EventForm: React.FC<EventFormProps> = ({ action, event, onError, on
* This method provides event type options * This method provides event type options
*/ */
const buildEventTypeOptions = (): Array<SelectOption<EventType>> => { const buildEventTypeOptions = (): Array<SelectOption<EventType>> => {
return [ const options = [
{ label: t('app.admin.event_form.event_types.standard'), value: 'standard' }, { label: t('app.admin.event_form.event_types.standard'), value: 'standard' as EventType },
{ label: t('app.admin.event_form.event_types.nominative'), value: 'nominative' }, { label: t('app.admin.event_form.event_types.nominative'), value: 'nominative' as EventType }
{ label: t('app.admin.event_form.event_types.family'), value: 'family' }
]; ];
if (isActiveFamilyAccount) {
options.push({ label: t('app.admin.event_form.event_types.family'), value: 'family' as EventType });
}
return options;
}; };
return ( return (

View File

@ -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', 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) { function ($scope, $state,$rootScope, Event, $uibModal, Member, Reservation, Price, CustomAsset, SlotsReservation, eventPromise, growl, _t, Wallet, AuthService, helpers, dialogs, priceCategoriesPromise, settingsPromise, LocalPayment, Child) {
/* PUBLIC SCOPE */ /* PUBLIC SCOPE */
// reservations for the currently shown event // reservations for the currently shown event
@ -150,6 +150,9 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
$scope.ctrl = $scope.ctrl =
{ member: {} }; { member: {} };
// children for the member
$scope.children = [];
// parameters for a new reservation // parameters for a new reservation
$scope.reserve = { $scope.reserve = {
nbPlaces: { 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 * Callback to call when the number of tickets to book changes in the current booking
*/ */
$scope.changeNbPlaces = function (priceType) { $scope.changeNbPlaces = function (priceType) {
// compute the total remaining places // 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) { for (let ticket in $scope.reserve.tickets) {
remain -= $scope.reserve.tickets[ticket]; remain -= $scope.reserve.tickets[ticket];
} }
@ -260,36 +253,41 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
} }
} }
const nbBookingUsers = $scope.reserve.bookingUsers[priceType].length; if ($scope.event.event_type === 'nominative' || $scope.event.event_type === 'family') {
const nbReservePlaces = priceType === 'normal' ? $scope.reserve.nbReservePlaces : $scope.reserve.tickets[priceType]; const nbBookingUsers = $scope.reserve.bookingUsers[priceType].length;
if (nbReservePlaces > nbBookingUsers) { const nbReservePlaces = priceType === 'normal' ? $scope.reserve.nbReservePlaces : $scope.reserve.tickets[priceType];
_.times(nbReservePlaces - nbBookingUsers, () => { if (nbReservePlaces > nbBookingUsers) {
/* _.times(nbReservePlaces - nbBookingUsers, () => {
if (!hasMemberInBookingUsers()) { $scope.reserve.bookingUsers[priceType].push({ event_price_category_id: priceType === 'normal' ? null : priceType, bookedUsers: buildBookedUsersOptions() });
$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 { } else {
$scope.reserve.bookingUsers[priceType].push({ event_price_category_id: priceType === 'normal' ? null : priceType }); _.times(nbBookingUsers - nbReservePlaces, () => {
} $scope.reserve.bookingUsers[priceType].pop();
*/ });
$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 // recompute the total price
return $scope.computeEventAmount(); 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 * Callback to reset the current reservation parameters
* @param e {Object} see https://docs.angularjs.org/guide/expression#-event- * @param e {Object} see https://docs.angularjs.org/guide/expression#-event-
*/ */
$scope.cancelReserve = function (e) { $scope.cancelReserve = function (e) {
e.preventDefault(); e.preventDefault();
return resetEventReserve(); resetEventReserve();
updateNbReservePlaces();
return;
}; };
$scope.isUserValidatedByType = () => { $scope.isUserValidatedByType = () => {
@ -354,6 +352,9 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
Member.get({ id: $scope.ctrl.member.id }, function (member) { Member.get({ id: $scope.ctrl.member.id }, function (member) {
$scope.ctrl.member = member; $scope.ctrl.member = member;
getReservations($scope.event.id, 'Event', $scope.ctrl.member.id); 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); 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 */ /* PRIVATE SCOPE */
/** /**
@ -634,6 +660,9 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
// get the current user's reservations into $scope.reservations // get the current user's reservations into $scope.reservations
if ($scope.currentUser) { if ($scope.currentUser) {
getReservations($scope.event.id, 'Event', $scope.currentUser.id); 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 // 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; }); }).$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 * Create a hash map implementing the Reservation specs
* @param reserve {Object} Reservation parameters (places...) * @param reserve {Object} Reservation parameters (places...)
@ -694,9 +789,9 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
for (const user of $scope.reserve.bookingUsers[key]) { for (const user of $scope.reserve.bookingUsers[key]) {
reservation.booking_users_attributes.push({ reservation.booking_users_attributes.push({
event_price_category_id: user.event_price_category_id, event_price_category_id: user.event_price_category_id,
name: user.name, name: user.booked ? user.booked.name : _.trim(user.name),
booked_id: user.booked_id, booked_id: user.booked ? user.booked.id : undefined,
booked_type: user.booked_type booked_type: user.booked ? user.booked.type : undefined,
}); });
} }
} }
@ -865,6 +960,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
$scope.reservations.push(reservation); $scope.reservations.push(reservation);
}); });
resetEventReserve(); resetEventReserve();
updateNbReservePlaces();
$scope.reserveSuccess = true; $scope.reserveSuccess = true;
$scope.coupon.applied = null; $scope.coupon.applied = null;
if ($scope.currentUser.role === 'admin') { if ($scope.currentUser.role === 'admin') {

View File

@ -0,0 +1,11 @@
'use strict';
Application.Services.factory('Child', ['$resource', function ($resource) {
return $resource('/api/children/:id',
{ id: '@id' }, {
update: {
method: 'PUT'
}
}
);
}]);

View File

@ -122,7 +122,20 @@
<div class="col-sm-12 m-b" ng-if="event.event_type === 'nominative' && reserve.nbReservePlaces > 0"> <div class="col-sm-12 m-b" ng-if="event.event_type === 'nominative' && reserve.nbReservePlaces > 0">
<div ng-repeat="user in reserve.bookingUsers.normal"> <div ng-repeat="user in reserve.bookingUsers.normal">
<label class="" translate>{{ 'app.public.events_show.last_name_and_first_name '}}</label> <label class="" translate>{{ 'app.public.events_show.last_name_and_first_name '}}</label>
<input type="text" class="form-control" ng-model="user.name"> <input type="text" class="form-control" ng-model="user.name" ng-required="true">
</div>
</div>
<div class="col-sm-12 m-b" ng-if="event.event_type === 'family' && reserve.nbReservePlaces > 0">
<div ng-repeat="user in reserve.bookingUsers.normal">
<label class="" translate>{{ 'app.public.events_show.last_name_and_first_name '}}</label>
<select ng-model="user.booked"
ng-options="option.name for option in user.bookedUsers track by option.key"
ng-change="changeBookedUser()"
name="booked"
ng-required="true"
class="form-control">
<option value=""></option>
</select>
</div> </div>
</div> </div>
</div> </div>
@ -135,7 +148,20 @@
<div class="col-sm-12 m-b" ng-if="event.event_type === 'nominative' && reserve.tickets[price.id] > 0"> <div class="col-sm-12 m-b" ng-if="event.event_type === 'nominative' && reserve.tickets[price.id] > 0">
<div ng-repeat="user in reserve.bookingUsers[price.id]"> <div ng-repeat="user in reserve.bookingUsers[price.id]">
<label class="" translate>{{ 'app.public.events_show.last_name_and_first_name '}}</label> <label class="" translate>{{ 'app.public.events_show.last_name_and_first_name '}}</label>
<input type="text" class="form-control" ng-model="user.name"> <input type="text" class="form-control" ng-model="user.name" ng-required="true">
</div>
</div>
<div class="col-sm-12 m-b" ng-if="event.event_type === 'family' && reserve.tickets[price.id] > 0">
<div ng-repeat="user in reserve.bookingUsers[price.id]">
<label class="" translate>{{ 'app.public.events_show.last_name_and_first_name '}}</label>
<select ng-model="user.booked"
ng-options="option.name for option in user.bookedUsers track by option.key"
ng-change="changeBookedUser()"
name="booked"
ng-required="true"
class="form-control">
<option value=""></option>
</select>
</div> </div>
</div> </div>
</div> </div>
@ -202,11 +228,11 @@
</div> </div>
</div> </div>
<div class="panel-footer no-padder ng-scope" ng-if="event.amount"> <div class="panel-footer no-padder ng-scope" ng-if="event.amount && reservationIsValid()">
<button class="btn btn-valid btn-info btn-block p-l btn-lg text-u-c r-b text-base" ng-click="payEvent()" ng-if="reserve.totalSeats > 0">{{ 'app.public.events_show.confirm_and_pay' | translate }} {{reserve.amountTotal | currency}}</button> <button class="btn btn-valid btn-info btn-block p-l btn-lg text-u-c r-b text-base" ng-click="payEvent()" ng-if="reserve.totalSeats > 0">{{ 'app.public.events_show.confirm_and_pay' | translate }} {{reserve.amountTotal | currency}}</button>
</div> </div>
<div class="panel-footer no-padder ng-scope" ng-if="event.amount == 0"> <div class="panel-footer no-padder ng-scope" ng-if="event.amount == 0 && reservationIsValid()">
<button class="btn btn-valid btn-info btn-block p-l btn-lg text-u-c r-b text-base" ng-click="validReserveEvent()" ng-if="reserve.totalSeats > 0" ng-disabled="attempting">{{ 'app.shared.buttons.confirm' | translate }}</button> <button class="btn btn-valid btn-info btn-block p-l btn-lg text-u-c r-b text-base" ng-click="validReserveEvent()" ng-if="reserve.totalSeats > 0" ng-disabled="attempting">{{ 'app.shared.buttons.confirm' | translate }}</button>
</div> </div>

View File

@ -2,13 +2,6 @@
# Check the access policies for API::ChildrenController # Check the access policies for API::ChildrenController
class ChildPolicy < ApplicationPolicy class ChildPolicy < ApplicationPolicy
# Defines the scope of the children index, depending on the current user
class Scope < Scope
def resolve
scope.where(user_id: user.id)
end
end
def index? def index?
!user.organization? !user.organization?
end end