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

create/edit product form

This commit is contained in:
Du Peng 2022-07-22 18:48:28 +02:00
parent e23e83000d
commit 5e1436eda4
21 changed files with 677 additions and 21 deletions

View File

@ -4,7 +4,7 @@
# Products are used in store
class API::ProductsController < API::ApiController
before_action :authenticate_user!, except: %i[index show]
before_action :set_product, only: %i[update destroy]
before_action :set_product, only: %i[show update destroy]
def index
@products = ProductService.list
@ -15,6 +15,8 @@ class API::ProductsController < API::ApiController
def create
authorize Product
@product = Product.new(product_params)
@product.amount = nil if @product.amount.zero?
@product.amount *= 100 if @product.amount.present?
if @product.save
render status: :created
else
@ -25,7 +27,10 @@ class API::ProductsController < API::ApiController
def update
authorize @product
if @product.update(product_params)
product_parameters = product_params
product_parameters[:amount] = nil if product_parameters[:amount].zero?
product_parameters[:amount] = product_parameters[:amount] * 100 if product_parameters[:amount].present?
if @product.update(product_parameters)
render status: :ok
else
render json: @product.errors.full_messages, status: :unprocessable_entity
@ -47,6 +52,6 @@ class API::ProductsController < API::ApiController
def product_params
params.require(:product).permit(:name, :slug, :sku, :description, :is_active,
:product_category_id, :amount, :quantity_min,
:low_stock_alert, :low_stock_threshold)
:low_stock_alert, :low_stock_threshold, machine_ids: [])
end
end

View File

@ -0,0 +1,99 @@
import React, { BaseSyntheticEvent } from 'react';
import { Controller, Path, FieldPathValue } from 'react-hook-form';
import { FieldValues } from 'react-hook-form/dist/types/fields';
import { FieldPath } from 'react-hook-form/dist/types/path';
import { useTranslation } from 'react-i18next';
import { UnpackNestedValue } from 'react-hook-form/dist/types';
import { FormControlledComponent } from '../../models/form-component';
import { AbstractFormItem, AbstractFormItemProps } from './abstract-form-item';
import { FabButton } from '../base/fab-button';
/**
* Checklist Option format
*/
export type ChecklistOption<TOptionValue> = { value: TOptionValue, label: string };
interface FormCheckListProps<TFieldValues, TOptionValue, TContext extends object> extends FormControlledComponent<TFieldValues, TContext>, AbstractFormItemProps<TFieldValues> {
defaultValue?: Array<TOptionValue>,
options: Array<ChecklistOption<TOptionValue>>,
onChange?: (values: Array<TOptionValue>) => void,
}
/**
* This component is a template for an check list component to use within React Hook Form
*/
export const FormCheckList = <TFieldValues extends FieldValues, TOptionValue, TContext extends object>({ id, control, label, tooltip, defaultValue, className, rules, disabled, error, warning, formState, onChange, options }: FormCheckListProps<TFieldValues, TOptionValue, TContext>) => {
const { t } = useTranslation('shared');
/**
* Verify if the provided option is currently ticked
*/
const isChecked = (values: Array<TOptionValue>, option: ChecklistOption<TOptionValue>): boolean => {
return !!values?.includes(option.value);
};
/**
* Callback triggered when a checkbox is ticked or unticked.
*/
const toggleCheckbox = (option: ChecklistOption<TOptionValue>, values: Array<TOptionValue> = [], cb: (value: Array<TOptionValue>) => void) => {
return (event: BaseSyntheticEvent) => {
let newValues: Array<TOptionValue> = [];
if (event.target.checked) {
newValues = values.concat(option.value);
} else {
newValues = values.filter(v => v !== option.value);
}
cb(newValues);
if (typeof onChange === 'function') {
onChange(newValues);
}
};
};
/**
* Callback triggered to select all options
*/
const allSelect = (cb: (value: Array<TOptionValue>) => void) => {
return () => {
const newValues: Array<TOptionValue> = options.map(o => o.value);
cb(newValues);
if (typeof onChange === 'function') {
onChange(newValues);
}
};
};
// Compose classnames from props
const classNames = [
`${className || ''}`
].join(' ');
return (
<AbstractFormItem id={id} formState={formState} label={label}
className={`form-check-list form-input ${classNames}`} tooltip={tooltip}
disabled={disabled}
rules={rules} error={error} warning={warning}>
<Controller name={id as FieldPath<TFieldValues>}
control={control}
defaultValue={defaultValue as UnpackNestedValue<FieldPathValue<TFieldValues, Path<TFieldValues>>>}
rules={rules}
render={({ field: { onChange, value } }) => {
return (
<>
<div className="checklist">
{options.map((option, k) => {
return (
<div key={k} className="checklist-item">
<input id={`option-${k}`} type="checkbox" checked={isChecked(value, option)} onChange={toggleCheckbox(option, value, onChange)} />
<label htmlFor={`option-${k}`}>{option.label}</label>
</div>
);
})}
</div>
<FabButton type="button" onClick={allSelect(onChange)} className="checklist-all-button">{t('app.shared.form_check_list.select_all')}</FabButton>
</>
);
}} />
</AbstractFormItem>
);
};

