diff --git a/app/frontend/src/javascript/components/store/orders.tsx b/app/frontend/src/javascript/components/store/orders.tsx index 2402b226c..39aea6646 100644 --- a/app/frontend/src/javascript/components/store/orders.tsx +++ b/app/frontend/src/javascript/components/store/orders.tsx @@ -204,13 +204,13 @@ const Orders: React.FC = ({ currentUser, onSuccess, onError }) => { >
-
- from +
- to
diff --git a/app/frontend/src/javascript/components/store/products.tsx b/app/frontend/src/javascript/components/store/products.tsx index 56ade82c2..1b26e08ef 100644 --- a/app/frontend/src/javascript/components/store/products.tsx +++ b/app/frontend/src/javascript/components/store/products.tsx @@ -6,6 +6,7 @@ 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'; @@ -16,6 +17,8 @@ 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'; declare const Application: IApplication; @@ -29,12 +32,12 @@ interface ProductsProps { */ type selectOption = { value: number, label: string }; -/** - * This component shows the admin view of the store - */ +/** This component shows the admin view of the store */ const Products: React.FC = ({ onSuccess, onError }) => { const { t } = useTranslation('admin'); + const { register, control, getValues } = useForm(); + const [filteredProductsList, setFilteredProductList] = useImmer>([]); const [features, setFeatures] = useImmer(initFilters); const [filterVisible, setFilterVisible] = useState(false); @@ -48,7 +51,7 @@ const Products: React.FC = ({ onSuccess, onError }) => { const [currentPage, setCurrentPage] = useState(1); useEffect(() => { - ProductAPI.index({ page: 1 }).then(data => { + ProductAPI.index({ page: 1, is_active: filterVisible }).then(data => { setPageCount(data.total_pages); setFilteredProductList(data.products); }); @@ -62,30 +65,32 @@ const Products: React.FC = ({ onSuccess, onError }) => { }).catch(onError); }, []); - useEffect(() => { - ProductAPI.index({ page: currentPage }).then(data => { - setFilteredProductList(data.products); - setPageCount(data.total_pages); - window.document.getElementById('content-main').scrollTo({ top: 100, behavior: 'smooth' }); - }); - }, [currentPage]); - useEffect(() => { applyFilters(); setClearFilters(false); setUpdate(false); }, [filterVisible, clearFilters, update === true]); - /** - * Goto edit product page - */ + /** Handle products pagination */ + const handlePagination = (page: number) => { + if (page !== currentPage) { + ProductAPI.index({ page, is_active: filterVisible }).then(data => { + setCurrentPage(page); + setFilteredProductList(data.products); + setPageCount(data.total_pages); + window.document.getElementById('content-main').scrollTo({ top: 100, behavior: 'smooth' }); + }).catch(() => { + onError(t('app.admin.store.products.unexpected_error_occurred')); + }); + } + }; + + /** Goto edit product page */ const editProduct = (product: Product) => { window.location.href = `/#!/admin/store/products/${product.id}/edit`; }; - /** - * Delete a product - */ + /** Delete a product */ const deleteProduct = async (productId: number): Promise => { try { await ProductAPI.destroy(productId); @@ -97,24 +102,18 @@ const Products: React.FC = ({ onSuccess, onError }) => { } }; - /** - * Goto new product page - */ + /** Goto new product page */ const newProduct = (): void => { window.location.href = '/#!/admin/store/products/new'; }; - /** - * Filter: toggle non-available products visibility - */ + /** Filter: toggle non-available products visibility */ const toggleVisible = (checked: boolean) => { setFilterVisible(!filterVisible); console.log('Display on the shelf product only:', checked); }; - /** - * Filter: by categories - */ + /** Filter: by categories */ const handleSelectCategory = (c: ProductCategory, checked: boolean, instantUpdate?: boolean) => { let list = [...filters.categories]; const children = productCategories @@ -151,9 +150,7 @@ const Products: React.FC = ({ onSuccess, onError }) => { } }; - /** - * Filter: by machines - */ + /** Filter: by machines */ const handleSelectMachine = (m: checklistOption, checked, instantUpdate?) => { const list = [...filters.machines]; checked @@ -167,16 +164,39 @@ const Products: React.FC = ({ onSuccess, onError }) => { } }; - /** - * Display option: sorting - */ + /** Filter: by keyword or ref */ + const handleKeyword = (evt: React.ChangeEvent) => { + setFilters(draft => { + return { ...draft, keywords: evt.target.value }; + }); + }; + + /** Filter: by stock range */ + const handleStockRange = () => { + setFilters(draft => { + return { + ...draft, + stock_type: buildStockOptions()[getValues('stock_type')].label, + stock_from: getValues('stock_from'), + stock_to: getValues('stock_to') + }; + }); + }; + + /** Creates sorting options to the react-select format */ + const buildStockOptions = (): Array => { + 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); }; - /** - * Apply filters - */ + /** Apply filters */ const applyFilters = () => { let tags = initFilters; @@ -192,19 +212,15 @@ const Products: React.FC = ({ onSuccess, onError }) => { console.log('Apply filters:', filters); }; - /** - * Clear filters - */ + /** Clear filters */ const clearAllFilters = () => { setFilters(initFilters); setClearFilters(true); console.log('Clear all filters'); }; - /** - * Creates sorting options to the react-select format - */ - const buildOptions = (): Array => { + /** Creates sorting options to the react-select format */ + const buildSortOptions = (): Array => { return [ { value: 0, label: t('app.admin.store.products.sort.name_az') }, { value: 1, label: t('app.admin.store.products.sort.name_za') }, @@ -213,9 +229,7 @@ const Products: React.FC = ({ onSuccess, onError }) => { ]; }; - /** - * Open/close accordion items - */ + /** Open/close accordion items */ const handleAccordion = (id, state) => { setAccordion({ ...accordion, [id]: state }); }; @@ -271,12 +285,54 @@ const Products: React.FC = ({ onSuccess, onError }) => { setUpdate(true)} className="is-info">{t('app.admin.store.products.filter_apply')}
+ + +
+
+ handleKeyword(event)} /> + setUpdate(true)} className="is-info">{t('app.admin.store.products.filter_apply')} +
+
+
+ + +
+
+ +
+ + +
+ {t('app.admin.store.products.filter_apply')} +
+
+
= ({ onSuccess, onError }) => { ))}
{pageCount > 1 && - + } @@ -324,45 +380,30 @@ const ProductsWrapper: React.FC = ({ onSuccess, onError }) => { Application.Components.component('products', react2angular(ProductsWrapper, ['onSuccess', 'onError'])); -/** - * Option format, expected by checklist - */ +/** Option format, expected by checklist */ type checklistOption = { value: number, label: string }; -/** - * Convert the provided array of items to the checklist format - */ +/** Convert the provided array of items to the checklist format */ const buildChecklistOptions = (items: Array<{ id?: number, name: string }>): Array => { return items.map(t => { return { value: t.id, label: t.name }; }); }; -interface Stock { - from: number, - to: number -} - interface Filters { - instant: boolean, categories: ProductCategory[], machines: checklistOption[], keywords: string[], - internalStock: Stock, - externalStock: Stock + stock_type: 'internal' | 'external', + stock_from: number, + stock_to: number } const initFilters: Filters = { - instant: false, categories: [], machines: [], keywords: [], - internalStock: { - from: 0, - to: null - }, - externalStock: { - from: 0, - to: null - } + stock_type: 'internal', + stock_from: 0, + stock_to: 0 }; diff --git a/app/frontend/src/stylesheets/modules/store/store-filters.scss b/app/frontend/src/stylesheets/modules/store/store-filters.scss index b8009054d..64135358d 100644 --- a/app/frontend/src/stylesheets/modules/store/store-filters.scss +++ b/app/frontend/src/stylesheets/modules/store/store-filters.scss @@ -103,17 +103,19 @@ display: flex; flex-direction: column; opacity: 1; - &.u-scrollbar { overflow: hidden auto; } + &.u-scrollbar { + overflow: hidden auto; + label { + margin: 0 0.8rem 0 0; + padding: 0.6rem; + &:hover { background-color: var(--gray-soft-light); } + } + } - label { - margin: 0 0.8rem 0 0; - padding: 0.6rem; + & > label { display: flex; align-items: center; - &:hover { - background-color: var(--gray-soft-light); - cursor: pointer; - } + cursor: pointer; input[type=checkbox] { margin: 0 0.8rem 0 0; } p { margin: 0; @@ -121,9 +123,11 @@ } &.offset { margin-left: 1.6rem; } } + .form-item-field { width: 100%; } } input[type="text"] { + width: 100%; width: 100%; min-height: 4rem; padding: 0 0.8rem; @@ -144,9 +148,14 @@ } } - .period { + .range { display: flex; justify-content: center; align-items: center; + gap: 1.6rem; + label { + margin: 0; + flex-direction: column; + } } } \ No newline at end of file diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index 2d76ee343..2be625154 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -1934,6 +1934,7 @@ en: required: "This field is required" slug_pattern: "Only lowercase alphanumeric groups of characters separated by an hyphen" products: + unexpected_error_occurred: "An unexpected error occurred. Please try again later." all_products: "All products" create_a_product: "Create a product" successfully_deleted: "The product has been successfully deleted" @@ -1945,6 +1946,8 @@ en: 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: @@ -2043,6 +2046,8 @@ en: filter_status: "By status" filter_client: "By client" filter_period: "By period" + filter_period_from: "From" + filter_period_to: "to" status: error: "Payment error" canceled: "Canceled"