mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-19 13:54:25 +01:00
filter plans y duration
This commit is contained in:
parent
4580bfc7d7
commit
e184bf3d3c
@ -11,12 +11,13 @@
|
||||
- Improved documentations
|
||||
- Improved the style of the titles of the subscription page
|
||||
- Check the status of the assets' compilation during the upgrade
|
||||
- Footprints are now generated in a more reproductible way
|
||||
- Generate footprints in a more reproductible way
|
||||
- Task to reset the stripe payment methods in test mode
|
||||
- Validate on server side the reservation of slots restricted to subscribers
|
||||
− Unified and documented upgrade exit codes
|
||||
- During setup, ask for the name of the external network and create it, if it does not already exists
|
||||
- Ability to configure the prefix of the payment-schedules' files
|
||||
- Filter plans by group and by duration
|
||||
- Fix a bug: cannot select the recurrence end date on Safari or Internet Explorer
|
||||
- Fix a bug: build status badge is not working
|
||||
- Fix a bug: unable to set date formats during installation
|
||||
|
@ -4,7 +4,7 @@
|
||||
# Plan are used to define subscription's characteristics.
|
||||
# PartnerPlan is a special kind of plan which send notifications to an external user
|
||||
class API::PlansController < API::ApiController
|
||||
before_action :authenticate_user!, except: [:index]
|
||||
before_action :authenticate_user!, except: [:index, :durations]
|
||||
|
||||
def index
|
||||
@plans = Plan.includes(:plan_file)
|
||||
@ -51,6 +51,17 @@ class API::PlansController < API::ApiController
|
||||
head :no_content
|
||||
end
|
||||
|
||||
def durations
|
||||
grouped = Plan.all.map { |p| [p.human_readable_duration, p.id] }.group_by { |i| i[0] }
|
||||
@durations = []
|
||||
grouped.each_pair do |duration, plans|
|
||||
@durations.push(
|
||||
name: duration,
|
||||
plans: plans.map { |p| p[1] }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def plan_params
|
||||
|
@ -1,11 +1,16 @@
|
||||
import apiClient from './clients/api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { Plan } from '../models/plan';
|
||||
import { Plan, PlansDuration } 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;
|
||||
}
|
||||
|
||||
static async durations (): Promise<Array<PlansDuration>> {
|
||||
const res: AxiosResponse<Array<PlansDuration>> = await apiClient.get('/api/plans/durations');
|
||||
return res?.data;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,17 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Select from 'react-select';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Group } from '../../models/group';
|
||||
import { User } from '../../models/user';
|
||||
import PlanAPI from '../../api/plan';
|
||||
import { PlansDuration } from '../../models/plan';
|
||||
|
||||
interface PlansFilterProps {
|
||||
user?: User,
|
||||
groups: Array<Group>,
|
||||
onGroupSelected: (groupId: number) => void,
|
||||
onError: (message: string) => void,
|
||||
onDurationSelected: (plansIds: Array<number>) => void,
|
||||
}
|
||||
|
||||
/**
|
||||
@ -16,9 +20,18 @@ interface PlansFilterProps {
|
||||
*/
|
||||
type selectOption = { value: number, label: string };
|
||||
|
||||
export const PlansFilter: React.FC<PlansFilterProps> = ({ user, groups, onGroupSelected }) => {
|
||||
export const PlansFilter: React.FC<PlansFilterProps> = ({ user, groups, onGroupSelected, onError, onDurationSelected }) => {
|
||||
const { t } = useTranslation('public');
|
||||
|
||||
const [durations, setDurations] = useState<Array<PlansDuration>>(null);
|
||||
|
||||
// get the plans durations on component load
|
||||
useEffect(() => {
|
||||
PlanAPI.durations().then(data => {
|
||||
setDurations(data);
|
||||
}).catch(error => onError(error));
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Convert all groups to the react-select format
|
||||
*/
|
||||
@ -29,21 +42,48 @@ export const PlansFilter: React.FC<PlansFilterProps> = ({ user, groups, onGroupS
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback triggered when the user select a group in the dropdown list
|
||||
* Convert all durations to the react-select format
|
||||
*/
|
||||
const buildDurationOptions = (): Array<selectOption> => {
|
||||
const options = durations.map((d, index) => {
|
||||
return { value: index, label: d.name };
|
||||
});
|
||||
options.unshift({ value: null, label: t('app.public.plans_filter.all_durations') });
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback triggered when the user selects a group in the dropdown list
|
||||
*/
|
||||
const handleGroupSelected = (option: selectOption): void => {
|
||||
onGroupSelected(option.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback triggered when the user selects a duration in the dropdown list
|
||||
*/
|
||||
const handleDurationSelected = (option: selectOption): void => {
|
||||
onDurationSelected(durations[option.value]?.plans_ids);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="plans-filter">
|
||||
{!user && <div className="group-filter">
|
||||
<label htmlFor="group">{t('app.public.plans_filter.i_am')}</label>
|
||||
<Select placeholder={t('app.public.plans_filter.select_group')}
|
||||
id="group"
|
||||
className="group-select"
|
||||
onChange={handleGroupSelected}
|
||||
options={buildGroupOptions()}/>
|
||||
</div>}
|
||||
{durations && <div className="duration-filter">
|
||||
<label htmlFor="duration">{t('app.public.plans_filter.i_want_duration')}</label>
|
||||
<Select placeholder={t('app.public.plans_filter.select_duration')}
|
||||
id="duration"
|
||||
className="duration-select"
|
||||
onChange={handleDurationSelected}
|
||||
options={buildDurationOptions()}/>
|
||||
</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -42,8 +42,10 @@ const PlansList: React.FC<PlansListProps> = ({ onError, onPlanSelection, onLogin
|
||||
const [groups, setGroups] = useState<Array<Group>>(null);
|
||||
// currently selected plan
|
||||
const [selectedPlan, setSelectedPlan] = useState<Plan>(null);
|
||||
// filter shown plans by only one group
|
||||
// filtering shown plans by only one group
|
||||
const [groupFilter, setGroupFilter] = useState<number>(null);
|
||||
// filtering shown plans by ids
|
||||
const [plansFilter, setPlansFilter] = useState<Array<number>>(null);
|
||||
|
||||
// fetch data on component mounted
|
||||
useEffect(() => {
|
||||
@ -160,12 +162,29 @@ const PlansList: React.FC<PlansListProps> = ({ onError, onPlanSelection, onLogin
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback triggered when the user select a group to filter the current list
|
||||
* Callback triggered when the user selects a group to filter the current list
|
||||
*/
|
||||
const handleFilterByGroup = (groupId: number): void => {
|
||||
setGroupFilter(groupId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback triggered when the user selects a duration to filter the current list
|
||||
*/
|
||||
const handleFilterByDuration = (plansIds: Array<number>): void => {
|
||||
setPlansFilter(plansIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for filtering plans to display, depending on the filter-by-plans-ids selection
|
||||
* @see https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
|
||||
*/
|
||||
const filterPlan = (plan: Plan): boolean => {
|
||||
if (!plansFilter) return true;
|
||||
|
||||
return plansFilter.includes(plan.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the provided list of categories, with each associated plans
|
||||
*/
|
||||
@ -193,7 +212,7 @@ const PlansList: React.FC<PlansListProps> = ({ onError, onPlanSelection, onLogin
|
||||
{categoryPlans.length === 0 && <span className="no-plans">
|
||||
{t('app.public.plans.no_plans')}
|
||||
</span>}
|
||||
{categoryPlans.sort(comparePlans).map(plan => (
|
||||
{categoryPlans.filter(filterPlan).sort(comparePlans).map(plan => (
|
||||
<PlanCard key={plan.id}
|
||||
userId={customer?.id}
|
||||
subscribedPlanId={subscribedPlanId}
|
||||
@ -209,7 +228,7 @@ const PlansList: React.FC<PlansListProps> = ({ onError, onPlanSelection, onLogin
|
||||
|
||||
return (
|
||||
<div className="plans-list">
|
||||
{groups && <PlansFilter user={customer} groups={groups} onGroupSelected={handleFilterByGroup} />}
|
||||
{groups && <PlansFilter user={customer} groups={groups} onGroupSelected={handleFilterByGroup} onError={onError} onDurationSelected={handleFilterByDuration} />}
|
||||
{plans && Array.from(filteredPlans()).map(([groupId, plansByGroup]) => {
|
||||
return (
|
||||
<div key={groupId} className="plans-per-group">
|
||||
|
@ -41,3 +41,8 @@ export interface Plan {
|
||||
plan_file_url: string,
|
||||
partners: Array<Partner>
|
||||
}
|
||||
|
||||
export interface PlansDuration {
|
||||
name: string,
|
||||
plans_ids: Array<number>
|
||||
}
|
||||
|
@ -2,13 +2,38 @@
|
||||
margin: 1.5em;
|
||||
|
||||
.group-filter {
|
||||
padding-right: 1.5em;
|
||||
}
|
||||
|
||||
.duration-filter,
|
||||
.group-filter {
|
||||
& {
|
||||
display: inline-flex;
|
||||
width: 50%;
|
||||
}
|
||||
& > label {
|
||||
white-space: nowrap;
|
||||
line-height: 2em;
|
||||
}
|
||||
& > * {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.duration-select,
|
||||
.group-select {
|
||||
min-width: 40%;
|
||||
width: 100%;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 720px){
|
||||
.plans-filter {
|
||||
.group-filter {
|
||||
padding-right: 0;
|
||||
}
|
||||
.group-filter, .duration-filter {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
6
app/views/api/plans/durations.json.jbuilder
Normal file
6
app/views/api/plans/durations.json.jbuilder
Normal file
@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.array!(@durations) do |duration|
|
||||
json.name duration[:name]
|
||||
json.plans_ids duration[:plans]
|
||||
end
|
@ -262,6 +262,9 @@ en:
|
||||
plans_filter:
|
||||
i_am: "I am"
|
||||
select_group: "select a group"
|
||||
i_want_duration: "I want to subscribe for"
|
||||
all_durations: "All durations"
|
||||
select_duration: "select a duration"
|
||||
#Fablab's events list
|
||||
events_list:
|
||||
the_fablab_s_events: "The Fablab's events"
|
||||
|
@ -97,7 +97,9 @@ Rails.application.routes.draw do
|
||||
resources :groups, only: %i[index create update destroy]
|
||||
resources :subscriptions, only: %i[show update]
|
||||
resources :plan_categories
|
||||
resources :plans
|
||||
resources :plans do
|
||||
get 'durations', on: :collection
|
||||
end
|
||||
resources :slots, only: [:update] do
|
||||
put 'cancel', on: :member
|
||||
end
|
||||
|
Loading…
x
Reference in New Issue
Block a user