Compare commits
2 Commits
0aadef56ed
...
c81a1ea588
| Author | SHA1 | Date |
|---|---|---|
|
|
c81a1ea588 | |
|
|
ab3485984e |
Binary file not shown.
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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...
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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...
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7,4 +7,6 @@ public interface ProductServiceRepository extends JpaRepository<ProductService,
|
|||
boolean existsByName(String name);
|
||||
|
||||
boolean existsByIvaTypeId(Integer ivaTypeId);
|
||||
|
||||
ProductService findByName(String name);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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 />} />
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
Loading…
Reference in New Issue