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:
v0andcetinmustafa83 committed 2026-07-31 13:46:13 +00:00
1 parent 36a6206c3b
commit 8b33280c20
17 files changed
+3061 -1

No files matched your search

+119
View File
@@ -0,0 +1,119 @@
import { NextResponse } from 'next/server';
/**
* Standardized API Response Format
* All routes should use these helpers for consistency
*/
export interface StandardResponse<T = any> {
success: boolean;
data?: T;
error?: string;
timestamp: string;
path?: string;
}
/**
* Success response with data
*/
export const successResponse = <T = any>(
data: T,
status = 200,
path?: string
): NextResponse<StandardResponse<T>> => {
return NextResponse.json(
{
success: true,
data,
timestamp: new Date().toISOString(),
path,
},
{ status }
);
};
/**
* Error response
*/
export const errorResponse = (
error: string | Error,
status = 400,
path?: string
): NextResponse<StandardResponse> => {
const message = error instanceof Error ? error.message : error;
return NextResponse.json(
{
success: false,
error: message,
timestamp: new Date().toISOString(),
path,
},
{ status }
);
};
/**
* Created (201) response for POST
*/
export const createdResponse = <T = any>(
data: T,
path?: string
): NextResponse<StandardResponse<T>> => {
return successResponse(data, 201, path);
};
/**
* No content (204) response for DELETE
*/
export const noContentResponse = () => {
return new NextResponse(null, { status: 204 });
};
/**
* Unauthorized error
*/
export const unauthorizedError = (path?: string) => {
return errorResponse('Unauthorized', 401, path);
};
/**
* Forbidden error
*/
export const forbiddenError = (path?: string) => {
return errorResponse('Forbidden', 403, path);
};
/**
* Not found error
*/
export const notFoundError = (message = 'Not found', path?: string) => {
return errorResponse(message, 404, path);
};
/**
* Validation error
*/
export const validationError = (message: string, path?: string) => {
return errorResponse(message, 422, path);
};
/**
* Server error
*/
export const serverError = (error: Error | string, path?: string) => {
return errorResponse(error, 500, path);
};
/**
* Wrap handler with automatic error handling
*/
export const withErrorHandling = (handler: Function) => {
return async (...args: any[]) => {
try {
return await handler(...args);
} catch (error) {
console.error('[v0] API Error:', error);
return serverError(error instanceof Error ? error : 'Unknown error');
}
};
};
+235
View File
@@ -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,
};
}
+204
View File
@@ -0,0 +1,204 @@
'use client';
import { useCallback, useState, useEffect } from 'react';
import useSWR, { SWRConfiguration } from 'swr';
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
status: number;
}
export interface UseApiOptions extends SWRConfiguration {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
body?: any;
headers?: HeadersInit;
onSuccess?: (data: any) => void;
onError?: (error: any) => void;
skip?: boolean;
}
const apiUrl = process.env.NEXT_PUBLIC_API_URL || '';
/**
* Standardized API fetcher with error handling
*/
export const apiFetcher = async (
url: string,
options?: {
method?: string;
body?: any;
headers?: HeadersInit;
}
): Promise<any> => {
const method = options?.method || 'GET';
const headers = {
'Content-Type': 'application/json',
...options?.headers,
};
const response = await fetch(`${apiUrl}${url}`, {
method,
headers,
body: options?.body ? JSON.stringify(options.body) : undefined,
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.error || `HTTP ${response.status}`);
}
return response.json();
};
/**
* Hook for fetching data with built-in caching (GET requests)
* @param url - API endpoint
* @param options - SWR options
* @returns { data, error, isLoading, mutate }
*/
export function useApiGet<T = any>(
url: string | null,
options?: SWRConfiguration & { skip?: boolean }
) {
const { skip = false, ...swrOptions } = options || {};
const { data, error, isLoading, mutate } = useSWR<T>(
skip || !url ? null : url,
(url) => apiFetcher(url),
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
dedupingInterval: 60000,
...swrOptions,
}
);
return {
data,
error,
isLoading: isLoading && !error,
mutate,
};
}
/**
* Hook for mutations (POST, PUT, DELETE, PATCH)
* @param url - API endpoint
* @param options - Custom options including onSuccess, onError callbacks
* @returns { mutate, isLoading, error, data }
*/
export function useApiMutation<T = any>(
url: string,
options?: UseApiOptions
) {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [data, setData] = useState<T | null>(null);
const mutate = useCallback(
async (payload?: any) => {
setIsLoading(true);
setError(null);
try {
const response = await fetch(`${apiUrl}${url}`, {
method: options?.method || 'POST',
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
body: JSON.stringify(payload || options?.body),
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || `HTTP ${response.status}`);
}
setData(result.data || result);
options?.onSuccess?.(result.data || result);
return result.data || result;
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error';
setError(message);
options?.onError?.(err);
throw err;
} finally {
setIsLoading(false);
}
},
[url, options]
);
return { mutate, isLoading, error, data };
}
/**
* Hook for complex queries with multiple dependencies
* Useful for parallel requests
*/
export function useApiQueries<T = any>(
queries: Array<{ url: string | null; key?: string }>,
options?: SWRConfiguration
) {
const results = queries.map((query) => {
const { data, error, isLoading, mutate } = useApiGet(query.url, options);
return {
key: query.key || query.url,
data,
error,
isLoading,
mutate,
};
});
const isLoading = results.some((r) => r.isLoading);
const error = results.find((r) => r.error);
const data = Object.fromEntries(results.map((r) => [r.key, r.data])) as Record<
string,
T | undefined
>;
return { data, error, isLoading };
}
/**
* Imperative API call (for one-off requests)
*/
export const apiCall = async <T = any>(
url: string,
options?: {
method?: string;
body?: any;
headers?: HeadersInit;
}
): Promise<T> => {
return apiFetcher(`${apiUrl}${url}`, options);
};
/**
* Convenience methods
*/
export const apiGet = async <T = any>(url: string): Promise<T> => {
return apiCall(url, { method: 'GET' });
};
export const apiPost = async <T = any>(url: string, body?: any): Promise<T> => {
return apiCall(url, { method: 'POST', body });
};
export const apiPut = async <T = any>(url: string, body?: any): Promise<T> => {
return apiCall(url, { method: 'PUT', body });
};
export const apiDelete = async <T = any>(url: string, body?: any): Promise<T> => {
return apiCall(url, { method: 'DELETE', body });
};
export const apiPatch = async <T = any>(url: string, body?: any): Promise<T> => {
return apiCall(url, { method: 'PATCH', body });
};
+151
View File
@@ -0,0 +1,151 @@
import { NextRequest, NextResponse } from 'next/server';
import { getSession } from '@/lib/auth';
import { logComplianceAudit } from '@/lib/audit';
import { successResponse, unauthorizedError, forbiddenError } from '@/lib/api-response';
export type RouteHandler = (
request: NextRequest,
context: any
) => Promise<NextResponse>;
export interface RouteConfig {
auth?: boolean; // Require authentication
roles?: string[]; // Required roles
audit?: boolean; // Log to audit trail
}
/**
* Middleware for authentication check
*/
export const withAuth = (handler: RouteHandler) => {
return async (request: NextRequest, context: any) => {
try {
const session = await getSession();
if (!session) {
return unauthorizedError(request.url);
}
// Attach user to request for use in handler
(request as any).user = session.user;
return handler(request, context);
} catch (error) {
console.error('[v0] Auth error:', error);
return unauthorizedError(request.url);
}
};
};
/**
* Middleware for role-based access control
*/
export const withRoles = (allowedRoles: string[]) => {
return (handler: RouteHandler) => {
return async (request: NextRequest, context: any) => {
const session = await getSession();
if (!session) {
return unauthorizedError(request.url);
}
const userRole = session.user?.role;
if (!allowedRoles.includes(userRole)) {
return forbiddenError(request.url);
}
(request as any).user = session.user;
return handler(request, context);
};
};
};
/**
* Middleware for audit logging
*/
export const withAudit = (action: string) => {
return (handler: RouteHandler) => {
return async (request: NextRequest, context: any) => {
const session = await getSession();
const user = session?.user;
const response = await handler(request, context);
// Log after handler completes
if (user) {
try {
await logComplianceAudit({
userId: user.id,
schoolId: user.schoolId,
action,
entityType: request.url.split('/').pop() || 'unknown',
metadata: {
method: request.method,
path: request.url,
timestamp: new Date().toISOString(),
},
});
} catch (error) {
console.error('[v0] Audit log error:', error);
}
}
return response;
};
};
};
/**
* Middleware for request validation
*/
export const withValidation = (schema: any) => {
return (handler: RouteHandler) => {
return async (request: NextRequest, context: any) => {
try {
if (request.method === 'GET' || request.method === 'DELETE') {
return handler(request, context);
}
const body = await request.json();
const validation = schema.safeParse(body);
if (!validation.success) {
return NextResponse.json(
{
success: false,
error: 'Validation error',
details: validation.error.errors,
},
{ status: 422 }
);
}
(request as any).validatedBody = validation.data;
return handler(request, context);
} catch (error) {
return NextResponse.json(
{ success: false, error: 'Invalid request body' },
{ status: 400 }
);
}
};
};
};
/**
* Combine multiple middleware
*/
export const withMiddleware =
(...middleware: Array<(h: RouteHandler) => RouteHandler>) =>
(handler: RouteHandler) => {
return middleware.reduceRight((fn, mw) => mw(fn), handler);
};
/**
* Example usage in a route:
*
* export const GET = withMiddleware(
* withAuth,
* withRoles(['TEACHER', 'ADMIN']),
* withAudit('GET_STUDENTS')
* )(async (request) => {
* const user = (request as any).user;
* // handler logic
* });
*/
+149
View File
@@ -0,0 +1,149 @@
/**
* Signage utilities for emergency alerting system
*/
export type MessageType = 'emergency' | 'notification' | 'schedule' | 'announcement' | 'alert';
export type Priority = 'low' | 'normal' | 'high' | 'critical';
export interface SignageMessageData {
title: string;
content: string;
messageType: MessageType;
priority: Priority;
displayDuration?: number;
soundAlert?: boolean;
backgroundColor?: string;
textColor?: string;
soundFile?: string;
}
/**
* Get priority color for UI
*/
export const getPriorityColor = (priority: Priority): string => {
const colors: Record<Priority, string> = {
low: '#10b981', // green
normal: '#3b82f6', // blue
high: '#f59e0b', // amber
critical: '#ef4444', // red
};
return colors[priority] || colors.normal;
};
/**
* Get message type icon
*/
export const getMessageTypeIcon = (type: MessageType): string => {
const icons: Record<MessageType, string> = {
emergency: '🚨',
notification: '📢',
schedule: '📅',
announcement: '📣',
alert: '⚠️',
};
return icons[type] || '📢';
};
/**
* Format time display
*/
export const formatDisplayTime = (seconds: number): string => {
if (seconds < 60) return `${seconds}s`;
const minutes = Math.floor(seconds / 60);
const secs = seconds % 60;
return secs > 0 ? `${minutes}m ${secs}s` : `${minutes}m`;
};
/**
* Check if message is active
*/
export const isMessageActive = (message: {
startTime?: Date;
endTime?: Date;
}): boolean => {
const now = new Date();
if (message.startTime && now < message.startTime) return false;
if (message.endTime && now > message.endTime) return false;
return true;
};
/**
* Get contrast color for text (black or white based on background)
*/
export const getContrastColor = (hexColor?: string): string => {
if (!hexColor) return '#000000';
const r = parseInt(hexColor.slice(1, 3), 16);
const g = parseInt(hexColor.slice(3, 5), 16);
const b = parseInt(hexColor.slice(5, 7), 16);
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return luminance > 0.5 ? '#000000' : '#ffffff';
};
/**
* Validate signage message
*/
export const validateSignageMessage = (message: Partial<SignageMessageData>): string[] => {
const errors: string[] = [];
if (!message.title || message.title.trim().length === 0) {
errors.push('Title is required');
}
if (!message.content || message.content.trim().length === 0) {
errors.push('Content is required');
}
if (!message.messageType) {
errors.push('Message type is required');
}
if (!message.priority) {
errors.push('Priority is required');
}
if (message.displayDuration && message.displayDuration < 1) {
errors.push('Display duration must be at least 1 second');
}
return errors;
};
/**
* Generate emergency notification for school
*/
export const createEmergencyMessage = (
incident: string,
location: string,
additionalInfo?: string
): SignageMessageData => {
return {
title: 'NOTFALL MELDUNG',
content: `${incident}\nOrt: ${location}${additionalInfo ? '\n' + additionalInfo : ''}`,
messageType: 'emergency',
priority: 'critical',
displayDuration: 30,
soundAlert: true,
backgroundColor: '#ef4444', // red
textColor: '#ffffff', // white
};
};
/**
* Get next active message based on schedule
*/
export const getNextScheduledMessage = (
messages: Array<{
id: string;
displayOrder: number;
startTime?: Date;
endTime?: Date;
}>
): string | null => {
const activeMessages = messages
.filter((m) => isMessageActive(m))
.sort((a, b) => a.displayOrder - b.displayOrder);
return activeMessages.length > 0 ? activeMessages[0].id : null;
};