Initial commit: breakpilot-lehrer - Lehrer KI Platform

Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Boenisch
2026-02-11 23:47:26 +01:00
commit 5a31f52310
1224 changed files with 425430 additions and 0 deletions

View File

@@ -0,0 +1,199 @@
package handlers
import (
"net/http"
"strconv"
"github.com/breakpilot/school-service/internal/models"
"github.com/gin-gonic/gin"
)
// GetCertificateTemplates returns available certificate templates
func (h *Handler) GetCertificateTemplates(c *gin.Context) {
templates := h.certificateService.GetAvailableTemplates()
respondSuccess(c, templates)
}
// GenerateCertificate generates a certificate for a student
func (h *Handler) GenerateCertificate(c *gin.Context) {
var req models.GenerateCertificateRequest
if err := c.ShouldBindJSON(&req); err != nil {
respondError(c, http.StatusBadRequest, "Invalid request: "+err.Error())
return
}
certificate, err := h.certificateService.GenerateCertificate(c.Request.Context(), &req)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to generate certificate: "+err.Error())
return
}
respondCreated(c, certificate)
}
// GetClassCertificates returns certificates for a class
func (h *Handler) GetClassCertificates(c *gin.Context) {
classID := c.Param("classId")
semesterStr := c.DefaultQuery("semester", "1")
semester, err := strconv.Atoi(semesterStr)
if err != nil || semester < 1 || semester > 2 {
semester = 1
}
certificates, err := h.certificateService.GetCertificates(c.Request.Context(), classID, semester)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to get certificates: "+err.Error())
return
}
respondSuccess(c, certificates)
}
// GetCertificate returns a single certificate
func (h *Handler) GetCertificate(c *gin.Context) {
certificateID := c.Param("id")
certificate, err := h.certificateService.GetCertificate(c.Request.Context(), certificateID)
if err != nil {
respondError(c, http.StatusNotFound, "Certificate not found")
return
}
respondSuccess(c, certificate)
}
// UpdateCertificate updates a certificate's remarks
func (h *Handler) UpdateCertificate(c *gin.Context) {
certificateID := c.Param("id")
var req struct {
Remarks string `json:"remarks"`
}
if err := c.ShouldBindJSON(&req); err != nil {
respondError(c, http.StatusBadRequest, "Invalid request: "+err.Error())
return
}
certificate, err := h.certificateService.UpdateCertificate(c.Request.Context(), certificateID, req.Remarks)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to update certificate: "+err.Error())
return
}
respondSuccess(c, certificate)
}
// FinalizeCertificate finalizes a certificate
func (h *Handler) FinalizeCertificate(c *gin.Context) {
certificateID := c.Param("id")
if err := h.certificateService.FinalizeCertificate(c.Request.Context(), certificateID); err != nil {
respondError(c, http.StatusInternalServerError, "Failed to finalize certificate: "+err.Error())
return
}
c.JSON(http.StatusOK, gin.H{"message": "Certificate finalized"})
}
// GetCertificatePDF returns the PDF for a certificate
func (h *Handler) GetCertificatePDF(c *gin.Context) {
certificateID := c.Param("id")
pdf, err := h.certificateService.GeneratePDF(c.Request.Context(), certificateID)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to generate PDF: "+err.Error())
return
}
c.Header("Content-Type", "application/pdf")
c.Header("Content-Disposition", "attachment; filename=zeugnis.pdf")
c.Data(http.StatusOK, "application/pdf", pdf)
}
// DeleteCertificate deletes a certificate
func (h *Handler) DeleteCertificate(c *gin.Context) {
certificateID := c.Param("id")
if err := h.certificateService.DeleteCertificate(c.Request.Context(), certificateID); err != nil {
respondError(c, http.StatusInternalServerError, "Failed to delete certificate: "+err.Error())
return
}
c.JSON(http.StatusOK, gin.H{"message": "Certificate deleted"})
}
// BulkGenerateCertificates generates certificates for all students in a class
func (h *Handler) BulkGenerateCertificates(c *gin.Context) {
var req struct {
ClassID string `json:"class_id" binding:"required"`
SchoolYearID string `json:"school_year_id" binding:"required"`
Semester int `json:"semester" binding:"required,min=1,max=2"`
CertificateType models.CertificateType `json:"certificate_type" binding:"required"`
TemplateName string `json:"template_name"`
}
if err := c.ShouldBindJSON(&req); err != nil {
respondError(c, http.StatusBadRequest, "Invalid request: "+err.Error())
return
}
certificates, err := h.certificateService.BulkGenerateCertificates(c.Request.Context(), req.ClassID, req.SchoolYearID, req.Semester, req.CertificateType, req.TemplateName)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to generate certificates: "+err.Error())
return
}
respondSuccess(c, gin.H{
"message": "Certificates generated",
"count": len(certificates),
"certificates": certificates,
})
}
// GenerateGradeFeedback generates AI feedback for a student
func (h *Handler) GenerateGradeFeedback(c *gin.Context) {
studentID := c.Param("studentId")
semesterStr := c.DefaultQuery("semester", "1")
semester, err := strconv.Atoi(semesterStr)
if err != nil || semester < 1 || semester > 2 {
semester = 1
}
// Get student grades
grades, err := h.gradeService.GetStudentGrades(c.Request.Context(), studentID)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to get student grades: "+err.Error())
return
}
// Build grades map for the requested semester
gradeMap := make(map[string]float64)
var studentName string
for _, g := range grades {
if g.Semester == semester && g.FinalGrade != nil {
gradeMap[g.SubjectName] = *g.FinalGrade
studentName = g.StudentName
}
}
if len(gradeMap) == 0 {
respondError(c, http.StatusNotFound, "No grades found for this semester")
return
}
// Generate feedback using AI
feedback, err := h.aiService.GenerateGradeFeedback(c.Request.Context(), studentName, gradeMap, semester)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to generate feedback: "+err.Error())
return
}
respondSuccess(c, gin.H{
"student_id": studentID,
"student_name": studentName,
"semester": semester,
"feedback": feedback,
})
}

