Improved style of several classes

This commit is contained in:
Dennis Eckerskorn 2025-05-23 20:02:09 +02:00
parent c5a98435b3
commit e5cc695fd9
9 changed files with 412 additions and 186 deletions

View File

@ -20,6 +20,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@ -89,28 +90,31 @@ public class TrainingGroupController {
return ResponseEntity.ok().build();
}
@Operation(summary = "Update an existing training group", description = "Update an existing training group with the specified ID")
@Operation(summary = "Actualizar un grupo de entrenamiento existente", description = "Actualiza un grupo de entrenamiento por su ID")
@Transactional
@PutMapping("/update/{id}")
public ResponseEntity<TrainingGroupDTO> update(@PathVariable Integer id, @RequestBody TrainingGroupDTO dto) {
dto.setId(id);
Teacher teacher = teacherService.findById(dto.getTeacherId());
Set<Student> students = dto.getStudentIds().stream()
Set<Integer> studentIds = dto.getStudentIds() != null ? dto.getStudentIds() : Collections.emptySet();
Set<Student> students = studentIds.stream()
.map(studentService::findById)
.collect(Collectors.toSet());
TrainingGroup updated = trainingGroupService.update(dto.toEntity(teacher, students));
TrainingGroup updatedGroup = trainingGroupService.update(dto.toEntity(teacher, students));
List<TrainingSession> sessions = updated.getTrainingSessions().stream().toList();
if (!sessions.isEmpty()) {
TrainingSession session = sessions.get(0);
session.setDate(updated.getSchedule());
updatedGroup.getTrainingSessions().stream().findFirst().ifPresent(session -> {
session.setDate(updatedGroup.getSchedule());
trainingSessionService.save(session);
}
});
return ResponseEntity.ok(new TrainingGroupDTO(updated));
return ResponseEntity.ok(new TrainingGroupDTO(updatedGroup));
}
@Operation(summary = "Find a training group by ID", description = "Retrieve a training group with the specified ID")
@GetMapping("findById/{id}")
public ResponseEntity<TrainingGroupDTO> findGroupById(@PathVariable Integer id) {

View File

@ -8,6 +8,7 @@ import com.fasterxml.jackson.annotation.JsonFormat;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
@ -18,7 +19,7 @@ public class TrainingGroupDTO {
private String level;
private LocalDateTime schedule;
private Integer teacherId;
private Set<Integer> studentIds;
private Set<Integer> studentIds = new HashSet<>();
public TrainingGroupDTO() {
}

View File

@ -17,7 +17,7 @@ const TrainingGroupForm = () => {
useEffect(() => {
api.get("/teachers/getAll")
.then((res) => setTeachers(res.data))
.catch((err) => console.error("Error loading teachers", err));
.catch((err) => console.error("Error al cargar profesores", err));
}, []);
const handleChange = (e) => {
@ -37,7 +37,7 @@ const TrainingGroupForm = () => {
schedule: formData.schedule,
teacherId: parseInt(formData.teacherId),
});
setSuccessMsg("Training group created successfully.");
setSuccessMsg("✅ Grupo de entrenamiento creado correctamente.");
setFormData({ name: "", level: "", schedule: "", teacherId: "" });
} catch (err) {
console.error("Error al crear grupo", err);
@ -47,25 +47,46 @@ const TrainingGroupForm = () => {
"❌ Error al crear el grupo. Verifica los datos.";
setErrorMsg(backendMsg);
}
};
return (
<div className="card">
<h2>Create Training Group</h2>
<form onSubmit={handleSubmit} className="form">
<label>Name:</label>
<input type="text" name="name" value={formData.name} onChange={handleChange} required />
<h2>Crear Grupo de Entrenamiento</h2>
<form onSubmit={handleSubmit} className="form-column">
<input
type="text"
name="name"
placeholder="Nombre del grupo"
value={formData.name}
onChange={handleChange}
required
/>
<label>Level:</label>
<input type="text" name="level" value={formData.level} onChange={handleChange} required />
<input
type="text"
name="level"
placeholder="Nivel"
value={formData.level}
onChange={handleChange}
required
/>
<label>Schedule:</label>
<input type="datetime-local" name="schedule" value={formData.schedule} onChange={handleChange} required />
<label>Horario del grupo:</label>
<input
type="datetime-local"
name="schedule"
value={formData.schedule}
onChange={handleChange}
required
/>
<label>Teacher:</label>
<select name="teacherId" value={formData.teacherId} onChange={handleChange} required>
<option value="">-- Select Teacher --</option>
<select
name="teacherId"
value={formData.teacherId}
onChange={handleChange}
required
>
<option value="">-- Selecciona un profesor responsable del grupo --</option>
{teachers.map((teacher) => (
<option key={teacher.id} value={teacher.id}>
{teacher.user?.name} {teacher.user?.surname}
@ -73,12 +94,11 @@ const TrainingGroupForm = () => {
))}
</select>
<button type="submit">Create Group</button>
<button type="submit">Crear grupo</button>
</form>
<ErrorMessage message={errorMsg} type="error" />
<ErrorMessage message={successMsg} type="success" />
</div>
);
};

View File

@ -27,9 +27,9 @@ const TrainingGroupStudentManager = () => {
const group = await api.get(`/training-groups/findById/${groupId}`);
setGroupStudents(group.data.studentIds || []);
} catch (err) {
console.error("Error fetching group students", err);
console.error("Error al cargar alumnos del grupo", err);
const msg =
err.response?.data?.message || "❌ Error al cargar datos del grupo.";
err.response?.data?.message || "❌ Error al cargar alumnos del grupo.";
setErrorMsg(msg);
setGroupStudents([]);
}
@ -40,7 +40,7 @@ const TrainingGroupStudentManager = () => {
await api.put(`/training-groups/assign-student`, null, {
params: {
groupId: selectedGroupId,
studentId: studentId,
studentId,
},
});
setSuccessMsg("✅ Alumno asignado correctamente.");
@ -60,15 +60,16 @@ const TrainingGroupStudentManager = () => {
await api.put(`/training-groups/remove-student`, null, {
params: {
groupId: selectedGroupId,
studentId: studentId,
studentId,
},
});
setSuccessMsg("Alumno eliminado del grupo.");
setSuccessMsg("Alumno eliminado del grupo.");
setErrorMsg("");
loadGroupStudents(selectedGroupId);
} catch (err) {
console.error(err);
const msg = err.response?.data?.message || "❌ Error al eliminar alumno.";
const msg =
err.response?.data?.message || "❌ Error al eliminar el alumno.";
setErrorMsg(msg);
setSuccessMsg("");
}
@ -77,11 +78,9 @@ const TrainingGroupStudentManager = () => {
return (
<div className="content-area">
<div className="card">
<h2>Asignar Alumnos al Grupo de Entrenamiento</h2>
<h2>👥 Gestión de Alumnos por Grupo</h2>
<label htmlFor="groupSelect" style={{ fontWeight: "bold" }}>
🏷 Selecciona un grupo:
</label>
<label htmlFor="groupSelect">🏷 Selecciona un grupo:</label>
<select
id="groupSelect"
value={selectedGroupId}
@ -100,34 +99,42 @@ const TrainingGroupStudentManager = () => {
{selectedGroupId && (
<>
<hr style={{ margin: "20px 0" }} />
<h3>📋 Alumnos Asignados</h3>
<ul>
{groupStudents.length === 0 && <li>No hay alumnos asignados.</li>}
{groupStudents.map((id) => {
const student = students.find((s) => s.id === id);
return (
<li key={id}>
{student?.user?.name} {student?.user?.surname}
<button onClick={() => handleRemove(id)}>
Eliminar del grupo
</button>
</li>
);
})}
</ul>
{groupStudents.length === 0 ? (
<p>No hay alumnos asignados a este grupo.</p>
) : (
<ul className="group-student-list">
{groupStudents.map((id) => {
const student = students.find((s) => s.id === id);
return (
<li key={id}>
🎓 {student?.user?.name} {student?.user?.surname}
<button
className="delete-button"
onClick={() => handleRemove(id)}
>
Eliminar
</button>
</li>
);
})}
</ul>
)}
<h3> Añadir Alumno</h3>
<ul>
<hr style={{ margin: "20px 0" }} />
<h3> Añadir Alumnos Disponibles</h3>
<ul className="group-student-list">
{students
.filter((s) => !groupStudents.includes(s.id))
.map((s) => (
<li key={s.id}>
{s.user?.name} {s.user?.surname}
👤 {s.user?.name} {s.user?.surname}
<button
className="add-button"
onClick={() => handleAssign(s.id)}
>
Añadir al grupo
Añadir
</button>
</li>
))}

View File

@ -1,6 +1,7 @@
import React, { useEffect, useState } from "react";
import api from "../../api/axiosConfig";
import "../styles/ContentArea.css";
import "../styles/Timetable.css";
const ViewTimetable = () => {
const [groups, setGroups] = useState([]);
@ -9,7 +10,7 @@ const ViewTimetable = () => {
api.get("/training-groups/getAll").then((res) => setGroups(res.data));
}, []);
const days = ["Dom", "Lun", "Mar", "Mié", "Jue", "Vie", "Sáb"];
const days = ["Domingo", "Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado"];
const hours = [
"07:00", "08:00", "09:00", "10:00", "11:00",
"12:00", "13:00", "14:00", "15:00", "16:00",
@ -19,38 +20,43 @@ const ViewTimetable = () => {
return (
<div className="card">
<h2>Horario Semanal</h2>
<table className="styled-table" style={{ marginTop: "20px" }}>
<thead>
<tr>
<th>Hora</th>
{days.map((day) => (
<th key={day}>{day}</th>
))}
</tr>
</thead>
<tbody>
{hours.map((h) => (
<tr key={h}>
<td>{h}</td>
{days.map((_, dayIndex) => (
<td key={h + dayIndex}>
{groups
.filter((g) => {
const date = new Date(g.schedule);
return (
date.getHours() === parseInt(h.split(":")[0]) &&
date.getDay() === dayIndex
);
})
.map((g) => (
<div key={g.id}>{g.name} ({g.level})</div>
))}
</td>
<div className="timetable-wrapper">
<table className="styled-table timetable-table">
<thead>
<tr>
<th>Hora</th>
{days.map((day) => (
<th key={day}>{day}</th>
))}
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{hours.map((h) => (
<tr key={h}>
<td className="hour-label">{h}</td>
{days.map((_, dayIndex) => (
<td key={h + dayIndex} className="day-cell">
{groups
.filter((g) => {
const date = new Date(g.schedule);
return (
date.getHours() === parseInt(h.split(":")[0]) &&
date.getDay() === dayIndex
);
})
.map((g) => (
<div key={g.id} className="timetable-block">
<strong>{g.name}</strong>
<div className="level-label">{g.level}</div>
</div>
))}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
};

View File

@ -1,11 +1,15 @@
import React, { useEffect, useState } from "react";
import api from "../../api/axiosConfig";
import ErrorMessage from "../common/ErrorMessage";
import "../styles/ContentArea.css";
const StudentHistoryList = () => {
const [histories, setHistories] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [editingId, setEditingId] = useState(null);
const [editedData, setEditedData] = useState({});
const [errorMsg, setErrorMsg] = useState("");
const [successMsg, setSuccessMsg] = useState("");
useEffect(() => {
fetchHistories();
@ -15,27 +19,55 @@ const StudentHistoryList = () => {
try {
const res = await api.get("/student-history/getAll");
setHistories(res.data);
setLoading(false);
} catch (err) {
console.error("Error al cargar historial de estudiantes", err);
setError("Error al cargar historial");
setErrorMsg("❌ Error al cargar historial.");
} finally {
setLoading(false);
}
};
const handleEditClick = (h) => {
setEditingId(h.id);
setEditedData({
eventDate: h.eventDate,
eventType: h.eventType || "",
description: h.description || "",
studentId: h.studentId,
});
};
const handleChange = (e) => {
const { name, value } = e.target;
setEditedData((prev) => ({ ...prev, [name]: value }));
};
const handleUpdate = async () => {
try {
await api.put(`/student-history/update/${editingId}`, editedData);
setSuccessMsg("✅ Evento actualizado correctamente.");
setErrorMsg("");
setEditingId(null);
fetchHistories();
} catch (err) {
console.error(err);
setErrorMsg("❌ Error al actualizar el evento.");
setSuccessMsg("");
}
};
const handleDelete = async (id) => {
if (
!window.confirm("¿Seguro que quieres eliminar este evento del historial?")
)
return;
if (!window.confirm("¿Seguro que quieres eliminar este evento del historial?")) return;
try {
await api.delete(`/student-history/delete/${id}`);
setHistories(histories.filter((h) => h.id !== id));
alert("Evento eliminado correctamente");
setHistories((prev) => prev.filter((h) => h.id !== id));
setSuccessMsg("✅ Evento eliminado correctamente.");
setErrorMsg("");
} catch (err) {
console.error(err);
alert("Error al eliminar el evento");
setErrorMsg("❌ Error al eliminar el evento.");
setSuccessMsg("");
}
};
@ -45,17 +77,14 @@ const StudentHistoryList = () => {
<p>Cargando historial...</p>
</div>
);
if (error)
return (
<div className="card">
<p style={{ color: "red" }}>{error}</p>
</div>
);
return (
<div className="card">
<h2>Historial de Estudiantes</h2>
<ErrorMessage message={errorMsg} type="error" />
<ErrorMessage message={successMsg} type="success" />
{histories.length === 0 ? (
<p>No hay eventos registrados.</p>
) : (
@ -73,27 +102,53 @@ const StudentHistoryList = () => {
<tbody>
{histories.map((h) => (
<tr key={h.id}>
<td>
{h.student?.user?.name} {h.student?.user?.surname}
{(!h.student || !h.student.user) && `(ID: ${h.studentId})`}
</td>
<td>{h.eventDate}</td>
<td>{h.eventType}</td>
<td>{h.description}</td>
<td>
<button
className="edit-button"
onClick={() => alert("Editar aún no implementado")}
>
</button>
<button
className="delete-button"
onClick={() => handleDelete(h.id)}
>
🗑
</button>
</td>
{editingId === h.id ? (
<>
<td>
{h.student?.user?.name} {h.student?.user?.surname}
</td>
<td>
<input
type="date"
name="eventDate"
value={editedData.eventDate}
onChange={handleChange}
/>
</td>
<td>
<input
name="eventType"
value={editedData.eventType}
onChange={handleChange}
/>
</td>
<td>
<input
name="description"
value={editedData.description}
onChange={handleChange}
/>
</td>
<td>
<button className="edit-btn" onClick={handleUpdate}></button>
<button className="delete-btn" onClick={() => setEditingId(null)}></button>
</td>
</>
) : (
<>
<td>
{h.student?.user?.name} {h.student?.user?.surname}
{(!h.student || !h.student.user) && `(ID: ${h.studentId})`}
</td>
<td>{h.eventDate}</td>
<td>{h.eventType}</td>
<td>{h.description}</td>
<td>
<button className="edit-btn" onClick={() => handleEditClick(h)}></button>
<button className="delete-btn" onClick={() => handleDelete(h.id)}>🗑</button>
</td>
</>
)}
</tr>
))}
</tbody>

View File

@ -7,49 +7,93 @@ const TrainingGroupList = () => {
const [groups, setGroups] = useState([]);
const [teachers, setTeachers] = useState([]);
const [editingGroupId, setEditingGroupId] = useState(null);
const [newTeacherId, setNewTeacherId] = useState("");
const [editedGroup, setEditedGroup] = useState({});
const [errorMsg, setErrorMsg] = useState("");
const [successMsg, setSuccessMsg] = useState("");
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
const groupRes = await api.get("/training-groups/getAll");
const teacherRes = await api.get("/teachers/getAll");
setGroups(groupRes.data);
setTeachers(teacherRes.data);
};
const handleUpdateTeacher = async (groupId) => {
const group = groups.find((g) => g.id === groupId);
const updatedGroup = {
...group,
teacherId: parseInt(newTeacherId),
};
await api.put(`/training-groups/update/${groupId}`, updatedGroup);
setEditingGroupId(null);
fetchData();
};
const handleDelete = async (groupId) => {
if (window.confirm("Are you sure you want to delete this group?")) {
await api.delete(`/training-groups/delete/${groupId}`);
fetchData();
try {
const groupRes = await api.get("/training-groups/getAll");
const teacherRes = await api.get("/teachers/getAll");
setGroups(groupRes.data);
setTeachers(teacherRes.data);
} catch (err) {
setErrorMsg("❌ Error al cargar los grupos o profesores.");
console.error(err);
}
};
const handleEditClick = (group) => {
setEditingGroupId(group.id);
setEditedGroup({
name: group.name,
level: group.level,
schedule: group.schedule.slice(0, 16),
teacherId: group.teacherId,
});
};
const handleChange = (e) => {
const { name, value } = e.target;
setEditedGroup((prev) => ({ ...prev, [name]: value }));
};
const handleUpdate = async (id) => {
try {
await api.put(`/training-groups/update/${id}`, {
...editedGroup,
schedule: new Date(editedGroup.schedule).toISOString(),
teacherId: parseInt(editedGroup.teacherId),
studentIds: []
});
setSuccessMsg("✅ Grupo actualizado correctamente.");
setErrorMsg("");
setEditingGroupId(null);
fetchData();
} catch (err) {
setErrorMsg("❌ Error al actualizar el grupo.");
setSuccessMsg("");
console.error(err);
}
};
const handleDelete = async (id) => {
if (!window.confirm("¿Estás seguro de que deseas eliminar este grupo?")) return;
try {
await api.delete(`/training-groups/delete/${id}`);
setSuccessMsg("✅ Grupo eliminado correctamente.");
fetchData();
} catch (err) {
setErrorMsg("❌ Error al eliminar el grupo.");
console.error(err);
}
};
const getTeacherName = (id) => {
const teacher = teachers.find((t) => t.id === id);
return teacher ? `${teacher.user?.name} ${teacher.user?.surname}` : `(ID ${id})`;
};
return (
<div className="card">
<h2>Grupos de entrenamiento</h2>
<h2>Grupos de Entrenamiento</h2>
<ErrorMessage message={errorMsg} type="error" />
<ErrorMessage message={successMsg} type="success" />
<div className="table-wrapper">
<table className="styled-table">
<thead>
<tr>
<th>ID Grupo</th>
<th>ID</th>
<th>Nombre</th>
<th>Nivel</th>
<th>Fecha / Hora</th>
<th>Horario</th>
<th>Profesor</th>
<th>Acciones</th>
</tr>
@ -57,44 +101,63 @@ const TrainingGroupList = () => {
<tbody>
{groups.map((group) => (
<tr key={group.id}>
<td>{group.id}</td>
<td>{group.name}</td>
<td>{group.level}</td>
<td>{new Date(group.schedule).toLocaleString()}</td>
<td>
{editingGroupId === group.id ? (
<select
value={newTeacherId}
onChange={(e) => setNewTeacherId(e.target.value)}
>
<option value="">-- Select --</option>
{teachers.map((teacher) => (
<option key={teacher.id} value={teacher.id}>
{teacher.user?.name} {teacher.user?.surname}
</option>
))}
</select>
) : (
`${group.teacherId}`
)}
</td>
<td>
{editingGroupId === group.id ? (
<button onClick={() => handleUpdateTeacher(group.id)}>
Save
</button>
) : (
<button
onClick={() => {
setEditingGroupId(group.id);
setNewTeacherId(group.teacherId || "");
}}
>
Edit Teacher
</button>
)}
<button onClick={() => handleDelete(group.id)}>Delete</button>
</td>
{editingGroupId === group.id ? (
<>
<td>{group.id}</td>
<td>
<input
name="name"
value={editedGroup.name}
onChange={handleChange}
/>
</td>
<td>
<input
name="level"
value={editedGroup.level}
onChange={handleChange}
/>
</td>
<td>
<input
type="datetime-local"
name="schedule"
value={editedGroup.schedule}
onChange={handleChange}
/>
</td>
<td>
<select
name="teacherId"
value={editedGroup.teacherId}
onChange={handleChange}
>
<option value="">-- Selecciona profesor --</option>
{teachers.map((t) => (
<option key={t.id} value={t.id}>
{t.user?.name} {t.user?.surname}
</option>
))}
</select>
</td>
<td>
<button className="edit-btn" onClick={() => handleUpdate(group.id)}></button>
<button className="delete-btn" onClick={() => setEditingGroupId(null)}></button>
</td>
</>
) : (
<>
<td>{group.id}</td>
<td>{group.name}</td>
<td>{group.level}</td>
<td>{new Date(group.schedule).toLocaleString()}</td>
<td>{getTeacherName(group.teacherId)}</td>
<td>
<button className="edit-btn" onClick={() => handleEditClick(group)}></button>
<button className="delete-btn" onClick={() => handleDelete(group.id)}>🗑</button>
</td>
</>
)}
</tr>
))}
</tbody>

View File

@ -264,6 +264,28 @@ form textarea:focus {
outline: none;
}
.group-student-list li {
padding: 10px 12px;
margin-bottom: 6px;
background-color: #ffffff;
border: 1px solid #dcdde1;
border-radius: 10px;
display: flex;
justify-content: space-between;
align-items: center;
transition: background-color 0.2s;
}
.group-student-list li:hover {
background-color: #f0f4f8;
}
.group-student-list li .delete-button,
.group-student-list li .add-button {
margin-left: 10px;
}

View File

@ -0,0 +1,48 @@
/* Ajuste general de la tabla */
.timetable-wrapper {
overflow-x: auto;
}
.timetable-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed; /* fuerza a que todas las celdas tengan el mismo ancho */
}
.timetable-table th,
.timetable-table td {
padding: 6px;
text-align: center;
border: 1px solid #dcdde1;
height: 60px; /* ajusta esto si lo quieres más compacto */
vertical-align: middle;
position: relative;
}
/* Hora a la izquierda */
.hour-label {
font-weight: bold;
background-color: #f0f0f0;
width: 70px;
}
/* Celda del día */
.day-cell {
padding: 0;
}
/* Tarjeta dentro de la celda */
.timetable-block {
background-color: #2ecc71;
color: white;
width: 100%;
height: 100%;
font-size: 14px;
padding: 4px;
border-radius: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}