Added IVATypes

This commit is contained in:
Dennis Eckerskorn 2025-05-15 23:28:51 +02:00
parent c81a1ea588
commit 52918e3393
8 changed files with 203 additions and 53 deletions

View File

@ -14,6 +14,8 @@ public class ProductServiceDTO {
@NotNull @NotNull
private Integer ivaTypeId; private Integer ivaTypeId;
private IVATypeDTO ivaType;
@NotNull @NotNull
private String name; private String name;
@ -28,11 +30,13 @@ public class ProductServiceDTO {
@NotNull @NotNull
private StatusValues status; private StatusValues status;
public ProductServiceDTO() {} public ProductServiceDTO() {
}
public ProductServiceDTO(ProductService entity) { public ProductServiceDTO(ProductService entity) {
this.id = entity.getId(); this.id = entity.getId();
this.ivaTypeId = entity.getIvaType() != null ? entity.getIvaType().getId() : null; this.ivaTypeId = entity.getIvaType() != null ? entity.getIvaType().getId() : null;
this.ivaType = entity.getIvaType() != null ? new IVATypeDTO(entity.getIvaType()) : null;
this.name = entity.getName(); this.name = entity.getName();
this.description = entity.getDescription(); this.description = entity.getDescription();
this.price = entity.getPrice(); this.price = entity.getPrice();
@ -66,9 +70,7 @@ public class ProductServiceDTO {
return entity; return entity;
} }
// Getters y setters
// Getters y setters...
public Integer getId() { public Integer getId() {
return id; return id;
@ -86,6 +88,14 @@ public class ProductServiceDTO {
this.ivaTypeId = ivaTypeId; this.ivaTypeId = ivaTypeId;
} }
public IVATypeDTO getIvaType() {
return ivaType;
}
public void setIvaType(IVATypeDTO ivaType) {
this.ivaType = ivaType;
}
public String getName() { public String getName() {
return name; return name;
} }

View File

@ -131,10 +131,23 @@ public class InvoiceService extends AbstractService<Invoice, Integer> {
} }
public void updateInvoiceTotal(Invoice invoice) { public void updateInvoiceTotal(Invoice invoice) {
BigDecimal total = invoice.getInvoiceLines().stream() BigDecimal total = BigDecimal.ZERO;
.map(InvoiceLine::getSubtotal)
.filter(Objects::nonNull) for (InvoiceLine line : invoice.getInvoiceLines()) {
.reduce(BigDecimal.ZERO, BigDecimal::add); 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); invoice.setTotal(total);
} }

View File

@ -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 (
<div className="content-area">
<h2>Gestión de Tipos de IVA</h2>
{error && <p style={{ color: "red" }}>{error}</p>}
<div className="form-section">
<label>Porcentaje:</label>
<input
type="number"
name="percentage"
value={newIva.percentage}
onChange={handleChange}
step="0.01"
/>
<label>Descripción:</label>
<input
type="text"
name="description"
value={newIva.description}
onChange={handleChange}
/>
<button className="btn btn-primary" onClick={handleAddIva}>
+ Añadir IVA
</button>
</div>
<table className="table" style={{ marginTop: "2rem" }}>
<thead>
<tr>
<th>ID</th>
<th>Porcentaje</th>
<th>Descripción</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
{ivaTypes.map((iva) => (
<tr key={iva.id}>
<td>{iva.id}</td>
<td>{iva.percentage}%</td>
<td>{iva.description}</td>
<td>
<button className="btn btn-danger btn-sm" onClick={() => handleDelete(iva.id)}>
🗑 Eliminar
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
export default IVATypeManager;

View File

@ -10,7 +10,7 @@ const InvoiceForm = () => {
const [lines, setLines] = useState([]); const [lines, setLines] = useState([]);
const [products, setProducts] = useState([]); const [products, setProducts] = useState([]);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [createdInvoiceId, setCreatedInvoiceId] = useState(null); const [createdInvoice, setCreatedInvoice] = useState(null);
useEffect(() => { useEffect(() => {
api.get("/students/getAll").then((res) => setStudents(res.data)); api.get("/students/getAll").then((res) => setStudents(res.data));
@ -31,19 +31,31 @@ const InvoiceForm = () => {
setLines(lines.filter((_, i) => i !== index)); setLines(lines.filter((_, i) => i !== index));
}; };
const calculateTotal = () => { const calculateSubtotal = () => {
return lines.reduce((acc, line) => { return lines.reduce((acc, line) => {
const product = products.find( const product = products.find((p) => p.id === parseInt(line.productServiceId));
(p) => p.id === parseInt(line.productServiceId)
);
if (!product) return acc; if (!product) return acc;
return acc + product.price * line.quantity; return acc + product.price * (parseInt(line.quantity) || 1);
}, 0); }, 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 () => { const createInvoice = async () => {
setError(""); setError("");
setCreatedInvoiceId(null); setCreatedInvoice(null);
if (!userId || lines.length === 0) { if (!userId || lines.length === 0) {
setError("Selecciona un estudiante y al menos un producto."); setError("Selecciona un estudiante y al menos un producto.");
@ -52,9 +64,7 @@ const InvoiceForm = () => {
try { try {
const preparedLines = lines.map((line) => { const preparedLines = lines.map((line) => {
const product = products.find( const product = products.find((p) => p.id === parseInt(line.productServiceId));
(p) => p.id === parseInt(line.productServiceId)
);
return { return {
productServiceId: parseInt(line.productServiceId), productServiceId: parseInt(line.productServiceId),
quantity: parseInt(line.quantity), quantity: parseInt(line.quantity),
@ -71,9 +81,7 @@ const InvoiceForm = () => {
}; };
const res = await api.post("/invoices/createInvoiceWithLines", payload); const res = await api.post("/invoices/createInvoiceWithLines", payload);
const invoiceId = res.data.id; setCreatedInvoice(res.data);
setCreatedInvoiceId(invoiceId);
alert("✅ Factura creada correctamente."); alert("✅ Factura creada correctamente.");
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -83,14 +91,14 @@ const InvoiceForm = () => {
const downloadPdf = async () => { const downloadPdf = async () => {
try { try {
const response = await api.get(`/invoices/generatePDFById/${createdInvoiceId}`, { const response = await api.get(`/invoices/generatePDFById/${createdInvoice.id}`, {
responseType: "blob", // para recibir el PDF correctamente responseType: "blob",
}); });
const url = window.URL.createObjectURL(new Blob([response.data])); const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement("a"); const link = document.createElement("a");
link.href = url; link.href = url;
link.setAttribute("download", `factura_${createdInvoiceId}.pdf`); link.setAttribute("download", `factura_${createdInvoice.id}.pdf`);
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
} catch (err) { } catch (err) {
@ -141,9 +149,11 @@ const InvoiceForm = () => {
/> />
))} ))}
<h4 style={{ marginTop: "1rem" }}> <div style={{ marginTop: "1rem" }}>
Total: <strong>{calculateTotal().toFixed(2)} </strong> <h4>Subtotal: <strong>{calculateSubtotal().toFixed(2)} </strong></h4>
</h4> <h4>IVA: <strong>{calculateIVA().toFixed(2)} </strong></h4>
<h4>Total estimado con IVA: <strong>{calculateTotal().toFixed(2)} </strong></h4>
</div>
<ErrorMessage message={error} /> <ErrorMessage message={error} />
@ -160,9 +170,10 @@ const InvoiceForm = () => {
</button> </button>
</div> </div>
{createdInvoiceId && ( {createdInvoice && (
<div style={{ marginTop: "2rem" }}> <div style={{ marginTop: "2rem" }}>
<h4>Factura creada: #{createdInvoiceId}</h4> <h4>Factura creada: #{createdInvoice.id}</h4>
<h4>Total con IVA: <strong>{createdInvoice.total.toFixed(2)} </strong></h4>
<button className="btn btn-primary" onClick={downloadPdf}> <button className="btn btn-primary" onClick={downloadPdf}>
📄 Descargar PDF 📄 Descargar PDF
</button> </button>

View File

@ -1,5 +1,3 @@
// src/components/forms/InvoiceLineItem.jsx
import React from 'react'; import React from 'react';
const InvoiceLineItem = ({ index, line, products, onUpdate, onRemove }) => { 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 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 ( return (
<div className="content-area" style={{ marginBottom: "1rem", display: "flex", flexDirection: "column", gap: "0.5rem" }}> <div className="content-area" style={{ marginBottom: "1rem", display: "flex", flexDirection: "column", gap: "0.5rem" }}>
@ -36,9 +38,14 @@ const InvoiceLineItem = ({ index, line, products, onUpdate, onRemove }) => {
/> />
{selectedProduct && ( {selectedProduct && (
<div style={{ fontSize: '0.9rem', color: '#666' }}> <>
<strong>{selectedProduct.name}</strong>: {selectedProduct.description} <div style={{ fontSize: '0.9rem', color: '#666' }}>
</div> <strong>{selectedProduct.name}</strong>: {selectedProduct.description}
</div>
<div style={{ fontSize: '0.9rem', color: '#333' }}>
<strong>Total con IVA:</strong> {totalConIVA.toFixed(2)}
</div>
</>
)} )}
<button className="btn btn-danger" onClick={() => onRemove(index)}>Eliminar producto</button> <button className="btn btn-danger" onClick={() => onRemove(index)}>Eliminar producto</button>

View File

@ -2,7 +2,6 @@ import React from 'react';
import { Routes, Route } from 'react-router-dom'; import { Routes, Route } from 'react-router-dom';
import Topbar from './Topbar'; import Topbar from './Topbar';
import Sidebar from './Sidebar'; import Sidebar from './Sidebar';
import ContentArea from './ContentArea';
import AdminDashboard from '../../pages/admin/AdminDashboard'; import AdminDashboard from '../../pages/admin/AdminDashboard';
import TeacherDashboard from '../../pages/teacher/TeacherDashboard'; import TeacherDashboard from '../../pages/teacher/TeacherDashboard';
import StudentDashboard from '../../pages/student/StudentDashboard'; import StudentDashboard from '../../pages/student/StudentDashboard';
@ -11,7 +10,7 @@ import UserList from '../../components/lists/UserList';
import NotificationCreateForm from '../../components/forms/NotificationCreateForm'; import NotificationCreateForm from '../../components/forms/NotificationCreateForm';
import StudentHistoryCreateForm from '../../components/forms/StudentHistoryCreateForm'; import StudentHistoryCreateForm from '../../components/forms/StudentHistoryCreateForm';
import StudentHistoryList from '../../components/lists/StudentHistoryList'; import StudentHistoryList from '../../components/lists/StudentHistoryList';
import ProfilePage from '../../pages/ProfilePage' import ProfilePage from '../../pages/ProfilePage';
import NotificationList from '../../components/lists/NotificationList'; import NotificationList from '../../components/lists/NotificationList';
import TrainingGroupForm from '../forms/TrainingGroupFrom'; import TrainingGroupForm from '../forms/TrainingGroupFrom';
import TrainingGroupList from '../lists/TrainingGroupList'; import TrainingGroupList from '../lists/TrainingGroupList';
@ -28,9 +27,9 @@ import PaymentForm from '../forms/PaymentForm';
import PaymentList from '../lists/PaymentList'; import PaymentList from '../lists/PaymentList';
import ProductForm from '../forms/ProductForm'; import ProductForm from '../forms/ProductForm';
import ProductList from '../lists/ProductList'; import ProductList from '../lists/ProductList';
import IVATypeManager from '../forms/IVATypeManager';
import '../styles/MainLayout.css'; import '../styles/MainLayout.css';
const MainLayout = () => { const MainLayout = () => {
return ( return (
<div className="main-layout"> <div className="main-layout">
@ -40,27 +39,20 @@ const MainLayout = () => {
<div className="content-area"> <div className="content-area">
<Routes> <Routes>
{/* Dashboards principales */} {/* Dashboards */}
<Route path="/admin/dashboard" element={<AdminDashboard />} /> <Route path="/admin/dashboard" element={<AdminDashboard />} />
<Route path="/teacher/dashboard" element={<TeacherDashboard />} /> <Route path="/teacher/dashboard" element={<TeacherDashboard />} />
<Route path="/student/dashboard" element={<StudentDashboard />} /> <Route path="/student/dashboard" element={<StudentDashboard />} />
{/* User Management - rutas específicas */} {/* User Management */}
<Route path="/admin/user-management/users/create" element={<UserCreateForm />} /> <Route path="/admin/user-management/users/create" element={<UserCreateForm />} />
<Route path="/admin/user-management/users/list" element={<UserList />} /> <Route path="/admin/user-management/users/list" element={<UserList />} />
<Route path="/admin/user-management/notifications/create" element={<NotificationCreateForm />} /> <Route path="/admin/user-management/notifications/create" element={<NotificationCreateForm />} />
<Route path="/admin/user-management/notifications/list" element={<NotificationList />} /> <Route path="/admin/user-management/notifications/list" element={<NotificationList />} />
{/* Student History */}
<Route path="/admin/user-management/student-history/create" element={<StudentHistoryCreateForm />} /> <Route path="/admin/user-management/student-history/create" element={<StudentHistoryCreateForm />} />
<Route path="/admin/user-management/student-history/list" element={<StudentHistoryList />} /> <Route path="/admin/user-management/student-history/list" element={<StudentHistoryList />} />
{/* ContentArea general para secciones sin componentes específicos */} {/* Class Management */}
<Route path="/admin/user-management/*" element={<ContentArea />} />
<Route path="/admin/class-management/*" element={<ContentArea />} />
<Route path="/admin/finance/*" element={<ContentArea />} />
{/*Class Management - rutas específicas*/}
<Route path="/admin/class-management/training-groups/create" element={<TrainingGroupForm />} /> <Route path="/admin/class-management/training-groups/create" element={<TrainingGroupForm />} />
<Route path="/admin/class-management/training-groups/list" element={<TrainingGroupList />} /> <Route path="/admin/class-management/training-groups/list" element={<TrainingGroupList />} />
<Route path="/admin/class-management/training-groups/manage-students" element={<TrainingGroupStudentManager />} /> <Route path="/admin/class-management/training-groups/manage-students" element={<TrainingGroupStudentManager />} />
@ -71,19 +63,17 @@ 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 />} />
{/* Finance */}
<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/invoices/list" element={<InvoiceList />} />
<Route path="/admin/finance/payments/create" element={<PaymentForm />} /> <Route path="/admin/finance/payments/create" element={<PaymentForm />} />
<Route path="/admin/finance/payments/list" element={<PaymentList />} /> <Route path="/admin/finance/payments/list" element={<PaymentList />} />
<Route path="/admin/finance/products/create" element={<ProductForm />} /> <Route path="/admin/finance/products/create" element={<ProductForm />} />
<Route path="/admin/finance/products/list" element={<ProductList />} /> <Route path="/admin/finance/products/list" element={<ProductList />} />
<Route path="/admin/finance/ivatypes/create" element={<IVATypeManager />} />
{/* Perfil */}
{/* Profile Page*/}
<Route path="/profile" element={<ProfilePage />} /> <Route path="/profile" element={<ProfilePage />} />
</Routes> </Routes>
</div> </div>
</div> </div>

View File

@ -2,7 +2,7 @@
width: 320px; width: 320px;
background-color: #ffffff; background-color: #ffffff;
color: #2d3436; 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); box-shadow: 2px 0 10px rgba(0, 0, 0, 0.05);
height: 100vh; height: 100vh;
display: flex; display: flex;
@ -44,4 +44,5 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
min-height: 0;
} }