mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-17 11:54:22 +01:00
(feat) clone a product
This commit is contained in:
parent
7700737cf3
commit
ef9a5c22bb
@ -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[update clone destroy]
|
||||
|
||||
def index
|
||||
@products = ProductService.list(params, current_user)
|
||||
@ -35,6 +35,17 @@ class API::ProductsController < API::ApiController
|
||||
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
|
||||
authorize @product
|
||||
ProductService.destroy(@product)
|
||||
|
@ -91,6 +91,13 @@ export default class ProductAPI {
|
||||
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> {
|
||||
const res: AxiosResponse<void> = await apiClient.delete(`/api/products/${productId}`);
|
||||
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
|
||||
* or
|
||||
* Success to clone product and return to new product
|
||||
*/
|
||||
const saveProductSuccess = () => {
|
||||
onSuccess(t('app.admin.store.edit_product.successfully_updated'));
|
||||
window.location.href = '/#!/admin/store/products';
|
||||
const saveProductSuccess = (data: Product) => {
|
||||
if (data.id === product.id) {
|
||||
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) {
|
||||
|
@ -19,6 +19,7 @@ import MachineAPI from '../../api/machine';
|
||||
import ProductAPI from '../../api/product';
|
||||
import { Plus } from 'phosphor-react';
|
||||
import { ProductStockForm } from './product-stock-form';
|
||||
import { CloneProductModal } from './clone-product-modal';
|
||||
import ProductLib from '../../lib/product';
|
||||
import { UnsavedFormAlert } from '../form/unsaved-form-alert';
|
||||
import { UIRouter } from '@uirouter/angularjs';
|
||||
@ -54,6 +55,7 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
|
||||
const [productCategories, setProductCategories] = useState<selectOption[]>([]);
|
||||
const [machines, setMachines] = useState<checklistOption[]>([]);
|
||||
const [stockTab, setStockTab] = useState<boolean>(false);
|
||||
const [openCloneModal, setOpenCloneModal] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
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
|
||||
*/
|
||||
@ -235,6 +244,12 @@ export const ProductForm: React.FC<ProductFormProps> = ({ product, title, onSucc
|
||||
<header>
|
||||
<h2>{title}</h2>
|
||||
<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>
|
||||
</div>
|
||||
</header>
|
||||
|
@ -10,6 +10,10 @@ class ProductPolicy < ApplicationPolicy
|
||||
user.privileged?
|
||||
end
|
||||
|
||||
def clone?
|
||||
user.privileged?
|
||||
end
|
||||
|
||||
def destroy?
|
||||
user.privileged?
|
||||
end
|
||||
|
@ -67,6 +67,27 @@ class ProductService
|
||||
product
|
||||
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)
|
||||
used_in_order = OrderItem.joins(:order).where.not('orders.state' => 'cart')
|
||||
.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."
|
||||
edit_product:
|
||||
successfully_updated: "The product has been updated."
|
||||
successfully_cloned: "The product has been cloned."
|
||||
product_form:
|
||||
product_parameters: "Product parameters"
|
||||
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."
|
||||
add_product_image: "Add an image"
|
||||
save: "Save"
|
||||
clone: "Clone"
|
||||
product_stock_form:
|
||||
stock_up_to_date: "Stock up to date"
|
||||
date_time: "{DATE} - {TIME}"
|
||||
@ -2072,6 +2074,9 @@ en:
|
||||
damaged: "Damaged product"
|
||||
other_in: "Other (in)"
|
||||
other_out: "Other (out)"
|
||||
clone_product_model:
|
||||
clone_product: "Clone the product"
|
||||
clone: "Clone"
|
||||
orders:
|
||||
heading: "Orders"
|
||||
create_order: "Create an order"
|
||||
|
@ -2003,6 +2003,7 @@ fr:
|
||||
successfully_created: "Le nouveau produit a été créé."
|
||||
edit_product:
|
||||
successfully_updated: "Le produit a été modifié."
|
||||
successfully_cloned: "Le produit a été dupliqué."
|
||||
product_form:
|
||||
product_parameters: "Paramètres du produit"
|
||||
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."
|
||||
add_product_image: "Ajouter une image"
|
||||
save: "Enregistrer"
|
||||
clone: "Dupliquer"
|
||||
product_stock_form:
|
||||
stock_up_to_date: "Stock à jour"
|
||||
date_time: "{DATE} - {TIME}"
|
||||
@ -2072,6 +2074,9 @@ fr:
|
||||
damaged: "Produit endommagé"
|
||||
other_in: "Autre (entrant)"
|
||||
other_out: "Autre (sortant)"
|
||||
clone_product_model:
|
||||
clone_product: "Dupliquer le produit"
|
||||
clone: "Dupliquer"
|
||||
orders:
|
||||
heading: "Commandes"
|
||||
create_order: "Créer une commande"
|
||||
|
@ -155,6 +155,7 @@ Rails.application.routes.draw do
|
||||
end
|
||||
|
||||
resources :products do
|
||||
put 'clone', on: :member
|
||||
get 'stock_movements', on: :member
|
||||
end
|
||||
resources :cart, only: %i[create] do
|
||||
|
Loading…
x
Reference in New Issue
Block a user