Upload pdf + some more improvemnets, style changes and bug fixes

This commit is contained in:
Dennis Eckerskorn 2025-05-28 00:34:00 +02:00
parent c8bc967399
commit aa5c09a4f0
15 changed files with 531 additions and 400 deletions

Binary file not shown.

View File

@ -34,8 +34,15 @@ public class PaymentController {
throws DuplicateEntityException, EntityNotFoundException { throws DuplicateEntityException, EntityNotFoundException {
Invoice invoice = paymentService.getInvoiceById(dto.getInvoiceId()); Invoice invoice = paymentService.getInvoiceById(dto.getInvoiceId());
if (invoice == null) {
throw new EntityNotFoundException("Invoice not found with ID: " + dto.getInvoiceId());
}
Payment saved = paymentService.save(dto.toEntityWithInvoice(invoice)); Payment saved = paymentService.save(dto.toEntityWithInvoice(invoice));
if (saved == null) {
throw new DuplicateEntityException("Payment already exists for invoice ID: " + dto.getInvoiceId());
}
return new ResponseEntity<>(new PaymentDTO(saved), HttpStatus.CREATED); return new ResponseEntity<>(new PaymentDTO(saved), HttpStatus.CREATED);
} }
@ -46,8 +53,15 @@ public class PaymentController {
throws EntityNotFoundException, InvalidDataException { throws EntityNotFoundException, InvalidDataException {
Invoice invoice = paymentService.getInvoiceById(dto.getInvoiceId()); Invoice invoice = paymentService.getInvoiceById(dto.getInvoiceId());
if (invoice == null) {
throw new EntityNotFoundException("Invoice not found with ID: " + dto.getInvoiceId());
}
Payment updated = paymentService.update(dto.toEntityWithInvoice(invoice)); Payment updated = paymentService.update(dto.toEntityWithInvoice(invoice));
if (updated == null) {
throw new InvalidDataException("Payment update failed for invoice ID: " + dto.getInvoiceId());
}
return new ResponseEntity<>(new PaymentDTO(updated), HttpStatus.OK); return new ResponseEntity<>(new PaymentDTO(updated), HttpStatus.OK);
} }

View File

@ -50,7 +50,7 @@ public class PaymentService extends AbstractService<Payment, Integer> {
Payment savedPayment = super.save(payment); Payment savedPayment = super.save(payment);
invoice.setPayment(savedPayment); //invoice.setPayment(savedPayment);
invoice.setStatus(StatusValues.PAID); invoice.setStatus(StatusValues.PAID);
invoiceService.update(invoice); invoiceService.update(invoice);
@ -58,14 +58,28 @@ public class PaymentService extends AbstractService<Payment, Integer> {
} }
@Override @Override
@Transactional @Transactional
public Payment update(Payment entity) throws EntityNotFoundException, InvalidDataException { public Payment update(Payment payment) throws EntityNotFoundException, InvalidDataException {
logger.info("Updating payment: {}", entity); logger.info("Updating payment: {}", payment);
validate(entity); validate(payment);
updateInvoiceStatus(entity);
return super.update(entity); Payment existingPayment = findById(payment.getId());
if (existingPayment == null) {
throw new EntityNotFoundException("Payment not found");
}
Invoice invoice = payment.getInvoice();
if (invoice == null || invoiceService.findById(invoice.getId()) == null) {
throw new EntityNotFoundException("Invoice not found for the payment");
}
invoice.setStatus(StatusValues.PAID);
invoiceService.update(invoice);
return super.update(payment);
} }
@Override @Override
@ -102,18 +116,17 @@ public class PaymentService extends AbstractService<Payment, Integer> {
} }
} }
@Transactional
private void updateInvoiceStatus(Payment payment) {
Invoice invoice = payment.getInvoice();
invoice.setStatus(StatusValues.PAID);
invoiceService.update(invoice);
}
@Transactional @Transactional
public void removePayment(Integer paymentId) { public void removePayment(Integer paymentId) {
Payment payment = findById(paymentId); Payment payment = findById(paymentId);
if (payment == null) {
throw new EntityNotFoundException("The payment doesn't exist.");
}
Invoice invoice = payment.getInvoice(); Invoice invoice = payment.getInvoice();
super.deleteById(paymentId); super.deleteById(paymentId);
invoice.setStatus(StatusValues.NOT_PAID); invoice.setStatus(StatusValues.NOT_PAID);
invoiceService.update(invoice); invoiceService.update(invoice);
} }

Binary file not shown.

View File

