mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-19 13:54:25 +01:00
(feat) clone a product
This commit is contained in:
parent
7700737cf3
commit
ef9a5c22bb
@ -4,7 +4,7 @@
|
|||||||
# Products are used in store
|
# Products are used in store
|
||||||
class API::ProductsController < API::ApiController
|
class API::ProductsController < API::ApiController
|
||||||
before_action :authenticate_user!, except: %i[index show]
|
before_action :authenticate_user!, except: %i[index show]
|
||||||
before_action :set_product, only: %i[update destroy]
|
before_action :set_product, only: %i[update clone destroy]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@products = ProductService.list(params, current_user)
|
@products = ProductService.list(params, current_user)
|
||||||
@ -35,6 +35,17 @@ class API::ProductsController < API::ApiController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def clone
|
||||||
|
authorize @product
|
||||||
|
|
||||||
|
@product = ProductService.clone(@product, product_params)
|
||||||
|
if @product.save
|
||||||
|
render status: :ok
|
||||||
|
else
|
||||||
|
render json: @product.errors.full_messages, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
authorize @product
|
authorize @product
|
||||||
ProductService.destroy(@product)
|
ProductService.destroy(@product)
|
||||||
|
@ -91,6 +91,13 @@ export default class ProductAPI {
|
|||||||
return res?.data;
|
return res?.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async clone (product: Product, data: Product): Promise<Product> {
|
||||||
|
const res: AxiosResponse<Product> = await apiClient.put(`/api/products/${product.id}/clone`, {
|
||||||
|
product: data
|
||||||
|
});
|
||||||
|
return res?.data;
|
||||||
|
}
|
||||||
|
|
||||||
static async destroy (productId: number): Promise<void> {
|
static async destroy (productId: number): Promise<void> {
|
||||||
const res: AxiosResponse<void> = await apiClient.delete(`/api/products/${productId}`);
|
const res: AxiosResponse<void> = await apiClient.delete(`/api/products/${productId}`);
|
||||||
return res?.data;
|
return res?.data;
|
||||||
|
@ -0,0 +1,70 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { SubmitHandler, useForm, useWatch } from 'react-hook-form';
|
||||||
|
import { FormInput } from '../form/form-input';
|
||||||
|
import { FormSwitch } from '../form/form-switch';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { FabModal, ModalSize } from '../base/fab-modal';
|
||||||
|
import { Product } from '../../models/product';
|
||||||
|
import ProductAPI from '../../api/product';
|
||||||
|
|
||||||
|
interface CloneProductModalProps {
|
||||||
|
isOpen: boolean,
|
||||||
|
toggleModal: () => void,
|
||||||
|
onSuccess: (product: Product) => void,
|
||||||
|
onError: (message: string) => void,
|
||||||
|
product: Product,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modal dialog to clone a product
|
||||||
|
*/
|
||||||
|
export const CloneProductModal: React.FC<CloneProductModalProps> = ({ isOpen, toggleModal, onSuccess, onError, product }) => {
|
||||||
|
const { t } = useTranslation('admin');
|
||||||
|
const { handleSubmit, register, control, formState, reset } = useForm<Product>({ defaultValues: {
|
||||||
|
name: product.name,
|
||||||
|
sku: product.sku,
|
||||||
|
is_active: false,
|
||||||
|
} });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call product clone api
|
||||||
|
*/
|
||||||
|
const handleClone: SubmitHandler<Product> = (data: Product) => {
|
||||||
|
ProductAPI.clone(product, data).then((res) => {
|
||||||
|
reset(res);
|
||||||
|
onSuccess(res);
|
||||||
|
}).catch(onError);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FabModal title={t('app.admin.store.clone_product_model.clone_product')}
|
||||||
|
isOpen={isOpen}
|
||||||
|
toggleModal={toggleModal}
|
||||||
|
width={ModalSize.medium}
|
||||||
|
confirmButton={t('app.admin.store.clone_product_model.clone')}
|
||||||
|
onConfirm={handleSubmit(handleClone)}>
|
||||||
|
<form className="clone-product-form" onSubmit={handleSubmit(handleClone)}>
|
||||||
|
<FormInput id="name"
|
||||||
|
register={register}
|
||||||
|
rules={{ required: true }}
|
||||||
|
formState={formState}
|
||||||
|
label={t('app.admin.store.product_form.name')}
|
||||||
|
className="span-12" />
|
||||||
|
<FormInput id="sku"
|
||||||
|
register={register}
|
||||||
|
formState={formState}
|
||||||
|
label={t('app.admin.store.product_form.sku')}
|
||||||
|
className="span-12" />
|
||||||
|
{product.is_active &&
|
||||||
|
<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')}
|
||||||
|
className='span-12' />
|
||||||
|
}
|
||||||
|
</form>
|
||||||
|
</FabModal>
|
||||||
|
);
|
||||||
|
};
|
@ -33,10 +33,17 @@ const EditProduct: React.FC<EditProductProps> = ({ productId, onSuccess, onError
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Success to save product and return to product list
|
* Success to save product and return to product list
|
||||||
|
* or
|
||||||
|
* Success to clone product and return to new product
|
||||||
*/
|
*/
|
||||||
const saveProductSuccess = () => {
|
const saveProductSuccess = (data: Product) => {
|
||||||
onSuccess(t('app.admin.store.edit_product.successfully_updated'));
|
if (data.id === product.id) {
|
||||||
window.location.href = '/#!/admin/store/products';
|
onSuccess(t('app.admin.store.edit_product.successfully_updated'));
|
||||||
|
window.location.href = '/#!/admin/store/products';
|
||||||
|
} else {
|
||||||
|
onSuccess(t('app.admin.store.edit_product.successfully_cloned'));
|
||||||
|
window.location.href = `/#!/admin/store/products/${data.id}/edit`;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (product) {
|
if (product) {
|
||||||
|
@ -19,6 +19,7 @@ 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';
|
import { ProductStockForm } from './product-stock-form';
|
||||||
|
import { CloneProductModal } from './clone-product-modal';
|
||||||
import ProductLib from '../../lib/product';
|
import ProductLib from '../../lib/product';
|
||||||
import { UnsavedFormAlert } from '../form/unsaved-form-alert';
|
import { UnsavedFormAlert } from '../form/unsaved-form-alert';
|
||||||
import { UIRouter } from '@uirouter/angularjs';
|
import { UIRouter } from '@uirouter/angularjs';
|
||||||
@ -54,6 +55,7 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
|
|||||||
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);
|
const [stockTab, setStockTab] = useState<boolean>(false);
|
||||||
|
const [openCloneModal, setOpenCloneModal] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
ProductCategoryAPI.index().then(data => {
|
ProductCategoryAPI.index().then(data => {
|
||||||
@ -136,6 +138,13 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle clone product modal
|
||||||
|
*/
|
||||||
|
const toggleCloneModal = () => {
|
||||||
|
setOpenCloneModal(!openCloneModal);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add new product file
|
* Add new product file
|
||||||
*/
|
*/
|
||||||
@ -235,6 +244,12 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
|
|||||||
<header>
|
<header>
|
||||||
<h2>{title}</h2>
|
<h2>{title}</h2>
|
||||||
<div className="grpBtn">
|
<div className="grpBtn">
|
||||||
|
{product.id &&
|
||||||
|
<>
|
||||||
|
<FabButton className="main-action-btn" onClick={toggleCloneModal}>{t('app.admin.store.product_form.clone')}</FabButton>
|
||||||
|
<CloneProductModal isOpen={openCloneModal} toggleModal={toggleCloneModal} product={product} onSuccess={onSuccess} onError={onError} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
<FabButton className="main-action-btn" onClick={handleSubmit(saveProduct)}>{t('app.admin.store.product_form.save')}</FabButton>
|
<FabButton className="main-action-btn" onClick={handleSubmit(saveProduct)}>{t('app.admin.store.product_form.save')}</FabButton>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
@ -10,6 +10,10 @@ class ProductPolicy < ApplicationPolicy
|
|||||||
user.privileged?
|
user.privileged?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def clone?
|
||||||
|
user.privileged?
|
||||||
|
end
|
||||||
|
|
||||||
def destroy?
|
def destroy?
|
||||||
user.privileged?
|
user.privileged?
|
||||||
end
|
end
|
||||||
|
@ -67,6 +67,27 @@ class ProductService
|
|||||||
product
|
product
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def clone(product, product_params)
|
||||||
|
new_product = product.dup
|
||||||
|
new_product.name = product_params[:name]
|
||||||
|
new_product.sku = product_params[:sku]
|
||||||
|
new_product.is_active = product_params[:is_active]
|
||||||
|
new_product.stock['internal'] = 0
|
||||||
|
new_product.stock['external'] = 0
|
||||||
|
new_product.machine_ids = product.machine_ids
|
||||||
|
new_product.machine_ids = product.machine_ids
|
||||||
|
product.product_images.each do |image|
|
||||||
|
pi = new_product.product_images.build
|
||||||
|
pi.is_main = image.is_main
|
||||||
|
pi.attachment = File.open(image.attachment.file.file)
|
||||||
|
end
|
||||||
|
product.product_files.each do |file|
|
||||||
|
pf = new_product.product_files.build
|
||||||
|
pf.attachment = File.open(file.attachment.file.file)
|
||||||
|
end
|
||||||
|
new_product
|
||||||
|
end
|
||||||
|
|
||||||
def destroy(product)
|
def destroy(product)
|
||||||
used_in_order = OrderItem.joins(:order).where.not('orders.state' => 'cart')
|
used_in_order = OrderItem.joins(:order).where.not('orders.state' => 'cart')
|
||||||
.exists?(orderable: product)
|
.exists?(orderable: product)
|
||||||
|
3
app/views/api/products/clone.json.jbuilder
Normal file
3
app/views/api/products/clone.json.jbuilder
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
json.partial! 'api/products/product', product: @product
|
@ -2003,6 +2003,7 @@ en:
|
|||||||
successfully_created: "The new product has been created."
|
successfully_created: "The new product has been created."
|
||||||
edit_product:
|
edit_product:
|
||||||
successfully_updated: "The product has been updated."
|
successfully_updated: "The product has been updated."
|
||||||
|
successfully_cloned: "The product has been cloned."
|
||||||
product_form:
|
product_form:
|
||||||
product_parameters: "Product parameters"
|
product_parameters: "Product parameters"
|
||||||
stock_management: "Stock management"
|
stock_management: "Stock management"
|
||||||
@ -2029,6 +2030,7 @@ en:
|
|||||||
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: "<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."
|
||||||
add_product_image: "Add an image"
|
add_product_image: "Add an image"
|
||||||
save: "Save"
|
save: "Save"
|
||||||
|
clone: "Clone"
|
||||||
product_stock_form:
|
product_stock_form:
|
||||||
stock_up_to_date: "Stock up to date"
|
stock_up_to_date: "Stock up to date"
|
||||||
date_time: "{DATE} - {TIME}"
|
date_time: "{DATE} - {TIME}"
|
||||||
@ -2072,6 +2074,9 @@ en:
|
|||||||
damaged: "Damaged product"
|
damaged: "Damaged product"
|
||||||
other_in: "Other (in)"
|
other_in: "Other (in)"
|
||||||
other_out: "Other (out)"
|
other_out: "Other (out)"
|
||||||
|
clone_product_model:
|
||||||
|
clone_product: "Clone the product"
|
||||||
|
clone: "Clone"
|
||||||
orders:
|
orders:
|
||||||
heading: "Orders"
|
heading: "Orders"
|
||||||
create_order: "Create an order"
|
create_order: "Create an order"
|
||||||
|
@ -2003,6 +2003,7 @@ fr:
|
|||||||
successfully_created: "Le nouveau produit a été créé."
|
successfully_created: "Le nouveau produit a été créé."
|
||||||
edit_product:
|
edit_product:
|
||||||
successfully_updated: "Le produit a été modifié."
|
successfully_updated: "Le produit a été modifié."
|
||||||
|
successfully_cloned: "Le produit a été dupliqué."
|
||||||
product_form:
|
product_form:
|
||||||
product_parameters: "Paramètres du produit"
|
product_parameters: "Paramètres du produit"
|
||||||
stock_management: "Gestion des stocks"
|
stock_management: "Gestion des stocks"
|
||||||
@ -2029,6 +2030,7 @@ fr:
|
|||||||
product_images_info: "<strong>Conseil</strong></br>Nous vous conseillons d'utiliser un format carré, JPG ou PNG. Pour le JPG, veuillez utiliser le blanc pour la couleur de fond. Le visuel principal sera le premier présenté dans la fiche produit."
|
product_images_info: "<strong>Conseil</strong></br>Nous vous conseillons d'utiliser un format carré, JPG ou PNG. Pour le JPG, veuillez utiliser le blanc pour la couleur de fond. Le visuel principal sera le premier présenté dans la fiche produit."
|
||||||
add_product_image: "Ajouter une image"
|
add_product_image: "Ajouter une image"
|
||||||
save: "Enregistrer"
|
save: "Enregistrer"
|
||||||
|
clone: "Dupliquer"
|
||||||
product_stock_form:
|
product_stock_form:
|
||||||
stock_up_to_date: "Stock à jour"
|
stock_up_to_date: "Stock à jour"
|
||||||
date_time: "{DATE} - {TIME}"
|
date_time: "{DATE} - {TIME}"
|
||||||
@ -2072,6 +2074,9 @@ fr:
|
|||||||
damaged: "Produit endommagé"
|
damaged: "Produit endommagé"
|
||||||
other_in: "Autre (entrant)"
|
other_in: "Autre (entrant)"
|
||||||
other_out: "Autre (sortant)"
|
other_out: "Autre (sortant)"
|
||||||
|
clone_product_model:
|
||||||
|
clone_product: "Dupliquer le produit"
|
||||||
|
clone: "Dupliquer"
|
||||||
orders:
|
orders:
|
||||||
heading: "Commandes"
|
heading: "Commandes"
|
||||||
create_order: "Créer une commande"
|
create_order: "Créer une commande"
|
||||||
|
@ -155,6 +155,7 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
|
|
||||||
resources :products do
|
resources :products do
|
||||||
|
put 'clone', on: :member
|
||||||
get 'stock_movements', on: :member
|
get 'stock_movements', on: :member
|
||||||
end
|
end
|
||||||
resources :cart, only: %i[create] do
|
resources :cart, only: %i[create] do
|
||||||
|
Loading…
x
Reference in New Issue
Block a user