mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-30 19:52:20 +01:00
(quality) extract filter in separate components
This commit is contained in:
parent
945428e71c
commit
586dd5f9b5
@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable fabmanager/scoped-translation */
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { CaretDown } from 'phosphor-react';
|
import { CaretDown } from 'phosphor-react';
|
||||||
|
|
||||||
@ -14,6 +13,7 @@ interface AccordionItemProps {
|
|||||||
*/
|
*/
|
||||||
export const AccordionItem: React.FC<AccordionItemProps> = ({ isOpen, onChange, id, label, children }) => {
|
export const AccordionItem: React.FC<AccordionItemProps> = ({ isOpen, onChange, id, label, children }) => {
|
||||||
const [state, setState] = useState(isOpen);
|
const [state, setState] = useState(isOpen);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onChange(id, state);
|
onChange(id, state);
|
||||||
}, [state]);
|
}, [state]);
|
@ -0,0 +1,109 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import ProductCategoryAPI from '../../../api/product-category';
|
||||||
|
import ProductLib from '../../../lib/product';
|
||||||
|
import { ProductCategory } from '../../../models/product-category';
|
||||||
|
import { FabButton } from '../../base/fab-button';
|
||||||
|
import { AccordionItem } from '../../base/accordion-item';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
interface CategoriesFilterProps {
|
||||||
|
onError: (message: string) => void,
|
||||||
|
onApplyFilters: (categories: Array<ProductCategory>) => void,
|
||||||
|
currentFilters: Array<ProductCategory>,
|
||||||
|
openDefault?: boolean,
|
||||||
|
instantUpdate?: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to filter the products list by categories
|
||||||
|
*/
|
||||||
|
export const CategoriesFilter: React.FC<CategoriesFilterProps> = ({ onError, onApplyFilters, currentFilters, openDefault = false, instantUpdate = false }) => {
|
||||||
|
const { t } = useTranslation('admin');
|
||||||
|
|
||||||
|
const [productCategories, setProductCategories] = useState<ProductCategory[]>([]);
|
||||||
|
const [openedAccordion, setOpenedAccordion] = useState<boolean>(openDefault);
|
||||||
|
const [selectedCategories, setSelectedCategories] = useState<ProductCategory[]>(currentFilters || []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
ProductCategoryAPI.index().then(data => {
|
||||||
|
setProductCategories(ProductLib.sortCategories(data));
|
||||||
|
}).catch(onError);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentFilters && !_.isEqual(currentFilters, selectedCategories)) {
|
||||||
|
setSelectedCategories(currentFilters);
|
||||||
|
}
|
||||||
|
}, [currentFilters]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open/close the accordion item
|
||||||
|
*/
|
||||||
|
const handleAccordion = (id, state: boolean) => {
|
||||||
|
setOpenedAccordion(state);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback triggered when a category filter is selected or unselected.
|
||||||
|
* This may cause other categories to be selected or unselected accordingly.
|
||||||
|
*/
|
||||||
|
const handleSelectCategory = (currentCategory: ProductCategory, checked: boolean) => {
|
||||||
|
let list = [...selectedCategories];
|
||||||
|
const children = productCategories
|
||||||
|
.filter(el => el.parent_id === currentCategory.id);
|
||||||
|
const siblings = productCategories
|
||||||
|
.filter(el => el.parent_id === currentCategory.parent_id && el.parent_id !== null);
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
list.push(currentCategory);
|
||||||
|
if (children.length) {
|
||||||
|
// if a parent category is selected, we automatically select all its children
|
||||||
|
list = [...Array.from(new Set([...list, ...children]))];
|
||||||
|
}
|
||||||
|
if (siblings.length && siblings.every(el => list.includes(el))) {
|
||||||
|
// if a child category is selected, with every sibling of it, we automatically select its parent
|
||||||
|
list.push(productCategories.find(p => p.id === siblings[0].parent_id));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
list.splice(list.indexOf(currentCategory), 1);
|
||||||
|
const parent = productCategories.find(p => p.id === currentCategory.parent_id);
|
||||||
|
if (currentCategory.parent_id && list.includes(parent)) {
|
||||||
|
// if a child category is unselected, we unselect its parent
|
||||||
|
list.splice(list.indexOf(parent), 1);
|
||||||
|
}
|
||||||
|
if (children.length) {
|
||||||
|
// if a parent category is unselected, we unselect all its children
|
||||||
|
children.forEach(child => {
|
||||||
|
list.splice(list.indexOf(child), 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedCategories(list);
|
||||||
|
if (instantUpdate) {
|
||||||
|
onApplyFilters(list);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AccordionItem id={0}
|
||||||
|
isOpen={openedAccordion}
|
||||||
|
onChange={handleAccordion}
|
||||||
|
label={t('app.admin.store.categories_filter.filter_categories')}>
|
||||||
|
<div className='content'>
|
||||||
|
<div className="group u-scrollbar">
|
||||||
|
{productCategories.map(pc => (
|
||||||
|
<label key={pc.id} className={pc.parent_id ? 'offset' : ''}>
|
||||||
|
<input type="checkbox" checked={selectedCategories.includes(pc)} onChange={(event) => handleSelectCategory(pc, event.target.checked)} />
|
||||||
|
<p>{pc.name}</p>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<FabButton onClick={() => onApplyFilters(selectedCategories)} className="is-info">{t('app.admin.store.categories_filter.filter_apply')}</FabButton>
|
||||||
|
</div>
|
||||||
|
</AccordionItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,63 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { FabButton } from '../../base/fab-button';
|
||||||
|
import { AccordionItem } from '../../base/accordion-item';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
interface KeywordFilterProps {
|
||||||
|
onApplyFilters: (keywork: string) => void,
|
||||||
|
currentFilters: string,
|
||||||
|
openDefault?: boolean,
|
||||||
|
instantUpdate?: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to filter the products list by keyword or product reference
|
||||||
|
*/
|
||||||
|
export const KeywordFilter: React.FC<KeywordFilterProps> = ({ onApplyFilters, currentFilters, openDefault = false, instantUpdate = false }) => {
|
||||||
|
const { t } = useTranslation('admin');
|
||||||
|
|
||||||
|
const [openedAccordion, setOpenedAccordion] = useState<boolean>(openDefault);
|
||||||
|
const [keyword, setKeyword] = useState<string>(currentFilters);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentFilters && !_.isEqual(currentFilters, keyword)) {
|
||||||
|
setKeyword(currentFilters);
|
||||||
|
}
|
||||||
|
}, [currentFilters]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open/close the accordion item
|
||||||
|
*/
|
||||||
|
const handleAccordion = (id, state: boolean) => {
|
||||||
|
setOpenedAccordion(state);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback triggered when the user types anything in the input
|
||||||
|
*/
|
||||||
|
const handleKeywordTyping = (evt: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setKeyword(evt.target.value);
|
||||||
|
|
||||||
|
if (instantUpdate) {
|
||||||
|
onApplyFilters(evt.target.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AccordionItem id={2}
|
||||||
|
isOpen={openedAccordion}
|
||||||
|
onChange={handleAccordion}
|
||||||
|
label={t('app.admin.store.keyword_filter.filter_keywords_reference')}
|
||||||
|
>
|
||||||
|
<div className="content">
|
||||||
|
<div className="group">
|
||||||
|
<input type="text" onChange={event => handleKeywordTyping(event)} />
|
||||||
|
<FabButton onClick={() => onApplyFilters(keyword)} className="is-info">{t('app.admin.store.keyword_filter.filter_apply')}</FabButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AccordionItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,82 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { FabButton } from '../../base/fab-button';
|
||||||
|
import { AccordionItem } from '../../base/accordion-item';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Machine } from '../../../models/machine';
|
||||||
|
import MachineAPI from '../../../api/machine';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
interface MachinesFilterProps {
|
||||||
|
onError: (message: string) => void,
|
||||||
|
onApplyFilters: (categories: Array<Machine>) => void,
|
||||||
|
currentFilters: Array<Machine>,
|
||||||
|
openDefault?: boolean,
|
||||||
|
instantUpdate?: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to filter the products list by associated machine
|
||||||
|
*/
|
||||||
|
export const MachinesFilter: React.FC<MachinesFilterProps> = ({ onError, onApplyFilters, currentFilters, openDefault = false, instantUpdate = false }) => {
|
||||||
|
const { t } = useTranslation('admin');
|
||||||
|
|
||||||
|
const [machines, setMachines] = useState<Machine[]>([]);
|
||||||
|
const [openedAccordion, setOpenedAccordion] = useState<boolean>(openDefault);
|
||||||
|
const [selectedMachines, setSelectedMachines] = useState<Machine[]>(currentFilters || []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
MachineAPI.index({ disabled: false }).then(data => {
|
||||||
|
setMachines(data);
|
||||||
|
}).catch(onError);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentFilters && !_.isEqual(currentFilters, selectedMachines)) {
|
||||||
|
setSelectedMachines(currentFilters);
|
||||||
|
}
|
||||||
|
}, [currentFilters]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open/close the accordion item
|
||||||
|
*/
|
||||||
|
const handleAccordion = (id, state: boolean) => {
|
||||||
|
setOpenedAccordion(state);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback triggered when a machine filter is seleced or unselected.
|
||||||
|
*/
|
||||||
|
const handleSelectMachine = (currentMachine: Machine, checked: boolean) => {
|
||||||
|
const list = [...machines];
|
||||||
|
checked
|
||||||
|
? list.push(currentMachine)
|
||||||
|
: list.splice(list.indexOf(currentMachine), 1);
|
||||||
|
|
||||||
|
setSelectedMachines(list);
|
||||||
|
if (instantUpdate) {
|
||||||
|
onApplyFilters(list);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AccordionItem id={1}
|
||||||
|
isOpen={openedAccordion}
|
||||||
|
onChange={handleAccordion}
|
||||||
|
label={t('app.admin.store.machines_filter.filter_machines')}
|
||||||
|
>
|
||||||
|
<div className='content'>
|
||||||
|
<div className="group u-scrollbar">
|
||||||
|
{machines.map(m => (
|
||||||
|
<label key={m.id}>
|
||||||
|
<input type="checkbox" checked={machines.includes(m)} onChange={(event) => handleSelectMachine(m, event.target.checked)} />
|
||||||
|
<p>{m.name}</p>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<FabButton onClick={() => onApplyFilters(selectedMachines)} className="is-info">{t('app.admin.store.machines_filter.filter_apply')}</FabButton>
|
||||||
|
</div>
|
||||||
|
</AccordionItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,98 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { FabButton } from '../../base/fab-button';
|
||||||
|
import { AccordionItem } from '../../base/accordion-item';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { StockType } from '../../../models/product';
|
||||||
|
import { FormSelect } from '../../form/form-select';
|
||||||
|
import { FormInput } from '../../form/form-input';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
export interface StockFilterData {
|
||||||
|
stock_type: StockType,
|
||||||
|
stock_from: number,
|
||||||
|
stock_to: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StockFilterProps {
|
||||||
|
onApplyFilters: (filters: StockFilterData) => void,
|
||||||
|
currentFilters: StockFilterData,
|
||||||
|
openDefault?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option format, expected by react-select
|
||||||
|
* @see https://github.com/JedWatson/react-select
|
||||||
|
*/
|
||||||
|
type selectOption = { value: StockType, label: string };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component to filter the products list by stock
|
||||||
|
*/
|
||||||
|
export const StockFilter: React.FC<StockFilterProps> = ({ onApplyFilters, currentFilters, openDefault = false }) => {
|
||||||
|
const { t } = useTranslation('admin');
|
||||||
|
|
||||||
|
const [openedAccordion, setOpenedAccordion] = useState<boolean>(openDefault);
|
||||||
|
|
||||||
|
const { register, control, handleSubmit, getValues, reset } = useForm<StockFilterData>({ defaultValues: { ...currentFilters } });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentFilters && !_.isEqual(currentFilters, getValues())) {
|
||||||
|
reset(currentFilters);
|
||||||
|
}
|
||||||
|
}, [currentFilters]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open/close the accordion item
|
||||||
|
*/
|
||||||
|
const handleAccordion = (id, state: boolean) => {
|
||||||
|
setOpenedAccordion(state);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback triggered when the user clicks on "apply" to apply teh current filters.
|
||||||
|
*/
|
||||||
|
const onSubmit = (data: StockFilterData) => {
|
||||||
|
onApplyFilters(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Creates sorting options to the react-select format */
|
||||||
|
const buildStockOptions = (): Array<selectOption> => {
|
||||||
|
return [
|
||||||
|
{ value: 'internal', label: t('app.admin.store.stock_filter.stock_internal') },
|
||||||
|
{ value: 'external', label: t('app.admin.store.stock_filter.stock_external') }
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AccordionItem id={3}
|
||||||
|
isOpen={openedAccordion}
|
||||||
|
onChange={handleAccordion}
|
||||||
|
label={t('app.admin.store.stock_filter.filter_stock')}>
|
||||||
|
<form className="content" onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="group">
|
||||||
|
<FormSelect id="stock_type"
|
||||||
|
options={buildStockOptions()}
|
||||||
|
valueDefault="internal"
|
||||||
|
control={control}
|
||||||
|
/>
|
||||||
|
<div className='range'>
|
||||||
|
<FormInput id="stock_from"
|
||||||
|
label={t('app.admin.store.stock_filter.filter_stock_from')}
|
||||||
|
register={register}
|
||||||
|
defaultValue={0}
|
||||||
|
type="number" />
|
||||||
|
<FormInput id="stock_to"
|
||||||
|
label={t('app.admin.store.stock_filter.filter_stock_to')}
|
||||||
|
register={register}
|
||||||
|
defaultValue={0}
|
||||||
|
type="number" />
|
||||||
|
</div>
|
||||||
|
<FabButton type="submit" className="is-info">{t('app.admin.store.stock_filter.filter_apply')}</FabButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</AccordionItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -7,7 +7,7 @@ import { IApplication } from '../../models/application';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { FabButton } from '../base/fab-button';
|
import { FabButton } from '../base/fab-button';
|
||||||
import { StoreListHeader } from './store-list-header';
|
import { StoreListHeader } from './store-list-header';
|
||||||
import { AccordionItem } from './accordion-item';
|
import { AccordionItem } from '../base/accordion-item';
|
||||||
import { OrderItem } from './order-item';
|
import { OrderItem } from './order-item';
|
||||||
import { MemberSelect } from '../user/member-select';
|
import { MemberSelect } from '../user/member-select';
|
||||||
import { User } from '../../models/user';
|
import { User } from '../../models/user';
|
||||||
|
@ -6,19 +6,17 @@ import { Loader } from '../base/loader';
|
|||||||
import { IApplication } from '../../models/application';
|
import { IApplication } from '../../models/application';
|
||||||
import { Product } from '../../models/product';
|
import { Product } from '../../models/product';
|
||||||
import { ProductCategory } from '../../models/product-category';
|
import { ProductCategory } from '../../models/product-category';
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import { FabButton } from '../base/fab-button';
|
import { FabButton } from '../base/fab-button';
|
||||||
import { ProductItem } from './product-item';
|
import { ProductItem } from './product-item';
|
||||||
import ProductAPI from '../../api/product';
|
import ProductAPI from '../../api/product';
|
||||||
import ProductCategoryAPI from '../../api/product-category';
|
|
||||||
import MachineAPI from '../../api/machine';
|
|
||||||
import { AccordionItem } from './accordion-item';
|
|
||||||
import { X } from 'phosphor-react';
|
import { X } from 'phosphor-react';
|
||||||
import { StoreListHeader } from './store-list-header';
|
import { StoreListHeader } from './store-list-header';
|
||||||
import { FabPagination } from '../base/fab-pagination';
|
import { FabPagination } from '../base/fab-pagination';
|
||||||
import ProductLib from '../../lib/product';
|
import { CategoriesFilter } from './filters/categories-filter';
|
||||||
import { FormInput } from '../form/form-input';
|
import { Machine } from '../../models/machine';
|
||||||
import { FormSelect } from '../form/form-select';
|
import { MachinesFilter } from './filters/machines-filter';
|
||||||
|
import { KeywordFilter } from './filters/keyword-filter';
|
||||||
|
import { StockFilter, StockFilterData } from './filters/stock-filter';
|
||||||
|
|
||||||
declare const Application: IApplication;
|
declare const Application: IApplication;
|
||||||
|
|
||||||
@ -36,17 +34,12 @@ interface ProductsProps {
|
|||||||
const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
|
const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
|
||||||
const { t } = useTranslation('admin');
|
const { t } = useTranslation('admin');
|
||||||
|
|
||||||
const { register, control, getValues } = useForm();
|
|
||||||
|
|
||||||
const [filteredProductsList, setFilteredProductList] = useImmer<Array<Product>>([]);
|
const [filteredProductsList, setFilteredProductList] = useImmer<Array<Product>>([]);
|
||||||
const [features, setFeatures] = useImmer<Filters>(initFilters);
|
const [features, setFeatures] = useImmer<Filters>(initFilters);
|
||||||
const [filterVisible, setFilterVisible] = useState<boolean>(false);
|
const [filterVisible, setFilterVisible] = useState<boolean>(false);
|
||||||
const [filters, setFilters] = useImmer<Filters>(initFilters);
|
const [filters, setFilters] = useImmer<Filters>(initFilters);
|
||||||
const [clearFilters, setClearFilters] = useState<boolean>(false);
|
const [clearFilters, setClearFilters] = useState<boolean>(false);
|
||||||
const [productCategories, setProductCategories] = useState<ProductCategory[]>([]);
|
|
||||||
const [machines, setMachines] = useState<checklistOption[]>([]);
|
|
||||||
const [update, setUpdate] = useState(false);
|
const [update, setUpdate] = useState(false);
|
||||||
const [accordion, setAccordion] = useState({});
|
|
||||||
const [pageCount, setPageCount] = useState<number>(0);
|
const [pageCount, setPageCount] = useState<number>(0);
|
||||||
const [currentPage, setCurrentPage] = useState<number>(1);
|
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||||
|
|
||||||
@ -55,14 +48,6 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
|
|||||||
setPageCount(data.total_pages);
|
setPageCount(data.total_pages);
|
||||||
setFilteredProductList(data.products);
|
setFilteredProductList(data.products);
|
||||||
});
|
});
|
||||||
|
|
||||||
ProductCategoryAPI.index().then(data => {
|
|
||||||
setProductCategories(ProductLib.sortCategories(data));
|
|
||||||
}).catch(onError);
|
|
||||||
|
|
||||||
MachineAPI.index({ disabled: false }).then(data => {
|
|
||||||
setMachines(buildChecklistOptions(data));
|
|
||||||
}).catch(onError);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -114,92 +99,42 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback triggered when a category filter is selected or unselected.
|
* Update the list of applied filters with the given categories
|
||||||
* This may cause other filters to be selected or unselected accordingly.
|
|
||||||
*/
|
*/
|
||||||
const handleSelectCategory = (currentCategory: ProductCategory, checked: boolean, instantUpdate?: boolean) => {
|
const handleCategoriesFilterUpdate = (categories: Array<ProductCategory>) => {
|
||||||
let list = [...filters.categories];
|
|
||||||
const children = productCategories
|
|
||||||
.filter(el => el.parent_id === currentCategory.id);
|
|
||||||
const siblings = productCategories
|
|
||||||
.filter(el => el.parent_id === currentCategory.parent_id && el.parent_id !== null);
|
|
||||||
|
|
||||||
if (checked) {
|
|
||||||
list.push(currentCategory);
|
|
||||||
if (children.length) {
|
|
||||||
// if a parent category is selected, we automatically select all its children
|
|
||||||
list = [...Array.from(new Set([...list, ...children]))];
|
|
||||||
}
|
|
||||||
if (siblings.length && siblings.every(el => list.includes(el))) {
|
|
||||||
// if a child category is selected, with every sibling of it, we automatically select its parent
|
|
||||||
list.push(productCategories.find(p => p.id === siblings[0].parent_id));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
list.splice(list.indexOf(currentCategory), 1);
|
|
||||||
const parent = productCategories.find(p => p.id === currentCategory.parent_id);
|
|
||||||
if (currentCategory.parent_id && list.includes(parent)) {
|
|
||||||
// if a child category is unselected, we unselect its parent
|
|
||||||
list.splice(list.indexOf(parent), 1);
|
|
||||||
}
|
|
||||||
if (children.length) {
|
|
||||||
// if a parent category is unselected, we unselect all its children
|
|
||||||
children.forEach(child => {
|
|
||||||
list.splice(list.indexOf(child), 1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setFilters(draft => {
|
setFilters(draft => {
|
||||||
return { ...draft, categories: list };
|
return { ...draft, categories };
|
||||||
});
|
});
|
||||||
if (instantUpdate) {
|
|
||||||
setUpdate(true);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback triggered when a machine filter is seleced or unselected.
|
* Update the list of applied filters with the given machines
|
||||||
*/
|
*/
|
||||||
const handleSelectMachine = (currentMachine: checklistOption, checked: boolean, instantUpdate?: boolean) => {
|
const handleMachinesFilterUpdate = (machines: Array<Machine>) => {
|
||||||
const list = [...filters.machines];
|
|
||||||
checked
|
|
||||||
? list.push(currentMachine)
|
|
||||||
: list.splice(list.indexOf(currentMachine), 1);
|
|
||||||
setFilters(draft => {
|
setFilters(draft => {
|
||||||
return { ...draft, machines: list };
|
return { ...draft, machines };
|
||||||
});
|
});
|
||||||
if (instantUpdate) {
|
|
||||||
setUpdate(true);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Filter: by keyword or ref */
|
/**
|
||||||
const handleKeyword = (evt: React.ChangeEvent<HTMLInputElement>) => {
|
* Update the list of applied filters with the given keywords (or reference)
|
||||||
|
*/
|
||||||
|
const handleKeywordFilterUpdate = (keywords: Array<string>) => {
|
||||||
setFilters(draft => {
|
setFilters(draft => {
|
||||||
return { ...draft, keywords: evt.target.value };
|
return { ...draft, keywords };
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Filter: by stock range */
|
/** Filter: by stock range */
|
||||||
const handleStockRange = () => {
|
const handleStockFilterUpdate = (filters: StockFilterData) => {
|
||||||
setFilters(draft => {
|
setFilters(draft => {
|
||||||
return {
|
return {
|
||||||
...draft,
|
...draft,
|
||||||
stock_type: buildStockOptions()[getValues('stock_type')].label,
|
...filters
|
||||||
stock_from: getValues('stock_from'),
|
|
||||||
stock_to: getValues('stock_to')
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Creates sorting options to the react-select format */
|
|
||||||
const buildStockOptions = (): Array<selectOption> => {
|
|
||||||
return [
|
|
||||||
{ value: 0, label: t('app.admin.store.products.stock_internal') },
|
|
||||||
{ value: 1, label: t('app.admin.store.products.stock_external') }
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Display option: sorting */
|
/** Display option: sorting */
|
||||||
const handleSorting = (option: selectOption) => {
|
const handleSorting = (option: selectOption) => {
|
||||||
console.log('Sort option:', option);
|
console.log('Sort option:', option);
|
||||||
@ -238,11 +173,6 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Open/close accordion items */
|
|
||||||
const handleAccordion = (id, state) => {
|
|
||||||
setAccordion({ ...accordion, [id]: state });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='products'>
|
<div className='products'>
|
||||||
<header>
|
<header>
|
||||||
@ -259,83 +189,19 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className='accordion'>
|
<div className='accordion'>
|
||||||
<AccordionItem id={0}
|
<CategoriesFilter onError={onError}
|
||||||
isOpen={accordion[0]}
|
onApplyFilters={handleCategoriesFilterUpdate}
|
||||||
onChange={handleAccordion}
|
currentFilters={filters.categories} />
|
||||||
label={t('app.admin.store.products.filter_categories')}
|
|
||||||
>
|
|
||||||
<div className='content'>
|
|
||||||
<div className="group u-scrollbar">
|
|
||||||
{productCategories.map(pc => (
|
|
||||||
<label key={pc.id} className={pc.parent_id ? 'offset' : ''}>
|
|
||||||
<input type="checkbox" checked={filters.categories.includes(pc)} onChange={(event) => handleSelectCategory(pc, event.target.checked)} />
|
|
||||||
<p>{pc.name}</p>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<FabButton onClick={() => setUpdate(true)} className="is-info">{t('app.admin.store.products.filter_apply')}</FabButton>
|
|
||||||
</div>
|
|
||||||
</AccordionItem>
|
|
||||||
|
|
||||||
<AccordionItem id={1}
|
<MachinesFilter onError={onError}
|
||||||
isOpen={accordion[1]}
|
onApplyFilters={handleMachinesFilterUpdate}
|
||||||
onChange={handleAccordion}
|
currentFilters={filters.machines} />
|
||||||
label={t('app.admin.store.products.filter_machines')}
|
|
||||||
>
|
|
||||||
<div className='content'>
|
|
||||||
<div className="group u-scrollbar">
|
|
||||||
{machines.map(m => (
|
|
||||||
<label key={m.value}>
|
|
||||||
<input type="checkbox" checked={filters.machines.includes(m)} onChange={(event) => handleSelectMachine(m, event.target.checked)} />
|
|
||||||
<p>{m.label}</p>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<FabButton onClick={() => setUpdate(true)} className="is-info">{t('app.admin.store.products.filter_apply')}</FabButton>
|
|
||||||
</div>
|
|
||||||
</AccordionItem>
|
|
||||||
|
|
||||||
<AccordionItem id={2}
|
<KeywordFilter onApplyFilters={keyword => handleKeywordFilterUpdate([...filters.keywords, keyword])}
|
||||||
isOpen={accordion[2]}
|
currentFilters={filters.keywords[0]} />
|
||||||
onChange={handleAccordion}
|
|
||||||
label={t('app.admin.store.products.filter_keywords_reference')}
|
|
||||||
>
|
|
||||||
<div className="content">
|
|
||||||
<div className="group">
|
|
||||||
<input type="text" onChange={event => handleKeyword(event)} />
|
|
||||||
<FabButton onClick={() => setUpdate(true)} className="is-info">{t('app.admin.store.products.filter_apply')}</FabButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AccordionItem>
|
|
||||||
|
|
||||||
<AccordionItem id={3}
|
<StockFilter onApplyFilters={handleStockFilterUpdate}
|
||||||
isOpen={accordion[3]}
|
currentFilters={filters} />
|
||||||
onChange={handleAccordion}
|
|
||||||
label={t('app.admin.store.products.filter_stock')}
|
|
||||||
>
|
|
||||||
<div className="content">
|
|
||||||
<div className="group">
|
|
||||||
<FormSelect id="stock_type"
|
|
||||||
options={buildStockOptions()}
|
|
||||||
valueDefault={0}
|
|
||||||
control={control}
|
|
||||||
/>
|
|
||||||
<div className='range'>
|
|
||||||
<FormInput id="stock_from"
|
|
||||||
label={t('app.admin.store.products.filter_stock_from')}
|
|
||||||
register={register}
|
|
||||||
defaultValue={filters.stock_from}
|
|
||||||
type="number" />
|
|
||||||
<FormInput id="stock_to"
|
|
||||||
label={t('app.admin.store.products.filter_stock_to')}
|
|
||||||
register={register}
|
|
||||||
defaultValue={filters.stock_to}
|
|
||||||
type="number" />
|
|
||||||
</div>
|
|
||||||
<FabButton onClick={handleStockRange} className="is-info">{t('app.admin.store.products.filter_apply')}</FabButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</AccordionItem>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='store-list'>
|
<div className='store-list'>
|
||||||
@ -350,13 +216,19 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
|
|||||||
{features.categories.map(c => (
|
{features.categories.map(c => (
|
||||||
<div key={c.id} className='features-item'>
|
<div key={c.id} className='features-item'>
|
||||||
<p>{c.name}</p>
|
<p>{c.name}</p>
|
||||||
<button onClick={() => handleSelectCategory(c, false, true)}><X size={16} weight="light" /></button>
|
<button onClick={() => handleCategoriesFilterUpdate(filters.categories.filter(cat => cat !== c))}><X size={16} weight="light" /></button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{features.machines.map(m => (
|
{features.machines.map(m => (
|
||||||
<div key={m.value} className='features-item'>
|
<div key={m.id} className='features-item'>
|
||||||
<p>{m.label}</p>
|
<p>{m.name}</p>
|
||||||
<button onClick={() => handleSelectMachine(m, false, true)}><X size={16} weight="light" /></button>
|
<button onClick={() => handleMachinesFilterUpdate(filters.machines.filter(machine => machine !== m))}><X size={16} weight="light" /></button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{features.keywords.map(k => (
|
||||||
|
<div key={k} className='features-item'>
|
||||||
|
<p>{k}</p>
|
||||||
|
<button onClick={() => handleKeywordFilterUpdate(filters.keywords.filter(keyword => keyword !== k))}><X size={16} weight="light" /></button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -389,19 +261,9 @@ const ProductsWrapper: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
|
|||||||
|
|
||||||
Application.Components.component('products', react2angular(ProductsWrapper, ['onSuccess', 'onError']));
|
Application.Components.component('products', react2angular(ProductsWrapper, ['onSuccess', 'onError']));
|
||||||
|
|
||||||
/** Option format, expected by checklist */
|
|
||||||
type checklistOption = { value: number, label: string };
|
|
||||||
|
|
||||||
/** Convert the provided array of items to the checklist format */
|
|
||||||
const buildChecklistOptions = (items: Array<{ id?: number, name: string }>): Array<checklistOption> => {
|
|
||||||
return items.map(t => {
|
|
||||||
return { value: t.id, label: t.name };
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Filters {
|
interface Filters {
|
||||||
categories: ProductCategory[],
|
categories: ProductCategory[],
|
||||||
machines: checklistOption[],
|
machines: Machine[],
|
||||||
keywords: string[],
|
keywords: string[],
|
||||||
stock_type: 'internal' | 'external',
|
stock_type: 'internal' | 'external',
|
||||||
stock_from: number,
|
stock_from: number,
|
||||||
|
@ -13,7 +13,7 @@ import { StoreProductItem } from './store-product-item';
|
|||||||
import useCart from '../../hooks/use-cart';
|
import useCart from '../../hooks/use-cart';
|
||||||
import { User } from '../../models/user';
|
import { User } from '../../models/user';
|
||||||
import { Order } from '../../models/order';
|
import { Order } from '../../models/order';
|
||||||
import { AccordionItem } from './accordion-item';
|
import { AccordionItem } from '../base/accordion-item';
|
||||||
import { StoreListHeader } from './store-list-header';
|
import { StoreListHeader } from './store-list-header';
|
||||||
import { FabPagination } from '../base/fab-pagination';
|
import { FabPagination } from '../base/fab-pagination';
|
||||||
|
|
||||||
|
@ -1933,6 +1933,22 @@ en:
|
|||||||
save: "Save"
|
save: "Save"
|
||||||
required: "This field is required"
|
required: "This field is required"
|
||||||
slug_pattern: "Only lowercase alphanumeric groups of characters separated by an hyphen"
|
slug_pattern: "Only lowercase alphanumeric groups of characters separated by an hyphen"
|
||||||
|
categories_filter:
|
||||||
|
filter_categories: "By categories"
|
||||||
|
filter_apply: "Apply"
|
||||||
|
machines_filter:
|
||||||
|
filter_machines: "By machines"
|
||||||
|
filter_apply: "Apply"
|
||||||
|
keyword_filter:
|
||||||
|
filter_keywords_reference: "By keywords or reference"
|
||||||
|
filter_apply: "Apply"
|
||||||
|
stock_filter:
|
||||||
|
stock_internal: "Private stock"
|
||||||
|
stock_external: "Public stock"
|
||||||
|
filter_stock: "By stock status"
|
||||||
|
filter_stock_from: "From"
|
||||||
|
filter_stock_to: "to"
|
||||||
|
filter_apply: "Apply"
|
||||||
products:
|
products:
|
||||||
unexpected_error_occurred: "An unexpected error occurred. Please try again later."
|
unexpected_error_occurred: "An unexpected error occurred. Please try again later."
|
||||||
all_products: "All products"
|
all_products: "All products"
|
||||||
@ -1941,15 +1957,6 @@ en:
|
|||||||
unable_to_delete: "Unable to delete the product: "
|
unable_to_delete: "Unable to delete the product: "
|
||||||
filter: "Filter"
|
filter: "Filter"
|
||||||
filter_clear: "Clear all"
|
filter_clear: "Clear all"
|
||||||
filter_apply: "Apply"
|
|
||||||
filter_categories: "By categories"
|
|
||||||
filter_machines: "By machines"
|
|
||||||
filter_keywords_reference: "By keywords or reference"
|
|
||||||
filter_stock: "By stock status"
|
|
||||||
stock_internal: "Private stock"
|
|
||||||
stock_external: "Public stock"
|
|
||||||
filter_stock_from: "From"
|
|
||||||
filter_stock_to: "to"
|
|
||||||
sort:
|
sort:
|
||||||
name_az: "A-Z"
|
name_az: "A-Z"
|
||||||
name_za: "Z-A"
|
name_za: "Z-A"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user