@ -1,35 +1,39 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import api from "../../api/axiosConfig"; import api from "../../api/axiosConfig";
import ErrorMessage from "../common/ErrorMessage";
import "../styles/ContentArea.css";
const IVATypeManager = () => { const IVATypeManager = () => {
const [ivaTypes, setIvaTypes] = useState([]); const [ivaTypes, setIvaTypes] = useState([]);
const [newIva, setNewIva] = useState({ percentage: "", description: "" }); const [newIva, setNewIva] = useState({ percentage: "", description: "" });
const [error, setError] = useState(""); const [errorMsg, setErrorMsg] = useState("");
const [successMsg, setSuccessMsg] = useState("");
useEffect(() => {
fetchIvaTypes();
}, []);
const fetchIvaTypes = async () => { const fetchIvaTypes = async () => {
try { try {
const res = await api.get("/iva-types/getAll"); const res = await api.get("/iva-types/getAll");
setIvaTypes(res.data); setIvaTypes(res.data);
setErrorMsg("");
} catch (err) { } catch (err) {
console.error(err); console.error(err);
setError("Error al obtener los tipos de IVA."); setErrorMsg("Error al obtener los tipos de IVA.");
} }
}; };
useEffect(() => {
fetchIvaTypes();
}, []);
const handleChange = (e) => { const handleChange = (e) => {
const { name, value } = e.target; const { name, value } = e.target;
setNewIva((prev) => ({ ...prev, [name]: value })); setNewIva((prev) => ({ ...prev, [name]: value }));
}; };
const handleAddIva = async () => { const handleAddIva = async () => {
setError(""); setErrorMsg("");
setSuccessMsg("");
if (!newIva.percentage || isNaN(newIva.percentage)) { if (!newIva.percentage || isNaN(newIva.percentage)) {
setError("Introduce un porcentaje válido."); setErrorMsg("Introduce un porcentaje válido.");
return; return;
} }
@ -41,10 +45,11 @@ const IVATypeManager = () => {
await api.post("/iva-types/create", payload); await api.post("/iva-types/create", payload);
setNewIva({ percentage: "", description: "" }); setNewIva({ percentage: "", description: "" });
setSuccessMsg("✅ Tipo de IVA añadido correctamente.");
fetchIvaTypes(); fetchIvaTypes();
} catch (err) { } catch (err) {
console.error(err); console.error(err);
setError("No se pudo crear el tipo de IVA."); setErrorMsg("No se pudo crear el tipo de IVA.");
} }
}; };
@ -52,66 +57,70 @@ const IVATypeManager = () => {
if (!window.confirm("¿Seguro que deseas eliminar este tipo de IVA?")) return; if (!window.confirm("¿Seguro que deseas eliminar este tipo de IVA?")) return;
try { try {
await api.delete(`/iva-types/deleteById/${id}`); await api.delete(`/iva-types/deleteById/${id}`);
setSuccessMsg("✅ Tipo de IVA eliminado correctamente.");
fetchIvaTypes(); fetchIvaTypes();
} catch (err) { } catch (err) {
console.error(err); console.error(err);
setError("No se pudo eliminar el tipo de IVA."); setErrorMsg("No se pudo eliminar el tipo de IVA.");
} }
}; };
return ( return (
<div className="content-area"> <div className="card">
<h2>Gestión de Tipos de IVA</h2> <h2>Gestión de Tipos de IVA</h2>
{error && <p style={{ color: "red" }}>{error}</p>} <ErrorMessage message={errorMsg} type="error" />
<ErrorMessage message={successMsg} type="success" />
<div className="form-section"> <div className="form-inline" style={{ marginBottom: "1.5rem" }}>
<label>Porcentaje:</label>
<input <input
type="number" type="number"
name="percentage" name="percentage"
placeholder="Porcentaje (%)"
value={newIva.percentage} value={newIva.percentage}
onChange={handleChange} onChange={handleChange}
step="0.01" step="0.01"
className="form-input"
/> />
<label>Descripción:</label>
<input <input
type="text" type="text"
name="description" name="description"
placeholder="Descripción"
value={newIva.description} value={newIva.description}
onChange={handleChange} onChange={handleChange}
className="form-input"
/> />
<button className="btn btn-primary" onClick={handleAddIva}> <button className="btn btn-primary" onClick={handleAddIva}>
+ Añadir IVA + Añadir IVA
</button> </button>
</div> </div>
<table className="table" style={{ marginTop: "2rem" }}> <div className="table-wrapper">
<thead> <table className="styled-table">
<tr> <thead>
<th>ID</th> <tr>
<th>Porcentaje</th> <th>ID</th>
<th>Descripción</th> <th>Porcentaje</th>
<th>Acciones</th> <th>Descripción</th>
</tr> <th>Acciones</th>
</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> </tr>
))} </thead>
</tbody> <tbody>
</table> {ivaTypes.map((iva) => (
<tr key={iva.id}>
<td>{iva.id}</td>
<td>{iva.percentage}%</td>
<td>{iva.description}</td>
<td>
<button className="delete-btn" onClick={() => handleDelete(iva.id)}>
🗑
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div> </div>
); );
}; };

View File

