| @ -0,0 +1,38 @@ | |||
| import NewCliente from "./pages/Cliente/NewCliente"; | |||
| import PageBase from "./pages/PageBase/PageBase"; | |||
| import Clientes from "./pages/Cliente/Clientes"; | |||
| import NewProduto from "./pages/Produto/NewProduto"; | |||
| import Produtos from "./pages/Produto/Produtos"; | |||
| import NewEncomenda from "./pages/Encomenda/NewEncomenda"; | |||
| export const Routes = [ | |||
| { | |||
| path: "/", | |||
| element: <PageBase />, | |||
| children: [ | |||
| { | |||
| path: "/cliente/new", | |||
| element: <NewCliente /> | |||
| }, | |||
| { | |||
| path: "/cliente/list", | |||
| element: <Clientes /> | |||
| }, | |||
| //produto routes | |||
| { | |||
| path: "/produto/new", | |||
| element: <NewProduto /> | |||
| }, | |||
| { | |||
| path: "/produto/list", | |||
| element: <Produtos /> | |||
| }, | |||
| //encomeda routes | |||
| { | |||
| path: "/encomenda/new", | |||
| element: <NewEncomenda/> | |||
| } | |||
| ] | |||
| } | |||
| ] | |||
| @ -0,0 +1,102 @@ | |||
| import { Input, Select, Button, Form } from "antd"; | |||
| import { Produto } from "../interfaces/Produto"; | |||
| import styles from '../pages/Produto/produto.module.css' | |||
| import { | |||
| getAllCategoria | |||
| } from "../slices/ProdutoSlice"; | |||
| import { useDispatch, useSelector } from "react-redux"; | |||
| import { useEffect } from "react"; | |||
| interface FormProdutoProps { | |||
| produto?:Produto, | |||
| btnTitle:string, | |||
| eventEmitter:any | |||
| } | |||
| function ProdutoForm( props:FormProdutoProps ) { | |||
| const dispatch = useDispatch<any>(); | |||
| const { categorias, loading } = useSelector((state:any) => state.produto); | |||
| const onFinish = (values: any) => { | |||
| const produto:Produto = { | |||
| nome:values.nome, | |||
| preco:values.preco, | |||
| categoria:{ | |||
| uid:values.categoria | |||
| } | |||
| } | |||
| props.eventEmitter(produto); | |||
| }; | |||
| const onFinishFailed = (errorInfo: any) => { | |||
| console.log('Failed:', errorInfo); | |||
| }; | |||
| useEffect(() => { | |||
| dispatch(getAllCategoria()) | |||
| },[]) | |||
| return ( | |||
| <> | |||
| <Form | |||
| className={styles.form_produto} | |||
| name="basic" | |||
| layout="vertical" | |||
| initialValues={{ remember: true }} | |||
| onFinish={onFinish} | |||
| onFinishFailed={onFinishFailed} | |||
| autoComplete="off" | |||
| > | |||
| <Form.Item | |||
| label="Nome" | |||
| name="nome" | |||
| initialValue={props.produto?.nome ? props.produto.nome : ''} | |||
| rules={[{ required: true, message: 'Nome é obrigatorio!' }]} | |||
| > | |||
| <Input /> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="Preço" | |||
| name="preco" | |||
| initialValue={props.produto?.preco ? props.produto.preco : ''} | |||
| rules={[{ required: true, message: 'preço é obrigatorio!' }]} | |||
| > | |||
| <Input type='number' prefix="R$"/> | |||
| </Form.Item> | |||
| <Form.Item | |||
| label="Categoria" | |||
| name="categoria" | |||
| rules={[{ required: true, message: 'É obrigatorio informar a categria!' }]} | |||
| > | |||
| <Select | |||
| options={ | |||
| categorias.map((categoria: any) => ( | |||
| { value: categoria.uid, label: categoria.nome } | |||
| )) | |||
| } | |||
| /> | |||
| </Form.Item> | |||
| <Form.Item> | |||
| {(loading === false) && ( | |||
| <Button type="primary" htmlType="submit"> | |||
| {props.btnTitle} | |||
| </Button> | |||
| )} | |||
| {(loading === true) && ( | |||
| <Button type="primary" loading> | |||
| Aguarde... | |||
| </Button> | |||
| )} | |||
| </Form.Item> | |||
| </Form> | |||
| </> | |||
| ) | |||
| } | |||
| export default ProdutoForm; | |||
| @ -0,0 +1,6 @@ | |||
| import { Produto } from "./Produto"; | |||
| export interface ItemCart { | |||
| produto:Produto, | |||
| quantidade:number | |||
| } | |||
| @ -0,0 +1,9 @@ | |||
| export interface Produto { | |||
| uid?:string, | |||
| nome:string, | |||
| preco:number, | |||
| categoria:{ | |||
| uid:string, | |||
| nome?:string | |||
| } | |||
| } | |||
| @ -0,0 +1,75 @@ | |||
| import { Button, Collapse, Popconfirm, Space, Table } from 'antd'; | |||
| import styles from './encomenda.module.css'; | |||
| import Column from 'antd/es/table/Column'; | |||
| import { useSelector } from 'react-redux'; | |||
| import { ItemCart } from '../../interfaces/ItemCart'; | |||
| import { | |||
| DeleteOutlined | |||
| } from '@ant-design/icons'; | |||
| function NewEncomenda() { | |||
| const { itens } = useSelector((state: any) => state.cart); | |||
| const handleDelete = () => { | |||
| } | |||
| return ( | |||
| <div className={styles.container_table}> | |||
| <Collapse | |||
| size="large" | |||
| items={[{ | |||
| key: '1', label: 'Itens da encomenda.', children: ( | |||
| <Table | |||
| dataSource={ | |||
| itens.map((item: ItemCart) => ( | |||
| { key: item.produto.uid, name: item.produto.nome, quantidade: item.quantidade, preco: item.produto.preco } | |||
| )) | |||
| } | |||
| pagination={false} | |||
| bordered={true} | |||
| scroll={{ x: 300, y: 300 }} | |||
| > | |||
| <Column title="Nome" dataIndex="name" key="name" /> | |||
| <Column title="Quantidade" dataIndex="quantidade" key="quantidade" /> | |||
| <Column title="Preço" dataIndex="preco" key="preco" /> | |||
| <Column | |||
| title="Actions" | |||
| key="action" | |||
| render={(_: any) => ( | |||
| <Space size="middle"> | |||
| <Popconfirm | |||
| title="Atenção:" | |||
| description="Confirme a ação de delete!" | |||
| onConfirm={() => { handleDelete() }} | |||
| okText="Sim" | |||
| cancelText="Não" | |||
| > | |||
| <Button value="small" | |||
| type="primary" | |||
| danger icon={<DeleteOutlined />} | |||
| size="large" | |||
| title="Deletar" | |||
| /> | |||
| </Popconfirm> | |||
| </Space> | |||
| )} | |||
| /> | |||
| </Table> | |||
| ) | |||
| }]} | |||
| /> | |||
| </div> | |||
| ) | |||
| } | |||
| export default NewEncomenda; | |||
| @ -0,0 +1,18 @@ | |||
| .container_table { | |||
| width: 800px; | |||
| height: 100%; | |||
| margin-top: 3em; | |||
| display: flex; | |||
| flex-direction: column; | |||
| } | |||
| .container_table__filter{ | |||
| width: 100%; | |||
| display: flex; | |||
| justify-content: end; | |||
| margin-bottom: 1em; | |||
| } | |||
| .container_table__search{ | |||
| width: 300px; | |||
| } | |||
| @ -0,0 +1,23 @@ | |||
| import { useDispatch } from "react-redux"; | |||
| import ProdutoForm from "../../components/ProdutoForm"; | |||
| import { Produto } from "../../interfaces/Produto"; | |||
| import { | |||
| registerProduto | |||
| } from "../../slices/ProdutoSlice"; | |||
| function NewProduto () { | |||
| const dispatch = useDispatch<any>(); | |||
| const handleSubmit = (produto:Produto) => { | |||
| dispatch(registerProduto(produto)); | |||
| } | |||
| return ( | |||
| <> | |||
| <ProdutoForm btnTitle="Cadastrar" eventEmitter={handleSubmit}/> | |||
| </> | |||
| ) | |||
| } | |||
| export default NewProduto; | |||
| @ -0,0 +1,203 @@ | |||
| import Search from 'antd/es/input/Search' | |||
| import styles from './produto.module.css'; | |||
| import { Button, Popconfirm, Select, Space, Table } from 'antd' | |||
| import Column from 'antd/es/table/Column' | |||
| import { useDispatch, useSelector } from 'react-redux'; | |||
| import { ParamsType } from '../../interfaces/ParamsType'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { Produto } from '../../interfaces/Produto'; | |||
| import { | |||
| DeleteOutlined, | |||
| EditOutlined, | |||
| ShoppingCartOutlined | |||
| } from '@ant-design/icons'; | |||
| import { | |||
| getAllProdutos, | |||
| getAllCategoria, | |||
| deleteProduto | |||
| } from '../../slices/ProdutoSlice'; | |||
| import { | |||
| addItem | |||
| } from '../../slices/CarrinhoSlice'; | |||
| import { useNavigate } from 'react-router-dom'; | |||
| import { ItemCart } from '../../interfaces/ItemCart'; | |||
| function Produtos() { | |||
| const [nomeSearch, setNomeSearch] = useState(''); | |||
| const [categoriaFilter, setCategoriaFilter] = useState(''); | |||
| const dispatch = useDispatch<any>(); | |||
| const navigate = useNavigate(); | |||
| const { produtos, total, loading, categorias } = useSelector((state: any) => state.produto); | |||
| const handleSearch = () => { | |||
| const params: ParamsType = { | |||
| pagination: { | |||
| current: 1, | |||
| pageSize: 5 | |||
| }, | |||
| filter: { | |||
| nome: nomeSearch, | |||
| categoria: categoriaFilter | |||
| } | |||
| } | |||
| dispatch(getAllProdutos(params)); | |||
| } | |||
| const handleDelete = (uid: string) => { | |||
| dispatch(deleteProduto(uid)); | |||
| } | |||
| const onChangePage = (page: any, size: any) => { | |||
| const params: ParamsType = { | |||
| pagination: { | |||
| current: page, | |||
| pageSize: size | |||
| }, | |||
| filter: { | |||
| nome: nomeSearch != '' ? nomeSearch : '', | |||
| categoria: categoriaFilter | |||
| } | |||
| } | |||
| dispatch(getAllProdutos(params)); | |||
| } | |||
| const filterByCategoria = (categoriaUid: string) => { | |||
| setCategoriaFilter(categoriaUid); | |||
| const params: ParamsType = { | |||
| pagination: { | |||
| current: 1, | |||
| pageSize: 5 | |||
| }, | |||
| filter: { | |||
| nome: nomeSearch != '' ? nomeSearch : '', | |||
| categoria: categoriaUid | |||
| } | |||
| } | |||
| dispatch(getAllProdutos(params)) | |||
| } | |||
| const addItemCart = (produto: any) => { | |||
| const item:ItemCart = { | |||
| produto:{ | |||
| uid:produto.key, | |||
| nome:produto.name, | |||
| preco:produto.preco, | |||
| categoria:{ | |||
| uid:"" | |||
| } | |||
| }, | |||
| quantidade:1 | |||
| } | |||
| dispatch(addItem(item)); | |||
| } | |||
| useEffect(() => { | |||
| dispatch(getAllProdutos( | |||
| { pagination: { current: 1, pageSize: 5 } } | |||
| )) | |||
| dispatch(getAllCategoria()); | |||
| }, []) | |||
| return ( | |||
| <div className={styles.container_table}> | |||
| <div className={styles.container_table__filter}> | |||
| <Select | |||
| style={{ width: 120 }} | |||
| defaultValue='' | |||
| options={ | |||
| categorias.map((categoria: any) => ( | |||
| { value: categoria.uid, label: categoria.nome } | |||
| )).concat({ value: '', label: 'Todos' }) | |||
| } | |||
| onChange={filterByCategoria} | |||
| /> | |||
| <Search | |||
| placeholder="Buscar por nome..." | |||
| enterButton | |||
| className={styles.container_table__search} | |||
| onSearch={handleSearch} | |||
| onChange={(value) => setNomeSearch(value.target.value)} | |||
| /> | |||
| </div> | |||
| <Table | |||
| dataSource={ | |||
| produtos.map((produto: Produto) => ( | |||
| { key: produto.uid, name: produto.nome, preco: produto.preco, categoria: produto.categoria.nome } | |||
| )) | |||
| } | |||
| pagination={{ | |||
| position: ["bottomCenter"], | |||
| total: total, | |||
| defaultPageSize: 5, | |||
| onChange(page, pageSize) { | |||
| onChangePage(page, pageSize) | |||
| }, | |||
| pageSizeOptions: [5, 10, 15], | |||
| showSizeChanger: true, | |||
| onShowSizeChange(current, size) { | |||
| onChangePage(current, size) | |||
| }, | |||
| }} | |||
| loading={loading} | |||
| bordered={true} | |||
| > | |||
| <Column title="Nome" dataIndex="name" key="name" /> | |||
| <Column title="Preço" dataIndex="preco" key="preco" /> | |||
| <Column title="Categoria" dataIndex="categoria" key="categoria" /> | |||
| <Column | |||
| title="Actions" | |||
| key="action" | |||
| render={(_: any, record: any) => ( | |||
| <Space size="middle"> | |||
| <Popconfirm | |||
| title="Atenção:" | |||
| description="Confirme a ação de delete!" | |||
| onConfirm={() => { handleDelete(record.key) }} | |||
| okText="Sim" | |||
| cancelText="Não" | |||
| > | |||
| <Button value="small" | |||
| type="primary" | |||
| danger icon={<DeleteOutlined />} | |||
| size="large" | |||
| title="Deletar" | |||
| /> | |||
| </Popconfirm> | |||
| <Button | |||
| value="small" | |||
| type="primary" | |||
| icon={<EditOutlined />} size="large" | |||
| title="Atualizar" | |||
| onClick={() => { navigate(`/cliente/new`) }} | |||
| /> | |||
| <Button | |||
| value="small" | |||
| type="primary" | |||
| icon={<ShoppingCartOutlined />} size="large" | |||
| title="AddCart" | |||
| onClick={() => { addItemCart(record) }} | |||
| /> | |||
| </Space> | |||
| )} | |||
| /> | |||
| </Table> | |||
| </div> | |||
| ) | |||
| } | |||
| export default Produtos; | |||
| @ -0,0 +1,20 @@ | |||
| .form_produto{ | |||
| width: 600px; | |||
| } | |||
| .container_table { | |||
| width: 800px; | |||
| display: flex; | |||
| flex-direction: column; | |||
| } | |||
| .container_table__filter{ | |||
| width: 100%; | |||
| display: flex; | |||
| justify-content: space-between; | |||
| margin-bottom: 1em; | |||
| } | |||
| .container_table__search{ | |||
| width: 300px; | |||
| } | |||
| @ -0,0 +1,71 @@ | |||
| import { Environments } from "../environments"; | |||
| import { ParamsType } from "../interfaces/ParamsType"; | |||
| import { Produto } from "../interfaces/Produto"; | |||
| const URLApi = Environments.URLApi; | |||
| const getAllCategorias = async () => { | |||
| const data = await fetch(`${URLApi}/categoria`) | |||
| .then((res)=>res.json()) | |||
| .catch((err)=>err); | |||
| return data; | |||
| } | |||
| const registerProduto = async (produto:Produto) => { | |||
| const config = { | |||
| method:"POST", | |||
| body:JSON.stringify(produto), | |||
| headers: { | |||
| "Content-type":"application/json", | |||
| } | |||
| } | |||
| try { | |||
| const data = await fetch(`${URLApi}/produto`,config) | |||
| .then((res)=>res.json()) | |||
| .catch((err)=>err); | |||
| return data; | |||
| } catch (error) { | |||
| console.info(error); | |||
| return error; | |||
| } | |||
| } | |||
| const getAllProdutos = async (params:ParamsType) => { | |||
| const config = { | |||
| method:"POST", | |||
| body:JSON.stringify(params), | |||
| headers: { | |||
| "Content-type":"application/json", | |||
| } | |||
| } | |||
| const data = await fetch (`${URLApi}/produto/list`,config) | |||
| .then((res)=>res.json()) | |||
| .catch((err)=>err); | |||
| return data; | |||
| } | |||
| const deleteProduto = async (uid:string) => { | |||
| try { | |||
| const data = await fetch(`${URLApi}/produto?uid=${uid}`,{method:'DELETE'}) | |||
| .then((res)=>res.json()) | |||
| .catch((err)=> err); | |||
| return data; | |||
| } catch (error) { | |||
| console.info(error); | |||
| } | |||
| } | |||
| export const ProdutoService = { | |||
| getAllCategorias, | |||
| registerProduto, | |||
| getAllProdutos, | |||
| deleteProduto | |||
| } | |||
| @ -0,0 +1,51 @@ | |||
| import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; | |||
| import { ItemCart } from "../interfaces/ItemCart"; | |||
| import { notify } from "../hooks/useToastfy"; | |||
| const initialState = { | |||
| itens:[], | |||
| valorTotal:0 | |||
| } | |||
| const calclQuantidade = (itens:Array<ItemCart>) =>{ | |||
| let valor = 0; | |||
| itens.map((item) => { | |||
| valor += Number(item.produto.preco); | |||
| }) | |||
| return valor; | |||
| } | |||
| export const addItem = createAsyncThunk( | |||
| "cart/add", | |||
| async (item:ItemCart) => { | |||
| return item; | |||
| } | |||
| ) | |||
| export const cartSlice = createSlice({ | |||
| name:"cliente", | |||
| initialState, | |||
| reducers:{ | |||
| resetState:(state)=>{ | |||
| state.itens = [], | |||
| state.valorTotal=0 | |||
| }, | |||
| }, | |||
| extraReducers(builder) { | |||
| builder | |||
| .addCase(addItem.fulfilled,(state,action:any)=>{ | |||
| state.itens = state.itens.concat(action.payload); | |||
| state.valorTotal = calclQuantidade(state.itens); | |||
| notify('Produto adicionado com sucessso!','success') | |||
| }) | |||
| }, | |||
| }); | |||
| export const { resetState } = cartSlice.actions; | |||
| export default cartSlice.reducer; | |||
| @ -0,0 +1,117 @@ | |||
| import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; | |||
| import { ProdutoService } from "../services/ProdutoService"; | |||
| import { Produto } from "../interfaces/Produto"; | |||
| import { notify } from "../hooks/useToastfy"; | |||
| import { ParamsType } from "../interfaces/ParamsType"; | |||
| const initialState = { | |||
| produtos:[] as any, | |||
| total: 0, | |||
| categorias:[] as any, | |||
| loading:false, | |||
| error:false, | |||
| message:'' | |||
| } | |||
| export const getAllCategoria = createAsyncThunk( | |||
| "produto/categorias", | |||
| async () =>{ | |||
| const data = await ProdutoService.getAllCategorias(); | |||
| return data; | |||
| } | |||
| ) | |||
| export const registerProduto = createAsyncThunk( | |||
| "produto/new", | |||
| async (produto:Produto, thunkApi) =>{ | |||
| const data = await ProdutoService.registerProduto(produto); | |||
| if (data.error) { | |||
| return thunkApi.rejectWithValue(data); | |||
| } | |||
| return data; | |||
| } | |||
| ) | |||
| export const getAllProdutos = createAsyncThunk( | |||
| "produto/getAll", | |||
| async (parans:ParamsType) => { | |||
| const data = await ProdutoService.getAllProdutos(parans); | |||
| return data; | |||
| } | |||
| ) | |||
| export const deleteProduto = createAsyncThunk( | |||
| "produto/delete", | |||
| async (uid:string, thunkApi) => { | |||
| const data = await ProdutoService.deleteProduto(uid); | |||
| if (data.error) { | |||
| return thunkApi.rejectWithValue(data); | |||
| } | |||
| return data; | |||
| } | |||
| ) | |||
| export const produtoSlice = createSlice({ | |||
| name:"produto", | |||
| initialState, | |||
| reducers:{ | |||
| resetState:(state)=>{ | |||
| state.loading = false; | |||
| state.error = false; | |||
| state.message = ''; | |||
| }, | |||
| }, | |||
| extraReducers(builder) { | |||
| builder | |||
| .addCase(getAllCategoria.pending,(state)=>{ | |||
| state.loading = true; | |||
| }).addCase(getAllCategoria.fulfilled,(state,action)=>{ | |||
| state.categorias = action.payload.result; | |||
| state.loading = false; | |||
| }) | |||
| .addCase(registerProduto.pending,(state)=>{ | |||
| state.loading = true; | |||
| }).addCase(registerProduto.fulfilled,(state)=>{ | |||
| state.loading = false; | |||
| state.message = 'Novo produto cadastrado!'; | |||
| notify(state.message,'success'); | |||
| }).addCase(registerProduto.rejected,(state, action:any)=>{ | |||
| state.loading = false; | |||
| state.error = true; | |||
| state.message = action.payload.message ? action.payload.message : 'Error ao cadastrar produto, tente novamente mais tarde!'; | |||
| notify(state.message, 'error'); | |||
| }) | |||
| .addCase(getAllProdutos.pending,(state)=>{ | |||
| state.loading = true; | |||
| }).addCase(getAllProdutos.fulfilled,(state,action)=>{ | |||
| state.produtos = action.payload.result; | |||
| state.loading = false; | |||
| state.total = action.payload.total; | |||
| }) | |||
| .addCase(deleteProduto.pending,(state)=>{ | |||
| state.loading = true; | |||
| }).addCase(deleteProduto.fulfilled,(state,action:any)=>{ | |||
| state.loading = false; | |||
| state.message = 'Produo deletado com sucesso!'; | |||
| state.produtos = state.produtos.filter((produto:Produto) => (produto.uid != action.payload.uid)); | |||
| notify(state.message,'success'); | |||
| }).addCase(deleteProduto.rejected,(state, action:any)=>{ | |||
| state.loading = false; | |||
| state.error = true; | |||
| state.message = action.payload.message ? action.payload.message : 'Error ao deletar produto, tente novamente mais tarde!'; | |||
| notify(state.message, 'error'); | |||
| }) | |||
| }, | |||
| }); | |||
| export const { resetState } = produtoSlice.actions; | |||
| export default produtoSlice.reducer; | |||