1
0
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:
Du Peng 2022-10-11 18:53:12 +02:00
parent 7700737cf3
commit ef9a5c22bb
11 changed files with 153 additions and 4 deletions

View File

@ -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)

View File

@ -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;

View File

@ -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>
);
};

View File

@ -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) {

View File

@ -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>

View File

@ -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

View File

@ -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)

View File

@ -0,0 +1,3 @@
# frozen_string_literal: true
json.partial! 'api/products/product', product: @product

View File

@ -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"

View File

@ -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"

View File

@ -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