View File

@ -0,0 +1,56 @@
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { react2angular } from 'react2angular';
import { Loader } from '../base/loader';
import { IApplication } from '../../models/application';
import { ProductForm } from './product-form';
import { Product } from '../../models/product';
import ProductAPI from '../../api/product';
declare const Application: IApplication;
interface EditProductProps {
productId: number,
onSuccess: (message: string) => void,
onError: (message: string) => void,
}
/**
* This component show new product form
*/
const EditProduct: React.FC<EditProductProps> = ({ productId, onSuccess, onError }) => {
const { t } = useTranslation('admin');
const [product, setProduct] = useState<Product>();
useEffect(() => {
ProductAPI.get(productId).then(data => {
setProduct(data);
}).catch(onError);
}, []);
/**
* Success to save product and return to product list
*/
const saveProductSuccess = () => {
onSuccess(t('app.admin.store.edit_product.successfully_updated'));
window.location.href = '/#!/admin/store/products';
};
if (product) {
return (
<ProductForm product={product} title={product.name} onSuccess={saveProductSuccess} onError={onError} />
);
}
return null;
};
const EditProductWrapper: React.FC<EditProductProps> = ({ productId, onSuccess, onError }) => {
return (
<Loader>
<EditProduct productId={productId} onSuccess={onSuccess} onError={onError} />
</Loader>
);
};
Application.Components.component('editProduct', react2angular(EditProductWrapper, ['productId', 'onSuccess', 'onError']));

View File

@ -0,0 +1,58 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { react2angular } from 'react2angular';
import { Loader } from '../base/loader';
import { IApplication } from '../../models/application';
import { ProductForm } from './product-form';
declare const Application: IApplication;
interface NewProductProps {
onSuccess: (message: string) => void,
onError: (message: string) => void,
}
/**
* This component show new product form
*/
const NewProduct: React.FC<NewProductProps> = ({ onSuccess, onError }) => {
const { t } = useTranslation('admin');
const product = {
id: undefined,
name: '',
slug: '',
sku: '',
description: '',
is_active: false,
quantity_min: 1,
stock: {
internal: 0,
external: 0
},
low_stock_alert: false,
machine_ids: []
};
/**
* Success to save product and return to product list
*/
const saveProductSuccess = () => {
onSuccess(t('app.admin.store.new_product.successfully_created'));
window.location.href = '/#!/admin/store/products';
};
return (
<ProductForm product={product} title={t('app.admin.store.new_product.add_a_new_product')} onSuccess={saveProductSuccess} onError={onError} />
);
};
const NewProductWrapper: React.FC<NewProductProps> = ({ onSuccess, onError }) => {
return (
<Loader>
<NewProduct onSuccess={onSuccess} onError={onError} />
</Loader>
);
};
Application.Components.component('newProduct', react2angular(NewProductWrapper, ['onSuccess', 'onError']));

View File

