diff --git a/memberflow-api/libs/memberflow-data-1.0-SNAPSHOT.jar b/memberflow-api/libs/memberflow-data-1.0-SNAPSHOT.jar index ec84b55..2776aa2 100644 Binary files a/memberflow-api/libs/memberflow-data-1.0-SNAPSHOT.jar and b/memberflow-api/libs/memberflow-data-1.0-SNAPSHOT.jar differ 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;