Created
May 14, 2024 15:06
-
-
Save sostenesapollo/09c989d0e7addb3c330de952ab50286b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import axios from "axios" | |
import { ModalTop } from "~/components/modal-top" | |
import { notify } from "~/components/snackbar" | |
import { useAddProductContext } from "./helpers/use-add-product-context" | |
import CurrencyInput from "~/components/money-field" | |
import Label from "./label" | |
import { Input } from "~/components/input" | |
import { useStockContext } from "../../stock/helpers/use-stock-context" | |
import { defaultProductData } from "./helpers/use-add-product-context" | |
import CategorySelection from "./category-selection" | |
import { formatProductDataToSend } from "./helpers/format-new-product-send" | |
import { useRef, useState } from "react" | |
import { base64MimeType, getB64extension } from "~/utils" | |
import LoadingSpinner from "~/components/loading-spinner" | |
// import { useDropzone } from 'react-dropzone'; | |
import { Title } from '~/components/Text'; | |
import Divider from "~/components/divider" | |
import { ButtonWithIcon } from "~/components/button-with-icon" | |
import ProductSelection from "../add-stock-movement-modal/product-selection" | |
const { v4: uuid } = require('uuid') | |
export const convertBase64 = (file: any) => { | |
return new Promise((resolve, reject) => { | |
const fileReader = new FileReader(); | |
fileReader.readAsDataURL(file) | |
fileReader.onload = () => { | |
resolve(fileReader.result); | |
} | |
fileReader.onerror = (error) => { | |
reject(error); | |
} | |
}) | |
} | |
export const isImgUrl = (url: string) => { | |
// Remove query parameters (if any) | |
const urlWithoutQueryParams = url.split('?')[0]; | |
return /\.(jpg|jpeg|png|webp|avif|gif)$/.test(urlWithoutQueryParams); | |
} | |
export const submitImage = async (params: any): Promise<string | null> => { | |
return new Promise(async (resolve, reject)=>{ | |
const [file, url, base64String, dbType] = params | |
console.log('SUBMIT IMAGE', file, url, base64String, {dbType}); | |
let b64, imageUrl, filename = 'img.png', type = 'image/png'; | |
if(file) { | |
b64 = await convertBase64(file[0]) | |
filename = file[0]?.name | |
type = file[0].type | |
} else if (url?.includes('http')) { | |
imageUrl = url | |
}else if (base64String) { | |
b64 = base64String | |
type = base64MimeType(base64String) || 'image/png' | |
filename = `${uuid}.${getB64extension(base64String || '')}` | |
} | |
// return resolve('example.png') | |
axios.post('/upload-to-s3', { fileBase64: b64, imageUrl, filename, type, id: params.id, databaseType: dbType }) | |
.then(rst=>{ | |
resolve(rst.data) | |
}) | |
.catch(e=>{ | |
reject() | |
}) | |
}) | |
} | |
export const onDropImage = (e: any, _file: any) => { | |
e?.preventDefault() | |
let _previewImage; | |
let _paramsToSubmit = [] | |
if(_file) { | |
_previewImage = URL.createObjectURL(_file) | |
_paramsToSubmit = [[_file]] | |
} else{ | |
const imageUrl = e.dataTransfer.getData('URL') | |
if(e.dataTransfer.files.length > 0) { | |
const file = e.dataTransfer.files[0] | |
if(!isImgUrl(file.name)) { | |
throw new Error('Imagem inválida, tente outra.') | |
} | |
_previewImage = URL.createObjectURL(file) | |
_paramsToSubmit = [[file]] | |
// submitImage([file]) | |
} else if(imageUrl.trim()!=='' && imageUrl.includes('http')) { | |
console.log('IS IMAGE URL'); | |
if(!isImgUrl(imageUrl)) { | |
throw new Error('Imagem inválida, tente outra.') | |
} | |
_paramsToSubmit = [null, imageUrl] | |
_previewImage = imageUrl | |
} else if( imageUrl.trim()!=='' && imageUrl.includes('data:') ){ | |
console.log('IS BASE 64'); | |
_paramsToSubmit = [undefined, undefined, imageUrl] | |
_previewImage = imageUrl | |
} else { | |
throw new Error('Imagem inválida, tente outra.') | |
} | |
} | |
return { | |
_paramsToSubmit, | |
_previewImage | |
} | |
} | |
export default function Content() { | |
const { closeModal: _closeModal, newProductData, setNewProductData, inputRef } = useAddProductContext() | |
const { onKeyDownProduct, previewImage, setPreviewImage, getStockMovements, editProductData } = useStockContext() | |
const closeModal = () => { _closeModal(); setNewProductData(defaultProductData); } | |
const onDrop = async (e: any, _file: any)=> { | |
try { | |
const result = await onDropImage(e, _file) | |
setParamsToSubmit(result._paramsToSubmit) | |
setPreviewImage(result._previewImage) | |
}catch(e: any){ | |
notify.error(e.message || 'Erro ao selecionar imagem') | |
} | |
} | |
const onSubmit = async () => { | |
if(loading) return; | |
if(!newProductData.name) { | |
return notify.error('É necessário informar um nome') | |
} | |
console.log('>', editProductData.image_url); | |
console.log('>', newProductData.image_url); | |
const isCreate = !newProductData.id | |
setLoading(true) | |
try { | |
let image; | |
if(isCreate) { | |
image = paramsToSubmit.length ? await submitImage(paramsToSubmit) : null | |
} else if(editProductData.image_url !== newProductData.image_url) { | |
image = paramsToSubmit.length ? await submitImage(paramsToSubmit) : null | |
} | |
axios.post(isCreate ? '/action?table=product&operation=create' : '/action?table=product&operation=update', | |
formatProductDataToSend({ | |
...newProductData, | |
image_url: image || newProductData.image_url | |
}) | |
).then(rst=>{ | |
notify.success(`${isCreate? 'Criado' : 'Atualizado'} com sucesso.`, {position:'bottom-left'}) | |
closeModal() | |
onKeyDownProduct({target:{value:newProductData?.name }} as any) | |
setNewProductData(defaultProductData) | |
getStockMovements() | |
}) | |
.catch(e=>{ | |
if(e.response.data.message) { | |
notify.error(e.response.data.message) | |
} else { | |
notify.error(`Erro ao ${isCreate? 'criar' : 'atualizar'}`) | |
} | |
}) | |
} catch (error) { | |
notify.error('Erro ao salvar imagem, tente novamente ou escolha outra imagem.') | |
return; | |
} finally { | |
setLoading(false) | |
} | |
} | |
const [updatingImage, setUpdatingImage] = useState<boolean>(false); | |
const [loading, setLoading] = useState<boolean>(false); | |
const onDragOver = (e: any) => { | |
e.preventDefault() | |
}; | |
const [paramsToSubmit, setParamsToSubmit] = useState<any>([]) | |
return ( | |
<section> | |
<ModalTop title={ newProductData.id ? 'Atualizar produto' : 'Adicionar produto' } closeModal={closeModal}/> | |
{loading && ( | |
<div className="flex bg-indigo-200 p-2"> | |
<LoadingSpinner color="blue"/> | |
<b className="text-indigo-700"> | |
{newProductData.id ? "Atualizando produto..." : "Criando Produto..."} | |
</b> | |
</div> | |
)} | |
{/* <pre> | |
data: {JSON.stringify(newProductData, null, 2)} | |
</pre> */} | |
<main className="flex flex-col min-h-[50vh]"> | |
<div className="md:flex md:flex-row"> | |
<div className="grow"> | |
<Label text="Nome do produto"/> | |
<Input | |
placeholder="Nome do produto" | |
className="mt-1 mb-1 bg-white" | |
value={newProductData.name} | |
onChange={(e: React.ChangeEvent<HTMLInputElement>)=>setNewProductData({...newProductData, name: e.target.value})} | |
inputReference={inputRef} | |
onKeyDown={(e:any)=>{if(e.key === 'Enter') onSubmit()}} | |
/> | |
</div> | |
<div className="grow md:px-2"> | |
<Label text="Modelo"/> | |
<Input | |
placeholder="Modelo" | |
className="mt-1 mb-1 bg-white" | |
value={newProductData.model} | |
onChange={(e: React.ChangeEvent<HTMLInputElement>)=>setNewProductData({...newProductData, model: e.target.value})} | |
onKeyDown={(e:any)=>{if(e.key === 'Enter') onSubmit()}} | |
/> | |
</div> | |
<div> | |
<Label text="Código de barras"/> | |
<Input | |
placeholder="Código de barras" | |
className="mt-1 mb-1 bg-white" | |
value={newProductData.barcode} | |
onChange={(e: React.ChangeEvent<HTMLInputElement>)=>setNewProductData({...newProductData, barcode: e.target.value})} | |
onKeyDown={(e:any)=>{if(e.key === 'Enter') onSubmit()}} | |
/> | |
</div> | |
</div> | |
<div className="flex flex-col"> | |
<div className="md:flex"> | |
<div className="grow-0"> | |
<Label text="Imagem"/> | |
{ updatingImage ? ( | |
<b className="text-indigo-600"> | |
Fazendo upload da imagem, aguarde um instante... | |
</b> | |
) : ( | |
<b className="text-red-600 pl-2"> | |
{/* Imagem atualizada com sucesso. */} | |
</b> | |
) | |
} | |
<div> | |
<div className="flex items-center justify-center w-full"> | |
<label className="flex flex-col items-center justify-center w-full h-30 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-800 hover:bg-gray-600 " style={{backgroundImage: `url(${previewImage || newProductData.image_url})`, backgroundSize: 'contain' }} onDragOver={onDragOver} onDrop={onDrop as any}> | |
<div className="flex flex-col items-center justify-center pt-5 pb-6"> | |
<svg className="w-8 h-8 mb-4 text-gray-300 text-gray-200" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 16"> | |
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"/> | |
</svg> | |
<p className="mb-2 text-sm text-gray-300"><span className="font-semibold">Clique para fazer upload</span> ou arraste e solte</p> | |
<p className="text-xs text-gray-200">SVG, PNG, JPG or GIF</p> | |
</div> | |
<input | |
id="dropzone-file" | |
type="file" | |
className="hidden" | |
accept="image/*" | |
onChange={(e: any)=>{ | |
onDrop(null,e.target.files[0] ) | |
}} | |
style={{background:'red'}} | |
/> | |
</label> | |
</div> | |
</div> | |
</div> | |
<div className="grow md:pl-2 md:pr-2"> | |
<CategorySelection/> | |
<div> | |
<Label text="Modelos compatíveis"/> | |
<Input | |
placeholder="Modelos compatíveis" | |
className="mt-1 mb-1 mt-2 bg-white" | |
value={newProductData.compatible_models} | |
onChange={(e: React.ChangeEvent<HTMLInputElement>)=>setNewProductData({...newProductData, compatible_models: e.target.value})} | |
/> | |
</div> | |
</div> | |
<div className="grow"> | |
<div className="flex flex-row"> | |
<div className="w-2/3 pr-2"> | |
<Label text="Código do produto"/> | |
<Input | |
placeholder="Código do produto" | |
className="mt-2 pt-2 mb-1 bg-white" | |
value={newProductData.code} | |
onChange={(e: React.ChangeEvent<HTMLInputElement>)=>setNewProductData({...newProductData, code: e.target.value})} | |
onKeyDown={(e:any)=>{if(e.key === 'Enter') onSubmit()}} | |
/> | |
</div> | |
<div className="w-1/3"> | |
<Label text="Unidade"/> | |
<Input | |
placeholder="Unidade" | |
className="mt-2 pt-2 mb-1 bg-white" | |
value={newProductData.un} | |
onChange={(e: React.ChangeEvent<HTMLInputElement>)=>setNewProductData({...newProductData, un: e.target.value})} | |
onKeyDown={(e:any)=>{if(e.key === 'Enter') onSubmit()}} | |
/> | |
</div> | |
</div> | |
<div className="flex"> | |
<div className="grow pr-2"> | |
<Label text="Valor de compra"/> | |
<CurrencyInput | |
value={newProductData.default_buy_price} | |
onChange={(value: number)=>setNewProductData({...newProductData, default_buy_price: value})} | |
className="block w-full md:pr-2 py-3 rounded-md border-0 py-1.5 mt-1 pl-3 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-md sm:leading-6" | |
onKeyDown={(e:any)=>{if(e.key === 'Enter') onSubmit()}} | |
/> | |
</div> | |
<div className="grow"> | |
<Label text="Valor de venda"/> | |
<CurrencyInput | |
value={newProductData.price} | |
onChange={(value: number)=>setNewProductData({...newProductData, price: value})} | |
className="block w-full py-3 rounded-md border-0 py-1.5 mt-1 pl-3 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-md sm:leading-6" | |
onKeyDown={(e:any)=>{if(e.key === 'Enter') onSubmit()}} | |
/> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<> | |
<Divider className="py-2"/> | |
<Title text="Configurações do produto" className="pb-2" /> | |
<div className="md:flex md:flex-row"> | |
<div className="md:w-1/2"> | |
<div className="grow md:flex md:flex-row"> | |
<div className="grow pt-4"> | |
<input | |
type="checkbox" | |
className="w-8 h-8 border-gray-300 rounded cursor-pointer focus:ring-green-600" | |
checked={newProductData.has_stock} | |
onChange={(e: React.ChangeEvent<HTMLInputElement>)=> { | |
setNewProductData({...newProductData, has_stock: e.target.checked } ) | |
}} | |
onKeyDown={(e:any)=>{if(e.key === 'Enter') onSubmit()}} | |
/> | |
<label htmlFor="candidates" className="pl-1 font-medium text-gray-900"> | |
Produto com estoque | |
</label> | |
</div> | |
{newProductData.has_stock && ( | |
<div className="grow pt-4"> | |
<input | |
type="checkbox" | |
className="w-8 h-8 border-gray-300 rounded cursor-pointer focus:ring-green-600" | |
checked={newProductData.is_returnable} | |
onChange={(e: React.ChangeEvent<HTMLInputElement>)=> { | |
setNewProductData({...newProductData, is_returnable: e.target.checked } ) | |
}} | |
onKeyDown={(e:any)=>{if(e.key === 'Enter') onSubmit()}} | |
/> | |
<label htmlFor="candidates" className="pl-1 font-medium text-gray-900"> | |
É retornável | |
</label> | |
</div> | |
)} | |
</div> | |
</div> | |
{!newProductData.id && newProductData.has_stock && ( | |
<div className="md:w-1/2"> | |
<div className="pr-2 flex flex-col"> | |
<Label text="Quantidade inicial no estoque"/> | |
<Input | |
placeholder="Quantidade inicial no estoque" | |
type="number" | |
className="mt-1 mb-1 mt-2 bg-white" | |
value={newProductData.stock_initial_quantity} | |
onChange={(e: React.ChangeEvent<HTMLInputElement>)=>setNewProductData({...newProductData, stock_initial_quantity: parseFloat(e.target.value) || 0})} | |
/> | |
</div> | |
</div> | |
)} | |
{newProductData.has_stock && ( | |
<div className="md:w-1/2"> | |
<div className="pr-2 flex flex-col"> | |
<Label text="Estoque mínimo(Para criar alertas)"/> | |
<Input | |
placeholder="Estoque mínimo" | |
type="number" | |
className="mt-1 mb-1 mt-2 bg-white" | |
value={newProductData.minimum_stock} | |
onChange={(e: React.ChangeEvent<HTMLInputElement>)=>setNewProductData({...newProductData, minimum_stock: parseFloat(e.target.value)})} | |
/> | |
</div> | |
</div> | |
)} | |
</div> | |
<div className="md:flex"> | |
{newProductData.has_stock && newProductData.is_returnable && ( | |
<div className="md:w-1/2"> | |
<ProductSelection | |
fieldName="Selecione o vasilhame ou deixe em branco caso seja o líquido." | |
setSelectedExternal={(v: any)=>setNewProductData({...newProductData, container: v})} | |
valueExternal={newProductData.container} | |
className="md:pr-2" | |
/> | |
</div> | |
)} | |
{newProductData.has_stock && newProductData.is_returnable && ( | |
<div className="md:w-1/2"> | |
<Label text="Nome do produto para ser usado em listagens"/> | |
<Input | |
placeholder="Nome do produto para ser usado em listagens ( Completo, ex: P13 )" | |
className="mt-1 mb-1 pr-2 mt-2 bg-white" | |
value={newProductData.has_container_product_name} | |
onChange={(e: React.ChangeEvent<HTMLInputElement>)=>setNewProductData({...newProductData, has_container_product_name: e.target.value })} | |
/> | |
</div> | |
)} | |
</div> | |
</> | |
<Divider className="py-2"/> | |
<Title text="Informações tributárias" className="pb-2"/> | |
<div className="flex flex-row md:w-1/2"> | |
<div className="grow pr-2"> | |
<Label text="NCM"/> | |
<Input | |
placeholder="NCM" | |
className="mt-1 mb-1 bg-white" | |
value={newProductData.ncmsh} | |
onChange={(e: React.ChangeEvent<HTMLInputElement>)=>setNewProductData({...newProductData, ncmsh: e.target.value})} | |
/> | |
</div> | |
<div className="grow pr-2"> | |
<Label text="CFOP"/> | |
<Input | |
placeholder="CFOP" | |
className="mt-1 mb-1 bg-white" | |
value={newProductData.cfop} | |
onChange={(e: React.ChangeEvent<HTMLInputElement>)=>setNewProductData({...newProductData, cfop: e.target.value})} | |
/> | |
</div> | |
<div className="grow"> | |
<Label text="CST"/> | |
<Input | |
placeholder="CST" | |
className="mt-1 mb-1 bg-white" | |
value={newProductData.cst} | |
onChange={(e: React.ChangeEvent<HTMLInputElement>)=>setNewProductData({...newProductData, cst: e.target.value})} | |
/> | |
</div> | |
</div> | |
</main> | |
{!updatingImage ? | |
<div className="bottom-0 pt-3 flex"> | |
<div className="flex-1 flex flex-row pr-2 justify-end items-center"> | |
<ButtonWithIcon | |
title="Fechar" | |
icon="TimesIcon" | |
onClick={closeModal} | |
color="gray" | |
paddingY="2" | |
/> | |
<ButtonWithIcon | |
onClick={onSubmit} | |
title="Salvar" | |
color="indigo" | |
icon="CheckIcon" | |
paddingY="2" | |
/> | |
</div> | |
</div> : | |
'Aguarde um instante enquanto a imagem é salva.' | |
} | |
</section> | |
) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment