1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-20 14:54:15 +01:00

(feat) validate pre-registration reservation

This commit is contained in:
Du Peng 2023-06-26 16:02:54 +02:00
parent 35d1a7b183
commit 0f9a5ff8af
21 changed files with 231 additions and 16 deletions

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

View File

@ -436,7 +436,7 @@ Application.Controllers.controller('AdminEventsController', ['$scope', '$state',
/**
* Controller used in the reservations listing page for a specific event
*/
Application.Controllers.controller('ShowEventReservationsController', ['$scope', 'eventPromise', 'reservationsPromise', function ($scope, eventPromise, reservationsPromise) {
Application.Controllers.controller('ShowEventReservationsController', ['$scope', 'eventPromise', 'reservationsPromise', 'dialogs', 'SlotsReservation', 'growl', '_t', function ($scope, eventPromise, reservationsPromise, dialogs, SlotsReservation, growl, _t) {
// retrieve the event from the ID provided in the current URL
$scope.event = eventPromise;
@ -451,6 +451,42 @@ Application.Controllers.controller('ShowEventReservationsController', ['$scope',
$scope.isCancelled = function (reservation) {
return !!(reservation.slots_reservations_attributes[0].canceled_at);
};
/**
* Test if the provided reservation has been validated
* @param reservation {Reservation}
* @returns {boolean}
*/
$scope.isValidated = function (reservation) {
return !!(reservation.slots_reservations_attributes[0].validated_at);
};
/**
* Callback to validate a reservation
* @param reservation {Reservation}
*/
$scope.validateReservation = function (reservation) {
dialogs.confirm({
resolve: {
object: function () {
return {
title: _t('app.admin.event_reservations.validate_the_reservation'),
msg: _t('app.admin.event_reservations.do_you_really_want_to_validate_this_reservation_this_apply_to_all_booked_tickets')
};
}
}
}, function () { // validate confirmed
SlotsReservation.validate({
id: reservation.slots_reservations_attributes[0].id
}, () => { // successfully validated
growl.success(_t('app.admin.event_reservations.reservation_was_successfully_validated'));
const index = $scope.reservations.indexOf(reservation);
$scope.reservations[index].slots_reservations_attributes[0].validated_at = new Date();
}, () => {
growl.warning(_t('app.admin.event_reservations.validation_failed'));
});
});
};
}]);
/**

View File

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

View File

@ -12,8 +12,9 @@
<tr>
<th style="width:30%" translate>{{ 'app.admin.events.title' }}</th>
<th style="width:30%" translate>{{ 'app.admin.events.dates' }}</th>
<th style="width:10%" translate>{{ 'app.admin.events.booking' }}</th>
<th style="width:30%"></th>
<th style="width:15%" translate>{{ 'app.admin.events.types' }}</th>
<th style="width:10%" translate>{{ 'app.admin.events.booking' }}</th>
<th style="width:15%"></th>
</tr>
</thead>
<tbody>
@ -48,6 +49,13 @@
</span>
</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">
<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="badge font-sbold cancelled" ng-if="event.nb_total_places == -1" translate>{{ 'app.admin.events.cancelled' }}</span>
@ -57,10 +65,10 @@
<td style="vertical-align:middle">
<div class="buttons">
<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 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>
</div>
</td>

View File

@ -20,31 +20,49 @@
<table class="table" ng-if="reservations.length > 0">
<thead>
<tr>
<th style="width:25%" translate>{{ 'app.admin.event_reservations.user' }}</th>
<th style="width:25%" translate>{{ 'app.admin.event_reservations.payment_date' }}</th>
<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 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>
</thead>
<tbody>
<tr ng-repeat="reservation in reservations" ng-class="{'disabled': isCancelled(reservation)}">
<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">
<span ng-if="bu.booked_type !== 'User'">{{bu.name}}</span>
<a ui-sref="app.logged.members_show({id: bu.booked_id})" ng-if="bu.booked_type === 'User'">{{bu.name}}</a>
<span>{{bu.name}}</span>
</div>
</td>
<td>{{ reservation.created_at | amDateFormat:'LL LTS' }}</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-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)" 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)" 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)" translate>
{{ 'app.admin.event_reservations.validate' }}
</button>
<button class="btn btn-default" ng-if="isValidated(reservation) && !isCancelled(reservation)" translate>
{{ 'app.admin.event_reservations.pay' }}
</button>
</td>
<td>
<div class="buttons">
<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>
</div>
</td>

View File

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

View File

@ -21,5 +21,18 @@ class SlotsReservationsService
rescue Faraday::ConnectionFailed
warn 'Unable to update data in elasticsearch'
end
def validate(slot_reservation)
if slot_reservation.update(validated_at: Time.current)
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

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,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.slot_attributes do
json.id sr.slot_id
json.start_at sr.slot.start_at.iso8601
@ -39,3 +40,4 @@ json.booking_users_attributes reservation.booking_users.order(booked_type: :desc
json.booked_id bu.booked_id
json.booked_type bu.booked_type
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

@ -601,6 +601,12 @@ en:
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_deletion_failed: "Price category deletion failed."
types: "Types"
event_type:
standard: "Standard"
family: "Family"
nominative: "Nominative"
pre_registration: "Pre-registration"
#add a new event
events_new:
add_an_event: "Add an event"
@ -635,6 +641,22 @@ en:
no_reservations_for_now: "No reservation for now."
back_to_monitoring: "Back to monitoring"
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."
events_settings:
title: "Settings"
generic_text_block: "Editorial text block"

View File

@ -601,6 +601,12 @@ fr:
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_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"
#add a new event
events_new:
add_an_event: "Ajouter un événement"
@ -635,6 +641,22 @@ fr:
no_reservations_for_now: "Aucune réservation pour le moment."
back_to_monitoring: "Retour au suivi"
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é."
events_settings:
title: "Paramètres"
generic_text_block: "Bloc de texte rédactionnel"

View File

@ -469,6 +469,10 @@ en:
order_canceled: "Your command %{REFERENCE} is canceled"
notify_user_order_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:
subscriptions: "Subscriptions"

View File

@ -469,6 +469,10 @@ fr:
order_canceled: "Votre commande %{REFERENCE} est annulée"
notify_user_order_is_refunded:
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:
subscriptions: "Abonnements"

View File

@ -451,3 +451,13 @@ en:
subject: "Your command was refunded"
body:
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"
body:
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

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

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

@ -3272,7 +3272,8 @@ CREATE TABLE public.slots_reservations (
ex_start_at timestamp without time zone,
ex_end_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
);
@ -9062,9 +9063,10 @@ INSERT INTO "schema_migrations" (version) VALUES
('20230524080448'),
('20230524083558'),
('20230524110215');
('20230525101006');
('20230525101006'),
('20230626122844'),
('20230626122947');
('20230612123250');
('20230626103314');