Compare commits
	
		
			No commits in common. "67b11504a75f52a62f2c58bc947a0ab5281d1010" and "c81a1ea5882cb9aa8595097025da173c586e4c9d" have entirely different histories.
		
	
	
		
			67b11504a7
			...
			c81a1ea588
		
	
		|  | @ -1 +0,0 @@ | |||
| /docker/mysql-data/ | ||||
							
								
								
									
										1885
									
								
								docker/backup.sql
								
								
								
								
							
							
						
						
									
										1885
									
								
								docker/backup.sql
								
								
								
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -12,7 +12,7 @@ services: | |||
|     ports: | ||||
|       - "3307:3306" | ||||
|     volumes: | ||||
|       - ./mysql-data:/var/lib/mysql | ||||
|       - mysql_data:/var/lib/mysql | ||||
|     healthcheck: | ||||
|       test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-p1234"] | ||||
|       interval: 10s | ||||
|  | @ -26,8 +26,7 @@ services: | |||
|       dockerfile: ../docker/Dockerfile-api | ||||
|     container_name: memberflow-backend | ||||
|     depends_on: | ||||
|       mysql: | ||||
|         condition: service_healthy | ||||
|       - mysql | ||||
|     ports: | ||||
|       - "8080:8080" | ||||
|     environment: | ||||
|  | @ -38,7 +37,6 @@ services: | |||
|       JWT_EXPIRATION: 7200000 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|   frontend: | ||||
|     build: | ||||
|       context: .. | ||||
|  | @ -48,3 +46,6 @@ services: | |||
|       - backend | ||||
|     ports: | ||||
|       - "3000:80" | ||||
| 
 | ||||
| volumes: | ||||
|   mysql_data: | ||||
|  |  | |||
										
											Binary file not shown.
										
									
								
							|  | @ -14,8 +14,6 @@ public class ProductServiceDTO { | |||
|     @NotNull | ||||
|     private Integer ivaTypeId; | ||||
| 
 | ||||
|     private IVATypeDTO ivaType; | ||||
| 
 | ||||
|     @NotNull | ||||
|     private String name; | ||||
| 
 | ||||
|  | @ -30,13 +28,11 @@ 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(); | ||||
|  | @ -70,7 +66,9 @@ public class ProductServiceDTO { | |||
|         return entity; | ||||
|     } | ||||
| 
 | ||||
|     // Getters y setters | ||||
| 
 | ||||
|     // Getters y setters... | ||||
| 
 | ||||
| 
 | ||||
|     public Integer getId() { | ||||
|         return id; | ||||
|  | @ -88,14 +86,6 @@ public class ProductServiceDTO { | |||
|         this.ivaTypeId = ivaTypeId; | ||||
|     } | ||||
| 
 | ||||
|     public IVATypeDTO getIvaType() { | ||||
|         return ivaType; | ||||
|     } | ||||
| 
 | ||||
|     public void setIvaType(IVATypeDTO ivaType) { | ||||
|         this.ivaType = ivaType; | ||||
|     } | ||||
| 
 | ||||
|     public String getName() { | ||||
|         return name; | ||||
|     } | ||||
|  |  | |||
|  | @ -131,23 +131,10 @@ public class InvoiceService extends AbstractService<Invoice, Integer> { | |||
|     } | ||||
| 
 | ||||
|     public void updateInvoiceTotal(Invoice invoice) { | ||||
|         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); | ||||
|             } | ||||
|         } | ||||
|         BigDecimal total = invoice.getInvoiceLines().stream() | ||||
|                 .map(InvoiceLine::getSubtotal) | ||||
|                 .filter(Objects::nonNull) | ||||
|                 .reduce(BigDecimal.ZERO, BigDecimal::add); | ||||
| 
 | ||||
