fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
261
school-service/internal/services/gradebook_service.go
Normal file
261
school-service/internal/services/gradebook_service.go
Normal file
@@ -0,0 +1,261 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/breakpilot/school-service/internal/models"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// GradebookService handles gradebook-related operations
|
||||
type GradebookService struct {
|
||||
db *pgxpool.Pool
|
||||
}
|
||||
|
||||
// NewGradebookService creates a new GradebookService
|
||||
func NewGradebookService(db *pgxpool.Pool) *GradebookService {
|
||||
return &GradebookService{db: db}
|
||||
}
|
||||
|
||||
// Attendance Operations
|
||||
|
||||
// CreateAttendance creates or updates an attendance record
|
||||
func (s *GradebookService) CreateAttendance(ctx context.Context, req *models.CreateAttendanceRequest) (*models.Attendance, error) {
|
||||
date, _ := time.Parse("2006-01-02", req.Date)
|
||||
periods := req.Periods
|
||||
if periods == 0 {
|
||||
periods = 1
|
||||
}
|
||||
|
||||
var attendance models.Attendance
|
||||
err := s.db.QueryRow(ctx, `
|
||||
INSERT INTO attendance (student_id, date, status, periods, reason)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (student_id, date) DO UPDATE SET
|
||||
status = EXCLUDED.status,
|
||||
periods = EXCLUDED.periods,
|
||||
reason = EXCLUDED.reason
|
||||
RETURNING id, student_id, date, status, periods, reason, created_at
|
||||
`, req.StudentID, date, req.Status, periods, req.Reason).Scan(
|
||||
&attendance.ID, &attendance.StudentID, &attendance.Date, &attendance.Status, &attendance.Periods, &attendance.Reason, &attendance.CreatedAt,
|
||||
)
|
||||
return &attendance, err
|
||||
}
|
||||
|
||||
// GetClassAttendance returns attendance records for a class
|
||||
func (s *GradebookService) GetClassAttendance(ctx context.Context, classID string, startDate, endDate *time.Time) ([]models.Attendance, error) {
|
||||
query := `
|
||||
SELECT a.id, a.student_id, a.date, a.status, a.periods, a.reason, a.created_at,
|
||||
CONCAT(st.first_name, ' ', st.last_name) as student_name
|
||||
FROM attendance a
|
||||
JOIN students st ON a.student_id = st.id
|
||||
WHERE st.class_id = $1
|
||||
`
|
||||
args := []interface{}{classID}
|
||||
|
||||
if startDate != nil {
|
||||
query += ` AND a.date >= $2`
|
||||
args = append(args, startDate)
|
||||
}
|
||||
if endDate != nil {
|
||||
query += ` AND a.date <= $` + string(rune('0'+len(args)+1))
|
||||
args = append(args, endDate)
|
||||
}
|
||||
|
||||
query += ` ORDER BY a.date DESC, st.last_name, st.first_name`
|
||||
|
||||
rows, err := s.db.Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var records []models.Attendance
|
||||
for rows.Next() {
|
||||
var a models.Attendance
|
||||
if err := rows.Scan(&a.ID, &a.StudentID, &a.Date, &a.Status, &a.Periods, &a.Reason, &a.CreatedAt, &a.StudentName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
records = append(records, a)
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// GetStudentAttendance returns attendance records for a student
|
||||
func (s *GradebookService) GetStudentAttendance(ctx context.Context, studentID string) ([]models.Attendance, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT id, student_id, date, status, periods, reason, created_at
|
||||
FROM attendance
|
||||
WHERE student_id = $1
|
||||
ORDER BY date DESC
|
||||
`, studentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var records []models.Attendance
|
||||
for rows.Next() {
|
||||
var a models.Attendance
|
||||
if err := rows.Scan(&a.ID, &a.StudentID, &a.Date, &a.Status, &a.Periods, &a.Reason, &a.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
records = append(records, a)
|
||||
}
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// GetAttendanceSummary returns absence counts for a student
|
||||
func (s *GradebookService) GetAttendanceSummary(ctx context.Context, studentID string, schoolYearID string) (int, int, error) {
|
||||
var excused, unexcused int
|
||||
err := s.db.QueryRow(ctx, `
|
||||
SELECT
|
||||
COALESCE(SUM(CASE WHEN status = 'absent_excused' THEN periods ELSE 0 END), 0) as excused,
|
||||
COALESCE(SUM(CASE WHEN status = 'absent_unexcused' THEN periods ELSE 0 END), 0) as unexcused
|
||||
FROM attendance a
|
||||
JOIN students st ON a.student_id = st.id
|
||||
JOIN classes c ON st.class_id = c.id
|
||||
WHERE a.student_id = $1 AND c.school_year_id = $2
|
||||
`, studentID, schoolYearID).Scan(&excused, &unexcused)
|
||||
return excused, unexcused, err
|
||||
}
|
||||
|
||||
// DeleteAttendance deletes an attendance record
|
||||
func (s *GradebookService) DeleteAttendance(ctx context.Context, attendanceID string) error {
|
||||
_, err := s.db.Exec(ctx, `DELETE FROM attendance WHERE id = $1`, attendanceID)
|
||||
return err
|
||||
}
|
||||
|
||||
// Gradebook Entry Operations
|
||||
|
||||
// CreateGradebookEntry creates a new gradebook entry
|
||||
func (s *GradebookService) CreateGradebookEntry(ctx context.Context, req *models.CreateGradebookEntryRequest) (*models.GradebookEntry, error) {
|
||||
date, _ := time.Parse("2006-01-02", req.Date)
|
||||
|
||||
var studentID *uuid.UUID
|
||||
if req.StudentID != "" {
|
||||
id, _ := uuid.Parse(req.StudentID)
|
||||
studentID = &id
|
||||
}
|
||||
|
||||
var entry models.GradebookEntry
|
||||
err := s.db.QueryRow(ctx, `
|
||||
INSERT INTO gradebook_entries (class_id, student_id, date, entry_type, content, is_visible_to_parents)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id, class_id, student_id, date, entry_type, content, is_visible_to_parents, created_at
|
||||
`, req.ClassID, studentID, date, req.EntryType, req.Content, req.IsVisibleToParents).Scan(
|
||||
&entry.ID, &entry.ClassID, &entry.StudentID, &entry.Date, &entry.EntryType, &entry.Content, &entry.IsVisibleToParents, &entry.CreatedAt,
|
||||
)
|
||||
return &entry, err
|
||||
}
|
||||
|
||||
// GetGradebookEntries returns entries for a class
|
||||
func (s *GradebookService) GetGradebookEntries(ctx context.Context, classID string, entryType *string, startDate, endDate *time.Time) ([]models.GradebookEntry, error) {
|
||||
query := `
|
||||
SELECT ge.id, ge.class_id, ge.student_id, ge.date, ge.entry_type, ge.content, ge.is_visible_to_parents, ge.created_at,
|
||||
COALESCE(CONCAT(st.first_name, ' ', st.last_name), '') as student_name
|
||||
FROM gradebook_entries ge
|
||||
LEFT JOIN students st ON ge.student_id = st.id
|
||||
WHERE ge.class_id = $1
|
||||
`
|
||||
args := []interface{}{classID}
|
||||
argCount := 1
|
||||
|
||||
if entryType != nil {
|
||||
argCount++
|
||||
query += ` AND ge.entry_type = $` + string(rune('0'+argCount))
|
||||
args = append(args, *entryType)
|
||||
}
|
||||
if startDate != nil {
|
||||
argCount++
|
||||
query += ` AND ge.date >= $` + string(rune('0'+argCount))
|
||||
args = append(args, startDate)
|
||||
}
|
||||
if endDate != nil {
|
||||
argCount++
|
||||
query += ` AND ge.date <= $` + string(rune('0'+argCount))
|
||||
args = append(args, endDate)
|
||||
}
|
||||
|
||||
query += ` ORDER BY ge.date DESC, ge.created_at DESC`
|
||||
|
||||
rows, err := s.db.Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var entries []models.GradebookEntry
|
||||
for rows.Next() {
|
||||
var e models.GradebookEntry
|
||||
if err := rows.Scan(&e.ID, &e.ClassID, &e.StudentID, &e.Date, &e.EntryType, &e.Content, &e.IsVisibleToParents, &e.CreatedAt, &e.StudentName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries = append(entries, e)
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// GetStudentEntries returns gradebook entries for a specific student
|
||||
func (s *GradebookService) GetStudentEntries(ctx context.Context, studentID string) ([]models.GradebookEntry, error) {
|
||||
rows, err := s.db.Query(ctx, `
|
||||
SELECT ge.id, ge.class_id, ge.student_id, ge.date, ge.entry_type, ge.content, ge.is_visible_to_parents, ge.created_at,
|
||||
CONCAT(st.first_name, ' ', st.last_name) as student_name
|
||||
FROM gradebook_entries ge
|
||||
JOIN students st ON ge.student_id = st.id
|
||||
WHERE ge.student_id = $1
|
||||
ORDER BY ge.date DESC
|
||||
`, studentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var entries []models.GradebookEntry
|
||||
for rows.Next() {
|
||||
var e models.GradebookEntry
|
||||
if err := rows.Scan(&e.ID, &e.ClassID, &e.StudentID, &e.Date, &e.EntryType, &e.Content, &e.IsVisibleToParents, &e.CreatedAt, &e.StudentName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries = append(entries, e)
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// DeleteGradebookEntry deletes a gradebook entry
|
||||
func (s *GradebookService) DeleteGradebookEntry(ctx context.Context, entryID string) error {
|
||||
_, err := s.db.Exec(ctx, `DELETE FROM gradebook_entries WHERE id = $1`, entryID)
|
||||
return err
|
||||
}
|
||||
|
||||
// BulkCreateAttendance creates attendance records for multiple students at once
|
||||
func (s *GradebookService) BulkCreateAttendance(ctx context.Context, classID string, date string, records []struct {
|
||||
StudentID string
|
||||
Status models.AttendanceStatus
|
||||
Periods int
|
||||
Reason string
|
||||
}) error {
|
||||
parsedDate, _ := time.Parse("2006-01-02", date)
|
||||
|
||||
for _, r := range records {
|
||||
periods := r.Periods
|
||||
if periods == 0 {
|
||||
periods = 1
|
||||
}
|
||||
|
||||
_, err := s.db.Exec(ctx, `
|
||||
INSERT INTO attendance (student_id, date, status, periods, reason)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (student_id, date) DO UPDATE SET
|
||||
status = EXCLUDED.status,
|
||||
periods = EXCLUDED.periods,
|
||||
reason = EXCLUDED.reason
|
||||
`, r.StudentID, parsedDate, r.Status, periods, r.Reason)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user