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/controllers/api/reservations_controller.rb b/app/controllers/api/reservations_controller.rb index 3bfe28d08..260fec568 100644 --- a/app/controllers/api/reservations_controller.rb +++ b/app/controllers/api/reservations_controller.rb @@ -4,7 +4,7 @@ # Reservations are used for Training, Machine, Space and Event class API::ReservationsController < API::APIController before_action :authenticate_user! - before_action :set_reservation, only: %i[show update] + before_action :set_reservation, only: %i[show update confirm_payment] respond_to :json def index @@ -34,6 +34,16 @@ class API::ReservationsController < API::APIController end end + def confirm_payment + authorize @reservation + invoice = ReservationConfirmPaymentService.new(@reservation, current_user, params[:coupon_code], params[:offered]).call + if invoice + render :show, status: :ok, location: @reservation + else + render json: @reservation.errors, status: :unprocessable_entity + end + end + private def set_reservation diff --git a/app/controllers/api/slots_reservations_controller.rb b/app/controllers/api/slots_reservations_controller.rb index 2fcda5e83..c0f002f94 100644 --- a/app/controllers/api/slots_reservations_controller.rb +++ b/app/controllers/api/slots_reservations_controller.rb @@ -5,7 +5,7 @@ # availability by Availability.slot_duration, or otherwise globally by Setting.get('slot_duration') class API::SlotsReservationsController < API::APIController before_action :authenticate_user! - before_action :set_slots_reservation, only: %i[update cancel] + before_action :set_slots_reservation, only: %i[update cancel validate] respond_to :json def update @@ -23,6 +23,11 @@ class API::SlotsReservationsController < API::APIController SlotsReservationsService.cancel(@slot_reservation) end + def validate + authorize @slot_reservation + SlotsReservationsService.validate(@slot_reservation) + end + private def set_slots_reservation 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/controllers/admin/events.js b/app/frontend/src/javascript/controllers/admin/events.js index 6ce0cadee..ff1162224 100644 --- a/app/frontend/src/javascript/controllers/admin/events.js +++ b/app/frontend/src/javascript/controllers/admin/events.js @@ -436,7 +436,7 @@ Application.Controllers.controller('AdminEventsController', ['$scope', '$state', /** * Controller used in the reservations listing page for a specific event */ -Application.Controllers.controller('ShowEventReservationsController', ['$scope', 'eventPromise', 'reservationsPromise', function ($scope, eventPromise, reservationsPromise) { +Application.Controllers.controller('ShowEventReservationsController', ['$scope', 'eventPromise', 'reservationsPromise', 'dialogs', 'SlotsReservation', 'growl', '_t', 'Price', 'Wallet', '$uibModal', function ($scope, eventPromise, reservationsPromise, dialogs, SlotsReservation, growl, _t, Price, Wallet, $uibModal) { // retrieve the event from the ID provided in the current URL $scope.event = eventPromise; @@ -451,6 +451,170 @@ Application.Controllers.controller('ShowEventReservationsController', ['$scope', $scope.isCancelled = function (reservation) { return !!(reservation.slots_reservations_attributes[0].canceled_at); }; + + /** + * Test if the provided reservation has been validated + * @param reservation {Reservation} + * @returns {boolean} + */ + $scope.isValidated = function (reservation) { + return !!(reservation.slots_reservations_attributes[0].validated_at); + }; + + /** + * Callback to validate a reservation + * @param reservation {Reservation} + */ + $scope.validateReservation = function (reservation) { + dialogs.confirm({ + resolve: { + object: function () { + return { + title: _t('app.admin.event_reservations.validate_the_reservation'), + msg: _t('app.admin.event_reservations.do_you_really_want_to_validate_this_reservation_this_apply_to_all_booked_tickets') + }; + } + } + }, function () { // validate confirmed + SlotsReservation.validate({ + id: reservation.slots_reservations_attributes[0].id + }, () => { // successfully validated + growl.success(_t('app.admin.event_reservations.reservation_was_successfully_validated')); + const index = $scope.reservations.indexOf(reservation); + $scope.reservations[index].slots_reservations_attributes[0].validated_at = new Date(); + }, () => { + growl.warning(_t('app.admin.event_reservations.validation_failed')); + }); + }); + }; + + const mkCartItems = function (reservation, coupon) { + return { + customer_id: reservation.user_id, + items: [{ + reservation: { + ...reservation, + slots_reservations_attributes: reservation.slots_reservations_attributes.map(sr => ({ slot_id: sr.slot_id })), + tickets_attributes: reservation.tickets_attributes.map(t => ({ booked: t.booked, event_price_category_id: t.event_price_category.id })), + booking_users_attributes: reservation.booking_users_attributes.map(bu => ( + { name: bu.name, event_price_category_id: bu.event_price_category_id, booked_id: bu.booked_id, booked_type: bu.booked_type } + )) + } + }], + coupon_code: ((coupon ? coupon.code : undefined)), + payment_method: '' + }; + }; + + $scope.payReservation = function (reservation) { + const modalInstance = $uibModal.open({ + templateUrl: '/admin/events/pay_reservation_modal.html', + size: 'sm', + resolve: { + reservation () { + return reservation; + }, + price () { + return Price.compute(mkCartItems(reservation)).$promise; + }, + wallet () { + return Wallet.getWalletByUser({ user_id: reservation.user_id }).$promise; + }, + cartItems () { + return mkCartItems(reservation); + } + }, + controller: ['$scope', '$uibModalInstance', 'reservation', 'price', 'wallet', 'cartItems', 'helpers', '$filter', '_t', 'Reservation', + function ($scope, $uibModalInstance, reservation, price, wallet, cartItems, helpers, $filter, _t, Reservation) { + // User's wallet amount + $scope.wallet = wallet; + + // Price + $scope.price = price; + + // Cart items + $scope.cartItems = cartItems; + + // price to pay + $scope.amount = helpers.getAmountToPay(price.price, wallet.amount); + + // Reservation + $scope.reservation = reservation; + + $scope.coupon = { applied: null }; + + $scope.offered = false; + + $scope.payment = false; + + // Button label + $scope.setValidButtonName = function () { + if ($scope.amount > 0 && !$scope.offered) { + $scope.validButtonName = _t('app.admin.event_reservations.confirm_payment_of_html', { ROLE: $scope.currentUser.role, AMOUNT: $filter('currency')($scope.amount) }); + } else { + $scope.validButtonName = _t('app.shared.buttons.confirm'); + } + }; + + /** + * Compute the total amount for the current reservation according to the previously set parameters + */ + $scope.computeEventAmount = function () { + Price.compute(mkCartItems(reservation, $scope.coupon.applied), function (res) { + $scope.price = res; + $scope.amount = helpers.getAmountToPay($scope.price.price, wallet.amount); + $scope.setValidButtonName(); + }); + }; + + // Callback to validate the payment + $scope.ok = function () { + $scope.attempting = true; + return Reservation.confirm_payment({ + id: reservation.id, + coupon_code: $scope.coupon.applied ? $scope.coupon.applied.code : null, + offered: $scope.offered + }, function (res) { + $uibModalInstance.close(res); + return $scope.attempting = true; + } + , function (response) { + $scope.alerts = []; + angular.forEach(response, function (v, k) { + angular.forEach(v, function (err) { + $scope.alerts.push({ + msg: k + ': ' + err, + type: 'danger' + }); + }); + }); + return $scope.attempting = false; + }); + }; + + // Callback to cancel the payment + $scope.cancel = function () { $uibModalInstance.dismiss('cancel'); }; + + $scope.$watch('coupon.applied', function (newValue, oldValue) { + if ((newValue !== null) || (oldValue !== null)) { + return $scope.computeEventAmount(); + } + }); + + $scope.setValidButtonName(); + }] + }); + modalInstance.result.then(function (reservation) { + $scope.reservations = $scope.reservations.map((r) => { + if (r.id === reservation.id) { + return reservation; + } + return r; + }); + }, function () { + $log.info('Pay reservation modal dismissed at: ' + new Date()); + }); + }; }]); /** diff --git a/app/frontend/src/javascript/controllers/events.js.erb b/app/frontend/src/javascript/controllers/events.js.erb index 0c7a86483..7b983f7cb 100644 --- a/app/frontend/src/javascript/controllers/events.js.erb +++ b/app/frontend/src/javascript/controllers/events.js.erb @@ -298,10 +298,15 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', ' }; $scope.isShowReserveEventButton = () => { - return $scope.event.nb_free_places > 0 && + const bookable = $scope.event.nb_free_places > 0 && !$scope.reserve.toReserve && $scope.now.isBefore($scope.eventEndDateTime) && helpers.isUserValidatedByType($scope.ctrl.member, $scope.settings, 'event'); + if ($scope.event.pre_registration) { + return bookable && $scope.event.pre_registration_end_date && $scope.now.isSameOrBefore($scope.event.pre_registration_end_date, 'day'); + } else { + return bookable; + } }; /** 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/src/javascript/services/reservation.js b/app/frontend/src/javascript/services/reservation.js index 95273fbb4..d83e7003a 100644 --- a/app/frontend/src/javascript/services/reservation.js +++ b/app/frontend/src/javascript/services/reservation.js @@ -5,6 +5,11 @@ Application.Services.factory('Reservation', ['$resource', function ($resource) { { id: '@id' }, { update: { method: 'PUT' + }, + confirm_payment: { + method: 'POST', + url: '/api/reservations/confirm_payment', + isArray: false } } ); diff --git a/app/frontend/src/javascript/services/slots_reservation.js b/app/frontend/src/javascript/services/slots_reservation.js index 4476d6371..ed833cbb5 100644 --- a/app/frontend/src/javascript/services/slots_reservation.js +++ b/app/frontend/src/javascript/services/slots_reservation.js @@ -9,6 +9,10 @@ Application.Services.factory('SlotsReservation', ['$resource', function ($resour cancel: { method: 'PUT', url: '/api/slots_reservations/:id/cancel' + }, + validate: { + method: 'PUT', + url: '/api/slots_reservations/:id/validate' } } ); diff --git a/app/frontend/templates/admin/events/monitoring.html b/app/frontend/templates/admin/events/monitoring.html index 91b2a73b7..01d8e9285 100644 --- a/app/frontend/templates/admin/events/monitoring.html +++ b/app/frontend/templates/admin/events/monitoring.html @@ -12,8 +12,9 @@ {{ 'app.admin.events.title' }} {{ 'app.admin.events.dates' }} - {{ 'app.admin.events.booking' }} - + {{ 'app.admin.events.types' }} + {{ 'app.admin.events.booking' }} + @@ -48,8 +49,16 @@ + + {{ 'app.admin.events.event_type.standard' }} + {{ 'app.admin.events.event_type.nominative' }} + {{ 'app.admin.events.event_type.family' }} + {{ 'app.admin.events.pre_registration' }} + + {{ event.nb_total_places - event.nb_free_places }} / {{ event.nb_total_places }} +
{{'app.admin.events.NUMBER_pre_registered' | translate:{NUMBER:event.nb_places_for_pre_registration} }}
{{ 'app.admin.events.cancelled' }} {{ 'app.admin.events.without_reservation' }} @@ -57,10 +66,10 @@ diff --git a/app/frontend/templates/admin/events/pay_reservation_modal.html b/app/frontend/templates/admin/events/pay_reservation_modal.html new file mode 100644 index 000000000..766d1e68f --- /dev/null +++ b/app/frontend/templates/admin/events/pay_reservation_modal.html @@ -0,0 +1,45 @@ + + + diff --git a/app/frontend/templates/admin/events/reservations.html b/app/frontend/templates/admin/events/reservations.html index b7a04527c..02fc0b260 100644 --- a/app/frontend/templates/admin/events/reservations.html +++ b/app/frontend/templates/admin/events/reservations.html @@ -20,31 +20,49 @@ - - + + + - + + + + + + diff --git a/app/frontend/templates/events/show.html b/app/frontend/templates/events/show.html index 66f217f80..421ec4aa6 100644 --- a/app/frontend/templates/events/show.html +++ b/app/frontend/templates/events/show.html @@ -89,6 +89,8 @@
{{ 'app.public.events_show.opening_hours' | translate }}
{{ 'app.public.events_show.all_day' }}
{{ 'app.public.events_show.from_time' | translate }} {{event.start_time}} {{ 'app.public.events_show.to_time' | translate }} {{event.end_time}}
+
{{ 'app.public.events_show.pre_registration_end_date' | translate }}
+
{{ 'app.public.events_show.ending' | translate }} {{event.pre_registration_end_date | amDateFormat:'L'}}
@@ -193,7 +195,7 @@
-
+
{{ 'app.shared.buttons.cancel' }}
-
{{ 'app.public.events_show.thank_you_your_payment_has_been_successfully_registered' | translate }}
+
{{ 'app.public.events_show.thank_you_your_payment_has_been_successfully_registered' | translate }}
+ {{ 'app.public.events_show.you_can_find_your_reservation_s_details_on_your_' | translate }} {{ 'app.public.events_show.dashboard' }} +
+
{{ 'app.public.events_show.thank_you_your_pre_registration_has_been_successfully_saved' | translate }}
{{ 'app.public.events_show.you_can_find_your_reservation_s_details_on_your_' | translate }} {{ 'app.public.events_show.dashboard' }}
-
{{ 'app.public.events_show.you_booked_DATE' | translate:{DATE:(reservation.created_at | amDateFormat:'L LT')} }}
+
{{ 'app.public.events_show.you_booked_DATE' | translate:{DATE:(reservation.created_at | amDateFormat:'L LT')} }}
+
{{ 'app.public.events_show.you_pre_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} }} @@ -243,7 +249,10 @@ {{ 'app.public.events_show.thanks_for_coming' }} {{ 'app.public.events_show.view_event_list' }}
- +

@@ -251,15 +260,15 @@

- +
-
{{ 'app.admin.event_reservations.user' }}{{ 'app.admin.event_reservations.payment_date' }}{{ 'app.admin.event_reservations.booked_by' }}{{ 'app.admin.event_reservations.reservations' }}{{ 'app.admin.event_reservations.date' }} {{ 'app.admin.event_reservations.reserved_tickets' }}{{ 'app.admin.event_reservations.status' }}{{ 'app.admin.event_reservations.gestion' }}
- {{ reservation.user_full_name }} + {{ reservation.user_full_name }} + + {{ reservation.user_full_name }}
- {{bu.name}} - {{bu.name}} + {{bu.name}}
{{ reservation.created_at | amDateFormat:'LL LTS' }} {{ 'app.admin.event_reservations.full_price_' | translate }} {{reservation.nb_reserve_places}}
{{ticket.event_price_category.price_category.name}} : {{ticket.booked}} -
{{ 'app.admin.event_reservations.canceled' }}
+
+ {{ 'app.admin.event_reservations.event_status.pre_registered' }} + {{ 'app.admin.event_reservations.event_status.to_pay' }} + {{ 'app.admin.event_reservations.event_status.paid' }} + {{ 'app.admin.event_reservations.event_status.canceled' }} + + +