|         invoice.setTotal(total); | ||||
|     } | ||||
|  |  | |||
|  | @ -1,118 +0,0 @@ | |||
| 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; | ||||
|  | @ -10,7 +10,7 @@ const InvoiceForm = () => { | |||
|   const [lines, setLines] = useState([]); | ||||
|   const [products, setProducts] = useState([]); | ||||
|   const [error, setError] = useState(""); | ||||
|   const [createdInvoice, setCreatedInvoice] = useState(null); | ||||
|   const [createdInvoiceId, setCreatedInvoiceId] = useState(null); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     api.get("/students/getAll").then((res) => setStudents(res.data)); | ||||
|  | @ -31,31 +31,19 @@ const InvoiceForm = () => { | |||
|     setLines(lines.filter((_, i) => i !== index)); | ||||
|   }; | ||||
| 
 | ||||
|   const calculateSubtotal = () => { | ||||
|     return lines.reduce((acc, line) => { | ||||
|       const product = products.find((p) => p.id === parseInt(line.productServiceId)); | ||||
|       if (!product) return acc; | ||||
|       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(); | ||||
|     return lines.reduce((acc, line) => { | ||||
|       const product = products.find( | ||||
|         (p) => p.id === parseInt(line.productServiceId) | ||||
|       ); | ||||
|       if (!product) return acc; | ||||
|       return acc + product.price * line.quantity; | ||||
|     }, 0); | ||||
|   }; | ||||
| 
 | ||||
|   const createInvoice = async () => { | ||||
|     setError(""); | ||||
|     setCreatedInvoice(null); | ||||
|     setCreatedInvoiceId(null); | ||||
| 
 | ||||
|     if (!userId || lines.length === 0) { | ||||
|       setError("Selecciona un estudiante y al menos un producto."); | ||||
|  | @ -64,7 +52,9 @@ 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), | ||||
|  | @ -81,7 +71,9 @@ const InvoiceForm = () => { | |||
|       }; | ||||
| 
 | ||||
|       const res = await api.post("/invoices/createInvoiceWithLines", payload); | ||||
|       setCreatedInvoice(res.data); | ||||
|       const invoiceId = res.data.id; | ||||
| 
 | ||||
|       setCreatedInvoiceId(invoiceId); | ||||
|       alert("✅ Factura creada correctamente."); | ||||
|     } catch (err) { | ||||
|       console.error(err); | ||||
|  | @ -91,14 +83,14 @@ const InvoiceForm = () => { | |||
| 
 | ||||
|   const downloadPdf = async () => { | ||||
|     try { | ||||
|       const response = await api.get(`/invoices/generatePDFById/${createdInvoice.id}`, { | ||||
|         responseType: "blob", | ||||
|       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_${createdInvoice.id}.pdf`); | ||||
|       link.setAttribute("download", `factura_${createdInvoiceId}.pdf`); | ||||
|       document.body.appendChild(link); | ||||
|       link.click(); | ||||
|     } catch (err) { | ||||
|  | @ -149,11 +141,9 @@ const InvoiceForm = () => { | |||
|           /> | ||||
|         ))} | ||||
| 
 | ||||
|         <div style={{ marginTop: "1rem" }}> | ||||
|           <h4>Subtotal: <strong>{calculateSubtotal().toFixed(2)} €</strong></h4> | ||||
|           <h4>IVA: <strong>{calculateIVA().toFixed(2)} €</strong></h4> | ||||
|           <h4>Total estimado con IVA: <strong>{calculateTotal().toFixed(2)} €</strong></h4> | ||||
|         </div> | ||||
|         <h4 style={{ marginTop: "1rem" }}> | ||||
|           Total: <strong>{calculateTotal().toFixed(2)} €</strong> | ||||
|         </h4> | ||||
| 
 | ||||
|         <ErrorMessage message={error} /> | ||||
| 
 | ||||
|  | @ -170,10 +160,9 @@ const InvoiceForm = () => { | |||
|           </button> | ||||
|         </div> | ||||
| 
 | ||||
|         {createdInvoice && ( | ||||
|         {createdInvoiceId && ( | ||||
|           <div style={{ marginTop: "2rem" }}> | ||||
|             <h4>Factura creada: #{createdInvoice.id}</h4> | ||||
|             <h4>Total con IVA: <strong>{createdInvoice.total.toFixed(2)} €</strong></h4> | ||||
|             <h4>Factura creada: #{createdInvoiceId}</h4> | ||||
|             <button className="btn btn-primary" onClick={downloadPdf}> | ||||
|               📄 Descargar PDF | ||||
|             </button> | ||||
|  |  | |||
|  | @ -1,3 +1,5 @@ | |||
| // src/components/forms/InvoiceLineItem.jsx | ||||
| 
 | ||||
| import React from 'react'; | ||||
| 
 | ||||
| const InvoiceLineItem = ({ index, line, products, onUpdate, onRemove }) => { | ||||
|  | @ -6,10 +8,6 @@ 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 ( | ||||
|     <div className="content-area" style={{ marginBottom: "1rem", display: "flex", flexDirection: "column", gap: "0.5rem" }}> | ||||
|  | @ -38,14 +36,9 @@ const InvoiceLineItem = ({ index, line, products, onUpdate, onRemove }) => { | |||
|       /> | ||||
| 
 | ||||
|       {selectedProduct && ( | ||||
|         <> | ||||
|           <div style={{ fontSize: '0.9rem', color: '#666' }}> | ||||
|             <strong>{selectedProduct.name}</strong>: {selectedProduct.description} | ||||
|           </div> | ||||
|           <div style={{ fontSize: '0.9rem', color: '#333' }}> | ||||
|             <strong>Total con IVA:</strong> {totalConIVA.toFixed(2)} € | ||||
|           </div> | ||||
|         </> | ||||
|         <div style={{ fontSize: '0.9rem', color: '#666' }}> | ||||
|           <strong>{selectedProduct.name}</strong>: {selectedProduct.description} | ||||
|         </div> | ||||
|       )} | ||||
| 
 | ||||
|       <button className="btn btn-danger" onClick={() => onRemove(index)}>Eliminar producto</button> | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ 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'; | ||||
|  | @ -10,7 +11,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'; | ||||
|  | @ -27,9 +28,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 ( | ||||
|     <div className="main-layout"> | ||||
|  | @ -39,20 +40,27 @@ const MainLayout = () => { | |||
| 
 | ||||
|         <div className="content-area"> | ||||
|           <Routes> | ||||
|             {/* Dashboards */} | ||||
|             {/* Dashboards principales */} | ||||
|             <Route path="/admin/dashboard" element={<AdminDashboard />} /> | ||||
|             <Route path="/teacher/dashboard" element={<TeacherDashboard />} /> | ||||
|             <Route path="/student/dashboard" element={<StudentDashboard />} /> | ||||
| 
 | ||||
|             {/* User Management */} | ||||
|             {/* User Management - rutas específicas */} | ||||
|             <Route path="/admin/user-management/users/create" element={<UserCreateForm />} /> | ||||
|             <Route path="/admin/user-management/users/list" element={<UserList />} /> | ||||
|             <Route path="/admin/user-management/notifications/create" element={<NotificationCreateForm />} /> | ||||
|             <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/list" element={<StudentHistoryList />} /> | ||||
| 
 | ||||
|             {/* Class Management */} | ||||
|             {/* ContentArea general para secciones sin componentes específicos */} | ||||
|             <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/list" element={<TrainingGroupList />} /> | ||||
|             <Route path="/admin/class-management/training-groups/manage-students" element={<TrainingGroupStudentManager />} /> | ||||
|  | @ -63,17 +71,19 @@ const MainLayout = () => { | |||
|             <Route path="/admin/class-management/memberships/create" element={<MembershipForm />} /> | ||||
|             <Route path="/admin/class-management/memberships/list" element={<MembershipList />} /> | ||||
| 
 | ||||
|             {/* Finance */} | ||||
|             <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 />} /> | ||||
|             <Route path="/admin/finance/products/create" element={<ProductForm />} /> | ||||
|             <Route path="/admin/finance/products/list" element={<ProductList />} /> | ||||
|             <Route path="/admin/finance/ivatypes/create" element={<IVATypeManager />} /> | ||||
| 
 | ||||
|             {/* Perfil */} | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|             {/* Profile Page*/} | ||||
|             <Route path="/profile" element={<ProfilePage />} /> | ||||
| 
 | ||||
|           </Routes> | ||||
|         </div> | ||||
|       </div> | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
|   width: 320px; | ||||
|   background-color: #ffffff; | ||||
|   color: #2d3436; | ||||
|   padding: 0; | ||||
|   padding: 0; /* importante para evitar que el padding afecte la altura */ | ||||
|   box-shadow: 2px 0 10px rgba(0, 0, 0, 0.05); | ||||
|   height: 100vh; | ||||
|   display: flex; | ||||
|  | @ -44,5 +44,4 @@ | |||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   gap: 10px; | ||||
|   min-height: 0; | ||||
| } | ||||
		Loading…
	
		Reference in New Issue