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 {
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));
if (saved == null) {
throw new DuplicateEntityException("Payment already exists for invoice ID: " + dto.getInvoiceId());
}
return new ResponseEntity<>(new PaymentDTO(saved), HttpStatus.CREATED);
}
@ -46,8 +53,15 @@ public class PaymentController {
throws EntityNotFoundException, InvalidDataException {
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));
if (updated == null) {
throw new InvalidDataException("Payment update failed for invoice ID: " + dto.getInvoiceId());
}
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);
invoice.setPayment(savedPayment);
//invoice.setPayment(savedPayment);
invoice.setStatus(StatusValues.PAID);
invoiceService.update(invoice);
@ -58,14 +58,28 @@ public class PaymentService extends AbstractService<Payment, Integer> {
}
@Override
@Transactional
public Payment update(Payment entity) throws EntityNotFoundException, InvalidDataException {
logger.info("Updating payment: {}", entity);
validate(entity);
updateInvoiceStatus(entity);
return super.update(entity);
public Payment update(Payment payment) throws EntityNotFoundException, InvalidDataException {
logger.info("Updating payment: {}", payment);
validate(payment);
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
@ -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
public void removePayment(Integer paymentId) {
Payment payment = findById(paymentId);
if (payment == null) {
throw new EntityNotFoundException("The payment doesn't exist.");
}
Invoice invoice = payment.getInvoice();
super.deleteById(paymentId);
invoice.setStatus(StatusValues.NOT_PAID);
invoiceService.update(invoice);
}

Binary file not shown.

View File

@ -1,35 +1,39 @@
import React, { useEffect, useState } from "react";
import api from "../../api/axiosConfig";
import ErrorMessage from "../common/ErrorMessage";
import "../styles/ContentArea.css";
const IVATypeManager = () => {
const [ivaTypes, setIvaTypes] = useState([]);
const [newIva, setNewIva] = useState({ percentage: "", description: "" });
const [error, setError] = useState("");
const [errorMsg, setErrorMsg] = useState("");
const [successMsg, setSuccessMsg] = useState("");
useEffect(() => {
fetchIvaTypes();
}, []);
const fetchIvaTypes = async () => {
try {
const res = await api.get("/iva-types/getAll");
setIvaTypes(res.data);
setErrorMsg("");
} catch (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 { name, value } = e.target;
setNewIva((prev) => ({ ...prev, [name]: value }));
};
const handleAddIva = async () => {
setError("");
setErrorMsg("");
setSuccessMsg("");
if (!newIva.percentage || isNaN(newIva.percentage)) {
setError("Introduce un porcentaje válido.");
setErrorMsg("Introduce un porcentaje válido.");
return;
}
@ -41,10 +45,11 @@ const IVATypeManager = () => {
await api.post("/iva-types/create", payload);
setNewIva({ percentage: "", description: "" });
setSuccessMsg("✅ Tipo de IVA añadido correctamente.");
fetchIvaTypes();
} catch (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;
try {
await api.delete(`/iva-types/deleteById/${id}`);
setSuccessMsg("✅ Tipo de IVA eliminado correctamente.");
fetchIvaTypes();
} catch (err) {
console.error(err);
setError("No se pudo eliminar el tipo de IVA.");
setErrorMsg("No se pudo eliminar el tipo de IVA.");
}
};
return (
<div className="content-area">
<div className="card">
<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">
<label>Porcentaje:</label>
<div className="form-inline" style={{ marginBottom: "1.5rem" }}>
<input
type="number"
name="percentage"
placeholder="Porcentaje (%)"
value={newIva.percentage}
onChange={handleChange}
step="0.01"
className="form-input"
/>
<label>Descripción:</label>
<input
type="text"
name="description"
placeholder="Descripción"
value={newIva.description}
onChange={handleChange}
className="form-input"
/>
<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>
<div className="table-wrapper">
<table className="styled-table">
<thead>
<tr>
<th>ID</th>
<th>Porcentaje</th>
<th>Descripción</th>
<th>Acciones</th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{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>
);
};

View File

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

View File

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

View File

@ -24,7 +24,6 @@ import MembershipList from '../lists/MembershipList';
import InvoiceForm from '../forms/InvoiceForm';
import InvoiceList from '../lists/InvoiceList';
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';
@ -67,7 +66,6 @@ const MainLayout = () => {
<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 />} />

View File

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

View File

@ -202,20 +202,6 @@ ul li button:hover {
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 {
display: flex;
flex-direction: column;
@ -308,6 +294,83 @@ form textarea:focus {
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;
display: flex;
flex-direction: column;
overflow: hidden; /* Evita overflow exterior */
}
@ -15,6 +16,7 @@
font-size: 1.1rem;
font-weight: 600;
color: #0984e3;
margin: 0;
}
.sidebar button {
@ -40,9 +42,18 @@
.sidebar-scroll {
flex: 1;
overflow-y: auto;
padding: 0 20px 20px;
padding: 0 20px 80px; /* MÁS espacio inferior */
display: flex;
flex-direction: column;
gap: 10px;
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 navigate = useNavigate();
const options = [
{ title: ' Crear Usuario', route: '/admin/user-management/users/create' },
{ title: '📜 Listar Usuarios', route: '/admin/user-management/users/list' },
{ title: '🔔 Crear Notificación', route: '/admin/user-management/notifications/create' },
{ title: '🕓 Crear Historial', route: '/admin/user-management/student-history/create' },
{ title: '📜 Ver Historial', route: '/admin/user-management/student-history/list' },
{ title: '🎓 Gestión Estudiantes', route: '/admin/user-management/students' },
{ title: '👨‍🏫 Gestión Profesores', route: '/admin/user-management/teachers' },
{ title: '🛡️ Gestión Admins', route: '/admin/user-management/admins' },
{ title: '📚 Gestión de Clases', route: '/admin/class-management' },
{ title: '💰 Finanzas', route: '/admin/finance' }
const sections = [
{
title: '👥 Administración de Usuarios',
options: [
{ title: ' Crear Usuario', route: '/admin/user-management/users/create' },
{ title: '📋 Ver Usuarios', route: '/admin/user-management/users/list' },
{ title: '🔔 Crear Notificación', route: '/admin/user-management/notifications/create' },
{ title: '📨 Ver Notificaciones', route: '/admin/user-management/notifications/list' },
{ title: '🕒 Crear Historial', route: '/admin/user-management/student-history/create' },
{ title: '📜 Ver Historial', route: '/admin/user-management/student-history/list' },
]
},
{
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 (
<div className="dashboard">
<h2>Panel de Control del Administrador</h2>
<div className="card-grid">
{options.map((opt, index) => (
<div key={index} className="dashboard-card" onClick={() => navigate(opt.route)}>
{opt.title}
{sections.map((section, index) => (
<div key={index} className="dashboard-section">
<h3>{section.title}</h3>
<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>
);
};