diff --git a/memberflow-api/pom.xml b/memberflow-api/pom.xml index ef0e6bb..55a9741 100644 --- a/memberflow-api/pom.xml +++ b/memberflow-api/pom.xml @@ -94,9 +94,18 @@ 8.0.1.Final + + com.itextpdf + itext7-core + 8.0.3 + pom + + + + 21 21 diff --git a/memberflow-api/src/main/java/com/denniseckerskorn/controllers/finance_management/InvoiceController.java b/memberflow-api/src/main/java/com/denniseckerskorn/controllers/finance_management/InvoiceController.java index ce42d84..43e6c21 100644 --- a/memberflow-api/src/main/java/com/denniseckerskorn/controllers/finance_management/InvoiceController.java +++ b/memberflow-api/src/main/java/com/denniseckerskorn/controllers/finance_management/InvoiceController.java @@ -1,17 +1,26 @@ 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.entities.finance.Invoice; 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.EntityNotFoundException; 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.ProductServiceService; +import com.denniseckerskorn.services.user_managment_services.UserService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -24,11 +33,56 @@ import java.util.stream.Collectors; public class InvoiceController { 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.userService = userService; + this.productServiceService = productServiceService; + this.invoicePdfGenerator = invoicePdfGenerator; } + @PostMapping("/createInvoiceWithLines") + @Operation(summary = "Create a new invoice with lines") + public ResponseEntity 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 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") @Operation(summary = "Create a new invoice") public ResponseEntity createInvoice(@Valid @RequestBody InvoiceDTO dto) throws DuplicateEntityException { diff --git a/memberflow-api/src/main/java/com/denniseckerskorn/dtos/finance_management_dtos/CreateInvoiceLineDTO.java b/memberflow-api/src/main/java/com/denniseckerskorn/dtos/finance_management_dtos/CreateInvoiceLineDTO.java new file mode 100644 index 0000000..d81f895 --- /dev/null +++ b/memberflow-api/src/main/java/com/denniseckerskorn/dtos/finance_management_dtos/CreateInvoiceLineDTO.java @@ -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; + } +} diff --git a/memberflow-api/src/main/java/com/denniseckerskorn/dtos/finance_management_dtos/CreateInvoiceRequestDTO.java b/memberflow-api/src/main/java/com/denniseckerskorn/dtos/finance_management_dtos/CreateInvoiceRequestDTO.java new file mode 100644 index 0000000..5d34acb --- /dev/null +++ b/memberflow-api/src/main/java/com/denniseckerskorn/dtos/finance_management_dtos/CreateInvoiceRequestDTO.java @@ -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 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 getLines() { + return lines; + } + + public void setLines(Set 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; + } +} diff --git a/memberflow-api/src/main/java/com/denniseckerskorn/services/finance_service/InvoicePdfGenerator.java b/memberflow-api/src/main/java/com/denniseckerskorn/services/finance_service/InvoicePdfGenerator.java new file mode 100644 index 0000000..02dbeda --- /dev/null +++ b/memberflow-api/src/main/java/com/denniseckerskorn/services/finance_service/InvoicePdfGenerator.java @@ -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(); + } +} diff --git a/memberflow-frontend/src/components/forms/InvoiceForm.jsx b/memberflow-frontend/src/components/forms/InvoiceForm.jsx index 67f59e7..91acfe1 100644 --- a/memberflow-frontend/src/components/forms/InvoiceForm.jsx +++ b/memberflow-frontend/src/components/forms/InvoiceForm.jsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from "react"; import InvoiceLineItem from "./InvoiceLineItem"; +import ErrorMessage from "../common/ErrorMessage"; import api from "../../api/axiosConfig"; const InvoiceForm = () => { @@ -8,10 +9,11 @@ const InvoiceForm = () => { const [date, setDate] = useState(new Date().toISOString().slice(0, 16)); const [lines, setLines] = useState([]); const [products, setProducts] = useState([]); + const [error, setError] = useState(""); + const [createdInvoiceId, setCreatedInvoiceId] = useState(null); useEffect(() => { api.get("/students/getAll").then((res) => setStudents(res.data)); - api.get("/products-services/getAll").then((res) => setProducts(res.data)); }, []); @@ -29,96 +31,143 @@ const InvoiceForm = () => { setLines(lines.filter((_, i) => i !== index)); }; - const createInvoice = async () => { - if (!userId || lines.length === 0) { - 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 calculateTotal = () => { + return lines.reduce((acc, line) => { const product = products.find( (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}`, { - productServiceId: parseInt(line.productServiceId), - quantity: parseInt(line.quantity), - unitPrice: product.price, - subtotal, - description: product.description, - }); + const createInvoice = async () => { + setError(""); + setCreatedInvoiceId(null); + + if (!userId || lines.length === 0) { + 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."); - setUserId(""); - setLines([]); + const payload = { + userId: parseInt(userId), + 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 (
-

Crear Factura

+
+

Crear Factura

- - - - - setDate(e.target.value)} - /> - -

Productos / Servicios

- - {lines.map((line, i) => ( - - ))} - -
- - + +
+ + {createdInvoiceId && ( +
+

Factura creada: #{createdInvoiceId}

+ +
+ )}
); diff --git a/memberflow-frontend/src/components/layout/SidebarAdmin.jsx b/memberflow-frontend/src/components/layout/SidebarAdmin.jsx index d368009..2dda034 100644 --- a/memberflow-frontend/src/components/layout/SidebarAdmin.jsx +++ b/memberflow-frontend/src/components/layout/SidebarAdmin.jsx @@ -15,7 +15,7 @@ const SidebarAdmin = () => {

👥 Administración de Usuarios

- +