View File

@@ -0,0 +1,242 @@
package handlers
import (
"net/http"
"github.com/breakpilot/school-service/internal/models"
"github.com/gin-gonic/gin"
)
// School Year Handlers
// CreateSchoolYear creates a new school year
func (h *Handler) CreateSchoolYear(c *gin.Context) {
teacherID := getUserID(c)
if teacherID == "" {
respondError(c, http.StatusUnauthorized, "User not authenticated")
return
}
var req models.CreateSchoolYearRequest
if err := c.ShouldBindJSON(&req); err != nil {
respondError(c, http.StatusBadRequest, "Invalid request: "+err.Error())
return
}
year, err := h.classService.CreateSchoolYear(c.Request.Context(), teacherID, &req)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to create school year: "+err.Error())
return
}
respondCreated(c, year)
}
// GetSchoolYears returns all school years for the current teacher
func (h *Handler) GetSchoolYears(c *gin.Context) {
teacherID := getUserID(c)
if teacherID == "" {
respondError(c, http.StatusUnauthorized, "User not authenticated")
return
}
years, err := h.classService.GetSchoolYears(c.Request.Context(), teacherID)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to get school years: "+err.Error())
return
}
respondSuccess(c, years)
}
// Class Handlers
// CreateClass creates a new class
func (h *Handler) CreateClass(c *gin.Context) {
teacherID := getUserID(c)
if teacherID == "" {
respondError(c, http.StatusUnauthorized, "User not authenticated")
return
}
var req models.CreateClassRequest
if err := c.ShouldBindJSON(&req); err != nil {
respondError(c, http.StatusBadRequest, "Invalid request: "+err.Error())
return
}
class, err := h.classService.CreateClass(c.Request.Context(), teacherID, &req)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to create class: "+err.Error())
return
}
respondCreated(c, class)
}
// GetClasses returns all classes for the current teacher
func (h *Handler) GetClasses(c *gin.Context) {
teacherID := getUserID(c)
if teacherID == "" {
respondError(c, http.StatusUnauthorized, "User not authenticated")
return
}
classes, err := h.classService.GetClasses(c.Request.Context(), teacherID)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to get classes: "+err.Error())
return
}
respondSuccess(c, classes)
}
// GetClass returns a single class
func (h *Handler) GetClass(c *gin.Context) {
teacherID := getUserID(c)
classID := c.Param("id")
if teacherID == "" {
respondError(c, http.StatusUnauthorized, "User not authenticated")
return
}
class, err := h.classService.GetClass(c.Request.Context(), classID, teacherID)
if err != nil {
respondError(c, http.StatusNotFound, "Class not found")
return
}
respondSuccess(c, class)
}
// DeleteClass deletes a class
func (h *Handler) DeleteClass(c *gin.Context) {
teacherID := getUserID(c)
classID := c.Param("id")
if teacherID == "" {
respondError(c, http.StatusUnauthorized, "User not authenticated")
return
}
if err := h.classService.DeleteClass(c.Request.Context(), classID, teacherID); err != nil {
respondError(c, http.StatusInternalServerError, "Failed to delete class: "+err.Error())
return
}
c.JSON(http.StatusOK, gin.H{"message": "Class deleted"})
}
// Student Handlers
// CreateStudent creates a new student in a class
func (h *Handler) CreateStudent(c *gin.Context) {
classID := c.Param("id")
var req models.CreateStudentRequest
if err := c.ShouldBindJSON(&req); err != nil {
respondError(c, http.StatusBadRequest, "Invalid request: "+err.Error())
return
}
student, err := h.classService.CreateStudent(c.Request.Context(), classID, &req)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to create student: "+err.Error())
return
}
respondCreated(c, student)
}
// GetStudents returns all students in a class
func (h *Handler) GetStudents(c *gin.Context) {
classID := c.Param("id")
students, err := h.classService.GetStudents(c.Request.Context(), classID)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to get students: "+err.Error())
return
}
respondSuccess(c, students)
}
// DeleteStudent deletes a student
func (h *Handler) DeleteStudent(c *gin.Context) {
studentID := c.Param("studentId")
if err := h.classService.DeleteStudent(c.Request.Context(), studentID); err != nil {
respondError(c, http.StatusInternalServerError, "Failed to delete student: "+err.Error())
return
}
c.JSON(http.StatusOK, gin.H{"message": "Student deleted"})
}
// ImportStudents imports students from CSV (placeholder)
func (h *Handler) ImportStudents(c *gin.Context) {
// TODO: Implement CSV import
// For now, return a not implemented response
respondError(c, http.StatusNotImplemented, "CSV import not yet implemented")
}
// Subject Handlers
// CreateSubject creates a new subject
func (h *Handler) CreateSubject(c *gin.Context) {
teacherID := getUserID(c)
if teacherID == "" {
respondError(c, http.StatusUnauthorized, "User not authenticated")
return
}
var req models.CreateSubjectRequest
if err := c.ShouldBindJSON(&req); err != nil {
respondError(c, http.StatusBadRequest, "Invalid request: "+err.Error())
return
}
subject, err := h.classService.CreateSubject(c.Request.Context(), teacherID, &req)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to create subject: "+err.Error())
return
}
respondCreated(c, subject)
}
// GetSubjects returns all subjects for the current teacher
func (h *Handler) GetSubjects(c *gin.Context) {
teacherID := getUserID(c)
if teacherID == "" {
respondError(c, http.StatusUnauthorized, "User not authenticated")
return
}
subjects, err := h.classService.GetSubjects(c.Request.Context(), teacherID)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to get subjects: "+err.Error())
return
}
respondSuccess(c, subjects)
}
// DeleteSubject deletes a subject
func (h *Handler) DeleteSubject(c *gin.Context) {
teacherID := getUserID(c)
subjectID := c.Param("id")
if teacherID == "" {
respondError(c, http.StatusUnauthorized, "User not authenticated")
return
}
if err := h.classService.DeleteSubject(c.Request.Context(), subjectID, teacherID); err != nil {
respondError(c, http.StatusInternalServerError, "Failed to delete subject: "+err.Error())
return
}
c.JSON(http.StatusOK, gin.H{"message": "Subject deleted"})
}

