diff --git a/app/frontend/src/javascript/components/cart/store-cart.tsx b/app/frontend/src/javascript/components/cart/store-cart.tsx index fac4abb2f..5e81a0f6d 100644 --- a/app/frontend/src/javascript/components/cart/store-cart.tsx +++ b/app/frontend/src/javascript/components/cart/store-cart.tsx @@ -16,6 +16,7 @@ import { CouponInput } from '../coupon/coupon-input'; import { Coupon } from '../../models/coupon'; import { computePriceWithCoupon } from '../../lib/coupon'; import noImage from '../../../../images/no_image.png'; +import Switch from 'react-switch'; declare const Application: IApplication; @@ -112,6 +113,13 @@ const StoreCart: React.FC = ({ onSuccess, onError, currentUser, return cart && cart.order_items_attributes.length === 0; }; + /** + * Toggle product offer + */ + const onSwitch = (product, checked: boolean) => { + console.log('Offer ', product.orderable_name, ': ', checked); + }; + /** * Apply coupon to current cart */ @@ -162,6 +170,21 @@ const StoreCart: React.FC = ({ onSuccess, onError, currentUser, + {isPrivileged() && +
+ +
+ } ))} diff --git a/app/frontend/src/javascript/components/form/form-switch.tsx b/app/frontend/src/javascript/components/form/form-switch.tsx index 2deda376c..0d6979cec 100644 --- a/app/frontend/src/javascript/components/form/form-switch.tsx +++ b/app/frontend/src/javascript/components/form/form-switch.tsx @@ -41,8 +41,11 @@ export const FormSwitch = ({ id, label, t onChangeCb(val); }} checked={value as boolean || false} - height={19} width={40} + height={19} + uncheckedIcon={false} + checkedIcon={false} + handleDiameter={15} ref={ref} disabled={typeof disabled === 'function' ? disabled(id) : disabled} /> } /> diff --git a/app/frontend/src/javascript/components/store/orders.tsx b/app/frontend/src/javascript/components/store/orders.tsx index 25d0a0ea3..bbd6d8829 100644 --- a/app/frontend/src/javascript/components/store/orders.tsx +++ b/app/frontend/src/javascript/components/store/orders.tsx @@ -9,6 +9,7 @@ import { StoreListHeader } from './store-list-header'; import { AccordionItem } from './accordion-item'; import { OrderItem } from './order-item'; import { MemberSelect } from '../user/member-select'; +import { FabInput } from '../base/fab-input'; declare const Application: IApplication; diff --git a/app/frontend/src/javascript/components/store/product-form.tsx b/app/frontend/src/javascript/components/store/product-form.tsx index 503eb9323..62b25e874 100644 --- a/app/frontend/src/javascript/components/store/product-form.tsx +++ b/app/frontend/src/javascript/components/store/product-form.tsx @@ -18,6 +18,7 @@ import ProductCategoryAPI from '../../api/product-category'; import MachineAPI from '../../api/machine'; import ProductAPI from '../../api/product'; import { Plus } from 'phosphor-react'; +import { ProductStockForm } from './product-stock-form'; interface ProductFormProps { product: Product, @@ -48,6 +49,7 @@ export const ProductForm: React.FC = ({ product, title, onSucc const [isActivePrice, setIsActivePrice] = useState(product.id && _.isFinite(product.amount) && product.amount > 0); const [productCategories, setProductCategories] = useState([]); const [machines, setMachines] = useState([]); + const [stockTab, setStockTab] = useState(false); useEffect(() => { ProductCategoryAPI.index().then(data => { @@ -235,174 +237,183 @@ export const ProductForm: React.FC = ({ product, title, onSucc
-
- - +
+

setStockTab(false)}>{t('app.admin.store.product_form.product_parameters')}

+

setStockTab(true)}>{t('app.admin.store.product_form.stock_management')}

-
- - -
- -
- -
-
-

{t('app.admin.store.product_form.price_and_rule_of_selling_product')}

- -
- {isActivePrice &&
- + :
+
+ - + -
} -
- -
- -
-

{t('app.admin.store.product_form.product_images')}

- - - -
-
- {output.product_images_attributes.map((image, i) => ( - - ))} + label={t('app.admin.store.product_form.sku')} + className="span-3" /> +
+
+ +
- }> - {t('app.admin.store.product_form.add_product_image')} - -
-
-
+
-
-

{t('app.admin.store.product_form.assigning_category')}

- - - - -
+
+
+

{t('app.admin.store.product_form.price_and_rule_of_selling_product')}

+ +
+ {isActivePrice &&
+ + +
} +
-
+
-
-

{t('app.admin.store.product_form.assigning_machines')}

- - - - +

{t('app.admin.store.product_form.product_images')}

+ + + +
+
+ {output.product_images_attributes.map((image, i) => ( + + ))} +
+ }> + {t('app.admin.store.product_form.add_product_image')} + +
+
+ +
+ +
+

{t('app.admin.store.product_form.assigning_category')}

+ + + + -
- -
- -
-

{t('app.admin.store.product_form.product_description')}

- - - - -
- -
- -
-

{t('app.admin.store.product_form.product_files')}

- - - -
-
- {output.product_files_attributes.map((file, i) => ( - - ))} + id="product_category_id" + formState={formState} + label={t('app.admin.store.product_form.linking_product_to_category')} />
- }> - {t('app.admin.store.product_form.add_product_file')} - -
-
-
- {t('app.admin.store.product_form.save')} -
+
+ +
+

{t('app.admin.store.product_form.assigning_machines')}

+ + + + +
+ +
+ +
+

{t('app.admin.store.product_form.product_description')}

+ + + + +
+ +
+ +
+

{t('app.admin.store.product_form.product_files')}

+ + + +
+
+ {output.product_files_attributes.map((file, i) => ( + + ))} +
+ }> + {t('app.admin.store.product_form.add_product_file')} + +
+
+ +
+ {t('app.admin.store.product_form.save')} +
+ + } ); diff --git a/app/frontend/src/javascript/components/store/product-stock-form.tsx b/app/frontend/src/javascript/components/store/product-stock-form.tsx new file mode 100644 index 000000000..d2eb94d07 --- /dev/null +++ b/app/frontend/src/javascript/components/store/product-stock-form.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { Product } from '../../models/product'; +import { useTranslation } from 'react-i18next'; +import { Control } from 'react-hook-form'; +import { FormSwitch } from '../form/form-switch'; + +interface ProductStockFormProps { + product: Product, + control: Control, + onSuccess: (product: Product) => void, + onError: (message: string) => void, +} + +/** + * Form tab to manage product's stock + */ +export const ProductStockForm: React.FC = ({ product, control, onError, onSuccess }) => { + const { t } = useTranslation('admin'); + + /** + * Toggle stock threshold + */ + const toggleStockThreshold = (checked: boolean) => { + console.log('Stock threshold:', checked); + }; + + return ( +
+

Stock à jour 00/00/0000 - 00H30

+
+
+ +
+

{t('app.admin.store.product_stock_form.low_stock_threshold')}

+ +
+
+ ); +}; diff --git a/app/frontend/src/javascript/components/store/products.tsx b/app/frontend/src/javascript/components/store/products.tsx index 8c2da31fa..c1ab15dad 100644 --- a/app/frontend/src/javascript/components/store/products.tsx +++ b/app/frontend/src/javascript/components/store/products.tsx @@ -251,7 +251,7 @@ const Products: React.FC = ({ onSuccess, onError }) => { ))}
- {t('app.admin.store.products.filter_apply')} + setUpdate(true)} className="is-info">{t('app.admin.store.products.filter_apply')}
@@ -269,7 +269,7 @@ const Products: React.FC = ({ onSuccess, onError }) => { ))} - {t('app.admin.store.products.filter_apply')} + setUpdate(true)} className="is-info">{t('app.admin.store.products.filter_apply')} diff --git a/app/frontend/src/javascript/components/store/store.tsx b/app/frontend/src/javascript/components/store/store.tsx index ca4dd12fd..a5d140d29 100644 --- a/app/frontend/src/javascript/components/store/store.tsx +++ b/app/frontend/src/javascript/components/store/store.tsx @@ -13,6 +13,7 @@ import { StoreProductItem } from './store-product-item'; import useCart from '../../hooks/use-cart'; import { emitCustomEvent } from 'react-custom-events'; import { User } from '../../models/user'; +import { Order } from '../../models/order'; import { AccordionItem } from './accordion-item'; import { StoreListHeader } from './store-list-header'; @@ -20,6 +21,7 @@ declare const Application: IApplication; interface StoreProps { onError: (message: string) => void, + onSuccess: (message: string) => void, currentUser: User, } /** @@ -31,7 +33,7 @@ interface StoreProps { /** * This component shows public store */ -const Store: React.FC = ({ onError, currentUser }) => { +const Store: React.FC = ({ onError, onSuccess, currentUser }) => { const { t } = useTranslation('public'); const { cart, setCart } = useCart(currentUser); @@ -138,6 +140,14 @@ const Store: React.FC = ({ onError, currentUser }) => { console.log('Display in stock only:', checked); }; + /** + * Add product to the cart + */ + const addToCart = (cart: Order) => { + setCart(cart); + onSuccess(t('app.public.store.add_to_cart_success')); + }; + return (
    @@ -224,7 +234,7 @@ const Store: React.FC = ({ onError, currentUser }) => { />
    {products.map((product) => ( - + ))}
@@ -253,7 +263,7 @@ const StoreWrapper: React.FC = (props) => { ); }; -Application.Components.component('store', react2angular(StoreWrapper, ['onError', 'currentUser'])); +Application.Components.component('store', react2angular(StoreWrapper, ['onError', 'onSuccess', 'currentUser'])); interface ActiveCategory { id: number, diff --git a/app/frontend/src/stylesheets/modules/cart/store-cart.scss b/app/frontend/src/stylesheets/modules/cart/store-cart.scss index 58247baff..add2e1e7c 100644 --- a/app/frontend/src/stylesheets/modules/cart/store-cart.scss +++ b/app/frontend/src/stylesheets/modules/cart/store-cart.scss @@ -55,6 +55,24 @@ background-color: var(--gray-soft-light); border-radius: var(--border-radius); } + .offer { + align-self: stretch; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.8rem 1.6rem; + border: 1px solid var(--gray-soft-dark); + border-radius: var(--border-radius); + label { + display: flex; + justify-content: space-between; + align-items: center; + margin: 0; + @include text-base; + cursor: pointer; + span { margin-right: 0.8rem; } + } + } .price, .total { min-width: 10rem; @@ -158,7 +176,9 @@ justify-content: center; text-transform: uppercase; &:hover { + color: var(--gray-soft-lightest); opacity: 0.75; + cursor: pointer; } } } diff --git a/app/frontend/src/stylesheets/modules/form/abstract-form-item.scss b/app/frontend/src/stylesheets/modules/form/abstract-form-item.scss index dd242cf0b..55abc566a 100644 --- a/app/frontend/src/stylesheets/modules/form/abstract-form-item.scss +++ b/app/frontend/src/stylesheets/modules/form/abstract-form-item.scss @@ -32,7 +32,7 @@ background-color: var(--information-lightest); color: var(--information); border: 1px solid var(--information); - border-radius: 8px; + border-radius: var(--border-radius); font-size: 14px; font-weight: normal; line-height: 1.2em; diff --git a/app/frontend/src/stylesheets/modules/store/product-form.scss b/app/frontend/src/stylesheets/modules/store/product-form.scss index 5e1686e07..d94ecdfc4 100644 --- a/app/frontend/src/stylesheets/modules/store/product-form.scss +++ b/app/frontend/src/stylesheets/modules/store/product-form.scss @@ -1,5 +1,26 @@ .product-form { grid-column: 2 / -2; + + .tabs { + display: flex; + justify-content: space-between; + p { + flex: 1; + margin-bottom: 4rem; + padding: 0.8rem; + text-align: center; + color: var(--main); + border-bottom: 1px solid var(--gray-soft-dark); + &.is-active { + color: var(--gray-hard-dark); + border: 1px solid var(--gray-soft-dark); + border-bottom: none; + border-radius: var(--border-radius-sm) var(--border-radius-sm) 0 0; + } + &:hover { cursor: pointer; } + } + } + h4 { margin: 0 0 2.4rem; @include title-base; @@ -30,7 +51,7 @@ } } - .price-data-header { + .header-switch { @include grid-col(10); gap: 3.2rem; align-items: center; diff --git a/app/frontend/src/stylesheets/modules/store/store-filters.scss b/app/frontend/src/stylesheets/modules/store/store-filters.scss index 0614518d2..6dbd308aa 100644 --- a/app/frontend/src/stylesheets/modules/store/store-filters.scss +++ b/app/frontend/src/stylesheets/modules/store/store-filters.scss @@ -123,6 +123,18 @@ } } + input[type="text"] { + width: 100%; + min-height: 4rem; + padding: 0 0.8rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + border: 1px solid var(--gray-soft-dark); + border-radius: var(--border-radius-sm); + @include text-base; + } + button { opacity: 100; margin-top: 0.8rem; diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index fbb668217..32c24f5a0 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -1964,6 +1964,8 @@ en: edit_product: successfully_updated: "The product has been updated." product_form: + product_parameters: "Product parameters" + stock_management: "Stock management" name: "Name of product" sku: "Reference product (SKU)" slug: "Name of URL" @@ -1987,6 +1989,9 @@ en: product_images_info: "Advice
We advise you to use a square format, jpg or png, for jpgs, please use white for the background colour. The main visual will be the visual presented first in the product sheet." add_product_image: "Add an image" save: "Save" + product_stock_form: + low_stock_threshold: "Define a low stock threshold" + toggle_stock_threshold: "Activate stock threshold" orders: heading: "Orders" create_order: "Create an order" diff --git a/config/locales/app.public.en.yml b/config/locales/app.public.en.yml index 5c1a49f70..4c0c5e4cd 100644 --- a/config/locales/app.public.en.yml +++ b/config/locales/app.public.en.yml @@ -383,6 +383,7 @@ en: store: fablab_store: "FabLab Store" unexpected_error_occurred: "An unexpected error occurred. Please try again later." + add_to_cart_success: "Product added to the cart." products: all_products: "All the products" filter: "Filter"