1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-22 11:52:21 +01:00

(merge) merge branch pre_inscription

This commit is contained in:
Du Peng 2023-07-12 09:34:09 +02:00
commit 3f1d6bf378
36 changed files with 318 additions and 101 deletions

View File

@ -10,7 +10,7 @@ class API::ChildrenController < API::APIController
authorize Child
user_id = current_user.id
user_id = params[:user_id] if current_user.privileged? && params[:user_id]
@children = Child.where(user_id: user_id).includes(:supporting_document_files).order(:created_at)
@children = Child.where(user_id: user_id).where('birthday >= ?', 18.years.ago).includes(:supporting_document_files).order(:created_at)
end
def show

View File

@ -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 validate]
before_action :set_slots_reservation, only: %i[update cancel validate invalidate]
respond_to :json
def update
@ -28,6 +28,11 @@ class API::SlotsReservationsController < API::APIController
SlotsReservationsService.validate(@slot_reservation)
end
def invalidate
authorize @slot_reservation
SlotsReservationsService.invalidate(@slot_reservation)
end
private
def set_slots_reservation

View File

@ -100,7 +100,7 @@ export const EventForm: React.FC<EventFormProps> = ({ action, event, onError, on
* Callback triggered when the user validates the machine form: handle create or update
*/
const onSubmit: SubmitHandler<Event> = (data: Event) => {
if (data.pre_registration_end_date.toString() === 'Invalid Date') {
if (data.pre_registration_end_date?.toString() === 'Invalid Date') {
data.pre_registration_end_date = null;
}
if (action === 'update') {

View File

@ -44,9 +44,9 @@ export const EventReservationItem: React.FC<EventReservationItemProps> = ({ rese
* Return the pre-registration status
*/
const preRegistrationStatus = () => {
if (!reservation.validated_at && !reservation.canceled_at && !reservation.is_paid) {
if (!reservation.is_valid && !reservation.canceled_at && !reservation.is_paid) {
return t('app.logged.event_reservation_item.in_the_process_of_validation');
} else if (reservation.validated_at && !reservation.canceled_at && !reservation.is_paid) {
} else if (reservation.is_valid && !reservation.canceled_at && !reservation.is_paid) {
return t('app.logged.event_reservation_item.settle_your_payment');
} else if (reservation.is_paid && !reservation.canceled_at) {
return t('app.logged.event_reservation_item.paid');

View File

@ -458,7 +458,7 @@ Application.Controllers.controller('ShowEventReservationsController', ['$scope',
* @returns {boolean}
*/
$scope.isValidated = function (reservation) {
return !!(reservation.slots_reservations_attributes[0].validated_at);
return reservation.slots_reservations_attributes[0].is_valid === true || reservation.slots_reservations_attributes[0].is_valid === 'true';
};
/**
@ -466,25 +466,30 @@ Application.Controllers.controller('ShowEventReservationsController', ['$scope',
* @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'));
});
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].is_valid = true;
}, () => {
growl.warning(_t('app.admin.event_reservations.validation_failed'));
});
};
/**
* Callback to invalidate a reservation
* @param reservation {Reservation}
*/
$scope.invalidateReservation = function (reservation) {
SlotsReservation.invalidate({
id: reservation.slots_reservations_attributes[0].id
}, () => { // successfully validated
growl.success(_t('app.admin.event_reservations.reservation_was_successfully_invalidated'));
const index = $scope.reservations.indexOf(reservation);
$scope.reservations[index].slots_reservations_attributes[0].is_valid = false;
}, () => {
growl.warning(_t('app.admin.event_reservations.invalidation_failed'));
});
};
@ -511,6 +516,9 @@ Application.Controllers.controller('ShowEventReservationsController', ['$scope',
templateUrl: '/admin/events/pay_reservation_modal.html',
size: 'sm',
resolve: {
event () {
return $scope.event;
},
reservation () {
return reservation;
},
@ -524,8 +532,10 @@ Application.Controllers.controller('ShowEventReservationsController', ['$scope',
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) {
controller: ['$scope', '$uibModalInstance', 'reservation', 'price', 'wallet', 'cartItems', 'helpers', '$filter', '_t', 'Reservation', 'event',
function ($scope, $uibModalInstance, reservation, price, wallet, cartItems, helpers, $filter, _t, Reservation, event) {
$scope.event = event;
// User's wallet amount
$scope.wallet = wallet;
@ -609,7 +619,11 @@ Application.Controllers.controller('ShowEventReservationsController', ['$scope',
if (r.id === reservation.id) {
return reservation;
}
growl.success(_t('app.admin.event_reservations.reservation_was_successfully_paid'));
if ($scope.event.amount === 0) {
growl.success(_t('app.admin.event_reservations.reservation_was_successfully_present'));
} else {
growl.success(_t('app.admin.event_reservations.reservation_was_successfully_paid'));
}
return r;
});
}, function () {

View File

@ -644,7 +644,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
if (!user.booked) {
return false;
}
if ($scope.enableChildValidationRequired && user.booked.type === 'Child' && !user.booked.validatedAt) {
if ($scope.enableChildValidationRequired && user.booked.type === 'Child' && !user.booked.validated_at) {
return false;
}
}
@ -675,9 +675,10 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
// get the current user's reservations into $scope.reservations
if ($scope.currentUser) {
getReservations($scope.event.id, 'Event', $scope.currentUser.id);
getChildren($scope.currentUser.id).then(function (children) {
updateNbReservePlaces();
getReservations($scope.event.id, 'Event', $scope.currentUser.id).then(function (reservations) {
getChildren($scope.currentUser.id).then(function (children) {
updateNbReservePlaces();
});
});
}
@ -696,7 +697,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
* @param user_id {number} the user's id (current or managed)
*/
const getReservations = function (reservable_id, reservable_type, user_id) {
Reservation.query({
return Reservation.query({
reservable_id,
reservable_type,
user_id
@ -727,6 +728,14 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
}
}
}
for (const r of $scope.reservations) {
for (const user of r.booking_users_attributes) {
const key = user.booked_type === 'User' ? `user_${user.booked_id}` : `child_${user.booked_id}`;
if (key === userKey) {
return true;
}
}
}
return false;
};
@ -748,7 +757,7 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
name: child.first_name + ' ' + child.last_name,
id: child.id,
type: 'Child',
validatedAt: child.validated_at,
validated_at: child.validated_at,
birthday: child.birthday
});
}
@ -761,7 +770,10 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
*/
const updateNbReservePlaces = function () {
if ($scope.event.event_type === 'family') {
const maxPlaces = $scope.children.length + 1;
const reservedPlaces = $scope.reservations.reduce((sum, reservation) => {
return sum + reservation.booking_users_attributes.length;
}, 0);
const maxPlaces = $scope.children.length + 1 - reservedPlaces;
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)) {
@ -976,11 +988,11 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
Reservation.get({ id: invoice.main_object.id }, function (reservation) {
$scope.event.nb_free_places = $scope.event.nb_free_places - reservation.total_booked_seats;
$scope.reservations.push(reservation);
resetEventReserve();
updateNbReservePlaces();
$scope.reserveSuccess = true;
$scope.coupon.applied = null;
});
resetEventReserve();
updateNbReservePlaces();
$scope.reserveSuccess = true;
$scope.coupon.applied = null;
if ($scope.currentUser.role === 'admin') {
return $scope.ctrl.member = null;
}

View File

@ -70,7 +70,8 @@ export interface Reservation {
event_type?: string,
event_title?: string,
event_pre_registration?: boolean
validated_at?: TDateISO,
canceled_at?: TDateISO,
is_valid?: boolean,
is_paid?: boolean,
}

View File

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

View File

@ -1,45 +1,52 @@
<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>
<h1 translate ng-show="reservation && event.amount !== 0">{{ 'app.admin.event_reservations.confirm_payment' }}</h1>
<h1 translate ng-show="reservation && event.amount === 0">{{ 'app.admin.event_reservations.confirm_present' }}</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 ng-show="event.amount !== 0">
<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>
</div>
<coupon show="true" coupon="coupon.applied" total="price.price_without_coupon" user-id="{{reservation.user_id}}"></coupon>
<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 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 ng-if="event.amount === 0">
<p translate>{{ 'app.admin.event_reservations.confirm_present_info' }}</p>
</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-info" ng-if="event.amount !== 0" ng-click="ok()" ng-disabled="attempting || !payment" ng-bind-html="validButtonName"></button>
<button class="btn btn-info" ng-if="event.amount === 0" ng-click="ok()" ng-disabled="attempting" ng-bind-html="validButtonName"></button>
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
</div>

View File

@ -20,13 +20,13 @@
<table class="table" ng-if="reservations.length > 0">
<thead>
<tr>
<th style="width:25%" translate>{{ 'app.admin.event_reservations.booked_by' }}</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 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>
<th translate>{{ 'app.admin.event_reservations.booked_by' }}</th>
<th translate>{{ 'app.admin.event_reservations.reservations' }}</th>
<th translate>{{ 'app.admin.event_reservations.date' }}</th>
<th translate>{{ 'app.admin.event_reservations.reserved_tickets' }}</th>
<th ng-if="event.pre_registration" translate>{{ 'app.admin.event_reservations.status' }}</th>
<th ng-if="event.pre_registration" translate>{{ 'app.admin.event_reservations.validation' }}</th>
<th></th>
</tr>
</thead>
<tbody>
@ -38,6 +38,7 @@
<span ng-if="event.event_type === 'standard'">{{ reservation.user_full_name }} </span>
<div ng-repeat="bu in reservation.booking_users_attributes">
<span>{{bu.name}}</span>
<span ng-if="bu.booked_type === 'Child'" class="m-l-sm">({{ 'app.admin.event_reservations.age' | translate:{NUMBER: bu.age} }})</span>
</div>
</td>
<td>{{ reservation.created_at | amDateFormat:'LL LTS' }}</td>
@ -48,16 +49,27 @@
<td ng-if="event.pre_registration">
<span ng-if="!isValidated(reservation) && !isCancelled(reservation) && !reservation.is_paid" class="v-middle badge text-sm 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-sm 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-sm bg-success" translate="">{{ 'app.admin.event_reservations.event_status.paid' }}</span>
<span ng-if="event.amount !== 0 && reservation.is_paid && !isCancelled(reservation)" class="v-middle badge text-sm bg-success" translate="">{{ 'app.admin.event_reservations.event_status.paid' }}</span>
<span ng-if="event.amount === 0 && reservation.is_paid && !isCancelled(reservation)" class="v-middle badge text-sm bg-success" translate="">{{ 'app.admin.event_reservations.event_status.present' }}</span>
<span ng-if="isCancelled(reservation)" class="v-middle badge text-sm 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>
<div>
<div ng-if="!isCancelled(reservation) && !reservation.is_paid">
<label class="m-r-sm">
<span translate>{{ 'app.admin.event_reservations.negative' }}</span>
<input type="radio" name="validate" ng-value="false" ng-click="invalidateReservation(reservation)" ng-model="reservation.slots_reservations_attributes[0].is_valid" >
</label>
<label>
<span translate>{{ 'app.admin.event_reservations.affirmative' }}</span>
<input type="radio" name="validate" ng-value="true" ng-click="validateReservation(reservation)" ng-model="reservation.slots_reservations_attributes[0].is_valid" >
</label>
</div>
<button class="btn btn-default" ng-click="payReservation(reservation)" ng-if="isValidated(reservation) && !isCancelled(reservation) && !reservation.is_paid">
<span ng-if="event.amount !== 0" translate>{{ 'app.admin.event_reservations.pay' }}</span>
<span ng-if="event.amount === 0" translate>{{ 'app.admin.event_reservations.present' }}</span>
</button>
</div>
</td>
<td>
<div class="buttons">

View File

@ -142,7 +142,7 @@
class="form-control">
<option value=""></option>
</select>
<uib-alert type="danger" ng-if="enableChildValidationRequired && user.booked && user.booked.type === 'Child' && !user.booked.validatedAt" style="margin-bottom: 0.8rem;">
<uib-alert type="danger" ng-if="enableChildValidationRequired && user.booked && user.booked.type === 'Child' && !user.booked.validated_at" style="margin-bottom: 0.8rem;">
<span class="text-sm">
<i class="fa fa-warning"></i>
<span translate>{{ 'app.shared.cart.child_validation_required_alert' }}</span>
@ -180,7 +180,7 @@
class="form-control">
<option value=""></option>
</select>
<uib-alert type="danger" ng-if="enableChildValidationRequired && user.booked && user.booked.type === 'Child' && !user.booked.validatedAt">
<uib-alert type="danger" ng-if="enableChildValidationRequired && user.booked && user.booked.type === 'Child' && !user.booked.is_valid">
<p class="text-sm">
<i class="fa fa-warning"></i>
<span translate>{{ 'app.shared.cart.child_validation_required_alert' }}</span>

View File

@ -89,7 +89,7 @@ class Event < ApplicationRecord
else
reserved_places = reservations.joins(:slots_reservations)
.where('slots_reservations.canceled_at': nil)
.where.not('slots_reservations.validated_at': nil)
.where('slots_reservations.is_valid': true)
.map(&:total_booked_seats)
.inject(0) { |sum, t| sum + t }
self.nb_free_places = (nb_total_places - reserved_places)
@ -99,7 +99,7 @@ class Event < ApplicationRecord
def nb_places_for_pre_registration
reservations.joins(:slots_reservations)
.where('slots_reservations.canceled_at': nil)
.where('slots_reservations.validated_at': nil)
.where('slots_reservations.is_valid': nil)
.map(&:total_booked_seats)
.inject(0) { |sum, t| sum + t }
end

View File

@ -19,4 +19,8 @@ class SlotsReservationPolicy < ApplicationPolicy
def validate?
user.admin? || user.manager?
end
def invalidate?
user.admin? || user.manager?
end
end

View File

@ -23,7 +23,7 @@ class SlotsReservationsService
end
def validate(slot_reservation)
if slot_reservation.update(validated_at: Time.current)
if slot_reservation.update(is_valid: true)
reservable = slot_reservation.reservation.reservable
if reservable.is_a?(Event)
reservable.update_nb_free_places
@ -39,5 +39,23 @@ class SlotsReservationsService
end
false
end
def invalidate(slot_reservation)
if slot_reservation.update(is_valid: false)
reservable = slot_reservation.reservation.reservable
if reservable.is_a?(Event)
reservable.update_nb_free_places
reservable.save
end
NotificationCenter.call type: 'notify_member_reservation_invalidated',
receiver: slot_reservation.reservation.user,
attached_object: slot_reservation.reservation
NotificationCenter.call type: 'notify_admin_reservation_invalidated',
receiver: User.admins_and_managers,
attached_object: slot_reservation.reservation
return true
end
false
end
end
end

View File

@ -18,7 +18,7 @@ json.array!(@members) do |member|
end
end
json.validated_at member.validated_at
json.children member.children.order(:created_at) do |child|
json.children member.children.where('birthday >= ?', 18.years.ago).order(:created_at) do |child|
json.extract! child, :id, :first_name, :last_name, :email, :birthday, :phone, :user_id, :validated_at
json.supporting_document_files_attributes child.supporting_document_files do |f|
json.id f.id

View File

@ -87,7 +87,7 @@ json.events_reservations @member.reservations.where(reservable_type: 'Event').jo
json.event_type sr.reservation.reservable.event_type
json.event_title sr.reservation.reservable.title
json.event_pre_registration sr.reservation.reservable.pre_registration
json.validated_at sr.validated_at
json.is_valid sr.is_valid
json.is_paid sr.reservation.invoice_items.count.positive?
json.canceled_at sr.canceled_at
json.booking_users_attributes sr.reservation.booking_users.order(booked_type: :desc) do |bu|

View File

@ -0,0 +1,4 @@
json.title notification.notification_type
json.description t('.a_RESERVABLE_reservation_was_invalidated_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_invalidated_html',
RESERVABLE: notification.attached_object.reservable.name)

View File

@ -0,0 +1,6 @@
# frozen_string_literal: true
json.title notification.notification_type
json.description t('.child_age_will_be_18_years_ago',
NAME: notification.attached_object.full_name,
DATE: I18n.l(notification.attached_object.birthday, format: :default))

View File

@ -7,7 +7,7 @@ json.message reservation.message
json.slots_reservations_attributes reservation.slots_reservations do |sr|
json.id sr.id
json.canceled_at sr.canceled_at&.iso8601
json.validated_at sr.validated_at&.iso8601
json.is_valid sr.is_valid
json.slot_id sr.slot_id
json.slot_attributes do
json.id sr.slot_id
@ -40,5 +40,7 @@ json.booking_users_attributes reservation.booking_users.order(booked_type: :desc
json.event_price_category_id bu.event_price_category_id
json.booked_id bu.booked_id
json.booked_type bu.booked_type
json.age ((Time.zone.now - bu.booked.birthday.to_time) / 1.year.seconds).floor if bu.booked_type == 'Child'
end
json.is_valid reservation.slots_reservations[0].is_valid
json.is_paid reservation.invoice_items.count.positive?

View File

@ -0,0 +1,7 @@
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
<p>
<%= t('.body.reservation_invalidated_html',
NAME: @attached_object.user&.profile&.full_name || t('api.notifications.deleted_user'),
RESERVABLE: @attached_object.reservable.name) %>
</p>

View File

@ -0,0 +1,3 @@
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
<p><%= t('.body.reservation_invalidated_html', RESERVATION: @attached_object.reservable.name) %></p>

View File

@ -0,0 +1,6 @@
<%= render 'notifications_mailer/shared/hello', recipient: @recipient %>
<%= t('.body.child_age_will_be_18_years_ago', **{
NAME: @attached_object.full_name,
DATE: I18n.l(@attached_object.birthday, format: :default),
}) %>

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
# send a notification if child age > 18 years ago
class ChildAgeWorker
include Sidekiq::Worker
def perform
children = Child.where('birthday = ?', 18.years.ago + 2.days)
children.each do |child|
NotificationCenter.call type: 'notify_user_when_child_age_will_be_18',
receiver: child.user,
attached_object: child
end
end
end

View File

@ -647,22 +647,33 @@ en:
reservations: "Reservations"
status: "Status"
gestion: "Gestion"
validation: "Validation"
event_status:
pre_registered: "Pre-registered"
to_pay: "To pay"
paid: "Paid"
canceled: "Canceled"
present: "Present"
affirmative: "yes"
negative: "no"
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."
reservation_was_successfully_invalidated: "Reservation was successfully invalidated."
invalidation_failed: "Invalidation 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"
reservation_was_successfully_paid: "Reservation was successfully paid."
present: "Present"
confirm_present: "Confirm presence"
confirm_present_info: "Confirm the presence of the user for this event"
reservation_was_successfully_present: "The presence of the user was successfully confirmed."
age: "{NUMBER} years old"
events_settings:
title: "Settings"
generic_text_block: "Editorial text block"

View File

@ -647,22 +647,33 @@ fr:
reservations: "Réservations"
status: "Statut"
gestion: "Gestion"
validation: "Validation"
event_status:
pre_registered: "Pré-inscrit"
to_pay: "À payer"
paid: "Payé"
canceled: "Annulée"
present: "Présent"
affirmative: "Oui"
negative: "Non"
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é."
reservation_was_successfully_invalidated: "La réservation a bien été invalidée."
invalidation_failed: "L'invalidation 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"
reservation_was_successfully_paid: "La réservation a bien été payée."
present: "Présenter"
confirm_present: "Confirmer la présence"
confirm_present_info: "Confirmer la présence de l'utilisateur à l'événement"
reservation_was_successfully_present: "La présence a bien été confirmée."
age: "{NUMBER} ans"
events_settings:
title: "Paramètres"
generic_text_block: "Bloc de texte rédactionnel"

View File

@ -336,9 +336,9 @@ fr:
ticket: "{NUMBER, plural, =0{place} one{place} other{places}}"
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_pre_registration_has_been_successfully_saved: "Merci. Votre demande a bien été pris en compte !"
thank_you_your_pre_registration_has_been_successfully_saved: "Merci. Votre demande a bien été prise en compte !"
you_can_find_your_reservation_s_details_on_your_: "Vous pouvez retrouver le détail de votre réservation sur votre"
informed_by_email_your_pre_registration: "vous serez tenu informé par email de la suite donnée à votre pré-inscription"
informed_by_email_your_pre_registration: "Vous serez tenu informé par email de la suite donnée à votre pré-inscription"
dashboard: "tableau de bord"
you_booked_DATE: "Vous avez réservé ({DATE}) :"
you_pre_booked_DATE: "Votre pré-inscription ({DATE}) :"

View File

@ -477,6 +477,12 @@ en:
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."
notify_member_reservation_invalidated:
your_reservation_RESERVABLE_was_invalidated_html: "Your pre-registration of <strong><em>%{RESERVABLE}</em></strong> wasn't validated."
notify_admin_reservation_invalidated:
a_RESERVABLE_reservation_was_invalidated_html: "A <strong><em>%{RESERVABLE}</em></strong> pre-registration of <strong><em>%{USER}</em></strong> was invalidated."
notify_user_when_child_age_will_be_18:
child_age_will_be_18_years_ago: "Your child %{NAME} will turn 18 on %{DATE}, at which point they will be automatically detached from your Family account. They will need to create their own account in order to make reservations."
#statistics tools for admins
statistics:
subscriptions: "Subscriptions"

View File

@ -477,6 +477,12 @@ fr:
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."
notify_member_reservation_invalidated:
your_reservation_RESERVABLE_was_invalidated_html: "Votre demande de pré-inscription de <strong><em>%{RESERVABLE}</em></strong> n'a pas été validée."
notify_admin_reservation_invalidated:
a_RESERVABLE_reservation_was_invalidated_html: "La réservation de <strong><em>%{RESERVABLE}</em></strong> de <strong><em>%{NAME}</em></strong> a été invalidée."
notify_user_when_child_age_will_be_18:
child_age_will_be_18_years_ago: "Votre enfant %{NAME} va avoir 18ans, le %{DATE}, date à laquelle il sera automatiquement détaché de votre compte Famille. Il devra se créer son propre compte pour effectuer ses réservations."
#statistics tools for admins
statistics:
subscriptions: "Abonnements"

View File

@ -467,7 +467,19 @@ en:
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é"
subject: "Pre-registration was validated"
body:
reservation_validated_html: "<strong><em>%{RESERVABLE}</em></strong> of %{NAME} was validated."
reserved_slots: "Reserved slots are:"
notify_member_reservation_invalidated:
subject: "Your pre-registration wasn't validated"
body:
reservation_invalidated_html: "<strong><em>%{RESERVABLE}</em></strong> wasn't validated."
notify_admin_reservation_invalidated:
subject: "Pre-registration wasn't validated"
body:
reservation_invalidated_html: "<strong><em>%{RESERVABLE}</em></strong> of %{NAME} wasn't validated."
notify_user_when_child_age_will_be_18:
subject: "Your child will be 18 years old"
body:
child_age_will_be_18_years_ago: "Your child %{NAME} will turn 18 on %{DATE}, at which point they will be automatically detached from your Family account. They will need to create their own account in order to make reservations."

View File

@ -471,3 +471,15 @@ fr:
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 :"
notify_member_reservation_invalidated:
subject: "Votre demande of pré-inscription n'a pas été validée"
body:
reservation_invalidated_html: "Votre réservation <strong><em>%{RESERVATION}</em></strong> n'a pas été validée."
notify_admin_reservation_invalidated:
subject: "Demande of pré-inscription n'a pas été validée"
body:
reservation_invalidated_html: "<strong><em>%{RESERVABLE}</em></strong> du membre %{NAME} n'a pas été validée."
notify_user_when_child_age_will_be_18:
subject: "Votre enfant va avoir 18ans"
body:
child_age_will_be_18_years_ago: "Votre enfant %{NAME} va avoir 18ans, le %{DATE}, date à laquelle il sera automatiquement détaché de votre compte Famille. Il devra se créer son propre compte pour effectuer ses réservations."

View File

@ -126,6 +126,7 @@ Rails.application.routes.draw do
resources :slots_reservations, only: [:update] do
put 'cancel', on: :member
put 'validate', on: :member
put 'invalidate', on: :member
end
resources :events do

View File

@ -62,4 +62,9 @@ auto_cancel_authorizations:
class: TrainingAuthorizationWorker
queue: default
child_age_will_be_18:
cron: "0 0 0 * * *" # every day, at midnight
class: ChildAgeWorker
queue: default
<%= PluginRegistry.insert_code('yml.schedule') %>

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
# add is_valid to slots_reservations
# remove validated_at from slots_reservations
class AddIsValidToSlotsReservations < ActiveRecord::Migration[7.0]
def up
add_column :slots_reservations, :is_valid, :boolean
SlotsReservation.reset_column_information
SlotsReservation.all.each do |sr|
sr.update_column(:is_valid, true) if sr.validated_at.present?
end
remove_column :slots_reservations, :validated_at
end
def down
remove_column :slots_reservations, :is_valid
add_column :slots_reservations, :validated_at, :datetime
end
end

View File

@ -92,7 +92,10 @@ NOTIFICATIONS_TYPES = [
{ name: 'notify_member_reservation_validated', category: 'agenda', is_configurable: false },
{ name: 'notify_admin_reservation_validated', category: 'agenda', is_configurable: true },
{ name: 'notify_member_pre_booked_reservation', category: 'agenda', is_configurable: false },
{ name: 'notify_admin_member_pre_booked_reservation', category: 'agenda', is_configurable: true }
{ name: 'notify_admin_member_pre_booked_reservation', category: 'agenda', is_configurable: true },
{ name: 'notify_member_reservation_invalidated', category: 'agenda', is_configurable: false },
{ name: 'notify_admin_reservation_invalidated', category: 'agenda', is_configurable: true },
{ name: 'notify_user_when_child_age_will_be_18', category: 'users_accounts', is_configurable: false },
].freeze
NOTIFICATIONS_TYPES.each do |notification_type|

View File

@ -9,6 +9,13 @@ SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
--
-- Name: public; Type: SCHEMA; Schema: -; Owner: -
--
-- *not* creating schema, since initdb creates it
--
-- Name: fuzzystrmatch; Type: EXTENSION; Schema: -; Owner: -
--
@ -3273,7 +3280,7 @@ CREATE TABLE public.slots_reservations (
ex_end_at timestamp without time zone,
canceled_at timestamp without time zone,
offered boolean DEFAULT false,
validated_at timestamp(6) without time zone
is_valid boolean
);
@ -9064,9 +9071,10 @@ INSERT INTO "schema_migrations" (version) VALUES
('20230524083558'),
('20230524110215'),
('20230525101006'),
('20230612123250');
('20230626103314');
('20230612123250'),
('20230626103314'),
('20230626122844'),
('20230626122947');
('20230626122947'),
('20230710072403');