@ -0,0 +1,199 @@
import React, { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import slugify from 'slugify';
import _ from 'lodash';
import { HtmlTranslate } from '../base/html-translate';
import { Product } from '../../models/product';
import { FormInput } from '../form/form-input';
import { FormSwitch } from '../form/form-switch';
import { FormSelect } from '../form/form-select';
import { FormCheckList } from '../form/form-check-list';
import { FormRichText } from '../form/form-rich-text';
import { FabButton } from '../base/fab-button';
import { FabAlert } from '../base/fab-alert';
import ProductCategoryAPI from '../../api/product-category';
import MachineAPI from '../../api/machine';
import ProductAPI from '../../api/product';
interface ProductFormProps {
product: Product,
title: string,
onSuccess: (product: Product) => void,
onError: (message: string) => void,
}
/**
* Option format, expected by react-select
* @see https://github.com/JedWatson/react-select
*/
type selectOption = { value: number, label: string };
/**
* Option format, expected by checklist
*/
type checklistOption = { value: number, label: string };
/**
* Form component to create or update a product
*/
export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSuccess, onError }) => {
const { t } = useTranslation('admin');
const { handleSubmit, register, control, formState, setValue, reset } = useForm<Product>({ defaultValues: { ...product } });
const [isActivePrice, setIsActivePrice] = useState<boolean>(product.id && _.isFinite(product.amount) && product.amount > 0);
const [productCategories, setProductCategories] = useState<selectOption[]>([]);
const [machines, setMachines] = useState<checklistOption[]>([]);
useEffect(() => {
ProductCategoryAPI.index().then(data => {
setProductCategories(buildSelectOptions(data));
}).catch(onError);
MachineAPI.index({ disabled: false }).then(data => {
setMachines(buildChecklistOptions(data));
}).catch(onError);
}, []);
/**
* Convert the provided array of items to the react-select format
*/
const buildSelectOptions = (items: Array<{ id?: number, name: string }>): Array<selectOption> => {
return items.map(t => {
return { value: t.id, label: t.name };
});
};
/**
* Convert the provided array of items to the checklist format
*/
const buildChecklistOptions = (items: Array<{ id?: number, name: string }>): Array<checklistOption> => {
return items.map(t => {
return { value: t.id, label: t.name };
});
};
/**
* Callback triggered when the name has changed.
*/
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
const name = event.target.value;
const slug = slugify(name, { lower: true, strict: true });
setValue('slug', slug);
};
/**
* Callback triggered when is active price has changed.
*/
const toggleIsActivePrice = (value: boolean) => {
if (!value) {
setValue('amount', null);
}
setIsActivePrice(value);
};
/**
* Callback triggered when the form is submitted: process with the product creation or update.
*/
const onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
return handleSubmit((data: Product) => {
saveProduct(data);
})(event);
};
/**
* Call product creation or update api
*/
const saveProduct = (data: Product) => {
if (product.id) {
ProductAPI.update(data).then((res) => {
reset(res);
onSuccess(res);
}).catch(onError);
} else {
ProductAPI.create(data).then((res) => {
reset(res);
onSuccess(res);
}).catch(onError);
}
};
return (
<>
<h2>{title}</h2>
<FabButton className="save" onClick={handleSubmit(saveProduct)}>{t('app.admin.store.product_form.save')}</FabButton>
<form className="product-form" onSubmit={onSubmit}>
<FormInput id="name"
register={register}
rules={{ required: true }}
formState={formState}
onChange={handleNameChange}
label={t('app.admin.store.product_form.name')} />
<FormInput id="sku"
register={register}
formState={formState}
label={t('app.admin.store.product_form.sku')} />
<FormInput id="slug"
register={register}
rules={{ required: true }}
formState={formState}
label={t('app.admin.store.product_form.slug')} />
<FormSwitch control={control}
id="is_active"
formState={formState}
label={t('app.admin.store.product_form.is_show_in_store')} />
<div className="price-data">
<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')}
tooltip={t('app.admin.store.product_form.is_active_price')}
defaultValue={isActivePrice}
onChange={toggleIsActivePrice} />
{isActivePrice && <div className="price-fields">
<FormInput id="amount"
type="number"
register={register}
rules={{ required: true, min: 0.01 }}
step={0.01}
formState={formState}
label={t('app.admin.store.product_form.price')} />
<FormInput id="quantity_min"
type="number"
rules={{ required: true }}
register={register}
formState={formState}
label={t('app.admin.store.product_form.quantity_min')} />
</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')} />
<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} />
<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}
paragraphTools={true}
limit={1000}
id="description" />
</div>
<div className="main-actions">
<FabButton type="submit" className="submit-button">{t('app.admin.store.product_form.save')}</FabButton>
</div>
</form>
</>
);
};

