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>
252 lines
10 KiB
Go
252 lines
10 KiB
Go
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
|
|
}
|