View File

@@ -0,0 +1,208 @@
package handlers
import (
"net/http"
"github.com/breakpilot/school-service/internal/models"
"github.com/gin-gonic/gin"
)
// CreateExam creates a new exam
func (h *Handler) CreateExam(c *gin.Context) {
teacherID := getUserID(c)
if teacherID == "" {
respondError(c, http.StatusUnauthorized, "User not authenticated")
return
}
var req models.CreateExamRequest
if err := c.ShouldBindJSON(&req); err != nil {
respondError(c, http.StatusBadRequest, "Invalid request: "+err.Error())
return
}
exam, err := h.examService.CreateExam(c.Request.Context(), teacherID, &req)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to create exam: "+err.Error())
return
}
respondCreated(c, exam)
}
// GetExams returns all exams for the current teacher
func (h *Handler) GetExams(c *gin.Context) {
teacherID := getUserID(c)
if teacherID == "" {
respondError(c, http.StatusUnauthorized, "User not authenticated")
return
}
exams, err := h.examService.GetExams(c.Request.Context(), teacherID)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to get exams: "+err.Error())
return
}
respondSuccess(c, exams)
}
// GetExam returns a single exam
func (h *Handler) GetExam(c *gin.Context) {
teacherID := getUserID(c)
examID := c.Param("id")
if teacherID == "" {
respondError(c, http.StatusUnauthorized, "User not authenticated")
return
}
exam, err := h.examService.GetExam(c.Request.Context(), examID, teacherID)
if err != nil {
respondError(c, http.StatusNotFound, "Exam not found")
return
}
respondSuccess(c, exam)
}
// UpdateExam updates an exam
func (h *Handler) UpdateExam(c *gin.Context) {
teacherID := getUserID(c)
examID := c.Param("id")
if teacherID == "" {
respondError(c, http.StatusUnauthorized, "User not authenticated")
return
}
var req models.CreateExamRequest
if err := c.ShouldBindJSON(&req); err != nil {
respondError(c, http.StatusBadRequest, "Invalid request: "+err.Error())
return
}
exam, err := h.examService.UpdateExam(c.Request.Context(), examID, teacherID, &req)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to update exam: "+err.Error())
return
}
respondSuccess(c, exam)
}
// DeleteExam deletes an exam
func (h *Handler) DeleteExam(c *gin.Context) {
teacherID := getUserID(c)
examID := c.Param("id")
if teacherID == "" {
respondError(c, http.StatusUnauthorized, "User not authenticated")
return
}
if err := h.examService.DeleteExam(c.Request.Context(), examID, teacherID); err != nil {
respondError(c, http.StatusInternalServerError, "Failed to delete exam: "+err.Error())
return
}
c.JSON(http.StatusOK, gin.H{"message": "Exam deleted"})
}
// GenerateExamVariant generates a variant of an exam using AI
func (h *Handler) GenerateExamVariant(c *gin.Context) {
teacherID := getUserID(c)
examID := c.Param("id")
if teacherID == "" {
respondError(c, http.StatusUnauthorized, "User not authenticated")
return
}
var req models.GenerateExamVariantRequest
if err := c.ShouldBindJSON(&req); err != nil {
respondError(c, http.StatusBadRequest, "Invalid request: "+err.Error())
return
}
// Get the original exam
originalExam, err := h.examService.GetExam(c.Request.Context(), examID, teacherID)
if err != nil {
respondError(c, http.StatusNotFound, "Original exam not found")
return
}
// Generate variant content using AI
newContent, err := h.aiService.GenerateExamVariant(c.Request.Context(), originalExam.Content, req.VariationType)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to generate variant: "+err.Error())
return
}
// Create the variant exam
variant, err := h.examService.CreateExamVariant(c.Request.Context(), examID, teacherID, newContent, req.VariationType)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to save variant: "+err.Error())
return
}
respondCreated(c, variant)
}
// SaveExamResults saves results for students
func (h *Handler) SaveExamResults(c *gin.Context) {
examID := c.Param("id")
var req models.UpdateExamResultRequest
if err := c.ShouldBindJSON(&req); err != nil {
respondError(c, http.StatusBadRequest, "Invalid request: "+err.Error())
return
}
result, err := h.examService.SaveExamResult(c.Request.Context(), examID, &req)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to save result: "+err.Error())
return
}
respondSuccess(c, result)
}
// GetExamResults returns all results for an exam
func (h *Handler) GetExamResults(c *gin.Context) {
examID := c.Param("id")
results, err := h.examService.GetExamResults(c.Request.Context(), examID)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to get results: "+err.Error())
return
}
respondSuccess(c, results)
}
// ApproveExamResult approves a result for grade transfer
func (h *Handler) ApproveExamResult(c *gin.Context) {
examID := c.Param("id")
studentID := c.Param("studentId")
if err := h.examService.ApproveExamResult(c.Request.Context(), examID, studentID); err != nil {
respondError(c, http.StatusInternalServerError, "Failed to approve result: "+err.Error())
return
}
c.JSON(http.StatusOK, gin.H{"message": "Result approved"})
}
// GetStudentsNeedingRewrite returns students who need to rewrite
func (h *Handler) GetStudentsNeedingRewrite(c *gin.Context) {
examID := c.Param("id")
students, err := h.examService.GetStudentsNeedingRewrite(c.Request.Context(), examID)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to get students: "+err.Error())
return
}
respondSuccess(c, students)
}

