diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 9c59f89..43ee9f0 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -2,9 +2,11 @@ name: CI/CD Pipeline
on:
push:
- branches: [ main, dev, "feature/**" ]
+ branches: [ main ]
+ # branches: [ main, dev, "feature/**" ]
pull_request:
- branches: [ main, dev ]
+ branches: [ main ]
+ # branches: [ main, dev ]
permissions:
contents: read
@@ -78,12 +80,12 @@ jobs:
docker-build:
name: Docker Build & Push
runs-on: ubuntu-latest
- needs: [build-and-test, qodana]
+ needs: [ build-and-test, qodana ]
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev'
strategy:
matrix:
- service: [config-service, eureka-service, api-gateway, user-service, academic-service]
+ service: [ config-service, eureka-service, api-gateway, user-service, academic-service ]
steps:
- name: Checkout code
diff --git a/services/academic-service/pom.xml b/services/academic-service/pom.xml
index 49d5d9c..296fca5 100644
--- a/services/academic-service/pom.xml
+++ b/services/academic-service/pom.xml
@@ -91,11 +91,6 @@
cloudinary-http44
1.36.0
-
-
- org.springframework.boot
- spring-boot-starter-web
-
@@ -128,4 +123,4 @@
-
\ No newline at end of file
+
diff --git a/services/academic-service/src/main/java/com/academicsService/controller/AcademicYearController.java b/services/academic-service/src/main/java/com/academicsService/controller/AcademicYearController.java
index edcb78c..bd588bb 100644
--- a/services/academic-service/src/main/java/com/academicsService/controller/AcademicYearController.java
+++ b/services/academic-service/src/main/java/com/academicsService/controller/AcademicYearController.java
@@ -83,7 +83,7 @@ public ResponseEntity setAsCurrent(
}
@DeleteMapping("/{id}")
- @PreAuthorize("hasRole('SCHOOL_ADMIN')")
+ @PreAuthorize("hasAnyRole('SCHOOL_ADMIN', 'STAFF')")
public ResponseEntity delete(
@PathVariable("id") String id,
@RequestHeader("X-User-Id") String currentUserId) {
diff --git a/services/academic-service/src/main/java/com/academicsService/controller/AttendanceController.java b/services/academic-service/src/main/java/com/academicsService/controller/AttendanceController.java
new file mode 100644
index 0000000..93ad139
--- /dev/null
+++ b/services/academic-service/src/main/java/com/academicsService/controller/AttendanceController.java
@@ -0,0 +1,96 @@
+package com.academicsService.controller;
+
+import com.academicsService.dto.create.AttendanceCreateDto;
+import com.academicsService.dto.response.AttendanceResponseDto;
+import com.academicsService.dto.update.AttendanceUpdateDto;
+import com.academicsService.service.AttendanceService;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Sort;
+import org.springframework.format.annotation.DateTimeFormat;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDate;
+
+@RestController
+@RequestMapping("/api/v1/attendances")
+@RequiredArgsConstructor
+@Slf4j
+public class AttendanceController {
+
+ private final AttendanceService attendanceService;
+
+ @PostMapping
+ @PreAuthorize("hasAnyRole('SCHOOL_ADMIN', 'STAFF', 'TEACHER')")
+ public ResponseEntity create(
+ @Valid @RequestBody AttendanceCreateDto dto,
+ @RequestHeader("X-User-Id") String currentUserId) {
+ log.info("POST /api/v1/attendances by user: {}", currentUserId);
+ return ResponseEntity.status(HttpStatus.CREATED)
+ .body(attendanceService.create(dto, currentUserId));
+ }
+
+ @PutMapping("/{id}")
+ @PreAuthorize("hasAnyRole('SCHOOL_ADMIN', 'STAFF', 'TEACHER')")
+ public ResponseEntity update(
+ @PathVariable("id") String id,
+ @Valid @RequestBody AttendanceUpdateDto dto,
+ @RequestHeader("X-User-Id") String currentUserId) {
+ log.info("PUT /api/v1/attendances/{} by user: {}", id, currentUserId);
+ return ResponseEntity.ok(attendanceService.update(id, dto, currentUserId));
+ }
+
+ @GetMapping("/{id}")
+ @PreAuthorize("hasAnyRole('SCHOOL_ADMIN', 'STAFF', 'TEACHER', 'STUDENT', 'PARENT')")
+ public ResponseEntity findById(
+ @PathVariable("id") String id) {
+ log.info("GET /api/v1/attendances/{}", id);
+ return ResponseEntity.ok(attendanceService.findById(id));
+ }
+
+ @GetMapping
+ @PreAuthorize("hasAnyRole('SCHOOL_ADMIN', 'STAFF', 'TEACHER')")
+ public ResponseEntity> findBySchool(
+ @RequestParam("schoolId") String schoolId,
+ @RequestParam(value = "classroomId", required = false) String classroomId,
+ @RequestParam(value = "date", required = false)
+ @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date,
+ @RequestParam(defaultValue = "0") int page,
+ @RequestParam(defaultValue = "20") int size) {
+ log.info("GET /api/v1/attendances?schoolId={}&classroomId={}&date={}", schoolId, classroomId, date);
+ return ResponseEntity.ok(attendanceService.findBySchool(
+ schoolId, classroomId, date,
+ PageRequest.of(page, size, Sort.by("date").descending())
+ ));
+ }
+
+ @GetMapping("/student/{studentId}")
+ @PreAuthorize("hasAnyRole('SCHOOL_ADMIN', 'STAFF', 'TEACHER', 'STUDENT', 'PARENT')")
+ public ResponseEntity> findByStudent(
+ @PathVariable("studentId") String studentId,
+ @RequestParam("schoolId") String schoolId,
+ @RequestParam(defaultValue = "0") int page,
+ @RequestParam(defaultValue = "20") int size) {
+ log.info("GET /api/v1/attendances/student/{}", studentId);
+ return ResponseEntity.ok(attendanceService.findByStudent(
+ studentId, schoolId,
+ PageRequest.of(page, size)
+ ));
+ }
+
+ @DeleteMapping("/{id}")
+ @PreAuthorize("hasAnyRole('SCHOOL_ADMIN', 'STAFF')")
+ public ResponseEntity delete(
+ @PathVariable("id") String id,
+ @RequestHeader("X-User-Id") String currentUserId) {
+ log.info("DELETE /api/v1/attendances/{} by user: {}", id, currentUserId);
+ attendanceService.delete(id, currentUserId);
+ return ResponseEntity.noContent().build();
+ }
+}
diff --git a/services/academic-service/src/main/java/com/academicsService/controller/StudentClassroomController.java b/services/academic-service/src/main/java/com/academicsService/controller/StudentClassroomController.java
index 2b6ec36..bc8a084 100644
--- a/services/academic-service/src/main/java/com/academicsService/controller/StudentClassroomController.java
+++ b/services/academic-service/src/main/java/com/academicsService/controller/StudentClassroomController.java
@@ -78,7 +78,7 @@ public ResponseEntity> findByStudent(
@PreAuthorize("hasAnyRole('SCHOOL_ADMIN', 'TEACHER', 'STAFF', 'PARENT', 'STUDENT')")
public ResponseEntity findCurrentByStudent(
@PathVariable("studentId") String studentId,
- @RequestParam("academicYear") String academicYear) {
+ @RequestParam(value = "academicYear", required = false) String academicYear) {
log.info("GET /api/v1/student-classrooms/student/{}/current", studentId);
return ResponseEntity.ok(
studentClassroomService.findCurrentByStudent(studentId, academicYear)
diff --git a/services/academic-service/src/main/java/com/academicsService/controller/TeacherSubjectController.java b/services/academic-service/src/main/java/com/academicsService/controller/TeacherSubjectController.java
index be9e79e..1615201 100644
--- a/services/academic-service/src/main/java/com/academicsService/controller/TeacherSubjectController.java
+++ b/services/academic-service/src/main/java/com/academicsService/controller/TeacherSubjectController.java
@@ -57,21 +57,22 @@ public ResponseEntity findById(
@PreAuthorize("hasAnyRole('SCHOOL_ADMIN', 'TEACHER', 'STAFF')")
public ResponseEntity> findByTeacher(
@PathVariable("teacherId") String teacherId,
- @RequestParam("academicYear") String academicYear,
+ @RequestParam(value = "academicYear", required = false) String academicYear,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
log.info("GET /api/v1/teacher-subjects/teacher/{}", teacherId);
- return ResponseEntity.ok(teacherSubjectService.findByTeacherAndYear(
- teacherId, academicYear,
- PageRequest.of(page, size, Sort.by("academicYear").descending())
- ));
+ PageRequest pageable = PageRequest.of(page, size, Sort.by("academicYear").descending());
+ if (academicYear != null && !academicYear.isBlank()) {
+ return ResponseEntity.ok(teacherSubjectService.findByTeacherAndYear(teacherId, academicYear, pageable));
+ }
+ return ResponseEntity.ok(teacherSubjectService.findByTeacher(teacherId, pageable));
}
@GetMapping("/subject/{subjectId}")
@PreAuthorize("hasAnyRole('SCHOOL_ADMIN', 'STAFF')")
public ResponseEntity> findBySubject(
@PathVariable("subjectId") String subjectId,
- @RequestParam("academicYear") String academicYear,
+ @RequestParam(value = "academicYear", required = false) String academicYear,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
log.info("GET /api/v1/teacher-subjects/subject/{}", subjectId);
@@ -82,18 +83,19 @@ public ResponseEntity> findBySubject(
}
@GetMapping("/school")
- @PreAuthorize("hasAnyRole('SCHOOL_ADMIN', 'STAFF')")
+ @PreAuthorize("hasAnyRole('SCHOOL_ADMIN', 'STAFF', 'TEACHER', 'STUDENT', 'PARENT')")
public ResponseEntity> findBySchool(
@RequestHeader("X-User-Id") String currentUserId,
- @RequestParam("academicYear") String academicYear,
+ @RequestParam(value = "academicYear", required = false) String academicYear,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
String schoolId = schoolValidationService.getSchoolIdByUserId(currentUserId);
log.info("GET /api/v1/teacher-subjects/school schoolId={} academicYear={}", schoolId, academicYear);
- return ResponseEntity.ok(teacherSubjectService.findBySchoolAndYear(
- schoolId, academicYear,
- PageRequest.of(page, size, Sort.by("academicYear").descending())
- ));
+ PageRequest pageable = PageRequest.of(page, size, Sort.by("academicYear").descending());
+ if (academicYear != null && !academicYear.isBlank()) {
+ return ResponseEntity.ok(teacherSubjectService.findBySchoolAndYear(schoolId, academicYear, pageable));
+ }
+ return ResponseEntity.ok(teacherSubjectService.findBySchool(schoolId, pageable));
}
@DeleteMapping("/{id}")
diff --git a/services/academic-service/src/main/java/com/academicsService/dto/create/AcademicYearCreateDto.java b/services/academic-service/src/main/java/com/academicsService/dto/create/AcademicYearCreateDto.java
index ca052f3..9de5295 100644
--- a/services/academic-service/src/main/java/com/academicsService/dto/create/AcademicYearCreateDto.java
+++ b/services/academic-service/src/main/java/com/academicsService/dto/create/AcademicYearCreateDto.java
@@ -1,5 +1,6 @@
package com.academicsService.dto.create;
+import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
@@ -24,6 +25,7 @@ public class AcademicYearCreateDto {
@NotNull(message = "End date is required")
private LocalDate endDate;
+ @JsonProperty("isCurrent")
private boolean isCurrent;
}
diff --git a/services/academic-service/src/main/java/com/academicsService/dto/create/AttendanceCreateDto.java b/services/academic-service/src/main/java/com/academicsService/dto/create/AttendanceCreateDto.java
new file mode 100644
index 0000000..db309b8
--- /dev/null
+++ b/services/academic-service/src/main/java/com/academicsService/dto/create/AttendanceCreateDto.java
@@ -0,0 +1,28 @@
+package com.academicsService.dto.create;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.time.LocalDate;
+import java.util.List;
+
+@Data
+public class AttendanceCreateDto {
+
+ @NotBlank(message = "Classroom ID is required")
+ private String classroomId;
+
+ private String subjectId;
+
+ @NotNull(message = "Date is required")
+ private LocalDate date;
+
+ private String notes;
+
+ @NotEmpty(message = "At least one record is required")
+ @Valid
+ private List records;
+}
diff --git a/services/academic-service/src/main/java/com/academicsService/dto/create/AttendanceRecordCreateDto.java b/services/academic-service/src/main/java/com/academicsService/dto/create/AttendanceRecordCreateDto.java
new file mode 100644
index 0000000..9b237f8
--- /dev/null
+++ b/services/academic-service/src/main/java/com/academicsService/dto/create/AttendanceRecordCreateDto.java
@@ -0,0 +1,18 @@
+package com.academicsService.dto.create;
+
+import com.academicsService.model.enums.AttendanceStatus;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Data
+public class AttendanceRecordCreateDto {
+
+ @NotBlank(message = "Student ID is required")
+ private String studentId;
+
+ @NotNull(message = "Status is required")
+ private AttendanceStatus status;
+
+ private String reason;
+}
diff --git a/services/academic-service/src/main/java/com/academicsService/dto/response/AcademicYearResponseDto.java b/services/academic-service/src/main/java/com/academicsService/dto/response/AcademicYearResponseDto.java
index ec2d8b7..948a99d 100644
--- a/services/academic-service/src/main/java/com/academicsService/dto/response/AcademicYearResponseDto.java
+++ b/services/academic-service/src/main/java/com/academicsService/dto/response/AcademicYearResponseDto.java
@@ -1,5 +1,7 @@
package com.academicsService.dto.response;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
import java.time.LocalDate;
public record AcademicYearResponseDto(
@@ -8,6 +10,7 @@ public record AcademicYearResponseDto(
String name,
LocalDate startDate,
LocalDate endDate,
+ @JsonProperty("isCurrent")
boolean isCurrent
) {}
diff --git a/services/academic-service/src/main/java/com/academicsService/dto/response/AttendanceRecordResponseDto.java b/services/academic-service/src/main/java/com/academicsService/dto/response/AttendanceRecordResponseDto.java
new file mode 100644
index 0000000..a59c951
--- /dev/null
+++ b/services/academic-service/src/main/java/com/academicsService/dto/response/AttendanceRecordResponseDto.java
@@ -0,0 +1,11 @@
+package com.academicsService.dto.response;
+
+import com.academicsService.model.enums.AttendanceStatus;
+
+public record AttendanceRecordResponseDto(
+ String id,
+ String attendanceId,
+ String studentId,
+ AttendanceStatus status,
+ String reason
+) {}
diff --git a/services/academic-service/src/main/java/com/academicsService/dto/response/AttendanceResponseDto.java b/services/academic-service/src/main/java/com/academicsService/dto/response/AttendanceResponseDto.java
new file mode 100644
index 0000000..0cb703f
--- /dev/null
+++ b/services/academic-service/src/main/java/com/academicsService/dto/response/AttendanceResponseDto.java
@@ -0,0 +1,21 @@
+package com.academicsService.dto.response;
+
+import java.time.LocalDate;
+import java.util.List;
+
+public record AttendanceResponseDto(
+ String id,
+ String schoolId,
+ String classroomId,
+ String subjectId,
+ String teacherId,
+ LocalDate date,
+ String notes,
+ List records,
+ long presentCount,
+ long absentCount,
+ long lateCount,
+ long excusedCount,
+ String createdAt,
+ String updatedAt
+) {}
diff --git a/services/academic-service/src/main/java/com/academicsService/dto/update/AcademicYearUpdateDto.java b/services/academic-service/src/main/java/com/academicsService/dto/update/AcademicYearUpdateDto.java
index 47766d5..100bfeb 100644
--- a/services/academic-service/src/main/java/com/academicsService/dto/update/AcademicYearUpdateDto.java
+++ b/services/academic-service/src/main/java/com/academicsService/dto/update/AcademicYearUpdateDto.java
@@ -1,5 +1,6 @@
package com.academicsService.dto.update;
+import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
@@ -24,6 +25,7 @@ public class AcademicYearUpdateDto {
@NotNull(message = "End date is required")
private LocalDate endDate;
+ @JsonProperty("isCurrent")
private boolean isCurrent;
}
diff --git a/services/academic-service/src/main/java/com/academicsService/dto/update/AttendanceUpdateDto.java b/services/academic-service/src/main/java/com/academicsService/dto/update/AttendanceUpdateDto.java
new file mode 100644
index 0000000..6d8d191
--- /dev/null
+++ b/services/academic-service/src/main/java/com/academicsService/dto/update/AttendanceUpdateDto.java
@@ -0,0 +1,24 @@
+package com.academicsService.dto.update;
+
+import com.academicsService.model.enums.AttendanceStatus;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.util.Map;
+
+@Data
+public class AttendanceUpdateDto {
+
+ private String notes;
+
+ // Map of studentId -> { status, reason }
+ private Map records;
+
+ @Data
+ public static class RecordUpdate {
+ @NotNull(message = "Status is required")
+ private AttendanceStatus status;
+ private String reason;
+ }
+}
diff --git a/services/academic-service/src/main/java/com/academicsService/exception/GlobalExceptionHandler.java b/services/academic-service/src/main/java/com/academicsService/exception/GlobalExceptionHandler.java
index 7f40640..651012b 100644
--- a/services/academic-service/src/main/java/com/academicsService/exception/GlobalExceptionHandler.java
+++ b/services/academic-service/src/main/java/com/academicsService/exception/GlobalExceptionHandler.java
@@ -10,6 +10,7 @@
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import java.time.LocalDateTime;
@@ -118,6 +119,18 @@ public ResponseEntity handleTypeMismatchException(
);
}
+ @ExceptionHandler(MissingServletRequestParameterException.class)
+ public ResponseEntity handleMissingParams(
+ MissingServletRequestParameterException ex,
+ HttpServletRequest request) {
+ log.warn("Missing parameter: {}", ex.getParameterName());
+ return buildErrorResponse(
+ HttpStatus.BAD_REQUEST,
+ "Required parameter '" + ex.getParameterName() + "' is missing",
+ request.getRequestURI()
+ );
+ }
+
@ExceptionHandler(FileUploadException.class)
public ResponseEntity handleFileUploadException(
FileUploadException ex,
diff --git a/services/academic-service/src/main/java/com/academicsService/mapper/AttendanceMapper.java b/services/academic-service/src/main/java/com/academicsService/mapper/AttendanceMapper.java
new file mode 100644
index 0000000..26cdd0b
--- /dev/null
+++ b/services/academic-service/src/main/java/com/academicsService/mapper/AttendanceMapper.java
@@ -0,0 +1,48 @@
+package com.academicsService.mapper;
+
+import com.academicsService.dto.response.AttendanceRecordResponseDto;
+import com.academicsService.dto.response.AttendanceResponseDto;
+import com.academicsService.model.Attendance;
+import com.academicsService.model.AttendanceRecord;
+import com.academicsService.model.enums.AttendanceStatus;
+
+import java.util.List;
+
+public class AttendanceMapper {
+
+ public static AttendanceRecordResponseDto toRecordDto(AttendanceRecord r) {
+ return new AttendanceRecordResponseDto(
+ r.getId(),
+ r.getAttendanceId(),
+ r.getStudentId(),
+ r.getStatus(),
+ r.getReason()
+ );
+ }
+
+ public static AttendanceResponseDto toResponseDto(Attendance a, List records) {
+ long present = records.stream().filter(r -> r.getStatus() == AttendanceStatus.PRESENT).count();
+ long absent = records.stream().filter(r -> r.getStatus() == AttendanceStatus.ABSENT).count();
+ long late = records.stream().filter(r -> r.getStatus() == AttendanceStatus.LATE).count();
+ long excused = records.stream().filter(r -> r.getStatus() == AttendanceStatus.EXCUSED).count();
+
+ return new AttendanceResponseDto(
+ a.getId(),
+ a.getSchoolId(),
+ a.getClassroomId(),
+ a.getSubjectId(),
+ a.getTeacherId(),
+ a.getDate(),
+ a.getNotes(),
+ records.stream().map(AttendanceMapper::toRecordDto).toList(),
+ present,
+ absent,
+ late,
+ excused,
+ a.getCreatedAt() != null ? a.getCreatedAt().toString() : null,
+ a.getUpdatedAt() != null ? a.getUpdatedAt().toString() : null
+ );
+ }
+
+ private AttendanceMapper() {}
+}
diff --git a/services/academic-service/src/main/java/com/academicsService/model/AcademicYear.java b/services/academic-service/src/main/java/com/academicsService/model/AcademicYear.java
index c810bd4..d8e5cdd 100644
--- a/services/academic-service/src/main/java/com/academicsService/model/AcademicYear.java
+++ b/services/academic-service/src/main/java/com/academicsService/model/AcademicYear.java
@@ -3,6 +3,7 @@
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -13,7 +14,9 @@
import java.time.LocalDate;
@Entity
-@Table(name = "academic_years")
+@Table(name = "academic_years", uniqueConstraints = {
+ @UniqueConstraint(name = "uc_academic_years_school_name", columnNames = {"school_id", "name"})
+})
@Getter
@Setter
@Builder
@@ -21,7 +24,7 @@
@AllArgsConstructor
public class AcademicYear extends BaseEntity {
- @Column(name = "name", nullable = false, unique = true)
+ @Column(name = "name", nullable = false)
private String name; // "2025 - 2026"
@Column(name = "start_date", nullable = false)
diff --git a/services/academic-service/src/main/java/com/academicsService/model/Attendance.java b/services/academic-service/src/main/java/com/academicsService/model/Attendance.java
new file mode 100644
index 0000000..02cb63d
--- /dev/null
+++ b/services/academic-service/src/main/java/com/academicsService/model/Attendance.java
@@ -0,0 +1,34 @@
+package com.academicsService.model;
+
+import jakarta.persistence.*;
+import lombok.*;
+
+import java.time.LocalDate;
+
+@Entity
+@Table(name = "attendances")
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Attendance extends BaseEntity {
+
+ @Column(name = "school_id", nullable = false)
+ private String schoolId;
+
+ @Column(name = "classroom_id", nullable = false)
+ private String classroomId;
+
+ @Column(name = "subject_id")
+ private String subjectId;
+
+ @Column(name = "teacher_id", nullable = false)
+ private String teacherId;
+
+ @Column(nullable = false)
+ private LocalDate date;
+
+ @Column(columnDefinition = "TEXT")
+ private String notes;
+}
diff --git a/services/academic-service/src/main/java/com/academicsService/model/AttendanceRecord.java b/services/academic-service/src/main/java/com/academicsService/model/AttendanceRecord.java
new file mode 100644
index 0000000..bf1be48
--- /dev/null
+++ b/services/academic-service/src/main/java/com/academicsService/model/AttendanceRecord.java
@@ -0,0 +1,28 @@
+package com.academicsService.model;
+
+import com.academicsService.model.enums.AttendanceStatus;
+import jakarta.persistence.*;
+import lombok.*;
+
+@Entity
+@Table(name = "attendance_records")
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AttendanceRecord extends BaseEntity {
+
+ @Column(name = "attendance_id", nullable = false)
+ private String attendanceId;
+
+ @Column(name = "student_id", nullable = false)
+ private String studentId;
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false)
+ private AttendanceStatus status;
+
+ @Column
+ private String reason;
+}
diff --git a/services/academic-service/src/main/java/com/academicsService/model/GradeLevel.java b/services/academic-service/src/main/java/com/academicsService/model/GradeLevel.java
index f698527..981ab13 100644
--- a/services/academic-service/src/main/java/com/academicsService/model/GradeLevel.java
+++ b/services/academic-service/src/main/java/com/academicsService/model/GradeLevel.java
@@ -6,6 +6,7 @@
import jakarta.persistence.Entity;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@@ -16,7 +17,10 @@
import java.util.List;
@Entity
-@Table(name = "grade_levels")
+@Table(name = "grade_levels", uniqueConstraints = {
+ @UniqueConstraint(name = "uc_grade_levels_school_display_order", columnNames = {"school_id", "display_order"}),
+ @UniqueConstraint(name = "uc_grade_levels_school_name", columnNames = {"school_id", "name"})
+})
@Getter
@Setter
@Builder
@@ -33,7 +37,7 @@ public class GradeLevel extends BaseEntity {
@Column(columnDefinition = "TEXT")
private String description;
- @Column(name = "display_order", nullable = false, unique = true)
+ @Column(name = "display_order", nullable = false)
private int displayOrder;
@OneToMany(mappedBy = "gradeLevel", cascade = CascadeType.ALL, orphanRemoval = true)
diff --git a/services/academic-service/src/main/java/com/academicsService/model/enums/AttendanceStatus.java b/services/academic-service/src/main/java/com/academicsService/model/enums/AttendanceStatus.java
new file mode 100644
index 0000000..b4656dd
--- /dev/null
+++ b/services/academic-service/src/main/java/com/academicsService/model/enums/AttendanceStatus.java
@@ -0,0 +1,8 @@
+package com.academicsService.model.enums;
+
+public enum AttendanceStatus {
+ PRESENT,
+ ABSENT,
+ LATE,
+ EXCUSED
+}
diff --git a/services/academic-service/src/main/java/com/academicsService/repository/AttendanceRecordRepository.java b/services/academic-service/src/main/java/com/academicsService/repository/AttendanceRecordRepository.java
new file mode 100644
index 0000000..552c744
--- /dev/null
+++ b/services/academic-service/src/main/java/com/academicsService/repository/AttendanceRecordRepository.java
@@ -0,0 +1,32 @@
+package com.academicsService.repository;
+
+import com.academicsService.model.AttendanceRecord;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface AttendanceRecordRepository extends JpaRepository {
+
+ List findByAttendanceId(String attendanceId);
+
+ void deleteByAttendanceId(String attendanceId);
+
+ @Query("""
+ SELECT ar FROM AttendanceRecord ar
+ WHERE ar.studentId = :studentId
+ AND ar.attendanceId IN (
+ SELECT a.id FROM Attendance a WHERE a.schoolId = :schoolId
+ )
+ ORDER BY ar.id DESC
+ """)
+ Page findByStudentIdAndSchoolId(
+ @Param("studentId") String studentId,
+ @Param("schoolId") String schoolId,
+ Pageable pageable);
+}
diff --git a/services/academic-service/src/main/java/com/academicsService/repository/AttendanceRepository.java b/services/academic-service/src/main/java/com/academicsService/repository/AttendanceRepository.java
new file mode 100644
index 0000000..022c2a5
--- /dev/null
+++ b/services/academic-service/src/main/java/com/academicsService/repository/AttendanceRepository.java
@@ -0,0 +1,27 @@
+package com.academicsService.repository;
+
+import com.academicsService.model.Attendance;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.time.LocalDate;
+import java.util.Optional;
+
+@Repository
+public interface AttendanceRepository extends JpaRepository {
+
+ Page findBySchoolId(String schoolId, Pageable pageable);
+
+ Page findBySchoolIdAndClassroomId(String schoolId, String classroomId, Pageable pageable);
+
+ Page findBySchoolIdAndDate(String schoolId, LocalDate date, Pageable pageable);
+
+ Page findBySchoolIdAndClassroomIdAndDate(
+ String schoolId, String classroomId, LocalDate date, Pageable pageable);
+
+ boolean existsByClassroomIdAndDate(String classroomId, LocalDate date);
+
+ Optional findByClassroomIdAndDate(String classroomId, LocalDate date);
+}
diff --git a/services/academic-service/src/main/java/com/academicsService/repository/StudentClassroomRepository.java b/services/academic-service/src/main/java/com/academicsService/repository/StudentClassroomRepository.java
index f711ba9..20d3a43 100644
--- a/services/academic-service/src/main/java/com/academicsService/repository/StudentClassroomRepository.java
+++ b/services/academic-service/src/main/java/com/academicsService/repository/StudentClassroomRepository.java
@@ -1,8 +1,6 @@
package com.academicsService.repository;
import com.academicsService.model.StudentClassroom;
-import org.springframework.data.domain.Page;
-import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
@@ -14,15 +12,32 @@
@Repository
public interface StudentClassroomRepository extends JpaRepository {
- Page findByClassroomIdAndIsActiveTrue(String classroomId, Pageable pageable);
+ @Query("""
+ SELECT sc FROM StudentClassroom sc
+ JOIN FETCH sc.classroom c
+ JOIN FETCH c.gradeLevel
+ WHERE c.id = :classroomId
+ AND sc.isActive = true
+ """)
+ List findByClassroomIdAndIsActiveTrueWithGraph(
+ @Param("classroomId") String classroomId);
- List findByStudentIdAndIsActiveTrue(String studentId);
+ @Query("""
+ SELECT sc FROM StudentClassroom sc
+ JOIN FETCH sc.classroom c
+ JOIN FETCH c.gradeLevel
+ WHERE sc.studentId = :studentId
+ AND sc.isActive = true
+ """)
+ List findByStudentIdAndIsActiveTrue(@Param("studentId") String studentId);
@Query("""
SELECT sc FROM StudentClassroom sc
+ JOIN FETCH sc.classroom c
+ JOIN FETCH c.gradeLevel
WHERE sc.studentId = :studentId
AND sc.isActive = true
- AND sc.classroom.academicYear = :academicYear
+ AND c.academicYear = :academicYear
""")
Optional findCurrentByStudentId(@Param("studentId") String studentId, @Param("academicYear") String academicYear);
diff --git a/services/academic-service/src/main/java/com/academicsService/repository/TeacherSubjectRepository.java b/services/academic-service/src/main/java/com/academicsService/repository/TeacherSubjectRepository.java
index 372b249..da7f6dc 100644
--- a/services/academic-service/src/main/java/com/academicsService/repository/TeacherSubjectRepository.java
+++ b/services/academic-service/src/main/java/com/academicsService/repository/TeacherSubjectRepository.java
@@ -1,22 +1,81 @@
package com.academicsService.repository;
import com.academicsService.model.TeacherSubject;
-import org.springframework.data.domain.Page;
-import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
+import java.util.List;
+import java.util.Optional;
+
@Repository
public interface TeacherSubjectRepository extends JpaRepository {
- Page findByTeacherIdAndAcademicYear(
- String teacherId, String academicYear, Pageable pageable);
+ @Query("""
+ SELECT ts FROM TeacherSubject ts
+ JOIN FETCH ts.subject s
+ JOIN FETCH s.gradeLevel
+ WHERE ts.teacherId = :teacherId
+ AND ts.academicYear = :academicYear
+ """)
+ List findByTeacherIdAndAcademicYear(
+ @Param("teacherId") String teacherId,
+ @Param("academicYear") String academicYear);
+
+ @Query("""
+ SELECT ts FROM TeacherSubject ts
+ JOIN FETCH ts.subject s
+ JOIN FETCH s.gradeLevel
+ WHERE s.id = :subjectId
+ AND ts.academicYear = :academicYear
+ """)
+ List findBySubjectIdAndAcademicYear(
+ @Param("subjectId") String subjectId,
+ @Param("academicYear") String academicYear);
+
+ @Query("""
+ SELECT ts FROM TeacherSubject ts
+ JOIN FETCH ts.subject s
+ JOIN FETCH s.gradeLevel
+ WHERE ts.schoolId = :schoolId
+ AND ts.academicYear = :academicYear
+ """)
+ List findBySchoolIdAndAcademicYear(
+ @Param("schoolId") String schoolId,
+ @Param("academicYear") String academicYear);
+
+ @Query("""
+ SELECT ts FROM TeacherSubject ts
+ JOIN FETCH ts.subject s
+ JOIN FETCH s.gradeLevel
+ WHERE ts.schoolId = :schoolId
+ """)
+ List findBySchoolId(@Param("schoolId") String schoolId);
+
+ @Query("""
+ SELECT ts FROM TeacherSubject ts
+ JOIN FETCH ts.subject s
+ JOIN FETCH s.gradeLevel
+ WHERE ts.teacherId = :teacherId
+ """)
+ List findByTeacherId(@Param("teacherId") String teacherId);
- Page findBySubjectIdAndAcademicYear(
- String subjectId, String academicYear, Pageable pageable);
+ @Query("""
+ SELECT ts FROM TeacherSubject ts
+ JOIN FETCH ts.subject s
+ JOIN FETCH s.gradeLevel
+ WHERE s.id = :subjectId
+ """)
+ List findBySubjectId(@Param("subjectId") String subjectId);
- Page findBySchoolIdAndAcademicYear(
- String schoolId, String academicYear, Pageable pageable);
+ @Query("""
+ SELECT ts FROM TeacherSubject ts
+ JOIN FETCH ts.subject s
+ JOIN FETCH s.gradeLevel
+ WHERE ts.id = :id
+ """)
+ Optional findByIdWithGraph(@Param("id") String id);
boolean existsByTeacherIdAndSubjectIdAndAcademicYear(
String teacherId, String subjectId, String academicYear);
diff --git a/services/academic-service/src/main/java/com/academicsService/service/AttendanceService.java b/services/academic-service/src/main/java/com/academicsService/service/AttendanceService.java
new file mode 100644
index 0000000..2a843c3
--- /dev/null
+++ b/services/academic-service/src/main/java/com/academicsService/service/AttendanceService.java
@@ -0,0 +1,26 @@
+package com.academicsService.service;
+
+import com.academicsService.dto.create.AttendanceCreateDto;
+import com.academicsService.dto.response.AttendanceResponseDto;
+import com.academicsService.dto.update.AttendanceUpdateDto;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+
+import java.time.LocalDate;
+
+public interface AttendanceService {
+
+ AttendanceResponseDto create(AttendanceCreateDto dto, String currentUserId);
+
+ AttendanceResponseDto update(String id, AttendanceUpdateDto dto, String currentUserId);
+
+ AttendanceResponseDto findById(String id);
+
+ Page findBySchool(
+ String schoolId, String classroomId, LocalDate date, Pageable pageable);
+
+ Page findByStudent(
+ String studentId, String schoolId, Pageable pageable);
+
+ void delete(String id, String currentUserId);
+}
diff --git a/services/academic-service/src/main/java/com/academicsService/service/TeacherSubjectService.java b/services/academic-service/src/main/java/com/academicsService/service/TeacherSubjectService.java
index f59a65c..c449377 100644
--- a/services/academic-service/src/main/java/com/academicsService/service/TeacherSubjectService.java
+++ b/services/academic-service/src/main/java/com/academicsService/service/TeacherSubjectService.java
@@ -14,6 +14,8 @@ public interface TeacherSubjectService {
TeacherSubjectResponseDto findById(String id);
+ Page findByTeacher(String teacherId, Pageable pageable);
+
Page findByTeacherAndYear(
String teacherId, String academicYear, Pageable pageable);
@@ -23,6 +25,8 @@ Page findBySubjectAndYear(
Page findBySchoolAndYear(
String schoolId, String academicYear, Pageable pageable);
+ Page findBySchool(String schoolId, Pageable pageable);
+
void delete(String id);
}
diff --git a/services/academic-service/src/main/java/com/academicsService/service/impl/AttendanceServiceImpl.java b/services/academic-service/src/main/java/com/academicsService/service/impl/AttendanceServiceImpl.java
new file mode 100644
index 0000000..15cac7c
--- /dev/null
+++ b/services/academic-service/src/main/java/com/academicsService/service/impl/AttendanceServiceImpl.java
@@ -0,0 +1,195 @@
+package com.academicsService.service.impl;
+
+import com.academicsService.client.SchoolValidationService;
+import com.academicsService.dto.create.AttendanceCreateDto;
+import com.academicsService.dto.create.AttendanceRecordCreateDto;
+import com.academicsService.dto.response.AttendanceResponseDto;
+import com.academicsService.dto.update.AttendanceUpdateDto;
+import com.academicsService.exception.BusinessRuleException;
+import com.academicsService.exception.ResourceNotFoundException;
+import com.academicsService.mapper.AttendanceMapper;
+import com.academicsService.model.Attendance;
+import com.academicsService.model.AttendanceRecord;
+import com.academicsService.repository.AttendanceRecordRepository;
+import com.academicsService.repository.AttendanceRepository;
+import com.academicsService.service.AttendanceService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class AttendanceServiceImpl implements AttendanceService {
+
+ private final AttendanceRepository attendanceRepository;
+ private final AttendanceRecordRepository recordRepository;
+ private final SchoolValidationService schoolValidationService;
+
+ @Override
+ @Transactional
+ public AttendanceResponseDto create(AttendanceCreateDto dto, String currentUserId) {
+ log.info("Creating attendance for classroom '{}' on '{}' by user '{}'",
+ dto.getClassroomId(), dto.getDate(), currentUserId);
+
+ String schoolId = schoolValidationService.getSchoolIdByUserId(currentUserId);
+
+ // Prevent duplicate session for same classroom + date
+ if (attendanceRepository.existsByClassroomIdAndDate(dto.getClassroomId(), dto.getDate())) {
+ throw new BusinessRuleException(
+ "An attendance session already exists for this classroom on " + dto.getDate() +
+ ". Use update instead."
+ );
+ }
+
+ Attendance attendance = Attendance.builder()
+ .schoolId(schoolId)
+ .classroomId(dto.getClassroomId())
+ .subjectId(dto.getSubjectId())
+ .teacherId(currentUserId)
+ .date(dto.getDate())
+ .notes(dto.getNotes())
+ .build();
+
+ Attendance saved = attendanceRepository.save(attendance);
+
+ List records = new ArrayList<>();
+ for (AttendanceRecordCreateDto r : dto.getRecords()) {
+ AttendanceRecord record = AttendanceRecord.builder()
+ .attendanceId(saved.getId())
+ .studentId(r.getStudentId())
+ .status(r.getStatus())
+ .reason(r.getReason())
+ .build();
+ records.add(recordRepository.save(record));
+ }
+
+ log.info("Attendance '{}' created with {} records", saved.getId(), records.size());
+ return AttendanceMapper.toResponseDto(saved, records);
+ }
+
+ @Override
+ @Transactional
+ public AttendanceResponseDto update(String id, AttendanceUpdateDto dto, String currentUserId) {
+ log.info("Updating attendance '{}' by user '{}'", id, currentUserId);
+
+ Attendance attendance = findEntityById(id);
+ String schoolId = schoolValidationService.getSchoolIdByUserId(currentUserId);
+
+ if (!attendance.getSchoolId().equals(schoolId)) {
+ throw new BusinessRuleException("You don't have permission to update this attendance session");
+ }
+
+ if (dto.getNotes() != null) {
+ attendance.setNotes(dto.getNotes());
+ }
+ attendanceRepository.save(attendance);
+
+ // Update individual records
+ List records = recordRepository.findByAttendanceId(id);
+ if (dto.getRecords() != null) {
+ for (AttendanceRecord record : records) {
+ AttendanceUpdateDto.RecordUpdate update = dto.getRecords().get(record.getStudentId());
+ if (update != null) {
+ record.setStatus(update.getStatus());
+ record.setReason(update.getReason());
+ recordRepository.save(record);
+ }
+ }
+ // Re-fetch after updates
+ records = recordRepository.findByAttendanceId(id);
+ }
+
+ log.info("Attendance '{}' updated", id);
+ return AttendanceMapper.toResponseDto(attendance, records);
+ }
+
+ @Override
+ @Transactional(readOnly = true)
+ public AttendanceResponseDto findById(String id) {
+ Attendance attendance = findEntityById(id);
+ List records = recordRepository.findByAttendanceId(id);
+ return AttendanceMapper.toResponseDto(attendance, records);
+ }
+
+ @Override
+ @Transactional(readOnly = true)
+ public Page findBySchool(
+ String schoolId, String classroomId, LocalDate date, Pageable pageable) {
+ log.debug("Fetching attendances for school '{}' classroom '{}' date '{}'",
+ schoolId, classroomId, date);
+
+ Page page;
+ if (classroomId != null && date != null) {
+ page = attendanceRepository.findBySchoolIdAndClassroomIdAndDate(schoolId, classroomId, date, pageable);
+ } else if (classroomId != null) {
+ page = attendanceRepository.findBySchoolIdAndClassroomId(schoolId, classroomId, pageable);
+ } else if (date != null) {
+ page = attendanceRepository.findBySchoolIdAndDate(schoolId, date, pageable);
+ } else {
+ page = attendanceRepository.findBySchoolId(schoolId, pageable);
+ }
+
+ return page.map(a -> {
+ List records = recordRepository.findByAttendanceId(a.getId());
+ return AttendanceMapper.toResponseDto(a, records);
+ });
+ }
+
+ @Override
+ @Transactional(readOnly = true)
+ public Page findByStudent(
+ String studentId, String schoolId, Pageable pageable) {
+ log.debug("Fetching attendance records for student '{}' school '{}'", studentId, schoolId);
+
+ Page recordPage = recordRepository.findByStudentIdAndSchoolId(
+ studentId, schoolId, pageable);
+
+ List results = new ArrayList<>();
+ for (AttendanceRecord r : recordPage.getContent()) {
+ Attendance attendance = attendanceRepository.findById(r.getAttendanceId()).orElse(null);
+ if (attendance != null) {
+ // For student view: only show their own record
+ results.add(AttendanceMapper.toResponseDto(attendance, List.of(r)));
+ }
+ }
+
+ return new PageImpl<>(results, pageable, recordPage.getTotalElements());
+ }
+
+ @Override
+ @Transactional
+ public void delete(String id, String currentUserId) {
+ log.info("Deleting attendance '{}' by user '{}'", id, currentUserId);
+
+ Attendance attendance = findEntityById(id);
+ String schoolId = schoolValidationService.getSchoolIdByUserId(currentUserId);
+
+ if (!attendance.getSchoolId().equals(schoolId)) {
+ throw new BusinessRuleException("You don't have permission to delete this attendance session");
+ }
+
+ recordRepository.deleteByAttendanceId(id);
+ attendanceRepository.delete(attendance);
+ log.info("Attendance '{}' and all its records deleted", id);
+ }
+
+ // ================================================================
+ // PRIVATE HELPERS
+ // ================================================================
+
+ private Attendance findEntityById(String id) {
+ return attendanceRepository.findById(id)
+ .orElseThrow(() -> new ResourceNotFoundException(
+ "Attendance session not found with id: " + id));
+ }
+}
diff --git a/services/academic-service/src/main/java/com/academicsService/service/impl/StudentClassroomServiceImpl.java b/services/academic-service/src/main/java/com/academicsService/service/impl/StudentClassroomServiceImpl.java
index a34e151..27be998 100644
--- a/services/academic-service/src/main/java/com/academicsService/service/impl/StudentClassroomServiceImpl.java
+++ b/services/academic-service/src/main/java/com/academicsService/service/impl/StudentClassroomServiceImpl.java
@@ -17,6 +17,7 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -124,9 +125,12 @@ public Page findByClassroom(
String classroomId, Pageable pageable) {
log.debug("Fetching enrollments for classroom '{}'", classroomId);
findClassroomById(classroomId);
- return studentClassroomRepository
- .findByClassroomIdAndIsActiveTrue(classroomId, pageable)
- .map(StudentClassroomMapper::toResponseDto);
+ List content = studentClassroomRepository
+ .findByClassroomIdAndIsActiveTrueWithGraph(classroomId)
+ .stream()
+ .map(StudentClassroomMapper::toResponseDto)
+ .toList();
+ return new PageImpl<>(content, pageable, content.size());
}
@Override
@@ -146,12 +150,25 @@ public StudentClassroomResponseDto findCurrentByStudent(
String studentId, String academicYear) {
log.debug("Fetching current enrollment for student '{}' year '{}'",
studentId, academicYear);
+
+ if (academicYear != null && !academicYear.isBlank()) {
+ return studentClassroomRepository
+ .findCurrentByStudentId(studentId, academicYear)
+ .map(StudentClassroomMapper::toResponseDto)
+ .orElseThrow(() -> new ResourceNotFoundException(
+ "No active enrollment found for student: " + studentId +
+ " in academic year: " + academicYear
+ ));
+ }
+
+ // No academicYear provided — return the most recent active enrollment
return studentClassroomRepository
- .findCurrentByStudentId(studentId, academicYear)
+ .findByStudentIdAndIsActiveTrue(studentId)
+ .stream()
+ .findFirst()
.map(StudentClassroomMapper::toResponseDto)
.orElseThrow(() -> new ResourceNotFoundException(
- "No active enrollment found for student: " + studentId +
- " in academic year: " + academicYear
+ "No active enrollment found for student: " + studentId
));
}
diff --git a/services/academic-service/src/main/java/com/academicsService/service/impl/TeacherSubjectServiceImpl.java b/services/academic-service/src/main/java/com/academicsService/service/impl/TeacherSubjectServiceImpl.java
index cdaec3e..6cbffa4 100644
--- a/services/academic-service/src/main/java/com/academicsService/service/impl/TeacherSubjectServiceImpl.java
+++ b/services/academic-service/src/main/java/com/academicsService/service/impl/TeacherSubjectServiceImpl.java
@@ -16,10 +16,13 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import java.util.List;
+
@Service
@RequiredArgsConstructor
@Slf4j
@@ -108,14 +111,29 @@ public TeacherSubjectResponseDto findById(String id) {
return TeacherSubjectMapper.toResponseDto(findEntityById(id));
}
+ @Override
+ @Transactional(readOnly = true)
+ public Page findByTeacher(String teacherId, Pageable pageable) {
+ log.debug("Fetching all subjects for teacher '{}'", teacherId);
+ List content = teacherSubjectRepository
+ .findByTeacherId(teacherId)
+ .stream()
+ .map(TeacherSubjectMapper::toResponseDto)
+ .toList();
+ return new PageImpl<>(content, pageable, content.size());
+ }
+
@Override
@Transactional(readOnly = true)
public Page findByTeacherAndYear(
String teacherId, String academicYear, Pageable pageable) {
log.debug("Fetching subjects for teacher '{}' year '{}'", teacherId, academicYear);
- return teacherSubjectRepository
- .findByTeacherIdAndAcademicYear(teacherId, academicYear, pageable)
- .map(TeacherSubjectMapper::toResponseDto);
+ List content = teacherSubjectRepository
+ .findByTeacherIdAndAcademicYear(teacherId, academicYear)
+ .stream()
+ .map(TeacherSubjectMapper::toResponseDto)
+ .toList();
+ return new PageImpl<>(content, pageable, content.size());
}
@Override
@@ -124,9 +142,13 @@ public Page findBySubjectAndYear(
String subjectId, String academicYear, Pageable pageable) {
log.debug("Fetching teachers for subject '{}' year '{}'", subjectId, academicYear);
findSubjectById(subjectId);
- return teacherSubjectRepository
- .findBySubjectIdAndAcademicYear(subjectId, academicYear, pageable)
- .map(TeacherSubjectMapper::toResponseDto);
+ List raw = (academicYear != null && !academicYear.isBlank())
+ ? teacherSubjectRepository.findBySubjectIdAndAcademicYear(subjectId, academicYear)
+ : teacherSubjectRepository.findBySubjectId(subjectId);
+ List content = raw.stream()
+ .map(TeacherSubjectMapper::toResponseDto)
+ .toList();
+ return new PageImpl<>(content, pageable, content.size());
}
@Override
@@ -134,9 +156,24 @@ public Page findBySubjectAndYear(
public Page findBySchoolAndYear(
String schoolId, String academicYear, Pageable pageable) {
log.debug("Fetching teacher subjects for school '{}' year '{}'", schoolId, academicYear);
- return teacherSubjectRepository
- .findBySchoolIdAndAcademicYear(schoolId, academicYear, pageable)
- .map(TeacherSubjectMapper::toResponseDto);
+ List content = teacherSubjectRepository
+ .findBySchoolIdAndAcademicYear(schoolId, academicYear)
+ .stream()
+ .map(TeacherSubjectMapper::toResponseDto)
+ .toList();
+ return new PageImpl<>(content, pageable, content.size());
+ }
+
+ @Override
+ @Transactional(readOnly = true)
+ public Page findBySchool(String schoolId, Pageable pageable) {
+ log.debug("Fetching all teacher subjects for school '{}'", schoolId);
+ List content = teacherSubjectRepository
+ .findBySchoolId(schoolId)
+ .stream()
+ .map(TeacherSubjectMapper::toResponseDto)
+ .toList();
+ return new PageImpl<>(content, pageable, content.size());
}
@Override
@@ -152,7 +189,7 @@ public void delete(String id) {
// ================================================================
private TeacherSubject findEntityById(String id) {
- return teacherSubjectRepository.findById(id)
+ return teacherSubjectRepository.findByIdWithGraph(id)
.orElseThrow(() -> new ResourceNotFoundException(
"Teacher subject assignment not found with id: " + id
));
diff --git a/services/academic-service/src/main/resources/db/migration/V5__add_attendance.sql b/services/academic-service/src/main/resources/db/migration/V5__add_attendance.sql
new file mode 100644
index 0000000..b06bf3f
--- /dev/null
+++ b/services/academic-service/src/main/resources/db/migration/V5__add_attendance.sql
@@ -0,0 +1,26 @@
+CREATE TABLE attendance_records
+(
+ id VARCHAR(255) NOT NULL,
+ created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL,
+ updated_at TIMESTAMP WITHOUT TIME ZONE,
+ attendance_id VARCHAR(255) NOT NULL,
+ student_id VARCHAR(255) NOT NULL,
+ status VARCHAR(255) NOT NULL,
+ reason VARCHAR(255),
+ CONSTRAINT pk_attendance_records PRIMARY KEY (id)
+);
+
+CREATE TABLE attendances
+(
+ id VARCHAR(255) NOT NULL,
+ created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL,
+ updated_at TIMESTAMP WITHOUT TIME ZONE,
+ school_id VARCHAR(255) NOT NULL,
+ classroom_id VARCHAR(255) NOT NULL,
+ subject_id VARCHAR(255),
+ teacher_id VARCHAR(255) NOT NULL,
+ date date NOT NULL,
+ notes TEXT,
+ CONSTRAINT pk_attendances PRIMARY KEY (id)
+);
+
diff --git a/services/academic-service/src/main/resources/db/migration/V6__edit_academic_year_name.sql b/services/academic-service/src/main/resources/db/migration/V6__edit_academic_year_name.sql
new file mode 100644
index 0000000..1bb1541
--- /dev/null
+++ b/services/academic-service/src/main/resources/db/migration/V6__edit_academic_year_name.sql
@@ -0,0 +1,2 @@
+ALTER TABLE academic_years
+ ADD CONSTRAINT uc_academic_years_school_name UNIQUE (school_id, name);
diff --git a/services/academic-service/src/main/resources/db/migration/V7__fix_grade_level_constraints.sql b/services/academic-service/src/main/resources/db/migration/V7__fix_grade_level_constraints.sql
new file mode 100644
index 0000000..f155702
--- /dev/null
+++ b/services/academic-service/src/main/resources/db/migration/V7__fix_grade_level_constraints.sql
@@ -0,0 +1,24 @@
+-- DO $$
+-- BEGIN
+-- -- Drop the old constraint if it exists
+-- IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'uc_grade_levels_display_order') THEN
+-- ALTER TABLE grade_levels DROP CONSTRAINT uc_grade_levels_display_order;
+-- END IF;
+--
+-- -- Add new constraint on (school_id, display_order) if not exists
+-- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'uc_grade_levels_school_display_order') THEN
+-- ALTER TABLE grade_levels ADD CONSTRAINT uc_grade_levels_school_display_order UNIQUE (school_id, display_order);
+-- END IF;
+--
+-- -- Add new constraint on (school_id, name) if not exists
+-- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'uc_grade_levels_school_name') THEN
+-- ALTER TABLE grade_levels ADD CONSTRAINT uc_grade_levels_school_name UNIQUE (school_id, name);
+-- END IF;
+-- END $$;
+--
+
+ALTER TABLE grade_levels
+ ADD CONSTRAINT uc_grade_levels_school_display_order UNIQUE (school_id, display_order);
+
+ALTER TABLE grade_levels
+ ADD CONSTRAINT uc_grade_levels_school_name UNIQUE (school_id, name);
diff --git a/services/academic-service/src/main/resources/db/migration/V8__fix_academic_year_constraints.sql b/services/academic-service/src/main/resources/db/migration/V8__fix_academic_year_constraints.sql
new file mode 100644
index 0000000..1b9fd76
--- /dev/null
+++ b/services/academic-service/src/main/resources/db/migration/V8__fix_academic_year_constraints.sql
@@ -0,0 +1,16 @@
+DO $$
+BEGIN
+ -- Drop the incorrect unique constraint on name if it exists
+ IF EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'uc_academic_years_name') THEN
+ ALTER TABLE academic_years DROP CONSTRAINT uc_academic_years_name;
+ END IF;
+
+ -- Ensure the correct unique constraint on (school_id, name) exists
+ -- The table already had "uc_academic_years_school_name" in the output from \d academic_years,
+ -- but this ensures it's enforced.
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'uc_academic_years_school_name') THEN
+ ALTER TABLE academic_years ADD CONSTRAINT uc_academic_years_school_name UNIQUE (school_id, name);
+ END IF;
+
+END $$;
+
diff --git a/services/api-gateway/pom.xml b/services/api-gateway/pom.xml
index 3e40a4e..516af7d 100644
--- a/services/api-gateway/pom.xml
+++ b/services/api-gateway/pom.xml
@@ -62,4 +62,4 @@
-
\ No newline at end of file
+
diff --git a/services/api-gateway/src/main/java/com/apigateway/config/GatewaySecurityConfig.java b/services/api-gateway/src/main/java/com/apigateway/config/GatewaySecurityConfig.java
index 0afd52d..2642264 100644
--- a/services/api-gateway/src/main/java/com/apigateway/config/GatewaySecurityConfig.java
+++ b/services/api-gateway/src/main/java/com/apigateway/config/GatewaySecurityConfig.java
@@ -32,11 +32,10 @@ public SecurityWebFilterChain springSecurityFilterChain(
.authorizeExchange(exchanges -> exchanges
.pathMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.pathMatchers(
- "/api/auth/**",
- "/api/admin/super-admin",
- "/api/schools/register",
- "/api/users/v1/schools/register",
- "/actuator/**"
+ "/api/auth/v1/auth/**",
+ "/api/admin/v1/admin/super-admin",
+ "/api/schools/v1/schools/register",
+ "/actuator/**"
).permitAll()
.anyExchange().authenticated()
)
@@ -102,4 +101,4 @@ private List extractRoles(Jwt jwt) {
new SimpleGrantedAuthority("ROLE_" + role))
.toList();
}
-}
\ No newline at end of file
+}
diff --git a/services/config-service/src/main/resources/config-repo/api-gateway.yml b/services/config-service/src/main/resources/config-repo/api-gateway.yml
index 8f008dc..cc34221 100644
--- a/services/config-service/src/main/resources/config-repo/api-gateway.yml
+++ b/services/config-service/src/main/resources/config-repo/api-gateway.yml
@@ -115,4 +115,4 @@ logging:
level:
org.springframework.cloud.gateway: DEBUG
org.springframework.security: DEBUG
- reactor.netty: INFO
\ No newline at end of file
+ reactor.netty: INFO
diff --git a/services/config-service/src/main/resources/config-repo/user-service.yml b/services/config-service/src/main/resources/config-repo/user-service.yml
index dc57bfc..dced79c 100644
--- a/services/config-service/src/main/resources/config-repo/user-service.yml
+++ b/services/config-service/src/main/resources/config-repo/user-service.yml
@@ -21,7 +21,7 @@ keycloak:
realm: schoolsphere
resource: schoolsphere-app
credentials:
- secret: jfMBILB8hJMjggs8VUHYU4oDoyjVa3qw
+ secret: erwM0QLaq7iGHYsfwbAV1ZNvRoD6infi
admin:
username: ${KEYCLOAK_ADMIN_USERNAME:admin}
password: ${KEYCLOAK_ADMIN_PASSWORD:admin}
diff --git a/services/user-service/pom.xml b/services/user-service/pom.xml
index 493f788..7423554 100644
--- a/services/user-service/pom.xml
+++ b/services/user-service/pom.xml
@@ -186,4 +186,4 @@
-
\ No newline at end of file
+