Compare commits

...

2 Commits

Author SHA1 Message Date
Dennis Eckerskorn c81a1ea588 Added productservices and list to add, update, remove products 2025-05-15 22:33:54 +02:00
Dennis Eckerskorn ab3485984e Fixed issues with invoice, payments creation and also added validation of amount 2025-05-15 20:20:41 +02:00
14 changed files with 498 additions and 41 deletions

View File

@ -1,6 +1,7 @@
package com.denniseckerskorn.controllers.finance_management;
import com.denniseckerskorn.dtos.finance_management_dtos.PaymentDTO;
import com.denniseckerskorn.entities.finance.Invoice;
import com.denniseckerskorn.entities.finance.Payment;
import com.denniseckerskorn.exceptions.DuplicateEntityException;
import com.denniseckerskorn.exceptions.EntityNotFoundException;
@ -29,18 +30,28 @@ public class PaymentController {
@PostMapping("/create")
@Operation(summary = "Create a new payment and mark invoice as PAID")
public ResponseEntity<PaymentDTO> create(@Valid @RequestBody PaymentDTO dto) throws DuplicateEntityException {
Payment saved = paymentService.save(dto.toEntity());
public ResponseEntity<PaymentDTO> create(@Valid @RequestBody PaymentDTO dto)
throws DuplicateEntityException, EntityNotFoundException {
Invoice invoice = paymentService.getInvoiceById(dto.getInvoiceId());
Payment saved = paymentService.save(dto.toEntityWithInvoice(invoice));
return new ResponseEntity<>(new PaymentDTO(saved), HttpStatus.CREATED);
}
@PutMapping("/update")
@Operation(summary = "Update an existing payment and adjust invoice status")
public ResponseEntity<PaymentDTO> update(@Valid @RequestBody PaymentDTO dto) throws EntityNotFoundException, InvalidDataException {
Payment updated = paymentService.update(dto.toEntity());
public ResponseEntity<PaymentDTO> update(@Valid @RequestBody PaymentDTO dto)
throws EntityNotFoundException, InvalidDataException {
Invoice invoice = paymentService.getInvoiceById(dto.getInvoiceId());
Payment updated = paymentService.update(dto.toEntityWithInvoice(invoice));
return new ResponseEntity<>(new PaymentDTO(updated), HttpStatus.OK);
}
@GetMapping("/getById/{id}")
@Operation(summary = "Get payment by ID")
public ResponseEntity<PaymentDTO> getById(@PathVariable Integer id) throws EntityNotFoundException, InvalidDataException {

View File

@ -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<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);
}
@PutMapping("/update")
@Operation(summary = "Update an existing product/service")
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);
}
@GetMapping("/getById/{id}")
@Operation(summary = "Get product/service by ID")
public ResponseEntity<ProductServiceDTO> getById(@PathVariable Integer id) throws EntityNotFoundException, InvalidDataException {

View File

@ -1,5 +1,6 @@
package com.denniseckerskorn.dtos.finance_management_dtos;
import com.denniseckerskorn.entities.finance.Invoice;
import com.denniseckerskorn.entities.finance.Payment;
import com.denniseckerskorn.enums.PaymentMethodValues;
import com.denniseckerskorn.enums.StatusValues;
@ -27,7 +28,8 @@ public class PaymentDTO {
@NotNull
private StatusValues status;
public PaymentDTO() {}
public PaymentDTO() {
}
public PaymentDTO(Payment entity) {
this.id = entity.getId();
@ -44,6 +46,9 @@ public class PaymentDTO {
public Payment toEntity() {
Payment payment = new Payment();
if (this.id != null) {
payment.setId(this.id);
}
payment.setId(this.id);
payment.setPaymentDate(this.paymentDate);
payment.setAmount(this.amount);
@ -52,6 +57,17 @@ public class PaymentDTO {
return payment;
}
public Payment toEntityWithInvoice(Invoice invoice) {
Payment payment = new Payment();
payment.setPaymentDate(this.paymentDate);
payment.setAmount(this.amount);
payment.setPaymentMethod(this.paymentMethod);
payment.setStatus(this.status);
payment.setInvoice(invoice);
return payment;
}
// Getters y setters...

View File

@ -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...

View File

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

View File

@ -47,15 +47,20 @@ public class PaymentService extends AbstractService<Payment, Integer> {
}
payment.setInvoice(invoice);
invoice.setPayment(payment);
Payment savedPayment = super.save(payment);
invoice.setPayment(savedPayment);
invoice.setStatus(StatusValues.PAID);
invoiceService.update(invoice);
return super.save(payment);
return savedPayment;
}
@Override
@Transactional
public Payment update(Payment entity) throws EntityNotFoundException, InvalidDataException {
logger.info("Updating payment: {}", entity);
validate(entity);
@ -97,6 +102,7 @@ public class PaymentService extends AbstractService<Payment, Integer> {
}
}
@Transactional
private void updateInvoiceStatus(Payment payment) {
Invoice invoice = payment.getInvoice();
invoice.setStatus(StatusValues.PAID);
@ -116,4 +122,9 @@ public class PaymentService extends AbstractService<Payment, Integer> {
return paymentRepository.findByInvoice_User_Id(userId);
}
public Invoice getInvoiceById(Integer id) {
return invoiceService.findById(id);
}
}

View File

@ -1,11 +1,13 @@
package com.denniseckerskorn.services.finance_services;
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.repositories.finance_repositories.ProductServiceRepository;
import com.denniseckerskorn.services.AbstractService;
import jakarta.transaction.Transactional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 final ProductServiceRepository productServiceRepository;
private final IVATypeService ivaTypeService;
public ProductServiceService(ProductServiceRepository productServiceRepository) {
public ProductServiceService(ProductServiceRepository productServiceRepository, IVATypeService ivaTypeService) {
super(productServiceRepository);
this.productServiceRepository = productServiceRepository;
this.ivaTypeService = ivaTypeService;
}
@Override
@Transactional
public ProductService save(ProductService entity) throws IllegalArgumentException, DuplicateEntityException {
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);
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");
}
return super.save(entity);
}
@Override
@Transactional
public ProductService update(ProductService entity) throws IllegalArgumentException, InvalidDataException, EntityNotFoundException {
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);
return super.update(entity);
}
@Override
public List<ProductService> findAll() {
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) {
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) {
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.ProductService;
@ -6,7 +6,6 @@ import com.denniseckerskorn.enums.StatusValues;
import com.denniseckerskorn.exceptions.DuplicateEntityException;
import com.denniseckerskorn.exceptions.InvalidDataException;
import com.denniseckerskorn.repositories.finance_repositories.ProductServiceRepository;
import com.denniseckerskorn.services.finance_services.ProductServiceService;
import jakarta.persistence.EntityManager;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -23,6 +22,9 @@ class ProductServiceServiceTest {
@Mock
private ProductServiceRepository productServiceRepository;
@Mock
private IVATypeService ivaTypeService;
@Mock
private EntityManager entityManager;
@ -38,17 +40,18 @@ class ProductServiceServiceTest {
ivaType = new IVAType();
ivaType.setId(1);
ivaType.setPercentage(new BigDecimal("21.00"));
ivaType.setPercentage(BigDecimal.valueOf(21));
ivaType.setDescription("IVA General");
product = new ProductService();
product.setId(10);
product.setName("Clase Avanzada");
product.setType("Servicio");
product.setPrice(new BigDecimal("30.00"));
product.setIvaType(ivaType);
product.setId(1);
product.setName("Producto Test");
product.setPrice(BigDecimal.valueOf(100));
product.setType("PRODUCT");
product.setStatus(StatusValues.ACTIVE);
product.setIvaType(ivaType);
// Set EntityManager in abstract service
Field emField = ProductServiceService.class.getSuperclass().getDeclaredField("entityManager");
emField.setAccessible(true);
emField.set(productServiceService, entityManager);
@ -56,23 +59,23 @@ class ProductServiceServiceTest {
@Test
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.save(any())).thenReturn(product);
ProductService saved = productServiceService.save(product);
assertEquals("Clase Avanzada", saved.getName());
assertEquals(product.getName(), saved.getName());
assertEquals(product.getPrice(), saved.getPrice());
}
@Test
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);
assertThrows(DuplicateEntityException.class, () -> 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

View File

@ -20,7 +20,9 @@ const PaymentForm = () => {
const fetchInvoices = async (userId) => {
try {
const res = await api.get(`/invoices/getAllInvoicesByUserId/${userId}`);
const notPaid = res.data.filter((invoice) => invoice.status === "NOT_PAID");
const notPaid = res.data.filter(
(invoice) => invoice.status === "NOT_PAID"
);
setInvoices(notPaid);
} catch (err) {
console.error(err);
@ -48,6 +50,19 @@ const PaymentForm = () => {
return;
}
const selectedInvoice = invoices.find(
(inv) => inv.id === parseInt(selectedInvoiceId)
);
if (!selectedInvoice) {
setError("Factura no encontrada.");
return;
}
if (parseFloat(amount) < selectedInvoice.total) {
setError("El importe pagado no puede ser menor al total de la factura.");
return;
}
try {
await api.post("/payments/create", {
invoiceId: parseInt(selectedInvoiceId),
@ -61,7 +76,7 @@ const PaymentForm = () => {
setSelectedInvoiceId("");
setAmount("");
setPaymentMethod("CASH");
fetchInvoices(selectedUserId); // actualizar facturas pendientes
fetchInvoices(selectedUserId);
} catch (err) {
console.error(err);
setError("❌ Error al registrar el pago.");
@ -71,10 +86,14 @@ const PaymentForm = () => {
return (
<div className="content-area">
<div className="card">
<h2>Registrar Pago</h2>
<h2>Registrar Pago de una Factura</h2>
<label>Seleccionar estudiante:</label>
<select className="form-select" value={selectedUserId} onChange={handleStudentChange}>
<select
className="form-select"
value={selectedUserId}
onChange={handleStudentChange}
>
<option value="">-- Selecciona un estudiante --</option>
{students.map((s) =>
s.user ? (
@ -96,13 +115,20 @@ const PaymentForm = () => {
<option value="">-- Selecciona una factura --</option>
{invoices.map((inv) => (
<option key={inv.id} value={inv.id}>
#{inv.id} - {new Date(inv.date).toLocaleDateString()} - Total: {inv.total.toFixed(2)}
#{inv.id} - {new Date(inv.date).toLocaleDateString()} - Total:{" "}
{inv.total.toFixed(2)}
</option>
))}
</select>
</>
)}
{invoices.length === 0 && selectedUserId && (
<p style={{ marginTop: "1rem", color: "gray" }}>
No hay facturas pendientes de pago para este estudiante.
</p>
)}
{selectedInvoiceId && (
<>
<label>Importe pagado ():</label>
@ -121,15 +147,18 @@ const PaymentForm = () => {
onChange={(e) => setPaymentMethod(e.target.value)}
>
<option value="CASH">Efectivo</option>
<option value="CARD">Tarjeta</option>
<option value="TRANSFER">Transferencia</option>
<option value="BIZUM">Bizum</option>
<option value="CREDIT_CARD">Tarjeta</option>
<option value="BANK_TRANSFER">Transferencia</option>
</select>
<ErrorMessage message={error} type="error" />
<ErrorMessage message={success} type="success" />
<button className="btn btn-primary" style={{ marginTop: "1rem" }} onClick={handleSubmit}>
<button
className="btn btn-primary"
style={{ marginTop: "1rem" }}
onClick={handleSubmit}
>
💳 Confirmar Pago
</button>
</>

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 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 = () => {
<Route path="/admin/finance/invoices/list" element={<InvoiceList />} />
<Route path="/admin/finance/payments/create" element={<PaymentForm />} />
<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

@ -48,7 +48,7 @@ const PaymentList = () => {
return (
<div className="content-area">
<div className="card">
<h2>Listado de Pagos</h2>
<h2>Listado de Facturas Pagadas por Estudiante</h2>
<label>Seleccionar estudiante:</label>
<select className="form-select" value={selectedUserId} onChange={handleStudentChange}>

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;