@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react";
import InvoiceLineItem from "./InvoiceLineItem"; import InvoiceLineItem from "./InvoiceLineItem";
import ErrorMessage from "../common/ErrorMessage"; import ErrorMessage from "../common/ErrorMessage";
import api from "../../api/axiosConfig"; import api from "../../api/axiosConfig";
import "../styles/ContentArea.css";
const InvoiceForm = () => { const InvoiceForm = () => {
const [students, setStudents] = useState([]); const [students, setStudents] = useState([]);
@ -17,9 +18,7 @@ const InvoiceForm = () => {
api.get("/products-services/getAll").then((res) => setProducts(res.data)); api.get("/products-services/getAll").then((res) => setProducts(res.data));
}, []); }, []);
const addLine = () => { const addLine = () => setLines([...lines, { productServiceId: "", quantity: 1 }]);
setLines([...lines, { productServiceId: "", quantity: 1 }]);
};
const updateLine = (index, updatedLine) => { const updateLine = (index, updatedLine) => {
const newLines = [...lines]; const newLines = [...lines];
@ -31,27 +30,21 @@ const InvoiceForm = () => {
setLines(lines.filter((_, i) => i !== index)); setLines(lines.filter((_, i) => i !== index));
}; };
const calculateSubtotal = () => { const calculateSubtotal = () =>
return lines.reduce((acc, line) => { 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 product ? acc + product.price * (parseInt(line.quantity) || 1) : acc;
return acc + product.price * (parseInt(line.quantity) || 1);
}, 0); }, 0);
};
const calculateIVA = () => { const calculateIVA = () =>
return lines.reduce((acc, line) => { 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 || !product.ivaType) return acc; if (!product || !product.ivaType) return acc;
const quantity = parseInt(line.quantity) || 1; const quantity = parseInt(line.quantity) || 1;
const iva = product.ivaType.percentage || 0; return acc + (product.price * quantity * (product.ivaType.percentage / 100));
return acc + (product.price * quantity * (iva / 100));
}, 0); }, 0);
};
const calculateTotal = () => { const calculateTotal = () => calculateSubtotal() + calculateIVA();
return calculateSubtotal() + calculateIVA();
};
const createInvoice = async () => { const createInvoice = async () => {
setError(""); setError("");
@ -84,7 +77,6 @@ const InvoiceForm = () => {
setCreatedInvoice(res.data); setCreatedInvoice(res.data);
alert("✅ Factura creada correctamente."); alert("✅ Factura creada correctamente.");
} catch (err) { } catch (err) {
console.error(err);
setError("❌ Error al crear la factura: " + (err.response?.data?.message || err.message)); setError("❌ Error al crear la factura: " + (err.response?.data?.message || err.message));
} }
}; };
@ -102,7 +94,6 @@ const InvoiceForm = () => {
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
} catch (err) { } catch (err) {
console.error(err);
setError("❌ Error al descargar el PDF."); setError("❌ Error al descargar el PDF.");
} }
}; };
@ -112,32 +103,30 @@ const InvoiceForm = () => {
<div className="card"> <div className="card">
<h2>Crear Factura</h2> <h2>Crear Factura</h2>
<label>Seleccionar estudiante:</label> <div className="form-column">
<select <label>Estudiante</label>
className="form-select" <select className="form-select" value={userId} onChange={(e) => setUserId(e.target.value)} required>
value={userId} <option value="">-- Selecciona un estudiante --</option>
onChange={(e) => setUserId(e.target.value)} {students.map((s) =>
> s.user ? (
<option value="">-- Selecciona un estudiante --</option> <option key={s.id} value={s.user.id}>
{students.map((s) => {s.user.name} {s.user.lastName} ({s.user.email})
s.user ? ( </option>
<option key={s.id} value={s.user.id}> ) : null
{s.user.name} {s.user.lastName} ({s.user.email}) )}
</option> </select>
) : null
)}
</select>
<label>Fecha de emisión:</label> <label>Fecha de emisión</label>
<input <input
className="form-input" className="form-input"
type="datetime-local" type="datetime-local"
value={date} value={date}
onChange={(e) => setDate(e.target.value)} onChange={(e) => setDate(e.target.value)}
/> required
/>
</div>
<h3>Productos / Servicios</h3> <h3>Productos / Servicios</h3>
{lines.map((line, i) => ( {lines.map((line, i) => (
<InvoiceLineItem <InvoiceLineItem
key={i} key={i}
@ -149,34 +138,33 @@ const InvoiceForm = () => {
/> />
))} ))}
<div style={{ marginTop: "1rem" }}> <div className="invoice-summary">
<h4>Subtotal: <strong>{calculateSubtotal().toFixed(2)} </strong></h4> <div>
<h4>IVA: <strong>{calculateIVA().toFixed(2)} </strong></h4> <h4>Subtotal:</h4>
<h4>Total estimado con IVA: <strong>{calculateTotal().toFixed(2)} </strong></h4> <p><strong>{calculateSubtotal().toFixed(2)} </strong></p>
</div>
<div>
<h4>IVA:</h4>
<p><strong>{calculateIVA().toFixed(2)} </strong></p>
</div>
<div>
<h4>Total con IVA:</h4>
<p><strong>{calculateTotal().toFixed(2)} </strong></p>
</div>
</div> </div>
<ErrorMessage message={error} /> <ErrorMessage message={error} />
<div style={{ marginTop: "1rem" }}> <div style={{ display: "flex", gap: "10px", marginTop: "1rem" }}>
<button className="btn btn-secondary" onClick={addLine}> <button className="btn btn-success" onClick={addLine}>+ Añadir Producto</button>
+ Añadir Producto <button className="btn btn-primary" onClick={createInvoice}>Crear Factura</button>
</button>
<button
className="btn btn-primary"
onClick={createInvoice}
style={{ marginLeft: "1rem" }}
>
Crear Factura
</button>
</div> </div>
{createdInvoice && ( {createdInvoice && (
<div style={{ marginTop: "2rem" }}> <div style={{ marginTop: "2rem" }}>
<h4>Factura creada: #{createdInvoice.id}</h4> <h4>Factura #{createdInvoice.id} creada correctamente</h4>
<h4>Total con IVA: <strong>{createdInvoice.total.toFixed(2)} </strong></h4> <p><strong>Total:</strong> {createdInvoice.total.toFixed(2)} </p>
<button className="btn btn-primary" onClick={downloadPdf}> <button className="btn btn-primary" onClick={downloadPdf}>📄 Descargar PDF</button>
📄 Descargar PDF
</button>
</div> </div>
)} )}
</div> </div>

View File

@ -8,47 +8,51 @@ 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 quantity = parseInt(line.quantity) || 1;
const price = selectedProduct?.price || 0; const price = selectedProduct?.price || 0;
const ivaPercentage = selectedProduct?.ivaType?.percentage || 0; const iva = selectedProduct?.ivaType?.percentage || 0;
const totalConIVA = price * quantity * (1 + ivaPercentage / 100); const totalConIVA = price * quantity * (1 + iva / 100);
return ( return (
<div className="content-area" style={{ marginBottom: "1rem", display: "flex", flexDirection: "column", gap: "0.5rem" }}> <div className="card" style={{ padding: "20px", marginBottom: "1rem" }}>
<select <div className="form-column">
name="productServiceId" <label>Producto</label>
value={line.productServiceId} <select
onChange={handleChange} name="productServiceId"
className="form-select" value={line.productServiceId}
> onChange={handleChange}
<option value="">Selecciona producto</option> className="form-select"
{products.map(p => ( required
<option key={p.id} value={p.id}> >
{p.name} - {p.price} ({p.type}) <option value="">Selecciona producto</option>
</option> {products.map((p) => (
))} <option key={p.id} value={p.id}>
</select> {p.name} - {p.price} ({p.type})
</option>
))}
</select>
<input <label>Cantidad</label>
type="number" <input
name="quantity" type="number"
min="1" name="quantity"
value={line.quantity} min="1"
onChange={handleChange} value={line.quantity}
className="form-input" onChange={handleChange}
placeholder="Cantidad" className="form-input"
/> required
/>
{selectedProduct && ( {selectedProduct && (
<> <div className="invoice-line-summary">
<div style={{ fontSize: '0.9rem', color: '#666' }}> <p><strong>{selectedProduct.name}</strong>: {selectedProduct.description}</p>
<strong>{selectedProduct.name}</strong>: {selectedProduct.description} <p><strong>IVA:</strong> {iva}%</p>
<p><strong>Total con IVA:</strong> {totalConIVA.toFixed(2)} </p>
</div> </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>
</div>
</div> </div>
); );
}; };

View File

@ -88,81 +88,82 @@ const PaymentForm = () => {
<div className="card"> <div className="card">
<h2>Registrar Pago de una Factura</h2> <h2>Registrar Pago de una Factura</h2>
<label>Seleccionar estudiante:</label> <form className="form-column" onSubmit={handleSubmit}>
<select <label>Seleccionar estudiante:</label>
className="form-select" <select
value={selectedUserId} className="form-select"
onChange={handleStudentChange} value={selectedUserId}
> onChange={handleStudentChange}
<option value="">-- Selecciona un estudiante --</option> required
{students.map((s) => >
s.user ? ( <option value="">-- Selecciona un estudiante --</option>
<option key={s.id} value={s.user.id}> {students.map((s) =>
{s.user.name} {s.user.lastName} ({s.user.email}) s.user ? (
</option> <option key={s.id} value={s.user.id}>
) : null {s.user.name} {s.user.lastName} ({s.user.email})
)}
</select>
{invoices.length > 0 && (
<>
<label>Seleccionar factura:</label>
<select
className="form-select"
value={selectedInvoiceId}
onChange={(e) => setSelectedInvoiceId(e.target.value)}
>
<option value="">-- Selecciona una factura --</option>
{invoices.map((inv) => (
<option key={inv.id} value={inv.id}>
#{inv.id} - {new Date(inv.date).toLocaleDateString()} - Total:{" "}
{inv.total.toFixed(2)}
</option> </option>
))} ) : null
</select> )}
</> </select>
)}
{invoices.length === 0 && selectedUserId && ( {invoices.length > 0 && (
<p style={{ marginTop: "1rem", color: "gray" }}> <>
No hay facturas pendientes de pago para este estudiante. <label>Seleccionar factura:</label>
</p> <select
)} className="form-select"
value={selectedInvoiceId}
onChange={(e) => setSelectedInvoiceId(e.target.value)}
required
>
<option value="">-- Selecciona una factura --</option>
{invoices.map((inv) => (
<option key={inv.id} value={inv.id}>
#{inv.id} - {new Date(inv.date).toLocaleDateString()} - Total: {inv.total.toFixed(2)}
</option>
))}
</select>
</>
)}
{selectedInvoiceId && ( {invoices.length === 0 && selectedUserId && (
<> <p className="text-muted">
<label>Importe pagado ():</label> No hay facturas pendientes de pago para este estudiante.
<input </p>
className="form-input" )}
type="number"
step="0.01"
value={amount}
onChange={(e) => setAmount(e.target.value)}
/>
<label>Método de pago:</label> {selectedInvoiceId && (
<select <>
className="form-select" <label>Importe pagado ():</label>
value={paymentMethod} <input
onChange={(e) => setPaymentMethod(e.target.value)} className="form-input"
> type="number"
<option value="CASH">Efectivo</option> step="0.01"
<option value="CREDIT_CARD">Tarjeta</option> value={amount}
<option value="BANK_TRANSFER">Transferencia</option> onChange={(e) => setAmount(e.target.value)}
</select> required
/>
<ErrorMessage message={error} type="error" /> <label>Método de pago:</label>
<ErrorMessage message={success} type="success" /> <select
className="form-select"
value={paymentMethod}
onChange={(e) => setPaymentMethod(e.target.value)}
required
>
<option value="CASH">Efectivo</option>
<option value="CREDIT_CARD">Tarjeta</option>
<option value="BANK_TRANSFER">Transferencia</option>
</select>
<button <ErrorMessage message={error} type="error" />
className="btn btn-primary" <ErrorMessage message={success} type="success" />
style={{ marginTop: "1rem" }}
onClick={handleSubmit} <button type="submit" className="btn btn-primary">
> 💳 Confirmar Pago
💳 Confirmar Pago </button>
</button> </>
</> )}
)} </form>
</div> </div>
</div> </div>
); );

View File

@ -24,7 +24,6 @@ import MembershipList from '../lists/MembershipList';
import InvoiceForm from '../forms/InvoiceForm'; import InvoiceForm from '../forms/InvoiceForm';
import InvoiceList from '../lists/InvoiceList'; import InvoiceList from '../lists/InvoiceList';
import PaymentForm from '../forms/PaymentForm'; import PaymentForm from '../forms/PaymentForm';
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 IVATypeManager from '../forms/IVATypeManager';
@ -67,7 +66,6 @@ const MainLayout = () => {
<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/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 />} /> <Route path="/admin/finance/ivatypes/create" element={<IVATypeManager />} />

View File

@ -40,7 +40,6 @@ const SidebarAdmin = () => {
<button onClick={() => navigate('/admin/finance/invoices/create')}>🧾 Crear Factura</button> <button onClick={() => navigate('/admin/finance/invoices/create')}>🧾 Crear Factura</button>
<button onClick={() => navigate('/admin/finance/invoices/list')}>📄 Ver Facturas</button> <button onClick={() => navigate('/admin/finance/invoices/list')}>📄 Ver Facturas</button>
<button onClick={() => navigate('/admin/finance/payments/create')}>📄 Nuevo Pago</button> <button onClick={() => navigate('/admin/finance/payments/create')}>📄 Nuevo Pago</button>
<button onClick={() => navigate('/admin/finance/payments/list')}>💳 Facturas Pagadas</button>
<button onClick={() => navigate('/admin/finance/products/create')}>🛒 Añadir Productos</button> <button onClick={() => navigate('/admin/finance/products/create')}>🛒 Añadir Productos</button>
<button onClick={() => navigate('/admin/finance/products/list')}>📦 Ver Productos</button> <button onClick={() => navigate('/admin/finance/products/list')}>📦 Ver Productos</button>
<button onClick={() => navigate('/admin/finance/ivatypes/create')}>💱 Añadir Tipo de IVA</button> <button onClick={() => navigate('/admin/finance/ivatypes/create')}>💱 Añadir Tipo de IVA</button>

View File

@ -1,5 +1,6 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import api from "../../api/axiosConfig"; import api from "../../api/axiosConfig";
import ErrorMessage from "../common/ErrorMessage";
import "../styles/ContentArea.css"; import "../styles/ContentArea.css";
const ProductList = () => { const ProductList = () => {
@ -7,29 +8,35 @@ const ProductList = () => {
const [ivaTypes, setIvaTypes] = useState([]); const [ivaTypes, setIvaTypes] = useState([]);
const [editIndex, setEditIndex] = useState(null); const [editIndex, setEditIndex] = useState(null);
const [editableProduct, setEditableProduct] = useState(null); const [editableProduct, setEditableProduct] = useState(null);
const [errorMsg, setErrorMsg] = useState("");
const fetchProducts = async () => { const [successMsg, setSuccessMsg] = useState("");
const res = await api.get("/products-services/getAll");
setProducts(res.data);
};
const fetchIvaTypes = async () => {
const res = await api.get("/iva-types/getAll");
setIvaTypes(res.data);
};
useEffect(() => { useEffect(() => {
fetchProducts(); fetchProducts();
fetchIvaTypes(); fetchIvaTypes();
}, []); }, []);
useEffect(() => { const fetchProducts = async () => {
console.log("IVA Types cargados:", ivaTypes); try {
}, [ivaTypes]); const res = await api.get("/products-services/getAll");
setProducts(res.data);
} catch (err) {
setErrorMsg("❌ Error al cargar los productos.");
}
};
const fetchIvaTypes = async () => {
try {
const res = await api.get("/iva-types/getAll");
setIvaTypes(res.data);
} catch (err) {
setErrorMsg("❌ Error al cargar tipos de IVA.");
}
};
const startEdit = (index) => { const startEdit = (index) => {
setEditIndex(index); setEditIndex(index);
setEditableProduct({ ...products[index] }); // contiene ivaTypeId directamente setEditableProduct({ ...products[index] });
}; };
const cancelEdit = () => { const cancelEdit = () => {
@ -47,152 +54,145 @@ const ProductList = () => {
const handleSave = async () => { const handleSave = async () => {
try { try {
const dto = { const dto = {
id: editableProduct.id, ...editableProduct,
name: editableProduct.name, price: parseFloat(editableProduct.price)
price: parseFloat(editableProduct.price),
type: editableProduct.type,
status: editableProduct.status,
ivaTypeId: editableProduct.ivaTypeId,
}; };
await api.put("/products-services/update", dto); await api.put("/products-services/update", dto);
alert("✅ Producto actualizado correctamente."); setSuccessMsg("✅ Producto actualizado correctamente.");
setEditIndex(null); setErrorMsg("");
setEditableProduct(null); cancelEdit();
fetchProducts(); fetchProducts();
} catch (err) { } catch (err) {
console.error(err); setErrorMsg("❌ Error al actualizar el producto.");
alert("❌ Error al actualizar el producto.");
} }
}; };
const handleDelete = async (id) => { const handleDelete = async (id) => {
if (window.confirm("¿Estás seguro de que deseas eliminar este producto?")) { if (window.confirm("¿Seguro que quieres eliminar este producto?")) {
await api.delete(`/products-services/deleteById/${id}`); try {
fetchProducts(); await api.delete(`/products-services/deleteById/${id}`);
fetchProducts();
setSuccessMsg("✅ Producto eliminado correctamente.");
setErrorMsg("");
} catch {
setErrorMsg("❌ Error al eliminar el producto.");
}
} }
}; };
const getIVADisplay = (ivaTypeId) => { const getIVADisplay = (ivaTypeId) => {
const iva = ivaTypes.find((i) => i.id === Number(ivaTypeId)); const iva = ivaTypes.find((i) => i.id === Number(ivaTypeId));
return iva ? `${iva.percentage}%` : "Sin IVA"; return iva?.percentage ? `${iva.percentage}%` : "";
}; };
return ( return (
<div className="content-area"> <div className="card">
<h2>Lista de Productos y Servicios</h2> <h2>Lista de Productos y Servicios</h2>
<ErrorMessage message={errorMsg} type="error" />
<ErrorMessage message={successMsg} type="success" />
<table className="table" style={{ marginTop: "2rem" }}> <div className="table-wrapper">
<thead> <table className="styled-table">
<tr> <thead>
<th>ID</th> <tr>
<th>Nombre</th> <th>Nombre</th>
<th>Precio</th> <th>Precio ()</th>
<th>IVA</th> <th>IVA</th>
<th>Tipo</th> <th>Tipo</th>
<th>Estado</th> <th>Estado</th>
<th>Acciones</th> <th>Acciones</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{products.map((p, index) => { {products.map((p, index) => {
const isEditing = index === editIndex; const isEditing = index === editIndex;
return ( return (
<tr key={p.id}> <tr key={p.id}>
<td>{p.id}</td> <td>
<td> {isEditing ? (
{isEditing ? ( <input
<input type="text"
type="text" value={editableProduct.name}
value={editableProduct.name} onChange={(e) => handleChange("name", e.target.value)}
onChange={(e) => handleChange("name", e.target.value)} />
/> ) : (
) : ( p.name
p.name )}
)} </td>
</td> <td>
<td> {isEditing ? (
{isEditing ? ( <input
<input type="number"
type="number" step="0.01"
value={editableProduct.price} value={editableProduct.price}
step="0.01" onChange={(e) => handleChange("price", e.target.value)}
onChange={(e) => handleChange("price", e.target.value)} />
/> ) : (
) : ( `${p.price.toFixed(2)}`
`${p.price.toFixed(2)}` )}
)} </td>
</td> <td>
<td> {isEditing ? (
{isEditing ? ( <select
<select value={editableProduct.ivaTypeId || ""}
value={editableProduct.ivaTypeId || ""} onChange={(e) => handleChange("ivaTypeId", e.target.value)}
onChange={(e) => handleChange("ivaTypeId", e.target.value)} >
> <option value="">-- IVA --</option>
<option value="">-- IVA --</option> {ivaTypes.map((iva) => (
{ivaTypes.map((iva) => ( <option key={iva.id} value={iva.id}>
<option key={iva.id} value={iva.id}> {iva.percentage}%
{iva.name} ({iva.percentage}%) </option>
</option> ))}
))} </select>
</select> ) : (
) : ( getIVADisplay(p.ivaTypeId)
getIVADisplay(p.ivaTypeId) )}
)} </td>
</td> <td>
<td> {isEditing ? (
{isEditing ? ( <select
<select value={editableProduct.type}
value={editableProduct.type} onChange={(e) => handleChange("type", e.target.value)}
onChange={(e) => handleChange("type", e.target.value)} >
> <option value="PRODUCT">Producto</option>
<option value="PRODUCT">Producto</option> <option value="SERVICE">Servicio</option>
<option value="SERVICE">Servicio</option> </select>
</select> ) : (
) : ( p.type
p.type )}
)} </td>
</td> <td>
<td> {isEditing ? (
{isEditing ? ( <select
<select value={editableProduct.status}
value={editableProduct.status} onChange={(e) => handleChange("status", e.target.value)}
onChange={(e) => handleChange("status", e.target.value)} >
> <option value="ACTIVE">Activo</option>
<option value="ACTIVE">Activo</option> <option value="INACTIVE">Inactivo</option>
<option value="INACTIVE">Inactivo</option> </select>
</select> ) : (
) : ( p.status
p.status )}
)} </td>
</td> <td>
<td> {isEditing ? (
{isEditing ? ( <>
<> <button className="edit-btn" onClick={handleSave}>💾</button>
<button className="btn btn-primary btn-sm" onClick={handleSave}> <button className="delete-btn" onClick={cancelEdit}></button>
💾 Guardar </>
</button> ) : (
<button className="btn btn-secondary btn-sm" onClick={cancelEdit}> <>
Cancelar <button className="edit-btn" onClick={() => startEdit(index)}></button>
</button> <button className="delete-btn" onClick={() => handleDelete(p.id)}>🗑</button>
</> </>
) : ( )}
<> </td>
<button className="btn btn-secondary btn-sm" onClick={() => startEdit(index)}> </tr>
Editar );
</button> })}
<button className="btn btn-danger btn-sm" onClick={() => handleDelete(p.id)}> </tbody>
🗑 Eliminar </table>
</button> </div>
</>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div> </div>
); );
}; };

View File

@ -202,20 +202,6 @@ ul li button:hover {
background-color: #c0392b; background-color: #c0392b;
} }
.add-button {
padding: 6px 12px;
border: none;
border-radius: 8px;
background-color: #2ecc71;
color: white;
cursor: pointer;
transition: background-color 0.3s;
}
.add-button:hover {
background-color: #27ae60;
}
.form-column { .form-column {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -308,6 +294,83 @@ form textarea:focus {
color: #7f8c8d; color: #7f8c8d;
} }
.invoice-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-top: 20px;
padding: 20px;
background-color: #f0f4f8;
border-radius: 12px;
}
.invoice-line-summary {
background-color: #ecf0f1;
padding: 12px;
border-radius: 10px;
margin-top: 10px;
margin-bottom: 10px;
font-size: 15px;
color: #2d3436;
}
/* Botones generales */
.add-button {
padding: 6px 12px;
border: none;
border-radius: 8px;
background-color: #2ecc71;
color: white;
cursor: pointer;
transition: background-color 0.3s;
}
.add-button:hover {
background-color: #27ae60;
}
.btn {
padding: 6px 12px;
border: none;
border-radius: 8px;
font-weight: bold;
cursor: pointer;
transition: background-color 0.3s, transform 0.2s;
}
/* Botón primario (azul) */
.btn-primary {
background-color: #0984e3;
color: white;
}
.btn-primary:hover {
background-color: #74b9ff;
transform: translateY(-2px);
}
/* Botón de peligro (rojo) */
.btn-danger {
background-color: #e74c3c;
color: white;
}
.btn-danger:hover {
background-color: #c0392b;
transform: translateY(-2px);
}
/* Botón de éxito (verde) */
.btn-success {
background-color: #2ecc71;
color: white;
}
.btn-success:hover {
background-color: #27ae60;
transform: translateY(-2px);
}

View File

@ -7,6 +7,7 @@
height: 100vh; height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; /* Evita overflow exterior */
} }
@ -15,6 +16,7 @@
font-size: 1.1rem; font-size: 1.1rem;
font-weight: 600; font-weight: 600;
color: #0984e3; color: #0984e3;
margin: 0;
} }
.sidebar button { .sidebar button {
@ -40,9 +42,18 @@
.sidebar-scroll { .sidebar-scroll {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 0 20px 20px; padding: 0 20px 80px; /* MÁS espacio inferior */
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
min-height: 0; min-height: 0;
} }
/* Opcional: para asegurar buen scroll en todos los navegadores */
.sidebar-scroll::-webkit-scrollbar {
width: 6px;
}
.sidebar-scroll::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.2);
border-radius: 4px;
}

View File

@ -5,29 +5,60 @@ import '../../components/styles/DashboardCards.css';
const AdminDashboard = () => { const AdminDashboard = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const options = [ const sections = [
{ title: ' Crear Usuario', route: '/admin/user-management/users/create' }, {
{ title: '📜 Listar Usuarios', route: '/admin/user-management/users/list' }, title: '👥 Administración de Usuarios',
{ title: '🔔 Crear Notificación', route: '/admin/user-management/notifications/create' }, options: [
{ title: '🕓 Crear Historial', route: '/admin/user-management/student-history/create' }, { title: ' Crear Usuario', route: '/admin/user-management/users/create' },
{ title: '📜 Ver Historial', route: '/admin/user-management/student-history/list' }, { title: '📋 Ver Usuarios', route: '/admin/user-management/users/list' },
{ title: '🎓 Gestión Estudiantes', route: '/admin/user-management/students' }, { title: '🔔 Crear Notificación', route: '/admin/user-management/notifications/create' },
{ title: '👨‍🏫 Gestión Profesores', route: '/admin/user-management/teachers' }, { title: '📨 Ver Notificaciones', route: '/admin/user-management/notifications/list' },
{ title: '🛡️ Gestión Admins', route: '/admin/user-management/admins' }, { title: '🕒 Crear Historial', route: '/admin/user-management/student-history/create' },
{ title: '📚 Gestión de Clases', route: '/admin/class-management' }, { title: '📜 Ver Historial', route: '/admin/user-management/student-history/list' },
{ title: '💰 Finanzas', route: '/admin/finance' } ]
},
{
title: '📚 Administración de Clases',
options: [
{ title: ' Crear Grupo', route: '/admin/class-management/training-groups/create' },
{ title: '👥 Ver Grupos', route: '/admin/class-management/training-groups/list' },
{ title: '🧑‍🏫 Administrar Grupos', route: '/admin/class-management/training-groups/manage-students' },
{ title: '🗓️ Ver Horario', route: '/admin/class-management/training-groups/view-timetable' },
{ title: '📆 Ver Sesiones', route: '/admin/class-management/training-session/list' },
{ title: '📝 Registrar Asistencia', route: '/admin/class-management/assistance/create' },
{ title: '📋 Ver Asistencias', route: '/admin/class-management/assistance/list' },
{ title: ' Detalles de las Membresías', route: '/admin/class-management/memberships/details' },
{ title: '🏷️ Asignar Membresías', route: '/admin/class-management/memberships/list' },
]
},
{
title: '💵 Finanzas',
options: [
{ title: '🧾 Crear Factura', route: '/admin/finance/invoices/create' },
{ title: '📄 Ver Facturas', route: '/admin/finance/invoices/list' },
{ title: '📄 Nuevo Pago', route: '/admin/finance/payments/create' },
{ title: '🛒 Añadir Productos', route: '/admin/finance/products/create' },
{ title: '📦 Ver Productos', route: '/admin/finance/products/list' },
{ title: '💱 Añadir Tipo de IVA', route: '/admin/finance/ivatypes/create' },
]
},
]; ];
return ( return (
<div className="dashboard"> <div className="dashboard">
<h2>Panel de Control del Administrador</h2> <h2>Panel de Control del Administrador</h2>
<div className="card-grid"> {sections.map((section, index) => (
{options.map((opt, index) => ( <div key={index} className="dashboard-section">
<div key={index} className="dashboard-card" onClick={() => navigate(opt.route)}> <h3>{section.title}</h3>
{opt.title} <div className="card-grid">
{section.options.map((opt, idx) => (
<div key={idx} className="dashboard-card" onClick={() => navigate(opt.route)}>
{opt.title}
</div>
))}
</div> </div>
))} </div>
</div> ))}
</div> </div>
); );
}; };