View File

@@ -0,0 +1,216 @@
package handlers
import (
"net/http"
"strconv"
"github.com/breakpilot/school-service/internal/models"
"github.com/gin-gonic/gin"
)
// GetClassGrades returns grades for a class
func (h *Handler) GetClassGrades(c *gin.Context) {
classID := c.Param("classId")
semesterStr := c.DefaultQuery("semester", "1")
semester, err := strconv.Atoi(semesterStr)
if err != nil || semester < 1 || semester > 2 {
semester = 1
}
grades, err := h.gradeService.GetGradeOverview(c.Request.Context(), classID, semester)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to get grades: "+err.Error())
return
}
respondSuccess(c, grades)
}
// GetStudentGrades returns all grades for a student
func (h *Handler) GetStudentGrades(c *gin.Context) {
studentID := c.Param("studentId")
grades, err := h.gradeService.GetStudentGrades(c.Request.Context(), studentID)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to get grades: "+err.Error())
return
}
respondSuccess(c, grades)
}
// UpdateOralGrade updates the oral grade for a student
func (h *Handler) UpdateOralGrade(c *gin.Context) {
studentID := c.Param("studentId")
subjectID := c.Param("subjectId")
var req models.UpdateOralGradeRequest
if err := c.ShouldBindJSON(&req); err != nil {
respondError(c, http.StatusBadRequest, "Invalid request: "+err.Error())
return
}
grade, err := h.gradeService.UpdateOralGrade(c.Request.Context(), studentID, subjectID, &req)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to update oral grade: "+err.Error())
return
}
respondSuccess(c, grade)
}
// CalculateFinalGrades calculates final grades for a class
func (h *Handler) CalculateFinalGrades(c *gin.Context) {
var req struct {
ClassID string `json:"class_id" binding:"required"`
Semester int `json:"semester" binding:"required,min=1,max=2"`
}
if err := c.ShouldBindJSON(&req); err != nil {
respondError(c, http.StatusBadRequest, "Invalid request: "+err.Error())
return
}
if err := h.gradeService.CalculateFinalGrades(c.Request.Context(), req.ClassID, req.Semester); err != nil {
respondError(c, http.StatusInternalServerError, "Failed to calculate grades: "+err.Error())
return
}
c.JSON(http.StatusOK, gin.H{"message": "Grades calculated successfully"})
}
// TransferApprovedGrades transfers approved exam results to grade overview
func (h *Handler) TransferApprovedGrades(c *gin.Context) {
teacherID := getUserID(c)
if teacherID == "" {
respondError(c, http.StatusUnauthorized, "User not authenticated")
return
}
if err := h.gradeService.TransferApprovedGrades(c.Request.Context(), teacherID); err != nil {
respondError(c, http.StatusInternalServerError, "Failed to transfer grades: "+err.Error())
return
}
c.JSON(http.StatusOK, gin.H{"message": "Grades transferred successfully"})
}
// LockFinalGrade locks a final grade
func (h *Handler) LockFinalGrade(c *gin.Context) {
studentID := c.Param("studentId")
subjectID := c.Param("subjectId")
semesterStr := c.DefaultQuery("semester", "1")
semester, err := strconv.Atoi(semesterStr)
if err != nil || semester < 1 || semester > 2 {
semester = 1
}
if err := h.gradeService.LockFinalGrade(c.Request.Context(), studentID, subjectID, semester); err != nil {
respondError(c, http.StatusInternalServerError, "Failed to lock grade: "+err.Error())
return
}
c.JSON(http.StatusOK, gin.H{"message": "Grade locked"})
}
// UpdateGradeWeights updates the grade weights
func (h *Handler) UpdateGradeWeights(c *gin.Context) {
studentID := c.Param("studentId")
subjectID := c.Param("subjectId")
var req struct {
WrittenWeight int `json:"written_weight" binding:"required,min=0,max=100"`
OralWeight int `json:"oral_weight" binding:"required,min=0,max=100"`
}
if err := c.ShouldBindJSON(&req); err != nil {
respondError(c, http.StatusBadRequest, "Invalid request: "+err.Error())
return
}
if req.WrittenWeight+req.OralWeight != 100 {
respondError(c, http.StatusBadRequest, "Weights must sum to 100")
return
}
if err := h.gradeService.UpdateGradeWeights(c.Request.Context(), studentID, subjectID, req.WrittenWeight, req.OralWeight); err != nil {
respondError(c, http.StatusInternalServerError, "Failed to update weights: "+err.Error())
return
}
c.JSON(http.StatusOK, gin.H{"message": "Weights updated"})
}
// =============================================
// STATISTICS ENDPOINTS
// =============================================
// GetClassStatistics returns grade statistics for a class
// GET /api/v1/school/statistics/:classId
func (h *Handler) GetClassStatistics(c *gin.Context) {
classID := c.Param("classId")
semesterStr := c.DefaultQuery("semester", "0") // 0 = both semesters
semester, _ := strconv.Atoi(semesterStr)
stats, err := h.gradeService.GetClassStatistics(c.Request.Context(), classID, semester)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to get statistics: "+err.Error())
return
}
respondSuccess(c, stats)
}
// GetSubjectStatistics returns grade statistics for a specific subject in a class
// GET /api/v1/school/statistics/:classId/subject/:subjectId
func (h *Handler) GetSubjectStatistics(c *gin.Context) {
classID := c.Param("classId")
subjectID := c.Param("subjectId")
semesterStr := c.DefaultQuery("semester", "0")
semester, _ := strconv.Atoi(semesterStr)
stats, err := h.gradeService.GetSubjectStatistics(c.Request.Context(), classID, subjectID, semester)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to get statistics: "+err.Error())
return
}
respondSuccess(c, stats)
}
// GetStudentStatistics returns grade statistics for a specific student
// GET /api/v1/school/statistics/student/:studentId
func (h *Handler) GetStudentStatistics(c *gin.Context) {
studentID := c.Param("studentId")
stats, err := h.gradeService.GetStudentStatistics(c.Request.Context(), studentID)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to get statistics: "+err.Error())
return
}
respondSuccess(c, stats)
}
// GetNotenspiegel returns the grade distribution for a class or exam
// GET /api/v1/school/statistics/:classId/notenspiegel
func (h *Handler) GetNotenspiegel(c *gin.Context) {
classID := c.Param("classId")
subjectID := c.Query("subject_id")
examID := c.Query("exam_id")
semesterStr := c.DefaultQuery("semester", "0")
semester, _ := strconv.Atoi(semesterStr)
notenspiegel, err := h.gradeService.GetNotenspiegel(c.Request.Context(), classID, subjectID, examID, semester)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to get Notenspiegel: "+err.Error())
return
}
respondSuccess(c, notenspiegel)
}

