From 447a3d4d36c72bc2519a11d8f3f76d1a390b6c0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodolfo=20Sousa=20=E2=9C=8C=F0=9F=8F=BC?= Date: Fri, 14 Nov 2025 10:17:42 -0300 Subject: [PATCH 1/5] add telas interativas --- pom.xml | 13 +- .../sistema_vendas_api/config/CorsConfig.java | 26 ++ .../controller/ClienteController.java | 2 +- .../controller/PedidoController.java | 2 +- .../controller/ProdutoController.java | 2 +- .../controller/TesteController.java | 9 +- .../controller/WebController.java | 29 ++ src/main/resources/application.properties | 4 +- src/main/resources/static/css/style.css | 252 ++++++++++++++++++ src/main/resources/static/js/clientes.js | 127 +++++++++ src/main/resources/static/js/pedidos.js | 211 +++++++++++++++ src/main/resources/static/js/produtos.js | 127 +++++++++ src/main/resources/templates/clientes.html | 47 ++++ src/main/resources/templates/index.html | 42 +++ src/main/resources/templates/pedidos.html | 60 +++++ src/main/resources/templates/produtos.html | 51 ++++ 16 files changed, 991 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/example/sistema_vendas_api/config/CorsConfig.java create mode 100644 src/main/java/com/example/sistema_vendas_api/controller/WebController.java create mode 100644 src/main/resources/static/css/style.css create mode 100644 src/main/resources/static/js/clientes.js create mode 100644 src/main/resources/static/js/pedidos.js create mode 100644 src/main/resources/static/js/produtos.js create mode 100644 src/main/resources/templates/clientes.html create mode 100644 src/main/resources/templates/index.html create mode 100644 src/main/resources/templates/pedidos.html create mode 100644 src/main/resources/templates/produtos.html diff --git a/pom.xml b/pom.xml index 1d3502e..6a0ce9f 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.example sistema-vendas-api - 0.0.1-SNAPSHOT + 1.0.0-SNAPSHOT sistema-vendas-api Demo project for Spring Boot @@ -30,6 +30,12 @@ 17 + + + org.springframework.boot + spring-boot-starter-actuator + + org.springframework.boot spring-boot-starter-data-jpa @@ -44,6 +50,11 @@ spring-boot-starter-validation + + org.springframework.boot + spring-boot-starter-thymeleaf + + org.postgresql postgresql diff --git a/src/main/java/com/example/sistema_vendas_api/config/CorsConfig.java b/src/main/java/com/example/sistema_vendas_api/config/CorsConfig.java new file mode 100644 index 0000000..a43246d --- /dev/null +++ b/src/main/java/com/example/sistema_vendas_api/config/CorsConfig.java @@ -0,0 +1,26 @@ +package com.example.sistema_vendas_api.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.lang.NonNull; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class CorsConfig { + + @Bean + public WebMvcConfigurer corsConfigurer() { + return new WebMvcConfigurer() { + @Override + public void addCorsMappings(@NonNull CorsRegistry registry) { + registry.addMapping("/api/**") + .allowedOrigins("http://localhost:8080") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true); + } + }; + } +} + diff --git a/src/main/java/com/example/sistema_vendas_api/controller/ClienteController.java b/src/main/java/com/example/sistema_vendas_api/controller/ClienteController.java index 97b990c..d9a2552 100644 --- a/src/main/java/com/example/sistema_vendas_api/controller/ClienteController.java +++ b/src/main/java/com/example/sistema_vendas_api/controller/ClienteController.java @@ -25,7 +25,7 @@ import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/clientes") +@RequestMapping("/api/clientes") public class ClienteController { @Autowired private ClienteRepository clienteRepository; diff --git a/src/main/java/com/example/sistema_vendas_api/controller/PedidoController.java b/src/main/java/com/example/sistema_vendas_api/controller/PedidoController.java index b2eb3e3..14c8df1 100644 --- a/src/main/java/com/example/sistema_vendas_api/controller/PedidoController.java +++ b/src/main/java/com/example/sistema_vendas_api/controller/PedidoController.java @@ -14,7 +14,7 @@ import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/pedidos") +@RequestMapping("/api/pedidos") public class PedidoController { @Autowired private PedidoService pedidoService; diff --git a/src/main/java/com/example/sistema_vendas_api/controller/ProdutoController.java b/src/main/java/com/example/sistema_vendas_api/controller/ProdutoController.java index 7a66888..5d1dc81 100644 --- a/src/main/java/com/example/sistema_vendas_api/controller/ProdutoController.java +++ b/src/main/java/com/example/sistema_vendas_api/controller/ProdutoController.java @@ -22,7 +22,7 @@ import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/produtos") +@RequestMapping("/api/produtos") public class ProdutoController { @Autowired private ProdutoRepository produtoRepository; diff --git a/src/main/java/com/example/sistema_vendas_api/controller/TesteController.java b/src/main/java/com/example/sistema_vendas_api/controller/TesteController.java index 15426b9..767f87b 100644 --- a/src/main/java/com/example/sistema_vendas_api/controller/TesteController.java +++ b/src/main/java/com/example/sistema_vendas_api/controller/TesteController.java @@ -4,13 +4,8 @@ @RestController public class TesteController { - @GetMapping("/") - public String home() { - return "šŸš€ API Sistema de Vendas funcionando! gg porraaaaaaaaaaaaaaaa !!!!!!"; - } - - @GetMapping("/teste") + @GetMapping("/api/teste") public String teste() { - return "āœ… Endpoint /teste funcionando corretamente!"; + return "āœ… Endpoint /api/teste funcionando corretamente!"; } } diff --git a/src/main/java/com/example/sistema_vendas_api/controller/WebController.java b/src/main/java/com/example/sistema_vendas_api/controller/WebController.java new file mode 100644 index 0000000..0ab3569 --- /dev/null +++ b/src/main/java/com/example/sistema_vendas_api/controller/WebController.java @@ -0,0 +1,29 @@ +package com.example.sistema_vendas_api.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class WebController { + + @GetMapping("/") + public String home() { + return "index"; + } + + @GetMapping("/clientes") + public String clientes() { + return "clientes"; + } + + @GetMapping("/produtos") + public String produtos() { + return "produtos"; + } + + @GetMapping("/pedidos") + public String pedidos() { + return "pedidos"; + } +} + diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 6bf4566..a259236 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,4 +1,4 @@ -# --- Configura��o do Banco de Dados PostgreSQL --- +# --- Configuraçãclso do Banco de Dados PostgreSQL --- # Altere "seu_usuario_postgres" e "sua_senha_postgres" # com as suas credenciais reais do PostgreSQL. @@ -7,7 +7,7 @@ spring.datasource.username=postgres spring.datasource.password=12345 spring.datasource.driver-class-name=org.postgresql.Driver -# --- Configura��es do JPA (Hibernate) --- +# --- ConfiguraƧƵes do JPA (Hibernate) --- spring.jpa.show-sql=true spring.jpa.hibernate.ddl-auto=update diff --git a/src/main/resources/static/css/style.css b/src/main/resources/static/css/style.css new file mode 100644 index 0000000..5318f82 --- /dev/null +++ b/src/main/resources/static/css/style.css @@ -0,0 +1,252 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + min-height: 100vh; + padding: 20px; +} + +.container { + max-width: 1200px; + margin: 0 auto; +} + +header { + background: white; + padding: 30px; + border-radius: 10px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + margin-bottom: 30px; + display: flex; + justify-content: space-between; + align-items: center; +} + +header h1 { + color: #333; + font-size: 2em; +} + +.subtitle { + color: #666; + margin-top: 10px; +} + +.menu { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 20px; + margin-bottom: 30px; +} + +.card { + background: white; + padding: 40px; + border-radius: 10px; + text-decoration: none; + color: inherit; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + transition: transform 0.3s, box-shadow 0.3s; + text-align: center; +} + +.card:hover { + transform: translateY(-5px); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); +} + +.card-icon { + font-size: 4em; + margin-bottom: 20px; +} + +.card h2 { + color: #333; + margin-bottom: 10px; +} + +.card p { + color: #666; +} + +.content { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 30px; +} + +.form-section, .list-section { + background: white; + padding: 30px; + border-radius: 10px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.form-section h2, .list-section h2 { + color: #333; + margin-bottom: 20px; + padding-bottom: 10px; + border-bottom: 2px solid #667eea; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + margin-bottom: 5px; + color: #333; + font-weight: 500; +} + +.form-group input, +.form-group select, +.form-group textarea { + width: 100%; + padding: 10px; + border: 2px solid #e0e0e0; + border-radius: 5px; + font-size: 1em; + transition: border-color 0.3s; +} + +.form-group input:focus, +.form-group select:focus, +.form-group textarea:focus { + outline: none; + border-color: #667eea; +} + +.btn-primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 12px 30px; + border: none; + border-radius: 5px; + font-size: 1em; + cursor: pointer; + transition: transform 0.2s; + width: 100%; +} + +.btn-primary:hover { + transform: scale(1.05); +} + +.btn-secondary { + background: #6c757d; + color: white; + padding: 10px 20px; + border: none; + border-radius: 5px; + text-decoration: none; + display: inline-block; + transition: background 0.3s; +} + +.btn-secondary:hover { + background: #5a6268; +} + +.btn-remove { + background: #dc3545; + color: white; + padding: 8px 15px; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 0.9em; +} + +.btn-remove:hover { + background: #c82333; +} + +.table-container { + margin-top: 20px; +} + +table { + width: 100%; + border-collapse: collapse; + margin-top: 20px; +} + +table th, +table td { + padding: 12px; + text-align: left; + border-bottom: 1px solid #e0e0e0; +} + +table th { + background: #f8f9fa; + color: #333; + font-weight: 600; +} + +table tr:hover { + background: #f8f9fa; +} + +.loading { + text-align: center; + padding: 20px; + color: #666; +} + +.error { + background: #f8d7da; + color: #721c24; + padding: 15px; + border-radius: 5px; + margin-bottom: 20px; +} + +.success { + background: #d4edda; + color: #155724; + padding: 15px; + border-radius: 5px; + margin-bottom: 20px; +} + +.item-pedido { + background: #f8f9fa; + padding: 15px; + border-radius: 5px; + margin-bottom: 15px; + display: grid; + grid-template-columns: 2fr 1fr auto; + gap: 15px; + align-items: end; +} + +footer { + text-align: center; + color: white; + margin-top: 30px; + padding: 20px; +} + +@media (max-width: 768px) { + .content { + grid-template-columns: 1fr; + } + + header { + flex-direction: column; + gap: 15px; + } + + .item-pedido { + grid-template-columns: 1fr; + } +} + diff --git a/src/main/resources/static/js/clientes.js b/src/main/resources/static/js/clientes.js new file mode 100644 index 0000000..13b41b7 --- /dev/null +++ b/src/main/resources/static/js/clientes.js @@ -0,0 +1,127 @@ +const API_URL = 'http://localhost:8080/api'; + +// Carregar clientes ao carregar a pĆ”gina +document.addEventListener('DOMContentLoaded', () => { + carregarClientes(); + document.getElementById('formCliente').addEventListener('submit', cadastrarCliente); +}); + +async function carregarClientes() { + const loading = document.getElementById('loading'); + const clientesList = document.getElementById('clientesList'); + + try { + loading.style.display = 'block'; + const response = await fetch(`${API_URL}/clientes`); + const clientes = await response.json(); + + loading.style.display = 'none'; + + if (clientes.length === 0) { + clientesList.innerHTML = '

Nenhum cliente cadastrado ainda.

'; + return; + } + + let html = ''; + + clientes.forEach(cliente => { + html += ` + + + + + + + + `; + }); + + html += '
IDNomeE-mailTelefoneAƧƵes
${cliente.id}${cliente.nome}${cliente.email}${cliente.telefone} + + +
'; + clientesList.innerHTML = html; + } catch (error) { + loading.style.display = 'none'; + clientesList.innerHTML = `
Erro ao carregar clientes: ${error.message}
`; + } +} + +async function cadastrarCliente(e) { + e.preventDefault(); + + const formData = { + nome: document.getElementById('nome').value, + email: document.getElementById('email').value, + telefone: document.getElementById('telefone').value + }; + + try { + const response = await fetch(`${API_URL}/clientes`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(formData) + }); + + if (response.ok) { + document.getElementById('formCliente').reset(); + mostrarMensagem('Cliente cadastrado com sucesso!', 'success'); + carregarClientes(); + } else { + const errors = await response.json(); + mostrarErros(errors); + } + } catch (error) { + mostrarMensagem(`Erro ao cadastrar cliente: ${error.message}`, 'error'); + } +} + +function mostrarMensagem(mensagem, tipo) { + const formSection = document.querySelector('.form-section'); + const div = document.createElement('div'); + div.className = tipo; + div.textContent = mensagem; + formSection.insertBefore(div, formSection.firstChild); + + setTimeout(() => div.remove(), 5000); +} + +function mostrarErros(errors) { + const formSection = document.querySelector('.form-section'); + let html = '
    '; + for (const [campo, mensagem] of Object.entries(errors)) { + html += `
  • ${campo}: ${mensagem}
  • `; + } + html += '
'; + formSection.insertAdjacentHTML('afterbegin', html); + + setTimeout(() => { + const errorDiv = formSection.querySelector('.error'); + if (errorDiv) errorDiv.remove(); + }, 5000); +} + +async function deletarCliente(id) { + if (!confirm('Tem certeza que deseja excluir este cliente?')) return; + + try { + const response = await fetch(`${API_URL}/clientes/${id}`, { + method: 'DELETE' + }); + + if (response.ok) { + mostrarMensagem('Cliente excluído com sucesso!', 'success'); + carregarClientes(); + } + } catch (error) { + mostrarMensagem(`Erro ao excluir cliente: ${error.message}`, 'error'); + } +} + +function editarCliente(id) { + // Implementar edição (pode abrir modal ou preencher formulÔrio) + alert(`Editar cliente ${id} - Funcionalidade a ser implementada`); +} + diff --git a/src/main/resources/static/js/pedidos.js b/src/main/resources/static/js/pedidos.js new file mode 100644 index 0000000..741e4ef --- /dev/null +++ b/src/main/resources/static/js/pedidos.js @@ -0,0 +1,211 @@ +const API_URL = 'http://localhost:8080/api'; + +let clientes = []; +let produtos = []; + +document.addEventListener('DOMContentLoaded', () => { + carregarClientes(); + carregarProdutos(); + carregarPedidos(); + document.getElementById('formPedido').addEventListener('submit', criarPedido); +}); + +async function carregarClientes() { + try { + const response = await fetch(`${API_URL}/clientes`); + clientes = await response.json(); + + const select = document.getElementById('clienteId'); + select.innerHTML = ''; + + clientes.forEach(cliente => { + const option = document.createElement('option'); + option.value = cliente.id; + option.textContent = `${cliente.nome} (${cliente.email})`; + select.appendChild(option); + }); + + // Atualizar selects de produtos nos itens + atualizarSelectsProdutos(); + } catch (error) { + console.error('Erro ao carregar clientes:', error); + } +} + +async function carregarProdutos() { + try { + const response = await fetch(`${API_URL}/produtos`); + produtos = await response.json(); + atualizarSelectsProdutos(); + } catch (error) { + console.error('Erro ao carregar produtos:', error); + } +} + +function atualizarSelectsProdutos() { + const selects = document.querySelectorAll('.produto-select'); + selects.forEach(select => { + const currentValue = select.value; + select.innerHTML = ''; + + produtos.forEach(produto => { + const option = document.createElement('option'); + option.value = produto.id; + option.textContent = `${produto.nome} - R$ ${parseFloat(produto.preco).toFixed(2)} (Estoque: ${produto.quantidadeEstoque})`; + option.dataset.estoque = produto.quantidadeEstoque; + select.appendChild(option); + }); + + if (currentValue) { + select.value = currentValue; + } + }); +} + +function adicionarItem() { + const container = document.getElementById('itensContainer'); + const itemDiv = document.createElement('div'); + itemDiv.className = 'item-pedido'; + + itemDiv.innerHTML = ` +
+ + +
+
+ + +
+ + `; + + container.appendChild(itemDiv); + atualizarSelectsProdutos(); +} + +function removerItem(button) { + button.closest('.item-pedido').remove(); +} + +async function criarPedido(e) { + e.preventDefault(); + + const clienteId = parseInt(document.getElementById('clienteId').value); + const itens = []; + + const itemDivs = document.querySelectorAll('.item-pedido'); + itemDivs.forEach(itemDiv => { + const produtoId = parseInt(itemDiv.querySelector('.produto-select').value); + const quantidade = parseInt(itemDiv.querySelector('.quantidade-input').value); + + if (produtoId && quantidade) { + itens.push({ produtoId, quantidade }); + } + }); + + if (itens.length === 0) { + mostrarMensagem('Adicione pelo menos um item ao pedido', 'error'); + return; + } + + const pedidoData = { + clienteId, + itens + }; + + try { + const response = await fetch(`${API_URL}/pedidos`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(pedidoData) + }); + + if (response.ok) { + const pedido = await response.json(); + mostrarMensagem(`Pedido #${pedido.id} criado com sucesso!`, 'success'); + document.getElementById('formPedido').reset(); + document.getElementById('itensContainer').innerHTML = ` +

Itens do Pedido

+
+
+ + +
+
+ + +
+ +
+ `; + atualizarSelectsProdutos(); + carregarPedidos(); + } else { + const error = await response.text(); + mostrarMensagem(`Erro ao criar pedido: ${error}`, 'error'); + } + } catch (error) { + mostrarMensagem(`Erro ao criar pedido: ${error.message}`, 'error'); + } +} + +async function carregarPedidos() { + const loading = document.getElementById('loading'); + const pedidosList = document.getElementById('pedidosList'); + + try { + loading.style.display = 'block'; + const response = await fetch(`${API_URL}/pedidos`); + const pedidos = await response.json(); + + loading.style.display = 'none'; + + if (pedidos.length === 0) { + pedidosList.innerHTML = '

Nenhum pedido cadastrado ainda.

'; + return; + } + + let html = ''; + + pedidos.forEach(pedido => { + const data = new Date(pedido.dataPedido).toLocaleString('pt-BR'); + const total = pedido.itens.reduce((sum, item) => { + return sum + (parseFloat(item.precoUnitario) * item.quantidade); + }, 0); + + html += ` + + + + + + + + + `; + }); + + html += '
IDClienteDataStatusItensTotal
#${pedido.id}${pedido.cliente.nome}${data}${pedido.status}${pedido.itens.length} item(ns)R$ ${total.toFixed(2)}
'; + pedidosList.innerHTML = html; + } catch (error) { + loading.style.display = 'none'; + pedidosList.innerHTML = `
Erro ao carregar pedidos: ${error.message}
`; + } +} + +function mostrarMensagem(mensagem, tipo) { + const formSection = document.querySelector('.form-section'); + const div = document.createElement('div'); + div.className = tipo; + div.textContent = mensagem; + formSection.insertBefore(div, formSection.firstChild); + + setTimeout(() => div.remove(), 5000); +} + diff --git a/src/main/resources/static/js/produtos.js b/src/main/resources/static/js/produtos.js new file mode 100644 index 0000000..072453c --- /dev/null +++ b/src/main/resources/static/js/produtos.js @@ -0,0 +1,127 @@ +const API_URL = 'http://localhost:8080/api'; + +document.addEventListener('DOMContentLoaded', () => { + carregarProdutos(); + document.getElementById('formProduto').addEventListener('submit', cadastrarProduto); +}); + +async function carregarProdutos() { + const loading = document.getElementById('loading'); + const produtosList = document.getElementById('produtosList'); + + try { + loading.style.display = 'block'; + const response = await fetch(`${API_URL}/produtos`); + const produtos = await response.json(); + + loading.style.display = 'none'; + + if (produtos.length === 0) { + produtosList.innerHTML = '

Nenhum produto cadastrado ainda.

'; + return; + } + + let html = ''; + + produtos.forEach(produto => { + html += ` + + + + + + + + + `; + }); + + html += '
IDNomeDescriçãoPreçoEstoqueAções
${produto.id}${produto.nome}${produto.descricao || '-'}R$ ${parseFloat(produto.preco).toFixed(2)}${produto.quantidadeEstoque} + + +
'; + produtosList.innerHTML = html; + } catch (error) { + loading.style.display = 'none'; + produtosList.innerHTML = `
Erro ao carregar produtos: ${error.message}
`; + } +} + +async function cadastrarProduto(e) { + e.preventDefault(); + + const formData = { + nome: document.getElementById('nome').value, + descricao: document.getElementById('descricao').value, + preco: parseFloat(document.getElementById('preco').value), + quantidadeEstoque: parseInt(document.getElementById('quantidadeEstoque').value) + }; + + try { + const response = await fetch(`${API_URL}/produtos`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(formData) + }); + + if (response.ok) { + document.getElementById('formProduto').reset(); + mostrarMensagem('Produto cadastrado com sucesso!', 'success'); + carregarProdutos(); + } else { + const errors = await response.json(); + mostrarErros(errors); + } + } catch (error) { + mostrarMensagem(`Erro ao cadastrar produto: ${error.message}`, 'error'); + } +} + +function mostrarMensagem(mensagem, tipo) { + const formSection = document.querySelector('.form-section'); + const div = document.createElement('div'); + div.className = tipo; + div.textContent = mensagem; + formSection.insertBefore(div, formSection.firstChild); + + setTimeout(() => div.remove(), 5000); +} + +function mostrarErros(errors) { + const formSection = document.querySelector('.form-section'); + let html = '
    '; + for (const [campo, mensagem] of Object.entries(errors)) { + html += `
  • ${campo}: ${mensagem}
  • `; + } + html += '
'; + formSection.insertAdjacentHTML('afterbegin', html); + + setTimeout(() => { + const errorDiv = formSection.querySelector('.error'); + if (errorDiv) errorDiv.remove(); + }, 5000); +} + +async function deletarProduto(id) { + if (!confirm('Tem certeza que deseja excluir este produto?')) return; + + try { + const response = await fetch(`${API_URL}/produtos/${id}`, { + method: 'DELETE' + }); + + if (response.ok) { + mostrarMensagem('Produto excluĆ­do com sucesso!', 'success'); + carregarProdutos(); + } + } catch (error) { + mostrarMensagem(`Erro ao excluir produto: ${error.message}`, 'error'); + } +} + +function editarProduto(id) { + alert(`Editar produto ${id} - Funcionalidade a ser implementada`); +} + diff --git a/src/main/resources/templates/clientes.html b/src/main/resources/templates/clientes.html new file mode 100644 index 0000000..6579245 --- /dev/null +++ b/src/main/resources/templates/clientes.html @@ -0,0 +1,47 @@ + + + + + + Gerenciar Clientes + + + +
+
+

šŸ‘„ Gerenciar Clientes

+ ← Voltar +
+ +
+
+

Cadastrar Novo Cliente

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ +
+

Lista de Clientes

+
Carregando...
+
+
+
+
+ + + + + diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html new file mode 100644 index 0000000..50ae9c0 --- /dev/null +++ b/src/main/resources/templates/index.html @@ -0,0 +1,42 @@ + + + + + + Sistema de Vendas - Home + + + +
+
+

šŸ›’ Sistema de Gerenciamento de Vendas

+

Gerencie clientes, produtos e pedidos de forma simples e eficiente

+
+ + + + +
+ + + diff --git a/src/main/resources/templates/pedidos.html b/src/main/resources/templates/pedidos.html new file mode 100644 index 0000000..b97430f --- /dev/null +++ b/src/main/resources/templates/pedidos.html @@ -0,0 +1,60 @@ + + + + + + Gerenciar Pedidos + + + +
+
+

šŸ›ļø Gerenciar Pedidos

+ ← Voltar +
+ +
+
+

Criar Novo Pedido

+
+
+ + +
+ +
+

Itens do Pedido

+
+
+ + +
+
+ + +
+ +
+
+ + + +
+
+ +
+

Lista de Pedidos

+
Carregando...
+
+
+
+
+ + + + + diff --git a/src/main/resources/templates/produtos.html b/src/main/resources/templates/produtos.html new file mode 100644 index 0000000..bdd73b0 --- /dev/null +++ b/src/main/resources/templates/produtos.html @@ -0,0 +1,51 @@ + + + + + + Gerenciar Produtos + + + +
+
+

šŸ“¦ Gerenciar Produtos

+ ← Voltar +
+ +
+
+

Cadastrar Novo Produto

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ +
+

Lista de Produtos

+
Carregando...
+
+
+
+
+ + + + + From 4c682e51c65f4948e4f34f628d21ce4d504670cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodolfo=20Sousa=20=E2=9C=8C=F0=9F=8F=BC?= Date: Fri, 28 Nov 2025 13:28:06 -0300 Subject: [PATCH 2/5] Updates --- README.md | 105 +++- docs/screenshots/.gitkeep | 3 + docs/screenshots/README.md | 27 ++ .../sistema_vendas_api/config/CorsConfig.java | 2 +- .../controller/ClienteController.java | 31 +- .../controller/ProdutoController.java | 33 +- .../sistema_vendas_api/model/ItemPedido.java | 20 +- .../sistema_vendas_api/model/Pedido.java | 14 +- .../service/ClienteService.java | 56 +++ .../service/ProdutoService.java | 57 +++ src/main/resources/application.properties | 7 +- src/main/resources/static/css/style.css | 450 +++++++++++------- src/main/resources/static/js/clientes.js | 9 +- src/main/resources/static/js/pedidos.js | 35 +- src/main/resources/static/js/produtos.js | 9 +- src/main/resources/templates/clientes.html | 81 +++- src/main/resources/templates/index.html | 89 ++-- src/main/resources/templates/pedidos.html | 105 ++-- src/main/resources/templates/produtos.html | 91 ++-- 19 files changed, 882 insertions(+), 342 deletions(-) create mode 100644 docs/screenshots/.gitkeep create mode 100644 docs/screenshots/README.md create mode 100644 src/main/java/com/example/sistema_vendas_api/service/ClienteService.java create mode 100644 src/main/java/com/example/sistema_vendas_api/service/ProdutoService.java diff --git a/README.md b/README.md index 65bf4e7..0fda5d2 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,8 @@ spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.show-sql=true spring.jpa.hibernate.ddl-auto=update spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect + +server.port=8081 ``` > **Observação:** ajuste porta, usuĆ”rio e senha para combinar com a sua instalação. Os endpoints usam Bean Validation (`spring-boot-starter-validation`); payloads invĆ”lidos retornam `400 Bad Request` com mensagens indicando cada campo. @@ -59,7 +61,9 @@ Ou, na IDE, execute a classe `SistemaVendasApiApplication`. ## 5. Endpoints -Base URL padrĆ£o: `http://localhost:8080` +Base URL padrĆ£o: `http://localhost:8081` + +> **Nota:** A aplicação estĆ” configurada para rodar na porta 8081. A interface web estĆ” disponĆ­vel em `http://localhost:8081`. ### Clientes @@ -161,7 +165,104 @@ mvn test --- -## 9. Próximos passos sugeridos +## 9. Interface Web + +A aplicação possui uma interface web moderna e responsiva desenvolvida com Bootstrap 5 e Thymeleaf, proporcionando uma experiĆŖncia de usuĆ”rio intuitiva para gerenciar clientes, produtos e pedidos. + +### šŸ  PĆ”gina Inicial + +A pĆ”gina inicial apresenta um dashboard com acesso rĆ”pido Ć s principais funcionalidades do sistema: + +![PĆ”gina Inicial](docs/screenshots/home.png) + +**CaracterĆ­sticas:** +- Design moderno com gradiente roxo e animação de partĆ­culas no fundo +- Cards interativos para navegação rĆ”pida +- Layout responsivo que se adapta a diferentes tamanhos de tela +- Header centralizado com Ć­cone e descrição do sistema + +### šŸ‘„ Gerenciamento de Clientes + +Interface completa para cadastro e gerenciamento de clientes: + +![Gerenciamento de Clientes](docs/screenshots/clientes.png) + +**Funcionalidades:** +- FormulĆ”rio de cadastro com validação em tempo real +- Lista de clientes em formato de tabela +- BotƵes de edição e exclusĆ£o para cada cliente +- Layout em duas colunas (formulĆ”rio e lista) + +**Exemplo de uso:** +1. Preencha o formulĆ”rio com nome, e-mail e telefone +2. Clique em "Cadastrar Cliente" +3. O cliente aparece automaticamente na lista +4. Use os botƵes "Editar" ou "Excluir" para gerenciar + +### šŸ“¦ Gerenciamento de Produtos + +Controle completo do catĆ”logo de produtos e estoque: + +![Gerenciamento de Produtos](docs/screenshots/produtos.png) + +**Funcionalidades:** +- Cadastro de produtos com nome, descrição, preƧo e quantidade em estoque +- Lista completa de produtos cadastrados +- Edição e exclusĆ£o de produtos +- Validação de preƧos e estoque + +**Exemplo de uso:** +1. Preencha os dados do produto (nome, descrição, preƧo, estoque) +2. Clique em "Cadastrar Produto" +3. O produto Ć© adicionado ao catĆ”logo +4. Gerencie produtos existentes atravĆ©s dos botƵes de ação + +### šŸ›ļø Gerenciamento de Pedidos + +Criação e acompanhamento de pedidos de venda: + +![Gerenciamento de Pedidos](docs/screenshots/pedidos.png) + +**Funcionalidades:** +- Seleção de cliente para o pedido +- Adição de mĆŗltiplos itens ao pedido +- Seleção de produto e quantidade para cada item +- Lista de todos os pedidos criados +- Validação automĆ”tica de estoque + +**Exemplo de uso:** +1. Selecione um cliente no dropdown +2. Escolha um produto e informe a quantidade +3. Clique em "+ Adicionar Item" para adicionar mais produtos +4. Clique em "Criar Pedido" para finalizar +5. O sistema valida o estoque automaticamente + +### šŸŽØ Design e ExperiĆŖncia do UsuĆ”rio + +**CaracterĆ­sticas visuais:** +- **Cores:** Gradiente roxo moderno (#667eea a #764ba2) +- **AnimaƧƵes:** PartĆ­culas flutuantes sutis no fundo +- **Tipografia:** Fonte Segoe UI para melhor legibilidade +- **Cards:** Efeitos de hover e sombras suaves +- **Responsividade:** Layout adaptĆ”vel para mobile, tablet e desktop + +**Componentes:** +- BotƵes com gradiente e efeitos de hover +- FormulĆ”rios com validação visual +- Tabelas responsivas com scroll horizontal em telas pequenas +- Footer com informaƧƵes da equipe e links para GitHub + +### šŸ“± Responsividade + +A aplicação Ć© totalmente responsiva, adaptando-se perfeitamente a diferentes dispositivos: + +- **Desktop:** Layout em duas colunas para formulĆ”rios e listas +- **Tablet:** Layout adaptado mantendo usabilidade +- **Mobile:** Layout em coluna Ćŗnica com elementos empilhados + +--- + +## 10. Próximos passos sugeridos - Adicionar paginação e filtros nas listagens. - Criar testes de integração cobrindo fluxos de pedidos. diff --git a/docs/screenshots/.gitkeep b/docs/screenshots/.gitkeep new file mode 100644 index 0000000..4d4bd3d --- /dev/null +++ b/docs/screenshots/.gitkeep @@ -0,0 +1,3 @@ +# Esta pasta contĆ©m as screenshots da aplicação +# Adicione as imagens conforme descrito no README.md desta pasta + diff --git a/docs/screenshots/README.md b/docs/screenshots/README.md new file mode 100644 index 0000000..108fe7a --- /dev/null +++ b/docs/screenshots/README.md @@ -0,0 +1,27 @@ +# Screenshots da Aplicação + +Esta pasta contĆ©m as capturas de tela da interface web do Sistema de Gerenciamento de Vendas. + +## Imagens necessĆ”rias + +Para completar a documentação, adicione as seguintes screenshots: + +1. **home.png** - PĆ”gina inicial com os cards de navegação +2. **clientes.png** - PĆ”gina de gerenciamento de clientes (formulĆ”rio e lista) +3. **produtos.png** - PĆ”gina de gerenciamento de produtos (formulĆ”rio e lista) +4. **pedidos.png** - PĆ”gina de gerenciamento de pedidos (formulĆ”rio e lista) + +## Como capturar as screenshots + +1. Execute a aplicação: `mvn spring-boot:run` +2. Acesse `http://localhost:8081` no navegador +3. Navegue pelas pĆ”ginas e capture as telas +4. Salve as imagens nesta pasta com os nomes indicados acima + +## RecomendaƧƵes + +- Use formato PNG para melhor qualidade +- Capture em resolução de pelo menos 1920x1080 +- Certifique-se de que os dados de exemplo estejam visĆ­veis nas capturas +- Considere capturar versƵes desktop e mobile se necessĆ”rio + diff --git a/src/main/java/com/example/sistema_vendas_api/config/CorsConfig.java b/src/main/java/com/example/sistema_vendas_api/config/CorsConfig.java index a43246d..28a62f9 100644 --- a/src/main/java/com/example/sistema_vendas_api/config/CorsConfig.java +++ b/src/main/java/com/example/sistema_vendas_api/config/CorsConfig.java @@ -15,7 +15,7 @@ public WebMvcConfigurer corsConfigurer() { @Override public void addCorsMappings(@NonNull CorsRegistry registry) { registry.addMapping("/api/**") - .allowedOrigins("http://localhost:8080") + .allowedOriginPatterns("http://localhost:*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true); diff --git a/src/main/java/com/example/sistema_vendas_api/controller/ClienteController.java b/src/main/java/com/example/sistema_vendas_api/controller/ClienteController.java index d9a2552..ec19259 100644 --- a/src/main/java/com/example/sistema_vendas_api/controller/ClienteController.java +++ b/src/main/java/com/example/sistema_vendas_api/controller/ClienteController.java @@ -2,7 +2,8 @@ import com.example.sistema_vendas_api.model.Cliente; -import com.example.sistema_vendas_api.repository.ClienteRepository; +import com.example.sistema_vendas_api.service.ClienteService; +import jakarta.persistence.EntityNotFoundException; import org.springframework.beans.factory.annotation.Autowired; import jakarta.validation.Valid; import jakarta.validation.ConstraintViolationException; @@ -28,45 +29,37 @@ @RequestMapping("/api/clientes") public class ClienteController { @Autowired - private ClienteRepository clienteRepository; + private ClienteService clienteService; // listar @GetMapping public List findAll(){ - return clienteRepository.findAll(); + return clienteService.listarClientes(); } // cadastro @PostMapping @ResponseStatus(HttpStatus.CREATED) public ResponseEntity save(@Valid @RequestBody Cliente cliente){ - Cliente salvo = clienteRepository.save(cliente); + Cliente salvo = clienteService.salvarCliente(cliente); return ResponseEntity.status(HttpStatus.CREATED).body(salvo); } // busca por id @GetMapping("/{id}") public ResponseEntity findById(@PathVariable Integer id){ - return clienteRepository.findById(id) + return clienteService.buscarPorId(id) .map(ResponseEntity::ok) .orElseGet(() -> ResponseEntity.notFound().build()); } // atualizar por id @PutMapping("/{id}") public ResponseEntity atualizar(@PathVariable Integer id, @Valid @RequestBody Cliente clienteAtualizado) { - return clienteRepository.findById(id) - .map(clienteExistente -> { - clienteExistente.setNome(clienteAtualizado.getNome()); - clienteExistente.setEmail(clienteAtualizado.getEmail()); - clienteExistente.setTelefone(clienteAtualizado.getTelefone()); - - Cliente atualizado = clienteRepository.save(clienteExistente); - return ResponseEntity.ok(atualizado); - }) - .orElseGet(() -> ResponseEntity.notFound().build()); + Cliente atualizado = clienteService.atualizarCliente(id, clienteAtualizado); + return ResponseEntity.ok(atualizado); } // deletar @DeleteMapping("/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) public void delete(@PathVariable Integer id){ - clienteRepository.deleteById(id); + clienteService.deletarCliente(id); } @ResponseStatus(HttpStatus.BAD_REQUEST) @@ -88,4 +81,10 @@ public Map handleConstraintViolation(ConstraintViolationExceptio violation -> violation.getMessage(), (msg1, msg2) -> msg1)); } + + @ResponseStatus(HttpStatus.NOT_FOUND) + @ExceptionHandler(EntityNotFoundException.class) + public Map handleEntityNotFound(EntityNotFoundException ex) { + return Map.of("message", ex.getMessage()); + } } diff --git a/src/main/java/com/example/sistema_vendas_api/controller/ProdutoController.java b/src/main/java/com/example/sistema_vendas_api/controller/ProdutoController.java index 5d1dc81..ebe127f 100644 --- a/src/main/java/com/example/sistema_vendas_api/controller/ProdutoController.java +++ b/src/main/java/com/example/sistema_vendas_api/controller/ProdutoController.java @@ -1,7 +1,8 @@ package com.example.sistema_vendas_api.controller; import com.example.sistema_vendas_api.model.Produto; -import com.example.sistema_vendas_api.repository.ProdutoRepository; +import com.example.sistema_vendas_api.service.ProdutoService; +import jakarta.persistence.EntityNotFoundException; import jakarta.validation.Valid; import java.util.List; import java.util.Map; @@ -12,6 +13,7 @@ import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -25,11 +27,11 @@ @RequestMapping("/api/produtos") public class ProdutoController { @Autowired - private ProdutoRepository produtoRepository; + private ProdutoService produtoService; //listar @GetMapping public List findAll(){ - return produtoRepository.findAll(); + return produtoService.listarProdutos(); } //cadastro @PostMapping @@ -40,13 +42,13 @@ public ResponseEntity save(@Valid @RequestBody Produto produto, BindingResult .collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage, (msg1, msg2) -> msg1)); return ResponseEntity.badRequest().body(errors); } - Produto salvo = produtoRepository.save(produto); + Produto salvo = produtoService.salvarProduto(produto); return ResponseEntity.status(HttpStatus.CREATED).body(salvo); } // busca por id @GetMapping("/{id}") public ResponseEntity findById(@PathVariable Integer id){ - return produtoRepository.findById(id) + return produtoService.buscarPorId(id) .map(ResponseEntity::ok) .orElseGet(() -> ResponseEntity.notFound().build()); } @@ -58,22 +60,19 @@ public ResponseEntity atualizar(@PathVariable Integer id, @Valid @RequestBody .collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage, (msg1, msg2) -> msg1)); return ResponseEntity.badRequest().body(errors); } - return produtoRepository.findById(id) - .map(produtoExistente -> { - produtoExistente.setNome(produtoAtualizado.getNome()); - produtoExistente.setDescricao(produtoAtualizado.getDescricao()); - produtoExistente.setPreco(produtoAtualizado.getPreco()); - produtoExistente.setQuantidadeEstoque(produtoAtualizado.getQuantidadeEstoque()); - - Produto atualizado = produtoRepository.save(produtoExistente); - return ResponseEntity.ok(atualizado); - }) - .orElseGet(() -> ResponseEntity.notFound().build()); + Produto atualizado = produtoService.atualizarProduto(id, produtoAtualizado); + return ResponseEntity.ok(atualizado); } // deletar @DeleteMapping("/{id}") @ResponseStatus(HttpStatus.NO_CONTENT) public void delete(@PathVariable Integer id){ - produtoRepository.deleteById(id); + produtoService.deletarProduto(id); + } + + @ResponseStatus(HttpStatus.NOT_FOUND) + @ExceptionHandler(EntityNotFoundException.class) + public Map handleEntityNotFound(EntityNotFoundException ex) { + return Map.of("message", ex.getMessage()); } } diff --git a/src/main/java/com/example/sistema_vendas_api/model/ItemPedido.java b/src/main/java/com/example/sistema_vendas_api/model/ItemPedido.java index 8d121d1..da4e61b 100644 --- a/src/main/java/com/example/sistema_vendas_api/model/ItemPedido.java +++ b/src/main/java/com/example/sistema_vendas_api/model/ItemPedido.java @@ -1,6 +1,15 @@ -package com.example.sistema_vendas_api.model; // (confirme seu pacote) - -import jakarta.persistence.*; +package com.example.sistema_vendas_api.model; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; import java.math.BigDecimal; @Entity @@ -12,9 +21,10 @@ public class ItemPedido { @Column(name = "id_item_pedido") private Integer id; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "id_pedido", nullable = false) - private Pedido pedido; // O objeto Pedido "pai" + @JsonBackReference("pedido-itens") + private Pedido pedido; @ManyToOne @JoinColumn(name = "id_produto", nullable = false) diff --git a/src/main/java/com/example/sistema_vendas_api/model/Pedido.java b/src/main/java/com/example/sistema_vendas_api/model/Pedido.java index e4ada34..462faaa 100644 --- a/src/main/java/com/example/sistema_vendas_api/model/Pedido.java +++ b/src/main/java/com/example/sistema_vendas_api/model/Pedido.java @@ -1,6 +1,17 @@ package com.example.sistema_vendas_api.model; -import jakarta.persistence.*; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.ArrayList; @@ -24,6 +35,7 @@ public class Pedido { private String status; @OneToMany(mappedBy = "pedido", cascade = CascadeType.ALL, fetch = FetchType.EAGER) + @JsonManagedReference("pedido-itens") private List itens = new ArrayList<>(); public BigDecimal getValorTotal() { diff --git a/src/main/java/com/example/sistema_vendas_api/service/ClienteService.java b/src/main/java/com/example/sistema_vendas_api/service/ClienteService.java new file mode 100644 index 0000000..570fc8d --- /dev/null +++ b/src/main/java/com/example/sistema_vendas_api/service/ClienteService.java @@ -0,0 +1,56 @@ +package com.example.sistema_vendas_api.service; + +import com.example.sistema_vendas_api.model.Cliente; +import com.example.sistema_vendas_api.repository.ClienteRepository; +import jakarta.persistence.EntityNotFoundException; +import java.util.List; +import java.util.Optional; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class ClienteService { + + private final ClienteRepository clienteRepository; + + public ClienteService(ClienteRepository clienteRepository) { + this.clienteRepository = clienteRepository; + } + + @Transactional(readOnly = true) + public List listarClientes() { + return clienteRepository.findAll(); + } + + @Transactional + public Cliente salvarCliente(@NonNull Cliente cliente) { + return clienteRepository.save(cliente); + } + + @Transactional(readOnly = true) + public Optional buscarPorId(@NonNull Integer id) { + return clienteRepository.findById(id); + } + + @Transactional + public Cliente atualizarCliente(@NonNull Integer id, @NonNull Cliente clienteAtualizado) { + Cliente clienteExistente = clienteRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("Cliente nĆ£o encontrado")); + + clienteExistente.setNome(clienteAtualizado.getNome()); + clienteExistente.setEmail(clienteAtualizado.getEmail()); + clienteExistente.setTelefone(clienteAtualizado.getTelefone()); + + return clienteRepository.save(clienteExistente); + } + + @Transactional + public void deletarCliente(@NonNull Integer id) { + if (!clienteRepository.existsById(id)) { + throw new EntityNotFoundException("Cliente nĆ£o encontrado"); + } + clienteRepository.deleteById(id); + } +} + diff --git a/src/main/java/com/example/sistema_vendas_api/service/ProdutoService.java b/src/main/java/com/example/sistema_vendas_api/service/ProdutoService.java new file mode 100644 index 0000000..f882f1b --- /dev/null +++ b/src/main/java/com/example/sistema_vendas_api/service/ProdutoService.java @@ -0,0 +1,57 @@ +package com.example.sistema_vendas_api.service; + +import com.example.sistema_vendas_api.model.Produto; +import com.example.sistema_vendas_api.repository.ProdutoRepository; +import jakarta.persistence.EntityNotFoundException; +import java.util.List; +import java.util.Optional; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class ProdutoService { + + private final ProdutoRepository produtoRepository; + + public ProdutoService(ProdutoRepository produtoRepository) { + this.produtoRepository = produtoRepository; + } + + @Transactional(readOnly = true) + public List listarProdutos() { + return produtoRepository.findAll(); + } + + @Transactional + public Produto salvarProduto(@NonNull Produto produto) { + return produtoRepository.save(produto); + } + + @Transactional(readOnly = true) + public Optional buscarPorId(@NonNull Integer id) { + return produtoRepository.findById(id); + } + + @Transactional + public Produto atualizarProduto(@NonNull Integer id, @NonNull Produto produtoAtualizado) { + Produto produtoExistente = produtoRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("Produto nĆ£o encontrado")); + + produtoExistente.setNome(produtoAtualizado.getNome()); + produtoExistente.setDescricao(produtoAtualizado.getDescricao()); + produtoExistente.setPreco(produtoAtualizado.getPreco()); + produtoExistente.setQuantidadeEstoque(produtoAtualizado.getQuantidadeEstoque()); + + return produtoRepository.save(produtoExistente); + } + + @Transactional + public void deletarProduto(@NonNull Integer id) { + if (!produtoRepository.existsById(id)) { + throw new EntityNotFoundException("Produto nĆ£o encontrado"); + } + produtoRepository.deleteById(id); + } +} + diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a259236..b63a218 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -2,7 +2,7 @@ # Altere "seu_usuario_postgres" e "sua_senha_postgres" # com as suas credenciais reais do PostgreSQL. -spring.datasource.url=jdbc:postgresql://localhost:5433/projeto_vendas +spring.datasource.url=jdbc:postgresql://localhost:5432/projeto_vendas spring.datasource.username=postgres spring.datasource.password=12345 spring.datasource.driver-class-name=org.postgresql.Driver @@ -11,4 +11,7 @@ spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.show-sql=true spring.jpa.hibernate.ddl-auto=update -spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect \ No newline at end of file +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect + +# --- Configuração do servidor --- +server.port=8081 \ No newline at end of file diff --git a/src/main/resources/static/css/style.css b/src/main/resources/static/css/style.css index 5318f82..fa081f1 100644 --- a/src/main/resources/static/css/style.css +++ b/src/main/resources/static/css/style.css @@ -8,243 +8,373 @@ body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; - padding: 20px; + padding: 32px 16px; + position: relative; + overflow-x: hidden; } -.container { +body::before { + content: ''; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: + radial-gradient(4px 4px at 20% 30%, rgba(255, 255, 255, 0.5), transparent), + radial-gradient(3px 3px at 60% 70%, rgba(255, 255, 255, 0.45), transparent), + radial-gradient(3px 3px at 50% 50%, rgba(255, 255, 255, 0.55), transparent), + radial-gradient(4px 4px at 80% 10%, rgba(255, 255, 255, 0.5), transparent), + radial-gradient(3px 3px at 90% 60%, rgba(255, 255, 255, 0.45), transparent), + radial-gradient(3px 3px at 33% 85%, rgba(255, 255, 255, 0.5), transparent), + radial-gradient(4px 4px at 10% 40%, rgba(255, 255, 255, 0.5), transparent), + radial-gradient(3px 3px at 70% 20%, rgba(255, 255, 255, 0.45), transparent), + radial-gradient(3px 3px at 40% 80%, rgba(255, 255, 255, 0.5), transparent), + radial-gradient(4px 4px at 15% 60%, rgba(255, 255, 255, 0.45), transparent); + background-size: 100% 100%; + animation: particlesMove1 30s ease-in-out infinite; + pointer-events: none; + z-index: 0; +} + +body::after { + content: ''; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: + radial-gradient(3px 3px at 15% 25%, rgba(255, 255, 255, 0.5), transparent), + radial-gradient(4px 4px at 75% 80%, rgba(255, 255, 255, 0.45), transparent), + radial-gradient(3px 3px at 45% 15%, rgba(255, 255, 255, 0.5), transparent), + radial-gradient(4px 4px at 85% 45%, rgba(255, 255, 255, 0.5), transparent), + radial-gradient(3px 3px at 25% 75%, rgba(255, 255, 255, 0.45), transparent), + radial-gradient(4px 4px at 55% 90%, rgba(255, 255, 255, 0.5), transparent), + radial-gradient(3px 3px at 5% 50%, rgba(255, 255, 255, 0.45), transparent), + radial-gradient(4px 4px at 95% 30%, rgba(255, 255, 255, 0.5), transparent); + background-size: 100% 100%; + animation: particlesMove2 35s ease-in-out infinite; + pointer-events: none; + z-index: 0; +} + +@keyframes particlesMove1 { + 0% { + transform: translate(0, 0); + } + 25% { + transform: translate(50px, -30px); + } + 50% { + transform: translate(-40px, 40px); + } + 75% { + transform: translate(30px, 25px); + } + 100% { + transform: translate(0, 0); + } +} + +@keyframes particlesMove2 { + 0% { + transform: translate(0, 0); + } + 25% { + transform: translate(-35px, 35px); + } + 50% { + transform: translate(45px, -25px); + } + 75% { + transform: translate(-20px, -30px); + } + 100% { + transform: translate(0, 0); + } +} + + +.app-wrapper { + position: relative; + z-index: 1; max-width: 1200px; margin: 0 auto; } -header { - background: white; - padding: 30px; - border-radius: 10px; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - margin-bottom: 30px; - display: flex; - justify-content: space-between; - align-items: center; +.app-header { + background: rgba(255, 255, 255, 0.95); + padding: 32px; + border-radius: 16px; + box-shadow: 0 20px 45px rgba(15, 23, 42, 0.15); + margin-bottom: 32px; + width: 100%; } -header h1 { - color: #333; - font-size: 2em; +.app-header > div { + width: 100%; + text-align: center; } -.subtitle { - color: #666; - margin-top: 10px; +.app-header h1 { + color: #1f2d3d; + font-weight: 700; + line-height: 1.2; + margin: 0; + justify-content: center; + text-align: center; } -.menu { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 20px; - margin-bottom: 30px; +.app-header h1 span:first-child { + display: inline-flex; + align-items: center; + justify-content: center; + line-height: 1; + flex-shrink: 0; } -.card { - background: white; - padding: 40px; - border-radius: 10px; - text-decoration: none; - color: inherit; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - transition: transform 0.3s, box-shadow 0.3s; +.app-header h1 > span:not(:first-child) { text-align: center; } -.card:hover { - transform: translateY(-5px); - box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15); +.app-header .subtitle { + color: #4c566a; + font-size: 1rem; + line-height: 1.5; + margin-top: 4px; + text-align: center; } -.card-icon { - font-size: 4em; - margin-bottom: 20px; +@media (max-width: 991px) { + .app-header { + padding: 24px; + } + + .app-header h1 { + font-size: 1.75rem; + flex-direction: column; + gap: 8px; + } + + .app-header h1 > span:not(:first-child) { + font-size: 1.5rem; + } } -.card h2 { - color: #333; - margin-bottom: 10px; +.feature-card { + background: rgba(255, 255, 255, 0.95); + border-radius: 16px; + padding: 32px; + box-shadow: 0 20px 45px rgba(15, 23, 42, 0.12); + transition: transform 0.3s ease, box-shadow 0.3s ease; + color: #1f2d3d; + text-decoration: none !important; + display: block; + height: 100%; } -.card p { - color: #666; +.feature-card:hover { + transform: translateY(-8px); + box-shadow: 0 32px 60px rgba(15, 23, 42, 0.15); + color: #1f2d3d; } -.content { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 30px; +.feature-icon { + font-size: 3.5rem; + margin-bottom: 16px; + line-height: 1; + width: 100%; + display: flex; + justify-content: center; + align-items: center; } -.form-section, .list-section { - background: white; - padding: 30px; - border-radius: 10px; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +.feature-card h2 { + font-weight: 600; + margin-bottom: 8px; + color: #1f2d3d; + text-align: center; } -.form-section h2, .list-section h2 { - color: #333; - margin-bottom: 20px; - padding-bottom: 10px; - border-bottom: 2px solid #667eea; +.feature-card p { + margin: 0; + color: #4c566a; + text-align: center; + line-height: 1.5; } -.form-group { - margin-bottom: 20px; +.section-card { + background: rgba(255, 255, 255, 0.95); + border-radius: 18px; + box-shadow: 0 20px 45px rgba(15, 23, 42, 0.12); + border: none; } -.form-group label { - display: block; - margin-bottom: 5px; - color: #333; - font-weight: 500; +.section-title { + font-weight: 600; + color: #1f2d3d; + border-bottom: 2px solid rgba(102, 126, 234, 0.3); + padding-bottom: 8px; + margin-bottom: 24px; } -.form-group input, -.form-group select, -.form-group textarea { - width: 100%; - padding: 10px; - border: 2px solid #e0e0e0; - border-radius: 5px; - font-size: 1em; - transition: border-color 0.3s; +.btn-gradient { + background: linear-gradient(120deg, #6c63ff, #5a2fc2); + border: none; + color: #fff; + box-shadow: 0 12px 20px rgba(108, 99, 255, 0.25); + transition: transform 0.2s ease, box-shadow 0.2s ease; } -.form-group input:focus, -.form-group select:focus, -.form-group textarea:focus { - outline: none; - border-color: #667eea; +.btn-gradient:hover { + transform: translateY(-1px); + box-shadow: 0 16px 30px rgba(90, 47, 194, 0.35); + color: #fff; } -.btn-primary { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - padding: 12px 30px; - border: none; - border-radius: 5px; - font-size: 1em; - cursor: pointer; - transition: transform 0.2s; - width: 100%; +.btn-soft { + background: rgba(255, 255, 255, 0.2); + color: #fff; + border: 1px solid rgba(255, 255, 255, 0.5); + backdrop-filter: blur(8px); } -.btn-primary:hover { - transform: scale(1.05); +.btn-soft:hover { + background: rgba(255, 255, 255, 0.4); + color: #472a87; } -.btn-secondary { - background: #6c757d; - color: white; - padding: 10px 20px; - border: none; - border-radius: 5px; - text-decoration: none; - display: inline-block; - transition: background 0.3s; +.item-pedido { + background: #f8f9fb; + padding: 18px; + border-radius: 12px; + margin-bottom: 16px; + border: 1px dashed rgba(102, 126, 234, 0.4); + display: grid; + grid-template-columns: 2fr 1fr auto; + gap: 16px; + align-items: end; } -.btn-secondary:hover { - background: #5a6268; +.item-pedido button { + height: 42px; } -.btn-remove { - background: #dc3545; - color: white; - padding: 8px 15px; - border: none; - border-radius: 5px; - cursor: pointer; - font-size: 0.9em; +.loading { + text-align: center; + padding: 20px; + color: #6c757d; } -.btn-remove:hover { - background: #c82333; +.error, +.success { + padding: 14px 16px; + border-radius: 10px; + margin-bottom: 20px; + font-weight: 500; } -.table-container { - margin-top: 20px; +.error { + background: #ffe2e5; + color: #b42318; } -table { - width: 100%; - border-collapse: collapse; +.success { + background: #d1f7c4; + color: #24613c; +} + +.table-container { margin-top: 20px; } -table th, -table td { - padding: 12px; - text-align: left; - border-bottom: 1px solid #e0e0e0; +.table-container table { + border-radius: 12px; + overflow: hidden; } -table th { - background: #f8f9fa; - color: #333; - font-weight: 600; +.table-container th { + background: rgba(102, 126, 234, 0.12); + color: #1f2d3d; } -table tr:hover { - background: #f8f9fa; +footer { + color: rgba(255, 255, 255, 0.9); } -.loading { +.app-footer { text-align: center; - padding: 20px; - color: #666; + margin-top: 48px; + color: rgba(255, 255, 255, 0.85); } -.error { - background: #f8d7da; - color: #721c24; - padding: 15px; - border-radius: 5px; - margin-bottom: 20px; +.footer-content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 16px; } -.success { - background: #d4edda; - color: #155724; - padding: 15px; - border-radius: 5px; - margin-bottom: 20px; +.footer-profiles-container { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; + gap: 8px; } -.item-pedido { - background: #f8f9fa; - padding: 15px; - border-radius: 5px; - margin-bottom: 15px; - display: grid; - grid-template-columns: 2fr 1fr auto; - gap: 15px; - align-items: end; +.footer-profile { + display: flex; + align-items: center; + justify-content: center; + padding: 4px; + background: rgba(255, 255, 255, 0.1); + border-radius: 10px; + backdrop-filter: blur(8px); + border: 1.5px solid rgba(255, 255, 255, 0.2); + box-shadow: 0 4px 12px rgba(15, 23, 42, 0.15); + transition: transform 0.3s ease, box-shadow 0.3s ease; + text-decoration: none; + cursor: pointer; } -footer { - text-align: center; - color: white; - margin-top: 30px; - padding: 20px; +.footer-profile:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(15, 23, 42, 0.25); + border-color: rgba(255, 255, 255, 0.35); +} + +.footer-avatar { + width: 40px; + height: 40px; + border-radius: 8px; + object-fit: cover; + border: 1.5px solid rgba(255, 255, 255, 0.3); + display: block; +} + +.footer-info small { + color: rgba(255, 255, 255, 0.7); } @media (max-width: 768px) { - .content { - grid-template-columns: 1fr; + .footer-content { + gap: 12px; } - - header { - flex-direction: column; - gap: 15px; + + .footer-profiles-container { + gap: 6px; + } + + .footer-avatar { + width: 36px; + height: 36px; } +} +@media (max-width: 992px) { .item-pedido { grid-template-columns: 1fr; } diff --git a/src/main/resources/static/js/clientes.js b/src/main/resources/static/js/clientes.js index 13b41b7..d5b2e1d 100644 --- a/src/main/resources/static/js/clientes.js +++ b/src/main/resources/static/js/clientes.js @@ -1,4 +1,7 @@ -const API_URL = 'http://localhost:8080/api'; +const API_URL = + window.location.origin && window.location.origin !== 'null' + ? `${window.location.origin}/api` + : 'http://localhost:8081/api'; // Carregar clientes ao carregar a pĆ”gina document.addEventListener('DOMContentLoaded', () => { @@ -32,8 +35,8 @@ async function carregarClientes() { ${cliente.email} ${cliente.telefone} - - + + `; diff --git a/src/main/resources/static/js/pedidos.js b/src/main/resources/static/js/pedidos.js index 741e4ef..89f7c84 100644 --- a/src/main/resources/static/js/pedidos.js +++ b/src/main/resources/static/js/pedidos.js @@ -1,4 +1,7 @@ -const API_URL = 'http://localhost:8080/api'; +const API_URL = + window.location.origin && window.location.origin !== 'null' + ? `${window.location.origin}/api` + : 'http://localhost:8081/api'; let clientes = []; let produtos = []; @@ -68,17 +71,17 @@ function adicionarItem() { itemDiv.className = 'item-pedido'; itemDiv.innerHTML = ` -
- -
-
- - +
+ +
- + `; container.appendChild(itemDiv); @@ -129,19 +132,19 @@ async function criarPedido(e) { mostrarMensagem(`Pedido #${pedido.id} criado com sucesso!`, 'success'); document.getElementById('formPedido').reset(); document.getElementById('itensContainer').innerHTML = ` -

Itens do Pedido

+

Itens do Pedido

-
- -
-
- - +
+ +
- +
`; atualizarSelectsProdutos(); diff --git a/src/main/resources/static/js/produtos.js b/src/main/resources/static/js/produtos.js index 072453c..61520dd 100644 --- a/src/main/resources/static/js/produtos.js +++ b/src/main/resources/static/js/produtos.js @@ -1,4 +1,7 @@ -const API_URL = 'http://localhost:8080/api'; +const API_URL = + window.location.origin && window.location.origin !== 'null' + ? `${window.location.origin}/api` + : 'http://localhost:8081/api'; document.addEventListener('DOMContentLoaded', () => { carregarProdutos(); @@ -32,8 +35,8 @@ async function carregarProdutos() { R$ ${parseFloat(produto.preco).toFixed(2)} ${produto.quantidadeEstoque} - - + + `; diff --git a/src/main/resources/templates/clientes.html b/src/main/resources/templates/clientes.html index 6579245..409604d 100644 --- a/src/main/resources/templates/clientes.html +++ b/src/main/resources/templates/clientes.html @@ -4,43 +4,74 @@ Gerenciar Clientes + + -
-
-

šŸ‘„ Gerenciar Clientes

- ← Voltar +
+
+
+

+ šŸ‘„ + Gerenciar Clientes +

+

Cadastre novos contatos e acompanhe toda a base

+
+ ← Voltar
-
-
-

Cadastrar Novo Cliente

-
-
- - -
-
- - +
+
+
+
+

Cadastrar Novo Cliente

-
- - + +
+ + +
+
+ + +
+
+ + +
+ + +
+
+ +
+
+
+

Lista de Clientes

- - +
Carregando...
+
+
+
+
-
-

Lista de Clientes

-
Carregando...
-
+
+ -
+

© 2025 Sistema de Vendas - Rodolfo Sousa

+ + diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index 50ae9c0..c0d0252 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -3,40 +3,75 @@ - Sistema de Vendas - Home + Sistema de Vendas + + -
-
-

šŸ›’ Sistema de Gerenciamento de Vendas

-

Gerencie clientes, produtos e pedidos de forma simples e eficiente

+
+
+
+

+ šŸ›’ + Sistema de Gerenciamento de Vendas +

+

Gerencie clientes, produtos e pedidos de forma simples e eficiente

+
- - -
+ + + diff --git a/src/main/resources/templates/pedidos.html b/src/main/resources/templates/pedidos.html index b97430f..0405727 100644 --- a/src/main/resources/templates/pedidos.html +++ b/src/main/resources/templates/pedidos.html @@ -4,56 +4,91 @@ Gerenciar Pedidos + + -
-
-

šŸ›ļø Gerenciar Pedidos

- ← Voltar +
+
+
+

+ šŸ›ļø + Gerenciar Pedidos +

+

Monte pedidos completos e acompanhe o status em tempo real

+
+ ← Voltar
-
-
-

Criar Novo Pedido

-
-
- - +
+
+
+
+

Criar Novo Pedido

- -
-

Itens do Pedido

-
-
- - + +
+ + +
+ +
+
+

Itens do Pedido

-
- - +
+
+ + +
+
+ + +
+
-
+ +
+ + +
+ +
+
+ +
+
+
+

Lista de Pedidos

- - - - +
Carregando...
+
+
+
+
-
-

Lista de Pedidos

-
Carregando...
-
+
+ -
+

© 2025 Sistema de Vendas - Rodolfo Sousa

+ + diff --git a/src/main/resources/templates/produtos.html b/src/main/resources/templates/produtos.html index bdd73b0..ce7e9b4 100644 --- a/src/main/resources/templates/produtos.html +++ b/src/main/resources/templates/produtos.html @@ -4,47 +4,80 @@ Gerenciar Produtos + + -
-
-

šŸ“¦ Gerenciar Produtos

- ← Voltar +
+
+
+

+ šŸ“¦ + Gerenciar Produtos +

+

Mantenha o catƔlogo atualizado e o estoque organizado

+
+ ← Voltar
-
-
-

Cadastrar Novo Produto

-
-
- - -
-
- - -
-
- - +
+
+
+
+

Cadastrar Novo Produto

-
- - + +
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+

Lista de Produtos

- - +
Carregando...
+
+
+
+
-
-

Lista de Produtos

-
Carregando...
-
+
+ -
+

© 2025 Sistema de Vendas - Rodolfo Sousa

+ + From df258c798af9404e551d3d7ba55b1ca51aef0384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodolfo=20Sousa=20=E2=9C=8C=F0=9F=8F=BC?= <168583881+iSousadev@users.noreply.github.com> Date: Fri, 28 Nov 2025 13:48:33 -0300 Subject: [PATCH 3/5] Replace local image links with external URLs --- README.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0fda5d2..017a0b9 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,8 @@ A aplicação possui uma interface web moderna e responsiva desenvolvida com Boo A pÔgina inicial apresenta um dashboard com acesso rÔpido às principais funcionalidades do sistema: -![PÔgina Inicial](docs/screenshots/home.png) +image + **Características:** - Design moderno com gradiente roxo e animação de partículas no fundo @@ -185,7 +186,9 @@ A pÔgina inicial apresenta um dashboard com acesso rÔpido às principais funci Interface completa para cadastro e gerenciamento de clientes: -![Gerenciamento de Clientes](docs/screenshots/clientes.png) +image + + **Funcionalidades:** - FormulÔrio de cadastro com validação em tempo real @@ -203,7 +206,10 @@ Interface completa para cadastro e gerenciamento de clientes: Controle completo do catÔlogo de produtos e estoque: -![Gerenciamento de Produtos](docs/screenshots/produtos.png) +image + + + **Funcionalidades:** - Cadastro de produtos com nome, descrição, preço e quantidade em estoque @@ -221,7 +227,8 @@ Controle completo do catÔlogo de produtos e estoque: Criação e acompanhamento de pedidos de venda: -![Gerenciamento de Pedidos](docs/screenshots/pedidos.png) +image + **Funcionalidades:** - Seleção de cliente para o pedido From f61fcb592bab53f12a60ecf8eeae45e0f8a82b56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodolfo=20Sousa=20=E2=9C=8C=F0=9F=8F=BC?= Date: Fri, 28 Nov 2025 13:52:45 -0300 Subject: [PATCH 4/5] Add safe delete with validation for clientes, pedidos, produtos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementamos validação para evitar a exclusão de clientes com pedidos associados e produtos com itens de pedido associados. Atualizamos os controladores para retornar o status HTTP e as mensagens de erro apropriadas em caso de falha na exclusão. Melhoramos o feedback no frontend para ações de exclusão e aprimoramos o layout do rodapé nos modelos HTML. --- .../controller/ClienteController.java | 23 +++++++++-- .../controller/PedidoController.java | 20 +++++++++- .../controller/ProdutoController.java | 23 +++++++++-- .../repository/ItemPedidoRepository.java | 3 +- .../repository/PedidoRepository.java | 3 +- .../service/ClienteService.java | 13 ++++-- .../service/PedidoService.java | 9 +++++ .../service/ProdutoService.java | 13 ++++-- src/main/resources/static/js/clientes.js | 35 ++++++++++++---- src/main/resources/static/js/pedidos.js | 40 +++++++++++++++++-- src/main/resources/static/js/produtos.js | 5 ++- src/main/resources/templates/clientes.html | 26 ++++++++---- src/main/resources/templates/pedidos.html | 26 ++++++++---- src/main/resources/templates/produtos.html | 26 ++++++++---- 14 files changed, 216 insertions(+), 49 deletions(-) diff --git a/src/main/java/com/example/sistema_vendas_api/controller/ClienteController.java b/src/main/java/com/example/sistema_vendas_api/controller/ClienteController.java index ec19259..c0ba333 100644 --- a/src/main/java/com/example/sistema_vendas_api/controller/ClienteController.java +++ b/src/main/java/com/example/sistema_vendas_api/controller/ClienteController.java @@ -57,9 +57,20 @@ public ResponseEntity atualizar(@PathVariable Integer id, @Valid @Reque } // deletar @DeleteMapping("/{id}") - @ResponseStatus(HttpStatus.NO_CONTENT) - public void delete(@PathVariable Integer id){ - clienteService.deletarCliente(id); + public ResponseEntity delete(@PathVariable Integer id){ + try { + clienteService.deletarCliente(id); + return ResponseEntity.noContent().build(); + } catch (EntityNotFoundException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(Map.of("message", e.getMessage())); + } catch (IllegalStateException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Map.of("message", e.getMessage())); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of("message", "Erro ao excluir cliente: " + e.getMessage())); + } } @ResponseStatus(HttpStatus.BAD_REQUEST) @@ -87,4 +98,10 @@ public Map handleConstraintViolation(ConstraintViolationExceptio public Map handleEntityNotFound(EntityNotFoundException ex) { return Map.of("message", ex.getMessage()); } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(IllegalStateException.class) + public Map handleIllegalState(IllegalStateException ex) { + return Map.of("message", ex.getMessage()); + } } diff --git a/src/main/java/com/example/sistema_vendas_api/controller/PedidoController.java b/src/main/java/com/example/sistema_vendas_api/controller/PedidoController.java index 14c8df1..d49a4e3 100644 --- a/src/main/java/com/example/sistema_vendas_api/controller/PedidoController.java +++ b/src/main/java/com/example/sistema_vendas_api/controller/PedidoController.java @@ -3,12 +3,16 @@ import com.example.sistema_vendas_api.dto.PedidoDTO; import com.example.sistema_vendas_api.model.Pedido; import com.example.sistema_vendas_api.service.PedidoService; +import jakarta.persistence.EntityNotFoundException; import java.util.List; +import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -34,4 +38,18 @@ public ResponseEntity> listarPedidos() { List pedidos = pedidoService.listarPedidos(); return ResponseEntity.ok(pedidos); } + + @DeleteMapping("/{id}") + public ResponseEntity delete(@PathVariable Integer id) { + try { + pedidoService.deletarPedido(id); + return ResponseEntity.noContent().build(); + } catch (EntityNotFoundException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(Map.of("message", e.getMessage())); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of("message", "Erro ao excluir pedido: " + e.getMessage())); + } + } } diff --git a/src/main/java/com/example/sistema_vendas_api/controller/ProdutoController.java b/src/main/java/com/example/sistema_vendas_api/controller/ProdutoController.java index ebe127f..8ecb276 100644 --- a/src/main/java/com/example/sistema_vendas_api/controller/ProdutoController.java +++ b/src/main/java/com/example/sistema_vendas_api/controller/ProdutoController.java @@ -65,9 +65,20 @@ public ResponseEntity atualizar(@PathVariable Integer id, @Valid @RequestBody } // deletar @DeleteMapping("/{id}") - @ResponseStatus(HttpStatus.NO_CONTENT) - public void delete(@PathVariable Integer id){ - produtoService.deletarProduto(id); + public ResponseEntity delete(@PathVariable Integer id){ + try { + produtoService.deletarProduto(id); + return ResponseEntity.noContent().build(); + } catch (EntityNotFoundException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(Map.of("message", e.getMessage())); + } catch (IllegalStateException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Map.of("message", e.getMessage())); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of("message", "Erro ao excluir produto: " + e.getMessage())); + } } @ResponseStatus(HttpStatus.NOT_FOUND) @@ -75,4 +86,10 @@ public void delete(@PathVariable Integer id){ public Map handleEntityNotFound(EntityNotFoundException ex) { return Map.of("message", ex.getMessage()); } + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(IllegalStateException.class) + public Map handleIllegalState(IllegalStateException ex) { + return Map.of("message", ex.getMessage()); + } } diff --git a/src/main/java/com/example/sistema_vendas_api/repository/ItemPedidoRepository.java b/src/main/java/com/example/sistema_vendas_api/repository/ItemPedidoRepository.java index 48912ea..d8cc1ce 100644 --- a/src/main/java/com/example/sistema_vendas_api/repository/ItemPedidoRepository.java +++ b/src/main/java/com/example/sistema_vendas_api/repository/ItemPedidoRepository.java @@ -1,10 +1,11 @@ package com.example.sistema_vendas_api.repository; import com.example.sistema_vendas_api.model.ItemPedido; +import com.example.sistema_vendas_api.model.Produto; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface ItemPedidoRepository extends JpaRepository { - + boolean existsByProduto(Produto produto); } \ No newline at end of file diff --git a/src/main/java/com/example/sistema_vendas_api/repository/PedidoRepository.java b/src/main/java/com/example/sistema_vendas_api/repository/PedidoRepository.java index dac23ee..e573afc 100644 --- a/src/main/java/com/example/sistema_vendas_api/repository/PedidoRepository.java +++ b/src/main/java/com/example/sistema_vendas_api/repository/PedidoRepository.java @@ -1,10 +1,11 @@ package com.example.sistema_vendas_api.repository; +import com.example.sistema_vendas_api.model.Cliente; import com.example.sistema_vendas_api.model.Pedido; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface PedidoRepository extends JpaRepository { - + boolean existsByCliente(Cliente cliente); } \ No newline at end of file diff --git a/src/main/java/com/example/sistema_vendas_api/service/ClienteService.java b/src/main/java/com/example/sistema_vendas_api/service/ClienteService.java index 570fc8d..25495a0 100644 --- a/src/main/java/com/example/sistema_vendas_api/service/ClienteService.java +++ b/src/main/java/com/example/sistema_vendas_api/service/ClienteService.java @@ -2,6 +2,7 @@ import com.example.sistema_vendas_api.model.Cliente; import com.example.sistema_vendas_api.repository.ClienteRepository; +import com.example.sistema_vendas_api.repository.PedidoRepository; import jakarta.persistence.EntityNotFoundException; import java.util.List; import java.util.Optional; @@ -13,9 +14,11 @@ public class ClienteService { private final ClienteRepository clienteRepository; + private final PedidoRepository pedidoRepository; - public ClienteService(ClienteRepository clienteRepository) { + public ClienteService(ClienteRepository clienteRepository, PedidoRepository pedidoRepository) { this.clienteRepository = clienteRepository; + this.pedidoRepository = pedidoRepository; } @Transactional(readOnly = true) @@ -47,9 +50,13 @@ public Cliente atualizarCliente(@NonNull Integer id, @NonNull Cliente clienteAtu @Transactional public void deletarCliente(@NonNull Integer id) { - if (!clienteRepository.existsById(id)) { - throw new EntityNotFoundException("Cliente não encontrado"); + Cliente cliente = clienteRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("Cliente não encontrado")); + + if (pedidoRepository.existsByCliente(cliente)) { + throw new IllegalStateException("Não é possível excluir o cliente pois existem pedidos associados a ele. Exclua os pedidos primeiro."); } + clienteRepository.deleteById(id); } } diff --git a/src/main/java/com/example/sistema_vendas_api/service/PedidoService.java b/src/main/java/com/example/sistema_vendas_api/service/PedidoService.java index d343865..ce06cfa 100644 --- a/src/main/java/com/example/sistema_vendas_api/service/PedidoService.java +++ b/src/main/java/com/example/sistema_vendas_api/service/PedidoService.java @@ -13,6 +13,7 @@ import java.time.LocalDateTime; import java.util.List; +import jakarta.persistence.EntityNotFoundException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -61,4 +62,12 @@ public Pedido criarPedido(PedidoDTO requestDTO) { public List listarPedidos() { return pedidoRepository.findAll(); } + + @Transactional + public void deletarPedido(Integer id) { + if (!pedidoRepository.existsById(id)) { + throw new EntityNotFoundException("Pedido não encontrado"); + } + pedidoRepository.deleteById(id); + } } \ No newline at end of file diff --git a/src/main/java/com/example/sistema_vendas_api/service/ProdutoService.java b/src/main/java/com/example/sistema_vendas_api/service/ProdutoService.java index f882f1b..76ba203 100644 --- a/src/main/java/com/example/sistema_vendas_api/service/ProdutoService.java +++ b/src/main/java/com/example/sistema_vendas_api/service/ProdutoService.java @@ -1,6 +1,7 @@ package com.example.sistema_vendas_api.service; import com.example.sistema_vendas_api.model.Produto; +import com.example.sistema_vendas_api.repository.ItemPedidoRepository; import com.example.sistema_vendas_api.repository.ProdutoRepository; import jakarta.persistence.EntityNotFoundException; import java.util.List; @@ -13,9 +14,11 @@ public class ProdutoService { private final ProdutoRepository produtoRepository; + private final ItemPedidoRepository itemPedidoRepository; - public ProdutoService(ProdutoRepository produtoRepository) { + public ProdutoService(ProdutoRepository produtoRepository, ItemPedidoRepository itemPedidoRepository) { this.produtoRepository = produtoRepository; + this.itemPedidoRepository = itemPedidoRepository; } @Transactional(readOnly = true) @@ -48,9 +51,13 @@ public Produto atualizarProduto(@NonNull Integer id, @NonNull Produto produtoAtu @Transactional public void deletarProduto(@NonNull Integer id) { - if (!produtoRepository.existsById(id)) { - throw new EntityNotFoundException("Produto não encontrado"); + Produto produto = produtoRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException("Produto não encontrado")); + + if (itemPedidoRepository.existsByProduto(produto)) { + throw new IllegalStateException("Não é possível excluir o produto pois existem itens de pedidos associados a ele. Exclua os pedidos primeiro."); } + produtoRepository.deleteById(id); } } diff --git a/src/main/resources/static/js/clientes.js b/src/main/resources/static/js/clientes.js index d5b2e1d..13a11e2 100644 --- a/src/main/resources/static/js/clientes.js +++ b/src/main/resources/static/js/clientes.js @@ -82,26 +82,42 @@ async function cadastrarCliente(e) { } function mostrarMensagem(mensagem, tipo) { - const formSection = document.querySelector('.form-section'); + // Remove mensagens anteriores + const mensagensAnteriores = document.querySelectorAll('.mensagem-temporaria'); + mensagensAnteriores.forEach(msg => msg.remove()); + const div = document.createElement('div'); - div.className = tipo; + div.className = `${tipo} mensagem-temporaria`; div.textContent = mensagem; - formSection.insertBefore(div, formSection.firstChild); + div.style.marginBottom = '16px'; + + // Insere a mensagem no início do main + const main = document.querySelector('main'); + if (main) { + main.insertBefore(div, main.firstChild); + } setTimeout(() => div.remove(), 5000); } function mostrarErros(errors) { - const formSection = document.querySelector('.form-section'); - let html = '
    '; + // Remove mensagens anteriores + const mensagensAnteriores = document.querySelectorAll('.mensagem-temporaria'); + mensagensAnteriores.forEach(msg => msg.remove()); + + let html = '
      '; for (const [campo, mensagem] of Object.entries(errors)) { html += `
    • ${campo}: ${mensagem}
    • `; } html += '
    '; - formSection.insertAdjacentHTML('afterbegin', html); + + const main = document.querySelector('main'); + if (main) { + main.insertAdjacentHTML('afterbegin', html); + } setTimeout(() => { - const errorDiv = formSection.querySelector('.error'); + const errorDiv = document.querySelector('.mensagem-temporaria'); if (errorDiv) errorDiv.remove(); }, 5000); } @@ -114,9 +130,12 @@ async function deletarCliente(id) { method: 'DELETE' }); - if (response.ok) { + if (response.ok || response.status === 204) { mostrarMensagem('Cliente excluĆ­do com sucesso!', 'success'); carregarClientes(); + } else { + const errorData = await response.json().catch(() => ({ message: 'Erro ao excluir cliente' })); + mostrarMensagem(`Erro ao excluir cliente: ${errorData.message || 'Erro desconhecido'}`, 'error'); } } catch (error) { mostrarMensagem(`Erro ao excluir cliente: ${error.message}`, 'error'); diff --git a/src/main/resources/static/js/pedidos.js b/src/main/resources/static/js/pedidos.js index 89f7c84..b261c38 100644 --- a/src/main/resources/static/js/pedidos.js +++ b/src/main/resources/static/js/pedidos.js @@ -174,7 +174,7 @@ async function carregarPedidos() { return; } - let html = ''; + let html = '
    IDClienteDataStatusItensTotal
    '; pedidos.forEach(pedido => { const data = new Date(pedido.dataPedido).toLocaleString('pt-BR'); @@ -190,6 +190,9 @@ async function carregarPedidos() { + `; }); @@ -202,12 +205,41 @@ async function carregarPedidos() { } } +async function deletarPedido(id) { + if (!confirm('Tem certeza que deseja excluir este pedido?')) return; + + try { + const response = await fetch(`${API_URL}/pedidos/${id}`, { + method: 'DELETE' + }); + + if (response.ok || response.status === 204) { + mostrarMensagem('Pedido excluĆ­do com sucesso!', 'success'); + carregarPedidos(); + } else { + const errorData = await response.json().catch(() => ({ message: 'Erro ao excluir pedido' })); + mostrarMensagem(`Erro ao excluir pedido: ${errorData.message || 'Erro desconhecido'}`, 'error'); + } + } catch (error) { + mostrarMensagem(`Erro ao excluir pedido: ${error.message}`, 'error'); + } +} + function mostrarMensagem(mensagem, tipo) { - const formSection = document.querySelector('.form-section'); + // Remove mensagens anteriores + const mensagensAnteriores = document.querySelectorAll('.mensagem-temporaria'); + mensagensAnteriores.forEach(msg => msg.remove()); + const div = document.createElement('div'); - div.className = tipo; + div.className = `${tipo} mensagem-temporaria`; div.textContent = mensagem; - formSection.insertBefore(div, formSection.firstChild); + div.style.marginBottom = '16px'; + + // Insere a mensagem no inĆ­cio do main + const main = document.querySelector('main'); + if (main) { + main.insertBefore(div, main.firstChild); + } setTimeout(() => div.remove(), 5000); } diff --git a/src/main/resources/static/js/produtos.js b/src/main/resources/static/js/produtos.js index 61520dd..a0dfc53 100644 --- a/src/main/resources/static/js/produtos.js +++ b/src/main/resources/static/js/produtos.js @@ -115,9 +115,12 @@ async function deletarProduto(id) { method: 'DELETE' }); - if (response.ok) { + if (response.ok || response.status === 204) { mostrarMensagem('Produto excluĆ­do com sucesso!', 'success'); carregarProdutos(); + } else { + const errorData = await response.json().catch(() => ({ message: 'Erro ao excluir produto' })); + mostrarMensagem(`Erro ao excluir produto: ${errorData.message || 'Erro desconhecido'}`, 'error'); } } catch (error) { mostrarMensagem(`Erro ao excluir produto: ${error.message}`, 'error'); diff --git a/src/main/resources/templates/clientes.html b/src/main/resources/templates/clientes.html index 409604d..cc08e24 100644 --- a/src/main/resources/templates/clientes.html +++ b/src/main/resources/templates/clientes.html @@ -58,15 +58,27 @@

    Lista de Clientes

    -
    -
    IDClienteDataStatusItensTotalAƧƵes
    ${pedido.status} ${pedido.itens.length} item(ns) R$ ${total.toFixed(2)} + +