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:
251
school-service/internal/services/certificate_service.go
Normal file
251
school-service/internal/services/certificate_service.go
Normal file
@@ -0,0 +1,251 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user