1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-26 20:54:21 +01:00

stock (wip)

This commit is contained in:
vincent 2022-09-02 18:17:15 +02:00
parent a5a45ee1ce
commit f21a68593a
13 changed files with 314 additions and 163 deletions

View File

@ -16,6 +16,7 @@ import { CouponInput } from '../coupon/coupon-input';
import { Coupon } from '../../models/coupon'; import { Coupon } from '../../models/coupon';
import { computePriceWithCoupon } from '../../lib/coupon'; import { computePriceWithCoupon } from '../../lib/coupon';
import noImage from '../../../../images/no_image.png'; import noImage from '../../../../images/no_image.png';
import Switch from 'react-switch';
declare const Application: IApplication; declare const Application: IApplication;
@ -112,6 +113,13 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
return cart && cart.order_items_attributes.length === 0; 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 * Apply coupon to current cart
*/ */
@ -162,6 +170,21 @@ const StoreCart: React.FC<StoreCartProps> = ({ onSuccess, onError, currentUser,
<i className="fa fa-trash" /> <i className="fa fa-trash" />
</FabButton> </FabButton>
</div> </div>
{isPrivileged() &&
<div className='offer'>
<label>
<span>Offer the product</span>
<Switch
checked={item.is_offered}
onChange={(checked) => onSwitch(item, checked)}
width={40}
height={19}
uncheckedIcon={false}
checkedIcon={false}
handleDiameter={15} />
</label>
</div>
}
</article> </article>
))} ))}
</div> </div>

View File

@ -41,8 +41,11 @@ export const FormSwitch = <TFieldValues, TContext extends object>({ id, label, t
onChangeCb(val); onChangeCb(val);
}} }}
checked={value as boolean || false} checked={value as boolean || false}
height={19}
width={40} width={40}
height={19}
uncheckedIcon={false}
checkedIcon={false}
handleDiameter={15}
ref={ref} ref={ref}
disabled={typeof disabled === 'function' ? disabled(id) : disabled} /> disabled={typeof disabled === 'function' ? disabled(id) : disabled} />
} /> } />

View File

@ -9,6 +9,7 @@ import { StoreListHeader } from './store-list-header';
import { AccordionItem } from './accordion-item'; import { AccordionItem } from './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 { FabInput } from '../base/fab-input';
declare const Application: IApplication; declare const Application: IApplication;

View File

