mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-19 13:54:25 +01:00
display list of plans grouped by category
This commit is contained in:
parent
b83a9e44d6
commit
05882b3743
@ -3,12 +3,10 @@
|
||||
# API Controller for resources of type PlanCategory
|
||||
# PlanCategory are used to sort plans
|
||||
class API::PlanCategoriesController < API::ApiController
|
||||
before_action :authenticate_user!
|
||||
before_action :authenticate_user!, except: :index
|
||||
before_action :set_category, only: %i[show update destroy]
|
||||
|
||||
def index
|
||||
authorize PlanCategory
|
||||
|
||||
@categories = PlanCategory.order(weight: :desc)
|
||||
end
|
||||
|
||||
|
11
app/frontend/src/javascript/api/group.ts
Normal file
11
app/frontend/src/javascript/api/group.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import apiClient from './clients/api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { Group } from '../models/group';
|
||||
|
||||
export default class GroupAPI {
|
||||
static async index (): Promise<Array<Group>> {
|
||||
const res: AxiosResponse<Array<Group>> = await apiClient.get('/api/groups');
|
||||
return res?.data;
|
||||
}
|
||||
}
|
||||
|
11
app/frontend/src/javascript/api/plan.ts
Normal file
11
app/frontend/src/javascript/api/plan.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import apiClient from './clients/api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { Plan } from '../models/plan';
|
||||
|
||||
export default class PlanAPI {
|
||||
static async index (): Promise<Array<Plan>> {
|
||||
const res: AxiosResponse<Array<Plan>> = await apiClient.get('/api/plans');
|
||||
return res?.data;
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,13 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { react2angular } from 'react2angular';
|
||||
import moment from 'moment';
|
||||
import _ from 'lodash'
|
||||
import { IApplication } from '../models/application';
|
||||
import { Plan } from '../models/plan';
|
||||
import { User, UserRole } from '../models/user';
|
||||
import { Loader } from './base/loader';
|
||||
import '../lib/i18n';
|
||||
import { IFablab } from '../models/fablab';
|
||||
import { IFablab } from '../../models/fablab';
|
||||
import { Plan } from '../../models/plan';
|
||||
import { User, UserRole } from '../../models/user';
|
||||
import { Loader } from '../base/loader';
|
||||
import '../../lib/i18n';
|
||||
|
||||
declare var Application: IApplication;
|
||||
declare var Fablab: IFablab;
|
||||
|
||||
interface PlanCardProps {
|
||||
@ -26,7 +23,7 @@ interface PlanCardProps {
|
||||
/**
|
||||
* This component is a "card" (visually), publicly presenting the details of a plan and allowing a user to subscribe.
|
||||
*/
|
||||
const PlanCard: React.FC<PlanCardProps> = ({ plan, userId, subscribedPlanId, operator, onSelectPlan, isSelected, onLoginRequested }) => {
|
||||
const PlanCardComponent: React.FC<PlanCardProps> = ({ plan, userId, subscribedPlanId, operator, onSelectPlan, isSelected, onLoginRequested }) => {
|
||||
const { t } = useTranslation('public');
|
||||
/**
|
||||
* Return the formatted localized amount of the given plan (eg. 20.5 => "20,50 €")
|
||||
@ -146,12 +143,10 @@ const PlanCard: React.FC<PlanCardProps> = ({ plan, userId, subscribedPlanId, ope
|
||||
);
|
||||
}
|
||||
|
||||
const PlanCardWrapper: React.FC<PlanCardProps> = ({ plan, userId, subscribedPlanId, operator, onSelectPlan, isSelected, onLoginRequested }) => {
|
||||
export const PlanCard: React.FC<PlanCardProps> = ({ plan, userId, subscribedPlanId, operator, onSelectPlan, isSelected, onLoginRequested }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<PlanCard plan={plan} userId={userId} subscribedPlanId={subscribedPlanId} operator={operator} isSelected={isSelected} onSelectPlan={onSelectPlan} onLoginRequested={onLoginRequested}/>
|
||||
<PlanCardComponent plan={plan} userId={userId} subscribedPlanId={subscribedPlanId} operator={operator} isSelected={isSelected} onSelectPlan={onSelectPlan} onLoginRequested={onLoginRequested}/>
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
Application.Components.component('planCard', react2angular(PlanCardWrapper, ['plan', 'userId', 'subscribedPlanId', 'operator', 'onSelectPlan', 'isSelected', 'onLoginRequested']));
|
213
app/frontend/src/javascript/components/plans/plans-list.tsx
Normal file
213
app/frontend/src/javascript/components/plans/plans-list.tsx
Normal file
@ -0,0 +1,213 @@
|
||||
import React, { ReactNode, useEffect, useState } from 'react';
|
||||
import _ from 'lodash';
|
||||
import PlanAPI from '../../api/plan';
|
||||
import { Plan } from '../../models/plan';
|
||||
import { PlanCategory } from '../../models/plan-category';
|
||||
import PlanCategoryAPI from '../../api/plan-category';
|
||||
import { User } from '../../models/user';
|
||||
import { Group } from '../../models/group';
|
||||
import GroupAPI from '../../api/group';
|
||||
import { PlanCard } from './plan-card';
|
||||
import { Loader } from '../base/loader';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { IApplication } from '../../models/application';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
declare var Application: IApplication;
|
||||
|
||||
interface PlansListProps {
|
||||
onError: (message: string) => void,
|
||||
onPlanSelection: (plan: Plan) => void,
|
||||
onLoginRequest: () => void,
|
||||
operator?: User,
|
||||
customer?: User,
|
||||
}
|
||||
|
||||
// A list of plans, organized by group ID - then organized by plan-category ID (or NaN if the plan has no category)
|
||||
type PlansTree = Map<number, Map<number, Array<Plan>>>;
|
||||
|
||||
/**
|
||||
* This component display an organized list of plans to allow the end-user to select one and subscribe online
|
||||
*/
|
||||
const PlansList: React.FC<PlansListProps> = ({ onError, onPlanSelection, onLoginRequest, operator, customer }) => {
|
||||
const { t } = useTranslation('public');
|
||||
|
||||
// all plans
|
||||
const [plans, setPlans] = useState<PlansTree>(null);
|
||||
// all plan-categories, ordered by weight
|
||||
const [planCategories, setPlanCategories] = useState<Array<PlanCategory>>(null);
|
||||
// all groups
|
||||
const [groups, setGroups] = useState<Array<Group>>(null);
|
||||
// currently selected plan
|
||||
const [selectedPlan, setSelectedPlan] = useState<Plan>(null);
|
||||
|
||||
// fetch data on component mounted
|
||||
useEffect(() => {
|
||||
PlanCategoryAPI.index()
|
||||
.then(data => setPlanCategories(data))
|
||||
.catch(error => onError(error));
|
||||
GroupAPI.index()
|
||||
.then(groupsData => {
|
||||
setGroups(groupsData);
|
||||
PlanAPI.index()
|
||||
.then(data => setPlans(sortPlans(data, groupsData)))
|
||||
.catch(error => onError(error));
|
||||
})
|
||||
.catch(error => onError(error))
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Group a flat array of plans and return a collection of the same plans, grouped by the given property
|
||||
*/
|
||||
const groupBy = (plans: Array<Plan>, criteria: string): Map<number, Array<Plan>> => {
|
||||
const grouped = _.groupBy(plans, criteria);
|
||||
|
||||
const map = new Map<number, Array<Plan>>();
|
||||
for (const criteriaId in grouped) {
|
||||
if (Object.prototype.hasOwnProperty.call(grouped, criteriaId)) {
|
||||
const enabled = grouped[criteriaId].filter(plan => !plan.disabled);
|
||||
// null ids will be converted to NaN
|
||||
map.set(Number(criteriaId), enabled);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sort the plans, by group and by category and return the corresponding map
|
||||
*/
|
||||
const sortPlans = (plans: Array<Plan>, groups: Array<Group>): PlansTree => {
|
||||
const byGroup = groupBy(plans, 'group_id');
|
||||
|
||||
const res = new Map<number, Map<number, Array<Plan>>>();
|
||||
for (const [groupId, plansByGroup] of byGroup) {
|
||||
const group = groups.find(g => g.id === groupId);
|
||||
if (!group.disabled) {
|
||||
res.set(groupId, groupBy(plansByGroup, 'plan_category_id'));
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the plans to display, depending on the connected/selected user
|
||||
*/
|
||||
const filteredPlans = (): PlansTree => {
|
||||
if (!customer) return plans;
|
||||
|
||||
return new Map([[customer.group_id, plans.get(customer.group_id)]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* When called with a group ID, returns the name of the requested group
|
||||
*/
|
||||
const groupName = (groupId: number): string => {
|
||||
return groups.find(g => g.id === groupId)?.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* When called with a category ID, returns the name of the requested plan-category
|
||||
*/
|
||||
const categoryName = (categoryId: number): string => {
|
||||
return planCategories.find(c => c.id === categoryId)?.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the currently selected plan matched the provided one
|
||||
*/
|
||||
const isSelectedPlan = (plan: Plan): boolean => {
|
||||
return (plan === selectedPlan);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for sorting plans by weight
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
|
||||
*/
|
||||
const comparePlans = (plan1: Plan, plan2: Plan): number => {
|
||||
return (plan2.ui_weight - plan1.ui_weight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for sorting categories by weight
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
|
||||
*/
|
||||
const compareCategories = (category1: [number, Array<Plan>], category2: [number, Array<Plan>]): number => {
|
||||
if (isNaN(category1[0])) return -1;
|
||||
if (isNaN(category2[0])) return 1;
|
||||
|
||||
const categoryObject1 = planCategories.find(c => c.id === category1[0]);
|
||||
const categoryObject2 = planCategories.find(c => c.id === category2[0]);
|
||||
return (categoryObject2.weight - categoryObject1.weight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback triggered when the user chooses a plan to subscribe
|
||||
*/
|
||||
const handlePlanSelection = (plan: Plan): void => {
|
||||
setSelectedPlan(plan);
|
||||
onPlanSelection(plan);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the provided list of categories, with each associated plans
|
||||
*/
|
||||
const renderPlansByCategory = (plans: Map<number, Array<Plan>>): ReactNode => {
|
||||
return (
|
||||
<div className="list-of-categories">
|
||||
{Array.from(plans).sort(compareCategories).map(([categoryId, plansByCategory]) => {
|
||||
return (
|
||||
<div key={categoryId} className={`plans-per-category ${categoryId ? 'with-category' : 'no-category' }`}>
|
||||
{!!categoryId && <h3 className="category-title">{ categoryName(categoryId) }</h3>}
|
||||
{renderPlans(plansByCategory)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the provided list of plans, ordered by ui_weight.
|
||||
*/
|
||||
const renderPlans = (categoryPlans: Array<Plan>): ReactNode => {
|
||||
return (
|
||||
<div className="list-of-plans">
|
||||
{categoryPlans.length === 0 && <span className="no-plans">
|
||||
{t('app.public.plans.no_plans')}
|
||||
</span>}
|
||||
{categoryPlans.sort(comparePlans).map(plan => (
|
||||
<PlanCard key={plan.id}
|
||||
plan={plan}
|
||||
operator={operator}
|
||||
isSelected={isSelectedPlan(plan)}
|
||||
onSelectPlan={handlePlanSelection}
|
||||
onLoginRequested={onLoginRequest} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="plans-list">
|
||||
{plans && Array.from(filteredPlans()).map(([groupId, plansByGroup]) => {
|
||||
return (
|
||||
<div key={groupId} className="plans-per-group">
|
||||
<h2 className="group-title">{ groupName(groupId) }</h2>
|
||||
{renderPlansByCategory(plansByGroup)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const PlansListWrapper: React.FC<PlansListProps> = ({ customer, onError, onPlanSelection, onLoginRequest, operator }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<PlansList customer={customer} onError={onError} onPlanSelection={onPlanSelection} onLoginRequest={onLoginRequest} operator={operator} />
|
||||
</Loader>
|
||||
);
|
||||
}
|
||||
|
||||
Application.Components.component('plansList', react2angular(PlansListWrapper, ['customer', 'onError', 'onPlanSelection', 'onLoginRequest', 'operator']));
|
@ -12,8 +12,8 @@
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScope', '$state', '$uibModal', 'Auth', 'AuthService', 'dialogs', 'growl', 'plansPromise', 'groupsPromise', 'Subscription', 'Member', 'subscriptionExplicationsPromise', '_t', 'Wallet', 'helpers', 'settingsPromise', 'Price',
|
||||
function ($scope, $rootScope, $state, $uibModal, Auth, AuthService, dialogs, growl, plansPromise, groupsPromise, Subscription, Member, subscriptionExplicationsPromise, _t, Wallet, helpers, settingsPromise, Price) {
|
||||
Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScope', '$state', '$uibModal', 'Auth', 'AuthService', 'dialogs', 'growl', 'groupsPromise', 'Subscription', 'Member', 'subscriptionExplicationsPromise', '_t', 'Wallet', 'helpers', 'settingsPromise', 'Price',
|
||||
function ($scope, $rootScope, $state, $uibModal, Auth, AuthService, dialogs, growl, groupsPromise, Subscription, Member, subscriptionExplicationsPromise, _t, Wallet, helpers, settingsPromise, Price) {
|
||||
/* PUBLIC SCOPE */
|
||||
|
||||
// list of groups
|
||||
@ -26,9 +26,6 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
|
||||
id: null
|
||||
};
|
||||
|
||||
// list of plans, classified by group
|
||||
$scope.plansClassifiedByGroup = [];
|
||||
|
||||
// user to deal with
|
||||
$scope.ctrl = {
|
||||
member: null,
|
||||
@ -93,22 +90,19 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
|
||||
* Open the modal dialog allowing the user to log into the system
|
||||
*/
|
||||
$scope.userLogin = function () {
|
||||
console.log('userLogin');
|
||||
setTimeout(() => {
|
||||
console.log('going throught timeout');
|
||||
if (!$scope.isAuthenticated()) {
|
||||
console.log('! authenticated');
|
||||
$scope.login();
|
||||
}
|
||||
}, 50);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the provided plan is currently selected
|
||||
* @param plan {Object} Resource plan
|
||||
* Callback triggered when an error is raised on a lower-level component
|
||||
* @param message {string}
|
||||
*/
|
||||
$scope.isSelected = function (plan) {
|
||||
return $scope.selectedPlan === plan;
|
||||
$scope.onError = function (message) {
|
||||
growl.error(message);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -195,18 +189,6 @@ Application.Controllers.controller('PlansIndexController', ['$scope', '$rootScop
|
||||
* Kind of constructor: these actions will be realized first when the controller is loaded
|
||||
*/
|
||||
const initialize = function () {
|
||||
// group all plans by Group
|
||||
for (const group of $scope.groups) {
|
||||
const groupObj = { id: group.id, name: group.name, plans: [], actives: 0 };
|
||||
for (const plan of plansPromise) {
|
||||
if (plan.group_id === group.id) {
|
||||
groupObj.plans.push(plan);
|
||||
if (!plan.disabled) { groupObj.actives++; }
|
||||
}
|
||||
}
|
||||
$scope.plansClassifiedByGroup.push(groupObj);
|
||||
}
|
||||
|
||||
if ($scope.currentUser) {
|
||||
if (!AuthService.isAuthorized('admin')) {
|
||||
$scope.ctrl.member = $scope.currentUser;
|
||||
|
7
app/frontend/src/javascript/models/group.ts
Normal file
7
app/frontend/src/javascript/models/group.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface Group {
|
||||
id: number,
|
||||
slug: string,
|
||||
name: string,
|
||||
disabled: boolean,
|
||||
users: number
|
||||
}
|
@ -24,6 +24,7 @@ export interface Plan {
|
||||
interval: Interval,
|
||||
interval_count: number,
|
||||
group_id: number,
|
||||
plan_category_id: number,
|
||||
training_credit_nb: number,
|
||||
is_rolling: boolean,
|
||||
description: string,
|
||||
|
@ -532,7 +532,6 @@ angular.module('application.router', ['ui.router'])
|
||||
},
|
||||
resolve: {
|
||||
subscriptionExplicationsPromise: ['Setting', function (Setting) { return Setting.get({ name: 'subscription_explications_alert' }).$promise; }],
|
||||
plansPromise: ['Plan', function (Plan) { return Plan.query().$promise; }],
|
||||
groupsPromise: ['Group', function (Group) { return Group.query().$promise; }],
|
||||
settingsPromise: ['Setting', function (Setting) { return Setting.query({ names: "['online_payment_module', 'payment_gateway']" }).$promise; }]
|
||||
}
|
||||
|
@ -267,56 +267,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.list-of-plans {
|
||||
.active-group ~ .active-group .group-title {
|
||||
/* select all active groups but the first (the first have no margin at the top) */
|
||||
margin: 3em auto 1em;
|
||||
}
|
||||
.group-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
padding: 2em;
|
||||
width: 83.33%;
|
||||
box-sizing: border-box;
|
||||
margin: 0 auto 1em;
|
||||
$border: 5px;
|
||||
background: #FFF;
|
||||
background-clip: padding-box;
|
||||
border: solid $border transparent;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; right: 0; bottom: 0; left: 0;
|
||||
z-index: -1;
|
||||
margin: -$border;
|
||||
border-radius: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.plans-per-group {
|
||||
& {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
& > * {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
& > * {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.well {
|
||||
&.well-warning {
|
||||
@include border-radius(3px, 3px, 3px, 3px);
|
||||
|
@ -34,6 +34,7 @@
|
||||
@import "modules/payment-schedules-list";
|
||||
@import "modules/stripe-confirm";
|
||||
@import "modules/payment-schedule-dashboard";
|
||||
@import "modules/plans-list";
|
||||
@import "modules/plan-card";
|
||||
@import "modules/event-themes";
|
||||
@import "modules/select-gateway-modal";
|
||||
|
60
app/frontend/src/stylesheets/modules/plans-list.scss
Normal file
60
app/frontend/src/stylesheets/modules/plans-list.scss
Normal file
@ -0,0 +1,60 @@
|
||||
.plans-list {
|
||||
.plans-per-group ~ .plans-per-group .group-title {
|
||||
/* select all groups but the first (the first have no margin at the top) */
|
||||
margin: 5em auto 1em;
|
||||
}
|
||||
.group-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
padding: 2em;
|
||||
width: 83.33%;
|
||||
box-sizing: border-box;
|
||||
margin: 1em auto 1em;
|
||||
$border: 5px;
|
||||
background: #FFF;
|
||||
background-clip: padding-box;
|
||||
border: solid $border transparent;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; right: 0; bottom: 0; left: 0;
|
||||
z-index: -1;
|
||||
margin: -$border;
|
||||
border-radius: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.plans-per-category {
|
||||
text-align: center;
|
||||
margin-top: 3em;
|
||||
|
||||
.category-title {
|
||||
width: 50%;
|
||||
margin: 2em auto;
|
||||
line-height: 3em;
|
||||
}
|
||||
}
|
||||
|
||||
.list-of-plans {
|
||||
& {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
& > * {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
& > * {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -49,7 +49,7 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{'has-error': planForm['plan[plan_category_id]'].$dirty && planForm['plan[group_id]'].$invalid}">
|
||||
<label for="plan[plan_category_id]">{{ 'app.shared.plan.category' | translate }} *</label>
|
||||
<label for="plan[plan_category_id]">{{ 'app.shared.plan.category' | translate }}</label>
|
||||
<select id="plan[plan_category_id]"
|
||||
class="form-control"
|
||||
ng-model="plan.plan_category_id"
|
||||
|
@ -17,38 +17,18 @@
|
||||
<div class="row no-gutter list-of-plans">
|
||||
<div class="col-sm-12 col-md-9 b-r">
|
||||
|
||||
<div class="row m-t m-b padder" ng-repeat="plansGroup in plansClassifiedByGroup | groupFilter:ctrl.member" ng-class="{'active-group': plansGroup.actives > 0}">
|
||||
<div ng-show="plansGroup.actives > 0">
|
||||
<h2 class="text-u-c group-title">{{plansGroup.name}}</h2>
|
||||
<div class="row row-centered padder">
|
||||
<div class="plans-per-group">
|
||||
|
||||
<plan-card ng-repeat="(key, plan) in plansGroup.plans.filter(filterDisabledPlans) | orderBy: '-ui_weight'"
|
||||
plan="plan"
|
||||
user-id="ctrl.member.id"
|
||||
subscribed-plan-id="ctrl.member.subscribed_plan.id"
|
||||
operator="currentUser"
|
||||
on-select-plan="selectPlan"
|
||||
on-login-requested="userLogin"
|
||||
is-selected="isSelected(plan)">
|
||||
</plan-card>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-xs-12 col-md-12 col-lg-10 col-centered no-gutter" ng-if="ctrl.member.subscription && isInPast(ctrl.member.subscription.expired_at)">
|
||||
<uib-alert type="info">
|
||||
{{ 'app.public.plans.your_subscription_expires_on_the_DATE' | translate:{DATE:(ctrl.member.subscription.expired_at | amDateFormat:'L' )} }}
|
||||
</uib-alert>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="plansGroup.actives === 0 && ctrl.member" class="m-lg" translate>
|
||||
{{ 'app.public.plans.no_plans' }}
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 col-md-12 col-lg-10 col-centered no-gutter" ng-if="ctrl.member.subscription && isInPast(ctrl.member.subscription.expired_at)">
|
||||
<uib-alert type="info">
|
||||
{{ 'app.public.plans.your_subscription_expires_on_the_DATE' | translate:{DATE:(ctrl.member.subscription.expired_at | amDateFormat:'L' )} }}
|
||||
</uib-alert>
|
||||
</div>
|
||||
|
||||
<plans-list customer="ctrl.member"
|
||||
on-error="onError"
|
||||
on-plan-selection="selectPlan"
|
||||
on-login-request="userLogin"
|
||||
operator="currentUser" />
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-sm-12 col-md-12 col-lg-3">
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
# Check the access policies for API::PlanCategoriesController
|
||||
class PlanCategoryPolicy < ApplicationPolicy
|
||||
%w[index show create update destroy].each do |action|
|
||||
%w[show create update destroy].each do |action|
|
||||
define_method "#{action}?" do
|
||||
user.admin?
|
||||
end
|
||||
|
@ -306,10 +306,14 @@ section#cookies-modal div.cookies-consent .cookies-actions button.accept {
|
||||
color: $secondary-text-color;
|
||||
}
|
||||
|
||||
.list-of-plans {
|
||||
.plans-list {
|
||||
.group-title:before {
|
||||
background: linear-gradient(to left, white, $primary, white);
|
||||
}
|
||||
.category-title {
|
||||
background-color: $primary;
|
||||
color: $primary-text-color !important;
|
||||
}
|
||||
}
|
||||
|
||||
.plan-card {
|
||||
|
@ -1,2 +1,4 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.extract! group, :id, :slug, :name, :disabled
|
||||
json.users group.users.count
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
json.array!(@plans) do |plan|
|
||||
json.extract! plan, :id, :base_name, :name, :interval, :interval_count, :group_id, :training_credit_nb, :description, :type, :ui_weight,
|
||||
:slug, :disabled, :monthly_payment
|
||||
:slug, :disabled, :monthly_payment, :plan_category_id
|
||||
json.amount plan.amount / 100.00
|
||||
json.plan_file_url plan.plan_file.attachment_url if plan.plan_file
|
||||
end
|
||||
|
Loading…
x
Reference in New Issue
Block a user