mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-29 18:52:22 +01:00
Merge branch 'book_slot_at_same_time' into dev
This commit is contained in:
commit
07b21f5094
@ -1,5 +1,6 @@
|
||||
# Changelog Fab-manager
|
||||
|
||||
- Ability to configure the policy (allow or prevent) for members booking a machine/formation/event slot, if they already have a reservation the same day at the same time
|
||||
- Ability to create and delete periodic calendar availabilities (recurrence)
|
||||
- Ability to fully customize the home page
|
||||
- Automated setup assistant
|
||||
@ -59,6 +60,7 @@
|
||||
- [TODO DEPLOY] add the `PHONE_REQUIRED` environment variable (see [doc/environment.md](doc/environment.md#PHONE_REQUIRED) for configuration details)
|
||||
- [TODO DEPLOY] add the `EVENTS_IN_CALENDAR` environment variable (see [doc/environment.md](doc/environment.md#EVENTS_IN_CALENDAR) for configuration details)
|
||||
- [TODO DEPLOY] add the `USER_CONFIRMATION_NEEDED_TO_SIGN_IN` environment variable (see [doc/environment.md](doc/environment.md#USER_CONFIRMATION_NEEDED_TO_SIGN_IN) for configuration details)
|
||||
- [TODO DEPLOY] add the `BOOK_SLOT_AT_SAME_TIME` environment variable (see [doc/environment.md](doc/environment.md#BOOK_SLOT_AT_SAME_TIME) for configuration details)
|
||||
- [TODO DEPLOY] -> (only dev) `bundle install && yarn install`
|
||||
- [TODO DEPLOY] `rake db:migrate && rake db:seed`
|
||||
- [TODO DEPLOY] `rake fablab:fix:name_stylesheet`
|
||||
|
@ -246,13 +246,34 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
$scope.reserveSuccess = false;
|
||||
if (!$scope.isAuthenticated()) {
|
||||
return $scope.login(null, function (user) {
|
||||
$scope.reserve.toReserve = !$scope.reserve.toReserve;
|
||||
if (user.role !== 'admin') {
|
||||
return $scope.ctrl.member = user;
|
||||
}
|
||||
const sameTimeReservations = findReservationsAtSameTime();
|
||||
if (sameTimeReservations.length > 0) {
|
||||
showReserveSlotSameTimeModal(sameTimeReservations, function(res) {
|
||||
return $scope.reserve.toReserve = !$scope.reserve.toReserve;
|
||||
});
|
||||
} else {
|
||||
return $scope.reserve.toReserve = !$scope.reserve.toReserve;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return $scope.reserve.toReserve = !$scope.reserve.toReserve;
|
||||
if ($scope.currentUser.role === 'admin') {
|
||||
return $scope.reserve.toReserve = !$scope.reserve.toReserve;
|
||||
} else {
|
||||
Member.get({ id: $scope.currentUser.id }, function (member) {
|
||||
$scope.ctrl.member = member;
|
||||
const sameTimeReservations = findReservationsAtSameTime();
|
||||
if (sameTimeReservations.length > 0) {
|
||||
showReserveSlotSameTimeModal(sameTimeReservations, function(res) {
|
||||
return $scope.reserve.toReserve = !$scope.reserve.toReserve;
|
||||
});
|
||||
} else {
|
||||
return $scope.reserve.toReserve = !$scope.reserve.toReserve;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -798,6 +819,48 @@ Application.Controllers.controller('ShowEventController', ['$scope', '$state', '
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Find user's reservations, the same date at the same time, with event
|
||||
*/
|
||||
var findReservationsAtSameTime = function () {
|
||||
let sameTimeReservations = [
|
||||
'training_reservations',
|
||||
'machine_reservations',
|
||||
'space_reservations',
|
||||
'events_reservations'
|
||||
].map(function(k) {
|
||||
return _.filter($scope.ctrl.member[k], function(r) {
|
||||
if (r.reservable_type === 'Event' && r.reservable.id === $scope.event.id) {
|
||||
return false;
|
||||
}
|
||||
return moment($scope.event.start_time).isSame(r.start_at) ||
|
||||
(moment($scope.event.end_time).isAfter(r.start_at) && moment($scope.event.end_time).isBefore(r.end_at)) ||
|
||||
(moment($scope.event.start_time).isAfter(r.start_at) && moment($scope.event.start_time).isBefore(r.end_at)) ||
|
||||
(moment($scope.event.start_time).isBefore(r.start_at) && moment($scope.event.end_time).isAfter(r.end_at));
|
||||
});
|
||||
});
|
||||
return _.union.apply(null, sameTimeReservations);
|
||||
};
|
||||
|
||||
/**
|
||||
* A modal for show reservations the same date at the same time
|
||||
*
|
||||
* @param sameTimeReservations {Array} reservations the same date at the same time
|
||||
* @param callback {function} callback will invoke when user confirm
|
||||
*/
|
||||
var showReserveSlotSameTimeModal = function(sameTimeReservations, callback) {
|
||||
const modalInstance = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: '<%= asset_path "shared/_reserve_slot_same_time.html" %>',
|
||||
size: 'md',
|
||||
controller: 'ReserveSlotSameTimeController',
|
||||
resolve: {
|
||||
sameTimeReservations: function() { return sameTimeReservations; },
|
||||
}
|
||||
});
|
||||
modalInstance.result.then(callback);
|
||||
};
|
||||
|
||||
// !!! MUST BE CALLED AT THE END of the controller
|
||||
return initialize();
|
||||
}
|
||||
@ -879,4 +942,3 @@ Application.Controllers.controller('DeleteRecurrentEventController', ['$scope',
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
|
@ -662,7 +662,7 @@ Application.Controllers.controller('ReserveMachineController', ['$scope', '$stat
|
||||
});
|
||||
|
||||
if ($scope.currentUser.role !== 'admin') {
|
||||
return $scope.ctrl.member = $scope.currentUser;
|
||||
return Member.get({ id: $scope.currentUser.id }, function (member) { $scope.ctrl.member = member; });
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -10,8 +10,8 @@
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', 'growl', 'Auth', 'Price', 'Wallet', 'CustomAsset', 'Slot', 'helpers', '_t',
|
||||
function ($rootScope, $uibModal, dialogs, growl, Auth, Price, Wallet, CustomAsset, Slot, helpers, _t) {
|
||||
Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs', 'growl', 'Auth', 'Price', 'Wallet', 'CustomAsset', 'Slot', 'helpers', '_t', '$uibModal',
|
||||
function ($rootScope, $uibModal, dialogs, growl, Auth, Price, Wallet, CustomAsset, Slot, helpers, _t, $uibModal) {
|
||||
return ({
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
@ -72,8 +72,38 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
|
||||
* @param slot {Object} fullCalendar event object
|
||||
*/
|
||||
$scope.validateSlot = function (slot) {
|
||||
slot.isValid = true;
|
||||
return updateCartPrice();
|
||||
let sameTimeReservations = [
|
||||
'training_reservations',
|
||||
'machine_reservations',
|
||||
'space_reservations',
|
||||
'events_reservations'
|
||||
].map(function (k) {
|
||||
return _.filter($scope.user[k], function(r) {
|
||||
return slot.start.isSame(r.start_at) ||
|
||||
(slot.end.isAfter(r.start_at) && slot.end.isBefore(r.end_at)) ||
|
||||
(slot.start.isAfter(r.start_at) && slot.start.isBefore(r.end_at)) ||
|
||||
(slot.start.isBefore(r.start_at) && slot.end.isAfter(r.end_at));
|
||||
})
|
||||
});
|
||||
sameTimeReservations = _.union.apply(null, sameTimeReservations);
|
||||
if (sameTimeReservations.length > 0) {
|
||||
const modalInstance = $uibModal.open({
|
||||
animation: true,
|
||||
templateUrl: '<%= asset_path "shared/_reserve_slot_same_time.html" %>',
|
||||
size: 'md',
|
||||
controller: 'ReserveSlotSameTimeController',
|
||||
resolve: {
|
||||
sameTimeReservations: function() { return sameTimeReservations; }
|
||||
}
|
||||
});
|
||||
modalInstance.result.then(function(res) {
|
||||
slot.isValid = true;
|
||||
return updateCartPrice();
|
||||
});
|
||||
} else {
|
||||
slot.isValid = true;
|
||||
return updateCartPrice();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@ -614,3 +644,25 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
|
||||
});
|
||||
}
|
||||
]);
|
||||
|
||||
/**
|
||||
* Controller of modal for show reservations the same date at the same time
|
||||
*/
|
||||
Application.Controllers.controller('ReserveSlotSameTimeController', ['$scope', '$uibModalInstance', 'sameTimeReservations', 'growl', '_t',
|
||||
function ($scope, $uibModalInstance, sameTimeReservations, growl, _t) {
|
||||
$scope.sameTimeReservations = sameTimeReservations;
|
||||
$scope.bookSlotAtSameTime = Fablab.bookSlotAtSameTime;
|
||||
/**
|
||||
* Confirmation callback
|
||||
*/
|
||||
$scope.ok = function () {
|
||||
$uibModalInstance.close({});
|
||||
}
|
||||
/**
|
||||
* Cancellation callback
|
||||
*/
|
||||
$scope.cancel = function () {
|
||||
$uibModalInstance.dismiss('cancel');
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
18
app/assets/templates/shared/_reserve_slot_same_time.html.erb
Normal file
18
app/assets/templates/shared/_reserve_slot_same_time.html.erb
Normal file
@ -0,0 +1,18 @@
|
||||
<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>{{ 'app.shared.cart.slot_at_same_time' }}</h1>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p ng-if="bookSlotAtSameTime || currentUser.role === 'admin'" translate>{{ 'app.shared.cart.do_you_really_want_to_book_slot_at_same_time' }}</p>
|
||||
<p ng-if="!bookSlotAtSameTime && currentUser.role !== 'admin'" translate>{{ 'app.shared.cart.unable_to_book_slot_because_really_have_reservation_at_same_time' }}</p>
|
||||
<ul>
|
||||
<li ng-repeat="r in sameTimeReservations">
|
||||
<span>{{::r.reservable.name}}{{::r.reservable.title}}</span>
|
||||
<div class="font-sbold text-u-c">{{ 'app.shared.cart.datetime_to_time' | translate:{START_DATETIME:(r.start_at | amDateFormat:'LLLL'), END_TIME:(r.end_at | amDateFormat:'LT') } }}</div>
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button ng-if="bookSlotAtSameTime || currentUser.role === 'admin'" class="btn btn-info" ng-click="ok()" translate>{{ 'app.shared.buttons.confirm' }}</button>
|
||||
<button class="btn btn-default" ng-click="cancel()" translate>{{ 'app.shared.buttons.cancel' }}</button>
|
||||
</div>
|
@ -15,9 +15,26 @@ json.training_reservations @member.reservations.where(reservable_type: 'Training
|
||||
json.start_at r.slots.first.start_at
|
||||
json.end_at r.slots.first.end_at
|
||||
json.reservable r.reservable
|
||||
json.reservable_type 'Training'
|
||||
json.is_valid @member.statistic_profile.training_ids.include?(r.reservable_id)
|
||||
json.canceled_at r.slots.first.canceled_at
|
||||
end
|
||||
json.machine_reservations @member.reservations.where(reservable_type: 'Machine') do |r|
|
||||
json.id r.id
|
||||
json.start_at r.slots.first.start_at
|
||||
json.end_at r.slots.first.end_at
|
||||
json.reservable r.reservable
|
||||
json.reservable_type 'Machine'
|
||||
json.canceled_at r.slots.first.canceled_at
|
||||
end
|
||||
json.space_reservations @member.reservations.where(reservable_type: 'Space') do |r|
|
||||
json.id r.id
|
||||
json.start_at r.slots.first.start_at
|
||||
json.end_at r.slots.first.end_at
|
||||
json.reservable r.reservable
|
||||
json.reservable_type 'Space'
|
||||
json.canceled_at r.slots.first.canceled_at
|
||||
end
|
||||
|
||||
json.all_projects @member.all_projects do |project|
|
||||
if requested_current || project.state == 'published'
|
||||
@ -65,6 +82,7 @@ json.events_reservations @member.reservations.where(reservable_type: 'Event').jo
|
||||
end
|
||||
end
|
||||
json.reservable r.reservable
|
||||
json.reservable_type 'Event'
|
||||
end
|
||||
json.invoices @member.invoices.order('reference DESC') do |i|
|
||||
json.id i.id
|
||||
|
@ -29,6 +29,7 @@
|
||||
Fablab.withoutInvoices = ('<%= Rails.application.secrets.fablab_without_invoices %>' === 'true');
|
||||
Fablab.phoneRequired = ('<%= Rails.application.secrets.phone_required %>' === 'true');
|
||||
Fablab.fablabWithoutWallet = ('<%= Rails.application.secrets.fablab_without_wallet %>' === 'true');
|
||||
Fablab.bookSlotAtSameTime = ('<%= Rails.application.secrets.book_slot_at_same_time %>' === 'true');
|
||||
Fablab.eventsInCalendar = ('<%= Rails.application.secrets.events_in_calendar %>' === 'true');
|
||||
Fablab.slotDuration = parseInt("<%= ApplicationHelper::SLOT_DURATION %>", 10);
|
||||
Fablab.featureTourDisplay = "<%= Rails.application.secrets.feature_tour_display %>";
|
||||
|
@ -22,6 +22,7 @@ FABLAB_WITHOUT_ONLINE_PAYMENT: 'false'
|
||||
FABLAB_WITHOUT_INVOICES: 'false'
|
||||
PHONE_REQUIRED: 'true'
|
||||
FABLAB_WITHOUT_WALLET: 'false'
|
||||
BOOK_SLOT_AT_SAME_TIME: 'true'
|
||||
|
||||
USER_CONFIRMATION_NEEDED_TO_SIGN_IN: 'false'
|
||||
|
||||
|
@ -439,3 +439,6 @@ en:
|
||||
a_problem_occurred_during_the_payment_process_please_try_again_later: "A problem occurred during the payment process. Please try again later."
|
||||
none: "None"
|
||||
online_payment_disabled: "Online payment is not available. Please contact the Fablab reception directly."
|
||||
slot_at_same_time: "Conflict with others reservations"
|
||||
do_you_really_want_to_book_slot_at_same_time: "Do you really want to book this slot? Other bookings take place at the same time"
|
||||
unable_to_book_slot_because_really_have_reservation_at_same_time: "Unable to book this slot because the following reservation occurs at the same time."
|
||||
|
@ -439,3 +439,6 @@ es:
|
||||
a_problem_occurred_during_the_payment_process_please_try_again_later: "A problem occurred during the payment process. Please try again later."
|
||||
none: "Ninguno"
|
||||
online_payment_disabled: "El pago en línea no está disponible. Póngase en contacto directamente con la recepción de Fablab."
|
||||
slot_at_same_time: "Conflict with others reservations"
|
||||
do_you_really_want_to_book_slot_at_same_time: "Do you really want to book this slot? Other bookings take place at the same time"
|
||||
unable_to_book_slot_because_really_have_reservation_at_same_time: "Unable to book this slot because the following reservation occurs at the same time."
|
||||
|
@ -439,6 +439,9 @@ fr:
|
||||
a_problem_occurred_during_the_payment_process_please_try_again_later: "Il y a eu un problème lors de la procédure de paiement. Veuillez réessayer plus tard."
|
||||
none: "Aucune"
|
||||
online_payment_disabled: "Le payment par carte bancaire n'est pas disponible. Merci de contacter directement l'accueil du Fablab."
|
||||
slot_at_same_time: "Conflit avec d'autres réservations"
|
||||
do_you_really_want_to_book_slot_at_same_time: "Êtes-vous sûr de réserver ce créneau ? D'autres réservations ont lieu en même temps"
|
||||
unable_to_book_slot_because_really_have_reservation_at_same_time: "Impossible de réserver ce créneau car les réservations ci-dessous ont lieu en même temps."
|
||||
|
||||
tour:
|
||||
previous: "Précédent"
|
||||
|
@ -439,3 +439,6 @@ pt:
|
||||
a_problem_occurred_during_the_payment_process_please_try_again_later: "Um problema ocorreu durante o processo de pagamento. Por favor tente novamente mais tarde."
|
||||
none: "Vazio"
|
||||
online_payment_disabled: "O pagamento online não está disponível. Entre em contato diretamente com a recepção do Fablab."
|
||||
slot_at_same_time: "Conflict with others reservations"
|
||||
do_you_really_want_to_book_slot_at_same_time: "Do you really want to book this slot? Other bookings take place at the same time"
|
||||
unable_to_book_slot_because_really_have_reservation_at_same_time: "Unable to book this slot because the following reservation occurs at the same time."
|
||||
|
@ -22,6 +22,7 @@ development:
|
||||
fablab_without_invoices: <%= ENV["FABLAB_WITHOUT_INVOICES"] %>
|
||||
phone_required: <%= ENV["PHONE_REQUIRED"] %>
|
||||
fablab_without_wallet: <%= ENV["FABLAB_WITHOUT_WALLET"] %>
|
||||
book_slot_at_same_time: <%= ENV["BOOK_SLOT_AT_SAME_TIME"] %>
|
||||
user_confirmation_needed_to_sign_in: <%= ENV["USER_CONFIRMATION_NEEDED_TO_SIGN_IN"] %>
|
||||
events_in_calendar: <%= ENV["EVENTS_IN_CALENDAR"] %>
|
||||
slot_duration: <%= ENV["SLOT_DURATION"] %>
|
||||
@ -70,6 +71,7 @@ test:
|
||||
fablab_without_invoices: false
|
||||
phone_required: true
|
||||
fablab_without_wallet: false
|
||||
book_slot_at_same_time: true
|
||||
user_confirmation_needed_to_sign_in: <%= ENV["USER_CONFIRMATION_NEEDED_TO_SIGN_IN"] %>
|
||||
events_in_calendar: false
|
||||
slot_duration: 60
|
||||
@ -118,6 +120,7 @@ staging:
|
||||
fablab_without_invoices: <%= ENV["FABLAB_WITHOUT_INVOICES"] %>
|
||||
phone_required: <%= ENV["PHONE_REQUIRED"] %>
|
||||
fablab_without_wallet: <%= ENV["FABLAB_WITHOUT_WALLET"] %>
|
||||
book_slot_at_same_time: <%= ENV["BOOK_SLOT_AT_SAME_TIME"] %>
|
||||
user_confirmation_needed_to_sign_in: <%= ENV["USER_CONFIRMATION_NEEDED_TO_SIGN_IN"] %>
|
||||
events_in_calendar: <%= ENV["EVENTS_IN_CALENDAR"] %>
|
||||
slot_duration: <%= ENV["SLOT_DURATION"] %>
|
||||
@ -178,6 +181,7 @@ production:
|
||||
fablab_without_invoices: <%= ENV["FABLAB_WITHOUT_INVOICES"] %>
|
||||
phone_required: <%= ENV["PHONE_REQUIRED"] %>
|
||||
fablab_without_wallet: <%= ENV["FABLAB_WITHOUT_WALLET"] %>
|
||||
book_slot_at_same_time: <%= ENV["BOOK_SLOT_AT_SAME_TIME"] %>
|
||||
user_confirmation_needed_to_sign_in: <%= ENV["USER_CONFIRMATION_NEEDED_TO_SIGN_IN"] %>
|
||||
events_in_calendar: <%= ENV["EVENTS_IN_CALENDAR"] %>
|
||||
slot_duration: <%= ENV["SLOT_DURATION"] %>
|
||||
|
@ -113,6 +113,11 @@ This is useful if you won't use wallet system.
|
||||
PHONE_REQUIRED
|
||||
|
||||
If set to 'false' the phone number won't be required to register a new user on the software.
|
||||
<a name="BOOK_SLOT_AT_SAME_TIME"></a>
|
||||
|
||||
BOOK_SLOT_AT_SAME_TIME
|
||||
|
||||
If set to 'false', users won't be able to book a machine/formation/event slot if they already have a reservation the same day at the same time.
|
||||
<a name="USER_CONFIRMATION_NEEDED_TO_SIGN_IN"></a>
|
||||
|
||||
USER_CONFIRMATION_NEEDED_TO_SIGN_IN
|
||||
|
@ -15,6 +15,7 @@ FABLAB_WITHOUT_ONLINE_PAYMENT=true
|
||||
FABLAB_WITHOUT_INVOICES=false
|
||||
PHONE_REQUIRED=false
|
||||
FABLAB_WITHOUT_WALLET=false
|
||||
BOOK_SLOT_AT_SAME_TIME=true
|
||||
|
||||
EVENTS_IN_CALENDAR=false
|
||||
SLOT_DURATION=60
|
||||
|
@ -211,7 +211,7 @@ configure_env_file()
|
||||
local doc variables secret
|
||||
doc=$(\curl -sSL https://raw.githubusercontent.com/sleede/fab-manager/master/doc/environment.md)
|
||||
variables=(STRIPE_API_KEY STRIPE_PUBLISHABLE_KEY STRIPE_CURRENCY INVOICE_PREFIX FABLAB_WITHOUT_PLANS FABLAB_WITHOUT_SPACES FABLAB_WITHOUT_ONLINE_PAYMENT FABLAB_WITHOUT_INVOICES FABLAB_WITHOUT_WALLET \
|
||||
PHONE_REQUIRED USER_CONFIRMATION_NEEDED_TO_SIGN_IN EVENTS_IN_CALENDAR SLOT_DURATION DEFAULT_MAIL_FROM DELIVERY_METHOD DEFAULT_HOST DEFAULT_PROTOCOL SMTP_ADDRESS SMTP_PORT SMTP_USER_NAME SMTP_PASSWORD SMTP_AUTHENTICATION \
|
||||
PHONE_REQUIRED BOOK_SLOT_AT_SAME_TIME USER_CONFIRMATION_NEEDED_TO_SIGN_IN EVENTS_IN_CALENDAR SLOT_DURATION DEFAULT_MAIL_FROM DELIVERY_METHOD DEFAULT_HOST DEFAULT_PROTOCOL SMTP_ADDRESS SMTP_PORT SMTP_USER_NAME SMTP_PASSWORD SMTP_AUTHENTICATION \
|
||||
SMTP_ENABLE_STARTTLS_AUTO SMTP_OPENSSL_VERIFY_MODE SMTP_TLS GA_ID RECAPTCHA_SITE_KEY RECAPTCHA_SECRET_KEY DISQUS_SHORTNAME TWITTER_NAME \
|
||||
FACEBOOK_APP_ID LOG_LEVEL ALLOWED_EXTENSIONS ALLOWED_MIME_TYPES MAX_IMAGE_SIZE MAX_CAO_SIZE MAX_IMPORT_SIZE DISK_SPACE_MB_ALERT FEATURE_TOUR_BEHAVIOR \
|
||||
SUPERADMIN_EMAIL APP_LOCALE RAILS_LOCALE MOMENT_LOCALE SUMMERNOTE_LOCALE ANGULAR_LOCALE MESSAGEFORMAT_LOCALE FULLCALENDAR_LOCALE ELASTICSEARCH_LANGUAGE_ANALYZER TIME_ZONE \
|
||||
|
Loading…
x
Reference in New Issue
Block a user