mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-01-18 07:52:23 +01:00
(feat) save/restore filters in admin/store/products
This commit is contained in:
parent
af81f10a4e
commit
75a4038a60
@ -4,7 +4,14 @@ import { useTranslation } from 'react-i18next';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { Loader } from '../base/loader';
|
||||
import { IApplication } from '../../models/application';
|
||||
import { Product, ProductIndexFilter, ProductsIndex, ProductSortOption } from '../../models/product';
|
||||
import {
|
||||
initialFilters, initialResources,
|
||||
Product,
|
||||
ProductIndexFilter,
|
||||
ProductResourcesFetching,
|
||||
ProductsIndex,
|
||||
ProductSortOption
|
||||
} from '../../models/product';
|
||||
import { ProductCategory } from '../../models/product-category';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { ProductItem } from './product-item';
|
||||
@ -16,15 +23,17 @@ import { Machine } from '../../models/machine';
|
||||
import { MachinesFilter } from './filters/machines-filter';
|
||||
import { KeywordFilter } from './filters/keyword-filter';
|
||||
import { StockFilter } from './filters/stock-filter';
|
||||
import ProductCategoryAPI from '../../api/product-category';
|
||||
import ProductLib, { initFilters } from '../../lib/product';
|
||||
import ProductLib from '../../lib/product';
|
||||
import { ActiveFiltersTags } from './filters/active-filters-tags';
|
||||
import SettingAPI from '../../api/setting';
|
||||
import { UIRouter } from '@uirouter/angularjs';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
interface ProductsProps {
|
||||
onSuccess: (message: string) => void,
|
||||
onError: (message: string) => void,
|
||||
uiRouter: UIRouter,
|
||||
}
|
||||
/**
|
||||
* Option format, expected by react-select
|
||||
@ -33,35 +42,52 @@ interface ProductsProps {
|
||||
type selectOption = { value: ProductSortOption, label: string };
|
||||
|
||||
/** This component shows the admin view of the store */
|
||||
const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
|
||||
const Products: React.FC<ProductsProps> = ({ onSuccess, onError, uiRouter }) => {
|
||||
const { t } = useTranslation('admin');
|
||||
|
||||
const [productCategories, setProductCategories] = useState<Array<ProductCategory>>([]);
|
||||
const [productsList, setProductList] = useState<Array<Product>>([]);
|
||||
const [filters, setFilters] = useImmer<ProductIndexFilter>(initFilters);
|
||||
// this includes the resources fetch from the API (machines, categories) and from the URL (filters)
|
||||
const [resources, setResources] = useImmer<ProductResourcesFetching>(initialResources);
|
||||
const [machinesModule, setMachinesModule] = useState<boolean>(false);
|
||||
const [pageCount, setPageCount] = useState<number>(0);
|
||||
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||
const [productsCount, setProductsCount] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProducts().then(scrollToProducts);
|
||||
ProductCategoryAPI.index().then(data => {
|
||||
setProductCategories(ProductLib.sortCategories(data));
|
||||
ProductLib.fetchInitialResources(setResources, onError);
|
||||
SettingAPI.get('machines_module').then(data => {
|
||||
setMachinesModule(data.value === 'true');
|
||||
}).catch(onError);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProducts().then(scrollToProducts);
|
||||
}, [filters]);
|
||||
if (resources.filters.ready) {
|
||||
fetchProducts().then(scrollToProducts);
|
||||
uiRouter.stateService.transitionTo(uiRouter.globals.current, ProductLib.indexFiltersToRouterParams(resources.filters.data));
|
||||
}
|
||||
}, [resources.filters]);
|
||||
|
||||
useEffect(() => {
|
||||
if (resources.machines.ready && resources.categories.ready) {
|
||||
setResources(draft => {
|
||||
return {
|
||||
...draft,
|
||||
filters: {
|
||||
data: ProductLib.readFiltersFromUrl(uiRouter.globals.params, resources.machines.data, resources.categories.data),
|
||||
ready: true
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}, [resources.machines, resources.categories]);
|
||||
|
||||
/**
|
||||
* Handle products pagination
|
||||
*/
|
||||
const handlePagination = (page: number) => {
|
||||
if (page !== currentPage) {
|
||||
setFilters(draft => {
|
||||
return { ...draft, page };
|
||||
});
|
||||
ProductLib.updateFilter(setResources, 'page', page);
|
||||
}
|
||||
};
|
||||
|
||||
@ -70,7 +96,7 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
|
||||
*/
|
||||
const fetchProducts = async (): Promise<ProductsIndex> => {
|
||||
try {
|
||||
const data = await ProductAPI.index(filters);
|
||||
const data = await ProductAPI.index(resources.filters.data);
|
||||
setCurrentPage(data.page);
|
||||
setProductList(data.data);
|
||||
setPageCount(data.total_pages);
|
||||
@ -112,25 +138,21 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
|
||||
|
||||
/** Filter: toggle non-available products visibility */
|
||||
const toggleVisible = (checked: boolean) => {
|
||||
setFilters(draft => {
|
||||
return { ...draft, is_active: checked };
|
||||
});
|
||||
ProductLib.updateFilter(setResources, 'is_active', checked);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the list of applied filters with the given categories
|
||||
*/
|
||||
const handleCategoriesFilterUpdate = (categories: Array<ProductCategory>) => {
|
||||
setFilters(draft => {
|
||||
return { ...draft, categories };
|
||||
});
|
||||
ProductLib.updateFilter(setResources, 'categories', categories);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove the provided category from the filters selection
|
||||
*/
|
||||
const handleRemoveCategory = (category: ProductCategory) => {
|
||||
const list = ProductLib.categoriesSelectionTree(productCategories, filters.categories, category, 'remove');
|
||||
const list = ProductLib.categoriesSelectionTree(resources.categories.data, resources.filters.data.categories, category, 'remove');
|
||||
handleCategoriesFilterUpdate(list);
|
||||
};
|
||||
|
||||
@ -138,43 +160,33 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
|
||||
* Update the list of applied filters with the given machines
|
||||
*/
|
||||
const handleMachinesFilterUpdate = (machines: Array<Machine>) => {
|
||||
setFilters(draft => {
|
||||
return { ...draft, machines };
|
||||
});
|
||||
ProductLib.updateFilter(setResources, 'machines', machines);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the list of applied filters with the given keywords (or reference)
|
||||
*/
|
||||
const handleKeywordFilterUpdate = (keywords: Array<string>) => {
|
||||
setFilters(draft => {
|
||||
return { ...draft, keywords };
|
||||
});
|
||||
ProductLib.updateFilter(setResources, 'keywords', keywords);
|
||||
};
|
||||
|
||||
/** Filter: by stock range */
|
||||
const handleStockFilterUpdate = (filters: ProductIndexFilter) => {
|
||||
setFilters(draft => {
|
||||
return {
|
||||
...draft,
|
||||
...filters
|
||||
};
|
||||
setResources(draft => {
|
||||
return { ...draft, filters: { ...draft.filters, data: { ...draft.filters.data, ...filters } } };
|
||||
});
|
||||
};
|
||||
|
||||
/** Display option: sorting */
|
||||
const handleSorting = (option: selectOption) => {
|
||||
setFilters(draft => {
|
||||
return {
|
||||
...draft,
|
||||
sort: option.value
|
||||
};
|
||||
});
|
||||
ProductLib.updateFilter(setResources, 'sort', option.value);
|
||||
};
|
||||
|
||||
/** Clear filters */
|
||||
const clearAllFilters = () => {
|
||||
setFilters(initFilters);
|
||||
setResources(draft => {
|
||||
return { ...draft, filters: { ...draft.filters, data: initialFilters } };
|
||||
});
|
||||
};
|
||||
|
||||
/** Creates sorting options to the react-select format */
|
||||
@ -203,19 +215,20 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
|
||||
</div>
|
||||
</header>
|
||||
<div className='accordion'>
|
||||
<CategoriesFilter productCategories={productCategories}
|
||||
<CategoriesFilter productCategories={resources.categories.data}
|
||||
onApplyFilters={handleCategoriesFilterUpdate}
|
||||
currentFilters={filters.categories} />
|
||||
currentFilters={resources.filters.data.categories} />
|
||||
|
||||
<MachinesFilter onError={onError}
|
||||
onApplyFilters={handleMachinesFilterUpdate}
|
||||
currentFilters={filters.machines} />
|
||||
{machinesModule && <MachinesFilter onError={onError}
|
||||
allMachines={resources.machines.data}
|
||||
onApplyFilters={handleMachinesFilterUpdate}
|
||||
currentFilters={resources.filters.data.machines} />}
|
||||
|
||||
<KeywordFilter onApplyFilters={keyword => handleKeywordFilterUpdate([keyword])}
|
||||
currentFilters={filters.keywords[0]} />
|
||||
currentFilters={resources.filters.data.keywords[0]} />
|
||||
|
||||
<StockFilter onApplyFilters={handleStockFilterUpdate}
|
||||
currentFilters={filters} />
|
||||
currentFilters={resources.filters.data} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='store-list'>
|
||||
@ -223,13 +236,14 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
|
||||
productsCount={productsCount}
|
||||
selectOptions={buildSortOptions()}
|
||||
onSelectOptionsChange={handleSorting}
|
||||
switchChecked={filters.is_active}
|
||||
selectValue={resources.filters.data.sort}
|
||||
switchChecked={resources.filters.data.is_active}
|
||||
onSwitch={toggleVisible}
|
||||
/>
|
||||
<div className='features'>
|
||||
<ActiveFiltersTags filters={filters}
|
||||
<ActiveFiltersTags filters={resources.filters.data}
|
||||
onRemoveCategory={handleRemoveCategory}
|
||||
onRemoveMachine={(m) => handleMachinesFilterUpdate(filters.machines.filter(machine => machine !== m))}
|
||||
onRemoveMachine={(m) => handleMachinesFilterUpdate(resources.filters.data.machines.filter(machine => machine !== m))}
|
||||
onRemoveKeyword={() => handleKeywordFilterUpdate([])}
|
||||
onRemoveStock={() => handleStockFilterUpdate({ stock_type: 'internal', stock_to: 0, stock_from: 0 })} />
|
||||
</div>
|
||||
@ -252,12 +266,12 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const ProductsWrapper: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
|
||||
const ProductsWrapper: React.FC<ProductsProps> = (props) => {
|
||||
return (
|
||||
<Loader>
|
||||
<Products onSuccess={onSuccess} onError={onError} />
|
||||
<Products {...props} />
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
Application.Components.component('products', react2angular(ProductsWrapper, ['onSuccess', 'onError']));
|
||||
Application.Components.component('products', react2angular(ProductsWrapper, ['onSuccess', 'onError', 'uiRouter']));
|
||||
|
@ -4,10 +4,16 @@ import { react2angular } from 'react2angular';
|
||||
import { Loader } from '../base/loader';
|
||||
import { IApplication } from '../../models/application';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { Product, ProductIndexFilter, ProductsIndex, ProductSortOption } from '../../models/product';
|
||||
import {
|
||||
initialFilters,
|
||||
initialResources,
|
||||
Product,
|
||||
ProductResourcesFetching,
|
||||
ProductsIndex,
|
||||
ProductSortOption
|
||||
} from '../../models/product';
|
||||
import { ProductCategory } from '../../models/product-category';
|
||||
import ProductAPI from '../../api/product';
|
||||
import ProductCategoryAPI from '../../api/product-category';
|
||||
import { StoreProductItem } from './store-product-item';
|
||||
import useCart from '../../hooks/use-cart';
|
||||
import { User } from '../../models/user';
|
||||
@ -19,11 +25,9 @@ import { useImmer } from 'use-immer';
|
||||
import { Machine } from '../../models/machine';
|
||||
import { KeywordFilter } from './filters/keyword-filter';
|
||||
import { ActiveFiltersTags } from './filters/active-filters-tags';
|
||||
import ProductLib, { initFilters } from '../../lib/product';
|
||||
import ProductLib from '../../lib/product';
|
||||
import { UIRouter } from '@uirouter/angularjs';
|
||||
import MachineAPI from '../../api/machine';
|
||||
import SettingAPI from '../../api/setting';
|
||||
import { ApiResource } from '../../models/api';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
@ -49,7 +53,7 @@ const Store: React.FC<StoreProps> = ({ onError, onSuccess, currentUser, uiRouter
|
||||
|
||||
const [products, setProducts] = useState<Array<Product>>([]);
|
||||
// this includes the resources fetch from the API (machines, categories) and from the URL (filters)
|
||||
const [resources, setResources] = useImmer<FetchResources>(initialResources);
|
||||
const [resources, setResources] = useImmer<ProductResourcesFetching>(initialResources);
|
||||
const [machinesModule, setMachinesModule] = useState<boolean>(false);
|
||||
const [categoriesTree, setCategoriesTree] = useState<CategoryTree[]>([]);
|
||||
const [pageCount, setPageCount] = useState<number>(0);
|
||||
@ -58,31 +62,7 @@ const Store: React.FC<StoreProps> = ({ onError, onSuccess, currentUser, uiRouter
|
||||
|
||||
useEffect(() => {
|
||||
fetchProducts().then(scrollToProducts);
|
||||
ProductCategoryAPI.index().then(data => {
|
||||
setResources(draft => {
|
||||
return {
|
||||
...draft,
|
||||
categories: {
|
||||
data,
|
||||
ready: true
|
||||
}
|
||||
};
|
||||
});
|
||||
formatCategories(data);
|
||||
}).catch(error => {
|
||||
onError(t('app.public.store.unexpected_error_occurred') + error);
|
||||
});
|
||||
MachineAPI.index({ disabled: false }).then(data => {
|
||||
setResources(draft => {
|
||||
return {
|
||||
...draft,
|
||||
machines: {
|
||||
data,
|
||||
ready: true
|
||||
}
|
||||
};
|
||||
});
|
||||
}).catch(onError);
|
||||
ProductLib.fetchInitialResources(setResources, onError, formatCategories);
|
||||
SettingAPI.get('machines_module').then(data => {
|
||||
setMachinesModule(data.value === 'true');
|
||||
}).catch(onError);
|
||||
@ -128,56 +108,27 @@ const Store: React.FC<StoreProps> = ({ onError, onSuccess, currentUser, uiRouter
|
||||
* Filter by category: the selected category will always be first
|
||||
*/
|
||||
const filterCategory = (category: ProductCategory) => {
|
||||
setResources(draft => {
|
||||
return {
|
||||
...draft,
|
||||
filters: {
|
||||
...draft.filters,
|
||||
data: {
|
||||
...draft.filters.data,
|
||||
categories: category
|
||||
? Array.from(new Set([category, ...ProductLib.categoriesSelectionTree(resources.categories.data, [], category, 'add')]))
|
||||
: []
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
ProductLib.updateFilter(
|
||||
setResources,
|
||||
'categories',
|
||||
category
|
||||
? Array.from(new Set([category, ...ProductLib.categoriesSelectionTree(resources.categories.data, [], category, 'add')]))
|
||||
: []
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the list of applied filters with the given machines
|
||||
*/
|
||||
const applyMachineFilters = (machines: Array<Machine>) => {
|
||||
setResources(draft => {
|
||||
return {
|
||||
...draft,
|
||||
filters: {
|
||||
...draft.filters,
|
||||
data: {
|
||||
...draft.filters.data,
|
||||
machines
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
ProductLib.updateFilter(setResources, 'machines', machines);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the list of applied filters with the given keywords (or reference)
|
||||
*/
|
||||
const applyKeywordFilter = (keywords: Array<string>) => {
|
||||
setResources(draft => {
|
||||
return {
|
||||
...draft,
|
||||
filters: {
|
||||
...draft.filters,
|
||||
data: {
|
||||
...draft.filters.data,
|
||||
keywords
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
ProductLib.updateFilter(setResources, 'keywords', keywords);
|
||||
};
|
||||
/**
|
||||
* Clear filters
|
||||
@ -189,7 +140,7 @@ const Store: React.FC<StoreProps> = ({ onError, onSuccess, currentUser, uiRouter
|
||||
filters: {
|
||||
...draft.filters,
|
||||
data: {
|
||||
...initFilters,
|
||||
...initialFilters,
|
||||
categories: draft.filters.data.categories
|
||||
}
|
||||
}
|
||||
@ -212,36 +163,14 @@ const Store: React.FC<StoreProps> = ({ onError, onSuccess, currentUser, uiRouter
|
||||
* Display option: sorting
|
||||
*/
|
||||
const handleSorting = (option: selectOption) => {
|
||||
setResources(draft => {
|
||||
return {
|
||||
...draft,
|
||||
filters: {
|
||||
...draft.filters,
|
||||
data: {
|
||||
...draft.filters.data,
|
||||
sort: option.value
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
ProductLib.updateFilter(setResources, 'sort', option.value);
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter: toggle non-available products visibility
|
||||
*/
|
||||
const toggleVisible = (checked: boolean) => {
|
||||
setResources(draft => {
|
||||
return {
|
||||
...draft,
|
||||
filters: {
|
||||
...draft.filters,
|
||||
data: {
|
||||
...draft.filters.data,
|
||||
is_active: checked
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
ProductLib.updateFilter(setResources, 'is_active', checked);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -257,18 +186,7 @@ const Store: React.FC<StoreProps> = ({ onError, onSuccess, currentUser, uiRouter
|
||||
*/
|
||||
const handlePagination = (page: number) => {
|
||||
if (page !== currentPage) {
|
||||
setResources(draft => {
|
||||
return {
|
||||
...draft,
|
||||
filters: {
|
||||
...draft.filters,
|
||||
data: {
|
||||
...draft.filters.data,
|
||||
page
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
ProductLib.updateFilter(setResources, 'page', page);
|
||||
}
|
||||
};
|
||||
|
||||
@ -403,24 +321,3 @@ interface CategoryTree {
|
||||
parent: ProductCategory,
|
||||
children: ProductCategory[]
|
||||
}
|
||||
|
||||
interface FetchResources {
|
||||
machines: ApiResource<Array<Machine>>,
|
||||
categories: ApiResource<Array<ProductCategory>>,
|
||||
filters: ApiResource<ProductIndexFilter>
|
||||
}
|
||||
|
||||
const initialResources: FetchResources = {
|
||||
machines: {
|
||||
data: [],
|
||||
ready: false
|
||||
},
|
||||
categories: {
|
||||
data: [],
|
||||
ready: false
|
||||
},
|
||||
filters: {
|
||||
data: initFilters,
|
||||
ready: false
|
||||
}
|
||||
};
|
||||
|
@ -4,8 +4,8 @@
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
Application.Controllers.controller('AdminStoreController', ['$scope', 'CSRF', 'growl', '$state',
|
||||
function ($scope, CSRF, growl, $state) {
|
||||
Application.Controllers.controller('AdminStoreController', ['$scope', 'CSRF', 'growl', '$state', '$uiRouter',
|
||||
function ($scope, CSRF, growl, $state, $uiRouter) {
|
||||
/* PRIVATE SCOPE */
|
||||
// Map of tab state and index
|
||||
const TABS = {
|
||||
@ -21,6 +21,9 @@ Application.Controllers.controller('AdminStoreController', ['$scope', 'CSRF', 'g
|
||||
active: TABS[$state.current.name]
|
||||
};
|
||||
|
||||
// the following item is used by the Products component to save/restore filters in the URL
|
||||
$scope.uiRouter = $uiRouter;
|
||||
|
||||
/**
|
||||
* Callback triggered in click tab
|
||||
*/
|
||||
|
@ -4,7 +4,7 @@ Application.Controllers.controller('StoreController', ['$scope', 'CSRF', 'growl'
|
||||
function ($scope, CSRF, growl, $uiRouter) {
|
||||
/* PUBLIC SCOPE */
|
||||
|
||||
// the following item is used by the Store component to store the filters in te URL
|
||||
// the following item is used by the Store component to store the filters in the URL
|
||||
$scope.uiRouter = $uiRouter;
|
||||
|
||||
/**
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { ProductCategory } from '../models/product-category';
|
||||
import {
|
||||
initialFilters,
|
||||
ProductIndexFilter,
|
||||
ProductIndexFilterIds, ProductIndexFilterUrl,
|
||||
ProductIndexFilterIds, ProductIndexFilterUrl, ProductResourcesFetching,
|
||||
stockMovementInReasons,
|
||||
stockMovementOutReasons,
|
||||
StockMovementReason
|
||||
@ -9,6 +10,9 @@ import {
|
||||
import { Machine } from '../models/machine';
|
||||
import { StateParams } from '@uirouter/angularjs';
|
||||
import ParsingLib from './parsing';
|
||||
import ProductCategoryAPI from '../api/product-category';
|
||||
import MachineAPI from '../api/machine';
|
||||
import { Updater } from 'use-immer';
|
||||
|
||||
export default class ProductLib {
|
||||
/**
|
||||
@ -120,6 +124,7 @@ export default class ProductLib {
|
||||
return {
|
||||
...filters,
|
||||
machines: filters.machines?.map(m => m.slug),
|
||||
categories: filters.categories?.map(c => c.slug),
|
||||
category,
|
||||
categoryTypeUrl
|
||||
};
|
||||
@ -129,17 +134,20 @@ export default class ProductLib {
|
||||
* Parse the provided URL and return a ready-to-use filter object
|
||||
*/
|
||||
static readFiltersFromUrl = (params: StateParams, machines: Array<Machine>, categories: Array<ProductCategory>): ProductIndexFilter => {
|
||||
const res: ProductIndexFilter = { ...initFilters };
|
||||
const res: ProductIndexFilter = { ...initialFilters };
|
||||
for (const key in params) {
|
||||
if (['#', 'categoryTypeUrl'].includes(key) || !Object.prototype.hasOwnProperty.call(params, key)) continue;
|
||||
|
||||
const value = ParsingLib.parse(params[key]) || initFilters[key];
|
||||
const value = ParsingLib.parse(params[key]) || initialFilters[key];
|
||||
switch (key) {
|
||||
case 'category':
|
||||
const parents = categories?.filter(c => (value as Array<string>)?.includes(c.slug));
|
||||
// we may also add to the selection children categories
|
||||
res.categories = [...parents, ...categories?.filter(c => parents.map(c => c.id).includes(c.parent_id))];
|
||||
break;
|
||||
case 'categories':
|
||||
res.categories = [...categories?.filter(c => (value as Array<string>)?.includes(c.slug))];
|
||||
break;
|
||||
case 'machines':
|
||||
res.machines = machines?.filter(m => (value as Array<string>)?.includes(m.slug));
|
||||
break;
|
||||
@ -149,16 +157,41 @@ export default class ProductLib {
|
||||
}
|
||||
return res;
|
||||
};
|
||||
}
|
||||
|
||||
export const initFilters: ProductIndexFilter = {
|
||||
categories: [],
|
||||
keywords: [],
|
||||
machines: [],
|
||||
is_active: false,
|
||||
stock_type: 'internal',
|
||||
stock_from: 0,
|
||||
stock_to: 0,
|
||||
page: 1,
|
||||
sort: ''
|
||||
};
|
||||
/**
|
||||
* Fetch the initial ressources needed to initialise the store and its filters (categories and machines)
|
||||
*/
|
||||
static fetchInitialResources = (setResources: Updater<ProductResourcesFetching>, onError: (message: string) => void, onProductCategoryFetched?: (data: Array<ProductCategory>) => void) => {
|
||||
ProductCategoryAPI.index().then(data => {
|
||||
setResources(draft => {
|
||||
return { ...draft, categories: { data: ProductLib.sortCategories(data), ready: true } };
|
||||
});
|
||||
if (typeof onProductCategoryFetched === 'function') onProductCategoryFetched(data);
|
||||
}).catch(error => {
|
||||
onError(error);
|
||||
});
|
||||
MachineAPI.index({ disabled: false }).then(data => {
|
||||
setResources(draft => {
|
||||
return { ...draft, machines: { data, ready: true } };
|
||||
});
|
||||
}).catch(onError);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the given filter in memory with the new provided value
|
||||
*/
|
||||
static updateFilter = (setResources: Updater<ProductResourcesFetching>, key: keyof ProductIndexFilter, value: unknown): void => {
|
||||
setResources(draft => {
|
||||
return {
|
||||
...draft,
|
||||
filters: {
|
||||
...draft.filters,
|
||||
data: {
|
||||
...draft.filters.data,
|
||||
[key]: value
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { TDateISO } from '../typings/date-iso';
|
||||
import { ApiFilter, PaginatedIndex } from './api';
|
||||
import { ApiFilter, ApiResource, PaginatedIndex } from './api';
|
||||
import { ProductCategory } from './product-category';
|
||||
import { Machine } from './machine';
|
||||
|
||||
@ -26,8 +26,42 @@ export interface ProductIndexFilterUrl extends Omit<Omit<ProductIndexFilter, 'ca
|
||||
categoryTypeUrl?: 'c' | 'sc',
|
||||
category?: string,
|
||||
machines?: Array<string>,
|
||||
categories?: Array<string>,
|
||||
}
|
||||
|
||||
export interface ProductResourcesFetching {
|
||||
machines: ApiResource<Array<Machine>>,
|
||||
categories: ApiResource<Array<ProductCategory>>,
|
||||
filters: ApiResource<ProductIndexFilter>
|
||||
}
|
||||
|
||||
export const initialFilters: ProductIndexFilter = {
|
||||
categories: [],
|
||||
keywords: [],
|
||||
machines: [],
|
||||
is_active: false,
|
||||
stock_type: 'internal',
|
||||
stock_from: 0,
|
||||
stock_to: 0,
|
||||
page: 1,
|
||||
sort: ''
|
||||
};
|
||||
|
||||
export const initialResources: ProductResourcesFetching = {
|
||||
machines: {
|
||||
data: [],
|
||||
ready: false
|
||||
},
|
||||
categories: {
|
||||
data: [],
|
||||
ready: false
|
||||
},
|
||||
filters: {
|
||||
data: initialFilters,
|
||||
ready: false
|
||||
}
|
||||
};
|
||||
|
||||
export type StockType = 'internal' | 'external' | 'all';
|
||||
|
||||
export const stockMovementInReasons = ['inward_stock', 'returned', 'cancelled', 'inventory_fix', 'other_in'] as const;
|
||||
|
@ -628,42 +628,13 @@ angular.module('application.router', ['ui.router'])
|
||||
}
|
||||
},
|
||||
params: {
|
||||
categoryTypeUrl: {
|
||||
dynamic: true,
|
||||
raw: true,
|
||||
type: 'path',
|
||||
value: null,
|
||||
squash: true
|
||||
},
|
||||
category: {
|
||||
dynamic: true,
|
||||
type: 'path',
|
||||
raw: true,
|
||||
value: null,
|
||||
squash: true
|
||||
},
|
||||
machines: {
|
||||
array: true,
|
||||
dynamic: true,
|
||||
type: 'query',
|
||||
raw: true
|
||||
},
|
||||
keywords: {
|
||||
dynamic: true,
|
||||
type: 'query'
|
||||
},
|
||||
is_active: {
|
||||
dynamic: true,
|
||||
type: 'query'
|
||||
},
|
||||
page: {
|
||||
dynamic: true,
|
||||
type: 'query'
|
||||
},
|
||||
sort: {
|
||||
dynamic: true,
|
||||
type: 'query'
|
||||
}
|
||||
categoryTypeUrl: { dynamic: true, raw: true, type: 'path', value: null, squash: true },
|
||||
category: { dynamic: true, type: 'path', raw: true, value: null, squash: true },
|
||||
machines: { array: true, dynamic: true, type: 'query', raw: true },
|
||||
keywords: { dynamic: true, type: 'query' },
|
||||
is_active: { dynamic: true, type: 'query', value: 'false', squash: true },
|
||||
page: { dynamic: true, type: 'query', value: '1', squash: true },
|
||||
sort: { dynamic: true, type: 'query' }
|
||||
}
|
||||
})
|
||||
|
||||
@ -1220,12 +1191,23 @@ angular.module('application.router', ['ui.router'])
|
||||
})
|
||||
|
||||
.state('app.admin.store.products', {
|
||||
url: '/products',
|
||||
url: '/products?{categories:string}{machines:string}{keywords:string}{stock_type:string}{stock_from:string}{stock_to:string}{is_active:string}{page:string}{sort:string}',
|
||||
views: {
|
||||
'main@': {
|
||||
templateUrl: '/admin/store/index.html',
|
||||
controller: 'AdminStoreController'
|
||||
}
|
||||
},
|
||||
params: {
|
||||
categories: { array: true, dynamic: true, type: 'query', raw: true },
|
||||
machines: { array: true, dynamic: true, type: 'query', raw: true },
|
||||
keywords: { dynamic: true, type: 'query' },
|
||||
stock_type: { dynamic: true, type: 'query', value: 'internal', squash: true },
|
||||
stock_from: { dynamic: true, type: 'query', value: '0', squash: true },
|
||||
stock_to: { dynamic: true, type: 'query', value: '0', squash: true },
|
||||
is_active: { dynamic: true, type: 'query', value: 'false', squash: true },
|
||||
page: { dynamic: true, type: 'query', value: '1', squash: true },
|
||||
sort: { dynamic: true, type: 'query' }
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -1 +1 @@
|
||||
<products on-success="onSuccess" on-error="onError"/>
|
||||
<products on-success="onSuccess" on-error="onError" ui-router="uiRouter"/>
|
||||
|
@ -111,10 +111,12 @@ class ProductService
|
||||
end
|
||||
|
||||
def filter_by_stock(products, filters, operator)
|
||||
filters[:stock_type] = 'external' unless operator.privileged?
|
||||
return products if filters[:stock_type] == 'internal' && !operator.privileged?
|
||||
|
||||
products.where("(stock ->> '#{filters[:stock_type]}')::int >= #{filters[:stock_from]}") if filters[:stock_from].to_i.positive?
|
||||
products.where("(stock ->> '#{filters[:stock_type]}')::int <= #{filters[:stock_to]}") if filters[:stock_to].to_i.positive?
|
||||
if filters[:stock_from].to_i.positive?
|
||||
products = products.where('(stock ->> ?)::int >= ?', filters[:stock_type], filters[:stock_from])
|
||||
end
|
||||
products = products.where('(stock ->> ?)::int <= ?', filters[:stock_type], filters[:stock_to]) if filters[:stock_to].to_i.positive?
|
||||
|
||||
products
|
||||
end
|
||||
|
Loading…
x
Reference in New Issue
Block a user