@ -18,6 +18,7 @@ import ProductCategoryAPI from '../../api/product-category';
import MachineAPI from '../../api/machine'; import MachineAPI from '../../api/machine';
import ProductAPI from '../../api/product'; import ProductAPI from '../../api/product';
import { Plus } from 'phosphor-react'; import { Plus } from 'phosphor-react';
import { ProductStockForm } from './product-stock-form';
interface ProductFormProps { interface ProductFormProps {
product: Product, product: Product,
@ -48,6 +49,7 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
const [isActivePrice, setIsActivePrice] = useState<boolean>(product.id && _.isFinite(product.amount) && product.amount > 0); const [isActivePrice, setIsActivePrice] = useState<boolean>(product.id && _.isFinite(product.amount) && product.amount > 0);
const [productCategories, setProductCategories] = useState<selectOption[]>([]); const [productCategories, setProductCategories] = useState<selectOption[]>([]);
const [machines, setMachines] = useState<checklistOption[]>([]); const [machines, setMachines] = useState<checklistOption[]>([]);
const [stockTab, setStockTab] = useState<boolean>(false);
useEffect(() => { useEffect(() => {
ProductCategoryAPI.index().then(data => { ProductCategoryAPI.index().then(data => {
@ -235,6 +237,13 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
</div> </div>
</header> </header>
<form className="product-form" onSubmit={onSubmit}> <form className="product-form" onSubmit={onSubmit}>
<div className='tabs'>
<p className={!stockTab ? 'is-active' : ''} onClick={() => setStockTab(false)}>{t('app.admin.store.product_form.product_parameters')}</p>
<p className={stockTab ? 'is-active' : ''} onClick={() => setStockTab(true)}>{t('app.admin.store.product_form.stock_management')}</p>
</div>
{stockTab
? <ProductStockForm product={product} control={control} onError={onError} onSuccess={onSuccess} />
: <section>
<div className="subgrid"> <div className="subgrid">
<FormInput id="name" <FormInput id="name"
register={register} register={register}
@ -267,7 +276,7 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
<hr /> <hr />
<div className="price-data"> <div className="price-data">
<div className="price-data-header"> <div className="header-switch">
<h4 className='span-7'>{t('app.admin.store.product_form.price_and_rule_of_selling_product')}</h4> <h4 className='span-7'>{t('app.admin.store.product_form.price_and_rule_of_selling_product')}</h4>
<FormSwitch control={control} <FormSwitch control={control}
id="is_active_price" id="is_active_price"
@ -403,6 +412,8 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
<div className="main-actions"> <div className="main-actions">
<FabButton type="submit" className="main-action-btn">{t('app.admin.store.product_form.save')}</FabButton> <FabButton type="submit" className="main-action-btn">{t('app.admin.store.product_form.save')}</FabButton>
</div> </div>
</section>
}
</form> </form>
</> </>
); );

View File

@ -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<ProductStockFormProps> = ({ product, control, onError, onSuccess }) => {
const { t } = useTranslation('admin');
/**
* Toggle stock threshold
*/
const toggleStockThreshold = (checked: boolean) => {
console.log('Stock threshold:', checked);
};
return (
<section>
<h4>Stock à jour <span>00/00/0000 - 00H30</span></h4>
<div></div>
<hr />
<div className="header-switch">
<h4 className='span-7'>{t('app.admin.store.product_stock_form.low_stock_threshold')}</h4>
<FormSwitch control={control}
id="is_active_threshold"
label={t('app.admin.store.product_stock_form.toggle_stock_threshold')}
defaultValue={false}
onChange={toggleStockThreshold}
className='span-3' />
</div>
</section>
);
};

View File

@ -251,7 +251,7 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
</label> </label>
))} ))}
</div> </div>
<FabButton onClick={applyFilters} className="is-info">{t('app.admin.store.products.filter_apply')}</FabButton> <FabButton onClick={() => setUpdate(true)} className="is-info">{t('app.admin.store.products.filter_apply')}</FabButton>
</div> </div>
</AccordionItem> </AccordionItem>
@ -269,7 +269,7 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
</label> </label>
))} ))}
</div> </div>
<FabButton onClick={applyFilters} className="is-info">{t('app.admin.store.products.filter_apply')}</FabButton> <FabButton onClick={() => setUpdate(true)} className="is-info">{t('app.admin.store.products.filter_apply')}</FabButton>
</div> </div>
</AccordionItem> </AccordionItem>
</div> </div>

View File

