prepare( 'SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :table_name AND COLUMN_NAME = :column_name' ); $stmt->execute([ ':table_name' => $table, ':column_name' => $column, ]); return (int) $stmt->fetchColumn() > 0; } function schema_table_has_index(PDO $pdo, string $table, string $index): bool { $stmt = $pdo->prepare( 'SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :table_name AND INDEX_NAME = :index_name' ); $stmt->execute([ ':table_name' => $table, ':index_name' => $index, ]); return (int) $stmt->fetchColumn() > 0; } function schema_table_has_foreign_key(PDO $pdo, string $table, string $constraint): bool { $stmt = $pdo->prepare( 'SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :table_name AND CONSTRAINT_NAME = :constraint_name AND CONSTRAINT_TYPE = "FOREIGN KEY"' ); $stmt->execute([ ':table_name' => $table, ':constraint_name' => $constraint, ]); return (int) $stmt->fetchColumn() > 0; } function ensure_school_cycle_schema(PDO $pdo): void { static $done = false; if ($done) { return; } $migrationPath = __DIR__ . '/../db/migrations/20260416_school_cycles.sql'; if (is_file($migrationPath)) { $sql = file_get_contents($migrationPath); if (is_string($sql) && trim($sql) !== '') { $pdo->exec($sql); } } $tables = [ 'school_students' => [ 'index' => 'idx_school_students_cycle', 'foreign_key' => 'fk_school_students_cycle', ], 'school_teachers' => [ 'index' => 'idx_school_teachers_cycle', 'foreign_key' => 'fk_school_teachers_cycle', ], 'school_assessment_types' => [ 'index' => 'idx_school_assessments_cycle', 'foreign_key' => 'fk_school_assessments_cycle', ], 'school_attendance_records' => [ 'index' => 'idx_school_attendance_cycle', 'foreign_key' => 'fk_school_attendance_cycle', ], ]; foreach ($tables as $table => $meta) { if (!schema_table_has_column($pdo, $table, 'cycle_id')) { $pdo->exec('ALTER TABLE ' . $table . ' ADD COLUMN cycle_id INT UNSIGNED NULL AFTER center_application_id'); } if (!schema_table_has_index($pdo, $table, $meta['index'])) { $pdo->exec('ALTER TABLE ' . $table . ' ADD INDEX ' . $meta['index'] . ' (cycle_id)'); } } ensure_school_cycle_backfill($pdo); if (schema_table_has_index($pdo, 'school_students', 'uniq_school_student_code') && !schema_table_has_index($pdo, 'school_students', 'uniq_school_student_cycle_code')) { $pdo->exec('ALTER TABLE school_students DROP INDEX uniq_school_student_code'); } if (!schema_table_has_index($pdo, 'school_students', 'uniq_school_student_cycle_code')) { $pdo->exec('ALTER TABLE school_students ADD UNIQUE KEY uniq_school_student_cycle_code (center_application_id, cycle_id, student_code)'); } foreach ($tables as $table => $meta) { if (!schema_table_has_foreign_key($pdo, $table, $meta['foreign_key'])) { $pdo->exec( 'ALTER TABLE ' . $table . ' ADD CONSTRAINT ' . $meta['foreign_key'] . ' FOREIGN KEY (cycle_id) REFERENCES school_cycles(id) ON DELETE CASCADE' ); } } $done = true; } function ensure_school_assessment_score_schema(PDO $pdo): void { static $done = false; if ($done) { return; } $migrationPath = __DIR__ . '/../db/migrations/20260417_school_assessment_scores.sql'; if (is_file($migrationPath)) { $sql = file_get_contents($migrationPath); if (is_string($sql) && trim($sql) !== '') { $pdo->exec($sql); } } $done = true; } function ensure_school_assessment_criteria_schema(PDO $pdo): void { static $done = false; if ($done) { return; } $migrationPath = __DIR__ . '/../db/migrations/20260417_school_assessment_criteria.sql'; if (is_file($migrationPath)) { $sql = file_get_contents($migrationPath); if (is_string($sql) && trim($sql) !== '') { $pdo->exec($sql); } } $done = true; } function ensure_center_assessment_schema(PDO $pdo): void { static $done = false; if ($done) { return; } $migrationPath = __DIR__ . '/../db/migrations/20260417_center_assessment_system.sql'; if (is_file($migrationPath)) { $sql = file_get_contents($migrationPath); if (is_string($sql) && trim($sql) !== '') { $pdo->exec($sql); } } $done = true; } function ensure_global_center_assessment_schema(PDO $pdo): void { static $done = false; if ($done) { return; } $migrationPath = __DIR__ . '/../db/migrations/20260417_global_center_assessments.sql'; if (is_file($migrationPath)) { $sql = file_get_contents($migrationPath); if (is_string($sql) && trim($sql) !== '') { $pdo->exec($sql); } } $done = true; } function ensure_school_cycle_backfill(PDO $pdo): void { $applicationRows = $pdo->query( "SELECT * FROM center_applications WHERE status = 'approved' OR id IN ( SELECT center_application_id FROM school_students UNION SELECT center_application_id FROM school_teachers UNION SELECT center_application_id FROM school_assessment_types UNION SELECT center_application_id FROM school_attendance_records ) ORDER BY id ASC" )->fetchAll(); foreach ($applicationRows as $application) { $applicationId = (int) ($application['id'] ?? 0); if ($applicationId <= 0) { continue; } $cycleId = ensure_default_school_cycle_record($pdo, $application); foreach (['school_students', 'school_teachers', 'school_assessment_types', 'school_attendance_records'] as $table) { if (!schema_table_has_column($pdo, $table, 'cycle_id')) { continue; } $stmt = $pdo->prepare('UPDATE ' . $table . ' SET cycle_id = :cycle_id WHERE center_application_id = :center_application_id AND cycle_id IS NULL'); $stmt->execute([ ':cycle_id' => $cycleId, ':center_application_id' => $applicationId, ]); } } } function school_cycle_season_map(): array { return [ 'summer' => ['label' => 'Summer', 'label_ar' => 'صيف'], 'winter' => ['label' => 'Winter', 'label_ar' => 'شتاء'], ]; } function school_cycle_status_map(): array { return [ 'active' => ['label' => 'نشطة', 'class' => 'status-approved'], 'upcoming' => ['label' => 'قادمة', 'class' => 'status-review'], 'archived' => ['label' => 'مؤرشفة', 'class' => 'status-muted'], ]; } function school_cycle_status_badge(string $status): string { $map = school_cycle_status_map(); $meta = $map[$status] ?? ['label' => 'غير محدد', 'class' => 'status-muted']; return '' . e($meta['label']) . ''; } function school_cycle_season_options(): array { return school_cycle_season_map(); } function detect_school_cycle_season(?string $date = null): string { $ts = $date ? strtotime($date) : time(); if ($ts === false) { $ts = time(); } $month = (int) date('n', $ts); return $month >= 5 && $month <= 9 ? 'summer' : 'winter'; } function format_school_cycle_name(string $season, int $year): string { $map = school_cycle_season_map(); $label = $map[$season]['label'] ?? 'Cycle'; return $label . ' ' . $year; } function school_cycle_defaults(?array $application = null): array { $season = detect_school_cycle_season((string) ($application['start_date'] ?? '')); $year = (int) date('Y', strtotime((string) ($application['start_date'] ?? 'now')) ?: time()); $startDate = clean_text((string) ($application['start_date'] ?? date('Y-m-d')), 20); $endDate = clean_text((string) ($application['end_date'] ?? date('Y-m-d', strtotime('+90 days'))), 20); return [ 'season' => $season, 'year' => (string) $year, 'start_date' => $startDate, 'end_date' => $endDate, 'status' => 'active', ]; } function validate_school_cycle_input(array $input, ?array $application = null): array { $defaults = school_cycle_defaults($application); $data = $defaults; $data['status'] = clean_text((string) ($input['status'] ?? 'active'), 20); $data['global_cycle_id'] = filter_var($input['global_cycle_id'] ?? null, FILTER_VALIDATE_INT) ?: null; $errors = []; if (empty($data['global_cycle_id'])) { $errors['global_cycle_id'] = 'يرجى اختيار الدورة.'; } else { try { $stmt = db_connection()->prepare('SELECT * FROM global_cycles WHERE id = ?'); $stmt->execute([$data['global_cycle_id']]); if ($cycle = $stmt->fetch(PDO::FETCH_ASSOC)) { $data['cycle_name'] = $cycle['cycle_name']; $data['start_date'] = $cycle['start_date']; $data['end_date'] = $cycle['end_date']; $data['season'] = null; // No longer used $data['year'] = null; // No longer used } else { $errors['global_cycle_id'] = 'الدورة غير صالحة'; } } catch (Throwable $e) {} } if (!in_array($data['status'], ['active', 'upcoming'], true)) { $errors['status'] = 'يرجى اختيار حالة تشغيل صحيحة للدورة.'; } return [$data, $errors]; } function normalize_school_cycle_row(array $row): array { $row['id'] = (int) ($row['id'] ?? 0); $row['center_application_id'] = (int) ($row['center_application_id'] ?? 0); $row['year'] = (int) ($row['year'] ?? 0); $row['cycle_name'] = (string) ($row['cycle_name'] ?? format_school_cycle_name((string) ($row['season'] ?? ''), (int) ($row['year'] ?? 0))); $row['status'] = (string) ($row['status'] ?? 'upcoming'); return $row; } function ensure_default_school_cycle_record(PDO $pdo, array $application): int { $applicationId = (int) ($application['id'] ?? 0); if ($applicationId <= 0) { throw new InvalidArgumentException('Missing application id for school cycle.'); } $stmt = $pdo->prepare('SELECT * FROM school_cycles WHERE center_application_id = :center_application_id ORDER BY FIELD(status, "active", "upcoming", "archived"), start_date DESC, id DESC LIMIT 1'); $stmt->execute([':center_application_id' => $applicationId]); $existing = $stmt->fetch(); if ($existing) { return (int) $existing['id']; } $season = null; $year = null; $startDate = (string) ($application['start_date'] ?? date('Y-m-d')); $endDate = (string) ($application['end_date'] ?? $startDate); $cycleName = 'الدورة الأساسية'; $globalCycleId = $application['global_cycle_id'] ?? null; if ($globalCycleId) { $gcStmt = $pdo->prepare('SELECT * FROM global_cycles WHERE id = ?'); $gcStmt->execute([$globalCycleId]); if ($gc = $gcStmt->fetch()) { $cycleName = $gc['cycle_name']; $startDate = $gc['start_date']; $endDate = $gc['end_date']; } } $status = ((string) ($application['status'] ?? '') === 'approved') ? 'active' : 'upcoming'; $insert = $pdo->prepare( 'INSERT INTO school_cycles ( center_application_id, season, year, cycle_name, start_date, end_date, status, archived_at, created_at, updated_at, global_cycle_id ) VALUES ( :center_application_id, :season, :year, :cycle_name, :start_date, :end_date, :status, NULL, NOW(), NOW(), :global_cycle_id )' ); $insert->execute([ ':center_application_id' => $applicationId, ':season' => $season, ':year' => $year, ':cycle_name' => $cycleName, ':start_date' => $startDate, ':end_date' => $endDate, ':status' => $status, ':global_cycle_id' => $globalCycleId ]); return (int) $pdo->lastInsertId(); } function update_teacher_in_cycle(int $centerApplicationId, int $cycleId, int $id, array $data): void { $pdo = db_connection(); $stmt = $pdo->prepare( 'UPDATE school_teachers SET full_name = :full_name, role_title = :role_title, specialization = :specialization, subject_ids = :subject_ids, phone = :phone, email = :email, employment_status = :employment_status, notes = :notes, updated_at = NOW() WHERE id = :id AND center_application_id = :center_application_id AND cycle_id = :cycle_id' ); $stmt->execute([ ':id' => $id, ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ':full_name' => $data['full_name'], ':role_title' => $data['role_title'], ':specialization' => $data['specialization'] !== '' ? $data['specialization'] : null, ':subject_ids' => !empty($data['subject_ids']) ? json_encode(array_values($data['subject_ids'])) : null, ':phone' => $data['phone'] !== '' ? $data['phone'] : null, ':email' => $data['email'] !== '' ? $data['email'] : null, ':employment_status' => $data['employment_status'], ':notes' => $data['notes'] !== '' ? $data['notes'] : null, ]); } function list_school_cycles(int $centerApplicationId): array { $pdo = db_connection(); $stmt = $pdo->prepare('SELECT * FROM school_cycles WHERE center_application_id = :center_application_id ORDER BY start_date DESC, id DESC'); $stmt->execute([':center_application_id' => $centerApplicationId]); $rows = $stmt->fetchAll(); return array_map('normalize_school_cycle_row', $rows); } function get_school_cycle(int $centerApplicationId, int $cycleId): ?array { $pdo = db_connection(); $stmt = $pdo->prepare('SELECT * FROM school_cycles WHERE center_application_id = :center_application_id AND id = :id LIMIT 1'); $stmt->execute([ ':center_application_id' => $centerApplicationId, ':id' => $cycleId, ]); $row = $stmt->fetch(); return $row ? normalize_school_cycle_row($row) : null; } function school_cycle_rollover_defaults(?array $selectedCycle = null): array { return [ 'source_cycle_id' => $selectedCycle ? (int) ($selectedCycle['id'] ?? 0) : 0, 'copy_teachers' => true, 'copy_assessments' => true, 'copy_students' => false, ]; } function school_cycle_rollover_input(int $centerApplicationId, array $input, ?array $selectedCycle = null): array { $defaults = school_cycle_rollover_defaults($selectedCycle); $sourceCycleId = isset($input['source_cycle_id']) ? (int) $input['source_cycle_id'] : (int) $defaults['source_cycle_id']; $sourceCycle = $sourceCycleId > 0 ? get_school_cycle($centerApplicationId, $sourceCycleId) : null; return [ 'source_cycle_id' => $sourceCycle ? (int) ($sourceCycle['id'] ?? 0) : 0, 'source_cycle' => $sourceCycle, 'copy_teachers' => isset($input['copy_teachers']) ? (bool) $input['copy_teachers'] : false, 'copy_assessments' => isset($input['copy_assessments']) ? (bool) $input['copy_assessments'] : false, 'copy_students' => isset($input['copy_students']) ? (bool) $input['copy_students'] : false, ]; } function copy_school_cycle_rollover(PDO $pdo, int $centerApplicationId, int $sourceCycleId, int $targetCycleId, array $rollover): array { $summary = [ 'teachers' => 0, 'assessments' => 0, 'students' => 0, ]; if (!empty($rollover['copy_teachers'])) { $stmt = $pdo->prepare( 'INSERT INTO school_teachers ( center_application_id, cycle_id, full_name, role_title, specialization, subject_ids, phone, email, employment_status, notes, created_at, updated_at ) SELECT center_application_id, :target_cycle_id, full_name, role_title, specialization, subject_ids, phone, email, employment_status, notes, NOW(), NOW() FROM school_teachers WHERE center_application_id = :center_application_id AND cycle_id = :source_cycle_id' ); $stmt->execute([ ':target_cycle_id' => $targetCycleId, ':center_application_id' => $centerApplicationId, ':source_cycle_id' => $sourceCycleId, ]); $summary['teachers'] = $stmt->rowCount(); } if (!empty($rollover['copy_assessments'])) { $sourceStmt = $pdo->prepare( 'SELECT * FROM school_assessment_types WHERE center_application_id = :center_application_id AND cycle_id = :source_cycle_id ORDER BY id ASC' ); $sourceStmt->execute([ ':center_application_id' => $centerApplicationId, ':source_cycle_id' => $sourceCycleId, ]); $sourceAssessments = $sourceStmt->fetchAll(); $insertAssessmentStmt = $pdo->prepare( 'INSERT INTO school_assessment_types ( center_application_id, cycle_id, subject_id, title, category, scale_type, max_score, weight_percentage, is_active, notes, created_at, updated_at ) VALUES ( :center_application_id, :cycle_id, :subject_id, :title, :category, :scale_type, :max_score, :weight_percentage, :is_active, :notes, NOW(), NOW() )' ); $copyCriteriaStmt = $pdo->prepare( 'INSERT INTO school_assessment_criteria ( center_application_id, cycle_id, assessment_type_id, title, max_score, sort_order, is_active, notes, created_at, updated_at ) SELECT center_application_id, :target_cycle_id, :target_assessment_id, title, max_score, sort_order, is_active, notes, NOW(), NOW() FROM school_assessment_criteria WHERE center_application_id = :center_application_id AND cycle_id = :source_cycle_id AND assessment_type_id = :source_assessment_id' ); foreach ($sourceAssessments as $sourceAssessment) { $insertAssessmentStmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $targetCycleId, ':subject_id' => !empty($sourceAssessment['subject_id']) ? (int) $sourceAssessment['subject_id'] : null, ':title' => (string) ($sourceAssessment['title'] ?? ''), ':category' => (string) ($sourceAssessment['category'] ?? ''), ':scale_type' => (string) ($sourceAssessment['scale_type'] ?? 'percentage'), ':max_score' => (float) ($sourceAssessment['max_score'] ?? 0), ':weight_percentage' => (float) ($sourceAssessment['weight_percentage'] ?? 0), ':is_active' => (int) ($sourceAssessment['is_active'] ?? 1), ':notes' => !empty($sourceAssessment['notes']) ? (string) $sourceAssessment['notes'] : null, ]); $newAssessmentId = (int) $pdo->lastInsertId(); if ($newAssessmentId > 0) { $copyCriteriaStmt->execute([ ':target_cycle_id' => $targetCycleId, ':target_assessment_id' => $newAssessmentId, ':center_application_id' => $centerApplicationId, ':source_cycle_id' => $sourceCycleId, ':source_assessment_id' => (int) ($sourceAssessment['id'] ?? 0), ]); sync_assessment_total_score_from_criteria($centerApplicationId, $targetCycleId, $newAssessmentId); } } $summary['assessments'] = count($sourceAssessments); } if (!empty($rollover['copy_students'])) { $stmt = $pdo->prepare( "INSERT INTO school_students ( center_application_id, cycle_id, student_code, full_name, gender, grade_level, guardian_name, guardian_phone, birth_date, enrollment_status, notes, created_at, updated_at ) SELECT center_application_id, :target_cycle_id, student_code, full_name, gender, grade_level, guardian_name, guardian_phone, birth_date, enrollment_status, notes, NOW(), NOW() FROM school_students WHERE center_application_id = :center_application_id AND cycle_id = :source_cycle_id AND enrollment_status IN ('active', 'waiting')" ); $stmt->execute([ ':target_cycle_id' => $targetCycleId, ':center_application_id' => $centerApplicationId, ':source_cycle_id' => $sourceCycleId, ]); $summary['students'] = $stmt->rowCount(); } return $summary; } function create_school_cycle(int $centerApplicationId, array $data, array $rollover = []): array { $pdo = db_connection(); $season = $data['season'] ?? null; $year = isset($data['year']) ? (int) $data['year'] : null; $cycleName = $data['cycle_name'] ?? format_school_cycle_name($season ?? 'summer', $year ?? (int)date('Y')); $globalCycleId = $data['global_cycle_id'] ?? null; $rollover = array_merge(school_cycle_rollover_defaults(), $rollover); $sourceCycleId = (int) ($rollover['source_cycle_id'] ?? 0); $pdo->beginTransaction(); try { $sourceCycle = null; if ($sourceCycleId > 0) { $sourceStmt = $pdo->prepare('SELECT * FROM school_cycles WHERE center_application_id = :center_application_id AND id = :id LIMIT 1'); $sourceStmt->execute([ ':center_application_id' => $centerApplicationId, ':id' => $sourceCycleId, ]); $sourceCycleRow = $sourceStmt->fetch(); if ($sourceCycleRow) { $sourceCycle = normalize_school_cycle_row($sourceCycleRow); } elseif (!empty($rollover['copy_teachers']) || !empty($rollover['copy_assessments']) || !empty($rollover['copy_students'])) { throw new InvalidArgumentException('Source cycle not found for rollover.'); } } if ($data['status'] === 'active') { $archiveStmt = $pdo->prepare( "UPDATE school_cycles SET status = 'archived', archived_at = COALESCE(archived_at, NOW()), updated_at = NOW() WHERE center_application_id = :center_application_id AND status = 'active'" ); $archiveStmt->execute([':center_application_id' => $centerApplicationId]); } $insert = $pdo->prepare( 'INSERT INTO school_cycles ( center_application_id, season, year, cycle_name, start_date, end_date, status, archived_at, created_at, updated_at, global_cycle_id ) VALUES ( :center_application_id, :season, :year, :cycle_name, :start_date, :end_date, :status, :archived_at, NOW(), NOW(), :global_cycle_id )' ); $insert->execute([ ':center_application_id' => $centerApplicationId, ':season' => $season, ':year' => $year, ':cycle_name' => $cycleName, ':start_date' => $data['start_date'], ':end_date' => $data['end_date'], ':status' => $data['status'], ':archived_at' => $data['status'] === 'archived' ? date('Y-m-d H:i:s') : null, ':global_cycle_id' => $globalCycleId, ]); $cycleId = (int) $pdo->lastInsertId(); $rolloverSummary = ['teachers' => 0, 'assessments' => 0, 'students' => 0]; if ($sourceCycle && ($rollover['copy_teachers'] || $rollover['copy_assessments'] || $rollover['copy_students'])) { $rolloverSummary = copy_school_cycle_rollover($pdo, $centerApplicationId, (int) $sourceCycle['id'], $cycleId, $rollover); } $pdo->commit(); return [ 'cycle_id' => $cycleId, 'cycle_name' => $cycleName, 'source_cycle_name' => $sourceCycle ? (string) ($sourceCycle['cycle_name'] ?? '') : '', 'rollover' => $rolloverSummary, ]; } catch (Throwable $exception) { if ($pdo->inTransaction()) { $pdo->rollBack(); } throw $exception; } } function archive_school_cycle(int $centerApplicationId, int $cycleId): void { $pdo = db_connection(); $stmt = $pdo->prepare( "UPDATE school_cycles SET status = 'archived', archived_at = COALESCE(archived_at, NOW()), updated_at = NOW() WHERE center_application_id = :center_application_id AND id = :id" ); $stmt->execute([ ':center_application_id' => $centerApplicationId, ':id' => $cycleId, ]); } function resolve_school_cycle_context(int $centerApplicationId, ?array $application, int $requestedCycleId = 0): array { if ($application && (string) ($application['status'] ?? '') === 'approved') { $pdo = db_connection(); ensure_default_school_cycle_record($pdo, $application); } $cycles = list_school_cycles($centerApplicationId); $selected = null; $active = null; foreach ($cycles as $cycle) { if ($cycle['status'] === 'active' && $active === null) { $active = $cycle; } if ($requestedCycleId > 0 && (int) $cycle['id'] === $requestedCycleId) { $selected = $cycle; } } if ($selected === null) { $selected = $active ?? ($cycles[0] ?? null); } return [ 'cycles' => $cycles, 'selected' => $selected, 'active' => $active, 'read_only' => $selected ? ((string) ($selected['status'] ?? '') === 'archived') : false, ]; } function school_page_url(string $page, int $applicationId, ?int $cycleId = null): string { $url = $page . '?id=' . urlencode((string) $applicationId); if ($cycleId !== null && $cycleId > 0) { $url .= '&cycle=' . urlencode((string) $cycleId); } return $url; } function next_student_code_for_cycle(int $centerApplicationId, int $cycleId): string { $pdo = db_connection(); $stmt = $pdo->prepare( 'SELECT student_code FROM school_students WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id ORDER BY id DESC' ); $stmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ]); $codes = $stmt->fetchAll(PDO::FETCH_COLUMN) ?: []; $prefix = 'ST-'; $padding = 3; $maxNumber = 0; foreach ($codes as $codeValue) { $code = trim((string) $codeValue); if ($code === '') { continue; } if (preg_match('/^(.*?)(\d+)$/', $code, $matches)) { if ($prefix === 'ST-' && trim((string) $matches[1]) !== '') { $prefix = (string) $matches[1]; } $padding = max($padding, strlen((string) $matches[2])); $maxNumber = max($maxNumber, (int) $matches[2]); } } return $prefix . str_pad((string) ($maxNumber + 1), $padding, '0', STR_PAD_LEFT); } function create_student_in_cycle(int $centerApplicationId, int $cycleId, array $data): int { $pdo = db_connection(); $stmt = $pdo->prepare( 'INSERT INTO school_students ( center_application_id, cycle_id, student_code, full_name, gender, grade_level, guardian_name, guardian_phone, birth_date, enrollment_status, notes, created_at, updated_at ) VALUES ( :center_application_id, :cycle_id, :student_code, :full_name, :gender, :grade_level, :guardian_name, :guardian_phone, :birth_date, :enrollment_status, :notes, NOW(), NOW() )' ); $stmt->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT); $stmt->bindValue(':cycle_id', $cycleId, PDO::PARAM_INT); $stmt->bindValue(':student_code', $data['student_code'], PDO::PARAM_STR); $stmt->bindValue(':full_name', $data['full_name'], PDO::PARAM_STR); $stmt->bindValue(':gender', $data['gender'], PDO::PARAM_STR); $stmt->bindValue(':grade_level', $data['grade_level'], PDO::PARAM_STR); $stmt->bindValue(':guardian_name', $data['guardian_name'], PDO::PARAM_STR); $stmt->bindValue(':guardian_phone', $data['guardian_phone'], PDO::PARAM_STR); $stmt->bindValue(':birth_date', $data['birth_date'] !== '' ? $data['birth_date'] : null, $data['birth_date'] !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL); $stmt->bindValue(':enrollment_status', $data['enrollment_status'], PDO::PARAM_STR); $stmt->bindValue(':notes', $data['notes'] !== '' ? $data['notes'] : null, $data['notes'] !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL); $stmt->execute(); return (int) $pdo->lastInsertId(); } function list_school_students_by_cycle(int $centerApplicationId, int $cycleId, string $search = '', int $limit = 0, int $offset = 0, array $filters = []): array { $pdo = db_connection(); $query = 'SELECT * FROM school_students WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id'; $params = [ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ]; if ($search !== '') { $query .= ' AND (student_code LIKE :search1 OR full_name LIKE :search2 OR guardian_phone LIKE :search3)'; $params[':search1'] = "%$search%"; $params[':search2'] = "%$search%"; $params[':search3'] = "%$search%"; } if (!empty($filters["gender"])) { $query .= " AND gender = :gender"; $params[":gender"] = $filters["gender"]; } if (!empty($filters["grade_level"])) { $query .= " AND grade_level = :grade_level"; $params[":grade_level"] = $filters["grade_level"]; } if (!empty($filters["enrollment_status"])) { $query .= " AND enrollment_status = :enrollment_status"; $params[":enrollment_status"] = $filters["enrollment_status"]; } $query .= ' ORDER BY created_at DESC, id DESC'; if ($limit > 0) { $query .= ' LIMIT ' . (int)$limit . ' OFFSET ' . (int)$offset; } $stmt = $pdo->prepare($query); $stmt->execute($params); return $stmt->fetchAll(); } function count_school_students_by_cycle(int $centerApplicationId, int $cycleId, string $search = '', array $filters = []): int { $pdo = db_connection(); $query = 'SELECT COUNT(*) FROM school_students WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id'; $params = [ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ]; if ($search !== '') { $query .= ' AND (student_code LIKE :search1 OR full_name LIKE :search2 OR guardian_phone LIKE :search3)'; $params[':search1'] = "%$search%"; $params[':search2'] = "%$search%"; $params[':search3'] = "%$search%"; } if (!empty($filters["gender"])) { $query .= " AND gender = :gender"; $params[":gender"] = $filters["gender"]; } if (!empty($filters["grade_level"])) { $query .= " AND grade_level = :grade_level"; $params[":grade_level"] = $filters["grade_level"]; } if (!empty($filters["enrollment_status"])) { $query .= " AND enrollment_status = :enrollment_status"; $params[":enrollment_status"] = $filters["enrollment_status"]; } $stmt = $pdo->prepare($query); $stmt->execute($params); return (int)$stmt->fetchColumn(); } function school_student_metrics_by_cycle(int $centerApplicationId, int $cycleId): array { $pdo = db_connection(); $stmt = $pdo->prepare( "SELECT COUNT(*) AS total, COALESCE(SUM(gender = 'طالب'), 0) AS boys_count, COALESCE(SUM(gender = 'طالبة'), 0) AS girls_count, COALESCE(SUM(enrollment_status = 'active'), 0) AS active_count, COALESCE(SUM(enrollment_status = 'waiting'), 0) AS waiting_count, COALESCE(SUM(enrollment_status = 'withdrawn'), 0) AS withdrawn_count FROM school_students WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id" ); $stmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ]); $row = $stmt->fetch() ?: []; return [ 'total' => (int) ($row['total'] ?? 0), 'boys' => (int) ($row['boys_count'] ?? 0), 'girls' => (int) ($row['girls_count'] ?? 0), 'active' => (int) ($row['active_count'] ?? 0), 'waiting' => (int) ($row['waiting_count'] ?? 0), 'withdrawn' => (int) ($row['withdrawn_count'] ?? 0), ]; } function create_teacher_in_cycle(int $centerApplicationId, int $cycleId, array $data): int { $pdo = db_connection(); $stmt = $pdo->prepare( 'INSERT INTO school_teachers ( center_application_id, cycle_id, full_name, role_title, specialization, subject_ids, phone, email, employment_status, notes, created_at, updated_at ) VALUES ( :center_application_id, :cycle_id, :full_name, :role_title, :specialization, :subject_ids, :phone, :email, :employment_status, :notes, NOW(), NOW() )' ); $stmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ':full_name' => $data['full_name'], ':role_title' => $data['role_title'], ':specialization' => $data['specialization'] !== '' ? $data['specialization'] : null, ':subject_ids' => !empty($data['subject_ids']) ? json_encode(array_values($data['subject_ids'])) : null, ':phone' => $data['phone'] !== '' ? $data['phone'] : null, ':email' => $data['email'] !== '' ? $data['email'] : null, ':employment_status' => $data['employment_status'], ':notes' => $data['notes'] !== '' ? $data['notes'] : null, ]); return (int) $pdo->lastInsertId(); } function list_school_teachers_by_cycle(int $centerApplicationId, int $cycleId, array $filters = [], int $limit = 0, int $offset = 0): array { $pdo = db_connection(); $query = 'SELECT * FROM school_teachers WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id'; $params = [ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ]; $search = $filters['search'] ?? ''; if ($search !== '') { $query .= ' AND (full_name LIKE :search1 OR email LIKE :search2 OR phone LIKE :search3)'; $params[':search1'] = "%$search%"; $params[':search2'] = "%$search%"; $params[':search3'] = "%$search%"; } if (!empty($filters['role_title'])) { $query .= ' AND role_title = :role'; $params[':role'] = $filters['role_title']; } if (!empty($filters['employment_status'])) { $query .= ' AND employment_status = :status'; $params[':status'] = $filters['employment_status']; } $query .= ' ORDER BY created_at DESC, id DESC'; if ($limit > 0) { $query .= ' LIMIT ' . (int)$limit . ' OFFSET ' . (int)$offset; } $stmt = $pdo->prepare($query); $stmt->execute($params); return $stmt->fetchAll(); } function count_school_teachers_by_cycle(int $centerApplicationId, int $cycleId, array $filters = []): int { $pdo = db_connection(); $query = 'SELECT COUNT(*) FROM school_teachers WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id'; $params = [ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ]; $search = $filters['search'] ?? ''; if ($search !== '') { $query .= ' AND (full_name LIKE :search1 OR email LIKE :search2 OR phone LIKE :search3)'; $params[':search1'] = "%$search%"; $params[':search2'] = "%$search%"; $params[':search3'] = "%$search%"; } if (!empty($filters['role_title'])) { $query .= ' AND role_title = :role'; $params[':role'] = $filters['role_title']; } if (!empty($filters['employment_status'])) { $query .= ' AND employment_status = :status'; $params[':status'] = $filters['employment_status']; } $stmt = $pdo->prepare($query); $stmt->execute($params); return (int)$stmt->fetchColumn(); } function school_teacher_metrics_by_cycle(int $centerApplicationId, int $cycleId): array { $pdo = db_connection(); $stmt = $pdo->prepare( "SELECT COUNT(*) AS total, COALESCE(SUM(employment_status = 'active'), 0) AS active_count, COALESCE(SUM(employment_status = 'pending'), 0) AS pending_count, COALESCE(SUM(employment_status = 'inactive'), 0) AS inactive_count, COALESCE(SUM(role_title = 'معلم' OR role_title = 'معلمة'), 0) AS teachers_count, COALESCE(SUM(role_title LIKE '%مشرف%' OR role_title LIKE '%منسق%'), 0) AS supervisors_count, COALESCE(SUM(email IS NOT NULL AND email <> ''), 0) AS email_ready_count FROM school_teachers WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id" ); $stmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ]); $row = $stmt->fetch() ?: []; return [ 'total' => (int) ($row['total'] ?? 0), 'active' => (int) ($row['active_count'] ?? 0), 'pending' => (int) ($row['pending_count'] ?? 0), 'inactive' => (int) ($row['inactive_count'] ?? 0), 'teachers' => (int) ($row['teachers_count'] ?? 0), 'supervisors' => (int) ($row['supervisors_count'] ?? 0), 'email_ready' => (int) ($row['email_ready_count'] ?? 0), ]; } function create_assessment_type_in_cycle(int $centerApplicationId, int $cycleId, array $data): int { $pdo = db_connection(); $stmt = $pdo->prepare( 'INSERT INTO school_assessment_types ( center_application_id, cycle_id, subject_id, title, category, scale_type, max_score, weight_percentage, is_active, notes, created_at, updated_at ) VALUES ( :center_application_id, :cycle_id, :subject_id, :title, :category, :scale_type, :max_score, :weight_percentage, :is_active, :notes, NOW(), NOW() )' ); $stmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ':subject_id' => !empty($data['subject_id']) ? (int) $data['subject_id'] : null, ':title' => $data['title'], ':category' => $data['category'], ':scale_type' => $data['scale_type'], ':max_score' => (float) $data['max_score'], ':weight_percentage' => (float) $data['weight_percentage'], ':is_active' => (int) $data['is_active'], ':notes' => $data['notes'] !== '' ? $data['notes'] : null, ]); return (int) $pdo->lastInsertId(); } function list_school_assessments_by_cycle(int $centerApplicationId, int $cycleId, array $filters = [], int $limit = 0, int $offset = 0): array { $pdo = db_connection(); $query = 'SELECT sat.*, ( SELECT COUNT(*) FROM school_assessment_criteria criteria WHERE criteria.assessment_type_id = sat.id AND criteria.center_application_id = sat.center_application_id AND criteria.cycle_id = sat.cycle_id AND criteria.is_active = 1 ) AS criteria_count, ( SELECT COALESCE(SUM(criteria.max_score), 0) FROM school_assessment_criteria criteria WHERE criteria.assessment_type_id = sat.id AND criteria.center_application_id = sat.center_application_id AND criteria.cycle_id = sat.cycle_id AND criteria.is_active = 1 ) AS criteria_total_max_score FROM school_assessment_types sat WHERE sat.center_application_id = :center_application_id AND sat.cycle_id = :cycle_id'; $params = [ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ]; $search = $filters['search'] ?? ''; if ($search !== '') { $query .= ' AND (sat.title LIKE :search1 OR sat.category LIKE :search2)'; $params[':search1'] = "%$search%"; $params[':search2'] = "%$search%"; } $subject_id = $filters['subject_id'] ?? ''; if ($subject_id !== '') { $query .= ' AND sat.subject_id = :subject_id'; $params[':subject_id'] = (int) $subject_id; } $category = $filters['category'] ?? ''; if ($category !== '') { $query .= ' AND sat.category = :category'; $params[':category'] = $category; } $query .= ' ORDER BY sat.is_active DESC, sat.created_at DESC, sat.id DESC'; if ($limit > 0) { $query .= ' LIMIT ' . (int)$limit . ' OFFSET ' . (int)$offset; } $stmt = $pdo->prepare($query); $stmt->execute($params); return $stmt->fetchAll(); } function count_school_assessments_by_cycle(int $centerApplicationId, int $cycleId, array $filters = []): int { $pdo = db_connection(); $query = 'SELECT COUNT(*) FROM school_assessment_types WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id'; $params = [ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ]; $search = $filters['search'] ?? ''; if ($search !== '') { $query .= ' AND (title LIKE :search1 OR category LIKE :search2)'; $params[':search1'] = "%$search%"; $params[':search2'] = "%$search%"; } $subject_id = $filters['subject_id'] ?? ''; if ($subject_id !== '') { $query .= ' AND subject_id = :subject_id'; $params[':subject_id'] = (int) $subject_id; } $category = $filters['category'] ?? ''; if ($category !== '') { $query .= ' AND category = :category'; $params[':category'] = $category; } $stmt = $pdo->prepare($query); $stmt->execute($params); return (int)$stmt->fetchColumn(); } function school_assessment_metrics_by_cycle(int $centerApplicationId, int $cycleId): array { $pdo = db_connection(); $stmt = $pdo->prepare( "SELECT COUNT(*) AS total, COALESCE(SUM(is_active = 1), 0) AS active_count, COALESCE(SUM(is_active = 0), 0) AS inactive_count, COALESCE(SUM(weight_percentage), 0) AS total_weight, COALESCE(SUM(CASE WHEN is_active = 1 THEN weight_percentage ELSE 0 END), 0) AS active_weight, COALESCE(AVG(max_score), 0) AS average_max_score, COALESCE(SUM(scale_type = 'percentage'), 0) AS percentage_count, COALESCE(SUM(scale_type = 'points'), 0) AS points_count, COALESCE(SUM(scale_type = 'rubric'), 0) AS rubric_count FROM school_assessment_types WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id" ); $stmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ]); $row = $stmt->fetch() ?: []; return [ 'total' => (int) ($row['total'] ?? 0), 'active' => (int) ($row['active_count'] ?? 0), 'inactive' => (int) ($row['inactive_count'] ?? 0), 'total_weight' => (float) ($row['total_weight'] ?? 0), 'active_weight' => (float) ($row['active_weight'] ?? 0), 'average_max_score' => (float) ($row['average_max_score'] ?? 0), 'percentage' => (int) ($row['percentage_count'] ?? 0), 'points' => (int) ($row['points_count'] ?? 0), 'rubric' => (int) ($row['rubric_count'] ?? 0), ]; } function assessment_score_status_map(): array { return [ 'present' => ['label' => 'حاضر', 'class' => 'status-approved'], 'absent' => ['label' => 'غائب', 'class' => 'status-review'], 'excused' => ['label' => 'بعذر', 'class' => 'status-muted'], ]; } function assessment_score_status_badge(string $status): string { $map = assessment_score_status_map(); $meta = $map[$status] ?? ['label' => 'غير محدد', 'class' => 'status-muted']; return '' . e($meta['label']) . ''; } function school_teacher_options_by_cycle(int $centerApplicationId, int $cycleId, bool $onlyActive = false): array { $filters = []; if ($onlyActive) { $filters['employment_status'] = 'active'; } $teachers = list_school_teachers_by_cycle($centerApplicationId, $cycleId, $filters); $options = []; foreach ($teachers as $teacher) { $teacherId = (int) ($teacher['id'] ?? 0); if ($teacherId <= 0) { continue; } $roleTitle = trim((string) ($teacher['role_title'] ?? '')); $specialization = trim((string) ($teacher['specialization'] ?? '')); $label = trim((string) ($teacher['full_name'] ?? '')); if ($roleTitle !== '') { $label .= ' — ' . $roleTitle; } if ($specialization !== '') { $label .= ' (' . $specialization . ')'; } $options[$teacherId] = [ 'label' => $label, 'full_name' => trim((string) ($teacher['full_name'] ?? '')), 'role_title' => $roleTitle, 'specialization' => $specialization, 'employment_status' => (string) ($teacher['employment_status'] ?? ''), ]; } return $options; } function create_center_assessment_type_in_cycle(int $centerApplicationId, int $cycleId, array $data): int { $pdo = db_connection(); $stmt = $pdo->prepare( 'INSERT INTO center_assessment_types ( center_application_id, cycle_id, title, category, scale_type, max_score, weight_percentage, is_active, notes, created_at, updated_at ) VALUES ( :center_application_id, :cycle_id, :title, :category, :scale_type, :max_score, :weight_percentage, :is_active, :notes, NOW(), NOW() )' ); $stmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ':title' => $data['title'], ':category' => $data['category'], ':scale_type' => $data['scale_type'], ':max_score' => (float) $data['max_score'], ':weight_percentage' => (float) $data['weight_percentage'], ':is_active' => (int) $data['is_active'], ':notes' => $data['notes'] !== '' ? $data['notes'] : null, ]); return (int) $pdo->lastInsertId(); } function list_center_assessments_by_cycle(int $centerApplicationId, int $cycleId, array $filters = [], int $limit = 0, int $offset = 0): array { $pdo = db_connection(); $query = 'SELECT cat.*, ( SELECT COUNT(*) FROM center_assessment_criteria criteria WHERE criteria.assessment_type_id = cat.id AND criteria.center_application_id = cat.center_application_id AND criteria.cycle_id = cat.cycle_id AND criteria.is_active = 1 ) AS criteria_count, ( SELECT COALESCE(SUM(criteria.max_score), 0) FROM center_assessment_criteria criteria WHERE criteria.assessment_type_id = cat.id AND criteria.center_application_id = cat.center_application_id AND criteria.cycle_id = cat.cycle_id AND criteria.is_active = 1 ) AS criteria_total_max_score FROM center_assessment_types cat WHERE cat.center_application_id = :center_application_id AND cat.cycle_id = :cycle_id'; $params = [ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ]; $search = trim((string) ($filters['search'] ?? '')); if ($search !== '') { $query .= ' AND (cat.title LIKE :search1 OR cat.category LIKE :search2)'; $params[':search1'] = "%{$search}%"; $params[':search2'] = "%{$search}%"; } $category = trim((string) ($filters['category'] ?? '')); if ($category !== '') { $query .= ' AND cat.category = :category'; $params[':category'] = $category; } $query .= ' ORDER BY cat.is_active DESC, cat.updated_at DESC, cat.id DESC'; if ($limit > 0) { $query .= ' LIMIT ' . (int) $limit . ' OFFSET ' . (int) $offset; } $stmt = $pdo->prepare($query); $stmt->execute($params); return $stmt->fetchAll(); } function count_center_assessments_by_cycle(int $centerApplicationId, int $cycleId, array $filters = []): int { $pdo = db_connection(); $query = 'SELECT COUNT(*) FROM center_assessment_types WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id'; $params = [ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ]; $search = trim((string) ($filters['search'] ?? '')); if ($search !== '') { $query .= ' AND (title LIKE :search1 OR category LIKE :search2)'; $params[':search1'] = "%{$search}%"; $params[':search2'] = "%{$search}%"; } $category = trim((string) ($filters['category'] ?? '')); if ($category !== '') { $query .= ' AND category = :category'; $params[':category'] = $category; } $stmt = $pdo->prepare($query); $stmt->execute($params); return (int) $stmt->fetchColumn(); } function center_assessment_metrics_by_cycle(int $centerApplicationId, int $cycleId): array { $pdo = db_connection(); $stmt = $pdo->prepare( "SELECT COUNT(*) AS total, COALESCE(SUM(is_active = 1), 0) AS active_count, COALESCE(SUM(is_active = 0), 0) AS inactive_count, COALESCE(SUM(CASE WHEN is_active = 1 THEN weight_percentage ELSE 0 END), 0) AS active_weight, COALESCE(AVG(max_score), 0) AS average_max_score, COALESCE(SUM(scale_type = 'percentage'), 0) AS percentage_count, COALESCE(SUM(scale_type = 'points'), 0) AS points_count, COALESCE(SUM(scale_type LIKE 'rubric_%'), 0) AS rubric_count FROM center_assessment_types WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id" ); $stmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ]); $row = $stmt->fetch() ?: []; return [ 'total' => (int) ($row['total'] ?? 0), 'active' => (int) ($row['active_count'] ?? 0), 'inactive' => (int) ($row['inactive_count'] ?? 0), 'active_weight' => (float) ($row['active_weight'] ?? 0), 'average_max_score' => (float) ($row['average_max_score'] ?? 0), 'percentage' => (int) ($row['percentage_count'] ?? 0), 'points' => (int) ($row['points_count'] ?? 0), 'rubric' => (int) ($row['rubric_count'] ?? 0), ]; } function update_center_assessment_type_in_cycle(int $centerApplicationId, int $cycleId, int $assessmentId, array $data): bool { $pdo = db_connection(); $stmt = $pdo->prepare( 'UPDATE center_assessment_types SET title = :title, category = :category, scale_type = :scale_type, max_score = :max_score, weight_percentage = :weight_percentage, is_active = :is_active, notes = :notes, updated_at = NOW() WHERE id = :id AND center_application_id = :center_application_id AND cycle_id = :cycle_id' ); return $stmt->execute([ ':id' => $assessmentId, ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ':title' => $data['title'], ':category' => $data['category'], ':scale_type' => $data['scale_type'], ':max_score' => (float) $data['max_score'], ':weight_percentage' => (float) $data['weight_percentage'], ':is_active' => (int) $data['is_active'], ':notes' => $data['notes'] !== '' ? $data['notes'] : null, ]); } function center_assessment_type_options_by_cycle(int $centerApplicationId, int $cycleId, bool $onlyActive = false): array { $rows = list_center_assessments_by_cycle($centerApplicationId, $cycleId); $options = []; foreach ($rows as $assessment) { $assessmentId = (int) ($assessment['id'] ?? 0); if ($assessmentId <= 0) { continue; } $isActive = (int) ($assessment['is_active'] ?? 0) === 1; if ($onlyActive && !$isActive) { continue; } $title = trim((string) ($assessment['title'] ?? '')); $label = $title !== '' ? $title : 'تقييم غير مسمى'; $category = trim((string) ($assessment['category'] ?? '')); if ($category !== '') { $label .= ' — ' . $category; } $criteriaCount = (int) ($assessment['criteria_count'] ?? 0); $criteriaTotal = (float) ($assessment['criteria_total_max_score'] ?? 0); $options[$assessmentId] = [ 'id' => $assessmentId, 'label' => $label, 'title' => $title, 'category' => $category, 'max_score' => (float) ($assessment['max_score'] ?? 0), 'weight_percentage' => (float) ($assessment['weight_percentage'] ?? 0), 'criteria_count' => $criteriaCount, 'criteria_total_max_score' => $criteriaTotal, 'has_criteria' => $criteriaCount > 0, 'is_active' => $isActive, ]; } return $options; } function list_center_assessment_criteria_by_assessment(int $centerApplicationId, int $cycleId, int $assessmentTypeId, bool $onlyActive = false): array { $pdo = db_connection(); $query = 'SELECT * FROM center_assessment_criteria WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id AND assessment_type_id = :assessment_type_id'; $params = [ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ':assessment_type_id' => $assessmentTypeId, ]; if ($onlyActive) { $query .= ' AND is_active = 1'; } $query .= ' ORDER BY sort_order ASC, id ASC'; $stmt = $pdo->prepare($query); $stmt->execute($params); return $stmt->fetchAll(); } function center_assessment_criteria_metrics(int $centerApplicationId, int $cycleId, int $assessmentTypeId): array { $pdo = db_connection(); $stmt = $pdo->prepare( "SELECT COUNT(*) AS total_count, COALESCE(SUM(is_active = 1), 0) AS active_count, COALESCE(SUM(CASE WHEN is_active = 1 THEN max_score ELSE 0 END), 0) AS active_max_score FROM center_assessment_criteria WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id AND assessment_type_id = :assessment_type_id" ); $stmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ':assessment_type_id' => $assessmentTypeId, ]); $row = $stmt->fetch() ?: []; return [ 'total' => (int) ($row['total_count'] ?? 0), 'active' => (int) ($row['active_count'] ?? 0), 'active_max_score' => (float) ($row['active_max_score'] ?? 0), ]; } function validate_center_assessment_criteria_input(int $centerApplicationId, int $cycleId, int $assessmentTypeId, array $input): array { $data = ['criteria' => []]; $errors = []; $assessmentOptions = center_assessment_type_options_by_cycle($centerApplicationId, $cycleId, false); if (!array_key_exists($assessmentTypeId, $assessmentOptions)) { return [$data, ['form' => 'يرجى اختيار تقييم مركز صحيح من نفس الدورة.']]; } $postedRows = $input['criteria'] ?? []; if (!is_array($postedRows)) { $postedRows = []; } $position = 1; $activeCount = 0; foreach ($postedRows as $rowKey => $row) { if (!is_array($row)) { continue; } $criterionId = (int) ($row['id'] ?? 0); $title = clean_text((string) ($row['title'] ?? ''), 150); $maxScoreRaw = str_replace(',', '.', clean_text((string) ($row['max_score'] ?? ''), 30)); $notes = clean_text((string) ($row['notes'] ?? ''), 500); $isActive = ((string) ($row['is_active'] ?? '1')) === '1' ? 1 : 0; if ($criterionId <= 0 && $title === '' && $maxScoreRaw === '' && $notes === '') { continue; } $rowErrors = []; if ($title === '') { $rowErrors[] = 'اسم البند مطلوب.'; } $maxScore = null; if ($maxScoreRaw === '' || !is_numeric($maxScoreRaw)) { $rowErrors[] = 'أدخل درجة رقمية للبند.'; } else { $maxScore = round((float) $maxScoreRaw, 2); if ($maxScore <= 0 || $maxScore > 1000) { $rowErrors[] = 'درجة البند يجب أن تكون بين 0.01 و1000.'; } } if ($rowErrors !== []) { $errors['criteria_' . $rowKey] = implode(' ', $rowErrors); } $data['criteria'][] = [ 'id' => $criterionId, 'title' => $title, 'max_score' => $maxScore !== null ? number_format($maxScore, 2, '.', '') : '', 'notes' => $notes, 'is_active' => (string) $isActive, 'sort_order' => $position, ]; if ($isActive === 1) { $activeCount++; } $position++; } if ($data['criteria'] === []) { $errors['form'] = 'أضف بند تقييم واحداً على الأقل قبل الحفظ.'; } elseif ($activeCount === 0) { $errors['form'] = 'فعّل بنداً واحداً على الأقل ليظهر في رصد تقييم المركز.'; } return [$data, $errors]; } function sync_center_assessment_total_score_from_criteria(int $centerApplicationId, int $cycleId, int $assessmentTypeId): void { $pdo = db_connection(); $criteria = list_center_assessment_criteria_by_assessment($centerApplicationId, $cycleId, $assessmentTypeId, true); if ($criteria === []) { return; } $totalMaxScore = 0.0; foreach ($criteria as $criterion) { $totalMaxScore += (float) ($criterion['max_score'] ?? 0); } $totalMaxScore = round($totalMaxScore, 2); $assessmentStmt = $pdo->prepare( 'UPDATE center_assessment_types SET max_score = :max_score, updated_at = NOW() WHERE id = :id AND center_application_id = :center_application_id AND cycle_id = :cycle_id' ); $assessmentStmt->execute([ ':max_score' => $totalMaxScore, ':id' => $assessmentTypeId, ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ]); $itemStmt = $pdo->prepare( 'UPDATE center_assessment_score_items items INNER JOIN center_assessment_criteria criteria ON criteria.id = items.criterion_id SET items.max_score = criteria.max_score, items.updated_at = NOW() WHERE items.center_application_id = :center_application_id AND items.cycle_id = :cycle_id AND items.assessment_type_id = :assessment_type_id' ); $itemStmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ':assessment_type_id' => $assessmentTypeId, ]); $scoreStmt = $pdo->prepare( 'UPDATE center_assessment_scores SET max_score = :max_score, updated_at = NOW() WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id AND assessment_type_id = :assessment_type_id' ); $scoreStmt->execute([ ':max_score' => $totalMaxScore, ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ':assessment_type_id' => $assessmentTypeId, ]); } function save_center_assessment_criteria_in_cycle(int $centerApplicationId, int $cycleId, int $assessmentTypeId, array $data): int { $pdo = db_connection(); $existingStmt = $pdo->prepare( 'SELECT id FROM center_assessment_criteria WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id AND assessment_type_id = :assessment_type_id' ); $existingStmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ':assessment_type_id' => $assessmentTypeId, ]); $existingIds = array_map('intval', $existingStmt->fetchAll(PDO::FETCH_COLUMN)); $existingMap = array_fill_keys($existingIds, true); $insertStmt = $pdo->prepare( 'INSERT INTO center_assessment_criteria ( center_application_id, cycle_id, assessment_type_id, title, max_score, sort_order, is_active, notes, created_at, updated_at ) VALUES ( :center_application_id, :cycle_id, :assessment_type_id, :title, :max_score, :sort_order, :is_active, :notes, NOW(), NOW() )' ); $updateStmt = $pdo->prepare( 'UPDATE center_assessment_criteria SET title = :title, max_score = :max_score, sort_order = :sort_order, is_active = :is_active, notes = :notes, updated_at = NOW() WHERE id = :id AND center_application_id = :center_application_id AND cycle_id = :cycle_id AND assessment_type_id = :assessment_type_id' ); $saved = 0; foreach ($data['criteria'] as $criterion) { $params = [ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ':assessment_type_id' => $assessmentTypeId, ':title' => (string) ($criterion['title'] ?? ''), ':max_score' => (float) ($criterion['max_score'] ?? 0), ':sort_order' => (int) ($criterion['sort_order'] ?? 0), ':is_active' => ((string) ($criterion['is_active'] ?? '1')) === '1' ? 1 : 0, ':notes' => !empty($criterion['notes']) ? (string) $criterion['notes'] : null, ]; $criterionId = (int) ($criterion['id'] ?? 0); if ($criterionId > 0 && isset($existingMap[$criterionId])) { $updateStmt->execute($params + [':id' => $criterionId]); } else { $insertStmt->execute($params); } $saved++; } sync_center_assessment_total_score_from_criteria($centerApplicationId, $cycleId, $assessmentTypeId); return $saved; } function school_assessment_type_options_by_cycle(int $centerApplicationId, int $cycleId, bool $onlyActive = false): array { $rows = list_school_assessments_by_cycle($centerApplicationId, $cycleId); $subjects = []; foreach (get_enabled_subjects() as $subject) { $subjects[(int) ($subject['id'] ?? 0)] = (string) ($subject['name'] ?? ''); } $options = []; foreach ($rows as $assessment) { $assessmentId = (int) ($assessment['id'] ?? 0); if ($assessmentId <= 0) { continue; } $isActive = (int) ($assessment['is_active'] ?? 0) === 1; if ($onlyActive && !$isActive) { continue; } $subjectId = (int) ($assessment['subject_id'] ?? 0); $subjectLabel = $subjectId > 0 ? ($subjects[$subjectId] ?? '') : ''; $title = trim((string) ($assessment['title'] ?? '')); $label = $title !== '' ? $title : 'تقييم غير مسمى'; if ($subjectLabel !== '') { $label .= ' — ' . $subjectLabel; } $criteriaCount = (int) ($assessment['criteria_count'] ?? 0); $criteriaTotal = (float) ($assessment['criteria_total_max_score'] ?? 0); $options[$assessmentId] = [ 'id' => $assessmentId, 'label' => $label, 'title' => $title, 'subject_label' => $subjectLabel, 'category' => (string) ($assessment['category'] ?? ''), 'max_score' => (float) ($assessment['max_score'] ?? 0), 'weight_percentage' => (float) ($assessment['weight_percentage'] ?? 0), 'criteria_count' => $criteriaCount, 'criteria_total_max_score' => $criteriaTotal, 'has_criteria' => $criteriaCount > 0, 'is_active' => $isActive, ]; } return $options; } function list_assessment_criteria_by_assessment(int $centerApplicationId, int $cycleId, int $assessmentTypeId, bool $onlyActive = false): array { $pdo = db_connection(); $query = 'SELECT * FROM school_assessment_criteria WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id AND assessment_type_id = :assessment_type_id'; $params = [ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ':assessment_type_id' => $assessmentTypeId, ]; if ($onlyActive) { $query .= ' AND is_active = 1'; } $query .= ' ORDER BY sort_order ASC, id ASC'; $stmt = $pdo->prepare($query); $stmt->execute($params); return $stmt->fetchAll(); } function school_assessment_criteria_metrics(int $centerApplicationId, int $cycleId, int $assessmentTypeId): array { $pdo = db_connection(); $stmt = $pdo->prepare( "SELECT COUNT(*) AS total_count, COALESCE(SUM(is_active = 1), 0) AS active_count, COALESCE(SUM(CASE WHEN is_active = 1 THEN max_score ELSE 0 END), 0) AS active_max_score FROM school_assessment_criteria WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id AND assessment_type_id = :assessment_type_id" ); $stmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ':assessment_type_id' => $assessmentTypeId, ]); $row = $stmt->fetch() ?: []; return [ 'total' => (int) ($row['total_count'] ?? 0), 'active' => (int) ($row['active_count'] ?? 0), 'active_max_score' => (float) ($row['active_max_score'] ?? 0), ]; } function validate_assessment_criteria_input(int $centerApplicationId, int $cycleId, int $assessmentTypeId, array $input): array { $data = ['criteria' => []]; $errors = []; $assessmentOptions = school_assessment_type_options_by_cycle($centerApplicationId, $cycleId, false); if (!array_key_exists($assessmentTypeId, $assessmentOptions)) { return [$data, ['form' => 'يرجى اختيار تقييم صحيح من نفس الدورة.']]; } $postedRows = $input['criteria'] ?? []; if (!is_array($postedRows)) { $postedRows = []; } $position = 1; $activeCount = 0; foreach ($postedRows as $rowKey => $row) { if (!is_array($row)) { continue; } $criterionId = (int) ($row['id'] ?? 0); $title = clean_text((string) ($row['title'] ?? ''), 150); $maxScoreRaw = str_replace(',', '.', clean_text((string) ($row['max_score'] ?? ''), 30)); $notes = clean_text((string) ($row['notes'] ?? ''), 500); $isActive = ((string) ($row['is_active'] ?? '1')) === '1' ? 1 : 0; if ($criterionId <= 0 && $title === '' && $maxScoreRaw === '' && $notes === '') { continue; } $rowErrors = []; if ($title === '') { $rowErrors[] = 'اسم البند مطلوب.'; } $maxScore = null; if ($maxScoreRaw === '' || !is_numeric($maxScoreRaw)) { $rowErrors[] = 'أدخل درجة رقمية للبند.'; } else { $maxScore = round((float) $maxScoreRaw, 2); if ($maxScore <= 0 || $maxScore > 1000) { $rowErrors[] = 'درجة البند يجب أن تكون بين 0.01 و1000.'; } } if ($rowErrors !== []) { $errors['criteria_' . $rowKey] = implode(' ', $rowErrors); } $data['criteria'][] = [ 'id' => $criterionId, 'title' => $title, 'max_score' => $maxScore !== null ? number_format($maxScore, 2, '.', '') : '', 'notes' => $notes, 'is_active' => (string) $isActive, 'sort_order' => $position, ]; if ($isActive === 1) { $activeCount++; } $position++; } if ($data['criteria'] === []) { $errors['form'] = 'أضف بند تقييم واحد على الأقل قبل الحفظ.'; } elseif ($activeCount === 0) { $errors['form'] = 'فعّل بنداً واحداً على الأقل ليظهر في ورقة الرصد.'; } return [$data, $errors]; } function sync_assessment_total_score_from_criteria(int $centerApplicationId, int $cycleId, int $assessmentTypeId): void { $pdo = db_connection(); $criteria = list_assessment_criteria_by_assessment($centerApplicationId, $cycleId, $assessmentTypeId, true); if ($criteria === []) { return; } $totalMaxScore = 0.0; foreach ($criteria as $criterion) { $totalMaxScore += (float) ($criterion['max_score'] ?? 0); } $totalMaxScore = round($totalMaxScore, 2); $assessmentStmt = $pdo->prepare( 'UPDATE school_assessment_types SET max_score = :max_score, updated_at = NOW() WHERE id = :id AND center_application_id = :center_application_id AND cycle_id = :cycle_id' ); $assessmentStmt->execute([ ':max_score' => $totalMaxScore, ':id' => $assessmentTypeId, ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ]); $itemStmt = $pdo->prepare( 'UPDATE school_assessment_score_items items INNER JOIN school_assessment_criteria criteria ON criteria.id = items.criterion_id SET items.max_score = criteria.max_score, items.updated_at = NOW() WHERE items.center_application_id = :center_application_id AND items.cycle_id = :cycle_id AND items.assessment_type_id = :assessment_type_id' ); $itemStmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ':assessment_type_id' => $assessmentTypeId, ]); $scoreStmt = $pdo->prepare( 'UPDATE school_assessment_scores SET max_score = :max_score, updated_at = NOW() WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id AND assessment_type_id = :assessment_type_id' ); $scoreStmt->execute([ ':max_score' => $totalMaxScore, ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ':assessment_type_id' => $assessmentTypeId, ]); } function save_assessment_criteria_in_cycle(int $centerApplicationId, int $cycleId, int $assessmentTypeId, array $data): int { $pdo = db_connection(); $existingStmt = $pdo->prepare( 'SELECT id FROM school_assessment_criteria WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id AND assessment_type_id = :assessment_type_id' ); $existingStmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ':assessment_type_id' => $assessmentTypeId, ]); $existingIds = array_map('intval', $existingStmt->fetchAll(PDO::FETCH_COLUMN)); $existingMap = array_fill_keys($existingIds, true); $insertStmt = $pdo->prepare( 'INSERT INTO school_assessment_criteria ( center_application_id, cycle_id, assessment_type_id, title, max_score, sort_order, is_active, notes, created_at, updated_at ) VALUES ( :center_application_id, :cycle_id, :assessment_type_id, :title, :max_score, :sort_order, :is_active, :notes, NOW(), NOW() )' ); $updateStmt = $pdo->prepare( 'UPDATE school_assessment_criteria SET title = :title, max_score = :max_score, sort_order = :sort_order, is_active = :is_active, notes = :notes, updated_at = NOW() WHERE id = :id AND center_application_id = :center_application_id AND cycle_id = :cycle_id AND assessment_type_id = :assessment_type_id' ); $saved = 0; foreach ($data['criteria'] as $criterion) { $params = [ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ':assessment_type_id' => $assessmentTypeId, ':title' => (string) ($criterion['title'] ?? ''), ':max_score' => (float) ($criterion['max_score'] ?? 0), ':sort_order' => (int) ($criterion['sort_order'] ?? 0), ':is_active' => ((string) ($criterion['is_active'] ?? '1')) === '1' ? 1 : 0, ':notes' => !empty($criterion['notes']) ? (string) $criterion['notes'] : null, ]; $criterionId = (int) ($criterion['id'] ?? 0); if ($criterionId > 0 && isset($existingMap[$criterionId])) { $updateStmt->execute($params + [':id' => $criterionId]); } else { $insertStmt->execute($params); } $saved++; } sync_assessment_total_score_from_criteria($centerApplicationId, $cycleId, $assessmentTypeId); return $saved; } function validate_assessment_scores_batch_input(int $centerApplicationId, int $cycleId, array $input): array { $data = [ 'assessment_type_id' => (string) ((int) ($input['assessment_type_id'] ?? 0)), 'teacher_id' => (string) ((int) ($input['teacher_id'] ?? 0)), 'assessed_on' => clean_text((string) ($input['assessed_on'] ?? date('Y-m-d')), 20), 'assessment_max_score' => 0.0, 'has_criteria' => false, 'criteria' => [], 'entries' => [], ]; $errors = []; $assessmentOptions = school_assessment_type_options_by_cycle($centerApplicationId, $cycleId, false); $teacherOptions = school_teacher_options_by_cycle($centerApplicationId, $cycleId, false); $studentOptions = school_student_options_by_cycle($centerApplicationId, $cycleId); $statusMap = assessment_score_status_map(); $assessmentId = (int) $data['assessment_type_id']; $selectedAssessment = $assessmentOptions[$assessmentId] ?? null; if ($selectedAssessment === null) { $errors['assessment_type_id'] = 'يرجى اختيار تقييم صحيح من نفس الدورة.'; } $criteriaRows = $assessmentId > 0 ? list_assessment_criteria_by_assessment($centerApplicationId, $cycleId, $assessmentId, true) : []; $criteriaMap = []; foreach ($criteriaRows as $criterion) { $criteriaId = (int) ($criterion['id'] ?? 0); if ($criteriaId <= 0) { continue; } $criteriaMap[$criteriaId] = $criterion; $data['assessment_max_score'] += (float) ($criterion['max_score'] ?? 0); } $data['assessment_max_score'] = round($data['assessment_max_score'], 2); $data['has_criteria'] = $criteriaMap !== []; $data['criteria'] = $criteriaRows; if (!$data['has_criteria'] && $selectedAssessment !== null) { $data['assessment_max_score'] = (float) ($selectedAssessment['max_score'] ?? 0); } $teacherId = (int) $data['teacher_id']; if ($teacherId > 0 && !array_key_exists($teacherId, $teacherOptions)) { $errors['teacher_id'] = 'يرجى اختيار معلم صحيح من نفس الدورة.'; } if ($data['assessed_on'] === '' || strtotime($data['assessed_on']) === false) { $errors['assessed_on'] = 'يرجى إدخال تاريخ تقييم صحيح.'; } $postedEntries = $input['entries'] ?? []; if (!is_array($postedEntries)) { $postedEntries = []; } $hasSaveableRow = false; foreach ($postedEntries as $studentKey => $row) { if (!is_array($row)) { continue; } $studentId = (int) $studentKey; if ($studentId <= 0 || !array_key_exists($studentId, $studentOptions)) { continue; } $status = clean_text((string) ($row['status'] ?? 'present'), 20); if (!array_key_exists($status, $statusMap)) { $status = 'present'; } $notes = clean_text((string) ($row['notes'] ?? ''), 1000); $criteriaScores = []; $score = null; $scoreRaw = ''; $hasCriteriaInput = false; if ($data['has_criteria']) { $postedCriterionScores = $row['criteria'] ?? []; if (!is_array($postedCriterionScores)) { $postedCriterionScores = []; } $missingCriteria = []; $totalScore = 0.0; foreach ($criteriaMap as $criterionId => $criterion) { $rawValue = str_replace(',', '.', clean_text((string) ($postedCriterionScores[$criterionId] ?? ''), 30)); $criteriaScores[$criterionId] = [ 'criterion_id' => $criterionId, 'score' => null, 'score_raw' => $rawValue, 'max_score' => (float) ($criterion['max_score'] ?? 0), ]; if ($rawValue === '') { $missingCriteria[] = (string) ($criterion['title'] ?? ''); continue; } $hasCriteriaInput = true; if (!is_numeric($rawValue)) { $errors['entries_' . $studentId] = 'كل بند يجب أن يحتوي على درجة رقمية صحيحة.'; continue; } $criterionScore = round((float) $rawValue, 2); $criterionMax = (float) ($criterion['max_score'] ?? 0); if ($criterionScore < 0 || $criterionScore > $criterionMax) { $errors['entries_' . $studentId] = 'درجة البند يجب أن تكون بين 0 و ' . rtrim(rtrim(number_format($criterionMax, 2, '.', ''), '0'), '.') . '.'; continue; } $criteriaScores[$criterionId]['score'] = $criterionScore; $totalScore += $criterionScore; } $shouldSave = $status !== 'present' || $hasCriteriaInput || $notes !== ''; if ($status === 'present' && $shouldSave && $missingCriteria !== []) { $errors['entries_' . $studentId] = 'للطالب الحاضر يجب تعبئة جميع البنود النشطة قبل الحفظ.'; } if ($status === 'present' && $missingCriteria === [] && !isset($errors['entries_' . $studentId])) { $score = round($totalScore, 2); $scoreRaw = number_format($score, 2, '.', ''); } if ($status !== 'present') { foreach ($criteriaScores as $criterionId => $criterionScoreData) { $criteriaScores[$criterionId]['score'] = null; $criteriaScores[$criterionId]['score_raw'] = ''; } $score = null; $scoreRaw = ''; } } else { $scoreRaw = clean_text((string) ($row['score'] ?? ''), 30); $shouldSave = $status !== 'present' || $scoreRaw !== '' || $notes !== ''; if ($scoreRaw !== '') { if (!is_numeric($scoreRaw)) { $errors['entries_' . $studentId] = 'الدرجة يجب أن تكون رقماً صحيحاً أو عشرياً.'; } else { $score = round((float) $scoreRaw, 2); if ($selectedAssessment !== null && ($score < 0 || $score > (float) $data['assessment_max_score'])) { $errors['entries_' . $studentId] = 'الدرجة يجب أن تكون بين 0 و ' . rtrim(rtrim(number_format((float) $data['assessment_max_score'], 2, '.', ''), '0'), '.'); } } } if ($status === 'present' && $shouldSave && $scoreRaw === '') { $errors['entries_' . $studentId] = 'أدخل الدرجة أو غيّر الحالة إلى غائب أو بعذر.'; } if ($status !== 'present') { $score = null; $scoreRaw = ''; } } if ($shouldSave) { $hasSaveableRow = true; } $data['entries'][$studentId] = [ 'student_id' => $studentId, 'status' => $status, 'score' => $score, 'score_raw' => $scoreRaw, 'total_score' => $score, 'notes' => $notes, 'criteria_scores' => $criteriaScores, 'should_save' => $shouldSave, ]; } if (!$hasSaveableRow && $errors === []) { $errors['form'] = $data['has_criteria'] ? 'أدخل درجات البنود أو حدّد حالة طالب واحد على الأقل قبل الحفظ.' : 'أدخل درجات أو حدّد حالة طالب واحد على الأقل قبل الحفظ.'; } return [$data, $errors, $selectedAssessment]; } function save_assessment_scores_in_cycle(int $centerApplicationId, int $cycleId, array $data): int { $pdo = db_connection(); $criteriaRows = !empty($data['has_criteria']) ? list_assessment_criteria_by_assessment($centerApplicationId, $cycleId, (int) $data['assessment_type_id'], true) : []; $criteriaMap = []; foreach ($criteriaRows as $criterion) { $criteriaId = (int) ($criterion['id'] ?? 0); if ($criteriaId > 0) { $criteriaMap[$criteriaId] = $criterion; } } $stmt = $pdo->prepare( 'INSERT INTO school_assessment_scores ( center_application_id, cycle_id, assessment_type_id, student_id, teacher_id, score, max_score, status, notes, assessed_on, created_at, updated_at ) VALUES ( :center_application_id, :cycle_id, :assessment_type_id, :student_id, :teacher_id, :score, :max_score, :status, :notes, :assessed_on, NOW(), NOW() ) ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id), teacher_id = VALUES(teacher_id), score = VALUES(score), max_score = VALUES(max_score), status = VALUES(status), notes = VALUES(notes), assessed_on = VALUES(assessed_on), updated_at = NOW()' ); $deleteItemsStmt = $pdo->prepare('DELETE FROM school_assessment_score_items WHERE assessment_score_id = :assessment_score_id'); $itemStmt = $pdo->prepare( 'INSERT INTO school_assessment_score_items ( center_application_id, cycle_id, assessment_score_id, assessment_type_id, criterion_id, student_id, score, max_score, created_at, updated_at ) VALUES ( :center_application_id, :cycle_id, :assessment_score_id, :assessment_type_id, :criterion_id, :student_id, :score, :max_score, NOW(), NOW() )' ); $saved = 0; foreach ($data['entries'] as $entry) { if (empty($entry['should_save'])) { continue; } $stmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ':assessment_type_id' => (int) $data['assessment_type_id'], ':student_id' => (int) $entry['student_id'], ':teacher_id' => !empty($data['teacher_id']) ? (int) $data['teacher_id'] : null, ':score' => $entry['score'], ':max_score' => (float) ($data['assessment_max_score'] ?? 0), ':status' => $entry['status'], ':notes' => $entry['notes'] !== '' ? $entry['notes'] : null, ':assessed_on' => $data['assessed_on'], ]); $assessmentScoreId = (int) $pdo->lastInsertId(); if ($assessmentScoreId > 0) { $deleteItemsStmt->execute([':assessment_score_id' => $assessmentScoreId]); if ($criteriaMap !== [] && (string) ($entry['status'] ?? 'present') === 'present') { foreach ($entry['criteria_scores'] as $criterionId => $criterionScoreData) { if (!array_key_exists((int) $criterionId, $criteriaMap) || ($criterionScoreData['score'] ?? null) === null) { continue; } $itemStmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ':assessment_score_id' => $assessmentScoreId, ':assessment_type_id' => (int) $data['assessment_type_id'], ':criterion_id' => (int) $criterionId, ':student_id' => (int) $entry['student_id'], ':score' => (float) $criterionScoreData['score'], ':max_score' => (float) ($criteriaMap[(int) $criterionId]['max_score'] ?? 0), ]); } } } $saved++; } return $saved; } function list_assessment_scores_by_assessment(int $centerApplicationId, int $cycleId, int $assessmentTypeId): array { $pdo = db_connection(); $stmt = $pdo->prepare( 'SELECT scores.*, teachers.full_name AS teacher_name FROM school_assessment_scores scores LEFT JOIN school_teachers teachers ON teachers.id = scores.teacher_id WHERE scores.center_application_id = :center_application_id AND scores.cycle_id = :cycle_id AND scores.assessment_type_id = :assessment_type_id ORDER BY scores.assessed_on DESC, scores.updated_at DESC, scores.id DESC' ); $stmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ':assessment_type_id' => $assessmentTypeId, ]); return $stmt->fetchAll(); } function list_assessment_score_items_by_assessment(int $centerApplicationId, int $cycleId, int $assessmentTypeId): array { $pdo = db_connection(); $stmt = $pdo->prepare( 'SELECT items.* FROM school_assessment_score_items items WHERE items.center_application_id = :center_application_id AND items.cycle_id = :cycle_id AND items.assessment_type_id = :assessment_type_id ORDER BY items.assessment_score_id ASC, items.id ASC' ); $stmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ':assessment_type_id' => $assessmentTypeId, ]); return $stmt->fetchAll(); } function school_assessment_score_map_by_assessment(int $centerApplicationId, int $cycleId, int $assessmentTypeId): array { $map = []; foreach (list_assessment_scores_by_assessment($centerApplicationId, $cycleId, $assessmentTypeId) as $row) { $studentId = (int) ($row['student_id'] ?? 0); if ($studentId <= 0 || array_key_exists($studentId, $map)) { continue; } $row['criteria_scores'] = []; $map[$studentId] = $row; } foreach (list_assessment_score_items_by_assessment($centerApplicationId, $cycleId, $assessmentTypeId) as $item) { $studentId = (int) ($item['student_id'] ?? 0); $criterionId = (int) ($item['criterion_id'] ?? 0); if ($studentId <= 0 || $criterionId <= 0 || !isset($map[$studentId])) { continue; } $map[$studentId]['criteria_scores'][$criterionId] = $item; } return $map; } function school_assessment_score_metrics_by_cycle(int $centerApplicationId, int $cycleId, int $assessmentTypeId = 0): array { $pdo = db_connection(); $query = "SELECT COUNT(*) AS total, COALESCE(SUM(status = 'present'), 0) AS present_count, COALESCE(SUM(status = 'absent'), 0) AS absent_count, COALESCE(SUM(status = 'excused'), 0) AS excused_count, COALESCE(AVG(CASE WHEN status = 'present' THEN score END), 0) AS average_score, MAX(assessed_on) AS latest_date FROM school_assessment_scores WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id"; $params = [ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ]; if ($assessmentTypeId > 0) { $query .= ' AND assessment_type_id = :assessment_type_id'; $params[':assessment_type_id'] = $assessmentTypeId; } $stmt = $pdo->prepare($query); $stmt->execute($params); $row = $stmt->fetch() ?: []; return [ 'total' => (int) ($row['total'] ?? 0), 'present' => (int) ($row['present_count'] ?? 0), 'absent' => (int) ($row['absent_count'] ?? 0), 'excused' => (int) ($row['excused_count'] ?? 0), 'average_score' => (float) ($row['average_score'] ?? 0), 'latest_date' => (string) ($row['latest_date'] ?? ''), ]; } function center_assessment_normalize_status(string $status): string { return match ($status) { 'present' => 'completed', 'absent', 'draft' => 'pending', 'excused' => 'waived', 'completed', 'pending', 'waived' => $status, default => 'pending', }; } function center_assessment_status_map(): array { return [ 'completed' => ['label' => 'مكتمل', 'class' => 'status-approved'], 'pending' => ['label' => 'مؤجل', 'class' => 'status-review'], 'waived' => ['label' => 'معفى', 'class' => 'status-muted'], ]; } function center_assessment_status_badge(string $status): string { $normalizedStatus = center_assessment_normalize_status($status); $map = center_assessment_status_map(); $meta = $map[$normalizedStatus] ?? ['label' => 'غير محدد', 'class' => 'status-muted']; return '' . e($meta['label']) . ''; } function validate_center_assessment_score_input(int $centerApplicationId, int $cycleId, array $input): array { $data = [ 'assessment_type_id' => (string) ((int) ($input['assessment_type_id'] ?? 0)), 'assessed_on' => clean_text((string) ($input['assessed_on'] ?? date('Y-m-d')), 20), 'status' => center_assessment_normalize_status(clean_text((string) ($input['status'] ?? 'completed'), 20)), 'assessment_max_score' => 0.0, 'has_criteria' => false, 'criteria' => [], 'criteria_scores' => [], 'score' => null, 'score_raw' => clean_text((string) ($input['score'] ?? ''), 30), 'notes' => clean_text((string) ($input['notes'] ?? ''), 1000), 'should_save' => false, ]; $errors = []; $assessmentOptions = center_assessment_type_options_by_cycle($centerApplicationId, $cycleId, false); $statusMap = center_assessment_status_map(); $assessmentId = (int) $data['assessment_type_id']; $selectedAssessment = $assessmentOptions[$assessmentId] ?? null; if ($selectedAssessment === null) { $errors['assessment_type_id'] = 'يرجى اختيار تقييم مركز صحيح من نفس الدورة.'; } if (!array_key_exists($data['status'], $statusMap)) { $data['status'] = 'completed'; } if ($data['assessed_on'] === '' || strtotime($data['assessed_on']) === false) { $errors['assessed_on'] = 'يرجى إدخال تاريخ تقييم صحيح.'; } $criteriaRows = $assessmentId > 0 ? list_center_assessment_criteria_by_assessment($centerApplicationId, $cycleId, $assessmentId, true) : []; $criteriaMap = []; foreach ($criteriaRows as $criterion) { $criterionId = (int) ($criterion['id'] ?? 0); if ($criterionId <= 0) { continue; } $criteriaMap[$criterionId] = $criterion; $data['assessment_max_score'] += (float) ($criterion['max_score'] ?? 0); } $data['assessment_max_score'] = round($data['assessment_max_score'], 2); $data['has_criteria'] = $criteriaMap !== []; $data['criteria'] = $criteriaRows; if (!$data['has_criteria'] && $selectedAssessment !== null) { $data['assessment_max_score'] = (float) ($selectedAssessment['max_score'] ?? 0); } if ($data['has_criteria']) { $postedCriterionScores = $input['criteria'] ?? []; if (!is_array($postedCriterionScores)) { $postedCriterionScores = []; } $missingCriteria = []; $totalScore = 0.0; $hasCriteriaInput = false; foreach ($criteriaMap as $criterionId => $criterion) { $rawValue = str_replace(',', '.', clean_text((string) ($postedCriterionScores[$criterionId] ?? ''), 30)); $data['criteria_scores'][$criterionId] = [ 'criterion_id' => $criterionId, 'score' => null, 'score_raw' => $rawValue, 'max_score' => (float) ($criterion['max_score'] ?? 0), ]; if ($rawValue == '') { $missingCriteria[] = (string) ($criterion['title'] ?? ''); continue; } $hasCriteriaInput = true; if (!is_numeric($rawValue)) { $errors['score'] = 'كل بند يجب أن يحتوي على درجة رقمية صحيحة.'; continue; } $criterionScore = round((float) $rawValue, 2); $criterionMax = (float) ($criterion['max_score'] ?? 0); if ($criterionScore < 0 || $criterionScore > $criterionMax) { $errors['score'] = 'درجة كل بند يجب أن تكون بين 0 و ' . rtrim(rtrim(number_format($criterionMax, 2, '.', ''), '0'), '.') . '.'; continue; } $data['criteria_scores'][$criterionId]['score'] = $criterionScore; $totalScore += $criterionScore; } $data['should_save'] = $data['status'] !== 'completed' || $hasCriteriaInput || $data['notes'] !== ''; if ($data['status'] === 'completed' && $data['should_save'] && $missingCriteria !== []) { $errors['score'] = 'عند اعتماد التقييم كمكتمل يجب تعبئة جميع البنود النشطة.'; } if ($data['status'] === 'completed' && $missingCriteria === [] && !isset($errors['score'])) { $data['score'] = round($totalScore, 2); $data['score_raw'] = number_format($data['score'], 2, '.', ''); } if ($data['status'] !== 'completed') { foreach ($data['criteria_scores'] as $criterionId => $criterionScoreData) { $data['criteria_scores'][$criterionId]['score'] = null; $data['criteria_scores'][$criterionId]['score_raw'] = ''; } $data['score'] = null; $data['score_raw'] = ''; } } else { $scoreRaw = str_replace(',', '.', clean_text((string) ($input['score'] ?? ''), 30)); $data['score_raw'] = $scoreRaw; $data['should_save'] = $data['status'] !== 'completed' || $scoreRaw !== '' || $data['notes'] !== ''; if ($scoreRaw !== '') { if (!is_numeric($scoreRaw)) { $errors['score'] = 'الدرجة يجب أن تكون رقماً صحيحاً أو عشرياً.'; } else { $data['score'] = round((float) $scoreRaw, 2); if ($selectedAssessment !== null && ($data['score'] < 0 || $data['score'] > (float) $data['assessment_max_score'])) { $errors['score'] = 'الدرجة يجب أن تكون بين 0 و ' . rtrim(rtrim(number_format((float) $data['assessment_max_score'], 2, '.', ''), '0'), '.'); } } } if ($data['status'] === 'completed' && $data['should_save'] && $scoreRaw === '') { $errors['score'] = 'أدخل الدرجة أو غيّر الحالة إلى مؤجل أو معفى.'; } if ($data['status'] !== 'completed') { $data['score'] = null; $data['score_raw'] = ''; } } if (!$data['should_save'] && $errors === []) { $errors['form'] = $data['has_criteria'] ? 'أدخل درجات البنود أو حدّد حالة التقييم قبل الحفظ.' : 'أدخل الدرجة أو حدّد حالة التقييم قبل الحفظ.'; } return [$data, $errors, $selectedAssessment]; } function save_center_assessment_score_in_cycle(int $centerApplicationId, int $cycleId, array $data): int { $pdo = db_connection(); $criteriaRows = !empty($data['has_criteria']) ? list_center_assessment_criteria_by_assessment($centerApplicationId, $cycleId, (int) $data['assessment_type_id'], true) : []; $criteriaMap = []; foreach ($criteriaRows as $criterion) { $criteriaId = (int) ($criterion['id'] ?? 0); if ($criteriaId > 0) { $criteriaMap[$criteriaId] = $criterion; } } $stmt = $pdo->prepare( 'INSERT INTO center_assessment_scores ( center_application_id, cycle_id, assessment_type_id, score, max_score, status, notes, assessed_on, created_at, updated_at ) VALUES ( :center_application_id, :cycle_id, :assessment_type_id, :score, :max_score, :status, :notes, :assessed_on, NOW(), NOW() ) ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id), score = VALUES(score), max_score = VALUES(max_score), status = VALUES(status), notes = VALUES(notes), assessed_on = VALUES(assessed_on), updated_at = NOW()' ); $deleteItemsStmt = $pdo->prepare('DELETE FROM center_assessment_score_items WHERE assessment_score_id = :assessment_score_id'); $itemStmt = $pdo->prepare( 'INSERT INTO center_assessment_score_items ( center_application_id, cycle_id, assessment_score_id, assessment_type_id, criterion_id, score, max_score, created_at, updated_at ) VALUES ( :center_application_id, :cycle_id, :assessment_score_id, :assessment_type_id, :criterion_id, :score, :max_score, NOW(), NOW() )' ); $stmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ':assessment_type_id' => (int) $data['assessment_type_id'], ':score' => $data['score'], ':max_score' => (float) ($data['assessment_max_score'] ?? 0), ':status' => center_assessment_normalize_status((string) ($data['status'] ?? 'completed')), ':notes' => $data['notes'] !== '' ? $data['notes'] : null, ':assessed_on' => $data['assessed_on'], ]); $assessmentScoreId = (int) $pdo->lastInsertId(); if ($assessmentScoreId > 0) { $deleteItemsStmt->execute([':assessment_score_id' => $assessmentScoreId]); if ($criteriaMap !== [] && center_assessment_normalize_status((string) ($data['status'] ?? 'completed')) === 'completed') { foreach ($data['criteria_scores'] as $criterionId => $criterionScoreData) { if (!array_key_exists((int) $criterionId, $criteriaMap) || ($criterionScoreData['score'] ?? null) === null) { continue; } $itemStmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ':assessment_score_id' => $assessmentScoreId, ':assessment_type_id' => (int) $data['assessment_type_id'], ':criterion_id' => (int) $criterionId, ':score' => (float) $criterionScoreData['score'], ':max_score' => (float) ($criteriaMap[(int) $criterionId]['max_score'] ?? 0), ]); } } } return 1; } function center_assessment_score_by_assessment(int $centerApplicationId, int $cycleId, int $assessmentTypeId): ?array { $pdo = db_connection(); $stmt = $pdo->prepare( 'SELECT scores.* FROM center_assessment_scores scores WHERE scores.center_application_id = :center_application_id AND scores.cycle_id = :cycle_id AND scores.assessment_type_id = :assessment_type_id LIMIT 1' ); $stmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ':assessment_type_id' => $assessmentTypeId, ]); $row = $stmt->fetch(); return $row ?: null; } function list_center_assessment_score_items_by_assessment(int $centerApplicationId, int $cycleId, int $assessmentTypeId): array { $pdo = db_connection(); $stmt = $pdo->prepare( 'SELECT items.*, criteria.title AS criterion_title FROM center_assessment_score_items items LEFT JOIN center_assessment_criteria criteria ON criteria.id = items.criterion_id WHERE items.center_application_id = :center_application_id AND items.cycle_id = :cycle_id AND items.assessment_type_id = :assessment_type_id ORDER BY items.id ASC' ); $stmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ':assessment_type_id' => $assessmentTypeId, ]); return $stmt->fetchAll(); } function center_assessment_score_bundle_by_assessment(int $centerApplicationId, int $cycleId, int $assessmentTypeId): array { $score = center_assessment_score_by_assessment($centerApplicationId, $cycleId, $assessmentTypeId); $criteriaScores = []; foreach (list_center_assessment_score_items_by_assessment($centerApplicationId, $cycleId, $assessmentTypeId) as $item) { $criterionId = (int) ($item['criterion_id'] ?? 0); if ($criterionId <= 0) { continue; } $criteriaScores[$criterionId] = $item; } return [ 'score' => $score, 'criteria_scores' => $criteriaScores, ]; } function center_assessment_score_metrics_by_cycle(int $centerApplicationId, int $cycleId, int $assessmentTypeId = 0): array { $pdo = db_connection(); $query = "SELECT COUNT(*) AS total, COALESCE(SUM(status IN ('completed', 'present')), 0) AS completed_count, COALESCE(SUM(status IN ('pending', 'absent', 'draft')), 0) AS pending_count, COALESCE(SUM(status IN ('waived', 'excused')), 0) AS waived_count, COALESCE(AVG(CASE WHEN status IN ('completed', 'present') THEN score END), 0) AS average_score, MAX(assessed_on) AS latest_date FROM center_assessment_scores WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id"; $params = [ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ]; if ($assessmentTypeId > 0) { $query .= ' AND assessment_type_id = :assessment_type_id'; $params[':assessment_type_id'] = $assessmentTypeId; } $stmt = $pdo->prepare($query); $stmt->execute($params); $row = $stmt->fetch() ?: []; return [ 'total' => (int) ($row['total'] ?? 0), 'completed' => (int) ($row['completed_count'] ?? 0), 'pending' => (int) ($row['pending_count'] ?? 0), 'waived' => (int) ($row['waived_count'] ?? 0), 'average_score' => (float) ($row['average_score'] ?? 0), 'latest_date' => (string) ($row['latest_date'] ?? ''), ]; } function center_assessment_summary_by_cycle(int $centerApplicationId, int $cycleId): array { $summary = [ 'assessments' => [], 'total_assessments' => 0, 'active_assessments' => 0, 'recorded_assessments' => 0, 'completed_assessments' => 0, 'pending_assessments' => 0, 'waived_assessments' => 0, 'missing_assessments' => 0, 'overall_percentage' => 0.0, 'score_total' => 0.0, 'max_score_total' => 0.0, 'latest_assessed_on' => '', 'performance' => student_certificate_performance_meta(0.0), ]; $pdo = db_connection(); $stmt = $pdo->prepare( 'SELECT assessments.*, scores.id AS score_id, scores.score, scores.max_score AS saved_max_score, scores.status AS score_status, scores.notes AS score_notes, scores.assessed_on, ( SELECT COUNT(*) FROM center_assessment_criteria criteria WHERE criteria.center_application_id = assessments.center_application_id AND criteria.cycle_id = assessments.cycle_id AND criteria.assessment_type_id = assessments.id AND criteria.is_active = 1 ) AS criteria_count FROM center_assessment_types assessments LEFT JOIN center_assessment_scores scores ON scores.center_application_id = assessments.center_application_id AND scores.cycle_id = assessments.cycle_id AND scores.assessment_type_id = assessments.id WHERE assessments.center_application_id = :center_application_id AND assessments.cycle_id = :cycle_id ORDER BY assessments.is_active DESC, assessments.updated_at DESC, assessments.id DESC' ); $stmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ]); $rows = $stmt->fetchAll(); foreach ($rows as $row) { $summary['total_assessments']++; $isActive = (int) ($row['is_active'] ?? 0) === 1; if ($isActive) { $summary['active_assessments']++; } $hasSavedScore = !empty($row['score_id']); $status = $hasSavedScore ? center_assessment_normalize_status((string) ($row['score_status'] ?? 'pending')) : 'missing'; $score = isset($row['score']) ? (float) $row['score'] : null; $maxScore = $hasSavedScore && (float) ($row['saved_max_score'] ?? 0) > 0 ? (float) ($row['saved_max_score'] ?? 0) : (float) ($row['max_score'] ?? 0); $percentage = ($status === 'completed' && $score !== null && $maxScore > 0) ? round(($score / $maxScore) * 100, 2) : null; if ($hasSavedScore) { $summary['recorded_assessments']++; if ($status === 'completed' && $score !== null && $maxScore > 0) { $summary['completed_assessments']++; $summary['score_total'] += $score; $summary['max_score_total'] += $maxScore; } elseif ($status === 'pending') { $summary['pending_assessments']++; } elseif ($status === 'waived') { $summary['waived_assessments']++; } $assessedOn = (string) ($row['assessed_on'] ?? ''); if ($assessedOn !== '' && ($summary['latest_assessed_on'] === '' || strtotime($assessedOn) > strtotime($summary['latest_assessed_on']))) { $summary['latest_assessed_on'] = $assessedOn; } } elseif ($isActive) { $summary['missing_assessments']++; } $summary['assessments'][] = [ 'id' => (int) ($row['id'] ?? 0), 'title' => (string) ($row['title'] ?? ''), 'category' => (string) ($row['category'] ?? ''), 'scale_type' => (string) ($row['scale_type'] ?? ''), 'weight_percentage' => (float) ($row['weight_percentage'] ?? 0), 'max_score' => (float) ($row['max_score'] ?? 0), 'score' => $score, 'saved_max_score' => $maxScore, 'status' => $status, 'status_label' => $status === 'missing' ? 'غير مرصود' : (center_assessment_status_map()[$status]['label'] ?? 'غير محدد'), 'notes' => (string) ($row['score_notes'] ?? ''), 'assessed_on' => (string) ($row['assessed_on'] ?? ''), 'criteria_count' => (int) ($row['criteria_count'] ?? 0), 'is_active' => $isActive, 'percentage' => $percentage, 'has_saved_score' => $hasSavedScore, ]; } if ($summary['max_score_total'] > 0) { $summary['overall_percentage'] = round(($summary['score_total'] / $summary['max_score_total']) * 100, 2); } $summary['performance'] = student_certificate_performance_meta((float) $summary['overall_percentage']); return $summary; } function school_student_options_by_cycle(int $centerApplicationId, int $cycleId): array { $students = list_school_students_by_cycle($centerApplicationId, $cycleId); $options = []; foreach ($students as $student) { $studentId = (int) ($student['id'] ?? 0); if ($studentId <= 0) { continue; } $options[$studentId] = [ 'label' => trim((string) ($student['full_name'] ?? '')), 'status' => (string) ($student['enrollment_status'] ?? ''), 'grade_level' => (string) ($student['grade_level'] ?? ''), 'guardian_phone' => (string) ($student['guardian_phone'] ?? ''), ]; } return $options; } function school_student_record_by_cycle(int $centerApplicationId, int $cycleId, int $studentId): ?array { if ($studentId <= 0) { return null; } $pdo = db_connection(); $stmt = $pdo->prepare( 'SELECT * FROM school_students WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id AND id = :student_id LIMIT 1' ); $stmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ':student_id' => $studentId, ]); $row = $stmt->fetch(); return $row ?: null; } function student_certificate_performance_meta(float $overallPercentage): array { if ($overallPercentage >= 90) { return ['key' => 'excellent', 'label' => 'Excellent', 'label_ar' => 'ممتاز']; } if ($overallPercentage >= 80) { return ['key' => 'very_good', 'label' => 'Very Good', 'label_ar' => 'جيد جداً']; } if ($overallPercentage >= 65) { return ['key' => 'good', 'label' => 'Good', 'label_ar' => 'جيد']; } return ['key' => 'poor', 'label' => 'Poor', 'label_ar' => 'ضعيف']; } function student_completion_certificate_honor_meta(float $overallPercentage): array { $performance = student_certificate_performance_meta($overallPercentage); return match ($performance['key']) { 'excellent' => [ 'key' => 'highest_honors', 'title' => 'With Highest Honors', 'title_ar' => 'بمرتبة الشرف العليا', 'completion_line_ar' => 'أتمّ/أتمّت الدورة بتميز استثنائي واستحقاق رفيع.', ], 'very_good' => [ 'key' => 'honors', 'title' => 'With Honors', 'title_ar' => 'بمرتبة الشرف', 'completion_line_ar' => 'أتمّ/أتمّت الدورة بمستوى قوي يبعث على الفخر.', ], 'good' => [ 'key' => 'merit', 'title' => 'With Merit', 'title_ar' => 'بتقدير جيد', 'completion_line_ar' => 'أتمّ/أتمّت الدورة بنجاح وأظهر/أظهرت التزاماً جيداً.', ], default => [ 'key' => 'completion', 'title' => 'Successful Completion', 'title_ar' => 'بإتمام ناجح', 'completion_line_ar' => 'أتمّ/أتمّت الدورة واستوفى/استوفت متطلباتها الأساسية.', ], }; } function school_student_certificate_summary(int $centerApplicationId, int $cycleId, int $studentId): array { $student = school_student_record_by_cycle($centerApplicationId, $cycleId, $studentId); $summary = [ 'student' => $student, 'assessments' => [], 'has_results' => false, 'completed_assessments' => 0, 'active_assessments' => 0, 'missing_assessments' => 0, 'absent_assessments' => 0, 'excused_assessments' => 0, 'overall_percentage' => 0.0, 'score_total' => 0.0, 'max_score_total' => 0.0, 'latest_assessed_on' => '', 'performance' => student_certificate_performance_meta(0.0), ]; if ($student === null) { return $summary; } $pdo = db_connection(); $activeAssessmentsStmt = $pdo->prepare( 'SELECT COUNT(*) FROM school_assessment_types WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id AND is_active = 1' ); $activeAssessmentsStmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ]); $summary['active_assessments'] = (int) $activeAssessmentsStmt->fetchColumn(); $stmt = $pdo->prepare( 'SELECT scores.assessment_type_id, scores.score, scores.max_score, scores.status, scores.notes, scores.assessed_on, assessments.title, assessments.category, assessments.weight_percentage, assessments.is_active, assessments.subject_id FROM school_assessment_scores scores INNER JOIN school_assessment_types assessments ON assessments.id = scores.assessment_type_id WHERE scores.center_application_id = :center_application_id AND scores.cycle_id = :cycle_id AND scores.student_id = :student_id AND assessments.is_active = 1 ORDER BY scores.assessed_on ASC, assessments.weight_percentage DESC, assessments.id ASC' ); $stmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ':student_id' => $studentId, ]); $rows = $stmt->fetchAll(); $subjectBuckets = []; $completedAssessments = 0; $scoreTotal = 0.0; $maxScoreTotal = 0.0; $latestAssessedOn = ''; foreach ($rows as $row) { $status = (string) ($row['status'] ?? ''); if ($status === 'absent') { $summary['absent_assessments']++; continue; } if ($status === 'excused') { $summary['excused_assessments']++; continue; } $score = isset($row['score']) ? (float) $row['score'] : null; $maxScore = (float) ($row['max_score'] ?? 0); if ($status !== 'present' || $score === null || $maxScore <= 0) { continue; } $percentage = round(($score / $maxScore) * 100, 2); $weight = max(0.0, (float) ($row['weight_percentage'] ?? 0)); $completedAssessments++; $scoreTotal += $score; $maxScoreTotal += $maxScore; $subjectId = (int) ($row['subject_id'] ?? 0); $subjectKey = $subjectId > 0 ? 'subject_' . $subjectId : 'assessment_' . (int) ($row['assessment_type_id'] ?? 0); if (!isset($subjectBuckets[$subjectKey])) { $subjectBuckets[$subjectKey] = [ 'score_total' => 0.0, 'max_score_total' => 0.0, ]; } $subjectBuckets[$subjectKey]['score_total'] += $score; $subjectBuckets[$subjectKey]['max_score_total'] += $maxScore; $assessedOn = (string) ($row['assessed_on'] ?? ''); if ($assessedOn !== '' && ($latestAssessedOn === '' || strtotime($assessedOn) > strtotime($latestAssessedOn))) { $latestAssessedOn = $assessedOn; } $summary['assessments'][] = [ 'assessment_type_id' => (int) ($row['assessment_type_id'] ?? 0), 'title' => (string) ($row['title'] ?? 'تقييم'), 'category' => (string) ($row['category'] ?? ''), 'weight_percentage' => $weight, 'score' => round($score, 2), 'max_score' => round($maxScore, 2), 'percentage' => $percentage, 'assessed_on' => $assessedOn, 'notes' => (string) ($row['notes'] ?? ''), ]; } $subjectPercentageSum = 0.0; $subjectCount = 0; foreach ($subjectBuckets as $bucket) { $subjectMaxScore = (float) ($bucket['max_score_total'] ?? 0); if ($subjectMaxScore <= 0) { continue; } $subjectPercentage = ((float) ($bucket['score_total'] ?? 0) / $subjectMaxScore) * 100; $subjectPercentageSum += max(0.0, min(100.0, $subjectPercentage)); $subjectCount++; } $overallPercentage = $subjectCount > 0 ? round($subjectPercentageSum / $subjectCount, 2) : 0.0; $summary['completed_assessments'] = $completedAssessments; $summary['missing_assessments'] = max(0, $summary['active_assessments'] - $summary['completed_assessments'] - $summary['absent_assessments'] - $summary['excused_assessments']); $summary['has_results'] = $completedAssessments > 0; $summary['overall_percentage'] = $overallPercentage; $summary['score_total'] = round($scoreTotal, 2); $summary['max_score_total'] = round($maxScoreTotal, 2); $summary['latest_assessed_on'] = $latestAssessedOn; $summary['performance'] = student_certificate_performance_meta($overallPercentage); return $summary; } function validate_attendance_input_for_cycle(int $centerApplicationId, int $cycleId, array $input): array { $data = attendance_defaults(); $data['student_id'] = (string) ((int) ($input['student_id'] ?? 0)); $data['attendance_status'] = clean_text((string) ($input['attendance_status'] ?? ''), 30); $data['absence_reason'] = clean_text((string) ($input['absence_reason'] ?? ''), 190); $data['notes'] = clean_text((string) ($input['notes'] ?? ''), 1000); $data['attendance_date'] = clean_text((string) ($input['attendance_date'] ?? ''), 20); $errors = []; $studentId = (int) $data['student_id']; $studentOptions = school_student_options_by_cycle($centerApplicationId, $cycleId); if ($studentId <= 0 || !array_key_exists($studentId, $studentOptions)) { $errors['student_id'] = 'يرجى اختيار طالب صحيح من نفس الدورة الموسمية.'; } $statusMap = attendance_status_map(); if (!array_key_exists($data['attendance_status'], $statusMap)) { $errors['attendance_status'] = 'يرجى اختيار حالة غياب صحيحة.'; } if ($data['attendance_date'] === '' || strtotime($data['attendance_date']) === false) { $errors['attendance_date'] = 'يرجى إدخال تاريخ صحيح للسجل اليومي.'; } if ($data['attendance_status'] !== 'late' && $data['absence_reason'] === '') { $errors['absence_reason'] = 'يرجى إدخال سبب الغياب أو العذر.'; } return [$data, $errors]; } function create_attendance_record_in_cycle(int $centerApplicationId, int $cycleId, array $data): int { $pdo = db_connection(); $stmt = $pdo->prepare( 'INSERT INTO school_attendance_records ( center_application_id, cycle_id, student_id, attendance_date, attendance_status, absence_reason, notes, created_at, updated_at ) VALUES ( :center_application_id, :cycle_id, :student_id, :attendance_date, :attendance_status, :absence_reason, :notes, NOW(), NOW() )' ); $stmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ':student_id' => (int) $data['student_id'], ':attendance_date' => $data['attendance_date'], ':attendance_status' => $data['attendance_status'], ':absence_reason' => $data['absence_reason'] !== '' ? $data['absence_reason'] : null, ':notes' => $data['notes'] !== '' ? $data['notes'] : null, ]); return (int) $pdo->lastInsertId(); } function list_school_attendance_records_by_cycle(int $centerApplicationId, int $cycleId, string $search = '', int $limit = 0, int $offset = 0): array { $pdo = db_connection(); $query = 'SELECT ar.*, s.student_code, s.full_name, s.grade_level, s.guardian_phone FROM school_attendance_records ar INNER JOIN school_students s ON s.id = ar.student_id WHERE ar.center_application_id = :center_application_id AND ar.cycle_id = :cycle_id'; $params = [ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ]; if ($search !== '') { $query .= ' AND (s.full_name LIKE :search1 OR s.student_code LIKE :search2)'; $params[':search1'] = "%$search%"; $params[':search2'] = "%$search%"; } $query .= ' ORDER BY ar.attendance_date DESC, ar.created_at DESC, ar.id DESC'; if ($limit > 0) { $query .= ' LIMIT ' . (int)$limit . ' OFFSET ' . (int)$offset; } $stmt = $pdo->prepare($query); $stmt->execute($params); return $stmt->fetchAll(); } function count_school_attendance_records_by_cycle(int $centerApplicationId, int $cycleId, string $search = ''): int { $pdo = db_connection(); $query = 'SELECT COUNT(*) FROM school_attendance_records ar INNER JOIN school_students s ON s.id = ar.student_id WHERE ar.center_application_id = :center_application_id AND ar.cycle_id = :cycle_id'; $params = [ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ]; if ($search !== '') { $query .= ' AND (s.full_name LIKE :search1 OR s.student_code LIKE :search2)'; $params[':search1'] = "%$search%"; $params[':search2'] = "%$search%"; } $stmt = $pdo->prepare($query); $stmt->execute($params); return (int)$stmt->fetchColumn(); } function school_attendance_metrics_by_cycle(int $centerApplicationId, int $cycleId): array { $pdo = db_connection(); $stmt = $pdo->prepare( "SELECT COUNT(*) AS total, COALESCE(SUM(attendance_status = 'absent'), 0) AS absent_count, COALESCE(SUM(attendance_status = 'excused'), 0) AS excused_count, COALESCE(SUM(attendance_status = 'late'), 0) AS late_count, COUNT(DISTINCT student_id) AS affected_students, MAX(attendance_date) AS latest_date, COALESCE(SUM(attendance_date = CURDATE()), 0) AS today_count FROM school_attendance_records WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id" ); $stmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ]); $row = $stmt->fetch() ?: []; return [ 'total' => (int) ($row['total'] ?? 0), 'absent' => (int) ($row['absent_count'] ?? 0), 'excused' => (int) ($row['excused_count'] ?? 0), 'late' => (int) ($row['late_count'] ?? 0), 'affected_students' => (int) ($row['affected_students'] ?? 0), 'latest_date' => (string) ($row['latest_date'] ?? ''), 'today_count' => (int) ($row['today_count'] ?? 0), ]; } function update_student_in_cycle(int $centerApplicationId, int $cycleId, int $studentId, array $data): void { $pdo = db_connection(); $stmt = $pdo->prepare( 'UPDATE school_students SET student_code = :student_code, full_name = :full_name, gender = :gender, grade_level = :grade_level, guardian_name = :guardian_name, guardian_phone = :guardian_phone, birth_date = :birth_date, enrollment_status = :enrollment_status, notes = :notes, updated_at = NOW() WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id AND id = :id' ); $stmt->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT); $stmt->bindValue(':cycle_id', $cycleId, PDO::PARAM_INT); $stmt->bindValue(':id', $studentId, PDO::PARAM_INT); $stmt->bindValue(':student_code', $data['student_code'], PDO::PARAM_STR); $stmt->bindValue(':full_name', $data['full_name'], PDO::PARAM_STR); $stmt->bindValue(':gender', $data['gender'], PDO::PARAM_STR); $stmt->bindValue(':grade_level', $data['grade_level'], PDO::PARAM_STR); $stmt->bindValue(':guardian_name', $data['guardian_name'], PDO::PARAM_STR); $stmt->bindValue(':guardian_phone', $data['guardian_phone'], PDO::PARAM_STR); $stmt->bindValue(':birth_date', $data['birth_date'] !== '' ? $data['birth_date'] : null, $data['birth_date'] !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL); $stmt->bindValue(':enrollment_status', $data['enrollment_status'], PDO::PARAM_STR); $stmt->bindValue(':notes', $data['notes'] !== '' ? $data['notes'] : null, $data['notes'] !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL); $stmt->execute(); } function update_assessment_type_in_cycle(int $centerApplicationId, int $cycleId, int $assessmentId, array $data): bool { $pdo = db_connection(); $stmt = $pdo->prepare( 'UPDATE school_assessment_types SET subject_id = :subject_id, title = :title, category = :category, scale_type = :scale_type, max_score = :max_score, weight_percentage = :weight_percentage, is_active = :is_active, notes = :notes WHERE id = :id AND center_application_id = :center_application_id AND cycle_id = :cycle_id' ); return $stmt->execute([ ':id' => $assessmentId, ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ':subject_id' => !empty($data['subject_id']) ? (int) $data['subject_id'] : null, ':title' => $data['title'], ':category' => $data['category'], ':scale_type' => $data['scale_type'], ':max_score' => (float) $data['max_score'], ':weight_percentage' => (float) $data['weight_percentage'], ':is_active' => (int) $data['is_active'], ':notes' => $data['notes'] !== '' ? $data['notes'] : null, ]); } function get_global_center_assessment_type(int $assessmentId): ?array { $pdo = db_connection(); $stmt = $pdo->prepare('SELECT * FROM global_center_assessment_types WHERE id = :id LIMIT 1'); $stmt->execute([':id' => $assessmentId]); $row = $stmt->fetch(); return $row ?: null; } function create_global_center_assessment_type(array $data): int { $pdo = db_connection(); $stmt = $pdo->prepare( 'INSERT INTO global_center_assessment_types ( title, category, scale_type, max_score, weight_percentage, is_active, notes, created_at, updated_at ) VALUES ( :title, :category, :scale_type, :max_score, :weight_percentage, :is_active, :notes, NOW(), NOW() )' ); $stmt->execute([ ':title' => $data['title'], ':category' => $data['category'], ':scale_type' => $data['scale_type'], ':max_score' => (float) $data['max_score'], ':weight_percentage' => (float) $data['weight_percentage'], ':is_active' => (int) $data['is_active'], ':notes' => $data['notes'] !== '' ? $data['notes'] : null, ]); return (int) $pdo->lastInsertId(); } function update_global_center_assessment_type(int $assessmentId, array $data): bool { $pdo = db_connection(); $stmt = $pdo->prepare( 'UPDATE global_center_assessment_types SET title = :title, category = :category, scale_type = :scale_type, max_score = :max_score, weight_percentage = :weight_percentage, is_active = :is_active, notes = :notes, updated_at = NOW() WHERE id = :id' ); return $stmt->execute([ ':id' => $assessmentId, ':title' => $data['title'], ':category' => $data['category'], ':scale_type' => $data['scale_type'], ':max_score' => (float) $data['max_score'], ':weight_percentage' => (float) $data['weight_percentage'], ':is_active' => (int) $data['is_active'], ':notes' => $data['notes'] !== '' ? $data['notes'] : null, ]); } function list_global_center_assessments(array $filters = [], int $limit = 0, int $offset = 0): array { $pdo = db_connection(); $query = 'SELECT gcat.*, ( SELECT COUNT(*) FROM global_center_assessment_criteria criteria WHERE criteria.assessment_type_id = gcat.id AND criteria.is_active = 1 ) AS criteria_count, ( SELECT COALESCE(SUM(criteria.max_score), 0) FROM global_center_assessment_criteria criteria WHERE criteria.assessment_type_id = gcat.id AND criteria.is_active = 1 ) AS criteria_total_max_score FROM global_center_assessment_types gcat WHERE 1 = 1'; $params = []; $search = trim((string) ($filters['search'] ?? '')); if ($search !== '') { $query .= ' AND (gcat.title LIKE :search1 OR gcat.category LIKE :search2 OR gcat.notes LIKE :search3)'; $params[':search1'] = "%{$search}%"; $params[':search2'] = "%{$search}%"; $params[':search3'] = "%{$search}%"; } $category = trim((string) ($filters['category'] ?? '')); if ($category !== '') { $query .= ' AND gcat.category = :category'; $params[':category'] = $category; } $query .= ' ORDER BY gcat.is_active DESC, gcat.updated_at DESC, gcat.id DESC'; if ($limit > 0) { $query .= ' LIMIT ' . (int) $limit . ' OFFSET ' . (int) $offset; } $stmt = $pdo->prepare($query); $stmt->execute($params); return $stmt->fetchAll(); } function count_global_center_assessments(array $filters = []): int { $pdo = db_connection(); $query = 'SELECT COUNT(*) FROM global_center_assessment_types WHERE 1 = 1'; $params = []; $search = trim((string) ($filters['search'] ?? '')); if ($search !== '') { $query .= ' AND (title LIKE :search1 OR category LIKE :search2 OR notes LIKE :search3)'; $params[':search1'] = "%{$search}%"; $params[':search2'] = "%{$search}%"; $params[':search3'] = "%{$search}%"; } $category = trim((string) ($filters['category'] ?? '')); if ($category !== '') { $query .= ' AND category = :category'; $params[':category'] = $category; } $stmt = $pdo->prepare($query); $stmt->execute($params); return (int) $stmt->fetchColumn(); } function global_center_assessment_metrics(): array { $pdo = db_connection(); $stmt = $pdo->query( "SELECT COUNT(*) AS total_count, COALESCE(SUM(is_active = 1), 0) AS active_count, COALESCE(SUM(is_active = 0), 0) AS inactive_count, COALESCE(SUM(CASE WHEN is_active = 1 THEN weight_percentage ELSE 0 END), 0) AS active_weight, COALESCE(AVG(max_score), 0) AS average_max_score, COALESCE(SUM(scale_type = 'percentage'), 0) AS percentage_count, COALESCE(SUM(scale_type = 'points'), 0) AS points_count, COALESCE(SUM(scale_type LIKE 'rubric_%'), 0) AS rubric_count FROM global_center_assessment_types" ); $row = $stmt ? ($stmt->fetch() ?: []) : []; return [ 'total' => (int) ($row['total_count'] ?? 0), 'active' => (int) ($row['active_count'] ?? 0), 'inactive' => (int) ($row['inactive_count'] ?? 0), 'active_weight' => (float) ($row['active_weight'] ?? 0), 'average_max_score' => (float) ($row['average_max_score'] ?? 0), 'percentage' => (int) ($row['percentage_count'] ?? 0), 'points' => (int) ($row['points_count'] ?? 0), 'rubric' => (int) ($row['rubric_count'] ?? 0), ]; } function global_center_assessment_type_options(bool $onlyActive = false): array { $rows = list_global_center_assessments(); $options = []; foreach ($rows as $assessment) { $assessmentId = (int) ($assessment['id'] ?? 0); if ($assessmentId <= 0) { continue; } $isActive = (int) ($assessment['is_active'] ?? 0) === 1; if ($onlyActive && !$isActive) { continue; } $title = trim((string) ($assessment['title'] ?? '')); $label = $title !== '' ? $title : ('قالب #' . $assessmentId); $category = trim((string) ($assessment['category'] ?? '')); if ($category !== '') { $label .= ' — ' . $category; } $criteriaCount = (int) ($assessment['criteria_count'] ?? 0); $criteriaTotal = (float) ($assessment['criteria_total_max_score'] ?? 0); $options[$assessmentId] = [ 'id' => $assessmentId, 'label' => $label, 'title' => $title, 'category' => $category, 'max_score' => (float) ($assessment['max_score'] ?? 0), 'weight_percentage' => (float) ($assessment['weight_percentage'] ?? 0), 'criteria_count' => $criteriaCount, 'criteria_total_max_score' => $criteriaTotal, 'has_criteria' => $criteriaCount > 0, 'is_active' => $isActive ? 1 : 0, ]; } return $options; } function list_global_center_assessment_criteria_by_assessment(int $assessmentTypeId, bool $onlyActive = false): array { $pdo = db_connection(); $query = 'SELECT * FROM global_center_assessment_criteria WHERE assessment_type_id = :assessment_type_id'; if ($onlyActive) { $query .= ' AND is_active = 1'; } $query .= ' ORDER BY sort_order ASC, id ASC'; $stmt = $pdo->prepare($query); $stmt->execute([':assessment_type_id' => $assessmentTypeId]); return $stmt->fetchAll(); } function global_center_assessment_criteria_metrics(int $assessmentTypeId): array { $pdo = db_connection(); $stmt = $pdo->prepare( "SELECT COUNT(*) AS total_count, COALESCE(SUM(is_active = 1), 0) AS active_count, COALESCE(SUM(CASE WHEN is_active = 1 THEN max_score ELSE 0 END), 0) AS active_max_score FROM global_center_assessment_criteria WHERE assessment_type_id = :assessment_type_id" ); $stmt->execute([':assessment_type_id' => $assessmentTypeId]); $row = $stmt->fetch() ?: []; return [ 'total' => (int) ($row['total_count'] ?? 0), 'active' => (int) ($row['active_count'] ?? 0), 'active_max_score' => (float) ($row['active_max_score'] ?? 0), ]; } function validate_global_center_assessment_criteria_input(int $assessmentTypeId, array $input): array { $data = ['criteria' => []]; $errors = []; $assessment = get_global_center_assessment_type($assessmentTypeId); if (!$assessment) { return [$data, ['form' => 'يرجى اختيار قالب تقييم صحيح.']]; } $postedRows = $input['criteria'] ?? []; if (!is_array($postedRows)) { $postedRows = []; } $position = 1; $activeCount = 0; foreach ($postedRows as $rowKey => $row) { if (!is_array($row)) { continue; } $criterionId = (int) ($row['id'] ?? 0); $title = clean_text((string) ($row['title'] ?? ''), 150); $maxScoreRaw = str_replace(',', '.', clean_text((string) ($row['max_score'] ?? ''), 30)); $notes = clean_text((string) ($row['notes'] ?? ''), 500); $isActive = ((string) ($row['is_active'] ?? '1')) === '1' ? 1 : 0; if ($criterionId <= 0 && $title === '' && $maxScoreRaw === '' && $notes === '') { continue; } $rowErrors = []; if ($title === '') { $rowErrors[] = 'اسم البند مطلوب.'; } $maxScore = null; if ($maxScoreRaw === '' || !is_numeric($maxScoreRaw)) { $rowErrors[] = 'أدخل درجة رقمية للبند.'; } else { $maxScore = round((float) $maxScoreRaw, 2); if ($maxScore <= 0 || $maxScore > 1000) { $rowErrors[] = 'درجة البند يجب أن تكون بين 0.01 و1000.'; } } if ($rowErrors !== []) { $errors['criteria_' . $rowKey] = implode(' ', $rowErrors); } $data['criteria'][] = [ 'id' => $criterionId, 'title' => $title, 'max_score' => $maxScore !== null ? number_format($maxScore, 2, '.', '') : '', 'notes' => $notes, 'is_active' => (string) $isActive, 'sort_order' => $position, ]; if ($isActive === 1) { $activeCount++; } $position++; } if ($data['criteria'] === []) { $errors['form'] = 'أضف بند تقييم واحداً على الأقل قبل الحفظ.'; } elseif ($activeCount === 0) { $errors['form'] = 'فعّل بنداً واحداً على الأقل ليكون متاحاً للمقيمين.'; } return [$data, $errors]; } function sync_global_center_assessment_total_score_from_criteria(int $assessmentTypeId): void { $pdo = db_connection(); $criteria = list_global_center_assessment_criteria_by_assessment($assessmentTypeId, true); if ($criteria === []) { return; } $totalMaxScore = 0.0; foreach ($criteria as $criterion) { $totalMaxScore += (float) ($criterion['max_score'] ?? 0); } $totalMaxScore = round($totalMaxScore, 2); $stmt = $pdo->prepare( 'UPDATE global_center_assessment_types SET max_score = :max_score, updated_at = NOW() WHERE id = :id' ); $stmt->execute([ ':max_score' => $totalMaxScore, ':id' => $assessmentTypeId, ]); } function save_global_center_assessment_criteria(int $assessmentTypeId, array $data): int { $pdo = db_connection(); $existingStmt = $pdo->prepare( 'SELECT id FROM global_center_assessment_criteria WHERE assessment_type_id = :assessment_type_id' ); $existingStmt->execute([':assessment_type_id' => $assessmentTypeId]); $existingIds = array_map('intval', $existingStmt->fetchAll(PDO::FETCH_COLUMN)); $existingMap = array_fill_keys($existingIds, true); $insertStmt = $pdo->prepare( 'INSERT INTO global_center_assessment_criteria ( assessment_type_id, title, max_score, sort_order, is_active, notes, created_at, updated_at ) VALUES ( :assessment_type_id, :title, :max_score, :sort_order, :is_active, :notes, NOW(), NOW() )' ); $updateStmt = $pdo->prepare( 'UPDATE global_center_assessment_criteria SET title = :title, max_score = :max_score, sort_order = :sort_order, is_active = :is_active, notes = :notes, updated_at = NOW() WHERE id = :id AND assessment_type_id = :assessment_type_id' ); $saved = 0; foreach ($data['criteria'] as $criterion) { $params = [ ':assessment_type_id' => $assessmentTypeId, ':title' => (string) ($criterion['title'] ?? ''), ':max_score' => (float) ($criterion['max_score'] ?? 0), ':sort_order' => (int) ($criterion['sort_order'] ?? 0), ':is_active' => ((string) ($criterion['is_active'] ?? '1')) === '1' ? 1 : 0, ':notes' => !empty($criterion['notes']) ? (string) $criterion['notes'] : null, ]; $criterionId = (int) ($criterion['id'] ?? 0); if ($criterionId > 0 && isset($existingMap[$criterionId])) { $updateStmt->execute($params + [':id' => $criterionId]); unset($existingMap[$criterionId]); } else { $insertStmt->execute($params); } $saved++; } if ($existingMap !== []) { $deleteStmt = $pdo->prepare( 'DELETE FROM global_center_assessment_criteria WHERE assessment_type_id = :assessment_type_id AND id = :id' ); foreach (array_keys($existingMap) as $obsoleteId) { $deleteStmt->execute([ ':assessment_type_id' => $assessmentTypeId, ':id' => (int) $obsoleteId, ]); } } sync_global_center_assessment_total_score_from_criteria($assessmentTypeId); return $saved; } function delete_global_center_assessment_type(int $assessmentId): bool { $pdo = db(); try { $pdo->beginTransaction(); $stmt = $pdo->prepare('DELETE FROM global_center_assessment_criteria WHERE assessment_type_id = :assessment_type_id'); $stmt->bindValue(':assessment_type_id', $assessmentId, PDO::PARAM_INT); $stmt->execute(); $stmt = $pdo->prepare('DELETE FROM global_center_assessment_types WHERE id = :id'); $stmt->bindValue(':id', $assessmentId, PDO::PARAM_INT); $stmt->execute(); $pdo->commit(); return true; } catch (PDOException $e) { $pdo->rollBack(); error_log("Failed to delete global_center_assessment_type ID $assessmentId: " . $e->getMessage()); return false; } } function import_global_center_assessment_to_center(int $globalAssessmentId, int $centerApplicationId, int $cycleId): int { $pdo = db(); $globalAssessment = get_global_center_assessment_type($globalAssessmentId); if (!$globalAssessment) { throw new InvalidArgumentException("Global assessment template not found."); } $globalCriteria = list_global_center_assessment_criteria_by_assessment($globalAssessmentId, true); try { $pdo->beginTransaction(); $stmt = $pdo->prepare('INSERT INTO center_assessment_types (center_application_id, cycle_id, global_template_id, title, category, scale_type, max_score, weight_percentage, is_active, notes) VALUES (:center_application_id, :cycle_id, :global_template_id, :title, :category, :scale_type, :max_score, :weight_percentage, :is_active, :notes)'); $stmt->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT); $stmt->bindValue(':cycle_id', $cycleId, PDO::PARAM_INT); $stmt->bindValue(':global_template_id', $globalAssessmentId, PDO::PARAM_INT); $stmt->bindValue(':title', $globalAssessment['title'], PDO::PARAM_STR); $stmt->bindValue(':category', $globalAssessment['category'], PDO::PARAM_STR); $stmt->bindValue(':scale_type', $globalAssessment['scale_type'], PDO::PARAM_STR); $stmt->bindValue(':max_score', $globalAssessment['max_score'], PDO::PARAM_STR); $stmt->bindValue(':weight_percentage', $globalAssessment['weight_percentage'], PDO::PARAM_STR); $stmt->bindValue(':is_active', $globalAssessment['is_active'], PDO::PARAM_INT); $stmt->bindValue(':notes', $globalAssessment['notes'] ?? null, PDO::PARAM_STR); $stmt->execute(); $newAssessmentId = (int) $pdo->lastInsertId(); if ($globalCriteria !== []) { $stmtCrit = $pdo->prepare('INSERT INTO center_assessment_criteria (center_application_id, cycle_id, assessment_type_id, title, notes, max_score, sort_order, is_active) VALUES (:center_application_id, :cycle_id, :assessment_type_id, :title, :notes, :max_score, :sort_order, :is_active)'); foreach ($globalCriteria as $criteria) { $stmtCrit->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT); $stmtCrit->bindValue(':cycle_id', $cycleId, PDO::PARAM_INT); $stmtCrit->bindValue(':assessment_type_id', $newAssessmentId, PDO::PARAM_INT); $stmtCrit->bindValue(':title', $criteria['title'], PDO::PARAM_STR); $stmtCrit->bindValue(':notes', $criteria['notes'] ?? null, PDO::PARAM_STR); $stmtCrit->bindValue(':max_score', $criteria['max_score'], PDO::PARAM_STR); $stmtCrit->bindValue(':sort_order', $criteria['sort_order'] ?? 0, PDO::PARAM_INT); $stmtCrit->bindValue(':is_active', $criteria['is_active'], PDO::PARAM_INT); $stmtCrit->execute(); } } $pdo->commit(); return $newAssessmentId; } catch (PDOException $e) { $pdo->rollBack(); error_log("Failed to import global center assessment: " . $e->getMessage()); throw $e; } }