1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-18 07:52:23 +01:00
This commit is contained in:
vincent 2022-09-06 12:08:19 +02:00
parent f21a68593a
commit 3a0248ed98
14 changed files with 451 additions and 56 deletions

View File

@ -242,7 +242,7 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
<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} />
? <ProductStockForm product={product} register={register} control={control} id="stock" onError={onError} onSuccess={onSuccess} />
: <section>
<div className="subgrid">
<FormInput id="name"
@ -277,13 +277,12 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
<div className="price-data">
<div className="header-switch">
<h4 className='span-7'>{t('app.admin.store.product_form.price_and_rule_of_selling_product')}</h4>
<h4>{t('app.admin.store.product_form.price_and_rule_of_selling_product')}</h4>
<FormSwitch control={control}
id="is_active_price"
label={t('app.admin.store.product_form.is_active_price')}
defaultValue={isActivePrice}
onChange={toggleIsActivePrice}
className='span-3' />
onChange={toggleIsActivePrice} />
</div>
{isActivePrice && <div className="price-data-content">
<FormInput id="amount"

View File

@ -1,44 +1,208 @@
import React from 'react';
import React, { useState } from 'react';
import { Product } from '../../models/product';
import { UseFormRegister } from 'react-hook-form';
import { Control, FormState } from 'react-hook-form/dist/types/form';
import { useTranslation } from 'react-i18next';
import { Control } from 'react-hook-form';
import { HtmlTranslate } from '../base/html-translate';
import { FormSwitch } from '../form/form-switch';
import { FormInput } from '../form/form-input';
import Select from 'react-select';
import { FabAlert } from '../base/fab-alert';
import { FabButton } from '../base/fab-button';
import { PencilSimple } from 'phosphor-react';
import { FabModal, ModalSize } from '../base/fab-modal';
import { ProductStockModal } from './product-stock-modal';
interface ProductStockFormProps {
interface ProductStockFormProps<TFieldValues, TContext extends object> {
product: Product,
control: Control,
register: UseFormRegister<TFieldValues>,
control: Control<TFieldValues, TContext>,
formState: FormState<TFieldValues>,
onSuccess: (product: Product) => void,
onError: (message: string) => void,
}
/**
* Form tab to manage product's stock
* Form tab to manage a product's stock
*/
export const ProductStockForm: React.FC<ProductStockFormProps> = ({ product, control, onError, onSuccess }) => {
export const ProductStockForm = <TFieldValues, TContext extends object> ({ product, register, control, formState, onError, onSuccess }: ProductStockFormProps<TFieldValues, TContext>) => {
const { t } = useTranslation('admin');
const [activeThreshold, setActiveThreshold] = useState<boolean>(false);
// is the modal open?
const [isOpen, setIsOpen] = useState<boolean>(false);
// Styles the React-select component
const customStyles = {
control: base => ({
...base,
width: '20ch',
border: 'none',
backgroundColor: 'transparent'
}),
indicatorSeparator: () => ({
display: 'none'
})
};
type selectOption = { value: number, label: string };
/**
* Creates sorting options to the react-select format
*/
const buildEventsOptions = (): Array<selectOption> => {
return [
{ value: 0, label: t('app.admin.store.product_stock_form.events.inward_stock') },
{ value: 1, label: t('app.admin.store.product_stock_form.events.returned') },
{ value: 2, label: t('app.admin.store.product_stock_form.events.canceled') },
{ value: 3, label: t('app.admin.store.product_stock_form.events.sold') },
{ value: 4, label: t('app.admin.store.product_stock_form.events.missing') },
{ value: 5, label: t('app.admin.store.product_stock_form.events.damaged') }
];
};
/**
* Creates sorting options to the react-select format
*/
const buildStocksOptions = (): Array<selectOption> => {
return [
{ value: 0, label: t('app.admin.store.product_stock_form.internal') },
{ value: 1, label: t('app.admin.store.product_stock_form.external') },
{ value: 2, label: t('app.admin.store.product_stock_form.all') }
];
};
/**
* On events option change
*/
const eventsOptionsChange = (evt: selectOption) => {
console.log('Event option:', evt);
};
/**
* On stocks option change
*/
const stocksOptionsChange = (evt: selectOption) => {
console.log('Stocks option:', evt);
};
/**
* Toggle stock threshold
*/
const toggleStockThreshold = (checked: boolean) => {
console.log('Stock threshold:', checked);
setActiveThreshold(checked);
};
/**
* Opens/closes the product category modal
*/
const toggleModal = (): void => {
setIsOpen(!isOpen);
};
/**
* Toggle stock threshold alert
*/
const toggleStockThresholdAlert = (checked: boolean) => {
console.log('Low stock notification:', checked);
};
return (
<section>
<section className='product-stock-form'>
<h4>Stock à jour <span>00/00/0000 - 00H30</span></h4>
<div></div>
<div className="stock-item">
<p className='title'>Product name</p>
<div className="group">
<span>{t('app.admin.store.product_stock_form.internal')}</span>
<p>00</p>
</div>
<div className="group">
<span>{t('app.admin.store.product_stock_form.external')}</span>
<p>000</p>
</div>
<FabButton onClick={toggleModal} icon={<PencilSimple size={20} weight="fill" />} className="is-black">Modifier</FabButton>
</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 className="threshold-data">
<div className="header-switch">
<h4>{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.stock_threshold_toggle')}
defaultValue={activeThreshold}
onChange={toggleStockThreshold} />
</div>
<FabAlert level="warning">
<HtmlTranslate trKey="app.admin.store.product_stock_form.stock_threshold_information" />
</FabAlert>
{activeThreshold && <>
<span className='stock-label'>{t('app.admin.store.product_stock_form.low_stock')}</span>
<div className="threshold-data-content">
<FormInput id="threshold"
type="number"
register={register}
rules={{ required: true, min: 1 }}
step={1}
formState={formState}
label={t('app.admin.store.product_stock_form.threshold_level')} />
<FormSwitch control={control}
id="threshold_alert"
formState={formState}
label={t('app.admin.store.product_stock_form.threshold_alert')}
defaultValue={activeThreshold}
onChange={toggleStockThresholdAlert} />
</div>
</>}
</div>
<hr />
<div className="store-list">
<h4>{t('app.admin.store.product_stock_form.events_history')}</h4>
<div className="store-list-header">
<div className='sort-events'>
<p>{t('app.admin.store.product_stock_form.event_type')}</p>
<Select
options={buildEventsOptions()}
onChange={evt => eventsOptionsChange(evt)}
styles={customStyles}
/>
</div>
<div className='sort-stocks'>
<p>{t('app.admin.store.product_stock_form.stocks')}</p>
<Select
options={buildStocksOptions()}
onChange={evt => stocksOptionsChange(evt)}
styles={customStyles}
/>
</div>
</div>
<div className="stock-history">
<div className="stock-item">
<p className='title'>Product name</p>
<p>00/00/0000</p>
<div className="group">
<span>[stock type]</span>
<p>00</p>
</div>
<div className="group">
<span>{t('app.admin.store.product_stock_form.event_type')}</span>
<p>[event type]</p>
</div>
<div className="group">
<span>{t('app.admin.store.product_stock_form.stock_level')}</span>
<p>000</p>
</div>
</div>
</div>
</div>
<FabModal title={t('app.admin.store.product_stock_form.modal_title')}
className="fab-modal-lg"
width={ModalSize.large}
isOpen={isOpen}
toggleModal={toggleModal}
closeButton>
<ProductStockModal product={product} register={register} control={control} id="stock-modal" onError={onError} onSuccess={onSuccess} />
</FabModal>
</section>
);
};

View File

@ -0,0 +1,96 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Product } from '../../models/product';
import { UseFormRegister } from 'react-hook-form';
import { Control, FormState } from 'react-hook-form/dist/types/form';
import { FormSelect } from '../form/form-select';
import { FormInput } from '../form/form-input';
import { FabButton } from '../base/fab-button';
type selectOption = { value: number, label: string };
interface ProductStockModalProps<TFieldValues, TContext extends object> {
product: Product,
register: UseFormRegister<TFieldValues>,
control: Control<TFieldValues, TContext>,
formState: FormState<TFieldValues>,
onSuccess: (product: Product) => void,
onError: (message: string) => void
}
/**
* Form to manage a product's stock movement and quantity
*/
export const ProductStockModal = <TFieldValues, TContext extends object> ({ product, register, control, formState, onError, onSuccess }: ProductStockModalProps<TFieldValues, TContext>) => {
const { t } = useTranslation('admin');
const [movement, setMovement] = useState<'in' | 'out'>('in');
/**
* Toggle between adding or removing product from stock
*/
const toggleMovementType = (evt: React.MouseEvent<HTMLButtonElement, MouseEvent>, type: 'in' | 'out') => {
evt.preventDefault();
setMovement(type);
};
/**
* Creates sorting options to the react-select format
*/
const buildEventsOptions = (): Array<selectOption> => {
let options = [];
movement === 'in'
? options = [
{ value: 0, label: t('app.admin.store.product_stock_modal.events.inward_stock') },
{ value: 1, label: t('app.admin.store.product_stock_modal.events.returned') },
{ value: 2, label: t('app.admin.store.product_stock_modal.events.canceled') }
]
: options = [
{ value: 0, label: t('app.admin.store.product_stock_modal.events.sold') },
{ value: 1, label: t('app.admin.store.product_stock_modal.events.missing') },
{ value: 2, label: t('app.admin.store.product_stock_modal.events.damaged') }
];
return options;
};
/**
* Creates sorting options to the react-select format
*/
const buildStocksOptions = (): Array<selectOption> => {
return [
{ value: 0, label: t('app.admin.store.product_stock_modal.internal') },
{ value: 1, label: t('app.admin.store.product_stock_modal.external') }
];
};
return (
<form className='product-stock-modal'>
<p className='subtitle'>{t('app.admin.store.product_stock_modal.new_event')}</p>
<div className="movement">
<button onClick={(evt) => toggleMovementType(evt, 'in')} className={movement === 'in' ? 'is-active' : ''}>
{t('app.admin.store.product_stock_modal.addition')}
</button>
<button onClick={(evt) => toggleMovementType(evt, 'out')} className={movement === 'out' ? 'is-active' : ''}>
{t('app.admin.store.product_stock_modal.withdrawal')}
</button>
</div>
<FormSelect options={buildStocksOptions()}
control={control}
id="updated_stock_type"
formState={formState}
label={t('app.admin.store.product_stock_modal.stocks')} />
<FormInput id="updated_stock_quantity"
type="number"
register={register}
rules={{ required: true, min: 1 }}
step={1}
formState={formState}
label={t('app.admin.store.product_stock_modal.quantity')} />
<FormSelect options={buildEventsOptions()}
control={control}
id="updated_stock_event"
formState={formState}
label={t('app.admin.store.product_stock_modal.event_type')} />
<FabButton type='submit'>{t('app.admin.store.product_stock_modal.update_stock')} </FabButton>
</form>
);
};

View File

@ -84,7 +84,7 @@ export const StoreProductItem: React.FC<StoreProductItemProps> = ({ product, car
<span>/ {t('app.public.store_product_item.unit')}</span>
</div>
}
<div className="stock">
<div className="stock-label">
{productStockStatus(product)}
</div>
{product.stock.external > 0 &&

View File

@ -160,7 +160,7 @@ export const StoreProduct: React.FC<StoreProductProps> = ({ productSlug, onError
</div>
<aside>
<div className="stock">
<div className="stock-label">
{productStockStatus(product)}
</div>
<div className='price'>

View File

@ -95,6 +95,8 @@
@import "modules/store/orders";
@import "modules/store/product-categories";
@import "modules/store/product-form";
@import "modules/store/product-stock-form";
@import "modules/store/product-stock-modal";
@import "modules/store/products-grid";
@import "modules/store/products-list";
@import "modules/store/products";

View File

@ -68,6 +68,21 @@
}
}
.stock-label {
display: flex;
align-items: center;
@include text-sm;
color: var(--status-color);
&::before {
content: "";
margin-right: 0.8rem;
width: 1rem;
height: 1rem;
background-color: var(--status-color);
border-radius: 50%;
}
}
// Custom scrollbar
.u-scrollbar {
&::-webkit-scrollbar-track

View File

@ -52,9 +52,12 @@
}
.header-switch {
@include grid-col(10);
display: flex;
flex-direction: row;
gap: 3.2rem;
justify-content: space-between;
align-items: center;
label { flex: 0 1 fit-content; }
}
.price-data-content {
display: grid;

View File

@ -0,0 +1,74 @@
.product-stock-form {
h4 span { @include text-sm; }
.store-list {
h4 { margin: 0; }
}
.store-list-header {
& > *:not(:first-child) {
&::before {
content: "";
margin: 0 2rem;
width: 1px;
height: 2rem;
background-color: var(--gray-hard-darkest);
}
}
.sort-events {
margin-left: auto;
display: flex;
align-items: center;
}
.sort-stocks {
display: flex;
align-items: center;
}
}
.threshold-data-content {
margin-top: 1.6rem;
padding: 1.6rem;
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 3.2rem;
border: 1px solid var(--gray-soft);
border-radius: var(--border-radius);
label { flex: 0 1 fit-content; }
}
.stock-label {
--status-color: var(--alert-light);
}
.stock-item {
width: 100%;
display: flex;
gap: 4.8rem;
justify-items: flex-start;
align-items: center;
padding: 1.6rem;
border: 1px solid var(--gray-soft-dark);
border-radius: var(--border-radius);
background-color: var(--gray-soft-lightest);
& > * { flex: 1 1 45%; }
button { flex: 0;}
p {
margin: 0;
@include text-base;
}
.title {
@include text-base(600);
flex: 1 1 100%;
}
.group {
span {
@include text-xs;
color: var(--gray-hard-light);
}
p { @include text-base(600); }
}
}
}

View File

@ -0,0 +1,30 @@
.product-stock-modal {
.movement {
margin-bottom: 3.2rem;
display: flex;
justify-content: space-between;
align-items: center;
button {
flex: 1;
padding: 1.6rem;
display: flex;
justify-content: center;
align-items: center;
background-color: var(--gray-soft-lightest);
border: 1px solid var(--gray-soft-dark);
color: var(--gray-soft-darkest);
@include text-base;
&.is-active {
border: 1px solid var(--gray-soft-darkest);
background-color: var(--gray-hard-darkest);
color: var(--gray-soft-lightest);
}
}
button:first-of-type {
border-radius: var(--border-radius) 0 0 var(--border-radius);
}
button:last-of-type {
border-radius: 0 var(--border-radius) var(--border-radius) 0;
}
}
}

View File

@ -49,21 +49,7 @@
word-break: break-all;
}
}
.stock {
grid-area: stock;
display: flex;
align-items: center;
@include text-sm;
color: var(--status-color);
&::before {
content: "";
margin-right: 0.8rem;
width: 1rem;
height: 1rem;
background-color: var(--status-color);
border-radius: 50%;
}
}
.stock-label { grid-area: stock; }
button {
grid-area: btn;
align-self: flex-end;

View File

@ -22,9 +22,13 @@
display: flex;
align-items: center;
& > *:not(:first-child) {
margin-left: 2rem;
padding-left: 2rem;
border-left: 1px solid var(--gray-hard-darkest);
&::before {
content: "";
margin: 0 2rem;
width: 1px;
height: 2rem;
background-color: var(--gray-hard-darkest);
}
}
.sort {

View File

@ -142,20 +142,6 @@
background-color: var(--gray-soft-light);
border-radius: var(--border-radius-sm);
.stock {
display: flex;
align-items: center;
@include text-sm;
color: var(--status-color);
&::before {
content: "";
margin-right: 0.8rem;
width: 1rem;
height: 1rem;
background-color: var(--status-color);
border-radius: 50%;
}
}
.price {
p {
margin: 0;

View File

@ -1991,7 +1991,43 @@ en:
save: "Save"
product_stock_form:
low_stock_threshold: "Define a low stock threshold"
toggle_stock_threshold: "Activate stock threshold"
stock_threshold_toggle: "Activate stock threshold"
stock_threshold_information: "<strong>Information</strong></br>Define a low stock threshold and receive a notification when it's reached.<br>Above the threshold, the product is available in the store. When the threshold is reached, the product quantity is labeled as low."
low_stock: "Low stock"
threshold_level: "Minimum threshold level"
threshold_alert: "Notify me when the threshold is reached"
event_type: "Events:"
stocks: "Stocks:"
internal: "Private stock"
external: "Public stock"
all: "All types"
stock_level: "Stock level"
events:
inward_stock: "Inward stock"
returned: "Returned by client"
canceled: "Canceled by client"
sold: "Sold"
missing: "Missing in stock"
damaged: "Damaged product"
events_history: "Events history"
modal_title: "Manage stock"
product_stock_modal:
internal: "Private stock"
external: "Public stock"
new_event: "New stock event"
addition: "Addition"
withdrawal: "Withdrawal"
update_stock: "Update stock"
event_type: "Events:"
stocks: "Stocks:"
quantity: "Quantity"
events:
inward_stock: "Inward stock"
returned: "Returned by client"
canceled: "Canceled by client"
sold: "Sold"
missing: "Missing in stock"
damaged: "Damaged product"
orders:
heading: "Orders"
create_order: "Create an order"