feat: complete refactoring phase 1 with API, component, and business logic consolidation
Co-authored-by: cetinmustafa83 <30570791+cetinmustafa83@users.noreply.github.com>
This commit is contained in:
17 files changed
+3061
-1
No files matched your search
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* Unified Grading Engine
|
||||
* Consolidates all grading logic from:
|
||||
* - grading-view.tsx
|
||||
* - tablet-grading-view.tsx
|
||||
* - grade-analytics-view.tsx
|
||||
* - ai-grading-audit logic
|
||||
*/
|
||||
|
||||
export type GradeScale = 1 | 2 | 3 | 4 | 5 | 6; // German grading scale
|
||||
export type MasteryLevel = 'emerging' | 'developing' | 'proficient' | 'advanced';
|
||||
|
||||
export interface GradingConfig {
|
||||
scaleType: 'numeric' | 'competency' | 'points'; // numeric=1-6, competency=E/D/P/A, points=0-100
|
||||
maxScore?: number; // for point-based grading
|
||||
masteryLevels?: MasteryLevel[];
|
||||
weights?: Record<string, number>; // for weighted grading
|
||||
passingScore?: number; // default 4 for numeric, configurable otherwise
|
||||
}
|
||||
|
||||
export interface GradeInput {
|
||||
score: number | GradeScale | MasteryLevel;
|
||||
maxScore?: number;
|
||||
studentId: string;
|
||||
assessmentId: string;
|
||||
timestamp: Date;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface GradeOutput {
|
||||
originalScore: number | GradeScale | MasteryLevel;
|
||||
normalizedScore: number; // 0-100 for comparison
|
||||
grade: GradeScale | MasteryLevel; // final grade
|
||||
isPassing: boolean;
|
||||
feedback: string;
|
||||
competencyMap?: Record<string, MasteryLevel>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize different grading scales to 0-100
|
||||
*/
|
||||
export function normalizeScore(
|
||||
score: number | GradeScale | MasteryLevel,
|
||||
config: GradingConfig
|
||||
): number {
|
||||
if (typeof score === 'string') {
|
||||
// Competency level to percentage
|
||||
const masteryMap: Record<MasteryLevel, number> = {
|
||||
emerging: 25,
|
||||
developing: 50,
|
||||
proficient: 75,
|
||||
advanced: 100,
|
||||
};
|
||||
return masteryMap[score as MasteryLevel] || 0;
|
||||
}
|
||||
|
||||
if (config.scaleType === 'numeric') {
|
||||
// German scale (1-6) to percentage: 1=100%, 6=0%
|
||||
return ((7 - (score as number)) / 6) * 100;
|
||||
}
|
||||
|
||||
if (config.scaleType === 'points') {
|
||||
// Point-based to percentage
|
||||
const max = config.maxScore || 100;
|
||||
return ((score as number) / max) * 100;
|
||||
}
|
||||
|
||||
return score as number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert normalized score back to target scale
|
||||
*/
|
||||
export function denormalizeScore(
|
||||
normalized: number,
|
||||
config: GradingConfig
|
||||
): GradeScale | MasteryLevel | number {
|
||||
if (config.scaleType === 'numeric') {
|
||||
// Percentage back to German scale
|
||||
const scale = Math.round((100 - normalized) / (100 / 6)) + 1;
|
||||
return Math.max(1, Math.min(6, scale)) as GradeScale;
|
||||
}
|
||||
|
||||
if (config.scaleType === 'competency') {
|
||||
if (normalized >= 75) return 'advanced';
|
||||
if (normalized >= 50) return 'proficient';
|
||||
if (normalized >= 25) return 'developing';
|
||||
return 'emerging';
|
||||
}
|
||||
|
||||
if (config.scaleType === 'points') {
|
||||
const max = config.maxScore || 100;
|
||||
return Math.round((normalized / 100) * max);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if grade is passing
|
||||
*/
|
||||
export function isPassing(
|
||||
score: number | GradeScale | MasteryLevel,
|
||||
config: GradingConfig
|
||||
): boolean {
|
||||
const normalized = normalizeScore(score, config);
|
||||
const passingPercentage =
|
||||
config.passingScore !== undefined ? config.passingScore : 50;
|
||||
|
||||
return normalized >= passingPercentage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate feedback message based on performance
|
||||
*/
|
||||
export function generateFeedback(
|
||||
normalized: number,
|
||||
config: GradingConfig
|
||||
): string {
|
||||
const feedbackMap: Record<string, string> = {
|
||||
excellent: 'Ausgezeichnete Leistung! Weiterhin so gut!',
|
||||
good: 'Gute Leistung. Du machst Fortschritte!',
|
||||
satisfactory: 'Befriedigende Leistung. Einige Bereiche können verbessert werden.',
|
||||
adequate: 'Ausreichende Leistung. Mehr Anstrengung ist erforderlich.',
|
||||
poor: 'Unzureichende Leistung. Zusätzliche Unterstützung wird empfohlen.',
|
||||
};
|
||||
|
||||
if (normalized >= 90) return feedbackMap.excellent;
|
||||
if (normalized >= 75) return feedbackMap.good;
|
||||
if (normalized >= 60) return feedbackMap.satisfactory;
|
||||
if (normalized >= 50) return feedbackMap.adequate;
|
||||
return feedbackMap.poor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a grade with full analysis
|
||||
*/
|
||||
export function processGrade(
|
||||
input: GradeInput,
|
||||
config: GradingConfig
|
||||
): GradeOutput {
|
||||
const normalized = normalizeScore(input.score, config);
|
||||
const grade = denormalizeScore(normalized, config);
|
||||
const passing = isPassing(input.score, config);
|
||||
const feedback = generateFeedback(normalized, config);
|
||||
|
||||
return {
|
||||
originalScore: input.score,
|
||||
normalizedScore: normalized,
|
||||
grade: grade as GradeScale | MasteryLevel,
|
||||
isPassing: passing,
|
||||
feedback,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate weighted grade from multiple components
|
||||
*/
|
||||
export function calculateWeightedGrade(
|
||||
components: Array<{
|
||||
name: string;
|
||||
score: number | GradeScale | MasteryLevel;
|
||||
weight: number;
|
||||
}>,
|
||||
config: GradingConfig
|
||||
): GradeOutput {
|
||||
let totalWeight = 0;
|
||||
let weightedSum = 0;
|
||||
|
||||
for (const comp of components) {
|
||||
const normalized = normalizeScore(comp.score, config);
|
||||
const weight = config.weights?.[comp.name] || comp.weight || 1;
|
||||
|
||||
weightedSum += normalized * weight;
|
||||
totalWeight += weight;
|
||||
}
|
||||
|
||||
const finalNormalized = totalWeight > 0 ? weightedSum / totalWeight : 0;
|
||||
const grade = denormalizeScore(finalNormalized, config);
|
||||
const passing = isPassing(grade, config);
|
||||
const feedback = generateFeedback(finalNormalized, config);
|
||||
|
||||
return {
|
||||
originalScore: components.map((c) => c.score),
|
||||
normalizedScore: finalNormalized,
|
||||
grade: grade as GradeScale | MasteryLevel,
|
||||
isPassing: passing,
|
||||
feedback,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare grades (for analytics)
|
||||
*/
|
||||
export function compareGrades(
|
||||
grade1: number | GradeScale | MasteryLevel,
|
||||
grade2: number | GradeScale | MasteryLevel,
|
||||
config: GradingConfig
|
||||
): number {
|
||||
const norm1 = normalizeScore(grade1, config);
|
||||
const norm2 = normalizeScore(grade2, config);
|
||||
return norm2 - norm1; // positive = improvement, negative = decline
|
||||
}
|
||||
|
||||
/**
|
||||
* Get grading statistics
|
||||
*/
|
||||
export function getStatistics(
|
||||
grades: Array<number | GradeScale | MasteryLevel>,
|
||||
config: GradingConfig
|
||||
) {
|
||||
const normalized = grades.map((g) => normalizeScore(g, config));
|
||||
|
||||
const sum = normalized.reduce((a, b) => a + b, 0);
|
||||
const avg = sum / normalized.length;
|
||||
const sorted = [...normalized].sort((a, b) => a - b);
|
||||
const median = sorted[Math.floor(sorted.length / 2)];
|
||||
const min = Math.min(...normalized);
|
||||
const max = Math.max(...normalized);
|
||||
const stdDev = Math.sqrt(
|
||||
normalized.reduce((sq, n) => sq + Math.pow(n - avg, 2), 0) / normalized.length
|
||||
);
|
||||
|
||||
return {
|
||||
average: avg,
|
||||
median,
|
||||
min,
|
||||
max,
|
||||
stdDev,
|
||||
count: grades.length,
|
||||
passingCount: grades.filter((g) => isPassing(g, config)).length,
|
||||
passingPercentage:
|
||||
(grades.filter((g) => isPassing(g, config)).length / grades.length) * 100,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user