From 52918e339382d7179067b99a8ea4cc2ee5c0e034 Mon Sep 17 00:00:00 2001 From: Dennis Eckerskorn Date: Thu, 15 May 2025 23:28:51 +0200 Subject: [PATCH] Added IVATypes --- .../libs/memberflow-data-1.0-SNAPSHOT.jar | Bin 260726 -> 260984 bytes .../ProductServiceDTO.java | 18 ++- .../finance_services/InvoiceService.java | 21 +++- .../src/components/forms/IVATypeManager.jsx | 118 ++++++++++++++++++ .../src/components/forms/InvoiceForm.jsx | 53 ++++---- .../src/components/forms/InvoiceLineItem.jsx | 17 ++- .../src/components/layout/MainLayout.jsx | 26 ++-- .../src/components/styles/Sidebar.css | 3 +- 8 files changed, 203 insertions(+), 53 deletions(-) create mode 100644 memberflow-frontend/src/components/forms/IVATypeManager.jsx diff --git a/memberflow-api/libs/memberflow-data-1.0-SNAPSHOT.jar b/memberflow-api/libs/memberflow-data-1.0-SNAPSHOT.jar index 2776aa273c5378e39d146818c26f0fd9aee1b2b4..0610f036bb8f499bcddd3a28cd6883d960a392cf 100644 GIT binary patch delta 5855 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 OH|8P)($Wi@c>e)703~1m 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