View File

@ -1,10 +1,8 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { react2angular } from 'react2angular';
import { HtmlTranslate } from '../base/html-translate';
import { Loader } from '../base/loader';
import { IApplication } from '../../models/application';
import { FabAlert } from '../base/fab-alert';
import { FabButton } from '../base/fab-button';
import { ProductsList } from './products-list';
import { Product } from '../../models/product';
@ -24,7 +22,6 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
const { t } = useTranslation('admin');
const [products, setProducts] = useState<Array<Product>>([]);
const [product, setProduct] = useState<Product>(null);
useEffect(() => {
ProductAPI.index().then(data => {
@ -33,10 +30,10 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
}, []);
/**
* Open edit the product modal
* Goto edit product page
*/
const editProduct = (product: Product) => {
setProduct(product);
window.location.href = `/#!/admin/store/products/${product.id}/edit`;
};
/**
@ -53,10 +50,17 @@ const Products: React.FC<ProductsProps> = ({ onSuccess, onError }) => {
}
};
/**
* Goto new product page
*/
const newProduct = (): void => {
window.location.href = '/#!/admin/store/products/new';
};
return (
<div>
<h2>{t('app.admin.store.products.all_products')}</h2>
<FabButton className="save">{t('app.admin.store.products.create_a_product')}</FabButton>
<FabButton className="save" onClick={newProduct}>{t('app.admin.store.products.create_a_product')}</FabButton>
<ProductsList
products={products}
onEdit={editProduct}

View File

@ -0,0 +1,47 @@
/* eslint-disable
no-return-assign,
no-undef,
*/
'use strict';
Application.Controllers.controller('AdminStoreProductController', ['$scope', 'CSRF', 'growl', '$state', '$transition$',
function ($scope, CSRF, growl, $state, $transition$) {
/* PUBLIC SCOPE */
$scope.productId = $transition$.params().id;
/**
* Callback triggered in case of error
*/
$scope.onError = (message) => {
growl.error(message);
};
/**
* Callback triggered in case of success
*/
$scope.onSuccess = (message) => {
growl.success(message);
};
/**
* Click Callback triggered in case of back products list
*/
$scope.backProductsList = () => {
$state.go('app.admin.store.products');
};
/* PRIVATE SCOPE */
/**
* Kind of constructor: these actions will be realized first when the controller is loaded
*/
const initialize = function () {
// set the authenticity tokens in the forms
CSRF.setMetaTags();
};
// init the controller (call at the end !)
return initialize();
}
]);

View File

@ -15,10 +15,11 @@ export interface Product {
sku: string,
description: string,
is_active: boolean,
product_category_id: number,
amount: number,
quantity_min: number,
product_category_id?: number,
amount?: number,
quantity_min?: number,
stock: Stock,
low_stock_alert: boolean,
low_stock_threshold: number,
low_stock_threshold?: number,
machine_ids: number[],
}

View File

@ -1106,7 +1106,11 @@ angular.module('application.router', ['ui.router'])
.state('app.admin.store', {
abstract: true,
url: '/admin/store',
url: '/admin/store'
})
.state('app.admin.store.settings', {
url: '/settings',
views: {
'main@': {
templateUrl: '/admin/store/index.html',
@ -1115,20 +1119,54 @@ angular.module('application.router', ['ui.router'])
}
})
.state('app.admin.store.settings', {
url: '/settings'
.state('app.admin.store.products', {
url: '/products',
views: {
'main@': {
templateUrl: '/admin/store/index.html',
controller: 'AdminStoreController'
}
}
})
.state('app.admin.store.products', {
url: '/products'
.state('app.admin.store.products_new', {
url: '/products/new',
views: {
'main@': {
templateUrl: '/admin/store/product_new.html',
controller: 'AdminStoreProductController'
}
}
})
.state('app.admin.store.products_edit', {
url: '/products/:id/edit',
views: {
'main@': {
templateUrl: '/admin/store/product_edit.html',
controller: 'AdminStoreProductController'
}
}
})
.state('app.admin.store.categories', {
url: '/categories'
url: '/categories',
views: {
'main@': {
templateUrl: '/admin/store/index.html',
controller: 'AdminStoreController'
}
}
})
.state('app.admin.store.orders', {
url: '/orders'
url: '/orders',
views: {
'main@': {
templateUrl: '/admin/store/index.html',
controller: 'AdminStoreController'
}
}
})
// OpenAPI Clients

View File

@ -38,6 +38,7 @@
@import "modules/form/form-input";
@import "modules/form/form-rich-text";
@import "modules/form/form-switch";
@import "modules/form/form-check-list";
@import "modules/group/change-group";
@import "modules/machines/machine-card";
@import "modules/machines/machines-filters";

View File

@ -0,0 +1,17 @@
.form-check-list {
position: relative;
.form-item-field {
display: block !important;
}
.checklist {
display: flex;
padding: 16px;
flex-wrap: wrap;
}
.checklist-item {
flex: 0 0 33.333333%;
}
}

View File

@ -0,0 +1,35 @@
<section class="heading b-b">
<div class="row no-gutter">
<div class="col-xs-2 col-sm-2 col-md-1">
<section class="heading-btn">
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
</section>
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
<section class="heading-title">
<h1 translate>{{ 'app.admin.store.manage_the_store' }}</h1>
</section>
</div>
</div>
</section>
<section class="m-lg admin-store-manage">
<div class="row">
<div class="col-md-12">
<a ng-click="backProductsList()">
<i class="fas fa-angle-left"></i>
<span translate>{{ 'app.admin.store.back_products_list' }}</span>
</a>
</div>
</div>
<div class="row">
<div class="col-md-12">
<edit-product product-id="productId" on-success="onSuccess" on-error="onError"/>
</div>
</div>
</section>

View File

@ -0,0 +1,35 @@
<section class="heading b-b">
<div class="row no-gutter">
<div class="col-xs-2 col-sm-2 col-md-1">
<section class="heading-btn">
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
</section>
</div>
<div class="col-xs-10 col-sm-10 col-md-8 b-l">
<section class="heading-title">
<h1 translate>{{ 'app.admin.store.manage_the_store' }}</h1>
</section>
</div>
</div>
</section>
<section class="m-lg admin-store-manage">
<div class="row">
<div class="col-md-12">
<a ng-click="backProductsList()">
<i class="fas fa-angle-left"></i>
<span translate>{{ 'app.admin.store.back_products_list' }}</span>
</a>
</div>
</div>
<div class="row">
<div class="col-md-12">
<new-product on-success="onSuccess" on-error="onError"/>
</div>
</div>
</section>

View File

@ -29,6 +29,7 @@ class Machine < ApplicationRecord
has_one :payment_gateway_object, as: :item
has_and_belongs_to_many :products
after_create :create_statistic_subtype
after_create :create_machine_prices

View File

@ -1,3 +1,10 @@
# frozen_string_literal: true
# Product is a model for the merchandise hold information of product in store
class Product < ApplicationRecord
belongs_to :product_category
has_and_belongs_to_many :machines
validates_numericality_of :amount, greater_than: 0, allow_nil: true
end

View File

@ -1,3 +1,4 @@
# frozen_string_literal: true
json.extract! product, :id, :name, :slug, :sku, :description, :is_active, :product_category_id, :amount, :quantity_min, :stock, :low_stock_alert, :low_stock_threshold
json.extract! product, :id, :name, :slug, :sku, :description, :is_active, :product_category_id, :quantity_min, :stock, :low_stock_alert, :low_stock_threshold, :machine_ids
json.amount product.amount / 100.0 if product.amount.present?

View File

@ -1899,6 +1899,7 @@ en:
all_products: "All products"
categories_of_store: "Store's categories"
the_orders: "Orders"
back_products_list: "Back to products list"
product_categories:
title: "Categories"
info: "<strong>Information:</strong></br>Find below all the categories created. The categories are arranged on two levels maximum, you can arrange them with a drag and drop. The order of the categories will be identical on the public view and the list below. Please note that you can delete a category or a sub-category even if they are associated with products. The latter will be left without categories. If you delete a category that contains sub-categories, the latter will also be deleted. <strong>Make sure that your categories are well arranged and save your choice.</strong>"
@ -1931,3 +1932,25 @@ en:
create_a_product: "Create a product"
successfully_deleted: "The product has been successfully deleted"
unable_to_delete: "Unable to delete the product: "
new_product:
add_a_new_product: "Add a new product"
successfully_created: "The new product has been created."
edit_product:
successfully_updated: "The product has been updated."
product_form:
name: "Name of product"
sku: "Reference product (SKU)"
slug: "Name of URL"
is_show_in_store: "Available in the store"
is_active_price: "Activate the price"
price_and_rule_of_selling_product: "Price and rule for selling the product"
price: "Price of product"
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_machines: "Assigning machines"
assigning_machines_info: "<strong>Information</strong></br>You can link one or more machines from your fablab 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 present in the product sheet. You have a few editorial styles at your disposal to create the product sheet."
save: "Save"

View File

@ -1899,6 +1899,7 @@ fr:
all_products: "Tous les produits"
categories_of_store: "Les catégories de la boutique"
the_orders: "Les commandes"
back_products_list: "Retrounez à la liste"
product_categories:
title: "Les catégories"
info: "<strong>Information:</strong></br>Retrouvez ci-dessous toutes les catégories créés. Les catégories se rangent sur deux niveaux maximum, vous pouvez les agencer avec un glisser-déposer. L'ordre d'affichage des catégories sera identique sur la vue publique et la liste ci-dessous. Attention, Vous pouvez supprimer une catégorie ou une sous-catégorie même si elles sont associées à des produits. Ces derniers se retrouveront sans catégories. Si vous supprimez une catégorie contenant des sous-catégories, ces dernières seront elles aussi supprimées. <strong>Veillez au bon agencement de vos catégories et sauvegarder votre choix.</strong>"
@ -1931,3 +1932,25 @@ fr:
create_a_product: "Créer un produit"
successfully_deleted: "Le produit a bien été supprimé"
unable_to_delete: "Impossible de supprimer le produit: "
new_product:
add_a_new_product: "Ajouter un nouveau produit"
successfully_created: "Le produit a bien été créée."
edit_product:
successfully_updated: "Le produit a bien été mise à jour."
product_form:
name: "Nom de produit"
sku: "Référence produit (SKU)"
slug: "Nom de l'URL"
is_show_in_store: "Visible dans la boutique"
is_active_price: "Activer le prix"
price_and_rule_of_selling_product: "Prix et règle de vente du produit"
price: "Prix du produit"
quantity_min: "Nombre d'article minimum pour la mise au panier"
linking_product_to_category: "Lier ce product à une catégorie exisante"
assigning_category: "Attribuer à une catégorie"
assigning_category_info: "<strong>Information</strong></br>Vous ne pouvez déclarer qu'une catégorie par produit. Si vous attribuez ce produit à une sous catégorie, il sera attribué automatiquement aussi à sa catégorie parent."
assigning_machines: "Attribuer aux machines"
assigning_machines_info: "<strong>Information</strong></br>Vous pouvez lier une ou plusieurs machines de votre fablab à votre produit, Ce produit sera alors assujetti aux filtres sur la vue catalogue.</br>Les machines sélectionnées ci-dessous seront liées au produit."
product_description: "Description du produit"
product_description_info: "<strong>Information</strong></br>Cette description du produit sera présente dans la fiche du produit. Vous avez à disposition quelques styles rédactionnels pour créer la fiche du produit."
save: "Enregistrer"

View File

@ -550,3 +550,5 @@ en:
validate_button: "Validate the new card"
form_multi_select:
create_label: "Add {VALUE}"
form_check_list:
select_all: "Select all"

View File

@ -550,3 +550,5 @@ fr:
validate_button: "Valider la nouvelle carte"
form_multi_select:
create_label: "Ajouter {VALUE}"
form_check_list:
select_all: "Tout sélectionner"

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class CreateProducts < ActiveRecord::Migration[5.2]
def change
create_table :products do |t|