1
0
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:
Sylvain 2021-06-10 14:06:53 +02:00
parent 4580bfc7d7
commit e184bf3d3c
10 changed files with 130 additions and 13 deletions

View File

@ -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

View File

@ -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

View File

@ -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;
}
}

View File

@ -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>
)
}

View File

@ -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">

View File

@ -41,3 +41,8 @@ export interface Plan {
plan_file_url: string,
partners: Array<Partner>
}
export interface PlansDuration {
name: string,
plans_ids: Array<number>
}

View File

@ -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%;
}
}
}

View File

@ -0,0 +1,6 @@
# frozen_string_literal: true
json.array!(@durations) do |duration|
json.name duration[:name]
json.plans_ids duration[:plans]
end

View File

@ -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"

View File

@ -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