Added productservices and list to add, update, remove products

This commit is contained in:
Dennis Eckerskorn 2025-05-15 22:33:54 +02:00
parent ab3485984e
commit c81a1ea588
9 changed files with 414 additions and 24 deletions

View File

@ -1,10 +1,12 @@
package com.denniseckerskorn.controllers.finance_management; package com.denniseckerskorn.controllers.finance_management;
import com.denniseckerskorn.dtos.finance_management_dtos.ProductServiceDTO; import com.denniseckerskorn.dtos.finance_management_dtos.ProductServiceDTO;
import com.denniseckerskorn.entities.finance.IVAType;
import com.denniseckerskorn.entities.finance.ProductService; import com.denniseckerskorn.entities.finance.ProductService;
import com.denniseckerskorn.exceptions.DuplicateEntityException; import com.denniseckerskorn.exceptions.DuplicateEntityException;
import com.denniseckerskorn.exceptions.EntityNotFoundException; import com.denniseckerskorn.exceptions.EntityNotFoundException;
import com.denniseckerskorn.exceptions.InvalidDataException; import com.denniseckerskorn.exceptions.InvalidDataException;
import com.denniseckerskorn.services.finance_services.IVATypeService;
import com.denniseckerskorn.services.finance_services.ProductServiceService; import com.denniseckerskorn.services.finance_services.ProductServiceService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@ -22,25 +24,30 @@ import java.util.stream.Collectors;
public class ProductServiceController { public class ProductServiceController {
private final ProductServiceService productServiceService; private final ProductServiceService productServiceService;
private final IVATypeService ivaTypeService;
public ProductServiceController(ProductServiceService productServiceService) { public ProductServiceController(ProductServiceService productServiceService, IVATypeService ivaTypeService) {
this.productServiceService = productServiceService; this.productServiceService = productServiceService;
this.ivaTypeService = ivaTypeService;
} }
@PostMapping("/create") @PostMapping("/create")
@Operation(summary = "Create a new product/service") @Operation(summary = "Create a new product/service")
public ResponseEntity<ProductServiceDTO> create(@Valid @RequestBody ProductServiceDTO dto) throws DuplicateEntityException { public ResponseEntity<ProductServiceDTO> 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); return new ResponseEntity<>(new ProductServiceDTO(saved), HttpStatus.CREATED);
} }
@PutMapping("/update") @PutMapping("/update")
@Operation(summary = "Update an existing product/service") @Operation(summary = "Update an existing product/service")
public ResponseEntity<ProductServiceDTO> update(@Valid @RequestBody ProductServiceDTO dto) throws EntityNotFoundException, InvalidDataException { public ResponseEntity<ProductServiceDTO> 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); return new ResponseEntity<>(new ProductServiceDTO(updated), HttpStatus.OK);
} }
@GetMapping("/getById/{id}") @GetMapping("/getById/{id}")
@Operation(summary = "Get product/service by ID") @Operation(summary = "Get product/service by ID")
public ResponseEntity<ProductServiceDTO> getById(@PathVariable Integer id) throws EntityNotFoundException, InvalidDataException { public ResponseEntity<ProductServiceDTO> getById(@PathVariable Integer id) throws EntityNotFoundException, InvalidDataException {

View File

@ -1,5 +1,6 @@
package com.denniseckerskorn.dtos.finance_management_dtos; package com.denniseckerskorn.dtos.finance_management_dtos;
import com.denniseckerskorn.entities.finance.IVAType;
import com.denniseckerskorn.entities.finance.ProductService; import com.denniseckerskorn.entities.finance.ProductService;
import com.denniseckerskorn.enums.StatusValues; import com.denniseckerskorn.enums.StatusValues;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
@ -54,6 +55,18 @@ public class ProductServiceDTO {
return ps; 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... // Getters y setters...

View File

@ -7,4 +7,6 @@ public interface ProductServiceRepository extends JpaRepository<ProductService,
boolean existsByName(String name); boolean existsByName(String name);
boolean existsByIvaTypeId(Integer ivaTypeId); boolean existsByIvaTypeId(Integer ivaTypeId);
ProductService findByName(String name);
} }

View File

@ -1,11 +1,13 @@
package com.denniseckerskorn.services.finance_services; package com.denniseckerskorn.services.finance_services;
import com.denniseckerskorn.entities.finance.IVAType;
import com.denniseckerskorn.entities.finance.ProductService; import com.denniseckerskorn.entities.finance.ProductService;
import com.denniseckerskorn.exceptions.DuplicateEntityException; import com.denniseckerskorn.exceptions.DuplicateEntityException;
import com.denniseckerskorn.exceptions.EntityNotFoundException; import com.denniseckerskorn.exceptions.EntityNotFoundException;
import com.denniseckerskorn.exceptions.InvalidDataException; import com.denniseckerskorn.exceptions.InvalidDataException;
import com.denniseckerskorn.repositories.finance_repositories.ProductServiceRepository; import com.denniseckerskorn.repositories.finance_repositories.ProductServiceRepository;
import com.denniseckerskorn.services.AbstractService; import com.denniseckerskorn.services.AbstractService;
import jakarta.transaction.Transactional;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -18,29 +20,55 @@ public class ProductServiceService extends AbstractService<ProductService, Integ
private static final Logger logger = LoggerFactory.getLogger(ProductServiceService.class); private static final Logger logger = LoggerFactory.getLogger(ProductServiceService.class);
private final ProductServiceRepository productServiceRepository; private final ProductServiceRepository productServiceRepository;
private final IVATypeService ivaTypeService;
public ProductServiceService(ProductServiceRepository productServiceRepository) { public ProductServiceService(ProductServiceRepository productServiceRepository, IVATypeService ivaTypeService) {
super(productServiceRepository); super(productServiceRepository);
this.productServiceRepository = productServiceRepository; this.productServiceRepository = productServiceRepository;
this.ivaTypeService = ivaTypeService;
} }
@Override @Override
@Transactional
public ProductService save(ProductService entity) throws IllegalArgumentException, DuplicateEntityException { public ProductService save(ProductService entity) throws IllegalArgumentException, DuplicateEntityException {
logger.info("Saving product/service: {}", entity); logger.info("Saving product/service: {}", entity);
if (entity.getIvaType() == null || entity.getIvaType().getId() == null) {
throw new IllegalArgumentException("Product must have an IVA type assigned");
}
IVAType ivaType = ivaTypeService.findById(entity.getIvaType().getId());
entity.setIvaType(ivaType);
validateProduct(entity); validateProduct(entity);
if (productServiceRepository.existsByName(entity.getName())) {
ProductService existingProduct = productServiceRepository.findByName(entity.getName());
if (existingProduct != null && productServiceRepository.existsByName(existingProduct.getName())) {
throw new DuplicateEntityException("Product/Service with this name already exists"); throw new DuplicateEntityException("Product/Service with this name already exists");
} }
return super.save(entity); return super.save(entity);
} }
@Override @Override
@Transactional
public ProductService update(ProductService entity) throws IllegalArgumentException, InvalidDataException, EntityNotFoundException { public ProductService update(ProductService entity) throws IllegalArgumentException, InvalidDataException, EntityNotFoundException {
logger.info("Updating product/service: {}", entity); logger.info("Updating product/service: {}", entity);
if (entity.getIvaType() == null || entity.getIvaType().getId() == null) {
throw new IllegalArgumentException("Product must have an IVA type assigned");
}
IVAType ivaType = ivaTypeService.findById(entity.getIvaType().getId());
entity.setIvaType(ivaType);
validateProduct(entity); validateProduct(entity);
return super.update(entity); return super.update(entity);
} }
@Override @Override
public List<ProductService> findAll() { public List<ProductService> findAll() {
logger.info("Retrieving all products/services"); logger.info("Retrieving all products/services");
@ -67,11 +95,11 @@ public class ProductServiceService extends AbstractService<ProductService, Integ
if (product.getPrice() == null || product.getPrice().compareTo(BigDecimal.ZERO) <= 0) { if (product.getPrice() == null || product.getPrice().compareTo(BigDecimal.ZERO) <= 0) {
throw new InvalidDataException("Product price must be greater than 0"); throw new InvalidDataException("Product price must be greater than 0");
} }
if (product.getIvaType() == null) {
throw new InvalidDataException("Product must have an IVA type assigned");
}
if (product.getStatus() == null) { if (product.getStatus() == null) {
throw new InvalidDataException("Product status cannot be null"); throw new InvalidDataException("Product status cannot be null");
} }
if (product.getIvaType() == null || product.getIvaType().getId() == null) {
throw new IllegalArgumentException("Product must have an IVA type assigned");
}
} }
} }

