1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-19 13:54:25 +01:00

improve slot is only subscription

This commit is contained in:
Du Peng 2020-02-12 12:58:17 +01:00
parent 870a092a81
commit b559d10b87
16 changed files with 234 additions and 36 deletions

View File

@ -18,8 +18,8 @@
* Controller used in the calendar management page
*/
Application.Controllers.controller('AdminCalendarController', ['$scope', '$state', '$uibModal', 'moment', 'Availability', 'Slot', 'Setting', 'Export', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', 'machinesPromise', '_t', 'uiCalendarConfig', 'CalendarConfig',
function ($scope, $state, $uibModal, moment, Availability, Slot, Setting, Export, growl, dialogs, bookingWindowStart, bookingWindowEnd, machinesPromise, _t, uiCalendarConfig, CalendarConfig) {
Application.Controllers.controller('AdminCalendarController', ['$scope', '$state', '$uibModal', 'moment', 'Availability', 'Slot', 'Setting', 'Export', 'growl', 'dialogs', 'bookingWindowStart', 'bookingWindowEnd', 'machinesPromise', 'plansPromise', 'groupsPromise', '_t', 'uiCalendarConfig', 'CalendarConfig',
function ($scope, $state, $uibModal, moment, Availability, Slot, Setting, Export, growl, dialogs, bookingWindowStart, bookingWindowEnd, machinesPromise, plansPromise, groupsPromise, _t, uiCalendarConfig, CalendarConfig) {
/* PRIVATE STATIC CONSTANTS */
// The calendar is divided in slots of 30 minutes
@ -164,6 +164,44 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state
}
};
/**
* Open a confirmation modal to remove a plan for the currently selected availability,
* @param plan {Object} must contain the machine ID and name
*/
$scope.removePlan = function (plan) {
// open a confirmation dialog
return dialogs.confirm({
resolve: {
object () {
return {
title: _t('app.admin.calendar.confirmation_required'),
msg: _t('app.admin.calendar.do_you_really_want_to_remove_PLAN_from_this_slot', { GENDER: getGender($scope.currentUser), PLAN: plan.name }) + ' ' +
_t('app.admin.calendar.this_will_prevent_any_new_reservation_on_this_slot_but_wont_cancel_those_existing') + '<br><strong>' +
_t('app.admin.calendar.beware_this_cannot_be_reverted') + '</strong>'
};
}
}
}
, function () {
// the admin has confirmed, remove the plan
const plans = _.drop($scope.availability.plan_ids, plan.id);
return Availability.update({ id: $scope.availability.id }, { availability: { plans_attributes: [{ id: plan.id, _destroy: true }] } }
, function (data, status) { // success
// update the plan_ids attribute
$scope.availability.plan_ids = data.plan_ids;
$scope.availability.plans = availabilityPlans();
uiCalendarConfig.calendars.calendar.fullCalendar('rerenderEvents');
// notify the admin
return growl.success(_t('app.admin.calendar.the_plan_was_successfully_removed_from_the_slot'));
}
, function (data, status) { // failed
growl.error(_t('app.admin.calendar.deletion_failed'));
}
);
});
};
/**
* Callback to alert the admin that the export request was acknowledged and is
* processing right now.
@ -265,6 +303,26 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state
} else { return 'other'; }
};
/**
* Return a list of plans classified by group
*
* @returns {array}
*/
var availabilityPlans = function() {
const plansClassifiedByGroup = [];
const _plans = _.filter(plansPromise, function (p) { return _.include($scope.availability.plan_ids, p.id) });
for (let group of Array.from(groupsPromise)) {
const groupObj = { id: group.id, name: group.name, plans: [] };
for (let plan of Array.from(_plans)) {
if (plan.group_id === group.id) { groupObj.plans.push(plan); }
}
if (groupObj.plans.length > 0) {
plansClassifiedByGroup.push(groupObj);
}
}
return plansClassifiedByGroup;
};
// Triggered when the admin drag on the agenda to create a new reservable slot.
// @see http://fullcalendar.io/docs/selection/select_callback/
//
@ -316,7 +374,8 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state
borderColor: availability.borderColor,
tag_ids: availability.tag_ids,
tags: availability.tags,
machine_ids: availability.machine_ids
machine_ids: availability.machine_ids,
plan_ids: availability.plan_ids
},
true
);
@ -333,6 +392,7 @@ Application.Controllers.controller('AdminCalendarController', ['$scope', '$state
*/
var calendarEventClickCb = function (event, jsEvent, view) {
$scope.availability = event;
$scope.availability.plans = availabilityPlans();
if ($scope.availabilityDom) {
$scope.availabilityDom.classList.remove("fc-selected")

View File

@ -138,41 +138,53 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
if (Object.keys($scope.user).length > 0) {
// check user was selected a plan if slot is restricted for subscriptions
const slotHasPlan = [];
const slotValidations = [];
let slotNotValid;
let slotNotValidError;
$scope.events.reserved.forEach(function (slot) {
if (slot.plan_ids.length > 0) {
if (
($scope.selectedPlan && _.include(slot.plan_ids, $scope.selectedPlan.id)) ||
($scope.user.subscribed_plan && _.include(slot.plan_ids, $scope.user.subscribed_plan.id))
) {
slotHasPlan.push(true);
slotValidations.push(true);
} else {
slotHasPlan.push(false);
slotNotValid = slot;
if ($scope.selectedPlan && !_.include(slot.plan_ids, $scope.selectedPlan.id)) {
slotNotValidError = 'selectedPlanError';
}
if ($scope.user.subscribed_plan && !_.include(slot.plan_ids, $scope.user.subscribed_plan.id)) {
slotNotValidError = 'userPlanError';
}
if (!$scope.selectedPlan || !$scope.user.subscribed_plan) {
slotNotValidError = 'noPlanError';
}
slotValidations.push(false);
}
}
});
const hasPlanForSlot = slotHasPlan.every(function (a) { return a });
const hasPlanForSlot = slotValidations.every(function (a) { return a; });
if (!hasPlanForSlot) {
return growl.error(_t('app.shared.cart.slot_restrict_subscriptions_must_select_plan'));
}
const reservation = mkReservation($scope.user, $scope.events.reserved, $scope.selectedPlan);
return Wallet.getWalletByUser({ user_id: $scope.user.id }, function (wallet) {
const amountToPay = helpers.getAmountToPay($scope.amountTotal, wallet.amount);
if (!$scope.isAdmin() && (amountToPay > 0)) {
if ($rootScope.fablabWithoutOnlinePayment) {
growl.error(_t('app.shared.cart.online_payment_disabled'));
} else {
return payByStripe(reservation);
}
if (!$scope.isAdmin()) {
return growl.error(_t('app.shared.cart.slot_restrict_subscriptions_must_select_plan'));
} else {
if ($scope.isAdmin() || (amountToPay === 0)) {
return payOnSite(reservation);
}
const modalInstance = $uibModal.open({
animation: true,
templateUrl: '<%= asset_path "shared/_reserve_slot_without_plan.html" %>',
size: 'md',
controller: 'ReserveSlotWithoutPlanController',
resolve: {
slot: function() { return slotNotValid; },
slotNotValidError: function() { return slotNotValidError; },
}
});
modalInstance.result.then(function(res) {
return paySlots();
});
}
});
} else {
return paySlots();
}
} else {
// otherwise we alert, this error musn't occur when the current user is not admin
return growl.error(_t('app.shared.cart.please_select_a_member_first'));
@ -289,15 +301,21 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
if ($scope.slot.plan_ids.length > 0) {
const _plans = _.filter($scope.plans, function (p) { return _.include($scope.slot.plan_ids, p.id) });
$scope.slot.plansGrouped = [];
$scope.slot.group_ids = [];
for (let group of Array.from($scope.groups)) {
const groupObj = { id: group.id, name: group.name, plans: [] };
for (let plan of Array.from(_plans)) {
if (plan.group_id === group.id) { groupObj.plans.push(plan); }
}
if (groupObj.plans.length > 0) {
$scope.slot.plansGrouped.push(groupObj);
if ($scope.isAdmin()) {
$scope.slot.plansGrouped.push(groupObj);
} else if ($scope.user.group_id === groupObj.id) {
$scope.slot.plansGrouped.push(groupObj);
}
}
}
$scope.slot.group_ids = $scope.slot.plansGrouped.map(function(g) { return g.id; });
}
if (!$scope.slot.is_reserved && !$scope.events.modifiable && !$scope.slot.is_completed) {
@ -646,9 +664,53 @@ Application.Directives.directive('cart', [ '$rootScope', '$uibModal', 'dialogs',
return $scope.selectedPlan = null;
};
/**
* Actions to pay slots
*/
var paySlots = function() {
const reservation = mkReservation($scope.user, $scope.events.reserved, $scope.selectedPlan);
return Wallet.getWalletByUser({ user_id: $scope.user.id }, function (wallet) {
const amountToPay = helpers.getAmountToPay($scope.amountTotal, wallet.amount);
if (!$scope.isAdmin() && (amountToPay > 0)) {
if ($rootScope.fablabWithoutOnlinePayment) {
growl.error(_t('app.shared.cart.online_payment_disabled'));
} else {
return payByStripe(reservation);
}
} else {
if ($scope.isAdmin() || (amountToPay === 0)) {
return payOnSite(reservation);
}
}
});
};
// !!! MUST BE CALLED AT THE END of the directive
return initialize();
}
});
}
]);
/**
* Controller used to alert admin reserve slot without plan
*/
Application.Controllers.controller('ReserveSlotWithoutPlanController', ['$scope', '$uibModalInstance', 'slot', 'slotNotValidError', 'growl', '_t',
function ($scope, $uibModalInstance, slot, slotNotValidError, growl, _t) {
$scope.slot = slot;
$scope.slotNotValidError = slotNotValidError;
/**
* Confirmation callback
*/
$scope.ok = function () {
$uibModalInstance.close({});
}
/**
* Cancellation callback
*/
$scope.cancel = function () {
$uibModalInstance.dismiss('cancel');
}
}
]);

View File

@ -584,7 +584,9 @@ angular.module('application.router', ['ui.router'])
resolve: {
bookingWindowStart: ['Setting', function (Setting) { return Setting.get({ name: 'booking_window_start' }).$promise; }],
bookingWindowEnd: ['Setting', function (Setting) { return Setting.get({ name: 'booking_window_end' }).$promise; }],
machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }]
machinesPromise: ['Machine', function (Machine) { return Machine.query().$promise; }],
plansPromise: ['Plan', function (Plan) { return Plan.query().$promise; }],
groupsPromise: ['Group', function (Group) { return Group.query().$promise; }]
}
})
.state('app.admin.calendar.icalendar', {

View File

@ -84,6 +84,25 @@
</div>
</div>
<div class="widget panel b-a m m-t-lg" ng-if="availability.plan_ids.length > 0">
<div class="panel-heading b-b small">
<h3 translate>{{ 'app.admin.calendar.plans' }}</h3>
</div>
<div class="widget-content no-bg auto wrapper">
<ul class="list-unstyled">
<li ng-repeat="g in availability.plans" class="m-b-xs">
<div class="font-sbold">{{::g.name}}</div>
<ul class="m-n" ng-repeat="plan in g.plans">
<li>
{{::plan.name}}
<span class="btn btn-warning btn-xs" ng-click="removePlan(plan)" ><i class="fa fa-times red"></i></span>
</li>
</ul>
</li>
</ul>
</div>
</div>
<div class="widget panel b-a m m-t-lg" ng-if="availability" >
<div class="panel-heading b-b small">
<h3 translate>{{ 'app.admin.calendar.actions' }}</h3>

View File

@ -28,17 +28,22 @@
switch-animate="true"
switch-readonly="{{slot.isValid}}"/>
</div>
<div class="alert alert-warning m-t-sm m-b-none" ng-show="slot.plansGrouped.length">
<div class="font-sbold text-u-c" translate>{{ 'app.shared.cart.slot_restrict_plans' }}</div>
<div ng-repeat="group in slot.plansGrouped">
<div class="font-sbold">{{::group.name}}</div>
<ul class="m-n" ng-repeat="plan in group.plans">
<li>{{::plan.name}}</li>
</ul>
<div class="alert alert-warning m-t-sm m-b-none" ng-show="slot.plan_ids.length > 0">
<div ng-show="slot.plansGrouped.length > 0">
<div class="font-sbold text-u-c" translate>{{ 'app.shared.cart.slot_restrict_plans' }}</div>
<div ng-repeat="group in slot.plansGrouped">
<div class="font-sbold">{{::group.name}}</div>
<ul class="m-n" ng-repeat="plan in group.plans">
<li>{{::plan.name}}</li>
</ul>
</div>
</div>
<div ng-show="slot.plansGrouped.length === 0">
<div class="font-sbold text-u-c" translate>{{ 'app.shared.cart.slot_restrict_plans_of_others_groups' }}</div>
</div>
</div>
</div>
<div>
<div ng-hide="slot.plan_ids.length > 0 && slot.plansGrouped.length === 0">
<button class="btn btn-valid btn-warning btn-block text-u-c r-b" ng-click="validateSlot(slot)" ng-if="!slot.isValid" translate>{{ 'app.shared.cart.confirm_this_slot' }}</button>
</div>

View File

@ -0,0 +1,22 @@
<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 ng-show="slotNotValidError === 'selectedPlanError'" translate>{{ 'app.shared.cart.selected_plan_dont_match_slot' }}</h1>
<h1 ng-show="slotNotValidError === 'userPlanError'" translate>{{ 'app.shared.cart.user_plan_dont_match_slot' }}</h1>
<h1 ng-show="slotNotValidError === 'noPlanError'" translate>{{ 'app.shared.cart.no_plan_match_slot' }}</h1>
</div>
<div class="modal-body">
<div class="font-sbold text-u-c">{{ 'app.shared.cart.datetime_to_time' | translate:{START_DATETIME:(slot.start | amDateFormat:'LLLL'), END_TIME:(slot.end | amDateFormat:'LT') } }}</div>
<div class="alert alert-warning m-t-sm m-b-none" ng-show="slot.plan_ids.length > 0">
<div class="font-sbold text-u-c" translate>{{ 'app.shared.cart.slot_restrict_plans' }}</div>
<div ng-repeat="group in slot.plansGrouped">
<div class="font-sbold">{{::group.name}}</div>
<ul class="m-n" ng-repeat="plan in group.plans">
<li>{{::plan.name}}</li>
</ul>
</div>
</div>
</div>
<div class="modal-footer">
<button 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>

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
json.array!(@availabilities) do |availability|
json.id availability.id
json.title availability.title
@ -15,4 +17,5 @@ json.array!(@availabilities) do |availability|
json.name t.name
end
json.lock availability.lock
json.plan_ids availability.plan_ids
end

View File

@ -5,10 +5,11 @@ json.start_at @availability.start_at.iso8601
json.end_at @availability.end_at.iso8601
json.available_type @availability.available_type
json.machine_ids @availability.machine_ids
json.plan_ids @availability.plan_ids
json.backgroundColor 'white'
json.borderColor availability_border_color(@availability)
json.tag_ids @availability.tag_ids
json.tags @availability.tags do |t|
json.id t.id
json.name t.name
end
end

View File

@ -32,6 +32,8 @@ en:
beware_this_cannot_be_reverted: "Beware: this cannot be reverted."
the_machine_was_successfully_removed_from_the_slot: "The machine was successfully removed from the slot."
deletion_failed: "Deletion failed."
do_you_really_want_to_remove_PLAN_from_this_slot: "Do you really want to remove \"{PLAN}\" from this slot?"
the_plan_was_successfully_removed_from_the_slot: "The plan was successfully removed from the slot."
DATE_slot: "{DATE} slot:"
what_kind_of_slot_do_you_want_to_create: "What kind of slot do you want to create?"
training: "Training"

View File

@ -30,6 +30,8 @@ es:
beware_this_cannot_be_reverted: "Beware: esto no puede ser revertido."
the_machine_was_successfully_removed_from_the_slot: "La máquina se eliminó correctamente de la ranura."
deletion_failed: "Fallo al borrar."
do_you_really_want_to_remove_PLAN_from_this_slot: "Do you really want to remove \"{PLAN}\" from this slot?"
the_plan_was_successfully_removed_from_the_slot: "The plan was successfully removed from the slot."
DATE_slot: "{DATE} espacio:"
what_kind_of_slot_do_you_want_to_create: "¿Qué tipo de horario desea crear?"
training: "Formación"

View File

@ -32,6 +32,8 @@ fr:
beware_this_cannot_be_reverted: "Attention : ceci n'est pas réversible."
the_machine_was_successfully_removed_from_the_slot: "La machine a bien été supprimée du créneau."
deletion_failed: "La suppression a échouée."
do_you_really_want_to_remove_PLAN_from_this_slot: "Êtes-vous {GENDER, select, female{sûre} other{sûr}} de vouloir retirer \"{PLAN}\" de ce créneau ?"
the_plan_was_successfully_removed_from_the_slot: "Le formule d'abonnement a bien été supprimée du créneau."
DATE_slot: "Créneau du {DATE} :"
what_kind_of_slot_do_you_want_to_create: "Quel type de créneau voulez-vous créer ?"
training: "Formation"

View File

@ -32,6 +32,8 @@ pt:
beware_this_cannot_be_reverted: "Cuidado: isso não pode ser revertido."
the_machine_was_successfully_removed_from_the_slot: "A máquina foi removida com sucesso desse slot."
deletion_failed: "Falha ao deletar."
do_you_really_want_to_remove_PLAN_from_this_slot: "Do you really want to remove \"{PLAN}\" from this slot?"
the_plan_was_successfully_removed_from_the_slot: "The plan was successfully removed from the slot."
DATE_slot: "{DATE} slot:"
what_kind_of_slot_do_you_want_to_create: "Qual tipo de slot você deseja criar?"
training: "Treinamento"

View File

@ -441,3 +441,7 @@ en:
online_payment_disabled: "Online payment is not available. Please contact the Fablab reception directly."
slot_restrict_plans: "This slot is restricted for the plans below:"
slot_restrict_subscriptions_must_select_plan: "The slot is restricted for the subscribers. Please select a plan first."
slot_restrict_plans_of_others_groups: "The slot is restricted for the subscribers of others groups."
selected_plan_dont_match_slot: "Selected plan dont match this slot"
user_plan_dont_match_slot: "User subscribed plan dont match this slot"
no_plan_match_slot: "You dont have any plan to match this slot"

View File

@ -418,3 +418,7 @@ es:
online_payment_disabled: "El pago en línea no está disponible. Póngase en contacto directamente con la recepción de Fablab."
slot_restrict_plans: "This slot is restricted for the plans below:"
slot_restrict_subscriptions_must_select_plan: "The slot is restricted for the subscribers. Please select a plan first."
slot_restrict_plans_of_others_groups: "The slot is restricted for the subscribers of others groups."
selected_plan_dont_match_slot: "Selected plan dont match this slot"
user_plan_dont_match_slot: "User subscribed plan dont match this slot"
no_plan_match_slot: "You dont have any plan to match this slot"

View File

@ -441,3 +441,7 @@ fr:
online_payment_disabled: "Le payment par carte bancaire n'est pas disponible. Merci de contacter directement l'accueil du Fablab."
slot_restrict_plans: "Ce créneau est restreint pour les formules d'abonnement ci-desous:"
slot_restrict_subscriptions_must_select_plan: "Le créneau est restreint pour les abonnés. Veuillez tout d'abord sélectionner une formule d'abonnement"
slot_restrict_plans_of_others_groups: "Ce créneau est restreint pour les abonnés d'autres groupes."
selected_plan_dont_match_slot: "L'abonnement sélectionné ne correspondent pas ce créneau"
user_plan_dont_match_slot: "L'abonnement du membre ne correspondent pas ce créneau"
no_plan_match_slot: "Aucun abonnement correspondent ce créneau"

View File

@ -441,3 +441,7 @@ pt:
online_payment_disabled: "O pagamento online não está disponível. Entre em contato diretamente com a recepção do Fablab."
slot_restrict_plans: "This slot is restricted for the plans below:"
slot_restrict_subscriptions_must_select_plan: "The slot is restricted for the subscribers. Please select a plan first."
slot_restrict_plans_of_others_groups: "The slot is restricted for the subscribers of others groups."
selected_plan_dont_match_slot: "Selected plan dont match this slot"
user_plan_dont_match_slot: "User subscribed plan dont match this slot"
no_plan_match_slot: "You dont have any plan to match this slot"