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>
262 lines
8.3 KiB
Go
262 lines
8.3 KiB
Go
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
|
|
}
|