View File

@ -1,4 +1,4 @@
package com.denniseckerskorn.services.finance_management_services; package com.denniseckerskorn.services.finance_services;
import com.denniseckerskorn.entities.finance.IVAType; import com.denniseckerskorn.entities.finance.IVAType;
import com.denniseckerskorn.entities.finance.ProductService; import com.denniseckerskorn.entities.finance.ProductService;
@ -6,7 +6,6 @@ import com.denniseckerskorn.enums.StatusValues;
import com.denniseckerskorn.exceptions.DuplicateEntityException; import com.denniseckerskorn.exceptions.DuplicateEntityException;
import com.denniseckerskorn.exceptions.InvalidDataException; import com.denniseckerskorn.exceptions.InvalidDataException;
import com.denniseckerskorn.repositories.finance_repositories.ProductServiceRepository; import com.denniseckerskorn.repositories.finance_repositories.ProductServiceRepository;
import com.denniseckerskorn.services.finance_services.ProductServiceService;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -23,6 +22,9 @@ class ProductServiceServiceTest {
@Mock @Mock
private ProductServiceRepository productServiceRepository; private ProductServiceRepository productServiceRepository;
@Mock
private IVATypeService ivaTypeService;
@Mock @Mock
private EntityManager entityManager; private EntityManager entityManager;
@ -38,17 +40,18 @@ class ProductServiceServiceTest {
ivaType = new IVAType(); ivaType = new IVAType();
ivaType.setId(1); ivaType.setId(1);
ivaType.setPercentage(new BigDecimal("21.00")); ivaType.setPercentage(BigDecimal.valueOf(21));
ivaType.setDescription("IVA General"); ivaType.setDescription("IVA General");
product = new ProductService(); product = new ProductService();
product.setId(10); product.setId(1);
product.setName("Clase Avanzada"); product.setName("Producto Test");
product.setType("Servicio"); product.setPrice(BigDecimal.valueOf(100));
product.setPrice(new BigDecimal("30.00")); product.setType("PRODUCT");
product.setIvaType(ivaType);
product.setStatus(StatusValues.ACTIVE); product.setStatus(StatusValues.ACTIVE);
product.setIvaType(ivaType);
// Set EntityManager in abstract service
Field emField = ProductServiceService.class.getSuperclass().getDeclaredField("entityManager"); Field emField = ProductServiceService.class.getSuperclass().getDeclaredField("entityManager");
emField.setAccessible(true); emField.setAccessible(true);
emField.set(productServiceService, entityManager); emField.set(productServiceService, entityManager);
@ -56,23 +59,23 @@ class ProductServiceServiceTest {
@Test @Test
void save_ValidProduct_ShouldReturnSaved() { void save_ValidProduct_ShouldReturnSaved() {
when(ivaTypeService.findById(ivaType.getId())).thenReturn(ivaType);
when(productServiceRepository.findByName(product.getName())).thenReturn(null);
when(productServiceRepository.existsByName(product.getName())).thenReturn(false); when(productServiceRepository.existsByName(product.getName())).thenReturn(false);
when(productServiceRepository.save(any())).thenReturn(product); when(productServiceRepository.save(any())).thenReturn(product);
ProductService saved = productServiceService.save(product); ProductService saved = productServiceService.save(product);
assertEquals("Clase Avanzada", saved.getName()); assertEquals(product.getName(), saved.getName());
assertEquals(product.getPrice(), saved.getPrice());
} }
@Test @Test
void save_DuplicateName_ShouldThrow() { void save_DuplicateName_ShouldThrow() {
when(ivaTypeService.findById(ivaType.getId())).thenReturn(ivaType);
when(productServiceRepository.findByName(product.getName())).thenReturn(product);
when(productServiceRepository.existsByName(product.getName())).thenReturn(true); when(productServiceRepository.existsByName(product.getName())).thenReturn(true);
assertThrows(DuplicateEntityException.class, () -> productServiceService.save(product));
}
@Test assertThrows(DuplicateEntityException.class, () -> productServiceService.save(product));
void save_NullName_ShouldThrow() {
product.setName(null);
assertThrows(InvalidDataException.class, () -> productServiceService.save(product));
} }
@Test @Test
@ -81,10 +84,16 @@ class ProductServiceServiceTest {
assertThrows(InvalidDataException.class, () -> productServiceService.save(product)); assertThrows(InvalidDataException.class, () -> productServiceService.save(product));
} }
@Test
void save_NullName_ShouldThrow() {
product.setName(null);
assertThrows(InvalidDataException.class, () -> productServiceService.save(product));
}
@Test @Test
void save_NoIVAType_ShouldThrow() { void save_NoIVAType_ShouldThrow() {
product.setIvaType(null); product.setIvaType(null);
assertThrows(InvalidDataException.class, () -> productServiceService.save(product)); assertThrows(IllegalArgumentException.class, () -> productServiceService.save(product));
} }
@Test @Test

View File

@ -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 (
<div className="content-area">
<div className="card">
<h2>Crear Producto o Servicio</h2>
<form onSubmit={handleSubmit}>
<label>Nombre:</label>
<input
type="text"
className="form-input"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<label>Precio ():</label>
<input
type="number"
className="form-input"
value={price}
onChange={(e) => setPrice(e.target.value)}
/>
<label>Tipo de IVA:</label>
<select
className="form-select"
value={selectedIvaId}
onChange={(e) => setSelectedIvaId(e.target.value)}
>
<option value="">-- Selecciona un IVA --</option>
{ivaTypes.map((iva) => (
<option key={iva.id} value={iva.id}>
{iva.name} ({iva.percentage}%)
</option>
))}
</select>
<label>Tipo:</label>
<select
className="form-select"
value={type}
onChange={(e) => setType(e.target.value)}
>
<option value="PRODUCT">Producto</option>
<option value="SERVICE">Servicio</option>
</select>
<label>Estado:</label>
<select
className="form-select"
value={status}
onChange={(e) => setStatus(e.target.value)}
>
<option value="ACTIVE">Activo</option>
<option value="INACTIVE">Inactivo</option>
</select>
<ErrorMessage message={error} type="error" />
<ErrorMessage message={success} type="success" />
<button className="btn btn-primary" type="submit" style={{ marginTop: "1rem" }}>
💾 Guardar
</button>
</form>
</div>
</div>
);
};
export default ProductForm;

View File

@ -26,6 +26,8 @@ import InvoiceForm from '../forms/InvoiceForm';
import InvoiceList from '../lists/InvoiceList'; import InvoiceList from '../lists/InvoiceList';
import PaymentForm from '../forms/PaymentForm'; import PaymentForm from '../forms/PaymentForm';
import PaymentList from '../lists/PaymentList'; import PaymentList from '../lists/PaymentList';
import ProductForm from '../forms/ProductForm';
import ProductList from '../lists/ProductList';
import '../styles/MainLayout.css'; import '../styles/MainLayout.css';
@ -73,6 +75,9 @@ const MainLayout = () => {
<Route path="/admin/finance/invoices/list" element={<InvoiceList />} /> <Route path="/admin/finance/invoices/list" element={<InvoiceList />} />
<Route path="/admin/finance/payments/create" element={<PaymentForm />} /> <Route path="/admin/finance/payments/create" element={<PaymentForm />} />
<Route path="/admin/finance/payments/list" element={<PaymentList />} /> <Route path="/admin/finance/payments/list" element={<PaymentList />} />
<Route path="/admin/finance/products/create" element={<ProductForm />} />
<Route path="/admin/finance/products/list" element={<ProductList />} />

View File

@ -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 (
<div className="content-area">
<h2>Lista de Productos y Servicios</h2>
<table className="table" style={{ marginTop: "2rem" }}>
<thead>
<tr>
<th>ID</th>
<th>Nombre</th>
<th>Precio</th>
<th>IVA</th>
<th>Tipo</th>
<th>Estado</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
{products.map((p, index) => {
const isEditing = index === editIndex;
return (
<tr key={p.id}>
<td>{p.id}</td>
<td>
{isEditing ? (
<input
type="text"
value={editableProduct.name}
onChange={(e) => handleChange("name", e.target.value)}
/>
) : (
p.name
)}
</td>
<td>
{isEditing ? (
<input
type="number"
value={editableProduct.price}
step="0.01"
onChange={(e) => handleChange("price", e.target.value)}
/>
) : (
`${p.price.toFixed(2)}`
)}
</td>
<td>
{isEditing ? (
<select
value={editableProduct.ivaTypeId || ""}
onChange={(e) => handleChange("ivaTypeId", e.target.value)}
>
<option value="">-- IVA --</option>
{ivaTypes.map((iva) => (
<option key={iva.id} value={iva.id}>
{iva.name} ({iva.percentage}%)
</option>
))}
</select>
) : (
getIVADisplay(p.ivaTypeId)
)}
</td>
<td>
{isEditing ? (
<select
value={editableProduct.type}
onChange={(e) => handleChange("type", e.target.value)}
>
<option value="PRODUCT">Producto</option>
<option value="SERVICE">Servicio</option>
</select>
) : (
p.type
)}
</td>
<td>
{isEditing ? (
<select
value={editableProduct.status}
onChange={(e) => handleChange("status", e.target.value)}
>
<option value="ACTIVE">Activo</option>
<option value="INACTIVE">Inactivo</option>
</select>
) : (
p.status
)}
</td>
<td>
{isEditing ? (
<>
<button className="btn btn-primary btn-sm" onClick={handleSave}>
💾 Guardar
</button>
<button className="btn btn-secondary btn-sm" onClick={cancelEdit}>
Cancelar
</button>
</>
) : (
<>
<button className="btn btn-secondary btn-sm" onClick={() => startEdit(index)}>
Editar
</button>
<button className="btn btn-danger btn-sm" onClick={() => handleDelete(p.id)}>
🗑 Eliminar
</button>
</>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
};
export default ProductList;