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

Merge branch 'pre_inscription' into staging

This commit is contained in:
Du Peng 2023-06-27 19:05:36 +02:00
commit 57e808e992
40 changed files with 715 additions and 112 deletions

View File

@ -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, 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, :amount, :nb_total_places, :availability_id, :all_day, :recurrence,
:recurrence_end_at, :category_id, :event_theme_ids, :age_range_id, :event_type, :recurrence_end_at, :category_id, :event_theme_ids, :age_range_id, :event_type,
:pre_registration, :pre_registration_end_date,
event_theme_ids: [], event_theme_ids: [],
event_image_attributes: %i[id attachment], event_image_attributes: %i[id attachment],
event_files_attributes: %i[id attachment _destroy], event_files_attributes: %i[id attachment _destroy],

View File

@ -4,7 +4,7 @@
# Reservations are used for Training, Machine, Space and Event # Reservations are used for Training, Machine, Space and Event
class API::ReservationsController < API::APIController class API::ReservationsController < API::APIController
before_action :authenticate_user! 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 respond_to :json
def index def index
@ -34,6 +34,16 @@ class API::ReservationsController < API::APIController
end end
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 private
def set_reservation def set_reservation

View File

@ -5,7 +5,7 @@
# availability by Availability.slot_duration, or otherwise globally by Setting.get('slot_duration') # availability by Availability.slot_duration, or otherwise globally by Setting.get('slot_duration')
class API::SlotsReservationsController < API::APIController class API::SlotsReservationsController < API::APIController
before_action :authenticate_user! 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 respond_to :json
def update def update
@ -23,6 +23,11 @@ class API::SlotsReservationsController < API::APIController
SlotsReservationsService.cancel(@slot_reservation) SlotsReservationsService.cancel(@slot_reservation)
end end
def validate
authorize @slot_reservation
SlotsReservationsService.validate(@slot_reservation)
end
private private
def set_slots_reservation def set_slots_reservation

View File

