Added more improvements and also fixed some issues

This commit is contained in:
Dennis Eckerskorn 2025-05-25 14:02:17 +02:00
parent e5cc695fd9
commit c8bc967399
18 changed files with 339 additions and 180 deletions

View File

@ -3,6 +3,9 @@ package com.denniseckerskorn.controllers.class_management_controllers;
import com.denniseckerskorn.dtos.class_managment_dtos.AssistanceDTO;
import com.denniseckerskorn.entities.class_managment.Assistance;
import com.denniseckerskorn.entities.class_managment.TrainingSession;
import com.denniseckerskorn.entities.user_managment.users.Student;
import com.denniseckerskorn.exceptions.BadRequestException;
import com.denniseckerskorn.services.class_managment_services.AssistanceService;
import com.denniseckerskorn.services.class_managment_services.TrainingSessionService;
import com.denniseckerskorn.services.user_managment_services.StudentService;
@ -36,14 +39,20 @@ public class AssistanceController {
@Operation(summary = "Create a new assistance record")
@PostMapping("/create")
public ResponseEntity<AssistanceDTO> create(@RequestBody AssistanceDTO dto) {
Assistance assistance = dto.toEntity(
studentService.findById(dto.getStudentId()),
trainingSessionService.findById(dto.getSessionId())
);
Student student = studentService.findById(dto.getStudentId());
TrainingSession session = trainingSessionService.findById(dto.getSessionId());
if (!student.getTrainingGroups().contains(session.getTrainingGroup())) {
throw new BadRequestException("The student does not belong to the selected training group.");
}
Assistance assistance = dto.toEntity(student, session);
Assistance saved = assistanceService.save(assistance);
return ResponseEntity.ok(AssistanceDTO.fromEntity(saved));
}
@Operation(summary = "Update an existing assistance record", description = "Update an existing assistance record")
@PutMapping("/update")
public ResponseEntity<AssistanceDTO> update(@RequestBody AssistanceDTO dto) {

View File

@ -60,16 +60,15 @@ public class TrainingGroupController {
group.setTeacher(teacher);
TrainingGroup createdGroup = trainingGroupService.save(group);
TrainingSession trainingSession = new TrainingSession();
trainingSession.setTrainingGroup(createdGroup);
trainingSession.setDate(createdGroup.getSchedule());
trainingSession.setStatus(StatusValues.ACTIVE);
trainingSessionService.save(trainingSession);
int months = dto.getRecurrenceMonths() != null && dto.getRecurrenceMonths() > 0
? dto.getRecurrenceMonths()
: 1;
trainingSessionService.generateRecurringSession(createdGroup, months);
return ResponseEntity.status(HttpStatus.CREATED).body(new TrainingGroupDTO(createdGroup));
}
@Operation(summary = "Assign a student to a group")
@PutMapping("/assign-student")
public ResponseEntity<Void> assignStudent(@RequestParam Integer groupId, @RequestParam Integer studentId) {
@ -162,4 +161,20 @@ public class TrainingGroupController {
return ResponseEntity.noContent().build();
}
@Operation(summary = "Generate recurring training sessions for a group", description = "Generates recurring training sessions for a specified number of months")
@Transactional
@PostMapping("/generate-recurring-sessions")
public ResponseEntity<TrainingGroupDTO> generateRecurringSessions(@RequestBody TrainingGroupDTO dto) {
TrainingGroup group = trainingGroupService.findById(dto.getId());
int months = (dto.getRecurrenceMonths() != null && dto.getRecurrenceMonths() > 0)
? dto.getRecurrenceMonths()
: 1;
trainingSessionService.generateRecurringSession(group, months);
return ResponseEntity.ok(new TrainingGroupDTO(group));
}
}

View File

@ -20,6 +20,7 @@ public class TrainingGroupDTO {
private LocalDateTime schedule;
private Integer teacherId;
private Set<Integer> studentIds = new HashSet<>();
private Integer recurrenceMonths;
public TrainingGroupDTO() {
}
@ -107,4 +108,12 @@ public class TrainingGroupDTO {
public void setStudentIds(Set<Integer> studentIds) {
this.studentIds = studentIds;
}
public Integer getRecurrenceMonths() {
return recurrenceMonths;
}
public void setRecurrenceMonths(Integer recurrenceMonths) {
this.recurrenceMonths = recurrenceMonths;
}
}

View File

@ -6,7 +6,9 @@ import com.denniseckerskorn.enums.StatusValues;
import jakarta.persistence.*;
import java.time.LocalDate;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
@Entity
@Table(name = "MEMBERSHIPS")
@ -29,8 +31,8 @@ public class Membership {
@Column(name = "status", nullable = false, length = 20)
private StatusValues status;
@OneToOne(mappedBy = "membership")
private Student student;
@OneToMany(mappedBy = "membership")
private Set<Student> students = new HashSet<>();
public Integer getId() {
return id;
@ -52,8 +54,8 @@ public class Membership {
return status;
}
public Student getStudent() {
return student;
public Set<Student> getStudents() {
return students;
}
public void setId(Integer id) {
@ -76,8 +78,8 @@ public class Membership {
this.status = status;
}
public void setStudent(Student student) {
this.student = student;
public void setStudents(Set<Student> students) {
this.students = students;
}
@Override
@ -102,7 +104,6 @@ public class Membership {
", startDate=" + startDate +
", endDate=" + endDate +
", status=" + status +
", student=" + (student != null ? student.getId() : "null") +
'}';
}
}

View File

@ -44,7 +44,7 @@ public class Student {
@OneToMany(mappedBy = "student", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<StudentHistory> histories = new HashSet<>();
@OneToOne
@ManyToOne
@JoinColumn(name = "fk_membership")
private Membership membership;

View File

@ -3,6 +3,8 @@ package com.denniseckerskorn.repositories.class_managment_repositories;
import com.denniseckerskorn.entities.class_managment.Membership;
import com.denniseckerskorn.enums.MembershipTypeValues;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.time.LocalDate;
@ -15,7 +17,8 @@ public interface MembershipRepository extends JpaRepository<Membership, Integer>
Membership findByStatus(String status);
Membership findByStudentId(Integer studentId);
@Query("SELECT m FROM Membership m JOIN m.students s WHERE s.id = :studentId")
Membership findByStudentId(@Param("studentId") Integer studentId);
boolean existsByType(MembershipTypeValues type);
}

View File

@ -82,13 +82,12 @@ public class MembershipService extends AbstractService<Membership, Integer> {
logger.info("Deleting membership by ID: {}", id);
Membership membership = findById(id);
// Verificar si está asignado a un estudiante
Student student = membership.getStudent();
if (student != null) {
logger.error("Cannot delete membership. It is assigned to student ID {}", student.getId());
throw new InvalidDataException("Cannot delete membership because it is assigned to a student");
if (!membership.getStudents().isEmpty()) {
logger.error("Cannot delete membership. It is assigned to {} student(s)", membership.getStudents().size());
throw new InvalidDataException("Cannot delete membership because it is assigned to one or more students");
}
super.deleteById(id);
logger.info("Membership with ID {} deleted", id);
}

View File

@ -13,7 +13,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
@Service
@ -145,5 +144,4 @@ public class TrainingGroupService extends AbstractService<TrainingGroup, Integer
trainingGroupRepository.save(managedGroup);
}
}
}

View File

@ -1,7 +1,9 @@
package com.denniseckerskorn.services.class_managment_services;
import com.denniseckerskorn.entities.class_managment.TrainingGroup;
import com.denniseckerskorn.entities.class_managment.TrainingSession;
import com.denniseckerskorn.entities.user_managment.users.Student;
import com.denniseckerskorn.enums.StatusValues;
import com.denniseckerskorn.exceptions.DuplicateEntityException;
import com.denniseckerskorn.exceptions.EntityNotFoundException;
import com.denniseckerskorn.exceptions.InvalidDataException;
@ -107,13 +109,29 @@ public class TrainingSessionService extends AbstractService<TrainingSession, Int
@Transactional
public void deleteAllAssistancesBySession(Integer sessionId) {
logger.info("Deleting all assistances for session ID: {}", sessionId);
TrainingSession session = findById(sessionId);
if (session.getAssistances() != null && !session.getAssistances().isEmpty()) {
session.getAssistances().clear();
update(session);
}
logger.info("All assistances for session ID: {} have been deleted", sessionId);
}
@Transactional
public void generateRecurringSession(TrainingGroup group, int months) {
logger.info("Generating recurring sessions for group: {} for {} months", group.getName(), months);
LocalDateTime baseDate = group.getSchedule();
for (int i = 0; i < months * 4; i++) {
LocalDateTime sessionDate = baseDate.plusWeeks(i);
TrainingSession newSession = new TrainingSession();
newSession.setDate(sessionDate);
newSession.setStatus(StatusValues.ACTIVE);
newSession.setTrainingGroup(group);
save(newSession);
logger.info("Created new session: {} for group: {}", newSession.getId(), group.getName());
}
}
}

View File

@ -6,6 +6,7 @@ import "../styles/ContentArea.css";
const AssistanceForm = () => {
const [students, setStudents] = useState([]);
const [sessions, setSessions] = useState([]);
const [filteredSessions, setFilteredSessions] = useState([]);
const [formData, setFormData] = useState({
studentId: "",
sessionId: ""
@ -19,7 +20,21 @@ const AssistanceForm = () => {
}, []);
const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
if (name === "studentId") {
const student = students.find((s) => s.id === parseInt(value));
if (student) {
const groupIds = student.trainingGroups?.map((g) => g.id) || [];
const filtered = sessions.filter((s) =>
groupIds.includes(s.trainingGroupId)
);
setFilteredSessions(filtered);
} else {
setFilteredSessions([]);
}
}
};
const handleSubmit = async (e) => {
@ -31,13 +46,13 @@ const AssistanceForm = () => {
studentId: formData.studentId,
sessionId: formData.sessionId,
date: new Date(new Date().setHours(new Date().getHours() + 2)).toISOString()
};
};
try {
await api.post("/assistances/create", payload);
setSuccessMsg("✅ Asistencia registrada correctamente.");
setFormData({ studentId: "", sessionId: "" });
setFilteredSessions([]);
} catch (err) {
console.error(err);
const msg = err.response?.data?.message || "❌ Error al registrar asistencia.";
@ -48,14 +63,15 @@ const AssistanceForm = () => {
return (
<div className="card">
<h2>Registrar Asistencia</h2>
<form onSubmit={handleSubmit}>
<form onSubmit={handleSubmit} className="form-column">
<label>Estudiante:</label>
<select
name="studentId"
value={formData.studentId}
onChange={handleChange}
required
>
<option value="">Selecciona estudiante</option>
<option value="">-- Selecciona estudiante --</option>
{students.map((s) => (
<option key={s.id} value={s.id}>
{s.user?.name} {s.user?.surname}
@ -63,21 +79,32 @@ const AssistanceForm = () => {
))}
</select>
<label>Sesión:</label>
{formData.studentId && filteredSessions.length === 0 ? (
<p style={{ color: "#e74c3c", margin: "5px 0 15px" }}>
Este estudiante no tiene sesiones disponibles según sus grupos asignados.
</p>
) : (
<select
name="sessionId"
value={formData.sessionId}
onChange={handleChange}
required
disabled={filteredSessions.length === 0}
>
<option value="">Selecciona sesión</option>
{sessions.map((s) => (
<option value="">-- Selecciona sesión --</option>
{filteredSessions.map((s) => (
<option key={s.id} value={s.id}>
{`${new Date(s.date).toLocaleString()} - Grupo: ${s.trainingGroup?.name || s.trainingGroupId}`}
</option>
))}
</select>
)}
<button type="submit" disabled={!formData.studentId || !formData.sessionId}>
Registrar
</button>
<button type="submit">Registrar</button>
<ErrorMessage message={successMsg} type="success" />
<ErrorMessage message={errorMsg} type="error" />
</form>

View File

@ -1,101 +0,0 @@
import React, { use, useState } from "react";
import api from "../../api/axiosConfig";
import ErrorMessage from "../common/ErrorMessage";
import "../styles/ContentArea.css";
const MembershipForm = () => {
const [formData, setFormData] = useState({
startDate: "",
endDate: "",
type: "BASIC",
status: "ACTIVE",
});
const [successMsg, setSuccessMsg] = useState("");
const [errorMsg, setErrorMsg] = useState("");
const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleSubmit = async (e) => {
e.preventDefault();
setSuccessMsg("");
setErrorMsg("");
try {
console.log("Payload:", formData);
await api.post("/memberships/create", formData);
setSuccessMsg("✅ Membresía creada correctamente");
setFormData({
startDate: "",
endDate: "",
type: "BASIC",
status: "ACTIVE",
});
} catch (err) {
console.error(err);
const msg =
err.response?.data?.message || "❌ Error al crear la membresía";
setErrorMsg(msg);
}
};
return (
<div className="card">
<h2>Crear Membresía nueva</h2>
<form onSubmit={handleSubmit}>
<label>Fecha de inicio de la membresía:</label>
<input
type="date"
name="startDate"
value={formData.startDate}
onChange={handleChange}
required
/>
<label>Fecha fin de la membresía:</label>
<input
type="date"
name="endDate"
value={formData.endDate}
onChange={handleChange}
required
/>
<label>Selecciona el tipo de membresía:</label>
<select
name="type"
value={formData.type}
onChange={handleChange}
required
>
<option value="BASIC">Básico</option>
<option value="ADVANCED">Avanzado</option>
<option value="PREMIUM">Premium</option>
<option value="NO_LIMIT">Ilimitado</option>
<option value="TRIAL">Prueba</option>
</select>
<label>Estado de la membresía:</label>
<select
name="status"
value={formData.status}
onChange={handleChange}
required
>
<option value="ACTIVE">Activo</option>
<option value="INACTIVE">Inactivo</option>
<option value="SUSPENDED">Suspendido / Expirado</option>
</select>
<button type="submit">Crear nueva membresía</button>
</form>
<ErrorMessage type="success" message={successMsg} />
<ErrorMessage type="error" message={errorMsg} />
</div>
);
};
export default MembershipForm;

View File

@ -0,0 +1,112 @@
import React, { useEffect, useState } from "react";
import api from "../../api/axiosConfig";
import ErrorMessage from "../common/ErrorMessage";
import "../styles/ContentArea.css";
const MembershipDetails = () => {
const [memberships, setMemberships] = useState([]);
const [errorMsg, setErrorMsg] = useState("");
const [successMsg, setSuccessMsg] = useState("");
useEffect(() => {
fetchMemberships();
}, []);
const fetchMemberships = async () => {
try {
const res = await api.get("/memberships/getAll");
setMemberships(res.data);
} catch (err) {
setErrorMsg("❌ Error al cargar las membresías");
}
};
const handleInputChange = (id, field, value) => {
setMemberships((prev) =>
prev.map((m) => (m.id === id ? { ...m, [field]: value } : m))
);
};
const handleUpdate = async (id) => {
const membership = memberships.find((m) => m.id === id);
if (!membership) return;
try {
await api.put(`/memberships/update/${id}`, membership);
setSuccessMsg("✅ Membresía actualizada correctamente");
setErrorMsg("");
fetchMemberships();
} catch (err) {
console.error(err);
setSuccessMsg("");
setErrorMsg("❌ Error al actualizar la membresía");
}
};
return (
<div className="card">
<h2>Detalles de Membresías</h2>
<ErrorMessage type="success" message={successMsg} />
<ErrorMessage type="error" message={errorMsg} />
<div className="table-wrapper">
<table className="styled-table">
<thead>
<tr>
<th>ID</th>
<th>Tipo</th>
<th>Inicio</th>
<th>Fin</th>
<th>Estado</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
{memberships.map((m) => (
<tr key={m.id}>
<td>{m.id}</td>
<td>{m.type}</td>
<td>
<input
type="date"
value={m.startDate}
onChange={(e) =>
handleInputChange(m.id, "startDate", e.target.value)
}
/>
</td>
<td>
<input
type="date"
value={m.endDate}
onChange={(e) =>
handleInputChange(m.id, "endDate", e.target.value)
}
/>
</td>
<td>
<select
value={m.status}
onChange={(e) =>
handleInputChange(m.id, "status", e.target.value)
}
>
<option value="ACTIVE">Activo</option>
<option value="INACTIVE">Inactivo</option>
<option value="SUSPENDED">Suspendido / Expirado</option>
</select>
</td>
<td>
<button onClick={() => handleUpdate(m.id)}>Actualizar</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
};
export default MembershipDetails;

View File

@ -8,6 +8,7 @@ const TrainingGroupForm = () => {
level: "",
schedule: "",
teacherId: "",
recurrenceMonths: 1, // nuevo campo
});
const [teachers, setTeachers] = useState([]);
@ -36,9 +37,10 @@ const TrainingGroupForm = () => {
level: formData.level,
schedule: formData.schedule,
teacherId: parseInt(formData.teacherId),
recurrenceMonths: parseInt(formData.recurrenceMonths), // incluir
});
setSuccessMsg("✅ Grupo de entrenamiento creado correctamente.");
setFormData({ name: "", level: "", schedule: "", teacherId: "" });
setFormData({ name: "", level: "", schedule: "", teacherId: "", recurrenceMonths: 1 });
} catch (err) {
console.error("Error al crear grupo", err);
const backendMsg =
@ -80,6 +82,17 @@ const TrainingGroupForm = () => {
required
/>
<label>Meses de recurrencia:</label>
<input
type="number"
name="recurrenceMonths"
value={formData.recurrenceMonths}
onChange={handleChange}
min={1}
max={24}
required
/>
<select
name="teacherId"
value={formData.teacherId}

View File

@ -19,7 +19,7 @@ import AssistanceForm from '../forms/AssitanceForm';
import AssistanceList from '../lists/AssistanceList';
import TrainingSessionList from '../lists/TrainingSessionList';
import ViewTimetable from '../forms/ViewTimetable';
import MembershipForm from '../forms/MembershipCreateForm';
import MembershipDetails from '../forms/MembershipDetails';
import MembershipList from '../lists/MembershipList';
import InvoiceForm from '../forms/InvoiceForm';
import InvoiceList from '../lists/InvoiceList';
@ -60,7 +60,7 @@ const MainLayout = () => {
<Route path="/admin/class-management/assistance/list" element={<AssistanceList />} />
<Route path="/admin/class-management/training-session/list" element={<TrainingSessionList />} />
<Route path="/admin/class-management/training-groups/view-timetable" element={<ViewTimetable />} />
<Route path="/admin/class-management/memberships/create" element={<MembershipForm />} />
<Route path="/admin/class-management/memberships/details" element={<MembershipDetails />} />
<Route path="/admin/class-management/memberships/list" element={<MembershipList />} />
{/* Finance */}

View File

@ -31,8 +31,8 @@ const SidebarAdmin = () => {
<button onClick={() => navigate('/admin/class-management/training-session/list')}>📆 Ver Sesiones</button>
<button onClick={() => navigate('/admin/class-management/assistance/create')}>📝 Registrar Asistencia</button>
<button onClick={() => navigate('/admin/class-management/assistance/list')}>📋 Ver Asistencias</button>
<button onClick={() => navigate('/admin/class-management/memberships/create')}> Crear Membresía</button>
<button onClick={() => navigate('/admin/class-management/memberships/list')}>🏷 Ver Membresías</button>
<button onClick={() => navigate('/admin/class-management/memberships/details')}> Detalles de las Membresías</button>
<button onClick={() => navigate('/admin/class-management/memberships/list')}>🏷 Asignar Membresías</button>
</div>
<div className="submenu">

View File

@ -1,19 +1,26 @@
import React, { useEffect, useState } from "react";
import api from "../../api/axiosConfig";
import "../styles/ContentArea.css";
import ErrorMessage from "../common/ErrorMessage";
const TrainingSessionList = () => {
const [sessions, setSessions] = useState([]);
const [groups, setGroups] = useState([]);
const [error, setError] = useState("");
const [successMsg, setSuccessMsg] = useState("");
useEffect(() => {
fetchSessions();
fetchData();
}, []);
const fetchSessions = async () => {
const fetchData = async () => {
try {
const res = await api.get("/training-sessions/getAll");
setSessions(res.data);
const [sessionRes, groupRes] = await Promise.all([
api.get("/training-sessions/getAll"),
api.get("/training-groups/getAll")
]);
setSessions(sessionRes.data);
setGroups(groupRes.data);
} catch (err) {
console.error(err);
setError("❌ Error al cargar las sesiones.");
@ -26,41 +33,67 @@ const TrainingSessionList = () => {
try {
await api.delete(`/training-sessions/delete/${id}`);
setSessions(sessions.filter((s) => s.id !== id));
setSuccessMsg("✅ Sesión eliminada correctamente.");
} catch (err) {
console.error(err);
alert("❌ Error al eliminar la sesión.");
setError("❌ Error al eliminar la sesión.");
}
};
const getGroupName = (id) => {
const group = groups.find((g) => g.id === id);
return group ? `${group.name} (${group.level})` : `Grupo ID ${id}`;
};
const renderStatus = (status) => {
const baseClass = "status-label";
switch (status) {
case "ACTIVE":
return <span className={`${baseClass} status-active`}>🟢 {status}</span>;
case "CANCELLED":
return <span className={`${baseClass} status-cancelled`}>🔴 {status}</span>;
case "FINISHED":
return <span className={`${baseClass} status-finished`}> {status}</span>;
default:
return <span className={baseClass}>{status}</span>;
}
};
return (
<div className="card">
<h2>Sesiones de Entrenamiento</h2>
{error ? (
<p style={{ color: "red" }}>{error}</p>
) : (
<ErrorMessage message={error} type="error" />
<ErrorMessage message={successMsg} type="success" />
<div className="table-wrapper">
<table className="styled-table">
<thead>
<tr>
<th>ID</th>
<th>Grupo</th>
<th>Fecha</th>
<th>Fecha y Hora</th>
<th>Estado</th>
</tr>
</thead>
<tbody>
{sessions.map((s) => (
{sessions.length === 0 ? (
<tr>
<td colSpan="5">No hay sesiones registradas.</td>
</tr>
) : (
sessions.map((s) => (
<tr key={s.id}>
<td>{s.id}</td>
<td>{s.trainingGroupId}</td>
<td>{getGroupName(s.trainingGroupId)}</td>
<td>{new Date(s.date).toLocaleString()}</td>
<td>{s.status}</td>
<td>{renderStatus(s.status)}</td>
</tr>
))}
))
)}
</tbody>
</table>
</div>
)}
</div>
);
};

View File

@ -286,6 +286,29 @@ form textarea:focus {
}
.status-label {
font-weight: bold;
padding: 4px 8px;
border-radius: 8px;
font-size: 0.9rem;
}
.status-active {
background-color: #dff9fb;
color: #27ae60;
}
.status-cancelled {
background-color: #fddede;
color: #e74c3c;
}
.status-finished {
background-color: #ecf0f1;
color: #7f8c8d;
}