diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d79f2ba4..9d79ab451 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog Fab-manager +## v4.7.6 2021 March 24 +- Ability to disable the trainings module +- Ability to set the address as a mandatory field +- The address is new requested when creating an account +- The profile completion page is less fuzzy for people landing on it without enabled SSO +- Prevent showing error message when testing for old versions during upgrade +- In the email notification, sent to admins on account creation, show the group of the user +- More explanations in the setup script +- Send pre-compressed assets to the browsers instead of the regular ones +- Links created using "medium editor" opens in new tabs +- Improved style of public plans page +- Improved the upgrade script +- Fix a bug: subscriptions tab is selected by default in statistics, even if the module is disabled +- Fix a bug: select all plans for slot restriction (through the dedicated button) also selects the disabled plans +- Fix a bug: recurring availabilities are not restricted to subscribers +- Fix a bug: accounting exports may ignore some invoices for the first and last days +- Fix a bug: accounting export caching is not working +- Fix a bug: unable to run the setup script if sudoers belong to another group than sudo +- Fix a security issue: updated elliptic to 6.5.4 to fix [CVE-2020-28498](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-28498) +- [TODO DEPLOY] `\curl -sSL https://raw.githubusercontent.com/sleede/fab-manager/master/scripts/nginx-packs-directive.sh | bash` +- [TODO DEPLOY] `rails db:seed` +- [TODO DEPLOY] `rails fablab:maintenance:rebuild_stylesheet` + ## v4.7.5 2021 March 08 - Fix a bug: unable to compile the assets during the upgrade, if the env file has some whitespaces around the equal sign diff --git a/app/controllers/api/accounting_exports_controller.rb b/app/controllers/api/accounting_exports_controller.rb index 24d15492a..20dd342dd 100644 --- a/app/controllers/api/accounting_exports_controller.rb +++ b/app/controllers/api/accounting_exports_controller.rb @@ -8,7 +8,7 @@ class API::AccountingExportsController < API::ApiController def export authorize :accounting_export - export = Export.where(category: 'accounting', export_type: 'accounting-software', key: params[:key]) + export = Export.where(category: 'accounting', export_type: params[:type], key: params[:key]) .where(extension: params[:extension], query: params[:query]) .where('created_at > ?', Invoice.maximum('updated_at')) .last diff --git a/app/controllers/api/auth_providers_controller.rb b/app/controllers/api/auth_providers_controller.rb index 1efd31c44..6a4b5bea0 100644 --- a/app/controllers/api/auth_providers_controller.rb +++ b/app/controllers/api/auth_providers_controller.rb @@ -49,6 +49,7 @@ class API::AuthProvidersController < API::ApiController def active authorize AuthProvider @provider = AuthProvider.active + @previous = AuthProvider.previous end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 181777e43..10b7c224b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -41,7 +41,8 @@ class ApplicationController < ActionController::Base { profile_attributes: %i[phone last_name first_name interest software_mastered], invoicing_profile_attributes: [ - organization_attributes: [:name, address_attributes: [:address]] + organization_attributes: [:name, address_attributes: [:address]], + address_attributes: [:address] ], statistic_profile_attributes: %i[gender birthday] }, diff --git a/app/frontend/src/javascript/controllers/admin/calendar.js b/app/frontend/src/javascript/controllers/admin/calendar.js index 3f84b0d35..79cf9a094 100644 --- a/app/frontend/src/javascript/controllers/admin/calendar.js +++ b/app/frontend/src/javascript/controllers/admin/calendar.js @@ -665,7 +665,7 @@ Application.Controllers.controller('CreateEventModalController', ['$scope', '$ui $scope.selectedPlans = []; $scope.selectedPlansBinding = {}; if (count === 0) { - plansPromise.forEach(function (plan) { + plansPromise.filter(p => !p.disabled).forEach(function (plan) { $scope.selectedPlans.push(plan); $scope.selectedPlansBinding[plan.id] = true; }); diff --git a/app/frontend/src/javascript/controllers/admin/graphs.js b/app/frontend/src/javascript/controllers/admin/graphs.js index 3883405e0..c7320006b 100644 --- a/app/frontend/src/javascript/controllers/admin/graphs.js +++ b/app/frontend/src/javascript/controllers/admin/graphs.js @@ -51,6 +51,9 @@ Application.Controllers.controller('GraphsController', ['$scope', '$state', '$ro // active tab will be set here $scope.selectedIndex = null; + // ui-bootstrap active tab index + $scope.selectedTab = 0; + // for palmares graphs, filters values are stored here $scope.ranking = { sortCriterion: 'ca', @@ -101,9 +104,11 @@ Application.Controllers.controller('GraphsController', ['$scope', '$state', '$ro * Callback called when the active tab is changed. * Recover the current tab and store its value in $scope.selectedIndex * @param tab {Object} elasticsearch statistic structure + * @param index {number} index of the tab in the $scope.statistics array */ - $scope.setActiveTab = function (tab) { + $scope.setActiveTab = function (tab, index) { $scope.selectedIndex = tab; + $scope.selectedTab = index; $scope.ranking.groupCriterion = 'subType'; if (tab.ca) { $scope.ranking.sortCriterion = 'ca'; @@ -113,6 +118,18 @@ Application.Controllers.controller('GraphsController', ['$scope', '$state', '$ro return refreshChart(); }; + /** + * Returns true if the provided tab must be hidden due to some global or local configuration + * @param tab {Object} elasticsearch statistic structure (from statistic_indices table) + */ + $scope.hiddenTab = function (tab) { + if (tab.graph) { + return !((tab.es_type_key === 'subscription' && !$rootScope.modules.plans) || + (tab.es_type_key === 'training' && !$rootScope.modules.trainings)); + } + return false; + }; + /** * Callback to close the date-picking popup and refresh the results */ @@ -137,11 +154,20 @@ Application.Controllers.controller('GraphsController', ['$scope', '$state', '$ro $scope.$watch(scope => scope.ranking.groupCriterion , (newValue, oldValue) => refreshChart()); return refreshChart(); + + // set the default tab to "machines" if "subscriptions" are disabled + if (!$rootScope.modules.plans) { + const idx = $scope.statistics.findIndex(s => s.es_type_key === 'machine'); + $scope.setActiveTab($scope.statistics[idx], idx); + } else { + const idx = $scope.statistics.findIndex(s => s.es_type_key === 'subscription'); + $scope.setActiveTab($scope.statistics[idx], idx); + } }); // workaround for angular-bootstrap::tabs behavior: on tab deletion, another tab will be selected // which will cause every tabs to reload, one by one, when the view is closed - return $rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) { + $rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) { if ((fromState.name === 'app.admin.stats_graphs') && (Object.keys(fromParams).length === 0)) { return $scope.preventRefresh = true; } diff --git a/app/frontend/src/javascript/controllers/admin/invoices.js b/app/frontend/src/javascript/controllers/admin/invoices.js index 168e42089..31a0a240a 100644 --- a/app/frontend/src/javascript/controllers/admin/invoices.js +++ b/app/frontend/src/javascript/controllers/admin/invoices.js @@ -1309,8 +1309,8 @@ Application.Controllers.controller('AccountingExportModalController', ['$scope', columns: $scope.exportTarget.settings.columns, encoding: $scope.exportTarget.settings.encoding, date_format: $scope.exportTarget.settings.dateFormat, - start_date: $scope.exportTarget.startDate, - end_date: $scope.exportTarget.endDate, + start_date: moment.utc($scope.exportTarget.startDate).startOf('day').toISOString(), + end_date: moment.utc($scope.exportTarget.endDate).endOf('day').toISOString(), label_max_length: $scope.exportTarget.settings.labelMaxLength, decimal_separator: $scope.exportTarget.settings.decimalSeparator, export_invoices_at_zero: $scope.exportTarget.settings.exportInvoicesAtZero @@ -1370,7 +1370,6 @@ Application.Controllers.controller('StripeKeysModalController', ['$scope', '$uib $scope.publicKeyStatus = false; return; } - const today = new Date(); $http({ method: 'POST', url: 'https://api.stripe.com/v1/tokens', diff --git a/app/frontend/src/javascript/controllers/admin/members.js b/app/frontend/src/javascript/controllers/admin/members.js index 505076714..ad5e718db 100644 --- a/app/frontend/src/javascript/controllers/admin/members.js +++ b/app/frontend/src/javascript/controllers/admin/members.js @@ -650,8 +650,8 @@ Application.Controllers.controller('AdminMembersController', ['$scope', '$sce', /** * Controller used in the member edition page */ -Application.Controllers.controller('EditMemberController', ['$scope', '$state', '$stateParams', 'Member', 'Training', 'dialogs', 'growl', 'Group', 'Subscription', 'CSRF', 'memberPromise', 'tagsPromise', '$uibModal', 'Plan', '$filter', '_t', 'walletPromise', 'transactionsPromise', 'activeProviderPromise', 'Wallet', 'phoneRequiredPromise', - function ($scope, $state, $stateParams, Member, Training, dialogs, growl, Group, Subscription, CSRF, memberPromise, tagsPromise, $uibModal, Plan, $filter, _t, walletPromise, transactionsPromise, activeProviderPromise, Wallet, phoneRequiredPromise) { +Application.Controllers.controller('EditMemberController', ['$scope', '$state', '$stateParams', 'Member', 'Training', 'dialogs', 'growl', 'Group', 'Subscription', 'CSRF', 'memberPromise', 'tagsPromise', '$uibModal', 'Plan', '$filter', '_t', 'walletPromise', 'transactionsPromise', 'activeProviderPromise', 'Wallet', 'settingsPromise', + function ($scope, $state, $stateParams, Member, Training, dialogs, growl, Group, Subscription, CSRF, memberPromise, tagsPromise, $uibModal, Plan, $filter, _t, walletPromise, transactionsPromise, activeProviderPromise, Wallet, settingsPromise) { /* PUBLIC SCOPE */ // API URL where the form will be posted @@ -670,7 +670,10 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state', $scope.password = { change: false }; // is the phone number required in _member_form? - $scope.phoneRequired = (phoneRequiredPromise.setting.value === 'true'); + $scope.phoneRequired = (settingsPromise.phone_required === 'true'); + + // is the address required in _member_form? + $scope.addressRequired = (settingsPromise.address_required === 'true'); // the user subscription if (($scope.user.subscribed_plan != null) && ($scope.user.subscription != null)) { @@ -990,8 +993,8 @@ Application.Controllers.controller('EditMemberController', ['$scope', '$state', /** * Controller used in the member's creation page (admin view) */ -Application.Controllers.controller('NewMemberController', ['$scope', '$state', '$stateParams', 'Member', 'Training', 'Group', 'CSRF', 'phoneRequiredPromise', - function ($scope, $state, $stateParams, Member, Training, Group, CSRF, phoneRequiredPromise) { +Application.Controllers.controller('NewMemberController', ['$scope', '$state', '$stateParams', 'Member', 'Training', 'Group', 'CSRF', 'settingsPromise', + function ($scope, $state, $stateParams, Member, Training, Group, CSRF, settingsPromise) { CSRF.setMetaTags(); /* PUBLIC SCOPE */ @@ -1006,7 +1009,10 @@ Application.Controllers.controller('NewMemberController', ['$scope', '$state', ' $scope.password = { change: false }; // is the phone number required in _member_form? - $scope.phoneRequired = (phoneRequiredPromise.setting.value === 'true'); + $scope.phoneRequired = (settingsPromise.phone_required === 'true'); + + // is the address required to sign-up? + $scope.addressRequired = (settingsPromise.address_required === 'true'); // Default member's profile parameters $scope.user = { @@ -1109,8 +1115,8 @@ Application.Controllers.controller('ImportMembersResultController', ['$scope', ' /** * Controller used in the admin creation page (admin view) */ -Application.Controllers.controller('NewAdminController', ['$state', '$scope', 'Admin', 'growl', '_t', 'phoneRequiredPromise', - function ($state, $scope, Admin, growl, _t, phoneRequiredPromise) { +Application.Controllers.controller('NewAdminController', ['$state', '$scope', 'Admin', 'growl', '_t', 'settingsPromise', + function ($state, $scope, Admin, growl, _t, settingsPromise) { // default admin profile let getGender; $scope.admin = { @@ -1131,7 +1137,10 @@ Application.Controllers.controller('NewAdminController', ['$state', '$scope', 'A }; // is the phone number required in _admin_form? - $scope.phoneRequired = (phoneRequiredPromise.setting.value === 'true'); + $scope.phoneRequired = (settingsPromise.phone_required === 'true'); + + // is the address required in _admin_form? + $scope.addressRequired = (settingsPromise.address_required === 'true'); /** * Shows the birthday datepicker diff --git a/app/frontend/src/javascript/controllers/admin/pricing.js b/app/frontend/src/javascript/controllers/admin/pricing.js index f3c08ab5b..da31903eb 100644 --- a/app/frontend/src/javascript/controllers/admin/pricing.js +++ b/app/frontend/src/javascript/controllers/admin/pricing.js @@ -111,7 +111,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', * @returns {float} */ $scope.findTrainingsPricing = function (trainingsPricings, trainingId, groupId) { - for (let trainingsPricing of Array.from(trainingsPricings)) { + for (const trainingsPricing of Array.from(trainingsPricings)) { if ((trainingsPricing.training_id === trainingId) && (trainingsPricing.group_id === groupId)) { return trainingsPricing; } @@ -138,7 +138,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', * @returns {Object} Plan, inherits from $resource */ $scope.getPlanFromId = function (id) { - for (let plan of Array.from($scope.plans)) { + for (const plan of Array.from($scope.plans)) { if (plan.id === parseInt(id)) { return plan; } @@ -151,7 +151,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', * @returns {Object} Group, inherits from $resource */ $scope.getGroupFromId = function (groups, id) { - for (let group of Array.from(groups)) { + for (const group of Array.from(groups)) { if (group.id === parseInt(id)) { return group; } @@ -313,7 +313,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', * @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)) { + for (const 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) { @@ -383,7 +383,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', * @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)) { + for (const 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) { @@ -459,7 +459,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', * 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)) { + for (const price of Array.from(prices)) { if ((price.priceable_id === machineId) && (price.group_id === groupId)) { return price; } @@ -603,7 +603,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', /** * Load the next 10 coupons */ - $scope.loadMore = function() { + $scope.loadMore = function () { $scope.couponsPage++; Coupon.query({ page: $scope.couponsPage, filter: $scope.filter.coupon }, function (data) { $scope.coupons = $scope.coupons.concat(data); @@ -613,19 +613,19 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', /** * Reset the list of coupons according to the newly selected filter */ - $scope.updateCouponFilter = function() { + $scope.updateCouponFilter = function () { $scope.couponsPage = 1; Coupon.query({ page: $scope.couponsPage, filter: $scope.filter.coupon }, function (data) { $scope.coupons = data; }); - } + }; /** * Return the exemple price based on the configuration of the default slot duration. * @param type {string} 'hourly_rate' | * * @returns {number} price for "SLOT_DURATION" minutes. */ - $scope.examplePrice = function(type) { + $scope.examplePrice = function (type) { const hourlyRate = 10; if (type === 'hourly_rate') { @@ -634,7 +634,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', const price = (hourlyRate / 60) * $scope.slotDuration; return $filter('currency')(price); - } + }; /** * Setup the feature-tour for the admin/pricing page. @@ -660,14 +660,16 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', 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' - }); + if ($scope.$root.modules.trainings) { + 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', @@ -733,7 +735,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', if (settingsPromise.feature_tour_display !== 'manual' && $scope.currentUser.profile.tours.indexOf('pricing') < 0) { uitour.start(); } - } + }; /* PRIVATE SCOPE */ @@ -746,7 +748,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', // adds empty array for plan which hasn't any credits yet return (function () { const result = []; - for (let plan of Array.from($scope.plans)) { + for (const plan of Array.from($scope.plans)) { if ($scope.trainingCreditsGroups[plan.id] == null) { result.push($scope.trainingCreditsGroups[plan.id] = []); } else { @@ -763,7 +765,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', * @param id {number} * @returns {number} item index in the provided array */ - var findItemIdxById = function (items, id) { + const findItemIdxById = function (items, id) { return (items.map(function (item) { return item.id; })).indexOf(id); }; @@ -771,7 +773,7 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', * 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 groupCreditsByPlan = function (credits) { const creditsMap = {}; angular.forEach(credits, function (c) { if (!creditsMap[c.plan_id]) { @@ -787,11 +789,11 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', * @param trainingId {number|string} training ID * @param planId {number|string} plan ID */ - var findTrainingCredit = function (trainingId, planId) { + const findTrainingCredit = function (trainingId, planId) { trainingId = parseInt(trainingId); planId = parseInt(planId); - for (let credit of Array.from($scope.trainingCredits)) { + for (const credit of Array.from($scope.trainingCredits)) { if ((credit.plan_id === planId) && (credit.creditable_id === trainingId)) { return credit; } @@ -803,8 +805,8 @@ Application.Controllers.controller('EditPricingController', ['$scope', '$state', * @param id {number} training ID * @returns {Object} Training inherited from $resource */ - var getTrainingFromId = function (id) { - for (let training of Array.from($scope.trainings)) { + const getTrainingFromId = function (id) { + for (const training of Array.from($scope.trainings)) { if (training.id === parseInt(id)) { return training; } diff --git a/app/frontend/src/javascript/controllers/admin/statistics.js b/app/frontend/src/javascript/controllers/admin/statistics.js index 997ee4b68..a08a5c2cb 100644 --- a/app/frontend/src/javascript/controllers/admin/statistics.js +++ b/app/frontend/src/javascript/controllers/admin/statistics.js @@ -76,6 +76,9 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', // active tab will be set here $scope.selectedIndex = null; + // ui-bootstrap active tab index + $scope.selectedTab = 0; + // type filter binding $scope.type = { selected: null, @@ -135,7 +138,7 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', */ $scope.customFieldName = function (field) { return _t(`app.admin.statistics.${field}`); - } + }; /** * Callback to open the datepicker (interval start) @@ -159,9 +162,11 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', * Callback called when the active tab is changed. * recover the current tab and store its value in $scope.selectedIndex * @param tab {Object} elasticsearch statistic structure (from statistic_indices table) + * @param index {number} index of the tab in the $scope.statistics array */ - $scope.setActiveTab = function (tab) { + $scope.setActiveTab = function (tab, index) { $scope.selectedIndex = tab; + $scope.selectedTab = index; $scope.type.selected = tab.types[0]; $scope.type.active = $scope.type.selected; $scope.customFilter.criterion = {}; @@ -179,9 +184,10 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', */ $scope.hiddenTab = function (tab) { if (tab.table) { - if ((tab.es_type_key === 'subscription') && !$rootScope.modules.plans) { - return true; - } else return (tab.es_type_key === 'space') && !$rootScope.modules.spaces; + return ((tab.es_type_key === 'subscription' && !$rootScope.modules.plans) || + (tab.es_type_key === 'training' && !$rootScope.modules.trainings) || + (tab.es_type_key === 'space' && !$rootScope.modules.spaces) + ); } else { return true; } @@ -292,8 +298,8 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', return refreshStats(); } else { return es.scroll({ - 'scroll': ES_SCROLL_TIME + 'm', - 'body': { scrollId: $scope.scrollId } + scroll: ES_SCROLL_TIME + 'm', + body: { scrollId: $scope.scrollId } } , function (error, response) { if (error) { @@ -335,7 +341,7 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', }; return $uibModal.open(options) - .result['finally'](null).then(function (info) { console.log(info); }); + .result.finally(null).then(function (info) { console.log(info); }); }; /** @@ -391,7 +397,7 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', if (settingsPromise.feature_tour_display !== 'manual' && $scope.currentUser.profile.tours.indexOf('statistics') < 0) { uitour.start(); } - } + }; /* PRIVATE SCOPE */ @@ -406,6 +412,15 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', return $scope.preventRefresh = true; } }); + + // set the default tab to "machines" if "subscriptions" are disabled + if (!$rootScope.modules.plans) { + const idx = $scope.statistics.findIndex(s => s.es_type_key === 'machine'); + $scope.setActiveTab($scope.statistics[idx], idx); + } else { + const idx = $scope.statistics.findIndex(s => s.es_type_key === 'subscription'); + $scope.setActiveTab($scope.statistics[idx], idx); + } }; /** @@ -471,15 +486,15 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', // run query return es.search({ - 'index': 'stats', - 'type': index, - 'size': RESULTS_PER_PAGE, - 'scroll': ES_SCROLL_TIME + 'm', + index: 'stats', + type: index, + size: RESULTS_PER_PAGE, + scroll: ES_SCROLL_TIME + 'm', 'stat-type': type, 'custom-query': custom ? JSON.stringify(Object.assign({ exclude: custom.exclude }, buildElasticCustomCriterion(custom))) : '', 'start-date': moment($scope.datePickerStart.selected).format(), 'end-date': moment($scope.datePickerEnd.selected).format(), - 'body': buildElasticDataQuery(type, custom, $scope.agePicker.start, $scope.agePicker.end, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected), $scope.sorting) + body: buildElasticDataQuery(type, custom, $scope.agePicker.start, $scope.agePicker.end, moment($scope.datePickerStart.selected), moment($scope.datePickerEnd.selected), $scope.sorting) } , function (error, response) { if (error) { @@ -503,19 +518,19 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', */ const buildElasticDataQuery = function (type, custom, ageMin, ageMax, intervalBegin, intervalEnd, sortings) { const q = { - 'query': { - 'bool': { - 'must': [ + query: { + bool: { + must: [ { - 'term': { - 'type': type + term: { + type: type } }, { - 'range': { - 'date': { - 'gte': intervalBegin.format(), - 'lte': intervalEnd.format() + range: { + date: { + gte: intervalBegin.format(), + lte: intervalEnd.format() } } } @@ -526,10 +541,10 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', // optional date range if ((typeof ageMin === 'number') && (typeof ageMax === 'number')) { q.query.bool.must.push({ - 'range': { - 'age': { - 'gte': ageMin, - 'lte': ageMax + range: { + age: { + gte: ageMin, + lte: ageMax } } }); @@ -539,7 +554,7 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', const criterion = buildElasticCustomCriterion(custom); if (custom.exclude) { q.query.bool.must_not = [ - { 'term': criterion.match } + { term: criterion.match } ]; } else { q.query.bool.must.push(criterion); @@ -547,24 +562,24 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', } if (sortings) { - q['sort'] = buildElasticSortCriteria(sortings); + q.sort = buildElasticSortCriteria(sortings); } // aggregations (avg age & CA sum) - q['aggs'] = { - 'total_ca': { - 'sum': { - 'field': 'ca' + q.aggs = { + total_ca: { + sum: { + field: 'ca' } }, - 'average_age': { - 'avg': { - 'field': 'age' + average_age: { + avg: { + field: 'age' } }, - 'total_stat': { - 'sum': { - 'field': 'stat' + total_stat: { + sum: { + field: 'stat' } } }; @@ -579,7 +594,7 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', const buildElasticCustomCriterion = function (custom) { if (custom) { const criterion = { - 'match': {} + match: {} }; switch ($scope.getCustomValueInputType($scope.customFilter.criterion)) { case 'input_date': criterion.match[custom.key] = moment(custom.value).format('YYYY-MM-DD'); break; @@ -602,7 +617,7 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', angular.forEach(criteria, function (value, key) { if ((typeof value !== 'undefined') && (value !== null) && (value !== 'none')) { const c = {}; - c[key] = { 'order': value }; + c[key] = { order: value }; return crits.push(c); } }); @@ -624,8 +639,7 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', // if no plans were created, there's no types for statisticIndex=subscriptions if ($scope.type.active) { - $scope.filters.splice(4, 0, { key: 'subType', label: _t('app.admin.statistics.type'), values: $scope.type.active.subtypes }) - + $scope.filters.splice(4, 0, { key: 'subType', label: _t('app.admin.statistics.type'), values: $scope.type.active.subtypes }); if (!$scope.type.active.simple) { const f = { key: 'stat', label: $scope.type.active.label, values: ['input_number'] }; @@ -670,7 +684,7 @@ Application.Controllers.controller('StatisticsController', ['$scope', '$state', ]); -Application.Controllers.controller('ExportStatisticsController', [ '$scope', '$uibModalInstance', 'Export', 'dates', 'query', 'index', 'type', 'CSRF', 'growl', '_t', +Application.Controllers.controller('ExportStatisticsController', ['$scope', '$uibModalInstance', 'Export', 'dates', 'query', 'index', 'type', 'CSRF', 'growl', '_t', function ($scope, $uibModalInstance, Export, dates, query, index, type, CSRF, growl, _t) { // Retrieve Anti-CSRF tokens from cookies CSRF.setMetaTags(); @@ -739,14 +753,14 @@ Application.Controllers.controller('ExportStatisticsController', [ '$scope', '$u if ($scope.export.type === 'global') { $scope.actionUrl = '/stats/global/export'; return $scope.query = JSON.stringify({ - 'query': { - 'bool': { - 'must': [ + query: { + bool: { + must: [ { - 'range': { - 'date': { - 'gte': moment($scope.dates.start).format(), - 'lte': moment($scope.dates.end).format() + range: { + date: { + gte: moment($scope.dates.start).format(), + lte: moment($scope.dates.end).format() } } } @@ -766,8 +780,8 @@ Application.Controllers.controller('ExportStatisticsController', [ '$scope', '$u $scope.exportData = function () { const statusQry = { category: 'statistics', type: $scope.export.type, query: $scope.query }; if ($scope.export.type !== 'global') { - statusQry['type'] = index.key; - statusQry['key'] = type.key; + statusQry.type = index.key; + statusQry.key = type.key; } Export.status(statusQry).then(function (res) { diff --git a/app/frontend/src/javascript/controllers/application.js.erb b/app/frontend/src/javascript/controllers/application.js.erb index c25601eb5..e3dd67d92 100644 --- a/app/frontend/src/javascript/controllers/application.js.erb +++ b/app/frontend/src/javascript/controllers/application.js.erb @@ -92,7 +92,7 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco templateUrl: '/shared/signupModal.html', size: 'md', resolve: { - settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['phone_required', 'recaptcha_site_key', 'confirmation_required']" }).$promise; }] + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['phone_required', 'recaptcha_site_key', 'confirmation_required', 'address_required']" }).$promise; }] }, controller: ['$scope', '$uibModalInstance', 'Group', 'CustomAsset', 'settingsPromise', 'growl', '_t', function ($scope, $uibModalInstance, Group, CustomAsset, settingsPromise, growl, _t) { // default parameters for the date picker in the account creation modal @@ -107,6 +107,9 @@ Application.Controllers.controller('ApplicationController', ['$rootScope', '$sco // is the phone number required to sign-up? $scope.phoneRequired = (settingsPromise.phone_required === 'true'); + // is the address required to sign-up? + $scope.addressRequired = (settingsPromise.address_required === 'true'); + // reCaptcha v2 site key (or undefined) $scope.recaptchaSiteKey = settingsPromise.recaptcha_site_key; diff --git a/app/frontend/src/javascript/controllers/main_nav.js b/app/frontend/src/javascript/controllers/main_nav.js index dfce96ad5..3479d4223 100644 --- a/app/frontend/src/javascript/controllers/main_nav.js +++ b/app/frontend/src/javascript/controllers/main_nav.js @@ -35,12 +35,6 @@ Application.Controllers.controller('MainNavController', ['$scope', function ($sc linkIcon: 'cogs', class: 'reserve-machine-link' }, - { - state: 'app.public.trainings_list', - linkText: 'app.public.common.trainings_registrations', - linkIcon: 'graduation-cap', - class: 'reserve-training-link' - }, { state: 'app.public.events_list', linkText: 'app.public.common.events_registrations', @@ -67,6 +61,15 @@ Application.Controllers.controller('MainNavController', ['$scope', function ($sc }); } + if ($scope.$root.modules.trainings) { + $scope.navLinks.splice(4, 0, { + state: 'app.public.trainings_list', + linkText: 'app.public.common.trainings_registrations', + linkIcon: 'graduation-cap', + class: 'reserve-training-link' + }); + } + if ($scope.$root.modules.spaces) { $scope.navLinks.splice(4, 0, { state: 'app.public.spaces_list', @@ -90,12 +93,6 @@ Application.Controllers.controller('MainNavController', ['$scope', function ($sc linkIcon: 'cogs', authorizedRoles: ['admin', 'manager'] }, - { - state: 'app.admin.trainings', - linkText: 'app.public.common.trainings_monitoring', - linkIcon: 'graduation-cap', - authorizedRoles: ['admin', 'manager'] - }, { state: 'app.admin.events', linkText: 'app.public.common.manage_the_events', @@ -147,6 +144,15 @@ Application.Controllers.controller('MainNavController', ['$scope', function ($sc $scope.adminNavLinks = adminNavLinks; + if ($scope.$root.modules.trainings) { + $scope.adminNavLinks.splice(3, 0, { + state: 'app.admin.trainings', + linkText: 'app.public.common.trainings_monitoring', + linkIcon: 'graduation-cap', + authorizedRoles: ['admin', 'manager'] + }); + } + if ($scope.$root.modules.spaces) { $scope.adminNavLinks.splice(3, 0, { state: 'app.public.spaces_list', diff --git a/app/frontend/src/javascript/controllers/members.js b/app/frontend/src/javascript/controllers/members.js index 2b23102ee..05c233fe0 100644 --- a/app/frontend/src/javascript/controllers/members.js +++ b/app/frontend/src/javascript/controllers/members.js @@ -72,8 +72,8 @@ Application.Controllers.controller('MembersController', ['$scope', 'Member', 'me /** * Controller used when editing the current user's profile (in dashboard) */ -Application.Controllers.controller('EditProfileController', ['$scope', '$rootScope', '$state', '$window', '$sce', '$cookies', '$injector', 'Member', 'Auth', 'Session', 'activeProviderPromise', 'phoneRequiredPromise', 'growl', 'dialogs', 'CSRF', 'memberPromise', 'groups', '_t', - function ($scope, $rootScope, $state, $window, $sce, $cookies, $injector, Member, Auth, Session, activeProviderPromise, phoneRequiredPromise, growl, dialogs, CSRF, memberPromise, groups, _t) { +Application.Controllers.controller('EditProfileController', ['$scope', '$rootScope', '$state', '$window', '$sce', '$cookies', '$injector', 'Member', 'Auth', 'Session', 'activeProviderPromise', 'settingsPromise', 'growl', 'dialogs', 'CSRF', 'memberPromise', 'groups', '_t', + function ($scope, $rootScope, $state, $window, $sce, $cookies, $injector, Member, Auth, Session, activeProviderPromise, settingsPromise, growl, dialogs, CSRF, memberPromise, groups, _t) { /* PUBLIC SCOPE */ // API URL where the form will be posted @@ -111,7 +111,10 @@ Application.Controllers.controller('EditProfileController', ['$scope', '$rootSco $scope.password = { change: false }; // is the phone number required in _member_form? - $scope.phoneRequired = (phoneRequiredPromise.setting.value === 'true'); + $scope.phoneRequired = (settingsPromise.phone_required === 'true'); + + // is the address required in _member_form? + $scope.addressRequired = (settingsPromise.address_required === 'true'); // Angular-Bootstrap datepicker configuration for birthday $scope.datePicker = { diff --git a/app/frontend/src/javascript/controllers/profile.js b/app/frontend/src/javascript/controllers/profile.js index e7fcf4788..5056c8a1a 100644 --- a/app/frontend/src/javascript/controllers/profile.js +++ b/app/frontend/src/javascript/controllers/profile.js @@ -13,8 +13,8 @@ 'use strict'; -Application.Controllers.controller('CompleteProfileController', ['$scope', '$rootScope', '$state', '$window', '_t', 'growl', 'CSRF', 'Auth', 'Member', 'settingsPromise', 'activeProviderPromise', 'groupsPromise', 'cguFile', 'memberPromise', 'Session', 'dialogs', 'AuthProvider', 'phoneRequiredPromise', - function ($scope, $rootScope, $state, $window, _t, growl, CSRF, Auth, Member, settingsPromise, activeProviderPromise, groupsPromise, cguFile, memberPromise, Session, dialogs, AuthProvider, phoneRequiredPromise) { +Application.Controllers.controller('CompleteProfileController', ['$scope', '$rootScope', '$state', '$window', '_t', 'growl', 'CSRF', 'Auth', 'Member', 'settingsPromise', 'activeProviderPromise', 'groupsPromise', 'cguFile', 'memberPromise', 'Session', 'dialogs', 'AuthProvider', + function ($scope, $rootScope, $state, $window, _t, growl, CSRF, Auth, Member, settingsPromise, activeProviderPromise, groupsPromise, cguFile, memberPromise, Session, dialogs, AuthProvider) { /* PUBLIC SCOPE */ // API URL where the form will be posted @@ -48,7 +48,10 @@ Application.Controllers.controller('CompleteProfileController', ['$scope', '$roo $scope.cgu = cguFile.custom_asset; // is the phone number required in _member_form? - $scope.phoneRequired = (phoneRequiredPromise.setting.value === 'true'); + $scope.phoneRequired = (settingsPromise.phone_required === 'true'); + + // is the address required in _member_form? + $scope.addressRequired = (settingsPromise.address_required === 'true'); // Angular-Bootstrap datepicker configuration for birthday $scope.datePicker = { @@ -200,6 +203,14 @@ Application.Controllers.controller('CompleteProfileController', ['$scope', '$roo }); }; + /** + * Hide the new account messages. + * If hidden, the page will be used only to complete the current user's profile. + */ + $scope.hideNewAccountConfirmation = function () { + return !$scope.activeProvider.previous_provider || $scope.activeProvider.previous_provider.id === $scope.activeProvider.id; + }; + /* PRIVATE SCOPE */ /** diff --git a/app/frontend/src/javascript/models/setting.ts b/app/frontend/src/javascript/models/setting.ts index b42653b85..ccde19dc3 100644 --- a/app/frontend/src/javascript/models/setting.ts +++ b/app/frontend/src/javascript/models/setting.ts @@ -98,7 +98,10 @@ export enum SettingName { ConfirmationRequired = 'confirmation_required', WalletModule = 'wallet_module', StatisticsModule = 'statistics_module', - UpcomingEventsShown = 'upcoming_events_shown' + UpcomingEventsShown = 'upcoming_events_shown', + PaymentSchedulePrefix = 'payment_schedule_prefix', + TrainingsModule = 'trainings_module', + AddressRequired = 'address_required' } export interface Setting { diff --git a/app/frontend/src/javascript/router.js b/app/frontend/src/javascript/router.js index c6dfb5702..a0e85c3a1 100644 --- a/app/frontend/src/javascript/router.js +++ b/app/frontend/src/javascript/router.js @@ -37,7 +37,7 @@ angular.module('application.router', ['ui.router']) logoFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'logo-file' }).$promise; }], logoBlackFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'logo-black-file' }).$promise; }], sharedTranslations: ['Translations', function (Translations) { return Translations.query(['app.shared', 'app.public.common']).$promise; }], - modulesPromise: ['Setting', function (Setting) { return Setting.query({ names: "['spaces_module', 'plans_module', 'invoicing_module', 'wallet_module', 'statistics_module']" }).$promise; }] + modulesPromise: ['Setting', function (Setting) { return Setting.query({ names: "['spaces_module', 'plans_module', 'invoicing_module', 'wallet_module', 'statistics_module', 'trainings_module']" }).$promise; }] }, onEnter: ['$rootScope', 'logoFile', 'logoBlackFile', 'modulesPromise', 'CSRF', function ($rootScope, logoFile, logoBlackFile, modulesPromise, CSRF) { // Retrieve Anti-CSRF tokens from cookies @@ -48,6 +48,7 @@ angular.module('application.router', ['ui.router']) $rootScope.modules = { spaces: (modulesPromise.spaces_module === 'true'), plans: (modulesPromise.plans_module === 'true'), + trainings: (modulesPromise.trainings_module === 'true'), invoicing: (modulesPromise.invoicing_module === 'true'), wallet: (modulesPromise.wallet_module === 'true'), statistics: (modulesPromise.statistics_module === 'true') @@ -129,12 +130,11 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['fablab_name', 'name_genre']" }).$promise; }], + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['fablab_name', 'name_genre', 'phone_required', 'address_required']" }).$promise; }], activeProviderPromise: ['AuthProvider', function (AuthProvider) { return AuthProvider.active().$promise; }], groupsPromise: ['Group', function (Group) { return Group.query().$promise; }], cguFile: ['CustomAsset', function (CustomAsset) { return CustomAsset.get({ name: 'cgu-file' }).$promise; }], - memberPromise: ['Member', 'currentUser', function (Member, currentUser) { return Member.get({ id: currentUser.id }).$promise; }], - phoneRequiredPromise: ['Setting', function (Setting) { return Setting.get({ name: 'phone_required' }).$promise; }] + memberPromise: ['Member', 'currentUser', function (Member, currentUser) { return Member.get({ id: currentUser.id }).$promise; }] } }) @@ -167,7 +167,7 @@ angular.module('application.router', ['ui.router']) resolve: { groups: ['Group', function (Group) { return Group.query().$promise; }], activeProviderPromise: ['AuthProvider', function (AuthProvider) { return AuthProvider.active().$promise; }], - phoneRequiredPromise: ['Setting', function (Setting) { return Setting.get({ name: 'phone_required' }).$promise; }] + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['phone_required', 'address_required']" }).$promise; }] } }) .state('app.logged.dashboard.projects', { @@ -458,6 +458,7 @@ angular.module('application.router', ['ui.router']) // trainings .state('app.public.trainings_list', { url: '/trainings', + abstract: !Fablab.trainingsModule, views: { 'main@': { templateUrl: '/trainings/index.html', @@ -470,6 +471,7 @@ angular.module('application.router', ['ui.router']) }) .state('app.public.training_show', { url: '/trainings/:id', + abstract: !Fablab.trainingsModule, views: { 'main@': { templateUrl: '/trainings/show.html', @@ -482,6 +484,7 @@ angular.module('application.router', ['ui.router']) }) .state('app.logged.trainings_reserve', { url: '/trainings/:id/reserve', + abstract: !Fablab.trainingsModule, views: { 'main@': { templateUrl: '/trainings/reserve.html', @@ -652,6 +655,7 @@ angular.module('application.router', ['ui.router']) // trainings .state('app.admin.trainings', { url: '/admin/trainings', + abstract: !Fablab.trainingsModule, views: { 'main@': { templateUrl: '/admin/trainings/index.html', @@ -666,6 +670,7 @@ angular.module('application.router', ['ui.router']) }) .state('app.admin.trainings_new', { url: '/admin/trainings/new', + abstract: !Fablab.trainingsModule, views: { 'main@': { templateUrl: '/admin/trainings/new.html', @@ -678,6 +683,7 @@ angular.module('application.router', ['ui.router']) }) .state('app.admin.trainings_edit', { url: '/admin/trainings/:id/edit', + abstract: !Fablab.trainingsModule, views: { 'main@': { templateUrl: '/admin/trainings/edit.html', @@ -909,7 +915,7 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - phoneRequiredPromise: ['Setting', function (Setting) { return Setting.get({ name: 'phone_required' }).$promise; }] + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['phone_required', 'address_required']" }).$promise; }] } }) .state('app.admin.members_import', { @@ -950,7 +956,7 @@ angular.module('application.router', ['ui.router']) walletPromise: ['Wallet', '$stateParams', function (Wallet, $stateParams) { return Wallet.getWalletByUser({ user_id: $stateParams.id }).$promise; }], transactionsPromise: ['Wallet', 'walletPromise', function (Wallet, walletPromise) { return Wallet.transactions({ id: walletPromise.id }).$promise; }], tagsPromise: ['Tag', function (Tag) { return Tag.query().$promise; }], - phoneRequiredPromise: ['Setting', function (Setting) { return Setting.get({ name: 'phone_required' }).$promise; }] + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['phone_required', 'address_required']" }).$promise; }] } }) .state('app.admin.admins_new', { @@ -962,7 +968,7 @@ angular.module('application.router', ['ui.router']) } }, resolve: { - phoneRequiredPromise: ['Setting', function (Setting) { return Setting.get({ name: 'phone_required' }).$promise; }] + settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['phone_required', 'address_required']" }).$promise; }] } }) .state('app.admin.managers_new', { @@ -1054,8 +1060,8 @@ angular.module('application.router', ['ui.router']) "'booking_move_enable', 'booking_move_delay', 'booking_cancel_enable', 'feature_tour_display', " + "'booking_cancel_delay', 'main_color', 'secondary_color', 'spaces_module', 'twitter_analytics', " + "'fablab_name', 'name_genre', 'reminder_enable', 'plans_module', 'confirmation_required', " + - "'reminder_delay', 'visibility_yearly', 'visibility_others', 'wallet_module', " + - "'display_name_enable', 'machines_sort_by', 'fab_analytics', 'statistics_module', " + + "'reminder_delay', 'visibility_yearly', 'visibility_others', 'wallet_module', 'trainings_module', " + + "'display_name_enable', 'machines_sort_by', 'fab_analytics', 'statistics_module', 'address_required', " + "'link_name', 'home_content', 'home_css', 'phone_required', 'upcoming_events_shown']" }).$promise; }], diff --git a/app/frontend/src/stylesheets/app.components.scss b/app/frontend/src/stylesheets/app.components.scss index 3d52a94c2..04d1e6691 100644 --- a/app/frontend/src/stylesheets/app.components.scss +++ b/app/frontend/src/stylesheets/app.components.scss @@ -267,125 +267,31 @@ } } -.pricing-panel { - border: 1px solid $border-color; - height: 391px; - - &:first-child { - border-right: none; - - @include border-radius(3px 0 0 3px); +.list-of-plans { + .group-title { + width: 83.33%; + border-bottom: 1px solid; + padding-bottom: 2em; + margin: auto auto 1em; } - &:last-child { - @include border-radius(0 3px 3px 0); - } - - .plan-card { - height: 100%; - display: flex; - flex-direction: column; - justify-content: flex-start; - } - - .title { - margin: 10px 0; - font-size: rem-calc(16); - text-transform: uppercase; - color: black; - } - - .content { - padding: 15px 0; - background-color: $bg-gray; - - .wrap, .wrap-monthly { - width: 130px; - height: 130px; - display: inline-block; - background: white; - - @include border-radius(50%, 50%, 50%, 50%); - - border: 3px solid; - - .price { - width: 114px; - display: flex; - flex-direction: column; - justify-content: center; - - @include border-radius(50%, 50%, 50%, 50%); - } + .plans-per-group { + & { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + border: 1px solid transparent; } - .wrap-monthly { - & > .price { - & > .amount { - padding-top: 4px; - line-height: 1.2em; - } - - & > .period { - padding-top: 4px; - } - } - } - .price { - position: relative; - top: 5px; - left: 5px; - height: 114px; - background-color: black; - - .amount { - padding-left: 4px; - padding-right: 4px; - font-weight: bold; - font-size: rem-calc(17); - color: white; - } - - .period { - position: relative; - top: -6px; - font-size: rem-calc(14); - color: white; - } - } - } - - .card-footer { - display: flex; - flex-direction: column; - justify-content: space-around; - height: 100%; - - .plan-description { - max-height: 5.2em; - overflow: hidden; + & > * { + width: 50%; } - .cta-button { - margin: 20px 0; - - .subscribe-button { - @extend .btn; - @extend .rounded; - - outline: 0; - font-weight: 600; - font-size: rem-calc(16); - background-color: white; - padding-left: 30px; - padding-right: 30px; + @media screen and (max-width: 992px) { + & > * { + width: 100%; } - button.subscribe-button:focus, button.subscribe-button:hover { - outline: 0; - } - } - .info-link { - margin-top: 1em; } } } diff --git a/app/frontend/src/stylesheets/application.scss b/app/frontend/src/stylesheets/application.scss index fac4aacf6..e07995746 100644 --- a/app/frontend/src/stylesheets/application.scss +++ b/app/frontend/src/stylesheets/application.scss @@ -32,5 +32,6 @@ @import "modules/payment-schedules-list"; @import "modules/stripe-confirm"; @import "modules/payment-schedule-dashboard"; +@import "modules/plan-card"; @import "app.responsive"; diff --git a/app/frontend/src/stylesheets/modules/plan-card.scss b/app/frontend/src/stylesheets/modules/plan-card.scss new file mode 100644 index 000000000..71c4fcb0b --- /dev/null +++ b/app/frontend/src/stylesheets/modules/plan-card.scss @@ -0,0 +1,102 @@ +.plan-card { + display: block; + height: 100%; + width: 100%; + + .title { + margin: 10px 0; + font-size: 1.6rem; + text-transform: uppercase; + } + + .content { + & { + padding: 15px 0; + background-color: #f5f5f5; + } + + .wrap, .wrap-monthly { + width: 130px; + height: 130px; + display: inline-block; + background: white; + + @include border-radius(50%, 50%, 50%, 50%); + + border: 3px solid; + + .price { + width: 114px; + display: flex; + flex-direction: column; + justify-content: center; + + @include border-radius(50%, 50%, 50%, 50%); + } + } + + .wrap-monthly { + & > .price { + & > .amount { + padding-top: 4px; + line-height: 1.2em; + } + + & > .period { + padding-top: 4px; + } + } + } + .price { + position: relative; + top: 5px; + left: 5px; + height: 114px; + background-color: black; + + .amount { + padding-left: 4px; + padding-right: 4px; + font-weight: bold; + font-size: rem-calc(17); + color: white; + } + + .period { + position: relative; + top: -6px; + font-size: rem-calc(14); + color: white; + } + } + } + + .card-footer { + .plan-description { + + & p:nth-child(2n+3), p:nth-child(2n+4) { + display: none; + } + } + .cta-button { + margin: 20px 0; + + .subscribe-button { + @extend .btn; + @extend .rounded; + outline: 0; + font-weight: 600; + font-size: rem-calc(16); + background-color: white; + padding-left: 30px; + padding-right: 30px; + } + button.subscribe-button:focus, button.subscribe-button:hover { + outline: 0; + } + } + .info-link { + margin-top: 1em; + } + } +} diff --git a/app/frontend/templates/admin/calendar/eventModal.html b/app/frontend/templates/admin/calendar/eventModal.html index a5bfc133b..8ed8f6184 100644 --- a/app/frontend/templates/admin/calendar/eventModal.html +++ b/app/frontend/templates/admin/calendar/eventModal.html @@ -6,7 +6,7 @@