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 {
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
@ -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,43 +57,46 @@ 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">
|
||||||
|
<table className="styled-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
|
|
@ -104,8 +112,8 @@ const IVATypeManager = () => {
|
||||||
<td>{iva.percentage}%</td>
|
<td>{iva.percentage}%</td>
|
||||||
<td>{iva.description}</td>
|
<td>{iva.description}</td>
|
||||||
<td>
|
<td>
|
||||||
<button className="btn btn-danger btn-sm" onClick={() => handleDelete(iva.id)}>
|
<button className="delete-btn" onClick={() => handleDelete(iva.id)}>
|
||||||
🗑️ Eliminar
|
🗑️
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -113,6 +121,7 @@ const IVATypeManager = () => {
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,12 +103,9 @@ 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}
|
|
||||||
onChange={(e) => setUserId(e.target.value)}
|
|
||||||
>
|
|
||||||
<option value="">-- Selecciona un estudiante --</option>
|
<option value="">-- Selecciona un estudiante --</option>
|
||||||
{students.map((s) =>
|
{students.map((s) =>
|
||||||
s.user ? (
|
s.user ? (
|
||||||
|
|
@ -128,16 +116,17 @@ const InvoiceForm = () => {
|
||||||
)}
|
)}
|
||||||
</select>
|
</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>
|
||||||
|
|
|
||||||
|
|
@ -8,25 +8,29 @@ 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" }}>
|
||||||
|
<div className="form-column">
|
||||||
|
<label>Producto</label>
|
||||||
<select
|
<select
|
||||||
name="productServiceId"
|
name="productServiceId"
|
||||||
value={line.productServiceId}
|
value={line.productServiceId}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="form-select"
|
className="form-select"
|
||||||
|
required
|
||||||
>
|
>
|
||||||
<option value="">Selecciona producto</option>
|
<option value="">Selecciona producto</option>
|
||||||
{products.map(p => (
|
{products.map((p) => (
|
||||||
<option key={p.id} value={p.id}>
|
<option key={p.id} value={p.id}>
|
||||||
{p.name} - {p.price}€ ({p.type})
|
{p.name} - {p.price}€ ({p.type})
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<label>Cantidad</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
name="quantity"
|
name="quantity"
|
||||||
|
|
@ -34,21 +38,21 @@ const InvoiceLineItem = ({ index, line, products, onUpdate, onRemove }) => {
|
||||||
value={line.quantity}
|
value={line.quantity}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="form-input"
|
className="form-input"
|
||||||
placeholder="Cantidad"
|
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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -88,11 +88,13 @@ const PaymentForm = () => {
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<h2>Registrar Pago de una Factura</h2>
|
<h2>Registrar Pago de una Factura</h2>
|
||||||
|
|
||||||
|
<form className="form-column" onSubmit={handleSubmit}>
|
||||||
<label>Seleccionar estudiante:</label>
|
<label>Seleccionar estudiante:</label>
|
||||||
<select
|
<select
|
||||||
className="form-select"
|
className="form-select"
|
||||||
value={selectedUserId}
|
value={selectedUserId}
|
||||||
onChange={handleStudentChange}
|
onChange={handleStudentChange}
|
||||||
|
required
|
||||||
>
|
>
|
||||||
<option value="">-- Selecciona un estudiante --</option>
|
<option value="">-- Selecciona un estudiante --</option>
|
||||||
{students.map((s) =>
|
{students.map((s) =>
|
||||||
|
|
@ -111,12 +113,12 @@ const PaymentForm = () => {
|
||||||
className="form-select"
|
className="form-select"
|
||||||
value={selectedInvoiceId}
|
value={selectedInvoiceId}
|
||||||
onChange={(e) => setSelectedInvoiceId(e.target.value)}
|
onChange={(e) => setSelectedInvoiceId(e.target.value)}
|
||||||
|
required
|
||||||
>
|
>
|
||||||
<option value="">-- Selecciona una factura --</option>
|
<option value="">-- Selecciona una factura --</option>
|
||||||
{invoices.map((inv) => (
|
{invoices.map((inv) => (
|
||||||
<option key={inv.id} value={inv.id}>
|
<option key={inv.id} value={inv.id}>
|
||||||
#{inv.id} - {new Date(inv.date).toLocaleDateString()} - Total:{" "}
|
#{inv.id} - {new Date(inv.date).toLocaleDateString()} - Total: {inv.total.toFixed(2)} €
|
||||||
{inv.total.toFixed(2)} €
|
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -124,7 +126,7 @@ const PaymentForm = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{invoices.length === 0 && selectedUserId && (
|
{invoices.length === 0 && selectedUserId && (
|
||||||
<p style={{ marginTop: "1rem", color: "gray" }}>
|
<p className="text-muted">
|
||||||
No hay facturas pendientes de pago para este estudiante.
|
No hay facturas pendientes de pago para este estudiante.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
@ -138,6 +140,7 @@ const PaymentForm = () => {
|
||||||
step="0.01"
|
step="0.01"
|
||||||
value={amount}
|
value={amount}
|
||||||
onChange={(e) => setAmount(e.target.value)}
|
onChange={(e) => setAmount(e.target.value)}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<label>Método de pago:</label>
|
<label>Método de pago:</label>
|
||||||
|
|
@ -145,6 +148,7 @@ const PaymentForm = () => {
|
||||||
className="form-select"
|
className="form-select"
|
||||||
value={paymentMethod}
|
value={paymentMethod}
|
||||||
onChange={(e) => setPaymentMethod(e.target.value)}
|
onChange={(e) => setPaymentMethod(e.target.value)}
|
||||||
|
required
|
||||||
>
|
>
|
||||||
<option value="CASH">Efectivo</option>
|
<option value="CASH">Efectivo</option>
|
||||||
<option value="CREDIT_CARD">Tarjeta</option>
|
<option value="CREDIT_CARD">Tarjeta</option>
|
||||||
|
|
@ -154,15 +158,12 @@ const PaymentForm = () => {
|
||||||
<ErrorMessage message={error} type="error" />
|
<ErrorMessage message={error} type="error" />
|
||||||
<ErrorMessage message={success} type="success" />
|
<ErrorMessage message={success} type="success" />
|
||||||
|
|
||||||
<button
|
<button type="submit" className="btn btn-primary">
|
||||||
className="btn btn-primary"
|
|
||||||
style={{ marginTop: "1rem" }}
|
|
||||||
onClick={handleSubmit}
|
|
||||||
>
|
|
||||||
💳 Confirmar Pago
|
💳 Confirmar Pago
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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 />} />
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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,48 +54,49 @@ 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?")) {
|
||||||
|
try {
|
||||||
await api.delete(`/products-services/deleteById/${id}`);
|
await api.delete(`/products-services/deleteById/${id}`);
|
||||||
fetchProducts();
|
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">
|
||||||
|
<table className="styled-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
|
||||||
<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>
|
||||||
|
|
@ -100,7 +108,6 @@ const ProductList = () => {
|
||||||
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
|
||||||
|
|
@ -116,8 +123,8 @@ const ProductList = () => {
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
value={editableProduct.price}
|
|
||||||
step="0.01"
|
step="0.01"
|
||||||
|
value={editableProduct.price}
|
||||||
onChange={(e) => handleChange("price", e.target.value)}
|
onChange={(e) => handleChange("price", e.target.value)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -133,7 +140,7 @@ const ProductList = () => {
|
||||||
<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.name} ({iva.percentage}%)
|
{iva.percentage}%
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
|
@ -170,21 +177,13 @@ const ProductList = () => {
|
||||||
<td>
|
<td>
|
||||||
{isEditing ? (
|
{isEditing ? (
|
||||||
<>
|
<>
|
||||||
<button className="btn btn-primary btn-sm" onClick={handleSave}>
|
<button className="edit-btn" onClick={handleSave}>💾</button>
|
||||||
💾 Guardar
|
<button className="delete-btn" onClick={cancelEdit}>❌</button>
|
||||||
</button>
|
|
||||||
<button className="btn btn-secondary btn-sm" onClick={cancelEdit}>
|
|
||||||
❌ Cancelar
|
|
||||||
</button>
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<button className="btn btn-secondary btn-sm" onClick={() => startEdit(index)}>
|
<button className="edit-btn" onClick={() => startEdit(index)}>✏️</button>
|
||||||
✏️ Editar
|
<button className="delete-btn" onClick={() => handleDelete(p.id)}>🗑️</button>
|
||||||
</button>
|
|
||||||
<button className="btn btn-danger btn-sm" onClick={() => handleDelete(p.id)}>
|
|
||||||
🗑️ Eliminar
|
|
||||||
</button>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -194,6 +193,7 @@ const ProductList = () => {
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -5,30 +5,61 @@ import '../../components/styles/DashboardCards.css';
|
||||||
const AdminDashboard = () => {
|
const AdminDashboard = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const options = [
|
const sections = [
|
||||||
|
{
|
||||||
|
title: '👥 Administración de Usuarios',
|
||||||
|
options: [
|
||||||
{ title: '➕ Crear Usuario', route: '/admin/user-management/users/create' },
|
{ title: '➕ Crear Usuario', route: '/admin/user-management/users/create' },
|
||||||
{ title: '📜 Listar Usuarios', route: '/admin/user-management/users/list' },
|
{ title: '📋 Ver Usuarios', route: '/admin/user-management/users/list' },
|
||||||
{ title: '🔔 Crear Notificación', route: '/admin/user-management/notifications/create' },
|
{ title: '🔔 Crear Notificación', route: '/admin/user-management/notifications/create' },
|
||||||
{ title: '🕓 Crear Historial', route: '/admin/user-management/student-history/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: '📜 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: '📚 Administración de Clases',
|
||||||
{ title: '💰 Finanzas', route: '/admin/finance' }
|
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>
|
||||||
|
{sections.map((section, index) => (
|
||||||
|
<div key={index} className="dashboard-section">
|
||||||
|
<h3>{section.title}</h3>
|
||||||
<div className="card-grid">
|
<div className="card-grid">
|
||||||
{options.map((opt, index) => (
|
{section.options.map((opt, idx) => (
|
||||||
<div key={index} className="dashboard-card" onClick={() => navigate(opt.route)}>
|
<div key={idx} className="dashboard-card" onClick={() => navigate(opt.route)}>
|
||||||
{opt.title}
|
{opt.title}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue