diff --git a/memberflow-api/libs/memberflow-data-1.0-SNAPSHOT.jar b/memberflow-api/libs/memberflow-data-1.0-SNAPSHOT.jar index 0610f03..c9650ee 100644 Binary files a/memberflow-api/libs/memberflow-data-1.0-SNAPSHOT.jar and b/memberflow-api/libs/memberflow-data-1.0-SNAPSHOT.jar differ diff --git a/memberflow-api/src/main/java/com/denniseckerskorn/controllers/class_management_controllers/AssistanceController.java b/memberflow-api/src/main/java/com/denniseckerskorn/controllers/class_management_controllers/AssistanceController.java index 299952b..8a17be6 100644 --- a/memberflow-api/src/main/java/com/denniseckerskorn/controllers/class_management_controllers/AssistanceController.java +++ b/memberflow-api/src/main/java/com/denniseckerskorn/controllers/class_management_controllers/AssistanceController.java @@ -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 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 update(@RequestBody AssistanceDTO dto) { diff --git a/memberflow-api/src/main/java/com/denniseckerskorn/controllers/class_management_controllers/TrainingGroupController.java b/memberflow-api/src/main/java/com/denniseckerskorn/controllers/class_management_controllers/TrainingGroupController.java index a4f9011..76dde1e 100644 --- a/memberflow-api/src/main/java/com/denniseckerskorn/controllers/class_management_controllers/TrainingGroupController.java +++ b/memberflow-api/src/main/java/com/denniseckerskorn/controllers/class_management_controllers/TrainingGroupController.java @@ -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 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 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)); + } + } diff --git a/memberflow-api/src/main/java/com/denniseckerskorn/dtos/class_managment_dtos/TrainingGroupDTO.java b/memberflow-api/src/main/java/com/denniseckerskorn/dtos/class_managment_dtos/TrainingGroupDTO.java index b41d9b1..cbebef4 100644 --- a/memberflow-api/src/main/java/com/denniseckerskorn/dtos/class_managment_dtos/TrainingGroupDTO.java +++ b/memberflow-api/src/main/java/com/denniseckerskorn/dtos/class_managment_dtos/TrainingGroupDTO.java @@ -20,6 +20,7 @@ public class TrainingGroupDTO { private LocalDateTime schedule; private Integer teacherId; private Set studentIds = new HashSet<>(); + private Integer recurrenceMonths; public TrainingGroupDTO() { } @@ -107,4 +108,12 @@ public class TrainingGroupDTO { public void setStudentIds(Set studentIds) { this.studentIds = studentIds; } + + public Integer getRecurrenceMonths() { + return recurrenceMonths; + } + + public void setRecurrenceMonths(Integer recurrenceMonths) { + this.recurrenceMonths = recurrenceMonths; + } } diff --git a/memberflow-data/src/main/java/com/denniseckerskorn/entities/class_managment/Membership.java b/memberflow-data/src/main/java/com/denniseckerskorn/entities/class_managment/Membership.java index 10e6d3c..3c4af2c 100644 --- a/memberflow-data/src/main/java/com/denniseckerskorn/entities/class_managment/Membership.java +++ b/memberflow-data/src/main/java/com/denniseckerskorn/entities/class_managment/Membership.java @@ -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 students = new HashSet<>(); public Integer getId() { return id; @@ -52,8 +54,8 @@ public class Membership { return status; } - public Student getStudent() { - return student; + public Set 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 students) { + this.students = students; } @Override @@ -102,7 +104,6 @@ public class Membership { ", startDate=" + startDate + ", endDate=" + endDate + ", status=" + status + - ", student=" + (student != null ? student.getId() : "null") + '}'; } } diff --git a/memberflow-data/src/main/java/com/denniseckerskorn/entities/user_managment/users/Student.java b/memberflow-data/src/main/java/com/denniseckerskorn/entities/user_managment/users/Student.java index bf12e50..b16af07 100644 --- a/memberflow-data/src/main/java/com/denniseckerskorn/entities/user_managment/users/Student.java +++ b/memberflow-data/src/main/java/com/denniseckerskorn/entities/user_managment/users/Student.java @@ -44,7 +44,7 @@ public class Student { @OneToMany(mappedBy = "student", cascade = CascadeType.ALL, orphanRemoval = true) private Set histories = new HashSet<>(); - @OneToOne + @ManyToOne @JoinColumn(name = "fk_membership") private Membership membership; diff --git a/memberflow-data/src/main/java/com/denniseckerskorn/repositories/class_managment_repositories/MembershipRepository.java b/memberflow-data/src/main/java/com/denniseckerskorn/repositories/class_managment_repositories/MembershipRepository.java index f7aaf03..d285639 100644 --- a/memberflow-data/src/main/java/com/denniseckerskorn/repositories/class_managment_repositories/MembershipRepository.java +++ b/memberflow-data/src/main/java/com/denniseckerskorn/repositories/class_managment_repositories/MembershipRepository.java @@ -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 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); } diff --git a/memberflow-data/src/main/java/com/denniseckerskorn/services/class_managment_services/MembershipService.java b/memberflow-data/src/main/java/com/denniseckerskorn/services/class_managment_services/MembershipService.java index 7e367bd..e0656e5 100644 --- a/memberflow-data/src/main/java/com/denniseckerskorn/services/class_managment_services/MembershipService.java +++ b/memberflow-data/src/main/java/com/denniseckerskorn/services/class_managment_services/MembershipService.java @@ -82,13 +82,12 @@ public class MembershipService extends AbstractService { 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); } diff --git a/memberflow-data/src/main/java/com/denniseckerskorn/services/class_managment_services/TrainingGroupService.java b/memberflow-data/src/main/java/com/denniseckerskorn/services/class_managment_services/TrainingGroupService.java index 7f2bf1a..864c6f8 100644 --- a/memberflow-data/src/main/java/com/denniseckerskorn/services/class_managment_services/TrainingGroupService.java +++ b/memberflow-data/src/main/java/com/denniseckerskorn/services/class_managment_services/TrainingGroupService.java @@ -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 { 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) => { @@ -27,17 +42,17 @@ const AssistanceForm = () => { setErrorMsg(""); setSuccessMsg(""); - const payload = { - studentId: formData.studentId, - sessionId: formData.sessionId, - date: new Date(new Date().setHours(new Date().getHours() + 2)).toISOString() -}; - + const payload = { + 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 (

Registrar Asistencia

-
+ + - + + {formData.studentId && filteredSessions.length === 0 ? ( +

+ ⚠️ Este estudiante no tiene sesiones disponibles según sus grupos asignados. +

+ ) : ( + + )} + + - diff --git a/memberflow-frontend/src/components/forms/MembershipCreateForm.jsx b/memberflow-frontend/src/components/forms/MembershipCreateForm.jsx deleted file mode 100644 index ff6876c..0000000 --- a/memberflow-frontend/src/components/forms/MembershipCreateForm.jsx +++ /dev/null @@ -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 ( -
-

Crear Membresía nueva

-
- - - - - - - - - - - - -
- - -
- ); -}; - -export default MembershipForm; diff --git a/memberflow-frontend/src/components/forms/MembershipDetails.jsx b/memberflow-frontend/src/components/forms/MembershipDetails.jsx new file mode 100644 index 0000000..37da3b0 --- /dev/null +++ b/memberflow-frontend/src/components/forms/MembershipDetails.jsx @@ -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 ( +
+

Detalles de Membresías

+ + + + +
+ + + + + + + + + + + + + {memberships.map((m) => ( + + + + + + + + + ))} + +
IDTipoInicioFinEstadoAcciones
{m.id}{m.type} + + handleInputChange(m.id, "startDate", e.target.value) + } + /> + + + handleInputChange(m.id, "endDate", e.target.value) + } + /> + + + + +
+
+
+ ); +}; + +export default MembershipDetails; diff --git a/memberflow-frontend/src/components/forms/TrainingGroupFrom.jsx b/memberflow-frontend/src/components/forms/TrainingGroupFrom.jsx index f230380..5593341 100644 --- a/memberflow-frontend/src/components/forms/TrainingGroupFrom.jsx +++ b/memberflow-frontend/src/components/forms/TrainingGroupFrom.jsx @@ -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 /> + + +