@ -13,6 +13,7 @@ import { StoreProductItem } from './store-product-item';
import useCart from '../../hooks/use-cart'; import useCart from '../../hooks/use-cart';
import { emitCustomEvent } from 'react-custom-events'; import { emitCustomEvent } from 'react-custom-events';
import { User } from '../../models/user'; import { User } from '../../models/user';
import { Order } from '../../models/order';
import { AccordionItem } from './accordion-item'; import { AccordionItem } from './accordion-item';
import { StoreListHeader } from './store-list-header'; import { StoreListHeader } from './store-list-header';
@ -20,6 +21,7 @@ declare const Application: IApplication;
interface StoreProps { interface StoreProps {
onError: (message: string) => void, onError: (message: string) => void,
onSuccess: (message: string) => void,
currentUser: User, currentUser: User,
} }
/** /**
@ -31,7 +33,7 @@ interface StoreProps {
/** /**
* This component shows public store * This component shows public store
*/ */
const Store: React.FC<StoreProps> = ({ onError, currentUser }) => { const Store: React.FC<StoreProps> = ({ onError, onSuccess, currentUser }) => {
const { t } = useTranslation('public'); const { t } = useTranslation('public');
const { cart, setCart } = useCart(currentUser); const { cart, setCart } = useCart(currentUser);
@ -138,6 +140,14 @@ const Store: React.FC<StoreProps> = ({ onError, currentUser }) => {
console.log('Display in stock only:', checked); 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 ( return (
<div className="store"> <div className="store">
<ul className="breadcrumbs"> <ul className="breadcrumbs">
@ -224,7 +234,7 @@ const Store: React.FC<StoreProps> = ({ onError, currentUser }) => {
/> />
<div className="products-grid"> <div className="products-grid">
{products.map((product) => ( {products.map((product) => (
<StoreProductItem key={product.id} product={product} cart={cart} onSuccessAddProductToCart={setCart} /> <StoreProductItem key={product.id} product={product} cart={cart} onSuccessAddProductToCart={addToCart} />
))} ))}
</div> </div>
</div> </div>
@ -253,7 +263,7 @@ const StoreWrapper: React.FC<StoreProps> = (props) => {
); );
}; };
Application.Components.component('store', react2angular(StoreWrapper, ['onError', 'currentUser'])); Application.Components.component('store', react2angular(StoreWrapper, ['onError', 'onSuccess', 'currentUser']));
interface ActiveCategory { interface ActiveCategory {
id: number, id: number,

View File

@ -55,6 +55,24 @@
background-color: var(--gray-soft-light); background-color: var(--gray-soft-light);
border-radius: var(--border-radius); 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, .price,
.total { .total {
min-width: 10rem; min-width: 10rem;
@ -158,7 +176,9 @@
justify-content: center; justify-content: center;
text-transform: uppercase; text-transform: uppercase;
&:hover { &:hover {
color: var(--gray-soft-lightest);
opacity: 0.75; opacity: 0.75;
cursor: pointer;
} }
} }
} }

View File

@ -32,7 +32,7 @@
background-color: var(--information-lightest); background-color: var(--information-lightest);
color: var(--information); color: var(--information);
border: 1px solid var(--information); border: 1px solid var(--information);
border-radius: 8px; border-radius: var(--border-radius);
font-size: 14px; font-size: 14px;
font-weight: normal; font-weight: normal;
line-height: 1.2em; line-height: 1.2em;

View File

@ -1,5 +1,26 @@
.product-form { .product-form {
grid-column: 2 / -2; 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 { h4 {
margin: 0 0 2.4rem; margin: 0 0 2.4rem;
@include title-base; @include title-base;
@ -30,7 +51,7 @@
} }
} }
.price-data-header { .header-switch {
@include grid-col(10); @include grid-col(10);
gap: 3.2rem; gap: 3.2rem;
align-items: center; align-items: center;

View File

@ -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 { button {
opacity: 100; opacity: 100;
margin-top: 0.8rem; margin-top: 0.8rem;

View File

@ -1964,6 +1964,8 @@ en:
edit_product: edit_product:
successfully_updated: "The product has been updated." successfully_updated: "The product has been updated."
product_form: product_form:
product_parameters: "Product parameters"
stock_management: "Stock management"
name: "Name of product" name: "Name of product"
sku: "Reference product (SKU)" sku: "Reference product (SKU)"
slug: "Name of URL" slug: "Name of URL"
@ -1987,6 +1989,9 @@ en:
product_images_info: "<strong>Advice</strong></br>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." product_images_info: "<strong>Advice</strong></br>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" add_product_image: "Add an image"
save: "Save" save: "Save"
product_stock_form:
low_stock_threshold: "Define a low stock threshold"
toggle_stock_threshold: "Activate stock threshold"
orders: orders:
heading: "Orders" heading: "Orders"
create_order: "Create an order" create_order: "Create an order"

View File

@ -383,6 +383,7 @@ en:
store: store:
fablab_store: "FabLab Store" fablab_store: "FabLab Store"
unexpected_error_occurred: "An unexpected error occurred. Please try again later." unexpected_error_occurred: "An unexpected error occurred. Please try again later."
add_to_cart_success: "Product added to the cart."
products: products:
all_products: "All the products" all_products: "All the products"
filter: "Filter" filter: "Filter"