mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-18 07:52:23 +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 { CaretDown } from 'phosphor-react';
|
||||
|
||||
@ -14,6 +13,7 @@ interface AccordionItemProps {
|
||||
*/
|
||||
export const AccordionItem: React.FC<AccordionItemProps> = ({ isOpen, onChange, id, label, children }) => {
|
||||
const [state, setState] = useState(isOpen);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(id, 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 { FabButton } from '../base/fab-button';
|
||||
import { StoreListHeader } from './store-list-header';
|
||||
import { AccordionItem } from './accordion-item';
|
||||
import { AccordionItem } from '../base/accordion-item';
|
||||
import { OrderItem } from './order-item';
|
||||
import { MemberSelect } from '../user/member-select';
|
||||
import { User } from '../../models/user';
|
||||
|
@ -6,19 +6,17 @@ import { Loader } from '../base/loader';
|
||||
import { IApplication } from '../../models/application';
|
||||
import { Product } from '../../models/product';
|
||||
import { ProductCategory } from '../../models/product-category';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { ProductItem } from './product-item';
|
||||
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 { StoreListHeader } from './store-list-header';
|
||||
import { FabPagination } from '../base/fab-pagination';
|
||||
import ProductLib from '../../lib/product';
|
||||
import { FormInput } from '../form/form-input';
|
||||
import { FormSelect } from '../form/form-select';
|
||||
import { CategoriesFilter } from './filters/categories-filter';
|
||||
import { Machine } from '../../models/machine';
|
||||
import { MachinesFilter } from './filters/machines-filter';
|
||||
import { KeywordFilter } from './filters/keyword-filter';
|
||||
import { StockFilter, StockFilterData } from './filters/stock-filter';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
@ -36,17 +34,12 @@ interface ProductsProps {
|
||||
const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const { register, control, getValues } = useForm();
|
||||
|
||||
const [filteredProductsList, setFilteredProductList] = useImmer<Array<Product>>([]);
|
||||
const [features, setFeatures] = useImmer<Filters>(initFilters);
|
||||
const [filterVisible, setFilterVisible] = useState<boolean>(false);
|
||||
const [filters, setFilters] = useImmer<Filters>(initFilters);
|
||||
const [clearFilters, setClearFilters] = useState<boolean>(false);
|
||||
const [productCategories, setProductCategories] = useState<ProductCategory[]>([]);
|
||||
const [machines, setMachines] = useState<checklistOption[]>([]);
|
||||
const [update, setUpdate] = useState(false);
|
||||
const [accordion, setAccordion] = useState({});
|
||||
const [pageCount, setPageCount] = useState<number>(0);
|
||||
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||
|
||||
@ -55,14 +48,6 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
|
||||
setPageCount(data.total_pages);
|
||||
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(() => {
|
||||
@ -114,92 +99,42 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered when a category filter is selected or unselected.
|
||||
* This may cause other filters to be selected or unselected accordingly.
|
||||
* Update the list of applied filters with the given categories
|
||||
*/
|
||||
const handleSelectCategory = (currentCategory: ProductCategory, checked: boolean, instantUpdate?: boolean) => {
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const handleCategoriesFilterUpdate = (categories: Array<ProductCategory>) => {
|
||||
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 list = [...filters.machines];
|
||||
checked
|
||||
? list.push(currentMachine)
|
||||
: list.splice(list.indexOf(currentMachine), 1);
|
||||
const handleMachinesFilterUpdate = (machines: Array<Machine>) => {
|
||||
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 => {
|
||||
return { ...draft, keywords: evt.target.value };
|
||||
return { ...draft, keywords };
|
||||
});
|
||||
};
|
||||
|
||||
/** Filter: by stock range */
|
||||
const handleStockRange = () => {
|
||||
const handleStockFilterUpdate = (filters: StockFilterData) => {
|
||||
setFilters(draft => {
|
||||
return {
|
||||
...draft,
|
||||
stock_type: buildStockOptions()[getValues('stock_type')].label,
|
||||
stock_from: getValues('stock_from'),
|
||||
stock_to: getValues('stock_to')
|
||||
...filters
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/** 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 */
|
||||
const handleSorting = (option: selectOption) => {
|
||||
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 (
|
||||
<div className='products'>
|
||||
<header>
|
||||
@ -259,83 +189,19 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
|
||||
</div>
|
||||
</header>
|
||||
<div className='accordion'>
|
||||
<AccordionItem id={0}
|
||||
isOpen={accordion[0]}
|
||||
onChange={handleAccordion}
|
||||
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>
|
||||
<CategoriesFilter onError={onError}
|
||||
onApplyFilters={handleCategoriesFilterUpdate}
|
||||
currentFilters={filters.categories} />
|
||||
|
||||
<AccordionItem id={1}
|
||||
isOpen={accordion[1]}
|
||||
onChange={handleAccordion}
|
||||
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>
|
||||
<MachinesFilter onError={onError}
|
||||
onApplyFilters={handleMachinesFilterUpdate}
|
||||
currentFilters={filters.machines} />
|
||||
|
||||
<AccordionItem id={2}
|
||||
isOpen={accordion[2]}
|
||||
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>
|
||||
<KeywordFilter onApplyFilters={keyword => handleKeywordFilterUpdate([...filters.keywords, keyword])}
|
||||
currentFilters={filters.keywords[0]} />
|
||||
|
||||
<AccordionItem id={3}
|
||||
isOpen={accordion[3]}
|
||||
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>
|
||||
<StockFilter onApplyFilters={handleStockFilterUpdate}
|
||||
currentFilters={filters} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='store-list'>
|
||||
@ -350,13 +216,19 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
|
||||
{features.categories.map(c => (
|
||||
<div key={c.id} className='features-item'>
|
||||
<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>
|
||||
))}
|
||||
{features.machines.map(m => (
|
||||
<div key={m.value} className='features-item'>
|
||||
<p>{m.label}</p>
|
||||
<button onClick={() => handleSelectMachine(m, false, true)}><X size={16} weight="light" /></button>
|
||||
<div key={m.id} className='features-item'>
|
||||
<p>{m.name}</p>
|
||||
<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>
|
||||
@ -389,19 +261,9 @@ const ProductsWrapper: React.FC<ProductsProps> = ({ 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 {
|
||||
categories: ProductCategory[],
|
||||
machines: checklistOption[],
|
||||
machines: Machine[],
|
||||
keywords: string[],
|
||||
stock_type: 'internal' | 'external',
|
||||
stock_from: number,
|
||||
|
@ -13,7 +13,7 @@ import { StoreProductItem } from './store-product-item';
|
||||
import useCart from '../../hooks/use-cart';
|
||||
import { User } from '../../models/user';
|
||||
import { Order } from '../../models/order';
|
||||
import { AccordionItem } from './accordion-item';
|
||||
import { AccordionItem } from '../base/accordion-item';
|
||||
import { StoreListHeader } from './store-list-header';
|
||||
import { FabPagination } from '../base/fab-pagination';
|
||||
|
||||
|
@ -77,7 +77,7 @@
|
||||
* { opacity: 0; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
header {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
@ -98,7 +98,7 @@
|
||||
align-items: stretch;
|
||||
transition: max-height 500ms ease-in-out;
|
||||
* { transition: opacity 250ms ease-in-out 300ms; }
|
||||
|
||||
|
||||
.group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -111,7 +111,7 @@
|
||||
&:hover { background-color: var(--gray-soft-light); }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
& > label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -158,4 +158,4 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1933,6 +1933,22 @@ en:
|
||||
save: "Save"
|
||||
required: "This field is required"
|
||||
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:
|
||||
unexpected_error_occurred: "An unexpected error occurred. Please try again later."
|
||||
all_products: "All products"
|
||||
@ -1941,15 +1957,6 @@ en:
|
||||
unable_to_delete: "Unable to delete the product: "
|
||||
filter: "Filter"
|
||||
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:
|
||||
name_az: "A-Z"
|
||||
name_za: "Z-A"
|
||||
|
Loading…
x
Reference in New Issue
Block a user