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

(ui) Update store layout

This commit is contained in:
vincent 2023-01-18 14:50:29 +01:00 committed by Sylvain
parent 6e38e72851
commit c12bf45ff8
8 changed files with 298 additions and 274 deletions

View File

@ -154,116 +154,32 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
* This function render the content of the 'products settings' tab
*/
const renderSettingsTab = () => (
<section>
<div className="subgrid">
<FormInput id="name"
register={register}
rules={{ required: true }}
formState={formState}
onChange={handleNameChange}
label={t('app.admin.store.product_form.name')}
className="span-7" />
<FormInput id="sku"
register={register}
formState={formState}
label={t('app.admin.store.product_form.sku')}
className="span-3" />
</div>
<div className="subgrid">
<FormInput id="slug"
<div className='product-form-content'>
<section>
<header>
<p className="title">{t('app.admin.store.product_form.description')}</p>
<p className="description">{t('app.admin.store.product_form.description_info')}</p>
</header>
<div className="content">
<FormInput id="name"
register={register}
rules={{ required: true }}
formState={formState}
onChange={handleNameChange}
label={t('app.admin.store.product_form.name')}
className="span-7" />
<FormInput id="slug"
register={register}
rules={{ required: true }}
formState={formState}
label={t('app.admin.store.product_form.slug')}
className='span-7' />
<FormSwitch control={control}
id="is_active"
<FormInput id="sku"
register={register}
formState={formState}
label={t('app.admin.store.product_form.is_show_in_store')}
tooltip={t('app.admin.store.product_form.active_price_info')}
onChange={handleIsActiveChanged}
className='span-3' />
</div>
<hr />
<div className="price-data">
<div className="header-switch">
<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} />
</div>
{isActivePrice && <div className="price-data-content">
<FormInput id="amount"
type="number"
register={register}
rules={{ required: isActivePrice, min: 0 }}
step={0.01}
formState={formState}
label={t('app.admin.store.product_form.price')}
nullable />
<FormInput id="quantity_min"
type="number"
rules={{ required: true }}
register={register}
formState={formState}
label={t('app.admin.store.product_form.quantity_min')} />
</div>}
</div>
<hr />
<div>
<h4>{t('app.admin.store.product_form.product_images')}</h4>
<FabAlert level="warning">
<HtmlTranslate trKey="app.admin.store.product_form.product_images_info" />
</FabAlert>
<FormMultiImageUpload setValue={setValue}
addButtonLabel={t('app.admin.store.product_form.add_product_image')}
register={register}
control={control}
id="product_images_attributes"
className="product-images" />
</div>
<hr />
<div>
<h4>{t('app.admin.store.product_form.assigning_category')}</h4>
<FabAlert level="warning">
<HtmlTranslate trKey="app.admin.store.product_form.assigning_category_info" />
</FabAlert>
<FormSelect options={productCategories}
control={control}
id="product_category_id"
formState={formState}
label={t('app.admin.store.product_form.linking_product_to_category')} />
</div>
<hr />
<div>
<h4>{t('app.admin.store.product_form.assigning_machines')}</h4>
<FabAlert level="warning">
<HtmlTranslate trKey="app.admin.store.product_form.assigning_machines_info" />
</FabAlert>
<FormChecklist options={machines}
control={control}
id="machine_ids"
formState={formState} />
</div>
<hr />
<div>
<h4>{t('app.admin.store.product_form.product_description')}</h4>
<FabAlert level="warning">
<HtmlTranslate trKey="app.admin.store.product_form.product_description_info" />
</FabAlert>
<FormRichText control={control}
label={t('app.admin.store.product_form.sku')}
className="span-3" />
<FormRichText control={control}
heading
bulletList
blockquote
@ -271,35 +187,106 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
limit={6000}
id="description"
ariaLabel={t('app.admin.store.product_form.product_description')} />
</div>
<FormSwitch control={control}
id="is_active"
formState={formState}
label={t('app.admin.store.product_form.is_show_in_store')}
tooltip={t('app.admin.store.product_form.active_price_info')}
onChange={handleIsActiveChanged}
className='span-3' />
</div>
</section>
<hr />
<section>
<header>
<p className="title">{t('app.admin.store.product_form.product_images')}</p>
<p className="description">{t('app.admin.store.product_form.product_images_info')}</p>
</header>
<div className="content">
<FormMultiImageUpload setValue={setValue}
addButtonLabel={t('app.admin.store.product_form.add_product_image')}
register={register}
control={control}
id="product_images_attributes" />
</div>
</section>
<div>
<h4>{t('app.admin.store.product_form.product_files')}</h4>
<FabAlert level="warning">
<HtmlTranslate trKey="app.admin.store.product_form.product_files_info" />
</FabAlert>
<FormMultiFileUpload setValue={setValue}
addButtonLabel={t('app.admin.store.product_form.add_product_file')}
control={control}
accept="application/pdf"
register={register}
id="product_files_attributes"
className="product-documents" />
</div>
<section>
<header>
<p className="title">{t('app.admin.store.product_form.price_and_rule_of_selling_product')}</p>
</header>
<div className="content">
<FormSwitch control={control}
id="is_active_price"
label={t('app.admin.store.product_form.is_active_price')}
defaultValue={isActivePrice}
onChange={toggleIsActivePrice} />
{isActivePrice && <>
<FormInput id="amount"
type="number"
register={register}
rules={{ required: isActivePrice, min: 0 }}
step={0.01}
formState={formState}
label={t('app.admin.store.product_form.price')}
nullable />
<FormInput id="quantity_min"
type="number"
rules={{ required: true }}
register={register}
formState={formState}
label={t('app.admin.store.product_form.quantity_min')} />
</>}
</div>
</section>
<hr />
<section>
<header>
<p className="title">{t('app.admin.store.product_form.assigning_category')}</p>
<p className="description">{t('app.admin.store.product_form.assigning_category_info')}</p>
</header>
<div className="content">
<FormSelect options={productCategories}
control={control}
id="product_category_id"
formState={formState}
label={t('app.admin.store.product_form.linking_product_to_category')} />
</div>
</section>
<AdvancedAccountingForm register={register} onError={onError} />
<section>
<header>
<p className="title">{t('app.admin.store.product_form.assigning_machines')}</p>
<p className="description">{t('app.admin.store.product_form.assigning_machines_info')}</p>
</header>
<div className="content">
<FormChecklist options={machines}
control={control}
id="machine_ids"
formState={formState} />
</div>
</section>
<div className="main-actions">
<FabButton type="submit" className="main-action-btn" disabled={saving}>
{!saving && t('app.admin.store.product_form.save')}
{saving && <i className="fa fa-spinner fa-pulse fa-fw" />}
</FabButton>
</div>
</section>
<section>
<header>
<p className="title">{t('app.admin.store.product_form.product_files')}</p>
<p className="description">{t('app.admin.store.product_form.product_files_info')}</p>
</header>
<div className="content">
<FormMultiFileUpload setValue={setValue}
addButtonLabel={t('app.admin.store.product_form.add_product_file')}
control={control}
accept="application/pdf"
register={register}
id="product_files_attributes"
className="product-documents" />
</div>
</section>
<section>
<AdvancedAccountingForm register={register} onError={onError} />
</section>
</div>
);
return (

View File

@ -162,7 +162,7 @@ export const ProductStockForm = <TContext extends object> ({ currentFormValues,
};
return (
<section className='product-stock-form'>
<div className='product-stock-form'>
<h4>{t('app.admin.store.product_stock_form.stock_up_to_date')}&nbsp;
<span>{t('app.admin.store.product_stock_form.date_time', {
DATE: FormatLib.date(lastStockUpdate()),
@ -214,36 +214,33 @@ export const ProductStockForm = <TContext extends object> ({ currentFormValues,
<hr />
<div className="threshold-data">
<div className="header-switch">
<h4>{t('app.admin.store.product_stock_form.low_stock_threshold')}</h4>
<header>
<p className="title">{t('app.admin.store.product_stock_form.low_stock_threshold')}</p>
<p className="description">{t('app.admin.store.product_stock_form.stock_threshold_info')}</p>
</header>
<div className="content">
<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 && <>
<FabStateLabel>{t('app.admin.store.product_stock_form.low_stock')}</FabStateLabel>
<div className="threshold-data-content">
{activeThreshold && <>
<FabStateLabel>{t('app.admin.store.product_stock_form.low_stock')}</FabStateLabel>
<FormInput id="low_stock_threshold"
type="number"
register={register}
rules={{ required: activeThreshold, min: 1 }}
step={1}
formState={formState}
nullable
label={t('app.admin.store.product_stock_form.threshold_level')} />
type="number"
register={register}
rules={{ required: activeThreshold, min: 1 }}
step={1}
formState={formState}
nullable
label={t('app.admin.store.product_stock_form.threshold_level')} />
<FormSwitch control={control}
id="low_stock_alert"
formState={formState}
label={t('app.admin.store.product_stock_form.threshold_alert')} />
</div>
</>}
</>}
</div>
</div>
<hr />
<div className="store-list">
<h4>{t('app.admin.store.product_stock_form.events_history')}</h4>
@ -292,6 +289,6 @@ export const ProductStockForm = <TContext extends object> ({ currentFormValues,
<ProductStockModal onSuccess={onNewStockMovement}
isOpen={isOpen}
toggleModal={toggleModal} />
</section>
</div>
);
};

View File

@ -52,28 +52,32 @@ export const StoreSettings: React.FC<StoreSettingsProps> = ({ onError, onSuccess
<div className='store-settings'>
<header>
<h2>{t('app.admin.store_settings.title')}</h2>
<FabButton onClick={handleSubmit(onSubmit)} className='save-btn is-main'>{t('app.admin.store_settings.save')}</FabButton>
</header>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="setting-section">
<p className="section-title">{t('app.admin.store_settings.withdrawal_instructions')}</p>
<FabAlert level="warning">
<HtmlTranslate trKey="app.admin.store_settings.withdrawal_info" />
</FabAlert>
<FormRichText control={control}
heading
bulletList
link
limit={400}
id="store_withdrawal_instructions" />
<form onSubmit={handleSubmit(onSubmit)} className="store-settings-content">
<div className="settings-section">
<header>
<p className="title">{t('app.admin.store_settings.withdrawal_instructions')}</p>
<p className="description">{t('app.admin.store_settings.withdrawal_info')}</p>
</header>
<div className="content">
<FormRichText control={control}
heading
bulletList
link
limit={400}
id="store_withdrawal_instructions" />
</div>
</div>
<div className="setting-section">
<p className="section-title">{t('app.admin.store_settings.store_hidden_title')}</p>
<FabAlert level="warning">
<HtmlTranslate trKey="app.admin.store_settings.store_hidden_info" />
</FabAlert>
<FormSwitch control={control} id="store_hidden" label={t('app.admin.store_settings.store_hidden')} />
<div className="settings-section">
<header>
<p className="title">{t('app.admin.store_settings.store_hidden_title')}</p>
<p className="description">{t('app.admin.store_settings.store_hidden_info')}</p>
</header>
<div className="content">
<FormSwitch control={control} id="store_hidden" label={t('app.admin.store_settings.store_hidden')} />
</div>
</div>
<FabButton type='submit' className='save-btn'>{t('app.admin.store_settings.save')}</FabButton>
</form>
</div>
);

View File

@ -1,48 +1,77 @@
.product-form {
grid-column: 2 / -2;
h4 {
margin: 0 0 2.4rem;
@include title-base;
}
hr {
margin: 4.8rem 0;
}
.subgrid {
@include grid-col(10);
gap: 3.2rem;
align-items: flex-end;
}
.span-3 { grid-column: span 3; }
.span-7 { grid-column: span 7; }
& > div {
grid-column: 2 / -2;
}
.flex {
&-content {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: 0 3.2rem;
& > * {
flex: 1 1 320px;
flex-direction: column;
gap: 3.2rem;
section { @include layout-settings; }
.form-checklist {
margin: 0;
.form-item-field {
background: none;
.checklist {
max-height: max(170px, 25vh);
overflow-y: auto;
padding: 1.6rem;
border: 1px solid var(--gray-soft-dark);
background-color: var(--gray-soft-lightest);
border-radius: var(--border-radius);
}
.actions { margin-bottom: 0; }
}
}
@media (max-width: 539px) {
.form-multi-image-upload .list {
grid-template-columns: 1fr;
}
}
}
.header-switch {
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;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 0 3.2rem;
align-items: flex-end;
}
//h4 {
// margin: 0 0 2.4rem;
// @include title-base;
//}
//hr {
// margin: 4.8rem 0;
//}
//.subgrid {
// @include grid-col(10);
// gap: 3.2rem;
// align-items: flex-end;
//}
//.span-3 { grid-column: span 3; }
//.span-7 { grid-column: span 7; }
//& > div {
// grid-column: 2 / -2;
//}
//.flex {
// display: flex;
// flex-wrap: wrap;
// align-items: flex-end;
// gap: 0 3.2rem;
// & > * {
// flex: 1 1 320px;
// }
//}
//.header-switch {
// 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;
// grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
// gap: 0 3.2rem;
// align-items: flex-end;
//}
}

View File

@ -27,6 +27,7 @@
}
}
.store-list {
margin-top: 2.4rem;
h4 { margin: 0; }
}
.store-list-header {
@ -50,17 +51,19 @@
}
}
.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; }
.threshold-data {
@include layout-settings;
&-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; }
}
}
.fab-state-label {
--status-color: var(--alert-light);

View File

@ -1,23 +1,22 @@
.products,
.new-product,
.edit-product {
max-width: 1600px;
margin: 0 auto;
padding-bottom: 6rem;
header {
@include header();
padding-bottom: 0;
grid-column: 1 / -1;
}
}
.products {
max-width: 1600px;
display: grid;
grid-template-rows: min-content 8rem 1fr;
align-items: flex-start;
& > header { margin-bottom: 2.4rem; }
& > header {
grid-column: 1 / -1;
margin-bottom: 2.4rem;
@include header();
gap: 2.4rem;
}
.store-filters {
grid-area: 2 / 1 / 4 / 2;
@ -44,20 +43,17 @@
.new-product,
.edit-product {
@include grid-col(12);
gap: 3.2rem;
align-items: flex-start;
max-width: 1200px;
display: flex;
flex-direction: column;
&-nav {
max-width: 1600px;
max-width: 1200px;
margin: 0 auto;
@include grid-col(12);
gap: 3.2rem;
justify-items: flex-start;
& > * {
grid-column: 2 / -2;
}
}
header { grid-column: 2 / -2; }
& > header {
padding-bottom: 0;
@include header($sticky: true);
gap: 2.4rem;
}
}

View File

@ -1,37 +1,45 @@
.store-settings {
max-width: 1600px;
max-width: 1200px;
margin: 0 auto;
padding-bottom: 6rem;
@include grid-col(12);
gap: 3.2rem;
align-items: flex-start;
header {
@include header();
display: flex;
flex-direction: column;
& > header {
padding-bottom: 0;
grid-column: 2 / -2;
@include header($sticky: true);
gap: 2.4rem;
}
form {
grid-column: 2 / -2;
@include grid-col(2);
gap: 2.4rem 3.2rem;
.setting-section { grid-column: 1 / -1; }
@media (min-width: 1024px) {
.setting-section { grid-column: span 1; }
}
&-content {
display: flex;
flex-direction: column;
gap: 3.2rem;
.section-title { @include title-base; }
.save-btn {
grid-column: 1 / -1;
justify-self: flex-start;
background-color: var(--main);
color: var(--gray-soft-lightest);
border: none;
&:hover {
background-color: var(--main);
color: var(--gray-soft-lightest);
opacity: 0.75;
}
}
.settings-section { @include layout-settings; }
.save-btn { align-self: flex-start; }
}
//form {
// grid-column: 2 / -2;
// @include grid-col(2);
// gap: 2.4rem 3.2rem;
// .setting-section { grid-column: 1 / -1; }
// @media (min-width: 1024px) {
// .setting-section { grid-column: span 1; }
// }
// .section-title { @include title-base; }
// .save-btn {
// grid-column: 1 / -1;
// justify-self: flex-start;
// background-color: var(--main);
// color: var(--gray-soft-lightest);
// border: none;
// &:hover {
// background-color: var(--main);
// color: var(--gray-soft-lightest);
// opacity: 0.75;
// }
// }
//}
}

View File

@ -2234,6 +2234,8 @@ en:
product_form:
product_parameters: "Product parameters"
stock_management: "Stock management"
description: "Description"
description_info: "The text will be presented in the product sheet. You have a few editorial styles at your disposal."
name: "Name of product"
sku: "Product reference (SKU)"
slug: "URL"
@ -2245,16 +2247,14 @@ en:
quantity_min: "Minimum number of items for the shopping cart"
linking_product_to_category: "Linking this product to an existing category"
assigning_category: "Assigning a category"
assigning_category_info: "<strong>Information</strong></br>You can only declare one category per product. If you assign this product to a sub-category, it will automatically be assigned to its parent category as well."
assigning_category_info: "You can only declare one category per product. If you assign this product to a sub-category, it will automatically be assigned to its parent category as well."
assigning_machines: "Assigning machines"
assigning_machines_info: "<strong>Information</strong></br>You can link one or more machines from your workshop to your product. This product will then be subject to the filters on the catalogue view.</br>The machines selected below will be linked to the product."
product_description: "Product description"
product_description_info: "<strong>Information</strong></br>This product description will be presented in the product sheet. You have a few editorial styles at your disposal to create the product sheet."
assigning_machines_info: "You can link one or more machines from your workshop to your product. This product will then be subject to the filters on the catalogue view. The selected machines will be linked to the product."
product_files: "Document"
product_files_info: "<strong>Information</strong></br>Add documents related to this product. They will be presented in the product sheet, in a separate block. You can only upload PDF documents."
product_files_info: "Add documents related to this product. They will be presented in the product sheet, in a separate block. You can only upload PDF documents."
add_product_file: "Add a document"
product_images: "Visuals of the product"
product_images_info: "<strong>Advice</strong></br>We advise you to use a square format, JPG or PNG. For JPG, please use white for the background colour. The main visual will be the first presented in the product sheet."
product_images_info: "We advise you to use a square format, JPG or PNG. For JPG, please use white for the background colour. The main visual will be the first presented in the product sheet."
add_product_image: "Add a visual"
save: "Save"
clone: "Duplicate"
@ -2265,7 +2265,7 @@ en:
save_reminder: "Don't forget to save your operations"
low_stock_threshold: "Define a low 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>When the threshold is reached, the product quantity is labeled as low."
stock_threshold_info: "Define a low stock threshold and receive a notification when it's reached. 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"