/* eslint-disable handle-callback-err, no-return-assign, no-undef, */ // TODO: This file was created by bulk-decaffeinate. // Fix any style issues and re-enable lint. /* * decaffeinate suggestions: * DS101: Remove unnecessary use of Array.from * DS102: Remove unnecessary code created because of implicit returns * DS205: Consider reworking code to avoid use of IIFEs * DS207: Consider shorter variations of null checks * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md */ 'use strict'; /** * Controller used in the prices edition page */ Application.Controllers.controller('EditPricingController', ['$scope', '$state', '$uibModal', '$filter', 'TrainingsPricing', 'Credit', 'Pricing', 'Plan', 'Coupon', 'plans', 'groups', 'growl', 'machinesPricesPromise', 'Price', 'dialogs', 'trainingsPricingsPromise', 'trainingsPromise', 'machineCreditsPromise', 'machinesPromise', 'trainingCreditsPromise', 'couponsPromise', 'spacesPromise', 'spacesPricesPromise', 'spacesCreditsPromise', '_t', 'Member', 'uiTourService', function ($scope, $state, $uibModal, $filter, TrainingsPricing, Credit, Pricing, Plan, Coupon, plans, groups, growl, machinesPricesPromise, Price, dialogs, trainingsPricingsPromise, trainingsPromise, machineCreditsPromise, machinesPromise, trainingCreditsPromise, couponsPromise, spacesPromise, spacesPricesPromise, spacesCreditsPromise, _t, Member, uiTourService) { /* PUBLIC SCOPE */ // List of machines prices (not considering any plan) $scope.machinesPrices = machinesPricesPromise; // List of trainings pricing $scope.trainingsPricings = trainingsPricingsPromise; // List of available subscriptions plans (eg. student/month, PME/year ...) $scope.plans = plans; $scope.enabledPlans = plans.filter(function (p) { return !p.disabled; }); // List of groups (eg. normal, student ...) $scope.groups = groups.filter(function (g) { return g.slug !== 'admins'; }); $scope.enabledGroups = groups.filter(function (g) { return (g.slug !== 'admins') && !g.disabled; }); // Associate free machine hours with subscriptions $scope.machineCredits = machineCreditsPromise; // Array of associations (plan <-> training) $scope.trainingCredits = trainingCreditsPromise; // Associate a plan with all its trainings ids $scope.trainingCreditsGroups = {}; // List of trainings $scope.trainings = trainingsPromise.filter(function (t) { return !t.disabled; }); // List of machines $scope.machines = machinesPromise; $scope.enabledMachines = machinesPromise.filter(function (m) { return !m.disabled; }); // List of coupons $scope.coupons = couponsPromise; $scope.couponsPage = 1; // List of spaces $scope.spaces = spacesPromise; $scope.enabledSpaces = spacesPromise.filter(function (s) { return !s.disabled; }); // Associate free space hours with subscriptions $scope.spaceCredits = spacesCreditsPromise; // List of spaces prices (not considering any plan) $scope.spacesPrices = spacesPricesPromise; // The plans list ordering. Default: by group $scope.orderPlans = 'group_id'; // Status of the drop-down menu in Credits tab $scope.status = { isopen: false }; // Default: we show only enabled plans $scope.planFiltering = 'enabled'; // Available options for filtering plans by status $scope.filterDisabled = [ 'enabled', 'disabled', 'all' ]; // Default: we do not filter coupons $scope.filter = { coupon: 'all' }; // Available status for filtering coupons $scope.couponStatus = [ 'all', 'disabled', 'expired', 'sold_out', 'active' ]; // default tab: plans list $scope.tabs = { active: 0 }; /** * Retrieve a training price from all the trainings prices * @param trainingsPricings {Array} all trainings prices * @param trainingId {number} * @param groupId {number} * @returns {float} */ $scope.findTrainingsPricing = function (trainingsPricings, trainingId, groupId) { for (let trainingsPricing of Array.from(trainingsPricings)) { if ((trainingsPricing.training_id === trainingId) && (trainingsPricing.group_id === groupId)) { return trainingsPricing; } } }; /** * Update the price of a training for the given parameters * @param data {float} the new price * @param trainingsPricing {Object} the training pricing to update * @returns {Promise|string} */ $scope.updateTrainingsPricing = function (data, trainingsPricing) { if (data != null) { return TrainingsPricing.update({ id: trainingsPricing.id }, { trainings_pricing: { amount: data } }).$promise; } else { return _t('app.admin.pricing.please_specify_a_number'); } }; /** * Retrieve a plan from its given identifier and returns it * @param id {number} plan ID * @returns {Object} Plan, inherits from $resource */ $scope.getPlanFromId = function (id) { for (let plan of Array.from($scope.plans)) { if (plan.id === parseInt(id)) { return plan; } } }; /** * Retrieve a group from its given identifier and returns it * @param id {number} group ID * @returns {Object} Group, inherits from $resource */ $scope.getGroupFromId = function (groups, id) { for (let group of Array.from(groups)) { if (group.id === parseInt(id)) { return group; } } }; /** * Returns a human readable string of named trainings, according to the provided array. * $scope.trainings may contains the full list of training. The returned string will only contains the trainings * whom ID are given in the provided parameter * @param trainings {Array} trainings IDs array */ $scope.showTrainings = function (trainings) { if (!angular.isArray(trainings) || !(trainings.length > 0)) { return _t('app.admin.pricing.none'); } const selected = []; angular.forEach($scope.trainings, function (t) { if (trainings.indexOf(t.id) >= 0) { return selected.push(t.name); } }); if (selected.length) { return selected.join(' | '); } else { return _t('app.admin.pricing.none'); } }; /** * Validation callback when editing training's credits. Save the changes. * @param newdata {Object} training and associated plans * @param planId {number|string} plan id */ $scope.saveTrainingCredits = function (newdata, planId) { // save the number of credits Plan.update( { id: planId }, { training_credit_nb: newdata.training_credits } , angular.noop() // do nothing in case of success , function (error) { growl.error(_t('app.admin.pricing.an_error_occurred_while_saving_the_number_of_credits')); console.error(error); } ); // save the associated trainings return angular.forEach($scope.trainingCreditsGroups, function (original, key) { if (parseInt(key) === parseInt(planId)) { // we've got the original data if (original.join('_') !== newdata.training_ids.join('_')) { // if any changes // iterate through the previous credits to remove angular.forEach(original, function (oldTrainingId) { if (newdata.training_ids.indexOf(oldTrainingId) === -1) { const tc = findTrainingCredit(oldTrainingId, planId); if (tc) { return tc.$delete({} , function () { $scope.trainingCredits.splice($scope.trainingCredits.indexOf(tc), 1); return $scope.trainingCreditsGroups[planId].splice($scope.trainingCreditsGroups[planId].indexOf(tc.id), 1); } , function (error) { growl.error(_t('app.admin.pricing.an_error_occurred_while_deleting_credit_with_the_TRAINING', { TRAINING: tc.creditable.name })); console.error(error); }); } else { return growl.error(_t('app.admin.pricing.an_error_occurred_unable_to_find_the_credit_to_revoke')); } } }); // iterate through the new credits to add return angular.forEach(newdata.training_ids, function (newTrainingId) { if (original.indexOf(newTrainingId) === -1) { return Credit.save({ credit: { creditable_id: newTrainingId, creditable_type: 'Training', plan_id: planId } } , function (newTc) { // success $scope.trainingCredits.push(newTc); return $scope.trainingCreditsGroups[newTc.plan_id].push(newTc.creditable_id); } , function (error) { // failed const training = getTrainingFromId(newTrainingId); growl.error(_t('app.admin.pricing.an_error_occurred_while_creating_credit_with_the_TRAINING', { TRAINING: training.name })); return console.error(error); }); } }); } } }); }; /** * Cancel the current training credit modification * @param rowform {Object} see http://vitalets.github.io/angular-xeditable/ */ $scope.cancelTrainingCredit = function (rowform) { rowform.$cancel(); }; /** * Create a new empty entry in the $scope.machineCredits array * @param e {Object} see https://docs.angularjs.org/guide/expression#-event- */ $scope.addMachineCredit = function (e) { e.preventDefault(); e.stopPropagation(); $scope.inserted = { creditable_type: 'Machine' }; $scope.machineCredits.push($scope.inserted); return $scope.status.isopen = !$scope.status.isopen; }; /** * In the Credits tab, return the name of the machine/space associated with the given credit * @param credit {Object} credit object, inherited from $resource * @returns {String} */ $scope.showCreditableName = function (credit) { let selected = _t('app.admin.pricing.not_set'); if (credit && credit.creditable_id) { const object = $scope.getCreditable(credit); selected = object.name; if (credit.creditable_type === 'Machine') { selected += ` ( id. ${object.id} )`; } } return selected; }; /** * In the Credits tab, return the machine/space associated with the given credit * @param credit {Object} credit object, inherited from $resource * @returns {Object} */ $scope.getCreditable = function (credit) { let selected; if (credit && credit.creditable_id) { if (credit.creditable_type === 'Machine') { angular.forEach($scope.machines, function (m) { if (m.id === credit.creditable_id) { return selected = m; } }); } else if (credit.creditable_type === 'Space') { angular.forEach($scope.spaces, function (s) { if (s.id === credit.creditable_id) { return selected = s; } }); } } return selected; }; /** * Validation callback when editing machine's credits. Save the changes. * This will prevent the creation of two credits associating the same machine and plan. * @param data {Object} machine, associated plan and number of credit hours. * @param [id] {number} credit id for edition, create a new credit object if not provided */ $scope.saveMachineCredit = function (data, id) { for (let mc of Array.from($scope.machineCredits)) { if ((mc.plan_id === data.plan_id) && (mc.creditable_id === data.creditable_id) && ((id === null) || (mc.id !== id))) { growl.error(_t('app.admin.pricing.error_a_credit_linking_this_machine_with_that_subscription_already_exists')); if (!id) { $scope.machineCredits.pop(); } return false; } } if (id != null) { return Credit.update({ id }, { credit: data }, function () { growl.success(_t('app.admin.pricing.changes_have_been_successfully_saved')); }); } else { data.creditable_type = 'Machine'; return Credit.save( { credit: data } , function (resp) { $scope.machineCredits[$scope.machineCredits.length - 1].id = resp.id; return growl.success(_t('app.admin.pricing.credit_was_successfully_saved')); } , function (err) { $scope.machineCredits.pop(); growl.error(_t('app.admin.pricing.error_creating_credit')); console.error(err); }); } }; /** * Removes the newly inserted but not saved machine credit / Cancel the current machine credit modification * @param rowform {Object} see http://vitalets.github.io/angular-xeditable/ * @param index {number} credit index in the $scope.machineCredits array */ $scope.cancelMachineCredit = function (rowform, index) { if ($scope.machineCredits[index].id != null) { return rowform.$cancel(); } else { return $scope.machineCredits.splice(index, 1); } }; /** * Deletes the machine credit at the specified index * @param index {number} machine credit index in the $scope.machineCredits array */ $scope.removeMachineCredit = function (index) { Credit.delete($scope.machineCredits[index]); $scope.machineCredits.splice(index, 1); }; /** * Create a new empty entry in the $scope.spaceCredits array * @param e {Object} see https://docs.angularjs.org/guide/expression#-event- */ $scope.addSpaceCredit = function (e) { e.preventDefault(); e.stopPropagation(); $scope.inserted = { creditable_type: 'Space' }; $scope.spaceCredits.push($scope.inserted); $scope.status.isopen = !$scope.status.isopen; }; /** * Validation callback when editing space's credits. Save the changes. * This will prevent the creation of two credits associated with the same space and plan. * @param data {Object} space, associated plan and number of credit hours. * @param [id] {number} credit id for edition, create a new credit object if not provided */ $scope.saveSpaceCredit = function (data, id) { for (let sc of Array.from($scope.spaceCredits)) { if ((sc.plan_id === data.plan_id) && (sc.creditable_id === data.creditable_id) && ((id === null) || (sc.id !== id))) { growl.error(_t('app.admin.pricing.error_a_credit_linking_this_space_with_that_subscription_already_exists')); if (!id) { $scope.spaceCredits.pop(); } return false; } } if (id != null) { return Credit.update({ id }, { credit: data }, function () { growl.success(_t('app.admin.pricing.changes_have_been_successfully_saved')); }); } else { data.creditable_type = 'Space'; return Credit.save( { credit: data } , function (resp) { $scope.spaceCredits[$scope.spaceCredits.length - 1].id = resp.id; return growl.success(_t('app.admin.pricing.credit_was_successfully_saved')); } , function (err) { $scope.spaceCredits.pop(); return growl.error(_t('app.admin.pricing.error_creating_credit')); }); } }; /** * Removes the newly inserted but not saved space credit / Cancel the current space credit modification * @param rowform {Object} see http://vitalets.github.io/angular-xeditable/ * @param index {number} credit index in the $scope.spaceCredits array */ $scope.cancelSpaceCredit = function (rowform, index) { if ($scope.spaceCredits[index].id != null) { return rowform.$cancel(); } else { return $scope.spaceCredits.splice(index, 1); } }; /** * Deletes the space credit at the specified index * @param index {number} space credit index in the $scope.spaceCredits array */ $scope.removeSpaceCredit = function (index) { Credit.delete($scope.spaceCredits[index]); return $scope.spaceCredits.splice(index, 1); }; /** * If the plan does not have a type, return a default value for display purposes * @param type {string|undefined|null} plan's type (eg. 'partner') * @returns {string} */ $scope.getPlanType = function (type) { if (type === 'PartnerPlan') { return _t('app.admin.pricing.partner'); } else { return _t('app.admin.pricing.standard'); } }; /** * Change the plans ordering criterion to the one provided * @param orderBy {string} ordering criterion */ $scope.setOrderPlans = function (orderBy) { if ($scope.orderPlans === orderBy) { return $scope.orderPlans = `-${orderBy}`; } else { return $scope.orderPlans = orderBy; } }; /** * Retrieve a price from prices array by a machineId and a groupId */ $scope.findPriceBy = function (prices, machineId, groupId) { for (let price of Array.from(prices)) { if ((price.priceable_id === machineId) && (price.group_id === groupId)) { return price; } } }; /** * update a price for a machine and a group, not considering any plan */ $scope.updatePrice = function (data, price) { if (data != null) { return Price.update({ id: price.id }, { price: { amount: data } }).$promise; } else { return _t('app.admin.pricing.please_specify_a_number'); } }; /** * Delete the specified subcription plan * @param id {number} plan id */ $scope.deletePlan = function (plans, id) { if (typeof id !== 'number') { return console.error('[EditPricingController::deletePlan] Error: invalid id parameter'); } else { // open a confirmation dialog return dialogs.confirm( { resolve: { object () { return { title: _t('app.admin.pricing.confirmation_required'), msg: _t('app.admin.pricing.do_you_really_want_to_delete_this_subscription_plan') }; } } }, function () { // the admin has confirmed, delete the plan Plan.delete( { id }, function (res) { growl.success(_t('app.admin.pricing.subscription_plan_was_successfully_deleted')); return $scope.plans.splice(findItemIdxById(plans, id), 1); }, function (error) { if (error.statusText) { console.error(`[EditPricingController::deletePlan] Error: ${error.statusText}`); } growl.error(_t('app.admin.pricing.unable_to_delete_the_specified_subscription_an_error_occurred')); } ); } ); } }; /** * Generate a string identifying the given plan by literal humain-readable name * @param plan {Object} Plan object, as recovered from GET /api/plan/:id * @param groups {Array} List of Groups objects, as recovered from GET /api/groups * @param short {boolean} If true, the generated name will contains the group slug, otherwise the group full name * will be included. * @returns {String} */ $scope.humanReadablePlanName = function (plan, groups, short) { return `${$filter('humanReadablePlanName')(plan, groups, short)}`; }; /** * Delete a coupon from the server's database and, in case of success, from the list in memory * @param coupons {Array} should be called with $scope.coupons * @param id {number} ID of the coupon to delete */ $scope.deleteCoupon = function (coupons, id) { if (typeof id !== 'number') { return console.error('[EditPricingController::deleteCoupon] Error: invalid id parameter'); } else { // open a confirmation dialog return dialogs.confirm({ resolve: { object () { return { title: _t('app.admin.pricing.confirmation_required'), msg: _t('app.admin.pricing.do_you_really_want_to_delete_this_coupon') }; } } } , function () { // the admin has confirmed, delete the coupon Coupon.delete({ id }, function (res) { growl.success(_t('app.admin.pricing.coupon_was_successfully_deleted')); return $scope.coupons.splice(findItemIdxById(coupons, id), 1); } , function (error) { if (error.statusText) { console.error(`[EditPricingController::deleteCoupon] Error: ${error.statusText}`); } if (error.status === 422) { return growl.error(_t('app.admin.pricing.unable_to_delete_the_specified_coupon_already_in_use')); } else { return growl.error(_t('app.admin.pricing.unable_to_delete_the_specified_coupon_an_unexpected_error_occurred')); } }); }); } }; /** * Open a modal allowing to select an user and send him the details of the provided coupon * @param coupon {Object} The coupon to send */ $scope.sendCouponToUser = function (coupon) { $uibModal.open({ templateUrl: '<%= asset_path "admin/pricing/sendCoupon.html" %>', resolve: { coupon () { return coupon; } }, size: 'md', controller: ['$scope', '$uibModalInstance', 'Coupon', 'coupon', '_t', function ($scope, $uibModalInstance, Coupon, coupon, _t) { // Member, receiver of the coupon $scope.ctrl = { member: null }; // Details of the coupon to send $scope.coupon = coupon; // Callback to validate sending of the coupon $scope.ok = function () { Coupon.send({ coupon_code: coupon.code, user_id: $scope.ctrl.member.id }, function (res) { growl.success(_t('app.admin.pricing.coupon_successfully_sent_to_USER', { USER: $scope.ctrl.member.name })); return $uibModalInstance.close({ user_id: $scope.ctrl.member.id }); } , function (err) { growl.error(_t('app.admin.pricing.an_error_occurred_unable_to_send_the_coupon')); console.error(err); }); }; // Callback to close the modal and cancel the sending process $scope.cancel = function () { $uibModalInstance.dismiss('cancel'); }; }] }); }; /** * Load the next 10 coupons */ $scope.loadMore = function() { $scope.couponsPage++; Coupon.query({ page: $scope.couponsPage, filter: $scope.filter.coupon }, function (data) { $scope.coupons = $scope.coupons.concat(data); }); }; /** * Reset the list of coupons according to the newly selected filter */ $scope.updateCouponFilter = function() { $scope.couponsPage = 1; Coupon.query({ page: $scope.couponsPage, filter: $scope.filter.coupon }, function (data) { $scope.coupons = data; }); } /** * Setup the feature-tour for the admin/pricing page. * This is intended as a contextual help (when pressing F1) */ $scope.setupPricingTour = function () { // get the tour defined by the ui-tour directive const uitour = uiTourService.getTourByName('pricing'); uitour.createStep({ selector: 'body', stepId: 'welcome', order: 0, title: _t('app.admin.tour.pricing.welcome.title'), content: _t('app.admin.tour.pricing.welcome.content'), placement: 'bottom', orphan: true }); uitour.createStep({ selector: '.plans-pricing .new-plan-button', stepId: 'new_plan', order: 1, title: _t('app.admin.tour.pricing.new_plan.title'), content: _t('app.admin.tour.pricing.new_plan.content'), placement: 'bottom' }); uitour.createStep({ selector: '.plans-pricing .trainings-tab', stepId: 'trainings', order: 2, title: _t('app.admin.tour.pricing.trainings.title'), content: _t('app.admin.tour.pricing.trainings.content'), placement: 'bottom' }); uitour.createStep({ selector: '.plans-pricing .machines-tab', stepId: 'machines', order: 3, title: _t('app.admin.tour.pricing.machines.title'), content: _t('app.admin.tour.pricing.machines.content'), placement: 'bottom' }); if (!Fablab.withoutSpaces) { uitour.createStep({ selector: '.plans-pricing .spaces-tab', stepId: 'spaces', order: 4, title: _t('app.admin.tour.pricing.spaces.title'), content: _t('app.admin.tour.pricing.spaces.content'), placement: 'bottom' }); } uitour.createStep({ selector: '.plans-pricing .credits-tab', stepId: 'credits', order: 5, title: _t('app.admin.tour.pricing.credits.title'), content: _t('app.admin.tour.pricing.credits.content'), placement: 'bottom' }); uitour.createStep({ selector: '.plans-pricing .coupons-tab', stepId: 'coupons', order: 6, title: _t('app.admin.tour.pricing.coupons.title'), content: _t('app.admin.tour.pricing.coupons.content'), placement: 'bottom' }); uitour.createStep({ selector: 'body', stepId: 'conclusion', order: 7, title: _t('app.admin.tour.conclusion.title'), content: _t('app.admin.tour.conclusion.content'), placement: 'bottom', orphan: true }); // on step change, change the active tab if needed uitour.on('stepChanged', function (nextStep) { if (nextStep.stepId === 'new_plan') { $scope.tabs.active = 0; } if (nextStep.stepId === 'trainings') { $scope.tabs.active = 1; } if (nextStep.stepId === 'machines') { $scope.tabs.active = 2; } if (nextStep.stepId === 'spaces') { $scope.tabs.active = 3; } if (nextStep.stepId === 'credits') { $scope.tabs.active = 4; } if (nextStep.stepId === 'coupons') { $scope.tabs.active = 5; } }); // on tour end, save the status in database uitour.on('ended', function () { if (uitour.getStatus() === uitour.Status.ON && $scope.currentUser.profile.tours.indexOf('pricing') < 0) { Member.completeTour({ id: $scope.currentUser.id }, { tour: 'pricing' }, function (res) { $scope.currentUser.profile.tours = res.tours; }); } }); // if the user has never seen the tour, show him now if ($scope.currentUser.profile.tours.indexOf('pricing') < 0) { uitour.start(); } // start this tour when an user press F1 - this is contextual help window.addEventListener('keydown', handleF1); } /* PRIVATE SCOPE */ /** * Kind of constructor: these actions will be realized first when the controller is loaded */ const initialize = function () { $scope.trainingCreditsGroups = groupCreditsByPlan($scope.trainingCredits); // listen the $destroy event of the controller to remove the F1 key binding $scope.$on('$destroy', function () { window.removeEventListener('keydown', handleF1); }); // adds empty array for plan which hasn't any credits yet return (function () { const result = []; for (let plan of Array.from($scope.plans)) { if ($scope.trainingCreditsGroups[plan.id] == null) { result.push($scope.trainingCreditsGroups[plan.id] = []); } else { result.push(undefined); } } return result; })(); }; /** * Callback used to trigger the feature tour when the user press the F1 key. * @param e {KeyboardEvent} */ const handleF1 = function (e) { if (e.key === 'F1') { e.preventDefault(); const tour = uiTourService.getTourByName('pricing'); if (tour) { tour.start(); } } }; /** * Retrieve an item index by its ID from the given array of objects * @param items {Array<{id:number}>} * @param id {number} * @returns {number} item index in the provided array */ var findItemIdxById = function (items, id) { return (items.map(function (item) { return item.id; })).indexOf(id); }; /** * Group the given credits array into a map associating the plan ID with its associated trainings/machines * @return {Object} the association map */ var groupCreditsByPlan = function (credits) { const creditsMap = {}; angular.forEach(credits, function (c) { if (!creditsMap[c.plan_id]) { creditsMap[c.plan_id] = []; } return creditsMap[c.plan_id].push(c.creditable_id); }); return creditsMap; }; /** * Iterate through $scope.traininfCredits to find the credit matching the given criterion * @param trainingId {number|string} training ID * @param planId {number|string} plan ID */ var findTrainingCredit = function (trainingId, planId) { trainingId = parseInt(trainingId); planId = parseInt(planId); for (let credit of Array.from($scope.trainingCredits)) { if ((credit.plan_id === planId) && (credit.creditable_id === trainingId)) { return credit; } } }; /** * Retrieve a training from its given identifier and returns it * @param id {number} training ID * @returns {Object} Training inherited from $resource */ var getTrainingFromId = function (id) { for (let training of Array.from($scope.trainings)) { if (training.id === parseInt(id)) { return training; } } }; // !!! MUST BE CALLED AT THE END of the controller return initialize(); } ]);