From c81a1ea5882cb9aa8595097025da173c586e4c9d Mon Sep 17 00:00:00 2001 From: Dennis Eckerskorn Date: Thu, 15 May 2025 22:33:54 +0200 Subject: [PATCH] Added productservices and list to add, update, remove products --- .../libs/memberflow-data-1.0-SNAPSHOT.jar | Bin 260412 -> 260726 bytes .../ProductServiceController.java | 13 +- .../ProductServiceDTO.java | 13 ++ .../ProductServiceRepository.java | 2 + .../ProductServiceService.java | 38 +++- .../ProductServiceServiceTest.java | 41 ++-- .../src/components/forms/ProductForm.jsx | 126 +++++++++++ .../src/components/layout/MainLayout.jsx | 5 + .../src/components/lists/ProductList.jsx | 200 ++++++++++++++++++ 9 files changed, 414 insertions(+), 24 deletions(-) create mode 100644 memberflow-frontend/src/components/forms/ProductForm.jsx create mode 100644 memberflow-frontend/src/components/lists/ProductList.jsx diff --git a/memberflow-api/libs/memberflow-data-1.0-SNAPSHOT.jar b/memberflow-api/libs/memberflow-data-1.0-SNAPSHOT.jar index ec84b553274bdc6a1a0438d083802a6255584da9..2776aa273c5378e39d146818c26f0fd9aee1b2b4 100644 GIT binary patch delta 4769 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 delta 4337 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 diff --git a/memberflow-api/src/main/java/com/denniseckerskorn/controllers/finance_management/ProductServiceController.java b/memberflow-api/src/main/java/com/denniseckerskorn/controllers/finance_management/ProductServiceController.java index da8873f..1b5eb90 100644 --- a/memberflow-api/src/main/java/com/denniseckerskorn/controllers/finance_management/ProductServiceController.java +++ b/memberflow-api/src/main/java/com/denniseckerskorn/controllers/finance_management/ProductServiceController.java @@ -1,10 +1,12 @@ package com.denniseckerskorn.controllers.finance_management; import com.denniseckerskorn.dtos.finance_management_dtos.ProductServiceDTO; +import com.denniseckerskorn.entities.finance.IVAType; import com.denniseckerskorn.entities.finance.ProductService; import com.denniseckerskorn.exceptions.DuplicateEntityException; import com.denniseckerskorn.exceptions.EntityNotFoundException; import com.denniseckerskorn.exceptions.InvalidDataException; +import com.denniseckerskorn.services.finance_services.IVATypeService; import com.denniseckerskorn.services.finance_services.ProductServiceService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -22,25 +24,30 @@ import java.util.stream.Collectors; public class ProductServiceController { private final ProductServiceService productServiceService; + private final IVATypeService ivaTypeService; - public ProductServiceController(ProductServiceService productServiceService) { + public ProductServiceController(ProductServiceService productServiceService, IVATypeService ivaTypeService) { this.productServiceService = productServiceService; + this.ivaTypeService = ivaTypeService; } @PostMapping("/create") @Operation(summary = "Create a new product/service") public ResponseEntity create(@Valid @RequestBody ProductServiceDTO dto) throws DuplicateEntityException { - ProductService saved = productServiceService.save(dto.toEntity()); + IVAType ivaType = ivaTypeService.findById(dto.getIvaTypeId()); + ProductService saved = productServiceService.save(dto.toEntityWithIVA(ivaType)); return new ResponseEntity<>(new ProductServiceDTO(saved), HttpStatus.CREATED); } @PutMapping("/update") @Operation(summary = "Update an existing product/service") public ResponseEntity update(@Valid @RequestBody ProductServiceDTO dto) throws EntityNotFoundException, InvalidDataException { - ProductService updated = productServiceService.update(dto.toEntity()); + IVAType ivaType = ivaTypeService.findById(dto.getIvaTypeId()); + ProductService updated = productServiceService.update(dto.toEntityWithIVA(ivaType)); return new ResponseEntity<>(new ProductServiceDTO(updated), HttpStatus.OK); } + @GetMapping("/getById/{id}") @Operation(summary = "Get product/service by ID") public ResponseEntity getById(@PathVariable Integer id) throws EntityNotFoundException, InvalidDataException { diff --git a/memberflow-api/src/main/java/com/denniseckerskorn/dtos/finance_management_dtos/ProductServiceDTO.java b/memberflow-api/src/main/java/com/denniseckerskorn/dtos/finance_management_dtos/ProductServiceDTO.java index ad10182..c5e6f90 100644 --- a/memberflow-api/src/main/java/com/denniseckerskorn/dtos/finance_management_dtos/ProductServiceDTO.java +++ b/memberflow-api/src/main/java/com/denniseckerskorn/dtos/finance_management_dtos/ProductServiceDTO.java @@ -1,5 +1,6 @@ package com.denniseckerskorn.dtos.finance_management_dtos; +import com.denniseckerskorn.entities.finance.IVAType; import com.denniseckerskorn.entities.finance.ProductService; import com.denniseckerskorn.enums.StatusValues; import jakarta.validation.constraints.NotNull; @@ -54,6 +55,18 @@ public class ProductServiceDTO { return ps; } + public ProductService toEntityWithIVA(IVAType ivaType) { + ProductService entity = new ProductService(); + entity.setId(this.id); + entity.setName(this.name); + entity.setPrice(this.price); + entity.setType(this.type); + entity.setStatus(this.status); + entity.setIvaType(ivaType); + return entity; + } + + // Getters y setters... diff --git a/memberflow-data/src/main/java/com/denniseckerskorn/repositories/finance_repositories/ProductServiceRepository.java b/memberflow-data/src/main/java/com/denniseckerskorn/repositories/finance_repositories/ProductServiceRepository.java index 51470bd..984052a 100644 --- a/memberflow-data/src/main/java/com/denniseckerskorn/repositories/finance_repositories/ProductServiceRepository.java +++ b/memberflow-data/src/main/java/com/denniseckerskorn/repositories/finance_repositories/ProductServiceRepository.java @@ -7,4 +7,6 @@ public interface ProductServiceRepository extends JpaRepository findAll() { logger.info("Retrieving all products/services"); @@ -67,11 +95,11 @@ public class ProductServiceService extends AbstractService productServiceService.save(product)); - } - @Test - void save_NullName_ShouldThrow() { - product.setName(null); - assertThrows(InvalidDataException.class, () -> productServiceService.save(product)); + assertThrows(DuplicateEntityException.class, () -> productServiceService.save(product)); } @Test @@ -81,10 +84,16 @@ class ProductServiceServiceTest { assertThrows(InvalidDataException.class, () -> productServiceService.save(product)); } + @Test + void save_NullName_ShouldThrow() { + product.setName(null); + assertThrows(InvalidDataException.class, () -> productServiceService.save(product)); + } + @Test void save_NoIVAType_ShouldThrow() { product.setIvaType(null); - assertThrows(InvalidDataException.class, () -> productServiceService.save(product)); + assertThrows(IllegalArgumentException.class, () -> productServiceService.save(product)); } @Test diff --git a/memberflow-frontend/src/components/forms/ProductForm.jsx b/memberflow-frontend/src/components/forms/ProductForm.jsx new file mode 100644 index 0000000..5dc581c --- /dev/null +++ b/memberflow-frontend/src/components/forms/ProductForm.jsx @@ -0,0 +1,126 @@ +import React, { useState, useEffect } from "react"; +import api from "../../api/axiosConfig"; +import ErrorMessage from "../common/ErrorMessage"; + +const ProductForm = ({ onProductAdded }) => { + const [name, setName] = useState(""); + const [price, setPrice] = useState(""); + const [type, setType] = useState("PRODUCT"); + const [status, setStatus] = useState("ACTIVE"); + const [ivaTypes, setIvaTypes] = useState([]); + const [selectedIvaId, setSelectedIvaId] = useState(""); + const [error, setError] = useState(""); + const [success, setSuccess] = useState(""); + + useEffect(() => { + api.get("/iva-types/getAll").then((res) => setIvaTypes(res.data)); + }, []); + + const handleSubmit = async (e) => { + e.preventDefault(); + setError(""); + setSuccess(""); + + if (!name || !price || !selectedIvaId || !type || !status) { + setError("Todos los campos son obligatorios."); + return; + } + + const ivaTypeIdParsed = parseInt(selectedIvaId); + if (isNaN(ivaTypeIdParsed)) { + setError("Debes seleccionar un tipo de IVA válido."); + return; + } + + try { + await api.post("/products-services/create", { + name, + price: parseFloat(price), + ivaTypeId: ivaTypeIdParsed, + type, + status, + }); + + setSuccess("✅ Producto/servicio creado correctamente."); + setName(""); + setPrice(""); + setSelectedIvaId(""); + setType("PRODUCT"); + setStatus("ACTIVE"); + + if (onProductAdded) onProductAdded(); // refrescar lista si se usa en conjunto + } catch (err) { + console.error(err); + setError("❌ Error al crear el producto/servicio."); + } + }; + + return ( +
+
+

Crear Producto o Servicio

+ +
+ + setName(e.target.value)} + /> + + + setPrice(e.target.value)} + /> + + + + + + + + + + + + + + + +
+
+ ); +}; + +export default ProductForm; diff --git a/memberflow-frontend/src/components/layout/MainLayout.jsx b/memberflow-frontend/src/components/layout/MainLayout.jsx index 93b6872..c6a676c 100644 --- a/memberflow-frontend/src/components/layout/MainLayout.jsx +++ b/memberflow-frontend/src/components/layout/MainLayout.jsx @@ -26,6 +26,8 @@ import InvoiceForm from '../forms/InvoiceForm'; import InvoiceList from '../lists/InvoiceList'; import PaymentForm from '../forms/PaymentForm'; import PaymentList from '../lists/PaymentList'; +import ProductForm from '../forms/ProductForm'; +import ProductList from '../lists/ProductList'; import '../styles/MainLayout.css'; @@ -73,6 +75,9 @@ const MainLayout = () => { } /> } /> } /> + } /> + } /> + diff --git a/memberflow-frontend/src/components/lists/ProductList.jsx b/memberflow-frontend/src/components/lists/ProductList.jsx new file mode 100644 index 0000000..cbb8ce3 --- /dev/null +++ b/memberflow-frontend/src/components/lists/ProductList.jsx @@ -0,0 +1,200 @@ +import React, { useEffect, useState } from "react"; +import api from "../../api/axiosConfig"; +import "../styles/ContentArea.css"; + +const ProductList = () => { + const [products, setProducts] = useState([]); + const [ivaTypes, setIvaTypes] = useState([]); + const [editIndex, setEditIndex] = useState(null); + const [editableProduct, setEditableProduct] = useState(null); + + const fetchProducts = async () => { + const res = await api.get("/products-services/getAll"); + setProducts(res.data); + }; + + const fetchIvaTypes = async () => { + const res = await api.get("/iva-types/getAll"); + setIvaTypes(res.data); + }; + + useEffect(() => { + fetchProducts(); + fetchIvaTypes(); + }, []); + + useEffect(() => { + console.log("IVA Types cargados:", ivaTypes); +}, [ivaTypes]); + + const startEdit = (index) => { + setEditIndex(index); + setEditableProduct({ ...products[index] }); // contiene ivaTypeId directamente + }; + + const cancelEdit = () => { + setEditIndex(null); + setEditableProduct(null); + }; + + const handleChange = (field, value) => { + setEditableProduct((prev) => ({ + ...prev, + [field]: field === "ivaTypeId" ? parseInt(value) : value, + })); + }; + + const handleSave = async () => { + try { + const dto = { + id: editableProduct.id, + name: editableProduct.name, + price: parseFloat(editableProduct.price), + type: editableProduct.type, + status: editableProduct.status, + ivaTypeId: editableProduct.ivaTypeId, + }; + + await api.put("/products-services/update", dto); + alert("✅ Producto actualizado correctamente."); + setEditIndex(null); + setEditableProduct(null); + fetchProducts(); + } catch (err) { + console.error(err); + alert("❌ Error al actualizar el producto."); + } + }; + + const handleDelete = async (id) => { + if (window.confirm("¿Estás seguro de que deseas eliminar este producto?")) { + await api.delete(`/products-services/deleteById/${id}`); + fetchProducts(); + } + }; + + const getIVADisplay = (ivaTypeId) => { + const iva = ivaTypes.find((i) => i.id === Number(ivaTypeId)); + return iva ? `${iva.percentage}%` : "Sin IVA"; +}; + + + return ( +
+

Lista de Productos y Servicios

+ + + + + + + + + + + + + + + {products.map((p, index) => { + const isEditing = index === editIndex; + return ( + + + + + + + + + + ); + })} + +
IDNombrePrecioIVATipoEstadoAcciones
{p.id} + {isEditing ? ( + handleChange("name", e.target.value)} + /> + ) : ( + p.name + )} + + {isEditing ? ( + handleChange("price", e.target.value)} + /> + ) : ( + `${p.price.toFixed(2)} €` + )} + + {isEditing ? ( + + ) : ( + getIVADisplay(p.ivaTypeId) + )} + + {isEditing ? ( + + ) : ( + p.type + )} + + {isEditing ? ( + + ) : ( + p.status + )} + + {isEditing ? ( + <> + + + + ) : ( + <> + + + + )} +
+
+ ); +}; + +export default ProductList;