@ -55,6 +55,7 @@ export const EventForm: React.FC<EventFormProps> = ({ action, event, onError, on
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); const [isActiveFamilyAccount, setIsActiveFamilyAccount] = useState<boolean>(false);
const [isAcitvePreRegistration, setIsActivePreRegistration] = useState<boolean>(event?.pre_registration);
useEffect(() => { useEffect(() => {
EventCategoryAPI.index() EventCategoryAPI.index()
@ -241,6 +242,19 @@ export const EventForm: React.FC<EventFormProps> = ({ action, event, onError, on
formState={formState} formState={formState}
options={ageRangeOptions} options={ageRangeOptions}
label={t('app.admin.event_form.age_range')} />} label={t('app.admin.event_form.age_range')} />}
<FormSwitch control={control}
id="pre_registration"
label={t('app.admin.event_form.pre_registration')}
formState={formState}
tooltip={t('app.admin.event_form.pre_registration_help')}
onChange={setIsActivePreRegistration} />
{isAcitvePreRegistration &&
<FormInput id="pre_registration_end_date"
type="date"
register={register}
formState={formState}
label={t('app.admin.event_form.pre_registration_end_date')} />
}
</div> </div>
</section> </section>

View File

@ -436,7 +436,7 @@ Application.Controllers.controller('AdminEventsController', ['$scope', '$state',
/** /**
* Controller used in the reservations listing page for a specific event * 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 // retrieve the event from the ID provided in the current URL
$scope.event = eventPromise; $scope.event = eventPromise;
@ -451,6 +451,170 @@ Application.Controllers.controller('ShowEventReservationsController', ['$scope',
$scope.isCancelled = function (reservation) { $scope.isCancelled = function (reservation) {
return !!(reservation.slots_reservations_attributes[0].canceled_at); 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());
});
};
}]); }]);
/** /**

View File

@ -298,10 +298,15 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
}; };
$scope.isShowReserveEventButton = () => { $scope.isShowReserveEventButton = () => {
return $scope.event.nb_free_places > 0 && const bookable = $scope.event.nb_free_places > 0 &&
!$scope.reserve.toReserve && !$scope.reserve.toReserve &&
$scope.now.isBefore($scope.eventEndDateTime) && $scope.now.isBefore($scope.eventEndDateTime) &&
helpers.isUserValidatedByType($scope.ctrl.member, $scope.settings, 'event'); 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;
}
}; };
/** /**

View File

@ -66,6 +66,8 @@ export interface Event {
recurrence_end_at: Date, recurrence_end_at: Date,
advanced_accounting_attributes?: AdvancedAccounting, advanced_accounting_attributes?: AdvancedAccounting,
event_type: EventType, event_type: EventType,
pre_registration?: boolean,
pre_registration_end_date?: TDateISODate | Date,
} }
export interface EventDecoration { export interface EventDecoration {

View File

@ -5,6 +5,11 @@ Application.Services.factory('Reservation', ['$resource', function ($resource) {
{ id: '@id' }, { { id: '@id' }, {
update: { update: {
method: 'PUT' method: 'PUT'
},
confirm_payment: {
method: 'POST',
url: '/api/reservations/confirm_payment',
isArray: false
} }
} }
); );

View File

@ -9,6 +9,10 @@ Application.Services.factory('SlotsReservation', ['$resource', function ($resour
cancel: { cancel: {
method: 'PUT', method: 'PUT',
url: '/api/slots_reservations/:id/cancel' url: '/api/slots_reservations/:id/cancel'
},
validate: {
method: 'PUT',
url: '/api/slots_reservations/:id/validate'
} }
} }
); );

View File

@ -12,8 +12,9 @@
<tr> <tr>
<th style="width:30%" translate>{{ 'app.admin.events.title' }}</th> <th style="width:30%" translate>{{ 'app.admin.events.title' }}</th>
<th style="width:30%" translate>{{ 'app.admin.events.dates' }}</th> <th style="width:30%" translate>{{ 'app.admin.events.dates' }}</th>
<th style="width:15%" translate>{{ 'app.admin.events.types' }}</th>
<th style="width:10%" translate>{{ 'app.admin.events.booking' }}</th> <th style="width:10%" translate>{{ 'app.admin.events.booking' }}</th>
<th style="width:30%"></th> <th style="width:15%"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -48,8 +49,16 @@
</span> </span>
</td> </td>
<td>
<span ng-if="event.event_type === 'standard'" class="v-middle badge text-base bg-stage" translate="">{{ 'app.admin.events.event_type.standard' }}</span>
<span ng-if="event.event_type === 'nominative'" class="v-middle badge text-base bg-event" translate="">{{ 'app.admin.events.event_type.nominative' }}</span>
<span ng-if="event.event_type === 'family'" class="v-middle badge text-base bg-atelier" translate="">{{ 'app.admin.events.event_type.family' }}</span>
<span ng-if="event.pre_registration" class="v-middle badge text-base bg-info" translate="">{{ 'app.admin.events.pre_registration' }}</span>
</td>
<td style="vertical-align:middle"> <td style="vertical-align:middle">
<span class="ng-binding" ng-if="event.nb_total_places > 0">{{ event.nb_total_places - event.nb_free_places }} / {{ event.nb_total_places }}</span> <span class="ng-binding" ng-if="event.nb_total_places > 0">{{ event.nb_total_places - event.nb_free_places }} / {{ event.nb_total_places }}</span>
<div class="ng-binding" ng-if="event.pre_registration">{{'app.admin.events.NUMBER_pre_registered' | translate:{NUMBER:event.nb_places_for_pre_registration} }}</div>
<span class="badge font-sbold cancelled" ng-if="event.nb_total_places == -1" translate>{{ 'app.admin.events.cancelled' }}</span> <span class="badge font-sbold cancelled" ng-if="event.nb_total_places == -1" translate>{{ 'app.admin.events.cancelled' }}</span>
<span class="badge font-sbold" ng-if="!event.nb_total_places" translate>{{ 'app.admin.events.without_reservation' }}</span> <span class="badge font-sbold" ng-if="!event.nb_total_places" translate>{{ 'app.admin.events.without_reservation' }}</span>
</td> </td>
@ -57,10 +66,10 @@
<td style="vertical-align:middle"> <td style="vertical-align:middle">
<div class="buttons"> <div class="buttons">
<a class="btn btn-default" ui-sref="app.admin.event_reservations({id: event.id})"> <a class="btn btn-default" ui-sref="app.admin.event_reservations({id: event.id})">
<i class="fa fa-bookmark"></i> {{ 'app.admin.events.view_reservations' | translate }} <i class="fa fa-eye"></i>
</a> </a>
<a class="btn btn-default" ui-sref="app.admin.events_edit({id: event.id})"> <a class="btn btn-default" ui-sref="app.admin.events_edit({id: event.id})">
<i class="fa fa-edit"></i> {{ 'app.shared.buttons.edit' | translate }} <i class="fa fa-edit"></i>
</a> </a>
</div> </div>
</td> </td>

View File

@ -0,0 +1,45 @@
<div class="modal-header">
<img ng-src="{{logoBlack.custom_asset_file_attributes.attachment_url}}" alt="{{logo.custom_asset_file_attributes.attachment}}" class="modal-logo"/>
<h1 translate ng-show="reservation">{{ 'app.admin.event_reservations.confirm_payment' }}</h1>
</div>
<div class="modal-body">
<uib-alert ng-repeat="alert in alerts" type="{{alert.type}}" close="closeAlert($index)">{{alert.msg}}</uib-alert>
<div class="row" ng-show="!offered">
<wallet-info current-user="currentUser"
cart="cartItems"
price="price.price"
wallet="wallet"/>
</div>
<div class="row m-b">
<div class="col-md-12">
<label for="offerSlot" class="control-label m-r" translate>{{ 'app.admin.event_reservations.offer_this_reservation' }}</label>
<input bs-switch
ng-model="offered"
id="offerSlot"
type="checkbox"
class="form-control"
switch-on-text="{{ 'app.shared.buttons.yes' | translate }}"
switch-off-text="{{ 'app.shared.buttons.no' | translate }}"
switch-animate="true"
ng-change="computeEventAmount()"/>
</div>
</div>
<coupon show="true" coupon="coupon.applied" total="price.price_without_coupon" user-id="{{reservation.user_id}}"></coupon>
<div class="row">
<div class="form-group col-sm-12">
<div class="checkbox-group">
<input type="checkbox"
name="paymentReceived"
id="paymentReceived"
ng-model="payment" />
<label for="paymentReceived" translate>{{ 'app.admin.event_reservations.i_have_received_the_payment' }}</label>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-info" ng-click="ok()" ng-disabled="attempting || !payment" ng-bind-html="validButtonName"></button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>

View File

@ -20,31 +20,49 @@
<table class="table" ng-if="reservations.length > 0"> <table class="table" ng-if="reservations.length > 0">
<thead> <thead>
<tr> <tr>
<th style="width:25%" translate>{{ 'app.admin.event_reservations.user' }}</th> <th style="width:25%" translate>{{ 'app.admin.event_reservations.booked_by' }}</th>
<th style="width:25%" translate>{{ 'app.admin.event_reservations.payment_date' }}</th> <th style="width:25%" translate>{{ 'app.admin.event_reservations.reservations' }}</th>
<th style="width:25%" translate>{{ 'app.admin.event_reservations.date' }}</th>
<th style="width:25%" translate>{{ 'app.admin.event_reservations.reserved_tickets' }}</th> <th style="width:25%" translate>{{ 'app.admin.event_reservations.reserved_tickets' }}</th>
<th style="width:25%"></th> <th ng-if="event.pre_registration" style="width:25%" translate>{{ 'app.admin.event_reservations.status' }}</th>
<th ng-if="event.pre_registration" style="width:25%" translate>{{ 'app.admin.event_reservations.gestion' }}</th>
<th style="width:5%"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr ng-repeat="reservation in reservations" ng-class="{'disabled': isCancelled(reservation)}"> <tr ng-repeat="reservation in reservations" ng-class="{'disabled': isCancelled(reservation)}">
<td class="text-c"> <td class="text-c">
<a ui-sref="app.logged.members_show({id: reservation.user_id})" ng-if="event.event_type === 'standard'">{{ reservation.user_full_name }} </a> <a ui-sref="app.logged.members_show({id: reservation.user_id})">{{ reservation.user_full_name }} </a>
</td>
<td>
<span ng-if="event.event_type === 'standard'">{{ reservation.user_full_name }} </span>
<div ng-repeat="bu in reservation.booking_users_attributes"> <div ng-repeat="bu in reservation.booking_users_attributes">
<span ng-if="bu.booked_type !== 'User'">{{bu.name}}</span> <span>{{bu.name}}</span>
<a ui-sref="app.logged.members_show({id: bu.booked_id})" ng-if="bu.booked_type === 'User'">{{bu.name}}</a>
</div> </div>
</td> </td>
<td>{{ reservation.created_at | amDateFormat:'LL LTS' }}</td> <td>{{ reservation.created_at | amDateFormat:'LL LTS' }}</td>
<td> <td>
<span ng-if="reservation.nb_reserve_places > 0">{{ 'app.admin.event_reservations.full_price_' | translate }} {{reservation.nb_reserve_places}}<br/></span> <span ng-if="reservation.nb_reserve_places > 0">{{ 'app.admin.event_reservations.full_price_' | translate }} {{reservation.nb_reserve_places}}<br/></span>
<span ng-repeat="ticket in reservation.tickets_attributes">{{ticket.event_price_category.price_category.name}} : {{ticket.booked}}</span> <span ng-repeat="ticket in reservation.tickets_attributes">{{ticket.event_price_category.price_category.name}} : {{ticket.booked}}</span>
<div ng-show="isCancelled(reservation)" class="canceled-marker" translate>{{ 'app.admin.event_reservations.canceled' }}</div> </td>
<td ng-if="event.pre_registration">
<span ng-if="!isValidated(reservation) && !isCancelled(reservation) && !reservation.is_paid" class="v-middle badge text-base bg-info" translate="">{{ 'app.admin.event_reservations.event_status.pre_registered' }}</span>
<span ng-if="isValidated(reservation) && !isCancelled(reservation) && !reservation.is_paid" class="v-middle badge text-base bg-stage" translate="">{{ 'app.admin.event_reservations.event_status.to_pay' }}</span>
<span ng-if="reservation.is_paid && !isCancelled(reservation)" class="v-middle badge text-base bg-success" translate="">{{ 'app.admin.event_reservations.event_status.paid' }}</span>
<span ng-if="isCancelled(reservation)" class="v-middle badge text-base bg-event" translate="">{{ 'app.admin.event_reservations.event_status.canceled' }}</span>
</td>
<td ng-if="event.pre_registration">
<button class="btn btn-default" ng-click="validateReservation(reservation)" ng-if="!isValidated(reservation) && !isCancelled(reservation) && !reservation.is_paid" translate>
{{ 'app.admin.event_reservations.validate' }}
</button>
<button class="btn btn-default" ng-click="payReservation(reservation)" ng-if="isValidated(reservation) && !isCancelled(reservation) && !reservation.is_paid" translate>
{{ 'app.admin.event_reservations.pay' }}
</button>
</td> </td>
<td> <td>
<div class="buttons"> <div class="buttons">
<button class="btn btn-default" ui-sref="app.public.events_show({id: event.id})"> <button class="btn btn-default" ui-sref="app.public.events_show({id: event.id})">
<i class="fa fa-tag"></i> {{ 'app.admin.event_reservations.show_the_event' | translate }} <i class="fa fa-eye"></i>
</button> </button>
</div> </div>
</td> </td>

View File

@ -89,6 +89,8 @@
<dt><i class="fas fa-clock"></i> {{ 'app.public.events_show.opening_hours' | translate }}</dt> <dt><i class="fas fa-clock"></i> {{ 'app.public.events_show.opening_hours' | translate }}</dt>
<dd ng-if="event.all_day"><span translate>{{ 'app.public.events_show.all_day' }}</span></dd> <dd ng-if="event.all_day"><span translate>{{ 'app.public.events_show.all_day' }}</span></dd>
<dd ng-if="!event.all_day">{{ 'app.public.events_show.from_time' | translate }} <span class="text-u-l">{{event.start_time}}</span> {{ 'app.public.events_show.to_time' | translate }} <span class="text-u-l">{{event.end_time}}</span></dd> <dd ng-if="!event.all_day">{{ 'app.public.events_show.from_time' | translate }} <span class="text-u-l">{{event.start_time}}</span> {{ 'app.public.events_show.to_time' | translate }} <span class="text-u-l">{{event.end_time}}</span></dd>
<dt ng-if="event.pre_registration_end_date"><i class="fa fa-calendar" aria-hidden="true"></i> {{ 'app.public.events_show.pre_registration_end_date' | translate }}</dt>
<dd ng-if="event.pre_registration_end_date">{{ 'app.public.events_show.ending' | translate }} <span class="text-u-l">{{event.pre_registration_end_date | amDateFormat:'L'}}</span></dd>
</dl> </dl>
<div class="text-sm" ng-if="event.amount"> <div class="text-sm" ng-if="event.amount">
@ -193,7 +195,7 @@
</div> </div>
</div> </div>
<div ng-show="currentUser.role == 'admin'" class="m-t"> <div ng-show="currentUser.role == 'admin' && !event.pre_registration" class="m-t">
<label for="offerSlot" class="control-label m-r" translate>{{ 'app.public.events_show.make_a_gift_of_this_reservation' }}</label> <label for="offerSlot" class="control-label m-r" translate>{{ 'app.public.events_show.make_a_gift_of_this_reservation' }}</label>
<input bs-switch <input bs-switch
ng-model="event.offered" ng-model="event.offered"
@ -215,12 +217,16 @@
<a class="pull-right m-t-xs text-u-l ng-scope" ng-click="cancelReserve($event)" ng-show="reserve.toReserve" translate>{{ 'app.shared.buttons.cancel' }}</a> <a class="pull-right m-t-xs text-u-l ng-scope" ng-click="cancelReserve($event)" ng-show="reserve.toReserve" translate>{{ 'app.shared.buttons.cancel' }}</a>
</div> </div>
<div ng-if="reserveSuccess" class="alert alert-success">{{ 'app.public.events_show.thank_you_your_payment_has_been_successfully_registered' | translate }}<br> <div ng-if="reserveSuccess && !event.pre_registration" class="alert alert-success">{{ 'app.public.events_show.thank_you_your_payment_has_been_successfully_registered' | translate }}<br>
{{ 'app.public.events_show.you_can_find_your_reservation_s_details_on_your_' | translate }} <a ui-sref="app.logged.dashboard.invoices" translate>{{ 'app.public.events_show.dashboard' }}</a>
</div>
<div ng-if="reserveSuccess && event.pre_registration" class="alert alert-success">{{ 'app.public.events_show.thank_you_your_pre_registration_has_been_successfully_saved' | translate }}<br>
{{ 'app.public.events_show.you_can_find_your_reservation_s_details_on_your_' | translate }} <a ui-sref="app.logged.dashboard.invoices" translate>{{ 'app.public.events_show.dashboard' }}</a> {{ 'app.public.events_show.you_can_find_your_reservation_s_details_on_your_' | translate }} <a ui-sref="app.logged.dashboard.invoices" translate>{{ 'app.public.events_show.dashboard' }}</a>
</div> </div>
<div class="m-t-sm" ng-if="reservations && !reserve.toReserve" ng-repeat="reservation in reservations"> <div class="m-t-sm" ng-if="reservations && !reserve.toReserve" ng-repeat="reservation in reservations">
<div ng-hide="isCancelled(reservation)" class="well well-warning"> <div ng-hide="isCancelled(reservation)" class="well well-warning">
<div class="font-sbold text-u-c text-sm">{{ 'app.public.events_show.you_booked_DATE' | translate:{DATE:(reservation.created_at | amDateFormat:'L LT')} }}</div> <div class="font-sbold text-u-c text-sm" ng-if="!event.pre_registration">{{ 'app.public.events_show.you_booked_DATE' | translate:{DATE:(reservation.created_at | amDateFormat:'L LT')} }}</div>
<div class="font-sbold text-u-c text-sm" ng-if="event.pre_registration">{{ 'app.public.events_show.you_pre_booked_DATE' | translate:{DATE:(reservation.created_at | amDateFormat:'L LT')} }}</div>
<div class="font-sbold text-sm" ng-if="reservation.nb_reserve_places > 0">{{ 'app.public.events_show.full_price_' | translate }} {{reservation.nb_reserve_places}} {{ 'app.public.events_show.ticket' | translate:{NUMBER:reservation.nb_reserve_places} }}</div> <div class="font-sbold text-sm" ng-if="reservation.nb_reserve_places > 0">{{ 'app.public.events_show.full_price_' | translate }} {{reservation.nb_reserve_places}} {{ 'app.public.events_show.ticket' | translate:{NUMBER:reservation.nb_reserve_places} }}</div>
<div class="font-sbold text-sm" ng-repeat="ticket in reservation.tickets_attributes"> <div class="font-sbold text-sm" ng-repeat="ticket in reservation.tickets_attributes">
{{ticket.event_price_category.price_category.name}} : {{ticket.booked}} {{ 'app.public.events_show.ticket' | translate:{NUMBER:ticket.booked} }} {{ticket.event_price_category.price_category.name}} : {{ticket.booked}} {{ 'app.public.events_show.ticket' | translate:{NUMBER:ticket.booked} }}
@ -243,7 +249,10 @@
<span ng-show="reservations.length > 0" translate>{{ 'app.public.events_show.thanks_for_coming' }}</span> <span ng-show="reservations.length > 0" translate>{{ 'app.public.events_show.thanks_for_coming' }}</span>
<a ui-sref="app.public.events_list" translate>{{ 'app.public.events_show.view_event_list' }}</a> <a ui-sref="app.public.events_list" translate>{{ 'app.public.events_show.view_event_list' }}</a>
</div> </div>
<button class="btn btn-warning-full rounded btn-block text-sm" ng-click="reserveEvent()" ng-show="isShowReserveEventButton()">{{ 'app.public.events_show.book' | translate }}</button> <button class="btn btn-warning-full rounded btn-block text-sm" ng-click="reserveEvent()" ng-show="isShowReserveEventButton()">
<span ng-if="event.pre_registration">{{ 'app.public.events_show.pre_book' | translate }}</span>
<span ng-if="!event.pre_registration">{{ 'app.public.events_show.book' | translate }}</span>
</button>
<uib-alert type="danger" ng-if="ctrl.member.id && !isUserValidatedByType()"> <uib-alert type="danger" ng-if="ctrl.member.id && !isUserValidatedByType()">
<p class="text-sm"> <p class="text-sm">
<i class="fa fa-warning"></i> <i class="fa fa-warning"></i>
@ -251,15 +260,15 @@
</p> </p>
</uib-alert> </uib-alert>
<coupon show="reserve.totalSeats > 0 && ctrl.member" coupon="coupon.applied" total="reserve.totalNoCoupon" user-id="{{ctrl.member.id}}"></coupon> <coupon show="reserve.totalSeats > 0 && ctrl.member && !event.pre_registration" coupon="coupon.applied" total="reserve.totalNoCoupon" user-id="{{ctrl.member.id}}"></coupon>
</div> </div>
</div> </div>
<div class="panel-footer no-padder ng-scope" ng-if="event.amount && reservationIsValid()"> <div class="panel-footer no-padder ng-scope" ng-if="!event.pre_registration && 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 && reservationIsValid()"> <div class="panel-footer no-padder ng-scope" ng-if="(event.pre_registration || 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

@ -89,12 +89,21 @@ class Event < ApplicationRecord
else else
reserved_places = reservations.joins(:slots_reservations) reserved_places = reservations.joins(:slots_reservations)
.where('slots_reservations.canceled_at': nil) .where('slots_reservations.canceled_at': nil)
.where.not('slots_reservations.validated_at': nil)
.map(&:total_booked_seats) .map(&:total_booked_seats)
.inject(0) { |sum, t| sum + t } .inject(0) { |sum, t| sum + t }
self.nb_free_places = (nb_total_places - reserved_places) self.nb_free_places = (nb_total_places - reserved_places)
end end
end end
def nb_places_for_pre_registration
reservations.joins(:slots_reservations)
.where('slots_reservations.canceled_at': nil)
.where('slots_reservations.validated_at': nil)
.map(&:total_booked_seats)
.inject(0) { |sum, t| sum + t }
end
def all_day? def all_day?
availability.start_at.hour.zero? availability.start_at.hour.zero?
end end

View File

@ -60,6 +60,16 @@ class ShoppingCart
items.each do |item| items.each do |item|
objects.push(save_item(item)) objects.push(save_item(item))
end end
event_reservation = objects.find { |o| o.is_a?(Reservation) && o.reservable_type == 'Event' }
if event_reservation&.reservable&.pre_registration
payment = Invoice.new(
invoicing_profile: @customer.invoicing_profile,
statistic_profile: @customer.statistic_profile,
operator_profile_id: @operator.invoicing_profile.id,
payment_method: @payment_method,
total: 0
)
else
update_credits(objects) update_credits(objects)
update_packs(objects) update_packs(objects)
@ -70,6 +80,7 @@ class ShoppingCart
payment.save payment.save
payment.post_save(payment_id, payment_type) payment.post_save(payment_id, payment_type)
end end
end
success = !payment.nil? && objects.map(&:errors).flatten.map(&:empty?).all? && items.map(&:errors).map(&:blank?).all? success = !payment.nil? && objects.map(&:errors).flatten.map(&:empty?).all? && items.map(&:errors).map(&:blank?).all?
errors = objects.map(&:errors).flatten.concat(items.map(&:errors)) errors = objects.map(&:errors).flatten.concat(items.map(&:errors))

View File

@ -5,7 +5,8 @@ class LocalPaymentPolicy < ApplicationPolicy
def confirm_payment? def confirm_payment?
# only admins and managers can offer free extensions of a subscription # only admins and managers can offer free extensions of a subscription
has_free_days = record.shopping_cart.items.any? { |item| item.is_a? CartItem::FreeExtension } has_free_days = record.shopping_cart.items.any? { |item| item.is_a? CartItem::FreeExtension }
event = record.shopping_cart.items.find { |item| item.is_a? CartItem::EventReservation }
((user.admin? || user.manager?) && record.shopping_cart.customer.id != user.id) || (record.price.zero? && !has_free_days) ((user.admin? || user.manager?) && record.shopping_cart.customer.id != user.id) || (record.price.zero? && !has_free_days) || event&.reservable&.pre_registration
end end
end end

View File

@ -9,4 +9,8 @@ class ReservationPolicy < ApplicationPolicy
def update? def update?
user.admin? || user.manager? || record.user == user user.admin? || user.manager? || record.user == user
end end
def confirm_payment?
user.admin? || user.manager?
end
end end

View File

@ -15,4 +15,8 @@ class SlotsReservationPolicy < ApplicationPolicy
def cancel? def cancel?
user.admin? || user.manager? || record.reservation.user == user user.admin? || user.manager? || record.reservation.user == user
end end
def validate?
user.admin? || user.manager?
end
end end

View File

@ -0,0 +1,82 @@
# frozen_string_literal: true
# confirm payment of a pre-registration reservation
class ReservationConfirmPaymentService
def initialize(reservation, operator, coupon, offered)
@reservation = reservation
@operator = operator
@offered = offered
@coupon = CartItem::Coupon.new(
customer_profile: @reservation.user.invoicing_profile,
operator_profile: @operator.invoicing_profile,
coupon: Coupon.find_by(code: coupon)
)
end
def total
slots_reservations = @reservation.slots_reservations.map do |sr|
{
slot_id: sr.slot_id,
offered: @offered
}
end
tickets = @reservation.tickets.map do |t|
{
event_price_category_id: t.event_price_category_id,
booked: t.booked
}
end
booking_users = @reservation.booking_users.map do |bu|
{
name: bu.name,
event_price_category_id: bu.event_price_category_id,
booked_id: bu.booked_id,
booked_type: bu.booked_type
}
end
event_reservation = CartItem::EventReservation.new(customer_profile: @reservation.user.invoicing_profile,
operator_profile: @operator.invoicing_profile,
event: @reservation.reservable,
cart_item_reservation_slots_attributes: slots_reservations,
normal_tickets: @reservation.nb_reserve_places,
cart_item_event_reservation_tickets_attributes: tickets,
cart_item_event_reservation_booking_users_attributes: booking_users)
all_elements = {
slots: @reservation.slots_reservations.map do |sr|
{ start_at: sr.slot.start_at, end_at: sr.slot.end_at, price: event_reservation.price[:amount] }
end
}
total_amount = event_reservation.price[:amount]
coupon_info = @coupon.price(total_amount)
# return result
{
elements: all_elements,
total: coupon_info[:total_with_coupon].to_i,
before_coupon: coupon_info[:total_without_coupon].to_i,
coupon: @coupon.coupon
}
end
def call
price = total
invoice = InvoicesService.create(
price,
@operator.invoicing_profile.id,
[@reservation],
@reservation.user
)
return invoice if Setting.get('prevent_invoices_zero') && price[:total].zero?
ActiveRecord::Base.transaction do
WalletService.debit_user_wallet(invoice, @reservation.user)
invoice.save
invoice.post_save
end
invoice
end
end

View File

@ -21,5 +21,23 @@ class SlotsReservationsService
rescue Faraday::ConnectionFailed rescue Faraday::ConnectionFailed
warn 'Unable to update data in elasticsearch' warn 'Unable to update data in elasticsearch'
end end
def validate(slot_reservation)
if slot_reservation.update(validated_at: Time.current)
reservable = slot_reservation.reservation.reservable
if reservable.is_a?(Event)
reservable.update_nb_free_places
reservable.save
end
NotificationCenter.call type: 'notify_member_reservation_validated',
receiver: slot_reservation.reservation.user,
attached_object: slot_reservation.reservation
NotificationCenter.call type: 'notify_admin_reservation_validated',
receiver: User.admins_and_managers,
attached_object: slot_reservation.reservation
return true
end
false
end
end end
end end

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
json.extract! event, :id, :title, :description, :event_type json.extract! event, :id, :title, :description, :event_type, :pre_registration, :pre_registration_end_date
json.pre_registration_end_date event.pre_registration_end_date&.to_date
json.nb_places_for_pre_registration event.nb_places_for_pre_registration
if event.event_image if event.event_image
json.event_image_attributes do json.event_image_attributes do
json.id event.event_image.id json.id event.event_image.id

View File

@ -11,8 +11,8 @@ json.is_online_card invoice.paid_by_card?
json.date invoice.is_a?(Avoir) ? invoice.avoir_date : invoice.created_at json.date invoice.is_a?(Avoir) ? invoice.avoir_date : invoice.created_at
json.chained_footprint invoice.check_footprint json.chained_footprint invoice.check_footprint
json.main_object do json.main_object do
json.type invoice.invoice_items.find(&:main).object_type json.type invoice.invoice_items.find(&:main)&.object_type
json.id invoice.invoice_items.find(&:main).object_id json.id invoice.invoice_items.find(&:main)&.object_id
end end
json.items invoice.invoice_items do |item| json.items invoice.invoice_items do |item|
json.id item.id json.id item.id

View File

@ -0,0 +1,4 @@
json.title notification.notification_type
json.description t('.a_RESERVABLE_reservation_was_validated_html',
RESERVABLE: notification.attached_object.reservable.name,
NAME: notification.attached_object.user&.profile&.full_name || t('api.notifications.deleted_user'))

View File

@ -0,0 +1,3 @@
json.title notification.notification_type
json.description t('.your_reservation_RESERVABLE_was_validated_html',
RESERVABLE: notification.attached_object.reservable.name)

View File

@ -7,6 +7,8 @@ json.message reservation.message
json.slots_reservations_attributes reservation.slots_reservations do |sr| json.slots_reservations_attributes reservation.slots_reservations do |sr|
json.id sr.id json.id sr.id
json.canceled_at sr.canceled_at&.iso8601 json.canceled_at sr.canceled_at&.iso8601
json.validated_at sr.validated_at&.iso8601
json.slot_id sr.slot_id
json.slot_attributes do json.slot_attributes do
json.id sr.slot_id json.id sr.slot_id
json.start_at sr.slot.start_at.iso8601 json.start_at sr.slot.start_at.iso8601
@ -39,3 +41,4 @@ json.booking_users_attributes reservation.booking_users.order(booked_type: :desc
json.booked_id bu.booked_id json.booked_id bu.booked_id
json.booked_type bu.booked_type json.booked_type bu.booked_type
end end
json.is_paid reservation.invoice_items.count.positive?

View File

@ -0,0 +1,19 @@
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
<p>
<%= t('.body.reservation_validated_html',
NAME: @attached_object.user&.profile&.full_name || t('api.notifications.deleted_user'),
RESERVABLE: @attached_object.reservable.name) %>
</p>
<p><%= t('.body.reserved_slots') %></p>
<ul>
<% @attached_object.slots.each do |slot| %>
<% if @attached_object.reservable_type == 'Event' %>
<% (slot.start_at.to_date..slot.end_at.to_date).each do |d| %>
<li><%= "#{I18n.l d, format: :long} #{I18n.l slot.start_at, format: :hour_minute} - #{I18n.l slot.end_at, format: :hour_minute}" %></li>
<% end %>
<% else %>
<li><%= "#{I18n.l slot.start_at, format: :long} - #{I18n.l slot.end_at, format: :hour_minute}" %></li>
<% end %>
<% end %>
</ul>

View File

@ -0,0 +1,16 @@
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
<p><%= t('.body.reservation_validated_html', RESERVATION: @attached_object.reservable.name) %></p>
<p><%= t('.body.your_reserved_slots') %> </p>
<ul>
<% @attached_object.slots.each do |slot| %>
<% if @attached_object.reservable_type == 'Event' %>
<% (slot.start_at.to_date..slot.end_at.to_date).each do |d| %>
<li><%= "#{I18n.l d, format: :long} #{I18n.l slot.start_at, format: :hour_minute} - #{I18n.l slot.end_at, format: :hour_minute}" %></li>
<% end %>
<% else %>
<li><%= "#{I18n.l slot.start_at, format: :long} - #{I18n.l slot.end_at, format: :hour_minute}" %></li>
<% end %>
<% end %>
</ul>

View File

@ -156,6 +156,9 @@ en:
standard: "Event standard" standard: "Event standard"
nominative: "Event nominative" nominative: "Event nominative"
family: "Event family" family: "Event family"
pre_registration: "Pre-registration"
pre_registration_help: "If this option is checked, administrators and managers must validate registrations before they become final."
pre_registration_end_date: "Pre-registration end date"
plan_form: plan_form:
ACTION_title: "{ACTION, select, create{New} other{Update the}} plan" ACTION_title: "{ACTION, select, create{New} other{Update the}} plan"
tab_settings: "Settings" tab_settings: "Settings"
@ -592,6 +595,13 @@ en:
do_you_really_want_to_delete_this_price_category: "Do you really want to delete this price category?" do_you_really_want_to_delete_this_price_category: "Do you really want to delete this price category?"
price_category_successfully_deleted: "Price category successfully deleted." price_category_successfully_deleted: "Price category successfully deleted."
price_category_deletion_failed: "Price category deletion failed." price_category_deletion_failed: "Price category deletion failed."
types: "Types"
event_type:
standard: "Standard"
family: "Family"
nominative: "Nominative"
pre_registration: "Pre-registration"
NUMBER_pre_registered: " {NUMBER} pre-registered"
#add a new event #add a new event
events_new: events_new:
add_an_event: "Add an event" add_an_event: "Add an event"
@ -626,6 +636,26 @@ en:
no_reservations_for_now: "No reservation for now." no_reservations_for_now: "No reservation for now."
back_to_monitoring: "Back to monitoring" back_to_monitoring: "Back to monitoring"
canceled: "Canceled" canceled: "Canceled"
date: "Date"
booked_by: "Booked by"
reservations: "Reservations"
status: "Status"
gestion: "Gestion"
event_status:
pre_registered: "Pre-registered"
to_pay: "To pay"
paid: "Paid"
canceled: "Canceled"
validate: "Validate"
pay: "Pay"
validate_the_reservation: "Validate the reservation"
do_you_really_want_to_validate_this_reservation_this_apply_to_all_booked_tickets: "Do you really want to validate this reservation? This apply to ALL booked tickets."
reservation_was_successfully_validated: "Reservation was successfully validated."
validation_failed: "Validation failed."
confirm_payment: "Confirm payment"
confirm_payment_of_html: "{ROLE, select, admin{Cash} other{Pay}}: {AMOUNT}" #(contexte : validate a payment of $20,00)
offer_this_reservation: "I offer this reservation"
i_have_received_the_payment: "I have received the payment"
events_settings: events_settings:
title: "Settings" title: "Settings"
generic_text_block: "Editorial text block" generic_text_block: "Editorial text block"

View File

@ -156,6 +156,9 @@ fr:
standard: "Evénement standard" standard: "Evénement standard"
nominative: "Evénement nominatif" nominative: "Evénement nominatif"
family: "Evénement famille" family: "Evénement famille"
pre_registration: "Pré-inscription"
pre_registration_help: "Si cette option est cochée, les administrateurs et les gestionnaires devent valider les inscriptions avant qu'elles ne soient définitives."
pre_registration_end_date: "Date de fin de pré-inscription"
plan_form: plan_form:
ACTION_title: "{ACTION, select, create{Nouvelle} other{Mettre à jour la}} formule d'abonnement" ACTION_title: "{ACTION, select, create{Nouvelle} other{Mettre à jour la}} formule d'abonnement"
tab_settings: "Paramètres" tab_settings: "Paramètres"
@ -592,6 +595,13 @@ fr:
do_you_really_want_to_delete_this_price_category: "Êtes vous sur de vouloir supprimer cette catégorie tarifaire ?" do_you_really_want_to_delete_this_price_category: "Êtes vous sur de vouloir supprimer cette catégorie tarifaire ?"
price_category_successfully_deleted: "Catégorie tarifaire supprimée avec succès." price_category_successfully_deleted: "Catégorie tarifaire supprimée avec succès."
price_category_deletion_failed: "Échec de la suppression de la catégorie tarifaire." price_category_deletion_failed: "Échec de la suppression de la catégorie tarifaire."
types: 'Types'
event_type:
standard: 'Standard'
family: "Famille"
nominative: "Nominatif"
pre_registration: "Pré-inscription"
NUMBER_pre_registered: " {NUMBER} pré-inscrit"
#add a new event #add a new event
events_new: events_new:
add_an_event: "Ajouter un événement" add_an_event: "Ajouter un événement"
@ -626,6 +636,26 @@ fr:
no_reservations_for_now: "Aucune réservation pour le moment." no_reservations_for_now: "Aucune réservation pour le moment."
back_to_monitoring: "Retour au suivi" back_to_monitoring: "Retour au suivi"
canceled: "Annulée" canceled: "Annulée"
date: "Date"
booked_by: "Réservé par"
reservations: "Réservations"
status: "Statut"
gestion: "Gestion"
event_status:
pre_registered: "Pré-inscrit"
to_pay: "À payer"
paid: "Payé"
canceled: "Annulée"
validate: "Valider"
pay: "Payer"
validate_the_reservation: "Valider la réservation"
do_you_really_want_to_validate_this_reservation_this_apply_to_all_booked_tickets: "Êtes vous sur de vouloir valider cette réservation? Ceci s'applique à TOUTES les places réservées."
reservation_was_successfully_validated: "La réservation a bien été validé."
validation_failed: "La validation a échoué."
confirm_payment: "Confirmer le paiement"
confirm_payment_of_html: "{ROLE, select, admin{Encaisser} other{Payer}} : {AMOUNT}" #(contexte : validate a payment of $20,00)
offer_this_reservation: "J'offre cette réservation"
i_have_received_the_payment: "J'ai reçu le paiement"
events_settings: events_settings:
title: "Paramètres" title: "Paramètres"
generic_text_block: "Bloc de texte rédactionnel" generic_text_block: "Bloc de texte rédactionnel"

View File

@ -330,9 +330,11 @@ en:
ticket: "{NUMBER, plural, one{ticket} other{tickets}}" ticket: "{NUMBER, plural, one{ticket} other{tickets}}"
make_a_gift_of_this_reservation: "Make a gift of this reservation" make_a_gift_of_this_reservation: "Make a gift of this reservation"
thank_you_your_payment_has_been_successfully_registered: "Thank you. Your payment has been successfully registered!" thank_you_your_payment_has_been_successfully_registered: "Thank you. Your payment has been successfully registered!"
thank_you_your_pre_registration_has_been_successfully_saved: "Thank you. Your pre-registration has been successfully saved!"
you_can_find_your_reservation_s_details_on_your_: "You can find your reservation's details on your" you_can_find_your_reservation_s_details_on_your_: "You can find your reservation's details on your"
dashboard: "dashboard" dashboard: "dashboard"
you_booked_DATE: "You booked ({DATE}):" you_booked_DATE: "You booked ({DATE}):"
you_pre_booked_DATE: "You pre-booked ({DATE}):"
canceled_reservation_SEATS: "Reservation canceled ({SEATS} seats)" canceled_reservation_SEATS: "Reservation canceled ({SEATS} seats)"
book: "Book" book: "Book"
confirm_and_pay: "Confirm and pay" confirm_and_pay: "Confirm and pay"
@ -361,6 +363,8 @@ en:
share_on_facebook: "Share on Facebook" share_on_facebook: "Share on Facebook"
share_on_twitter: "Share on Twitter" share_on_twitter: "Share on Twitter"
last_name_and_first_name: "Last name and first name" last_name_and_first_name: "Last name and first name"
pre_book: "Pre-book"
pre_registration_end_date: "Pre-registration end date"
#public calendar #public calendar
calendar: calendar:
calendar: "Calendar" calendar: "Calendar"

View File

@ -330,9 +330,11 @@ fr:
ticket: "{NUMBER, plural, =0{place} one{place} other{places}}" ticket: "{NUMBER, plural, =0{place} one{place} other{places}}"
make_a_gift_of_this_reservation: "Offrir cette réservation" make_a_gift_of_this_reservation: "Offrir cette réservation"
thank_you_your_payment_has_been_successfully_registered: "Merci. Votre paiement a bien été pris en compte !" thank_you_your_payment_has_been_successfully_registered: "Merci. Votre paiement a bien été pris en compte !"
thank_you_your_pre_registration_has_been_successfully_saved: "Merci. Votre pré-inscription a bien été pris en compte !"
you_can_find_your_reservation_s_details_on_your_: "Vous pouvez retrouver le détail de votre réservation sur votre" you_can_find_your_reservation_s_details_on_your_: "Vous pouvez retrouver le détail de votre réservation sur votre"
dashboard: "tableau de bord" dashboard: "tableau de bord"
you_booked_DATE: "Vous avez réservé ({DATE}) :" you_booked_DATE: "Vous avez réservé ({DATE}) :"
you_pre_booked_DATE: "Vous avez pré-réservé ({DATE}) :"
canceled_reservation_SEATS: "Réservation annulée ({SEATS} places)" canceled_reservation_SEATS: "Réservation annulée ({SEATS} places)"
book: "Réserver" book: "Réserver"
confirm_and_pay: "Valider et payer" confirm_and_pay: "Valider et payer"
@ -361,6 +363,8 @@ fr:
share_on_facebook: "Partager sur Facebook" share_on_facebook: "Partager sur Facebook"
share_on_twitter: "Partager sur Twitter" share_on_twitter: "Partager sur Twitter"
last_name_and_first_name: "Nom et prénom" last_name_and_first_name: "Nom et prénom"
pre_book: "Pré-réserver"
pre_registration_end_date: "Date de fin de pré-réservation"
#public calendar #public calendar
calendar: calendar:
calendar: "Calendrier" calendar: "Calendrier"

View File

@ -469,6 +469,10 @@ en:
order_canceled: "Your command %{REFERENCE} is canceled" order_canceled: "Your command %{REFERENCE} is canceled"
notify_user_order_is_refunded: notify_user_order_is_refunded:
order_refunded: "Your command %{REFERENCE} is refunded" order_refunded: "Your command %{REFERENCE} is refunded"
notify_member_reservation_validated:
your_reservation_RESERVABLE_was_validated_html: "Your reservation <strong><em>%{RESERVABLE}</em></strong> was successfully validated."
notify_admin_reservation_validated:
a_RESERVABLE_reservation_was_validated_html: "A <strong><em>%{RESERVABLE}</em></strong> reservation of <strong><em>%{USER}</em></strong> was validated."
#statistics tools for admins #statistics tools for admins
statistics: statistics:
subscriptions: "Subscriptions" subscriptions: "Subscriptions"

View File

@ -469,6 +469,10 @@ fr:
order_canceled: "Votre commande %{REFERENCE} est annulée" order_canceled: "Votre commande %{REFERENCE} est annulée"
notify_user_order_is_refunded: notify_user_order_is_refunded:
order_refunded: "Votre commande %{REFERENCE} est remboursée" order_refunded: "Votre commande %{REFERENCE} est remboursée"
notify_member_reservation_validated:
your_reservation_RESERVABLE_was_validated_html: "Votre réservation de <strong><em>%{RESERVABLE}</em></strong> a été validée."
notify_admin_reservation_validated:
a_RESERVABLE_reservation_was_validated_html: "La réservation de <strong><em>%{RESERVABLE}</em></strong> de <strong><em>%{NAME}</em></strong> a été validée."
#statistics tools for admins #statistics tools for admins
statistics: statistics:
subscriptions: "Abonnements" subscriptions: "Abonnements"

View File

@ -451,3 +451,13 @@ en:
subject: "Your command was refunded" subject: "Your command was refunded"
body: body:
notify_user_order_is_refunded: "Your command %{REFERENCE} was refunded." notify_user_order_is_refunded: "Your command %{REFERENCE} was refunded."
notify_member_reservation_validated:
subject: "Your reservation was validated"
body:
reservation_validated_html: "<strong><em>%{RESERVABLE}</em></strong> was validated."
your_reserved_slots: "Your reserved slots are:"
notify_admin_reservation_validated:
subject: "Réservation a bien été validé"
body:
reservation_validated_html: "<strong><em>%{RESERVABLE}</em></strong> of %{NAME} was validated."
reserved_slots: "Reserved slots are:"

View File

@ -451,3 +451,13 @@ fr:
subject: "Votre commande est remboursée" subject: "Votre commande est remboursée"
body: body:
notify_user_order_is_refunded: "Votre commande %{REFERENCE} est remboursée :" notify_user_order_is_refunded: "Votre commande %{REFERENCE} est remboursée :"
notify_member_reservation_validated:
subject: "Votre réservation a bien été validé"
body:
reservation_validated_html: "Votre réservation <strong><em>%{RESERVATION}</em></strong> a bien été validé."
your_reserved_slots: "Les créneaux que vous avez réservés sont :"
notify_admin_reservation_validated:
subject: "Réservation a bien été validé"
body:
reservation_validated_html: "<strong><em>%{RESERVABLE}</em></strong> du membre %{NAME} a bien été validé."
reserved_slots: "Les créneaux réservés sont :"

View File

@ -66,7 +66,9 @@ Rails.application.routes.draw do
patch ':id/update_role', action: 'update_role', on: :collection patch ':id/update_role', action: 'update_role', on: :collection
patch ':id/validate', action: 'validate', on: :collection patch ':id/validate', action: 'validate', on: :collection
end end
resources :reservations, only: %i[show index update] resources :reservations, only: %i[show index update] do
post :confirm_payment, on: :collection
end
resources :notifications, only: %i[index show update] do resources :notifications, only: %i[index show update] do
match :update_all, path: '/', via: %i[put patch], on: :collection match :update_all, path: '/', via: %i[put patch], on: :collection
get 'polling', action: 'polling', on: :collection get 'polling', action: 'polling', on: :collection
@ -121,6 +123,7 @@ Rails.application.routes.draw do
end end
resources :slots_reservations, only: [:update] do resources :slots_reservations, only: [:update] do
put 'cancel', on: :member put 'cancel', on: :member
put 'validate', on: :member
end end
resources :events do resources :events do

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
# Add pre-registration and pre_registration_end_date to event
class AddPreRegistrationToEvent < ActiveRecord::Migration[7.0]
def change
add_column :events, :pre_registration, :boolean, default: false
add_column :events, :pre_registration_end_date, :datetime
end
end

View File

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

View File

@ -1,89 +1,104 @@
# frozen_string_literal: true # frozen_string_literal: true
unless NotificationType.find_by(name: 'notify_member_training_authorization_expired') NOTIFICATIONS_TYPES = [
NotificationType.create!( { name: 'notify_admin_when_project_published', category: 'projects', is_configurable: true },
name: 'notify_member_training_authorization_expired', { name: 'notify_project_collaborator_to_valid', category: 'projects', is_configurable: false },
category: 'trainings', { name: 'notify_project_author_when_collaborator_valid', category: 'projects', is_configurable: true },
is_configurable: false { name: 'notify_user_training_valid', category: 'trainings', is_configurable: false },
) { name: 'notify_member_subscribed_plan', category: 'subscriptions', is_configurable: false },
end { name: 'notify_member_create_reservation', category: 'agenda', is_configurable: false },
{ name: 'notify_member_subscribed_plan_is_changed', category: 'deprecated', is_configurable: false },
{ name: 'notify_admin_member_create_reservation', category: 'agenda', is_configurable: true },
{ name: 'notify_member_slot_is_modified', category: 'agenda', is_configurable: false },
{ name: 'notify_admin_slot_is_modified', category: 'agenda', is_configurable: true },
unless NotificationType.find_by(name: 'notify_member_training_invalidated') { name: 'notify_admin_when_user_is_created', category: 'users_accounts', is_configurable: true },
NotificationType.create!( { name: 'notify_admin_subscribed_plan', category: 'subscriptions', is_configurable: true },
name: 'notify_member_training_invalidated', { name: 'notify_user_when_invoice_ready', category: 'payments', is_configurable: true },
category: 'trainings', { name: 'notify_member_subscription_will_expire_in_7_days', category: 'subscriptions', is_configurable: false },
is_configurable: false { name: 'notify_member_subscription_is_expired', category: 'subscriptions', is_configurable: false },
) { name: 'notify_admin_subscription_will_expire_in_7_days', category: 'subscriptions', is_configurable: true },
end { name: 'notify_admin_subscription_is_expired', category: 'subscriptions', is_configurable: true },
{ name: 'notify_admin_subscription_canceled', category: 'subscriptions', is_configurable: true },
{ name: 'notify_member_subscription_canceled', category: 'subscriptions', is_configurable: false },
{ name: 'notify_user_when_avoir_ready', category: 'wallet', is_configurable: false },
unless NotificationType.find_by(name: 'notify_admin_order_is_paid') { name: 'notify_member_slot_is_canceled', category: 'agenda', is_configurable: false },
NotificationType.create!( { name: 'notify_admin_slot_is_canceled', category: 'agenda', is_configurable: true },
name: 'notify_admin_order_is_paid', { name: 'notify_partner_subscribed_plan', category: 'subscriptions', is_configurable: false },
category: 'shop', { name: 'notify_member_subscription_extended', category: 'subscriptions', is_configurable: false },
is_configurable: true { name: 'notify_admin_subscription_extended', category: 'subscriptions', is_configurable: true },
) { name: 'notify_admin_user_group_changed', category: 'users_accounts', is_configurable: true },
end { name: 'notify_user_user_group_changed', category: 'users_accounts', is_configurable: false },
{ name: 'notify_admin_when_user_is_imported', category: 'users_accounts', is_configurable: true },
{ name: 'notify_user_profile_complete', category: 'users_accounts', is_configurable: false },
{ name: 'notify_user_auth_migration', category: 'user', is_configurable: false },
unless NotificationType.find_by(name: 'notify_member_reservation_limit_reached') { name: 'notify_admin_user_merged', category: 'users_accounts', is_configurable: true },
NotificationType.create!( { name: 'notify_admin_profile_complete', category: 'users_accounts', is_configurable: true },
name: 'notify_member_reservation_limit_reached', { name: 'notify_admin_abuse_reported', category: 'projects', is_configurable: true },
category: 'agenda', { name: 'notify_admin_invoicing_changed', category: 'deprecated', is_configurable: false },
is_configurable: false { name: 'notify_user_wallet_is_credited', category: 'wallet', is_configurable: false },
) { name: 'notify_admin_user_wallet_is_credited', category: 'wallet', is_configurable: true },
end { name: 'notify_admin_export_complete', category: 'exports', is_configurable: false },
{ name: 'notify_member_about_coupon', category: 'agenda', is_configurable: false },
{ name: 'notify_member_reservation_reminder', category: 'agenda', is_configurable: false },
unless NotificationType.find_by(name: 'notify_admin_user_child_supporting_document_refusal') { name: 'notify_admin_free_disk_space', category: 'app_management', is_configurable: false },
NotificationType.create!( { name: 'notify_admin_close_period_reminder', category: 'accountings', is_configurable: true },
name: 'notify_admin_user_child_supporting_document_refusal', { name: 'notify_admin_archive_complete', category: 'accountings', is_configurable: true },
category: 'supporting_documents', { name: 'notify_privacy_policy_changed', category: 'app_management', is_configurable: false },
is_configurable: true { name: 'notify_admin_import_complete', category: 'app_management', is_configurable: false },
) { name: 'notify_admin_refund_created', category: 'wallet', is_configurable: true },
end { name: 'notify_admins_role_update', category: 'users_accounts', is_configurable: true },
{ name: 'notify_user_role_update', category: 'users_accounts', is_configurable: false },
{ name: 'notify_admin_objects_stripe_sync', category: 'payments', is_configurable: false },
{ name: 'notify_user_when_payment_schedule_ready', category: 'payments', is_configurable: false },
unless NotificationType.find_by(name: 'notify_user_child_supporting_document_refusal') { name: 'notify_admin_payment_schedule_failed', category: 'payments', is_configurable: true },
NotificationType.create!( { name: 'notify_member_payment_schedule_failed', category: 'payments', is_configurable: false },
name: 'notify_user_child_supporting_document_refusal', { name: 'notify_admin_payment_schedule_check_deadline', category: 'payments', is_configurable: true },
category: 'supporting_documents', { name: 'notify_admin_payment_schedule_transfer_deadline', category: 'payments', is_configurable: true },
is_configurable: false { name: 'notify_admin_payment_schedule_error', category: 'payments', is_configurable: true },
) { name: 'notify_member_payment_schedule_error', category: 'payments', is_configurable: false },
end { name: 'notify_admin_payment_schedule_gateway_canceled', category: 'payments', is_configurable: true },
{ name: 'notify_member_payment_schedule_gateway_canceled', category: 'payments', is_configurable: false },
{ name: 'notify_admin_user_supporting_document_files_created', category: 'supporting_documents', is_configurable: true },
{ name: 'notify_admin_user_supporting_document_files_updated', category: 'supporting_documents', is_configurable: true },
unless NotificationType.find_by(name: 'notify_admin_child_created') { name: 'notify_user_is_validated', category: 'users_accounts', is_configurable: false },
NotificationType.create!( { name: 'notify_user_is_invalidated', category: 'users_accounts', is_configurable: false },
name: 'notify_admin_child_created', { name: 'notify_user_supporting_document_refusal', category: 'supporting_documents', is_configurable: false },
category: 'users_accounts', { name: 'notify_admin_user_supporting_document_refusal', category: 'supporting_documents', is_configurable: true },
is_configurable: true { name: 'notify_user_order_is_ready', category: 'shop', is_configurable: false },
) { name: 'notify_user_order_is_canceled', category: 'shop', is_configurable: false },
end { name: 'notify_user_order_is_refunded', category: 'shop', is_configurable: false },
{ name: 'notify_admin_low_stock_threshold', category: 'shop', is_configurable: true },
{ name: 'notify_admin_training_auto_cancelled', category: 'trainings', is_configurable: true },
{ name: 'notify_member_training_auto_cancelled', category: 'trainings', is_configurable: false },
unless NotificationType.find_by(name: 'notify_user_child_is_validated') { name: 'notify_member_training_authorization_expired', category: 'trainings', is_configurable: false },
NotificationType.create!( { name: 'notify_member_training_invalidated', category: 'trainings', is_configurable: false },
name: 'notify_user_child_is_validated', { name: 'notify_admin_order_is_paid', category: 'shop', is_configurable: true },
category: 'users_accounts', { name: 'notify_member_reservation_limit_reached', category: 'agenda', is_configurable: false },
is_configurable: false { name: 'notify_admin_user_child_supporting_document_refusal', category: 'supporting_documents', is_configurable: true },
) { name: 'notify_user_child_supporting_document_refusal', category: 'supporting_documents', is_configurable: false },
end { name: 'notify_admin_child_created', category: 'users_accounts', is_configurable: true },
{ name: 'notify_user_child_is_validated', category: 'users_accounts', is_configurable: false },
{ name: 'notify_user_child_is_invalidated', category: 'users_accounts', is_configurable: false },
{ name: 'notify_admin_user_child_supporting_document_files_updated', category: 'supporting_documents', is_configurable: true },
{ name: 'notify_admin_user_child_supporting_document_files_created', category: 'supporting_documents', is_configurable: true },
unless NotificationType.find_by(name: 'notify_user_child_is_invalidated') { name: 'notify_member_reservation_validated', category: 'agenda', is_configurable: false },
NotificationType.create!( { name: 'notify_admin_reservation_validated', category: 'agenda', is_configurable: true }
name: 'notify_user_child_is_invalidated', ].freeze
category: 'users_accounts',
is_configurable: false
)
end
unless NotificationType.find_by(name: 'notify_admin_user_child_supporting_document_files_updated') NOTIFICATIONS_TYPES.each do |notification_type|
NotificationType.create!( next if NotificationType.find_by(name: notification_type[:name])
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!( NotificationType.create!(
name: 'notify_admin_user_child_supporting_document_files_created', name: notification_type[:name],
category: 'supporting_documents', category: notification_type[:category],
is_configurable: true is_configurable: notification_type[:is_configurable]
) )
end end

View File

@ -1244,7 +1244,9 @@ CREATE TABLE public.events (
age_range_id integer, age_range_id integer,
category_id integer, category_id integer,
deleted_at timestamp without time zone, deleted_at timestamp without time zone,
event_type character varying DEFAULT 'standard'::character varying event_type character varying DEFAULT 'standard'::character varying,
pre_registration boolean DEFAULT false,
pre_registration_end_date timestamp(6) without time zone
); );
@ -3214,7 +3216,8 @@ CREATE TABLE public.slots_reservations (
ex_start_at timestamp without time zone, ex_start_at timestamp without time zone,
ex_end_at timestamp without time zone, ex_end_at timestamp without time zone,
canceled_at timestamp without time zone, canceled_at timestamp without time zone,
offered boolean DEFAULT false offered boolean DEFAULT false,
validated_at timestamp(6) without time zone
); );
@ -8937,6 +8940,8 @@ INSERT INTO "schema_migrations" (version) VALUES
('20230524080448'), ('20230524080448'),
('20230524083558'), ('20230524083558'),
('20230524110215'), ('20230524110215'),
('20230525101006'); ('20230525101006'),
('20230612123250'),
('20230626103314');