diff --git a/memberflow-api/libs/memberflow-data-1.0-SNAPSHOT.jar b/memberflow-api/libs/memberflow-data-1.0-SNAPSHOT.jar index 2776aa2..0610f03 100644 Binary files a/memberflow-api/libs/memberflow-data-1.0-SNAPSHOT.jar and b/memberflow-api/libs/memberflow-data-1.0-SNAPSHOT.jar differ diff --git a/memberflow-api/src/main/java/com/denniseckerskorn/dtos/finance_management_dtos/ProductServiceDTO.java b/memberflow-api/src/main/java/com/denniseckerskorn/dtos/finance_management_dtos/ProductServiceDTO.java index c5e6f90..dbba695 100644 --- a/memberflow-api/src/main/java/com/denniseckerskorn/dtos/finance_management_dtos/ProductServiceDTO.java +++ b/memberflow-api/src/main/java/com/denniseckerskorn/dtos/finance_management_dtos/ProductServiceDTO.java @@ -14,6 +14,8 @@ public class ProductServiceDTO { @NotNull private Integer ivaTypeId; + private IVATypeDTO ivaType; + @NotNull private String name; @@ -28,11 +30,13 @@ public class ProductServiceDTO { @NotNull private StatusValues status; - public ProductServiceDTO() {} + public ProductServiceDTO() { + } public ProductServiceDTO(ProductService entity) { this.id = entity.getId(); this.ivaTypeId = entity.getIvaType() != null ? entity.getIvaType().getId() : null; + this.ivaType = entity.getIvaType() != null ? new IVATypeDTO(entity.getIvaType()) : null; this.name = entity.getName(); this.description = entity.getDescription(); this.price = entity.getPrice(); @@ -66,9 +70,7 @@ public class ProductServiceDTO { return entity; } - - // Getters y setters... - + // Getters y setters public Integer getId() { return id; @@ -86,6 +88,14 @@ public class ProductServiceDTO { this.ivaTypeId = ivaTypeId; } + public IVATypeDTO getIvaType() { + return ivaType; + } + + public void setIvaType(IVATypeDTO ivaType) { + this.ivaType = ivaType; + } + public String getName() { return name; } diff --git a/memberflow-data/src/main/java/com/denniseckerskorn/services/finance_services/InvoiceService.java b/memberflow-data/src/main/java/com/denniseckerskorn/services/finance_services/InvoiceService.java index 5a375a4..53b6776 100644 --- a/memberflow-data/src/main/java/com/denniseckerskorn/services/finance_services/InvoiceService.java +++ b/memberflow-data/src/main/java/com/denniseckerskorn/services/finance_services/InvoiceService.java @@ -131,10 +131,23 @@ public class InvoiceService extends AbstractService { } public void updateInvoiceTotal(Invoice invoice) { - BigDecimal total = invoice.getInvoiceLines().stream() - .map(InvoiceLine::getSubtotal) - .filter(Objects::nonNull) - .reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal total = BigDecimal.ZERO; + + for (InvoiceLine line : invoice.getInvoiceLines()) { + if (line.getUnitPrice() != null && line.getQuantity() != null) { + BigDecimal quantity = BigDecimal.valueOf(line.getQuantity()); + BigDecimal base = line.getUnitPrice().multiply(quantity); + + BigDecimal ivaMultiplier = BigDecimal.ONE; + if (line.getProductService() != null && line.getProductService().getIvaType() != null) { + BigDecimal iva = line.getProductService().getIvaType().getPercentage(); + ivaMultiplier = ivaMultiplier.add(iva.divide(BigDecimal.valueOf(100))); + } + + BigDecimal lineTotal = base.multiply(ivaMultiplier); + total = total.add(lineTotal); + } + } invoice.setTotal(total); } diff --git a/memberflow-frontend/src/components/forms/IVATypeManager.jsx b/memberflow-frontend/src/components/forms/IVATypeManager.jsx new file mode 100644 index 0000000..1b213b0 --- /dev/null +++ b/memberflow-frontend/src/components/forms/IVATypeManager.jsx @@ -0,0 +1,118 @@ +import React, { useEffect, useState } from "react"; +import api from "../../api/axiosConfig"; + +const IVATypeManager = () => { + const [ivaTypes, setIvaTypes] = useState([]); + const [newIva, setNewIva] = useState({ percentage: "", description: "" }); + const [error, setError] = useState(""); + + const fetchIvaTypes = async () => { + try { + const res = await api.get("/iva-types/getAll"); + setIvaTypes(res.data); + } catch (err) { + console.error(err); + setError("Error al obtener los tipos de IVA."); + } + }; + + useEffect(() => { + fetchIvaTypes(); + }, []); + + const handleChange = (e) => { + const { name, value } = e.target; + setNewIva((prev) => ({ ...prev, [name]: value })); + }; + + const handleAddIva = async () => { + setError(""); + if (!newIva.percentage || isNaN(newIva.percentage)) { + setError("Introduce un porcentaje válido."); + return; + } + + try { + const payload = { + percentage: parseFloat(newIva.percentage), + description: newIva.description + }; + + await api.post("/iva-types/create", payload); + setNewIva({ percentage: "", description: "" }); + fetchIvaTypes(); + } catch (err) { + console.error(err); + setError("No se pudo crear el tipo de IVA."); + } + }; + + const handleDelete = async (id) => { + if (!window.confirm("¿Seguro que deseas eliminar este tipo de IVA?")) return; + try { + await api.delete(`/iva-types/deleteById/${id}`); + fetchIvaTypes(); + } catch (err) { + console.error(err); + setError("No se pudo eliminar el tipo de IVA."); + } + }; + + return ( +
+

Gestión de Tipos de IVA

+ + {error &&

{error}

} + +
+ + + + + + + +
+ + + + + + + + + + + + {ivaTypes.map((iva) => ( + + + + + + + ))} + +
IDPorcentajeDescripciónAcciones
{iva.id}{iva.percentage}%{iva.description} + +
+
+ ); +}; + +export default IVATypeManager; diff --git a/memberflow-frontend/src/components/forms/InvoiceForm.jsx b/memberflow-frontend/src/components/forms/InvoiceForm.jsx index 91acfe1..9f47838 100644 --- a/memberflow-frontend/src/components/forms/InvoiceForm.jsx +++ b/memberflow-frontend/src/components/forms/InvoiceForm.jsx @@ -10,7 +10,7 @@ const InvoiceForm = () => { const [lines, setLines] = useState([]); const [products, setProducts] = useState([]); const [error, setError] = useState(""); - const [createdInvoiceId, setCreatedInvoiceId] = useState(null); + const [createdInvoice, setCreatedInvoice] = useState(null); useEffect(() => { api.get("/students/getAll").then((res) => setStudents(res.data)); @@ -31,19 +31,31 @@ const InvoiceForm = () => { setLines(lines.filter((_, i) => i !== index)); }; - const calculateTotal = () => { + const calculateSubtotal = () => { return lines.reduce((acc, line) => { - const product = products.find( - (p) => p.id === parseInt(line.productServiceId) - ); + const product = products.find((p) => p.id === parseInt(line.productServiceId)); if (!product) return acc; - return acc + product.price * line.quantity; + return acc + product.price * (parseInt(line.quantity) || 1); }, 0); }; + const calculateIVA = () => { + return lines.reduce((acc, line) => { + const product = products.find((p) => p.id === parseInt(line.productServiceId)); + if (!product || !product.ivaType) return acc; + const quantity = parseInt(line.quantity) || 1; + const iva = product.ivaType.percentage || 0; + return acc + (product.price * quantity * (iva / 100)); + }, 0); + }; + + const calculateTotal = () => { + return calculateSubtotal() + calculateIVA(); + }; + const createInvoice = async () => { setError(""); - setCreatedInvoiceId(null); + setCreatedInvoice(null); if (!userId || lines.length === 0) { setError("Selecciona un estudiante y al menos un producto."); @@ -52,9 +64,7 @@ const InvoiceForm = () => { try { const preparedLines = lines.map((line) => { - const product = products.find( - (p) => p.id === parseInt(line.productServiceId) - ); + const product = products.find((p) => p.id === parseInt(line.productServiceId)); return { productServiceId: parseInt(line.productServiceId), quantity: parseInt(line.quantity), @@ -71,9 +81,7 @@ const InvoiceForm = () => { }; const res = await api.post("/invoices/createInvoiceWithLines", payload); - const invoiceId = res.data.id; - - setCreatedInvoiceId(invoiceId); + setCreatedInvoice(res.data); alert("✅ Factura creada correctamente."); } catch (err) { console.error(err); @@ -83,14 +91,14 @@ const InvoiceForm = () => { const downloadPdf = async () => { try { - const response = await api.get(`/invoices/generatePDFById/${createdInvoiceId}`, { - responseType: "blob", // para recibir el PDF correctamente + const response = await api.get(`/invoices/generatePDFById/${createdInvoice.id}`, { + responseType: "blob", }); const url = window.URL.createObjectURL(new Blob([response.data])); const link = document.createElement("a"); link.href = url; - link.setAttribute("download", `factura_${createdInvoiceId}.pdf`); + link.setAttribute("download", `factura_${createdInvoice.id}.pdf`); document.body.appendChild(link); link.click(); } catch (err) { @@ -141,9 +149,11 @@ const InvoiceForm = () => { /> ))} -

- Total: {calculateTotal().toFixed(2)} € -

+
+

Subtotal: {calculateSubtotal().toFixed(2)} €

+

IVA: {calculateIVA().toFixed(2)} €

+

Total estimado con IVA: {calculateTotal().toFixed(2)} €

+
@@ -160,9 +170,10 @@ const InvoiceForm = () => { - {createdInvoiceId && ( + {createdInvoice && (
-

Factura creada: #{createdInvoiceId}

+

Factura creada: #{createdInvoice.id}

+

Total con IVA: {createdInvoice.total.toFixed(2)} €

diff --git a/memberflow-frontend/src/components/forms/InvoiceLineItem.jsx b/memberflow-frontend/src/components/forms/InvoiceLineItem.jsx index c207f1c..c71ab8d 100644 --- a/memberflow-frontend/src/components/forms/InvoiceLineItem.jsx +++ b/memberflow-frontend/src/components/forms/InvoiceLineItem.jsx @@ -1,5 +1,3 @@ -// src/components/forms/InvoiceLineItem.jsx - import React from 'react'; const InvoiceLineItem = ({ index, line, products, onUpdate, onRemove }) => { @@ -8,6 +6,10 @@ const InvoiceLineItem = ({ index, line, products, onUpdate, onRemove }) => { }; const selectedProduct = products.find(p => p.id === parseInt(line.productServiceId)); + const quantity = parseInt(line.quantity) || 1; + const price = selectedProduct?.price || 0; + const ivaPercentage = selectedProduct?.ivaType?.percentage || 0; + const totalConIVA = price * quantity * (1 + ivaPercentage / 100); return (
@@ -36,9 +38,14 @@ const InvoiceLineItem = ({ index, line, products, onUpdate, onRemove }) => { /> {selectedProduct && ( -
- {selectedProduct.name}: {selectedProduct.description} -
+ <> +
+ {selectedProduct.name}: {selectedProduct.description} +
+
+ Total con IVA: {totalConIVA.toFixed(2)} € +
+ )} diff --git a/memberflow-frontend/src/components/layout/MainLayout.jsx b/memberflow-frontend/src/components/layout/MainLayout.jsx index c6a676c..0e36e6d 100644 --- a/memberflow-frontend/src/components/layout/MainLayout.jsx +++ b/memberflow-frontend/src/components/layout/MainLayout.jsx @@ -2,7 +2,6 @@ import React from 'react'; import { Routes, Route } from 'react-router-dom'; import Topbar from './Topbar'; import Sidebar from './Sidebar'; -import ContentArea from './ContentArea'; import AdminDashboard from '../../pages/admin/AdminDashboard'; import TeacherDashboard from '../../pages/teacher/TeacherDashboard'; import StudentDashboard from '../../pages/student/StudentDashboard'; @@ -11,7 +10,7 @@ import UserList from '../../components/lists/UserList'; import NotificationCreateForm from '../../components/forms/NotificationCreateForm'; import StudentHistoryCreateForm from '../../components/forms/StudentHistoryCreateForm'; import StudentHistoryList from '../../components/lists/StudentHistoryList'; -import ProfilePage from '../../pages/ProfilePage' +import ProfilePage from '../../pages/ProfilePage'; import NotificationList from '../../components/lists/NotificationList'; import TrainingGroupForm from '../forms/TrainingGroupFrom'; import TrainingGroupList from '../lists/TrainingGroupList'; @@ -28,9 +27,9 @@ import PaymentForm from '../forms/PaymentForm'; import PaymentList from '../lists/PaymentList'; import ProductForm from '../forms/ProductForm'; import ProductList from '../lists/ProductList'; +import IVATypeManager from '../forms/IVATypeManager'; import '../styles/MainLayout.css'; - const MainLayout = () => { return (
@@ -40,27 +39,20 @@ const MainLayout = () => {
- {/* Dashboards principales */} + {/* Dashboards */} } /> } /> } /> - {/* User Management - rutas específicas */} + {/* User Management */} } /> } /> } /> } /> - - {/* Student History */} } /> } /> - {/* ContentArea general para secciones sin componentes específicos */} - } /> - } /> - } /> - - {/*Class Management - rutas específicas*/} + {/* Class Management */} } /> } /> } /> @@ -71,19 +63,17 @@ const MainLayout = () => { } /> } /> + {/* Finance */} } /> } /> } /> } /> } /> } /> + } /> - - - - {/* Profile Page*/} + {/* Perfil */} } /> -
diff --git a/memberflow-frontend/src/components/styles/Sidebar.css b/memberflow-frontend/src/components/styles/Sidebar.css index 049f6fe..9ad3338 100644 --- a/memberflow-frontend/src/components/styles/Sidebar.css +++ b/memberflow-frontend/src/components/styles/Sidebar.css @@ -2,7 +2,7 @@ width: 320px; background-color: #ffffff; color: #2d3436; - padding: 0; /* importante para evitar que el padding afecte la altura */ + padding: 0; box-shadow: 2px 0 10px rgba(0, 0, 0, 0.05); height: 100vh; display: flex; @@ -44,4 +44,5 @@ display: flex; flex-direction: column; gap: 10px; + min-height: 0; } \ No newline at end of file