package assignments import ( "boostai-backend/internal/assignmentgen" "boostai-backend/internal/handlers/api/shared" "boostai-backend/internal/sqlc" "encoding/json" "fmt" "math" "strings" "github.com/jackc/pgx/v5/pgtype" ) func mapAssignment(assignment sqlc.Assignment) AssignmentResponse { return AssignmentResponse{ ID: assignment.ID, ClassroomID: assignment.ClassroomID, TeacherID: assignment.TeacherID, Title: assignment.Title, Instructions: shared.TextPointer(assignment.Instructions), PassThreshold: shared.NumericPointer(assignment.PassThreshold), Status: string(assignment.Status), DueAt: shared.TimePointer(assignment.DueAt), PublishedAt: shared.TimePointer(assignment.PublishedAt), CreatedAt: shared.TimePointer(assignment.CreatedAt), UpdatedAt: shared.TimePointer(assignment.UpdatedAt), } } func parseQuestionTopicValue(value string) (sqlc.QuestionTopic, error) { switch strings.TrimSpace(strings.ToLower(value)) { case string(sqlc.QuestionTopicPlaceValue): return sqlc.QuestionTopicPlaceValue, nil case string(sqlc.QuestionTopicArithmetic): return sqlc.QuestionTopicArithmetic, nil case string(sqlc.QuestionTopicNegativeNumbers): return sqlc.QuestionTopicNegativeNumbers, nil case string(sqlc.QuestionTopicBidmas): return sqlc.QuestionTopicBidmas, nil case string(sqlc.QuestionTopicFractions): return sqlc.QuestionTopicFractions, nil case string(sqlc.QuestionTopicAlgebra): return sqlc.QuestionTopicAlgebra, nil case string(sqlc.QuestionTopicGeometry): return sqlc.QuestionTopicGeometry, nil case string(sqlc.QuestionTopicData): return sqlc.QuestionTopicData, nil default: return "", fmt.Errorf("primary_topic must be one of place_value, arithmetic, negative_numbers, bidmas, fractions, algebra, geometry, or data") } } func parseQuestionDifficultyValue(value string) (sqlc.QuestionDifficulty, error) { switch strings.TrimSpace(strings.ToLower(value)) { case string(sqlc.QuestionDifficultyEasy): return sqlc.QuestionDifficultyEasy, nil case string(sqlc.QuestionDifficultyMedium): return sqlc.QuestionDifficultyMedium, nil case string(sqlc.QuestionDifficultyHard): return sqlc.QuestionDifficultyHard, nil default: return "", fmt.Errorf("difficulty must be one of easy, medium, or hard") } } func parseQuestionStatusValue(value string) (sqlc.QuestionStatus, error) { switch strings.TrimSpace(strings.ToLower(value)) { case string(sqlc.QuestionStatusDraft): return sqlc.QuestionStatusDraft, nil case string(sqlc.QuestionStatusPublished): return sqlc.QuestionStatusPublished, nil case string(sqlc.QuestionStatusArchived): return sqlc.QuestionStatusArchived, nil default: return "", fmt.Errorf("question_status must be one of draft, published, or archived") } } func mapAssignmentGenerationWeaknessSummary(summary assignmentgen.WeaknessSummary) mixedPlanWeaknessSummaryResponse { topicScores := make(map[string]float64, len(summary.TopicScores)) for topic, score := range summary.TopicScores { topicScores[string(topic)] = score } return mixedPlanWeaknessSummaryResponse{ TopicScores: topicScores, WeakTags: append([]string(nil), summary.WeakTags...), RecentIssues: append([]string(nil), summary.RecentIssues...), } } func questionTopicPointer(topic sqlc.NullQuestionTopic) *string { if !topic.Valid { return nil } value := string(topic.QuestionTopic) return &value } func questionDifficultyPointer(difficulty sqlc.NullQuestionDifficulty) *string { if !difficulty.Valid { return nil } value := string(difficulty.QuestionDifficulty) return &value } func personalizedRatioValue(value *float64) float64 { if value == nil || *value == 0 { return 0.30 } return *value } func int64Value(value *int64) int64 { if value == nil { return 0 } return *value } func int64Pointer(value int64) *int64 { return &value } func stringPointer(value string) *string { return &value } func trimmedPointerValue(value *string) string { if value == nil { return "" } return strings.TrimSpace(*value) } func mapAssignmentQuestion(question sqlc.ListQuestionsForAssignmentRow) AssignmentQuestionResponse { return AssignmentQuestionResponse{ AssignmentID: question.AssignmentID, QuestionID: question.QuestionID, Position: question.Position, AuthorTeacherID: question.AuthorTeacherID, Title: question.Title, Prompt: question.Prompt, Subject: shared.TextPointer(question.Subject), Source: shared.TextPointer(question.Source), QuestionStatus: string(question.Status), QuestionCreatedAt: shared.TimePointer(question.CreatedAt), QuestionUpdatedAt: shared.TimePointer(question.UpdatedAt), } } func mapAssignmentStudentQuestionDetail(row sqlc.ListQuestionDetailsForAssignmentStudentRow, studentID int64) AssignmentStudentQuestionDetailResponse { var answerStatus *string if row.AnswerStatus.Valid { status := string(row.AnswerStatus.AnswerStatus) answerStatus = &status } var passStatus *string if row.PassStatus.Valid { status := string(row.PassStatus.AssignmentPassStatus) passStatus = &status } var passStatusOverride *string if row.PassStatusOverride.Valid { status := string(row.PassStatusOverride.AssignmentPassStatus) passStatusOverride = &status } var nextStepOutcome *string if row.NextStepOutcome.Valid { outcome := string(row.NextStepOutcome.AssignmentNextStepOutcome) nextStepOutcome = &outcome } var reviewNeedsAttention *bool if row.AnswerID.Valid { reviewNeedsAttention = shared.BoolPointer(row.ReviewNeedsAttention) } return AssignmentStudentQuestionDetailResponse{ AssignmentID: row.AssignmentID, StudentID: studentID, QuestionID: row.QuestionID, Position: row.Position, Title: row.Title, Prompt: row.Prompt, Subject: shared.TextPointer(row.Subject), Source: shared.TextPointer(row.Source), QuestionTags: row.QuestionTags, QuestionStatus: string(row.QuestionStatus), CorrectAnswer: shared.TextPointer(row.CorrectAnswer), AssignmentAiFeedback: shared.TextPointer(row.AssignmentAiFeedback), AssignmentTeacherFeedback: shared.TextPointer(row.AssignmentTeacherFeedback), OverallScore: shared.NumericPointer(row.OverallScore), PassThreshold: shared.NumericPointer(row.PassThreshold), NextStepOutcome: nextStepOutcome, PassStatusOverride: passStatusOverride, PassStatus: passStatus, AnswerID: shared.Int64Pointer(row.AnswerID), AnswerText: shared.TextPointer(row.AnswerText), SolveMode: shared.TextPointer(row.SolveMode), WorkingSteps: shared.TextPointer(row.WorkingSteps), IsCorrect: shared.BoolPointer(row.IsCorrect), AiFeedback: shared.TextPointer(row.AiFeedback), TeacherFeedback: shared.TextPointer(row.TeacherFeedback), AnswerStatus: answerStatus, ReviewNeedsAttention: reviewNeedsAttention, ReviewIssueReason: shared.TextPointer(row.ReviewIssueReason), ReviewCorrectnessScore: shared.NumericPointer(row.ReviewCorrectnessScore), ReviewUnderstandingScore: shared.NumericPointer(row.ReviewUnderstandingScore), ReviewQuestionScore: shared.NumericPointer(row.ReviewQuestionScore), ReviewConfidence: shared.NumericPointer(row.ReviewConfidence), ReviewTags: row.ReviewTags, SubmittedAt: shared.TimePointer(row.SubmittedAt), ReviewedAt: shared.TimePointer(row.ReviewedAt), AnswerCreatedAt: shared.TimePointer(row.AnswerCreatedAt), AnswerUpdatedAt: shared.TimePointer(row.AnswerUpdatedAt), } } func mapAssignmentReviewSummary(summary sqlc.GetAssignmentReviewSummaryRow) AssignmentReviewSummaryResponse { return AssignmentReviewSummaryResponse{ AssignmentID: summary.AssignmentID, TotalQuestions: summary.TotalQuestions, TotalAssigned: summary.TotalAssigned, NotStarted: summary.NotStarted, InProgress: summary.InProgress, Submitted: summary.Submitted, Reviewed: summary.Reviewed, } } func mapAssignmentReviewQueueItem(row sqlc.ListAssignmentReviewQueueRow) AssignmentReviewQueueItemResponse { var nextStepOutcome *string if row.NextStepOutcome.Valid { outcome := string(row.NextStepOutcome.AssignmentNextStepOutcome) nextStepOutcome = &outcome } return AssignmentReviewQueueItemResponse{ AssignmentID: row.AssignmentID, StudentID: row.StudentID, NextStepOutcome: nextStepOutcome, StudentName: row.StudentName, StudentEmail: row.StudentEmail, TotalQuestions: row.TotalQuestions, AnsweredQuestions: row.AnsweredQuestions, ReviewedQuestions: row.ReviewedQuestions, SubmittedQuestions: row.SubmittedQuestions, InProgressQuestions: row.InProgressQuestions, ReviewStatus: string(row.ReviewStatus), LatestSubmittedAt: shared.TimePointer(row.LatestSubmittedAt), LatestReviewedAt: shared.TimePointer(row.LatestReviewedAt), } } func buildAssignmentCloseReadiness(queue []sqlc.ListAssignmentReviewQueueRow) assignmentCloseReadiness { blockers := make([]string, 0) if len(queue) == 0 { return assignmentCloseReadiness{ CanClose: false, Blockers: []string{"No students have been assigned yet."}, } } for _, item := range queue { name := strings.TrimSpace(item.StudentName) if name == "" { name = fmt.Sprintf("Student %d", item.StudentID) } switch { case item.SubmittedQuestions > 0 || item.ReviewStatus == sqlc.AnswerStatusSubmitted: blockers = append(blockers, fmt.Sprintf("%s still has submitted work waiting for review.", name)) case item.InProgressQuestions > 0 || item.ReviewStatus == sqlc.AnswerStatusInProgress: blockers = append(blockers, fmt.Sprintf("%s still has work in progress.", name)) case item.AnsweredQuestions == 0 || item.ReviewStatus == sqlc.AnswerStatusNotStarted: blockers = append(blockers, fmt.Sprintf("%s has not started this assignment yet.", name)) case !item.NextStepOutcome.Valid: blockers = append(blockers, fmt.Sprintf("%s still needs a next-step decision.", name)) } } return assignmentCloseReadiness{ CanClose: len(blockers) == 0, Blockers: blockers, } } func parseStoredRedoPlan(value string) (storedRedoPlan, error) { var payload storedRedoPlan if err := json.Unmarshal([]byte(value), &payload); err != nil { return storedRedoPlan{}, err } return payload, nil } func mapWeaknessSummary(studentID int64, summary weaknessSummary) StudentWeaknessSummaryResponse { return StudentWeaknessSummaryResponse{ StudentID: studentID, TopicScores: summary.TopicScores, WeakTags: summary.WeakTags, RecentIssues: summary.RecentIssues, } } func planningScore(isCorrect pgtype.Bool, understanding pgtype.Numeric) float64 { understandingValue := 0.0 if value := shared.NumericPointer(understanding); value != nil { understandingValue = *value } correctnessValue := 0.0 if isCorrect.Valid && isCorrect.Bool { correctnessValue = 1.0 } return (correctnessValue + understandingValue) / 2 } func roundToOneDecimal(value float64) float64 { return math.Round(value*10) / 10 } func allowedQuestionTopics() []string { return []string{ string(sqlc.QuestionTopicPlaceValue), string(sqlc.QuestionTopicArithmetic), string(sqlc.QuestionTopicNegativeNumbers), string(sqlc.QuestionTopicBidmas), string(sqlc.QuestionTopicFractions), string(sqlc.QuestionTopicAlgebra), string(sqlc.QuestionTopicGeometry), string(sqlc.QuestionTopicData), } } func emptyStringPointer(value string) *string { value = strings.TrimSpace(value) if value == "" { return nil } return &value } func minInt(a, b int) int { if a < b { return a } return b } func isValidAssignmentPassStatus(value string) bool { switch value { case string(sqlc.AssignmentPassStatusPending), string(sqlc.AssignmentPassStatusPass), string(sqlc.AssignmentPassStatusNoPass): return true default: return false } } func isValidAssignmentNextStepOutcome(value string) bool { switch value { case "redo", "accept", "support": return true default: return false } } func pointerToFloat64(value float64) *float64 { return &value }