Initial commit: breakpilot-core - Shared Infrastructure
Docker Compose with 24+ services: - PostgreSQL (PostGIS), Valkey, MinIO, Qdrant - Vault (PKI/TLS), Nginx (Reverse Proxy) - Backend Core API, Consent Service, Billing Service - RAG Service, Embedding Service - Gitea, Woodpecker CI/CD - Night Scheduler, Health Aggregator - Jitsi (Web/XMPP/JVB/Jicofo), Mailpit Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
933
consent-service/internal/handlers/school_handlers.go
Normal file
933
consent-service/internal/handlers/school_handlers.go
Normal file
@@ -0,0 +1,933 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/breakpilot/consent-service/internal/models"
|
||||
"github.com/breakpilot/consent-service/internal/services"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// SchoolHandlers contains all school-related HTTP handlers
|
||||
type SchoolHandlers struct {
|
||||
schoolService *services.SchoolService
|
||||
attendanceService *services.AttendanceService
|
||||
gradeService *services.GradeService
|
||||
}
|
||||
|
||||
// NewSchoolHandlers creates new school handlers
|
||||
func NewSchoolHandlers(schoolService *services.SchoolService, attendanceService *services.AttendanceService, gradeService *services.GradeService) *SchoolHandlers {
|
||||
return &SchoolHandlers{
|
||||
schoolService: schoolService,
|
||||
attendanceService: attendanceService,
|
||||
gradeService: gradeService,
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// School Handlers
|
||||
// ========================================
|
||||
|
||||
// CreateSchool creates a new school
|
||||
// POST /api/v1/schools
|
||||
func (h *SchoolHandlers) CreateSchool(c *gin.Context) {
|
||||
var req models.CreateSchoolRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
school, err := h.schoolService.CreateSchool(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, school)
|
||||
}
|
||||
|
||||
// GetSchool retrieves a school by ID
|
||||
// GET /api/v1/schools/:id
|
||||
func (h *SchoolHandlers) GetSchool(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid school ID"})
|
||||
return
|
||||
}
|
||||
|
||||
school, err := h.schoolService.GetSchool(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, school)
|
||||
}
|
||||
|
||||
// ListSchools lists all schools
|
||||
// GET /api/v1/schools
|
||||
func (h *SchoolHandlers) ListSchools(c *gin.Context) {
|
||||
schools, err := h.schoolService.ListSchools(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, schools)
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// School Year Handlers
|
||||
// ========================================
|
||||
|
||||
// CreateSchoolYear creates a new school year
|
||||
// POST /api/v1/schools/:id/years
|
||||
func (h *SchoolHandlers) CreateSchoolYear(c *gin.Context) {
|
||||
schoolIDStr := c.Param("id")
|
||||
schoolID, err := uuid.Parse(schoolIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid school ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
StartDate string `json:"start_date" binding:"required"`
|
||||
EndDate string `json:"end_date" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
startDate, err := time.Parse("2006-01-02", req.StartDate)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid start date format"})
|
||||
return
|
||||
}
|
||||
|
||||
endDate, err := time.Parse("2006-01-02", req.EndDate)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid end date format"})
|
||||
return
|
||||
}
|
||||
|
||||
schoolYear, err := h.schoolService.CreateSchoolYear(c.Request.Context(), schoolID, req.Name, startDate, endDate)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, schoolYear)
|
||||
}
|
||||
|
||||
// SetCurrentSchoolYear sets a school year as current
|
||||
// PUT /api/v1/schools/:id/years/:yearId/current
|
||||
func (h *SchoolHandlers) SetCurrentSchoolYear(c *gin.Context) {
|
||||
schoolIDStr := c.Param("id")
|
||||
schoolID, err := uuid.Parse(schoolIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid school ID"})
|
||||
return
|
||||
}
|
||||
|
||||
yearIDStr := c.Param("yearId")
|
||||
yearID, err := uuid.Parse(yearIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid school year ID"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.schoolService.SetCurrentSchoolYear(c.Request.Context(), schoolID, yearID); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "school year set as current"})
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Class Handlers
|
||||
// ========================================
|
||||
|
||||
// CreateClass creates a new class
|
||||
// POST /api/v1/schools/:id/classes
|
||||
func (h *SchoolHandlers) CreateClass(c *gin.Context) {
|
||||
schoolIDStr := c.Param("id")
|
||||
schoolID, err := uuid.Parse(schoolIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid school ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req models.CreateClassRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
class, err := h.schoolService.CreateClass(c.Request.Context(), schoolID, req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, class)
|
||||
}
|
||||
|
||||
// GetClass retrieves a class by ID
|
||||
// GET /api/v1/classes/:id
|
||||
func (h *SchoolHandlers) GetClass(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid class ID"})
|
||||
return
|
||||
}
|
||||
|
||||
class, err := h.schoolService.GetClass(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, class)
|
||||
}
|
||||
|
||||
// ListClasses lists all classes for a school in a school year
|
||||
// GET /api/v1/schools/:id/classes?school_year_id=...
|
||||
func (h *SchoolHandlers) ListClasses(c *gin.Context) {
|
||||
schoolIDStr := c.Param("id")
|
||||
schoolID, err := uuid.Parse(schoolIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid school ID"})
|
||||
return
|
||||
}
|
||||
|
||||
schoolYearIDStr := c.Query("school_year_id")
|
||||
if schoolYearIDStr == "" {
|
||||
// Get current school year
|
||||
schoolYear, err := h.schoolService.GetCurrentSchoolYear(c.Request.Context(), schoolID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no current school year set"})
|
||||
return
|
||||
}
|
||||
schoolYearIDStr = schoolYear.ID.String()
|
||||
}
|
||||
|
||||
schoolYearID, err := uuid.Parse(schoolYearIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid school year ID"})
|
||||
return
|
||||
}
|
||||
|
||||
classes, err := h.schoolService.ListClasses(c.Request.Context(), schoolID, schoolYearID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, classes)
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Student Handlers
|
||||
// ========================================
|
||||
|
||||
// CreateStudent creates a new student
|
||||
// POST /api/v1/schools/:id/students
|
||||
func (h *SchoolHandlers) CreateStudent(c *gin.Context) {
|
||||
schoolIDStr := c.Param("id")
|
||||
schoolID, err := uuid.Parse(schoolIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid school ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req models.CreateStudentRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
student, err := h.schoolService.CreateStudent(c.Request.Context(), schoolID, req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, student)
|
||||
}
|
||||
|
||||
// GetStudent retrieves a student by ID
|
||||
// GET /api/v1/students/:id
|
||||
func (h *SchoolHandlers) GetStudent(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := uuid.Parse(idStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid student ID"})
|
||||
return
|
||||
}
|
||||
|
||||
student, err := h.schoolService.GetStudent(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, student)
|
||||
}
|
||||
|
||||
// ListStudentsByClass lists all students in a class
|
||||
// GET /api/v1/classes/:id/students
|
||||
func (h *SchoolHandlers) ListStudentsByClass(c *gin.Context) {
|
||||
classIDStr := c.Param("id")
|
||||
classID, err := uuid.Parse(classIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid class ID"})
|
||||
return
|
||||
}
|
||||
|
||||
students, err := h.schoolService.ListStudentsByClass(c.Request.Context(), classID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, students)
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Subject Handlers
|
||||
// ========================================
|
||||
|
||||
// CreateSubject creates a new subject
|
||||
// POST /api/v1/schools/:id/subjects
|
||||
func (h *SchoolHandlers) CreateSubject(c *gin.Context) {
|
||||
schoolIDStr := c.Param("id")
|
||||
schoolID, err := uuid.Parse(schoolIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid school ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
ShortName string `json:"short_name" binding:"required"`
|
||||
Color *string `json:"color"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
subject, err := h.schoolService.CreateSubject(c.Request.Context(), schoolID, req.Name, req.ShortName, req.Color)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, subject)
|
||||
}
|
||||
|
||||
// ListSubjects lists all subjects for a school
|
||||
// GET /api/v1/schools/:id/subjects
|
||||
func (h *SchoolHandlers) ListSubjects(c *gin.Context) {
|
||||
schoolIDStr := c.Param("id")
|
||||
schoolID, err := uuid.Parse(schoolIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid school ID"})
|
||||
return
|
||||
}
|
||||
|
||||
subjects, err := h.schoolService.ListSubjects(c.Request.Context(), schoolID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, subjects)
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Attendance Handlers
|
||||
// ========================================
|
||||
|
||||
// RecordAttendance records attendance for a student
|
||||
// POST /api/v1/attendance
|
||||
func (h *SchoolHandlers) RecordAttendance(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
var req models.RecordAttendanceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
record, err := h.attendanceService.RecordAttendance(c.Request.Context(), req, userID.(uuid.UUID))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, record)
|
||||
}
|
||||
|
||||
// RecordBulkAttendance records attendance for multiple students
|
||||
// POST /api/v1/classes/:id/attendance
|
||||
func (h *SchoolHandlers) RecordBulkAttendance(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
classIDStr := c.Param("id")
|
||||
classID, err := uuid.Parse(classIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid class ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Date string `json:"date" binding:"required"`
|
||||
SlotID string `json:"slot_id" binding:"required"`
|
||||
Records []struct {
|
||||
StudentID string `json:"student_id"`
|
||||
Status string `json:"status"`
|
||||
Note *string `json:"note"`
|
||||
} `json:"records" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
slotID, err := uuid.Parse(req.SlotID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid slot ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to the expected type (without JSON tags)
|
||||
records := make([]struct {
|
||||
StudentID string
|
||||
Status string
|
||||
Note *string
|
||||
}, len(req.Records))
|
||||
for i, r := range req.Records {
|
||||
records[i] = struct {
|
||||
StudentID string
|
||||
Status string
|
||||
Note *string
|
||||
}{
|
||||
StudentID: r.StudentID,
|
||||
Status: r.Status,
|
||||
Note: r.Note,
|
||||
}
|
||||
}
|
||||
|
||||
err = h.attendanceService.RecordBulkAttendance(c.Request.Context(), classID, req.Date, slotID, records, userID.(uuid.UUID))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "attendance recorded"})
|
||||
}
|
||||
|
||||
// GetClassAttendance gets attendance for a class on a specific date
|
||||
// GET /api/v1/classes/:id/attendance?date=...
|
||||
func (h *SchoolHandlers) GetClassAttendance(c *gin.Context) {
|
||||
classIDStr := c.Param("id")
|
||||
classID, err := uuid.Parse(classIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid class ID"})
|
||||
return
|
||||
}
|
||||
|
||||
date := c.Query("date")
|
||||
if date == "" {
|
||||
date = time.Now().Format("2006-01-02")
|
||||
}
|
||||
|
||||
overview, err := h.attendanceService.GetAttendanceByClass(c.Request.Context(), classID, date)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, overview)
|
||||
}
|
||||
|
||||
// GetStudentAttendance gets attendance history for a student
|
||||
// GET /api/v1/students/:id/attendance?start_date=...&end_date=...
|
||||
func (h *SchoolHandlers) GetStudentAttendance(c *gin.Context) {
|
||||
studentIDStr := c.Param("id")
|
||||
studentID, err := uuid.Parse(studentIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid student ID"})
|
||||
return
|
||||
}
|
||||
|
||||
startDateStr := c.Query("start_date")
|
||||
endDateStr := c.Query("end_date")
|
||||
|
||||
var startDate, endDate time.Time
|
||||
if startDateStr == "" {
|
||||
startDate = time.Now().AddDate(0, -1, 0) // Last month
|
||||
} else {
|
||||
startDate, _ = time.Parse("2006-01-02", startDateStr)
|
||||
}
|
||||
|
||||
if endDateStr == "" {
|
||||
endDate = time.Now()
|
||||
} else {
|
||||
endDate, _ = time.Parse("2006-01-02", endDateStr)
|
||||
}
|
||||
|
||||
records, err := h.attendanceService.GetStudentAttendance(c.Request.Context(), studentID, startDate, endDate)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, records)
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Absence Report Handlers
|
||||
// ========================================
|
||||
|
||||
// ReportAbsence allows parents to report absence
|
||||
// POST /api/v1/absence/report
|
||||
func (h *SchoolHandlers) ReportAbsence(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
var req models.ReportAbsenceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
report, err := h.attendanceService.ReportAbsence(c.Request.Context(), req, userID.(uuid.UUID))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, report)
|
||||
}
|
||||
|
||||
// ConfirmAbsence allows teachers to confirm absence
|
||||
// PUT /api/v1/absence/:id/confirm
|
||||
func (h *SchoolHandlers) ConfirmAbsence(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
reportIDStr := c.Param("id")
|
||||
reportID, err := uuid.Parse(reportIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid report ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Status string `json:"status" binding:"required"` // "excused" or "unexcused"
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
err = h.attendanceService.ConfirmAbsence(c.Request.Context(), reportID, userID.(uuid.UUID), req.Status)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "absence confirmed"})
|
||||
}
|
||||
|
||||
// GetPendingAbsenceReports gets pending absence reports for a class
|
||||
// GET /api/v1/classes/:id/absence/pending
|
||||
func (h *SchoolHandlers) GetPendingAbsenceReports(c *gin.Context) {
|
||||
classIDStr := c.Param("id")
|
||||
classID, err := uuid.Parse(classIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid class ID"})
|
||||
return
|
||||
}
|
||||
|
||||
reports, err := h.attendanceService.GetPendingAbsenceReports(c.Request.Context(), classID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, reports)
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Grade Handlers
|
||||
// ========================================
|
||||
|
||||
// CreateGrade creates a new grade
|
||||
// POST /api/v1/grades
|
||||
func (h *SchoolHandlers) CreateGrade(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
var req models.CreateGradeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Get teacher ID from user ID
|
||||
teacher, err := h.schoolService.GetTeacherByUserID(c.Request.Context(), userID.(uuid.UUID))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "user is not a teacher"})
|
||||
return
|
||||
}
|
||||
|
||||
grade, err := h.gradeService.CreateGrade(c.Request.Context(), req, teacher.ID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, grade)
|
||||
}
|
||||
|
||||
// GetStudentGrades gets all grades for a student
|
||||
// GET /api/v1/students/:id/grades?school_year_id=...
|
||||
func (h *SchoolHandlers) GetStudentGrades(c *gin.Context) {
|
||||
studentIDStr := c.Param("id")
|
||||
studentID, err := uuid.Parse(studentIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid student ID"})
|
||||
return
|
||||
}
|
||||
|
||||
schoolYearIDStr := c.Query("school_year_id")
|
||||
if schoolYearIDStr == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "school_year_id is required"})
|
||||
return
|
||||
}
|
||||
|
||||
schoolYearID, err := uuid.Parse(schoolYearIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid school year ID"})
|
||||
return
|
||||
}
|
||||
|
||||
grades, err := h.gradeService.GetStudentGrades(c.Request.Context(), studentID, schoolYearID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, grades)
|
||||
}
|
||||
|
||||
// GetClassGrades gets grades for all students in a class for a subject (Notenspiegel)
|
||||
// GET /api/v1/classes/:id/grades/:subjectId?school_year_id=...&semester=...
|
||||
func (h *SchoolHandlers) GetClassGrades(c *gin.Context) {
|
||||
classIDStr := c.Param("id")
|
||||
classID, err := uuid.Parse(classIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid class ID"})
|
||||
return
|
||||
}
|
||||
|
||||
subjectIDStr := c.Param("subjectId")
|
||||
subjectID, err := uuid.Parse(subjectIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid subject ID"})
|
||||
return
|
||||
}
|
||||
|
||||
schoolYearIDStr := c.Query("school_year_id")
|
||||
if schoolYearIDStr == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "school_year_id is required"})
|
||||
return
|
||||
}
|
||||
|
||||
schoolYearID, err := uuid.Parse(schoolYearIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid school year ID"})
|
||||
return
|
||||
}
|
||||
|
||||
semesterStr := c.DefaultQuery("semester", "1")
|
||||
var semester int
|
||||
if semesterStr == "1" {
|
||||
semester = 1
|
||||
} else {
|
||||
semester = 2
|
||||
}
|
||||
|
||||
overviews, err := h.gradeService.GetClassGradesBySubject(c.Request.Context(), classID, subjectID, schoolYearID, semester)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, overviews)
|
||||
}
|
||||
|
||||
// GetGradeStatistics gets grade statistics for a class/subject
|
||||
// GET /api/v1/classes/:id/grades/:subjectId/stats?school_year_id=...&semester=...
|
||||
func (h *SchoolHandlers) GetGradeStatistics(c *gin.Context) {
|
||||
classIDStr := c.Param("id")
|
||||
classID, err := uuid.Parse(classIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid class ID"})
|
||||
return
|
||||
}
|
||||
|
||||
subjectIDStr := c.Param("subjectId")
|
||||
subjectID, err := uuid.Parse(subjectIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid subject ID"})
|
||||
return
|
||||
}
|
||||
|
||||
schoolYearIDStr := c.Query("school_year_id")
|
||||
if schoolYearIDStr == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "school_year_id is required"})
|
||||
return
|
||||
}
|
||||
|
||||
schoolYearID, err := uuid.Parse(schoolYearIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid school year ID"})
|
||||
return
|
||||
}
|
||||
|
||||
semesterStr := c.DefaultQuery("semester", "1")
|
||||
var semester int
|
||||
if semesterStr == "1" {
|
||||
semester = 1
|
||||
} else {
|
||||
semester = 2
|
||||
}
|
||||
|
||||
stats, err := h.gradeService.GetSubjectGradeStatistics(c.Request.Context(), classID, subjectID, schoolYearID, semester)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, stats)
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Parent Onboarding Handlers
|
||||
// ========================================
|
||||
|
||||
// GenerateOnboardingToken generates a QR code token for parent onboarding
|
||||
// POST /api/v1/onboarding/tokens
|
||||
func (h *SchoolHandlers) GenerateOnboardingToken(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
SchoolID string `json:"school_id" binding:"required"`
|
||||
ClassID string `json:"class_id" binding:"required"`
|
||||
StudentID string `json:"student_id" binding:"required"`
|
||||
Role string `json:"role"` // "parent" or "parent_representative"
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
schoolID, err := uuid.Parse(req.SchoolID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid school ID"})
|
||||
return
|
||||
}
|
||||
|
||||
classID, err := uuid.Parse(req.ClassID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid class ID"})
|
||||
return
|
||||
}
|
||||
|
||||
studentID, err := uuid.Parse(req.StudentID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid student ID"})
|
||||
return
|
||||
}
|
||||
|
||||
role := req.Role
|
||||
if role == "" {
|
||||
role = "parent"
|
||||
}
|
||||
|
||||
token, err := h.schoolService.GenerateParentOnboardingToken(c.Request.Context(), schoolID, classID, studentID, userID.(uuid.UUID), role)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Generate QR code URL
|
||||
qrURL := "/onboard-parent?token=" + token.Token
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"token": token.Token,
|
||||
"qr_url": qrURL,
|
||||
"expires_at": token.ExpiresAt,
|
||||
})
|
||||
}
|
||||
|
||||
// ValidateOnboardingToken validates an onboarding token
|
||||
// GET /api/v1/onboarding/validate?token=...
|
||||
func (h *SchoolHandlers) ValidateOnboardingToken(c *gin.Context) {
|
||||
token := c.Query("token")
|
||||
if token == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "token is required"})
|
||||
return
|
||||
}
|
||||
|
||||
onboardingToken, err := h.schoolService.ValidateOnboardingToken(c.Request.Context(), token)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "invalid or expired token"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get student and school info
|
||||
student, err := h.schoolService.GetStudent(c.Request.Context(), onboardingToken.StudentID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
class, err := h.schoolService.GetClass(c.Request.Context(), onboardingToken.ClassID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
school, err := h.schoolService.GetSchool(c.Request.Context(), onboardingToken.SchoolID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"valid": true,
|
||||
"role": onboardingToken.Role,
|
||||
"student_name": student.FirstName + " " + student.LastName,
|
||||
"class_name": class.Name,
|
||||
"school_name": school.Name,
|
||||
"expires_at": onboardingToken.ExpiresAt,
|
||||
})
|
||||
}
|
||||
|
||||
// RedeemOnboardingToken redeems a token and creates parent account
|
||||
// POST /api/v1/onboarding/redeem
|
||||
func (h *SchoolHandlers) RedeemOnboardingToken(c *gin.Context) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Token string `json:"token" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
err := h.schoolService.RedeemOnboardingToken(c.Request.Context(), req.Token, userID.(uuid.UUID))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "token redeemed successfully"})
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Register Routes
|
||||
// ========================================
|
||||
|
||||
// RegisterRoutes registers all school-related routes
|
||||
func (h *SchoolHandlers) RegisterRoutes(r *gin.RouterGroup, authMiddleware gin.HandlerFunc) {
|
||||
// Public routes (for onboarding)
|
||||
r.GET("/onboarding/validate", h.ValidateOnboardingToken)
|
||||
|
||||
// Protected routes
|
||||
protected := r.Group("")
|
||||
protected.Use(authMiddleware)
|
||||
|
||||
// Schools
|
||||
protected.POST("/schools", h.CreateSchool)
|
||||
protected.GET("/schools", h.ListSchools)
|
||||
protected.GET("/schools/:id", h.GetSchool)
|
||||
protected.POST("/schools/:id/years", h.CreateSchoolYear)
|
||||
protected.PUT("/schools/:id/years/:yearId/current", h.SetCurrentSchoolYear)
|
||||
protected.POST("/schools/:id/classes", h.CreateClass)
|
||||
protected.GET("/schools/:id/classes", h.ListClasses)
|
||||
protected.POST("/schools/:id/students", h.CreateStudent)
|
||||
protected.POST("/schools/:id/subjects", h.CreateSubject)
|
||||
protected.GET("/schools/:id/subjects", h.ListSubjects)
|
||||
|
||||
// Classes
|
||||
protected.GET("/classes/:id", h.GetClass)
|
||||
protected.GET("/classes/:id/students", h.ListStudentsByClass)
|
||||
protected.GET("/classes/:id/attendance", h.GetClassAttendance)
|
||||
protected.POST("/classes/:id/attendance", h.RecordBulkAttendance)
|
||||
protected.GET("/classes/:id/absence/pending", h.GetPendingAbsenceReports)
|
||||
protected.GET("/classes/:id/grades/:subjectId", h.GetClassGrades)
|
||||
protected.GET("/classes/:id/grades/:subjectId/stats", h.GetGradeStatistics)
|
||||
|
||||
// Students
|
||||
protected.GET("/students/:id", h.GetStudent)
|
||||
protected.GET("/students/:id/attendance", h.GetStudentAttendance)
|
||||
protected.GET("/students/:id/grades", h.GetStudentGrades)
|
||||
|
||||
// Attendance & Absence
|
||||
protected.POST("/attendance", h.RecordAttendance)
|
||||
protected.POST("/absence/report", h.ReportAbsence)
|
||||
protected.PUT("/absence/:id/confirm", h.ConfirmAbsence)
|
||||
|
||||
// Grades
|
||||
protected.POST("/grades", h.CreateGrade)
|
||||
|
||||
// Onboarding
|
||||
protected.POST("/onboarding/tokens", h.GenerateOnboardingToken)
|
||||
protected.POST("/onboarding/redeem", h.RedeemOnboardingToken)
|
||||
}
|
||||
Reference in New Issue
Block a user