diff --git a/README.md b/README.md index 65bf4e7..8d1e586 100644 --- a/README.md +++ b/README.md @@ -32,13 +32,16 @@ O arquivo `src/main/resources/application.properties` já está versionado. Atua ```properties spring.datasource.url=jdbc:postgresql://localhost:5433/projeto_vendas -spring.datasource.username=postgres -spring.datasource.password=12345 +spring.datasource.username=nome do seu banco aqui +spring.datasource.password=sua senha do banco aqui 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 +62,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 +166,111 @@ 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: + +image + + +**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: + +image + + + +**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: + +image + + + + +**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: + +image + + +**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/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..28a62f9 --- /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/**") + .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 97b990c..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 @@ -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; @@ -25,48 +26,51 @@ import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/clientes") +@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); + 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) @@ -88,4 +92,16 @@ 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()); + } + + @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 b2eb3e3..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,18 +3,22 @@ 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; @RestController -@RequestMapping("/pedidos") +@RequestMapping("/api/pedidos") public class PedidoController { @Autowired private PedidoService pedidoService; @@ -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 7a66888..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 @@ -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; @@ -22,14 +24,14 @@ import org.springframework.web.bind.annotation.RestController; @RestController -@RequestMapping("/produtos") +@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,36 @@ 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); + 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) + @ExceptionHandler(EntityNotFoundException.class) + 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/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/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/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 new file mode 100644 index 0000000..25495a0 --- /dev/null +++ b/src/main/java/com/example/sistema_vendas_api/service/ClienteService.java @@ -0,0 +1,63 @@ +package com.example.sistema_vendas_api.service; + +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; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class ClienteService { + + private final ClienteRepository clienteRepository; + private final PedidoRepository pedidoRepository; + + public ClienteService(ClienteRepository clienteRepository, PedidoRepository pedidoRepository) { + this.clienteRepository = clienteRepository; + this.pedidoRepository = pedidoRepository; + } + + @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) { + 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 new file mode 100644 index 0000000..76ba203 --- /dev/null +++ b/src/main/java/com/example/sistema_vendas_api/service/ProdutoService.java @@ -0,0 +1,64 @@ +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; +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; + private final ItemPedidoRepository itemPedidoRepository; + + public ProdutoService(ProdutoRepository produtoRepository, ItemPedidoRepository itemPedidoRepository) { + this.produtoRepository = produtoRepository; + this.itemPedidoRepository = itemPedidoRepository; + } + + @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) { + 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/application.properties b/src/main/resources/application.properties index 6bf4566..b63a218 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,14 +1,17 @@ -# --- 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. -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 -# --- Configura��es do JPA (Hibernate) --- +# --- Configurações do JPA (Hibernate) --- 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 new file mode 100644 index 0000000..fa081f1 --- /dev/null +++ b/src/main/resources/static/css/style.css @@ -0,0 +1,382 @@ +* { + 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: 32px 16px; + position: relative; + overflow-x: hidden; +} + +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; +} + +.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%; +} + +.app-header > div { + width: 100%; + text-align: center; +} + +.app-header h1 { + color: #1f2d3d; + font-weight: 700; + line-height: 1.2; + margin: 0; + justify-content: center; + text-align: center; +} + +.app-header h1 span:first-child { + display: inline-flex; + align-items: center; + justify-content: center; + line-height: 1; + flex-shrink: 0; +} + +.app-header h1 > span:not(:first-child) { + text-align: center; +} + +.app-header .subtitle { + color: #4c566a; + font-size: 1rem; + line-height: 1.5; + margin-top: 4px; + text-align: center; +} + +@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; + } +} + +.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%; +} + +.feature-card:hover { + transform: translateY(-8px); + box-shadow: 0 32px 60px rgba(15, 23, 42, 0.15); + color: #1f2d3d; +} + +.feature-icon { + font-size: 3.5rem; + margin-bottom: 16px; + line-height: 1; + width: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +.feature-card h2 { + font-weight: 600; + margin-bottom: 8px; + color: #1f2d3d; + text-align: center; +} + +.feature-card p { + margin: 0; + color: #4c566a; + text-align: center; + line-height: 1.5; +} + +.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; +} + +.section-title { + font-weight: 600; + color: #1f2d3d; + border-bottom: 2px solid rgba(102, 126, 234, 0.3); + padding-bottom: 8px; + margin-bottom: 24px; +} + +.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; +} + +.btn-gradient:hover { + transform: translateY(-1px); + box-shadow: 0 16px 30px rgba(90, 47, 194, 0.35); + color: #fff; +} + +.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-soft:hover { + background: rgba(255, 255, 255, 0.4); + color: #472a87; +} + +.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; +} + +.item-pedido button { + height: 42px; +} + +.loading { + text-align: center; + padding: 20px; + color: #6c757d; +} + +.error, +.success { + padding: 14px 16px; + border-radius: 10px; + margin-bottom: 20px; + font-weight: 500; +} + +.error { + background: #ffe2e5; + color: #b42318; +} + +.success { + background: #d1f7c4; + color: #24613c; +} + +.table-container { + margin-top: 20px; +} + +.table-container table { + border-radius: 12px; + overflow: hidden; +} + +.table-container th { + background: rgba(102, 126, 234, 0.12); + color: #1f2d3d; +} + +footer { + color: rgba(255, 255, 255, 0.9); +} + +.app-footer { + text-align: center; + margin-top: 48px; + color: rgba(255, 255, 255, 0.85); +} + +.footer-content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 16px; +} + +.footer-profiles-container { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; + gap: 8px; +} + +.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-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) { + .footer-content { + gap: 12px; + } + + .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 new file mode 100644 index 0000000..13a11e2 --- /dev/null +++ b/src/main/resources/static/js/clientes.js @@ -0,0 +1,149 @@ +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', () => { + 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) { + // Remove mensagens anteriores + const mensagensAnteriores = document.querySelectorAll('.mensagem-temporaria'); + mensagensAnteriores.forEach(msg => msg.remove()); + + const div = document.createElement('div'); + div.className = `${tipo} mensagem-temporaria`; + div.textContent = mensagem; + 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) { + // 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 += '
'; + + const main = document.querySelector('main'); + if (main) { + main.insertAdjacentHTML('afterbegin', html); + } + + setTimeout(() => { + const errorDiv = document.querySelector('.mensagem-temporaria'); + 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 || 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'); + } +} + +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..b261c38 --- /dev/null +++ b/src/main/resources/static/js/pedidos.js @@ -0,0 +1,246 @@ +const API_URL = + window.location.origin && window.location.origin !== 'null' + ? `${window.location.origin}/api` + : 'http://localhost:8081/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 += '
IDClienteDataStatusItensTotalAções
#${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}
`; + } +} + +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) { + // Remove mensagens anteriores + const mensagensAnteriores = document.querySelectorAll('.mensagem-temporaria'); + mensagensAnteriores.forEach(msg => msg.remove()); + + const div = document.createElement('div'); + div.className = `${tipo} mensagem-temporaria`; + div.textContent = mensagem; + 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 new file mode 100644 index 0000000..a0dfc53 --- /dev/null +++ b/src/main/resources/static/js/produtos.js @@ -0,0 +1,133 @@ +const API_URL = + window.location.origin && window.location.origin !== 'null' + ? `${window.location.origin}/api` + : 'http://localhost:8081/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 || 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'); + } +} + +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..cc08e24 --- /dev/null +++ b/src/main/resources/templates/clientes.html @@ -0,0 +1,90 @@ + + + + + + Gerenciar Clientes + + + + + +
+
+
+

+ 👥 + Gerenciar Clientes +

+

Cadastre novos contatos e acompanhe toda a base

+
+ ← 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..c0d0252 --- /dev/null +++ b/src/main/resources/templates/index.html @@ -0,0 +1,77 @@ + + + + + + Sistema de Vendas + + + + + +
+
+
+

+ 🛒 + 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..6507532 --- /dev/null +++ b/src/main/resources/templates/pedidos.html @@ -0,0 +1,107 @@ + + + + + + Gerenciar Pedidos + + + + + +
+
+
+

+ 🛍️ + Gerenciar Pedidos +

+

Monte pedidos completos e acompanhe o status em tempo real

+
+ ← 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..810ef37 --- /dev/null +++ b/src/main/resources/templates/produtos.html @@ -0,0 +1,96 @@ + + + + + + Gerenciar Produtos + + + + + +
+
+
+

+ 📦 + Gerenciar Produtos +

+

Mantenha o catálogo atualizado e o estoque organizado

+
+ ← Voltar +
+ +
+
+
+
+

Cadastrar Novo Produto

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

Lista de Produtos

+
+
Carregando...
+
+
+
+
+
+ + + + + + + +