View File

@@ -0,0 +1,209 @@
package handlers
import (
"net/http"
"time"
"github.com/breakpilot/school-service/internal/models"
"github.com/gin-gonic/gin"
)
// Attendance Handlers
// CreateAttendance creates an attendance record
func (h *Handler) CreateAttendance(c *gin.Context) {
var req models.CreateAttendanceRequest
if err := c.ShouldBindJSON(&req); err != nil {
respondError(c, http.StatusBadRequest, "Invalid request: "+err.Error())
return
}
attendance, err := h.gradebookService.CreateAttendance(c.Request.Context(), &req)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to create attendance: "+err.Error())
return
}
respondCreated(c, attendance)
}
// GetClassAttendance returns attendance for a class
func (h *Handler) GetClassAttendance(c *gin.Context) {
classID := c.Param("classId")
startDateStr := c.Query("start_date")
endDateStr := c.Query("end_date")
var startDate, endDate *time.Time
if startDateStr != "" {
t, err := time.Parse("2006-01-02", startDateStr)
if err == nil {
startDate = &t
}
}
if endDateStr != "" {
t, err := time.Parse("2006-01-02", endDateStr)
if err == nil {
endDate = &t
}
}
attendance, err := h.gradebookService.GetClassAttendance(c.Request.Context(), classID, startDate, endDate)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to get attendance: "+err.Error())
return
}
respondSuccess(c, attendance)
}
// GetStudentAttendance returns attendance for a student
func (h *Handler) GetStudentAttendance(c *gin.Context) {
studentID := c.Param("studentId")
attendance, err := h.gradebookService.GetStudentAttendance(c.Request.Context(), studentID)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to get attendance: "+err.Error())
return
}
respondSuccess(c, attendance)
}
// DeleteAttendance deletes an attendance record
func (h *Handler) DeleteAttendance(c *gin.Context) {
attendanceID := c.Param("id")
if err := h.gradebookService.DeleteAttendance(c.Request.Context(), attendanceID); err != nil {
respondError(c, http.StatusInternalServerError, "Failed to delete attendance: "+err.Error())
return
}
c.JSON(http.StatusOK, gin.H{"message": "Attendance deleted"})
}
// BulkCreateAttendance creates attendance for multiple students
func (h *Handler) BulkCreateAttendance(c *gin.Context) {
classID := c.Param("classId")
var req struct {
Date string `json:"date" binding:"required"`
Records []struct {
StudentID string `json:"student_id" binding:"required"`
Status models.AttendanceStatus `json:"status" binding:"required"`
Periods int `json:"periods"`
Reason string `json:"reason"`
} `json:"records" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
respondError(c, http.StatusBadRequest, "Invalid request: "+err.Error())
return
}
records := make([]struct {
StudentID string
Status models.AttendanceStatus
Periods int
Reason string
}, len(req.Records))
for i, r := range req.Records {
records[i] = struct {
StudentID string
Status models.AttendanceStatus
Periods int
Reason string
}{
StudentID: r.StudentID,
Status: r.Status,
Periods: r.Periods,
Reason: r.Reason,
}
}
if err := h.gradebookService.BulkCreateAttendance(c.Request.Context(), classID, req.Date, records); err != nil {
respondError(c, http.StatusInternalServerError, "Failed to create attendance: "+err.Error())
return
}
c.JSON(http.StatusOK, gin.H{"message": "Attendance created"})
}
// Gradebook Entry Handlers
// CreateGradebookEntry creates a gradebook entry
func (h *Handler) CreateGradebookEntry(c *gin.Context) {
var req models.CreateGradebookEntryRequest
if err := c.ShouldBindJSON(&req); err != nil {
respondError(c, http.StatusBadRequest, "Invalid request: "+err.Error())
return
}
entry, err := h.gradebookService.CreateGradebookEntry(c.Request.Context(), &req)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to create entry: "+err.Error())
return
}
respondCreated(c, entry)
}
// GetGradebookEntries returns entries for a class
func (h *Handler) GetGradebookEntries(c *gin.Context) {
classID := c.Param("classId")
entryTypeStr := c.Query("entry_type")
startDateStr := c.Query("start_date")
endDateStr := c.Query("end_date")
var entryType *string
if entryTypeStr != "" {
entryType = &entryTypeStr
}
var startDate, endDate *time.Time
if startDateStr != "" {
t, err := time.Parse("2006-01-02", startDateStr)
if err == nil {
startDate = &t
}
}
if endDateStr != "" {
t, err := time.Parse("2006-01-02", endDateStr)
if err == nil {
endDate = &t
}
}
entries, err := h.gradebookService.GetGradebookEntries(c.Request.Context(), classID, entryType, startDate, endDate)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to get entries: "+err.Error())
return
}
respondSuccess(c, entries)
}
// GetStudentEntries returns gradebook entries for a student
func (h *Handler) GetStudentEntries(c *gin.Context) {
studentID := c.Param("studentId")
entries, err := h.gradebookService.GetStudentEntries(c.Request.Context(), studentID)
if err != nil {
respondError(c, http.StatusInternalServerError, "Failed to get entries: "+err.Error())
return
}
respondSuccess(c, entries)
}
// DeleteGradebookEntry deletes a gradebook entry
func (h *Handler) DeleteGradebookEntry(c *gin.Context) {
entryID := c.Param("id")
if err := h.gradebookService.DeleteGradebookEntry(c.Request.Context(), entryID); err != nil {
respondError(c, http.StatusInternalServerError, "Failed to delete entry: "+err.Error())
return
}
c.JSON(http.StatusOK, gin.H{"message": "Entry deleted"})
}

View File

@@ -0,0 +1,66 @@
package handlers
import (
"net/http"
"github.com/breakpilot/school-service/internal/services"
"github.com/gin-gonic/gin"
"github.com/jackc/pgx/v5/pgxpool"
)
// Handler holds all the service dependencies
type Handler struct {
classService *services.ClassService
examService *services.ExamService
gradeService *services.GradeService
gradebookService *services.GradebookService
certificateService *services.CertificateService
aiService *services.AIService
}
// NewHandler creates a new Handler with all services
func NewHandler(db *pgxpool.Pool, llmGatewayURL string) *Handler {
classService := services.NewClassService(db)
examService := services.NewExamService(db)
gradeService := services.NewGradeService(db)
gradebookService := services.NewGradebookService(db)
certificateService := services.NewCertificateService(db, gradeService, gradebookService)
aiService := services.NewAIService(llmGatewayURL)
return &Handler{
classService: classService,
examService: examService,
gradeService: gradeService,
gradebookService: gradebookService,
certificateService: certificateService,
aiService: aiService,
}
}
// Health returns the service health status
func (h *Handler) Health(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
"service": "school-service",
})
}
// getUserID extracts the user ID from the context (set by auth middleware)
func getUserID(c *gin.Context) string {
return c.GetString("user_id")
}
// respondError sends an error response
func respondError(c *gin.Context, status int, message string) {
c.JSON(status, gin.H{"error": message})
}
// respondSuccess sends a success response with data
func respondSuccess(c *gin.Context, data interface{}) {
c.JSON(http.StatusOK, data)
}
// respondCreated sends a created response with data
func respondCreated(c *gin.Context, data interface{}) {
c.JSON(http.StatusCreated, data)
}