package services import ( "context" "encoding/json" "fmt" "github.com/breakpilot/school-service/internal/models" "github.com/jackc/pgx/v5/pgxpool" ) // CertificateService handles certificate-related operations type CertificateService struct { db *pgxpool.Pool gradeService *GradeService gradebookService *GradebookService } // NewCertificateService creates a new CertificateService func NewCertificateService(db *pgxpool.Pool, gradeService *GradeService, gradebookService *GradebookService) *CertificateService { return &CertificateService{ db: db, gradeService: gradeService, gradebookService: gradebookService, } } // CertificateTemplate represents a certificate template type CertificateTemplate struct { Name string `json:"name"` FederalState string `json:"federal_state"` SchoolType string `json:"school_type"` GradeLevel string `json:"grade_level"` // "1-4", "5-10", "11-13" TemplatePath string `json:"template_path"` } // GetAvailableTemplates returns available certificate templates func (s *CertificateService) GetAvailableTemplates() []CertificateTemplate { // In a real implementation, these would be loaded from a templates directory return []CertificateTemplate{ {Name: "Halbjahreszeugnis Grundschule", FederalState: "generic", SchoolType: "grundschule", GradeLevel: "1-4", TemplatePath: "generic/grundschule_halbjahr.html"}, {Name: "Jahreszeugnis Grundschule", FederalState: "generic", SchoolType: "grundschule", GradeLevel: "1-4", TemplatePath: "generic/grundschule_jahr.html"}, {Name: "Halbjahreszeugnis Sek I", FederalState: "generic", SchoolType: "sek1", GradeLevel: "5-10", TemplatePath: "generic/sek1_halbjahr.html"}, {Name: "Jahreszeugnis Sek I", FederalState: "generic", SchoolType: "sek1", GradeLevel: "5-10", TemplatePath: "generic/sek1_jahr.html"}, {Name: "Halbjahreszeugnis Sek II", FederalState: "generic", SchoolType: "sek2", GradeLevel: "11-13", TemplatePath: "generic/sek2_halbjahr.html"}, {Name: "Abiturzeugnis", FederalState: "generic", SchoolType: "sek2", GradeLevel: "11-13", TemplatePath: "generic/abitur.html"}, // Niedersachsen specific {Name: "Halbjahreszeugnis Gymnasium (NI)", FederalState: "niedersachsen", SchoolType: "gymnasium", GradeLevel: "5-10", TemplatePath: "niedersachsen/gymnasium_halbjahr.html"}, // NRW specific {Name: "Halbjahreszeugnis Gymnasium (NRW)", FederalState: "nrw", SchoolType: "gymnasium", GradeLevel: "5-10", TemplatePath: "nrw/gymnasium_halbjahr.html"}, } } // GenerateCertificate generates a certificate for a student func (s *CertificateService) GenerateCertificate(ctx context.Context, req *models.GenerateCertificateRequest) (*models.Certificate, error) { // Get student grades grades, err := s.gradeService.GetStudentGrades(ctx, req.StudentID) if err != nil { return nil, fmt.Errorf("failed to get student grades: %w", err) } // Filter grades for the requested school year and semester var relevantGrades []models.GradeOverview for _, g := range grades { if g.SchoolYearID.String() == req.SchoolYearID && g.Semester == req.Semester { relevantGrades = append(relevantGrades, g) } } // Get attendance summary excusedDays, unexcusedDays, err := s.gradebookService.GetAttendanceSummary(ctx, req.StudentID, req.SchoolYearID) if err != nil { // Non-fatal, continue with zero absences excusedDays, unexcusedDays = 0, 0 } // Build grades JSON gradesMap := make(map[string]interface{}) for _, g := range relevantGrades { gradesMap[g.SubjectName] = map[string]interface{}{ "written_avg": g.WrittenGradeAvg, "oral": g.OralGrade, "final": g.FinalGrade, "final_locked": g.FinalGradeLocked, } } gradesJSON, _ := json.Marshal(gradesMap) // Create certificate record var certificate models.Certificate err = s.db.QueryRow(ctx, ` INSERT INTO certificates (student_id, school_year_id, semester, certificate_type, template_name, grades_json, remarks, absence_days, absence_days_unexcused, status) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, 'draft') RETURNING id, student_id, school_year_id, semester, certificate_type, template_name, grades_json, remarks, absence_days, absence_days_unexcused, generated_pdf_path, status, created_at, updated_at `, req.StudentID, req.SchoolYearID, req.Semester, req.CertificateType, req.TemplateName, gradesJSON, req.Remarks, excusedDays+unexcusedDays, unexcusedDays).Scan( &certificate.ID, &certificate.StudentID, &certificate.SchoolYearID, &certificate.Semester, &certificate.CertificateType, &certificate.TemplateName, &certificate.GradesJSON, &certificate.Remarks, &certificate.AbsenceDays, &certificate.AbsenceDaysUnexcused, &certificate.GeneratedPDFPath, &certificate.Status, &certificate.CreatedAt, &certificate.UpdatedAt, ) if err != nil { return nil, err } return &certificate, nil } // GetCertificates returns certificates for a class func (s *CertificateService) GetCertificates(ctx context.Context, classID string, semester int) ([]models.Certificate, error) { rows, err := s.db.Query(ctx, ` SELECT c.id, c.student_id, c.school_year_id, c.semester, c.certificate_type, c.template_name, c.grades_json, c.remarks, c.absence_days, c.absence_days_unexcused, c.generated_pdf_path, c.status, c.created_at, c.updated_at, CONCAT(st.first_name, ' ', st.last_name) as student_name, cl.name as class_name FROM certificates c JOIN students st ON c.student_id = st.id JOIN classes cl ON st.class_id = cl.id WHERE cl.id = $1 AND c.semester = $2 ORDER BY st.last_name, st.first_name `, classID, semester) if err != nil { return nil, err } defer rows.Close() var certificates []models.Certificate for rows.Next() { var cert models.Certificate if err := rows.Scan(&cert.ID, &cert.StudentID, &cert.SchoolYearID, &cert.Semester, &cert.CertificateType, &cert.TemplateName, &cert.GradesJSON, &cert.Remarks, &cert.AbsenceDays, &cert.AbsenceDaysUnexcused, &cert.GeneratedPDFPath, &cert.Status, &cert.CreatedAt, &cert.UpdatedAt, &cert.StudentName, &cert.ClassName); err != nil { return nil, err } certificates = append(certificates, cert) } return certificates, nil } // GetCertificate returns a single certificate func (s *CertificateService) GetCertificate(ctx context.Context, certificateID string) (*models.Certificate, error) { var cert models.Certificate err := s.db.QueryRow(ctx, ` SELECT c.id, c.student_id, c.school_year_id, c.semester, c.certificate_type, c.template_name, c.grades_json, c.remarks, c.absence_days, c.absence_days_unexcused, c.generated_pdf_path, c.status, c.created_at, c.updated_at, CONCAT(st.first_name, ' ', st.last_name) as student_name, cl.name as class_name FROM certificates c JOIN students st ON c.student_id = st.id JOIN classes cl ON st.class_id = cl.id WHERE c.id = $1 `, certificateID).Scan( &cert.ID, &cert.StudentID, &cert.SchoolYearID, &cert.Semester, &cert.CertificateType, &cert.TemplateName, &cert.GradesJSON, &cert.Remarks, &cert.AbsenceDays, &cert.AbsenceDaysUnexcused, &cert.GeneratedPDFPath, &cert.Status, &cert.CreatedAt, &cert.UpdatedAt, &cert.StudentName, &cert.ClassName, ) return &cert, err } // UpdateCertificate updates a certificate func (s *CertificateService) UpdateCertificate(ctx context.Context, certificateID string, remarks string) (*models.Certificate, error) { var cert models.Certificate err := s.db.QueryRow(ctx, ` UPDATE certificates SET remarks = $2, updated_at = NOW() WHERE id = $1 RETURNING id, student_id, school_year_id, semester, certificate_type, template_name, grades_json, remarks, absence_days, absence_days_unexcused, generated_pdf_path, status, created_at, updated_at `, certificateID, remarks).Scan( &cert.ID, &cert.StudentID, &cert.SchoolYearID, &cert.Semester, &cert.CertificateType, &cert.TemplateName, &cert.GradesJSON, &cert.Remarks, &cert.AbsenceDays, &cert.AbsenceDaysUnexcused, &cert.GeneratedPDFPath, &cert.Status, &cert.CreatedAt, &cert.UpdatedAt, ) return &cert, err } // FinalizeCertificate finalizes a certificate (prevents further changes) func (s *CertificateService) FinalizeCertificate(ctx context.Context, certificateID string) error { _, err := s.db.Exec(ctx, ` UPDATE certificates SET status = 'final', updated_at = NOW() WHERE id = $1 `, certificateID) return err } // GeneratePDF generates a PDF for a certificate // In a real implementation, this would use a PDF generation library func (s *CertificateService) GeneratePDF(ctx context.Context, certificateID string) ([]byte, error) { cert, err := s.GetCertificate(ctx, certificateID) if err != nil { return nil, err } // Placeholder: In reality, this would: // 1. Load the HTML template // 2. Fill in student data, grades, attendance // 3. Convert to PDF using a library like wkhtmltopdf or chromedp // For now, return a simple text representation content := fmt.Sprintf(` ZEUGNIS Schüler/in: %s Klasse: %s Schuljahr: Halbjahr %d Typ: %s Noten: %v Fehlzeiten: %d Tage (davon %d unentschuldigt) Bemerkungen: %s Status: %s `, cert.StudentName, cert.ClassName, cert.Semester, cert.CertificateType, cert.GradesJSON, cert.AbsenceDays, cert.AbsenceDaysUnexcused, cert.Remarks, cert.Status) return []byte(content), nil } // DeleteCertificate deletes a certificate (only if draft) func (s *CertificateService) DeleteCertificate(ctx context.Context, certificateID string) error { _, err := s.db.Exec(ctx, `DELETE FROM certificates WHERE id = $1 AND status = 'draft'`, certificateID) return err } // BulkGenerateCertificates generates certificates for all students in a class func (s *CertificateService) BulkGenerateCertificates(ctx context.Context, classID, schoolYearID string, semester int, certificateType models.CertificateType, templateName string) ([]models.Certificate, error) { // Get all students in the class rows, err := s.db.Query(ctx, `SELECT id FROM students WHERE class_id = $1`, classID) if err != nil { return nil, err } defer rows.Close() var studentIDs []string for rows.Next() { var id string if err := rows.Scan(&id); err != nil { return nil, err } studentIDs = append(studentIDs, id) } // Generate certificate for each student var certificates []models.Certificate for _, studentID := range studentIDs { cert, err := s.GenerateCertificate(ctx, &models.GenerateCertificateRequest{ StudentID: studentID, SchoolYearID: schoolYearID, Semester: semester, CertificateType: certificateType, TemplateName: templateName, }) if err != nil { // Log error but continue with other students continue } certificates = append(certificates, *cert) } return certificates, nil }