Upload pdf + some more improvemnets, style changes and bug fixes
This commit is contained in:
parent
c8bc967399
commit
aa5c09a4f0
Binary file not shown.
Binary file not shown.
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 />} />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue