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) } // ======================================== // 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) }