Compare commits
3 Commits
6177976a6f
...
0aadef56ed
| Author | SHA1 | Date |
|---|---|---|
|
|
0aadef56ed | |
|
|
9d52a212fa | |
|
|
3d8963e197 |
Binary file not shown.
|
|
@ -94,9 +94,18 @@
|
||||||
<version>8.0.1.Final</version>
|
<version>8.0.1.Final</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.itextpdf</groupId>
|
||||||
|
<artifactId>itext7-core</artifactId>
|
||||||
|
<version>8.0.3</version>
|
||||||
|
<type>pom</type>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
<maven.compiler.target>21</maven.compiler.target>
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,26 @@
|
||||||
package com.denniseckerskorn.controllers.finance_management;
|
package com.denniseckerskorn.controllers.finance_management;
|
||||||
|
|
||||||
|
|
||||||
|
import com.denniseckerskorn.dtos.finance_management_dtos.CreateInvoiceLineDTO;
|
||||||
|
import com.denniseckerskorn.dtos.finance_management_dtos.CreateInvoiceRequestDTO;
|
||||||
import com.denniseckerskorn.dtos.finance_management_dtos.InvoiceDTO;
|
import com.denniseckerskorn.dtos.finance_management_dtos.InvoiceDTO;
|
||||||
import com.denniseckerskorn.entities.finance.Invoice;
|
import com.denniseckerskorn.entities.finance.Invoice;
|
||||||
import com.denniseckerskorn.entities.finance.InvoiceLine;
|
import com.denniseckerskorn.entities.finance.InvoiceLine;
|
||||||
|
import com.denniseckerskorn.entities.finance.ProductService;
|
||||||
|
import com.denniseckerskorn.entities.user_managment.users.User;
|
||||||
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_service.InvoicePdfGenerator;
|
||||||
import com.denniseckerskorn.services.finance_services.InvoiceService;
|
import com.denniseckerskorn.services.finance_services.InvoiceService;
|
||||||
|
import com.denniseckerskorn.services.finance_services.ProductServiceService;
|
||||||
|
import com.denniseckerskorn.services.user_managment_services.UserService;
|
||||||
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;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
|
@ -24,11 +33,56 @@ import java.util.stream.Collectors;
|
||||||
public class InvoiceController {
|
public class InvoiceController {
|
||||||
|
|
||||||
private final InvoiceService invoiceService;
|
private final InvoiceService invoiceService;
|
||||||
|
private final UserService userService;
|
||||||
|
private final ProductServiceService productServiceService;
|
||||||
|
private final InvoicePdfGenerator invoicePdfGenerator;
|
||||||
|
|
||||||
public InvoiceController(InvoiceService invoiceService) {
|
public InvoiceController(InvoiceService invoiceService, UserService userService, ProductServiceService productServiceService, InvoicePdfGenerator invoicePdfGenerator) {
|
||||||
this.invoiceService = invoiceService;
|
this.invoiceService = invoiceService;
|
||||||
|
this.userService = userService;
|
||||||
|
this.productServiceService = productServiceService;
|
||||||
|
this.invoicePdfGenerator = invoicePdfGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/createInvoiceWithLines")
|
||||||
|
@Operation(summary = "Create a new invoice with lines")
|
||||||
|
public ResponseEntity<InvoiceDTO> createInvoiceWithLines(@Valid @RequestBody CreateInvoiceRequestDTO dto) throws EntityNotFoundException, InvalidDataException {
|
||||||
|
User user = userService.findById(dto.getUserId());
|
||||||
|
if (user == null) {
|
||||||
|
throw new EntityNotFoundException("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
Invoice invoice = dto.toEntity(user);
|
||||||
|
invoiceService.save(invoice);
|
||||||
|
|
||||||
|
if (dto.getLines() != null && !dto.getLines().isEmpty()) {
|
||||||
|
for (CreateInvoiceLineDTO lineDTO : dto.getLines()) {
|
||||||
|
ProductService product = productServiceService.findById(lineDTO.getProductServiceId());
|
||||||
|
if (product == null) {
|
||||||
|
throw new EntityNotFoundException("Product/Service not found");
|
||||||
|
}
|
||||||
|
InvoiceLine line = lineDTO.toEntity(invoice, product);
|
||||||
|
invoiceService.addLineToInvoiceById(invoice.getId(), line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
invoiceService.recalculateTotal(invoice);
|
||||||
|
return new ResponseEntity<>(new InvoiceDTO(invoice), HttpStatus.CREATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/generatePDFById/{id}")
|
||||||
|
@Operation(summary = "Generar y descargar factura en PDF")
|
||||||
|
public ResponseEntity<byte[]> downloadInvoicePdf(@PathVariable Integer id) throws EntityNotFoundException {
|
||||||
|
Invoice invoice = invoiceService.findById(id);
|
||||||
|
|
||||||
|
byte[] pdf = invoicePdfGenerator.generateInvoicePdf(invoice);
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=factura_" + id + ".pdf")
|
||||||
|
.contentType(MediaType.APPLICATION_PDF)
|
||||||
|
.body(pdf);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@PostMapping("/create")
|
@PostMapping("/create")
|
||||||
@Operation(summary = "Create a new invoice")
|
@Operation(summary = "Create a new invoice")
|
||||||
public ResponseEntity<InvoiceDTO> createInvoice(@Valid @RequestBody InvoiceDTO dto) throws DuplicateEntityException {
|
public ResponseEntity<InvoiceDTO> createInvoice(@Valid @RequestBody InvoiceDTO dto) throws DuplicateEntityException {
|
||||||
|
|
|
||||||
|
|
@ -62,4 +62,13 @@ public class PaymentController {
|
||||||
paymentService.removePayment(id);
|
paymentService.removePayment(id);
|
||||||
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
|
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/getAllByUserId/{userId}")
|
||||||
|
@Operation(summary = "Get all payments by user ID")
|
||||||
|
public ResponseEntity<List<PaymentDTO>> getAllByUserId(@PathVariable Integer userId) {
|
||||||
|
List<PaymentDTO> dtos = paymentService.findAllByUserId(userId)
|
||||||
|
.stream().map(PaymentDTO::new).collect(Collectors.toList());
|
||||||
|
return new ResponseEntity<>(dtos, HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
package com.denniseckerskorn.dtos.finance_management_dtos;
|
||||||
|
|
||||||
|
import com.denniseckerskorn.entities.finance.Invoice;
|
||||||
|
import com.denniseckerskorn.entities.finance.InvoiceLine;
|
||||||
|
import com.denniseckerskorn.entities.finance.ProductService;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
public class CreateInvoiceLineDTO {
|
||||||
|
|
||||||
|
private Integer productServiceId;
|
||||||
|
private int quantity;
|
||||||
|
private BigDecimal unitPrice;
|
||||||
|
|
||||||
|
public CreateInvoiceLineDTO() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getProductServiceId() {
|
||||||
|
return productServiceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProductServiceId(Integer productServiceId) {
|
||||||
|
this.productServiceId = productServiceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getQuantity() {
|
||||||
|
return quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setQuantity(int quantity) {
|
||||||
|
this.quantity = quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getUnitPrice() {
|
||||||
|
return unitPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUnitPrice(BigDecimal unitPrice) {
|
||||||
|
this.unitPrice = unitPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convierte a entidad con parámetros necesarios
|
||||||
|
public InvoiceLine toEntity(Invoice invoice, ProductService productService) {
|
||||||
|
InvoiceLine entity = new InvoiceLine();
|
||||||
|
entity.setInvoice(invoice);
|
||||||
|
entity.setProductService(productService);
|
||||||
|
entity.setQuantity(this.quantity);
|
||||||
|
entity.setUnitPrice(this.unitPrice);
|
||||||
|
entity.setSubtotal(this.unitPrice.multiply(BigDecimal.valueOf(this.quantity)));
|
||||||
|
entity.setDescription(productService.getName()); // opcional
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convierte desde una entidad (opcional)
|
||||||
|
public static CreateInvoiceLineDTO fromEntity(InvoiceLine entity) {
|
||||||
|
CreateInvoiceLineDTO dto = new CreateInvoiceLineDTO();
|
||||||
|
dto.setProductServiceId(entity.getProductService() != null ? entity.getProductService().getId() : null);
|
||||||
|
dto.setQuantity(entity.getQuantity());
|
||||||
|
dto.setUnitPrice(entity.getUnitPrice());
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
package com.denniseckerskorn.dtos.finance_management_dtos;
|
||||||
|
|
||||||
|
import com.denniseckerskorn.entities.finance.Invoice;
|
||||||
|
import com.denniseckerskorn.entities.user_managment.users.User;
|
||||||
|
import com.denniseckerskorn.enums.StatusValues;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class CreateInvoiceRequestDTO {
|
||||||
|
|
||||||
|
private Integer userId;
|
||||||
|
private LocalDateTime date;
|
||||||
|
private StatusValues status;
|
||||||
|
private BigDecimal total;
|
||||||
|
private Set<CreateInvoiceLineDTO> lines;
|
||||||
|
|
||||||
|
public CreateInvoiceRequestDTO() {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters y setters
|
||||||
|
|
||||||
|
public Integer getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserId(Integer userId) {
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getDate() {
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDate(LocalDateTime date) {
|
||||||
|
this.date = date;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StatusValues getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(StatusValues status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getTotal() {
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTotal(BigDecimal total) {
|
||||||
|
this.total = total;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<CreateInvoiceLineDTO> getLines() {
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLines(Set<CreateInvoiceLineDTO> lines) {
|
||||||
|
this.lines = lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convierte el DTO a entidad base (sin líneas, requiere setUser)
|
||||||
|
public Invoice toEntity(User user) {
|
||||||
|
Invoice invoice = new Invoice();
|
||||||
|
invoice.setUser(user);
|
||||||
|
invoice.setDate(this.date);
|
||||||
|
invoice.setStatus(this.status);
|
||||||
|
invoice.setTotal(this.total);
|
||||||
|
return invoice;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crea el DTO desde una entidad (opcional)
|
||||||
|
public static CreateInvoiceRequestDTO fromEntity(Invoice invoice) {
|
||||||
|
CreateInvoiceRequestDTO dto = new CreateInvoiceRequestDTO();
|
||||||
|
dto.setUserId(invoice.getUser() != null ? invoice.getUser().getId().intValue() : null);
|
||||||
|
dto.setDate(invoice.getDate());
|
||||||
|
dto.setStatus(invoice.getStatus());
|
||||||
|
dto.setTotal(invoice.getTotal());
|
||||||
|
dto.setLines(invoice.getInvoiceLines() != null ?
|
||||||
|
invoice.getInvoiceLines().stream()
|
||||||
|
.map(CreateInvoiceLineDTO::fromEntity)
|
||||||
|
.collect(Collectors.toSet()) :
|
||||||
|
null);
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.denniseckerskorn.dtos.finance_management_dtos;
|
package com.denniseckerskorn.dtos.finance_management_dtos;
|
||||||
|
|
||||||
|
import com.denniseckerskorn.dtos.user_managment_dtos.UserDTO;
|
||||||
import com.denniseckerskorn.entities.finance.Invoice;
|
import com.denniseckerskorn.entities.finance.Invoice;
|
||||||
import com.denniseckerskorn.enums.StatusValues;
|
import com.denniseckerskorn.enums.StatusValues;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
@ -16,6 +17,9 @@ public class InvoiceDTO {
|
||||||
@NotNull
|
@NotNull
|
||||||
private Integer userId;
|
private Integer userId;
|
||||||
|
|
||||||
|
private UserDTO user;
|
||||||
|
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
private LocalDateTime date;
|
private LocalDateTime date;
|
||||||
|
|
||||||
|
|
@ -35,6 +39,7 @@ public class InvoiceDTO {
|
||||||
public InvoiceDTO(Invoice invoice) {
|
public InvoiceDTO(Invoice invoice) {
|
||||||
this.id = invoice.getId();
|
this.id = invoice.getId();
|
||||||
this.userId = invoice.getUser() != null ? invoice.getUser().getId() : null;
|
this.userId = invoice.getUser() != null ? invoice.getUser().getId() : null;
|
||||||
|
this.user = invoice.getUser() != null ? new UserDTO(invoice.getUser()) : null;
|
||||||
this.date = invoice.getDate();
|
this.date = invoice.getDate();
|
||||||
this.total = invoice.getTotal();
|
this.total = invoice.getTotal();
|
||||||
this.status = invoice.getStatus();
|
this.status = invoice.getStatus();
|
||||||
|
|
@ -77,6 +82,14 @@ public class InvoiceDTO {
|
||||||
this.userId = userId;
|
this.userId = userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UserDTO getUser() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUser(UserDTO user) {
|
||||||
|
this.user = user;
|
||||||
|
}
|
||||||
|
|
||||||
public LocalDateTime getDate() {
|
public LocalDateTime getDate() {
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,19 @@ public class UserDTO {
|
||||||
this.status = status;
|
this.status = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UserDTO(User user) {
|
||||||
|
this.id = user.getId();
|
||||||
|
this.name = user.getName();
|
||||||
|
this.surname = user.getSurname();
|
||||||
|
this.email = user.getEmail();
|
||||||
|
this.phoneNumber = user.getPhoneNumber();
|
||||||
|
this.address = user.getAddress();
|
||||||
|
this.registerDate = user.getRegisterDate();
|
||||||
|
this.roleName = user.getRole() != null ? user.getRole().getName() : null;
|
||||||
|
this.status = user.getStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Getters y Setters
|
// Getters y Setters
|
||||||
public Integer getId() {
|
public Integer getId() {
|
||||||
return id;
|
return id;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
package com.denniseckerskorn.services.finance_service;
|
||||||
|
|
||||||
|
import com.denniseckerskorn.entities.finance.Invoice;
|
||||||
|
import com.itextpdf.kernel.pdf.PdfWriter;
|
||||||
|
import com.itextpdf.kernel.pdf.PdfDocument;
|
||||||
|
import com.itextpdf.layout.Document;
|
||||||
|
import com.itextpdf.layout.element.Paragraph;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class InvoicePdfGenerator {
|
||||||
|
|
||||||
|
public byte[] generateInvoicePdf(Invoice invoice) {
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
|
||||||
|
PdfWriter writer = new PdfWriter(out);
|
||||||
|
PdfDocument pdf = new PdfDocument(writer);
|
||||||
|
Document doc = new Document(pdf);
|
||||||
|
|
||||||
|
doc.add(new Paragraph("Factura #" + invoice.getId()));
|
||||||
|
doc.add(new Paragraph("Fecha: " + invoice.getDate()));
|
||||||
|
doc.add(new Paragraph("Cliente: " + invoice.getUser().getName() + " " + invoice.getUser().getSurname()));
|
||||||
|
doc.add(new Paragraph("Estado: " + invoice.getStatus()));
|
||||||
|
|
||||||
|
doc.add(new Paragraph(" "));
|
||||||
|
doc.add(new Paragraph("Detalle de productos:"));
|
||||||
|
|
||||||
|
invoice.getInvoiceLines().forEach(line -> {
|
||||||
|
String product = line.getProductService().getName();
|
||||||
|
int qty = line.getQuantity();
|
||||||
|
var price = line.getUnitPrice();
|
||||||
|
var total = line.getSubtotal();
|
||||||
|
doc.add(new Paragraph("- " + product + " x" + qty + " - " + price + " € = " + total + " €"));
|
||||||
|
});
|
||||||
|
|
||||||
|
doc.add(new Paragraph(" "));
|
||||||
|
doc.add(new Paragraph("TOTAL: " + invoice.getTotal() + " €"));
|
||||||
|
|
||||||
|
doc.close();
|
||||||
|
return out.toByteArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
package com.denniseckerskorn.repositories.finance_repositories;
|
package com.denniseckerskorn.repositories.finance_repositories;
|
||||||
|
|
||||||
|
import com.denniseckerskorn.entities.finance.Invoice;
|
||||||
import com.denniseckerskorn.entities.finance.Payment;
|
import com.denniseckerskorn.entities.finance.Payment;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public interface PaymentRepository extends JpaRepository<Payment, Integer> {
|
public interface PaymentRepository extends JpaRepository<Payment, Integer> {
|
||||||
boolean existsByInvoiceId(Integer invoiceId);
|
boolean existsByInvoiceId(Integer invoiceId);
|
||||||
|
|
||||||
|
List<Payment> findByInvoice_User_Id(Integer userId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import com.denniseckerskorn.exceptions.EntityNotFoundException;
|
||||||
import com.denniseckerskorn.exceptions.InvalidDataException;
|
import com.denniseckerskorn.exceptions.InvalidDataException;
|
||||||
import com.denniseckerskorn.repositories.finance_repositories.PaymentRepository;
|
import com.denniseckerskorn.repositories.finance_repositories.PaymentRepository;
|
||||||
import com.denniseckerskorn.services.AbstractService;
|
import com.denniseckerskorn.services.AbstractService;
|
||||||
import com.denniseckerskorn.services.finance_services.InvoiceService;
|
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
@ -33,16 +32,29 @@ public class PaymentService extends AbstractService<Payment, Integer> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public Payment save(Payment entity) throws DuplicateEntityException {
|
public Payment save(Payment payment) throws DuplicateEntityException {
|
||||||
logger.info("Saving payment: {}", entity);
|
logger.info("Saving payment: {}", payment);
|
||||||
validate(entity);
|
validate(payment);
|
||||||
if (paymentRepository.existsByInvoiceId(entity.getInvoice().getId())) {
|
|
||||||
|
Integer invoiceId = payment.getInvoice().getId();
|
||||||
|
if (paymentRepository.existsByInvoiceId(invoiceId)) {
|
||||||
throw new DuplicateEntityException("A payment already exists for this invoice");
|
throw new DuplicateEntityException("A payment already exists for this invoice");
|
||||||
}
|
}
|
||||||
updateInvoiceStatus(entity);
|
|
||||||
return super.save(entity);
|
Invoice invoice = invoiceService.findById(invoiceId);
|
||||||
|
if (invoice == null) {
|
||||||
|
throw new EntityNotFoundException("Invoice not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
payment.setInvoice(invoice);
|
||||||
|
invoice.setPayment(payment);
|
||||||
|
|
||||||
|
invoice.setStatus(StatusValues.PAID);
|
||||||
|
invoiceService.update(invoice);
|
||||||
|
return super.save(payment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Payment update(Payment entity) throws EntityNotFoundException, InvalidDataException {
|
public Payment update(Payment entity) throws EntityNotFoundException, InvalidDataException {
|
||||||
logger.info("Updating payment: {}", entity);
|
logger.info("Updating payment: {}", entity);
|
||||||
|
|
@ -100,4 +112,8 @@ public class PaymentService extends AbstractService<Payment, Integer> {
|
||||||
invoiceService.update(invoice);
|
invoiceService.update(invoice);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Payment> findAllByUserId(Integer userId) {
|
||||||
|
return paymentRepository.findByInvoice_User_Id(userId);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,9 +29,6 @@ class PaymentServiceTest {
|
||||||
@Mock
|
@Mock
|
||||||
private InvoiceService invoiceService;
|
private InvoiceService invoiceService;
|
||||||
|
|
||||||
@Mock
|
|
||||||
private EntityManager entityManager;
|
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private PaymentService paymentService;
|
private PaymentService paymentService;
|
||||||
|
|
||||||
|
|
@ -44,59 +41,64 @@ class PaymentServiceTest {
|
||||||
|
|
||||||
invoice = new Invoice();
|
invoice = new Invoice();
|
||||||
invoice.setId(1);
|
invoice.setId(1);
|
||||||
invoice.setTotal(new BigDecimal("59.99"));
|
invoice.setTotal(new BigDecimal("140.00"));
|
||||||
invoice.setStatus(StatusValues.NOT_PAID);
|
invoice.setStatus(StatusValues.NOT_PAID);
|
||||||
|
|
||||||
payment = new Payment();
|
payment = new Payment();
|
||||||
payment.setId(101);
|
payment.setId(101);
|
||||||
payment.setInvoice(invoice);
|
payment.setInvoice(invoice);
|
||||||
payment.setAmount(new BigDecimal("59.99"));
|
payment.setAmount(new BigDecimal("140.00"));
|
||||||
payment.setPaymentDate(LocalDateTime.now().minusDays(1));
|
payment.setPaymentDate(LocalDateTime.now().minusDays(1));
|
||||||
payment.setPaymentMethod(PaymentMethodValues.CREDIT_CARD);
|
payment.setPaymentMethod(PaymentMethodValues.CASH);
|
||||||
payment.setStatus(StatusValues.PAID);
|
payment.setStatus(StatusValues.PAID);
|
||||||
|
|
||||||
Field emField = PaymentService.class.getSuperclass().getDeclaredField("entityManager");
|
|
||||||
emField.setAccessible(true);
|
|
||||||
emField.set(paymentService, entityManager);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void save_ShouldUpdateInvoiceStatusAndSavePayment() {
|
void save_ShouldUpdateInvoiceStatusAndSavePayment() {
|
||||||
when(paymentRepository.existsByInvoiceId(1)).thenReturn(false);
|
when(paymentRepository.existsByInvoiceId(1)).thenReturn(false);
|
||||||
|
when(invoiceService.findById(1)).thenReturn(invoice);
|
||||||
when(paymentRepository.save(any())).thenReturn(payment);
|
when(paymentRepository.save(any())).thenReturn(payment);
|
||||||
|
|
||||||
Payment saved = paymentService.save(payment);
|
Payment saved = paymentService.save(payment);
|
||||||
assertEquals(StatusValues.PAID, payment.getInvoice().getStatus());
|
|
||||||
|
assertEquals(StatusValues.PAID, saved.getInvoice().getStatus());
|
||||||
verify(invoiceService).update(invoice);
|
verify(invoiceService).update(invoice);
|
||||||
|
verify(paymentRepository).save(payment);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void save_DuplicatePayment_ShouldThrow() {
|
void save_WhenDuplicate_ShouldThrow() {
|
||||||
when(paymentRepository.existsByInvoiceId(1)).thenReturn(true);
|
when(paymentRepository.existsByInvoiceId(1)).thenReturn(true);
|
||||||
|
|
||||||
assertThrows(DuplicateEntityException.class, () -> paymentService.save(payment));
|
assertThrows(DuplicateEntityException.class, () -> paymentService.save(payment));
|
||||||
|
verify(invoiceService, never()).update(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void save_InvalidAmount_ShouldThrow() {
|
void save_WhenInvoiceNull_ShouldThrow() {
|
||||||
payment.setAmount(BigDecimal.ZERO);
|
|
||||||
assertThrows(InvalidDataException.class, () -> paymentService.save(payment));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void save_InvalidPaymentDate_ShouldThrow() {
|
|
||||||
payment.setPaymentDate(LocalDateTime.now().plusDays(1));
|
|
||||||
assertThrows(InvalidDataException.class, () -> paymentService.save(payment));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void save_InvalidMethod_ShouldThrow() {
|
|
||||||
payment.setPaymentMethod(null);
|
|
||||||
assertThrows(InvalidDataException.class, () -> paymentService.save(payment));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void save_InvoiceNull_ShouldThrow() {
|
|
||||||
payment.setInvoice(null);
|
payment.setInvoice(null);
|
||||||
|
|
||||||
|
assertThrows(InvalidDataException.class, () -> paymentService.save(payment));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void save_WhenInvalidAmount_ShouldThrow() {
|
||||||
|
payment.setAmount(BigDecimal.ZERO);
|
||||||
|
|
||||||
|
assertThrows(InvalidDataException.class, () -> paymentService.save(payment));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void save_WhenFutureDate_ShouldThrow() {
|
||||||
|
payment.setPaymentDate(LocalDateTime.now().plusDays(1));
|
||||||
|
|
||||||
|
assertThrows(InvalidDataException.class, () -> paymentService.save(payment));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void save_WhenMissingMethod_ShouldThrow() {
|
||||||
|
payment.setPaymentMethod(null);
|
||||||
|
|
||||||
assertThrows(InvalidDataException.class, () -> paymentService.save(payment));
|
assertThrows(InvalidDataException.class, () -> paymentService.save(payment));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import InvoiceLineItem from "./InvoiceLineItem";
|
import InvoiceLineItem from "./InvoiceLineItem";
|
||||||
|
import ErrorMessage from "../common/ErrorMessage";
|
||||||
import api from "../../api/axiosConfig";
|
import api from "../../api/axiosConfig";
|
||||||
|
|
||||||
const InvoiceForm = () => {
|
const InvoiceForm = () => {
|
||||||
|
|
@ -8,10 +9,11 @@ const InvoiceForm = () => {
|
||||||
const [date, setDate] = useState(new Date().toISOString().slice(0, 16));
|
const [date, setDate] = useState(new Date().toISOString().slice(0, 16));
|
||||||
const [lines, setLines] = useState([]);
|
const [lines, setLines] = useState([]);
|
||||||
const [products, setProducts] = useState([]);
|
const [products, setProducts] = useState([]);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [createdInvoiceId, setCreatedInvoiceId] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.get("/students/getAll").then((res) => setStudents(res.data));
|
api.get("/students/getAll").then((res) => setStudents(res.data));
|
||||||
|
|
||||||
api.get("/products-services/getAll").then((res) => setProducts(res.data));
|
api.get("/products-services/getAll").then((res) => setProducts(res.data));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -29,96 +31,143 @@ const InvoiceForm = () => {
|
||||||
setLines(lines.filter((_, i) => i !== index));
|
setLines(lines.filter((_, i) => i !== index));
|
||||||
};
|
};
|
||||||
|
|
||||||
const createInvoice = async () => {
|
const calculateTotal = () => {
|
||||||
if (!userId || lines.length === 0) {
|
return lines.reduce((acc, line) => {
|
||||||
alert("Selecciona un estudiante y al menos un producto.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const invoiceRes = await api.post("/invoices/create", {
|
|
||||||
user: { id: parseInt(userId) },
|
|
||||||
date,
|
|
||||||
total: 0,
|
|
||||||
status: "NOT_PAID",
|
|
||||||
invoiceLineIds: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const invoice = invoiceRes.data;
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
const product = products.find(
|
const product = products.find(
|
||||||
(p) => p.id === parseInt(line.productServiceId)
|
(p) => p.id === parseInt(line.productServiceId)
|
||||||
);
|
);
|
||||||
const subtotal = product.price * line.quantity;
|
if (!product) return acc;
|
||||||
|
return acc + product.price * line.quantity;
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
await api.post(`/invoices/addLinesByInvoiceId/${invoice.id}`, {
|
const createInvoice = async () => {
|
||||||
productServiceId: parseInt(line.productServiceId),
|
setError("");
|
||||||
quantity: parseInt(line.quantity),
|
setCreatedInvoiceId(null);
|
||||||
unitPrice: product.price,
|
|
||||||
subtotal,
|
if (!userId || lines.length === 0) {
|
||||||
description: product.description,
|
setError("Selecciona un estudiante y al menos un producto.");
|
||||||
});
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await api.put(`/invoices/recalculateTotalOfInvoiceById/${invoice.id}`);
|
try {
|
||||||
|
const preparedLines = lines.map((line) => {
|
||||||
|
const product = products.find(
|
||||||
|
(p) => p.id === parseInt(line.productServiceId)
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
productServiceId: parseInt(line.productServiceId),
|
||||||
|
quantity: parseInt(line.quantity),
|
||||||
|
unitPrice: product.price,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
alert("Factura creada correctamente.");
|
const payload = {
|
||||||
setUserId("");
|
userId: parseInt(userId),
|
||||||
setLines([]);
|
date: new Date(date).toISOString(),
|
||||||
|
status: "NOT_PAID",
|
||||||
|
total: calculateTotal(),
|
||||||
|
lines: preparedLines,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await api.post("/invoices/createInvoiceWithLines", payload);
|
||||||
|
const invoiceId = res.data.id;
|
||||||
|
|
||||||
|
setCreatedInvoiceId(invoiceId);
|
||||||
|
alert("✅ Factura creada correctamente.");
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setError("❌ Error al crear la factura: " + (err.response?.data?.message || err.message));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadPdf = async () => {
|
||||||
|
try {
|
||||||
|
const response = await api.get(`/invoices/generatePDFById/${createdInvoiceId}`, {
|
||||||
|
responseType: "blob", // para recibir el PDF correctamente
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = url;
|
||||||
|
link.setAttribute("download", `factura_${createdInvoiceId}.pdf`);
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setError("❌ Error al descargar el PDF.");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="content-area">
|
<div className="content-area">
|
||||||
<h2>Crear Factura</h2>
|
<div className="card">
|
||||||
|
<h2>Crear Factura</h2>
|
||||||
|
|
||||||
<label>Seleccionar estudiante:</label>
|
<label>Seleccionar estudiante:</label>
|
||||||
<select
|
<select
|
||||||
className="form-select"
|
className="form-select"
|
||||||
value={userId}
|
value={userId}
|
||||||
onChange={(e) => setUserId(e.target.value)}
|
onChange={(e) => setUserId(e.target.value)}
|
||||||
>
|
|
||||||
<option value="">-- Selecciona un estudiante --</option>
|
|
||||||
{students.map((s) =>
|
|
||||||
s.user ? (
|
|
||||||
<option key={s.id} value={s.user.id}>
|
|
||||||
{s.user.name} {s.user.lastName} ({s.user.email})
|
|
||||||
</option>
|
|
||||||
) : null
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<label>Fecha de emisión:</label>
|
|
||||||
<input
|
|
||||||
className="form-input"
|
|
||||||
type="datetime-local"
|
|
||||||
value={date}
|
|
||||||
onChange={(e) => setDate(e.target.value)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<h3>Productos / Servicios</h3>
|
|
||||||
|
|
||||||
{lines.map((line, i) => (
|
|
||||||
<InvoiceLineItem
|
|
||||||
key={i}
|
|
||||||
index={i}
|
|
||||||
line={line}
|
|
||||||
products={products}
|
|
||||||
onUpdate={updateLine}
|
|
||||||
onRemove={removeLine}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<div style={{ marginTop: "1rem" }}>
|
|
||||||
<button className="btn btn-secondary" onClick={addLine}>
|
|
||||||
+ Añadir Producto
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-primary"
|
|
||||||
onClick={createInvoice}
|
|
||||||
style={{ marginLeft: "1rem" }}
|
|
||||||
>
|
>
|
||||||
✅ Crear Factura
|
<option value="">-- Selecciona un estudiante --</option>
|
||||||
</button>
|
{students.map((s) =>
|
||||||
|
s.user ? (
|
||||||
|
<option key={s.id} value={s.user.id}>
|
||||||
|
{s.user.name} {s.user.lastName} ({s.user.email})
|
||||||
|
</option>
|
||||||
|
) : null
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label>Fecha de emisión:</label>
|
||||||
|
<input
|
||||||
|
className="form-input"
|
||||||
|
type="datetime-local"
|
||||||
|
value={date}
|
||||||
|
onChange={(e) => setDate(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<h3>Productos / Servicios</h3>
|
||||||
|
|
||||||
|
{lines.map((line, i) => (
|
||||||
|
<InvoiceLineItem
|
||||||
|
key={i}
|
||||||
|
index={i}
|
||||||
|
line={line}
|
||||||
|
products={products}
|
||||||
|
onUpdate={updateLine}
|
||||||
|
onRemove={removeLine}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<h4 style={{ marginTop: "1rem" }}>
|
||||||
|
Total: <strong>{calculateTotal().toFixed(2)} €</strong>
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<ErrorMessage message={error} />
|
||||||
|
|
||||||
|
<div style={{ marginTop: "1rem" }}>
|
||||||
|
<button className="btn btn-secondary" onClick={addLine}>
|
||||||
|
+ Añadir Producto
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={createInvoice}
|
||||||
|
style={{ marginLeft: "1rem" }}
|
||||||
|
>
|
||||||
|
✅ Crear Factura
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{createdInvoiceId && (
|
||||||
|
<div style={{ marginTop: "2rem" }}>
|
||||||
|
<h4>Factura creada: #{createdInvoiceId}</h4>
|
||||||
|
<button className="btn btn-primary" onClick={downloadPdf}>
|
||||||
|
📄 Descargar PDF
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import api from "../../api/axiosConfig";
|
||||||
|
import ErrorMessage from "../common/ErrorMessage";
|
||||||
|
import "../styles/ContentArea.css";
|
||||||
|
|
||||||
|
const PaymentForm = () => {
|
||||||
|
const [students, setStudents] = useState([]);
|
||||||
|
const [selectedUserId, setSelectedUserId] = useState("");
|
||||||
|
const [invoices, setInvoices] = useState([]);
|
||||||
|
const [selectedInvoiceId, setSelectedInvoiceId] = useState("");
|
||||||
|
const [amount, setAmount] = useState("");
|
||||||
|
const [paymentMethod, setPaymentMethod] = useState("CASH");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [success, setSuccess] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api.get("/students/getAll").then((res) => setStudents(res.data));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchInvoices = async (userId) => {
|
||||||
|
try {
|
||||||
|
const res = await api.get(`/invoices/getAllInvoicesByUserId/${userId}`);
|
||||||
|
const notPaid = res.data.filter((invoice) => invoice.status === "NOT_PAID");
|
||||||
|
setInvoices(notPaid);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setError("Error al cargar facturas.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStudentChange = (e) => {
|
||||||
|
const userId = e.target.value;
|
||||||
|
setSelectedUserId(userId);
|
||||||
|
setSelectedInvoiceId("");
|
||||||
|
setInvoices([]);
|
||||||
|
if (userId) {
|
||||||
|
fetchInvoices(userId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError("");
|
||||||
|
setSuccess("");
|
||||||
|
|
||||||
|
if (!selectedInvoiceId || !amount || !paymentMethod) {
|
||||||
|
setError("Completa todos los campos.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.post("/payments/create", {
|
||||||
|
invoiceId: parseInt(selectedInvoiceId),
|
||||||
|
paymentDate: new Date().toISOString(),
|
||||||
|
amount: parseFloat(amount),
|
||||||
|
paymentMethod,
|
||||||
|
status: "PAID",
|
||||||
|
});
|
||||||
|
|
||||||
|
setSuccess("✅ Pago registrado correctamente.");
|
||||||
|
setSelectedInvoiceId("");
|
||||||
|
setAmount("");
|
||||||
|
setPaymentMethod("CASH");
|
||||||
|
fetchInvoices(selectedUserId); // actualizar facturas pendientes
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setError("❌ Error al registrar el pago.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="content-area">
|
||||||
|
<div className="card">
|
||||||
|
<h2>Registrar Pago</h2>
|
||||||
|
|
||||||
|
<label>Seleccionar estudiante:</label>
|
||||||
|
<select className="form-select" value={selectedUserId} onChange={handleStudentChange}>
|
||||||
|
<option value="">-- Selecciona un estudiante --</option>
|
||||||
|
{students.map((s) =>
|
||||||
|
s.user ? (
|
||||||
|
<option key={s.id} value={s.user.id}>
|
||||||
|
{s.user.name} {s.user.lastName} ({s.user.email})
|
||||||
|
</option>
|
||||||
|
) : null
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{invoices.length > 0 && (
|
||||||
|
<>
|
||||||
|
<label>Seleccionar factura:</label>
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
value={selectedInvoiceId}
|
||||||
|
onChange={(e) => setSelectedInvoiceId(e.target.value)}
|
||||||
|
>
|
||||||
|
<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)} €
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedInvoiceId && (
|
||||||
|
<>
|
||||||
|
<label>Importe pagado (€):</label>
|
||||||
|
<input
|
||||||
|
className="form-input"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
value={amount}
|
||||||
|
onChange={(e) => setAmount(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label>Método de pago:</label>
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
value={paymentMethod}
|
||||||
|
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>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<ErrorMessage message={error} type="error" />
|
||||||
|
<ErrorMessage message={success} type="success" />
|
||||||
|
|
||||||
|
<button className="btn btn-primary" style={{ marginTop: "1rem" }} onClick={handleSubmit}>
|
||||||
|
💳 Confirmar Pago
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PaymentForm;
|
||||||
|
|
@ -23,6 +23,9 @@ import ViewTimetable from '../forms/ViewTimetable';
|
||||||
import MembershipForm from '../forms/MembershipCreateForm';
|
import MembershipForm from '../forms/MembershipCreateForm';
|
||||||
import MembershipList from '../lists/MembershipList';
|
import MembershipList from '../lists/MembershipList';
|
||||||
import InvoiceForm from '../forms/InvoiceForm';
|
import InvoiceForm from '../forms/InvoiceForm';
|
||||||
|
import InvoiceList from '../lists/InvoiceList';
|
||||||
|
import PaymentForm from '../forms/PaymentForm';
|
||||||
|
import PaymentList from '../lists/PaymentList';
|
||||||
import '../styles/MainLayout.css';
|
import '../styles/MainLayout.css';
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -66,9 +69,12 @@ const MainLayout = () => {
|
||||||
<Route path="/admin/class-management/memberships/create" element={<MembershipForm />} />
|
<Route path="/admin/class-management/memberships/create" element={<MembershipForm />} />
|
||||||
<Route path="/admin/class-management/memberships/list" element={<MembershipList />} />
|
<Route path="/admin/class-management/memberships/list" element={<MembershipList />} />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<Route path="/admin/finance/invoices/create" element={<InvoiceForm />} />
|
<Route path="/admin/finance/invoices/create" element={<InvoiceForm />} />
|
||||||
|
<Route path="/admin/finance/invoices/list" element={<InvoiceList />} />
|
||||||
|
<Route path="/admin/finance/payments/create" element={<PaymentForm />} />
|
||||||
|
<Route path="/admin/finance/payments/list" element={<PaymentList />} />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Profile Page*/}
|
{/* Profile Page*/}
|
||||||
<Route path="/profile" element={<ProfilePage />} />
|
<Route path="/profile" element={<ProfilePage />} />
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ const SidebarAdmin = () => {
|
||||||
<div className="submenu">
|
<div className="submenu">
|
||||||
<h4>👥 Administración de Usuarios</h4>
|
<h4>👥 Administración de Usuarios</h4>
|
||||||
<button onClick={() => navigate('/admin/user-management/users/create')}>➕ Crear Usuario</button>
|
<button onClick={() => navigate('/admin/user-management/users/create')}>➕ Crear Usuario</button>
|
||||||
<button onClick={() => navigate('/admin/user-management/users/list')}>📋 Ve Usuarios</button>
|
<button onClick={() => navigate('/admin/user-management/users/list')}>📋 Ver Usuarios</button>
|
||||||
<button onClick={() => navigate('/admin/user-management/notifications/create')}>🔔 Crear Notificación</button>
|
<button onClick={() => navigate('/admin/user-management/notifications/create')}>🔔 Crear Notificación</button>
|
||||||
<button onClick={() => navigate('/admin/user-management/notifications/list')}>📨 Ver Notificaciones</button>
|
<button onClick={() => navigate('/admin/user-management/notifications/list')}>📨 Ver Notificaciones</button>
|
||||||
<button onClick={() => navigate('/admin/user-management/student-history/create')}>🕒 Crear Historial</button>
|
<button onClick={() => navigate('/admin/user-management/student-history/create')}>🕒 Crear Historial</button>
|
||||||
|
|
@ -39,7 +39,8 @@ const SidebarAdmin = () => {
|
||||||
<h4>💵 Finanzas</h4>
|
<h4>💵 Finanzas</h4>
|
||||||
<button onClick={() => navigate('/admin/finance/invoices/create')}>🧾 Crear Factura</button>
|
<button onClick={() => navigate('/admin/finance/invoices/create')}>🧾 Crear Factura</button>
|
||||||
<button onClick={() => navigate('/admin/finance/invoices/list')}>📄 Ver Facturas</button>
|
<button onClick={() => navigate('/admin/finance/invoices/list')}>📄 Ver Facturas</button>
|
||||||
<button onClick={() => navigate('/admin/finance/payments/create')}>💳 Nuevo Pago</button>
|
<button onClick={() => navigate('/admin/finance/payments/create')}>📄 Nuevo Pago</button>
|
||||||
|
<button onClick={() => navigate('/admin/finance/payments/list')}>💳 Facturas Pagadas</button>
|
||||||
<button onClick={() => navigate('/admin/finance/products/create')}>🛒 Añadir Productos</button>
|
<button onClick={() => navigate('/admin/finance/products/create')}>🛒 Añadir Productos</button>
|
||||||
<button onClick={() => navigate('/admin/finance/products/list')}>📦 Ver Productos</button>
|
<button onClick={() => navigate('/admin/finance/products/list')}>📦 Ver Productos</button>
|
||||||
<button onClick={() => navigate('/admin/finance/ivatypes/create')}>💱 Añadir Tipo de IVA</button>
|
<button onClick={() => navigate('/admin/finance/ivatypes/create')}>💱 Añadir Tipo de IVA</button>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import api from "../../api/axiosConfig";
|
||||||
|
import ErrorMessage from "../common/ErrorMessage";
|
||||||
|
import "../styles/ContentArea.css";
|
||||||
|
|
||||||
|
const InvoiceList = () => {
|
||||||
|
const [invoices, setInvoices] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [errorMsg, setErrorMsg] = useState("");
|
||||||
|
const [successMsg, setSuccessMsg] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchInvoices();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchInvoices = async () => {
|
||||||
|
try {
|
||||||
|
const res = await api.get("/invoices/getAll");
|
||||||
|
setInvoices(res.data);
|
||||||
|
setErrorMsg("");
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setErrorMsg("❌ Error al cargar las facturas.");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id) => {
|
||||||
|
if (!window.confirm("¿Estás seguro de que deseas eliminar esta factura?")) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.delete(`/invoices/deleteById/${id}`);
|
||||||
|
setInvoices(invoices.filter((inv) => inv.id !== id));
|
||||||
|
setSuccessMsg("✅ Factura eliminada correctamente.");
|
||||||
|
setErrorMsg("");
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setErrorMsg("❌ Error al eliminar la factura.");
|
||||||
|
setSuccessMsg("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadPdf = async (id) => {
|
||||||
|
try {
|
||||||
|
const res = await api.get(`/invoices/generatePDFById/${id}`, {
|
||||||
|
responseType: "blob",
|
||||||
|
});
|
||||||
|
|
||||||
|
const blob = new Blob([res.data], { type: "application/pdf" });
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = `factura_${id}.pdf`;
|
||||||
|
a.click();
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setErrorMsg("❌ Error al descargar el PDF.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<p>Cargando facturas...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<h2>Listado de Facturas</h2>
|
||||||
|
<ErrorMessage message={errorMsg} type="error" />
|
||||||
|
<ErrorMessage message={successMsg} type="success" />
|
||||||
|
|
||||||
|
<div className="table-wrapper">
|
||||||
|
<table className="styled-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Cliente</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Fecha</th>
|
||||||
|
<th>Estado</th>
|
||||||
|
<th>Total (€)</th>
|
||||||
|
<th>Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{invoices.map((inv) => (
|
||||||
|
<tr key={inv.id}>
|
||||||
|
<td>{inv.id}</td>
|
||||||
|
<td>{inv.user?.name} {inv.user?.surname}</td>
|
||||||
|
<td>{inv.user?.email}</td>
|
||||||
|
<td>{new Date(inv.date).toLocaleString()}</td>
|
||||||
|
<td>{inv.status}</td>
|
||||||
|
<td>{inv.total?.toFixed(2)}</td>
|
||||||
|
<td>
|
||||||
|
<button className="edit-btn" onClick={() => downloadPdf(inv.id)}>📄 PDF</button>
|
||||||
|
<button className="delete-btn" onClick={() => handleDelete(inv.id)}>🗑️</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InvoiceList;
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import api from "../../api/axiosConfig";
|
||||||
|
import ErrorMessage from "../common/ErrorMessage";
|
||||||
|
import '../styles/ContentArea.css';
|
||||||
|
|
||||||
|
const PaymentList = () => {
|
||||||
|
const [students, setStudents] = useState([]);
|
||||||
|
const [selectedUserId, setSelectedUserId] = useState("");
|
||||||
|
const [payments, setPayments] = useState([]);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api.get("/students/getAll").then((res) => setStudents(res.data));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchPayments = async (userId) => {
|
||||||
|
try {
|
||||||
|
const res = await api.get(`/payments/getAllByUserId/${userId}`);
|
||||||
|
setPayments(res.data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setError("Error al cargar pagos.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStudentChange = (e) => {
|
||||||
|
const userId = e.target.value;
|
||||||
|
setSelectedUserId(userId);
|
||||||
|
if (userId) {
|
||||||
|
fetchPayments(userId);
|
||||||
|
} else {
|
||||||
|
setPayments([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (id) => {
|
||||||
|
if (window.confirm("¿Seguro que quieres eliminar este pago?")) {
|
||||||
|
try {
|
||||||
|
await api.delete(`/payments/deleteById/${id}`);
|
||||||
|
fetchPayments(selectedUserId);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setError("Error al eliminar el pago.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="content-area">
|
||||||
|
<div className="card">
|
||||||
|
<h2>Listado de Pagos</h2>
|
||||||
|
|
||||||
|
<label>Seleccionar estudiante:</label>
|
||||||
|
<select className="form-select" value={selectedUserId} onChange={handleStudentChange}>
|
||||||
|
<option value="">-- Selecciona un estudiante --</option>
|
||||||
|
{students.map((s) =>
|
||||||
|
s.user ? (
|
||||||
|
<option key={s.id} value={s.user.id}>
|
||||||
|
{s.user.name} {s.user.lastName} ({s.user.email})
|
||||||
|
</option>
|
||||||
|
) : null
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<ErrorMessage message={error} />
|
||||||
|
|
||||||
|
{payments.length > 0 && (
|
||||||
|
<table className="table" style={{ marginTop: "1rem" }}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Factura</th>
|
||||||
|
<th>Fecha</th>
|
||||||
|
<th>Monto (€)</th>
|
||||||
|
<th>Método</th>
|
||||||
|
<th>Estado</th>
|
||||||
|
<th>Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{payments.map((p) => (
|
||||||
|
<tr key={p.id}>
|
||||||
|
<td>{p.id}</td>
|
||||||
|
<td>#{p.invoiceId}</td>
|
||||||
|
<td>{new Date(p.paymentDate).toLocaleDateString()}</td>
|
||||||
|
<td>{p.amount.toFixed(2)}</td>
|
||||||
|
<td>{p.paymentMethod}</td>
|
||||||
|
<td>{p.status}</td>
|
||||||
|
<td>
|
||||||
|
{/* Botón editar lo montamos luego */}
|
||||||
|
{/* <button className="btn btn-secondary btn-sm" onClick={() => handleEdit(p)}>✏️</button> */}
|
||||||
|
<button className="btn btn-danger btn-sm" onClick={() => handleDelete(p.id)}>
|
||||||
|
🗑️
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PaymentList;
|
||||||
Loading…
Reference in New Issue