$type, 'message' => $message, ]; } function consume_flash(): ?array { if (empty($_SESSION['flash']) || !is_array($_SESSION['flash'])) { return null; } $flash = $_SESSION['flash']; unset($_SESSION['flash']); return $flash; } function status_map(): array { return [ 'submitted' => ['label' => 'قيد الاستلام', 'class' => 'status-submitted'], 'under_review' => ['label' => 'تحت المراجعة', 'class' => 'status-review'], 'approved' => ['label' => 'معتمد', 'class' => 'status-approved'], 'rejected' => ['label' => 'بحاجة إلى استكمال', 'class' => 'status-rejected'], ]; } function status_meta(string $status): array { $map = status_map(); return $map[$status] ?? ['label' => 'غير معروف', 'class' => 'status-muted']; } function status_badge(string $status): string { $meta = status_meta($status); return '' . e($meta['label']) . ''; } function db_connection(): PDO { static $pdo = null; static $bootstrapped = false; if (!$pdo instanceof PDO) { $pdo = db(); } if (!$bootstrapped) { ensure_center_application_schema($pdo); seed_center_application_demo_data($pdo); ensure_school_module_schema($pdo); ensure_school_cycle_schema($pdo); seed_school_module_demo_data($pdo); $bootstrapped = true; } return $pdo; } function ensure_center_application_schema(PDO $pdo): void { static $done = false; if ($done) { return; } $migrationPath = __DIR__ . '/../db/migrations/20260416_center_applications.sql'; if (is_file($migrationPath)) { $sql = file_get_contents($migrationPath); if (is_string($sql) && trim($sql) !== '') { $pdo->exec($sql); } } $done = true; } function ensure_school_module_schema(PDO $pdo): void { static $done = false; if ($done) { return; } $migrationPath = __DIR__ . '/../db/migrations/20260416_school_modules.sql'; if (is_file($migrationPath)) { $sql = file_get_contents($migrationPath); if (is_string($sql) && trim($sql) !== '') { $pdo->exec($sql); } } $done = true; } function seed_school_module_demo_data(PDO $pdo): void { $approvedStmt = $pdo->query("SELECT * FROM center_applications WHERE status = 'approved' ORDER BY id ASC LIMIT 1"); $approvedApplication = $approvedStmt ? ($approvedStmt->fetch() ?: null) : null; if (!$approvedApplication) { return; } $approvedId = (int) ($approvedApplication['id'] ?? 0); if ($approvedId <= 0) { return; } $defaultCycleId = ensure_default_school_cycle_record($pdo, $approvedApplication); $studentCount = (int) $pdo->query('SELECT COUNT(*) FROM school_students')->fetchColumn(); if ($studentCount === 0) { $studentRows = [ ['ST-301', 'أحمد بن سالم الحارثي', 'طالب', 'الصف السابع', 'سالم الحارثي', '0501112233', '2013-03-11', 'active', 'مسجل في مسار القرآن والابتكار.'], ['ST-302', 'مريم بنت ناصر الكندية', 'طالبة', 'الصف الثامن', 'ناصر الكندي', '0502223344', '2012-11-05', 'active', 'بحاجة إلى متابعة في الأنشطة العلمية.'], ['ST-303', 'سيف بن راشد المقبالي', 'طالب', 'الصف السادس', 'راشد المقبالي', '0503334455', '2014-01-22', 'waiting', 'بانتظار تأكيد المقعد بعد مراجعة السعة.'], ]; $studentStmt = $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() )' ); foreach ($studentRows as $row) { $studentStmt->execute([ ':center_application_id' => $approvedId, ':cycle_id' => $defaultCycleId, ':student_code' => $row[0], ':full_name' => $row[1], ':gender' => $row[2], ':grade_level' => $row[3], ':guardian_name' => $row[4], ':guardian_phone' => $row[5], ':birth_date' => $row[6], ':enrollment_status' => $row[7], ':notes' => $row[8], ]); } } $teacherCount = (int) $pdo->query('SELECT COUNT(*) FROM school_teachers')->fetchColumn(); if ($teacherCount === 0) { $teacherRows = [ ['أ. هدى بنت راشد المعمرية', 'مشرفة أكاديمية', 'اللغة العربية والقرآن', '0504445566', 'huda.muamari@example.com', 'active', 'تقود خطة الإشراف الأكاديمي لهذا الموسم.'], ['أ. مازن بن سعيد البلوشي', 'منسق أنشطة', 'الأنشطة العلمية والابتكار', '0505556677', 'mazin.balushi@example.com', 'active', 'مسؤول عن الربط بين الأنشطة الصفية والفعاليات الأسبوعية.'], ['أ. عائشة بنت خالد السعدية', 'معلمة', 'الرياضيات والمهارات الرقمية', '0506667788', 'aisha.saadiya@example.com', 'pending', 'بانتظار استكمال جدولها النهائي قبل التفعيل الكامل.'], ]; $teacherStmt = $pdo->prepare( 'INSERT INTO school_teachers ( center_application_id, cycle_id, full_name, role_title, specialization, phone, email, employment_status, notes, created_at, updated_at ) VALUES ( :center_application_id, :cycle_id, :full_name, :role_title, :specialization, :phone, :email, :employment_status, :notes, NOW(), NOW() )' ); foreach ($teacherRows as $row) { $teacherStmt->execute([ ':center_application_id' => $approvedId, ':cycle_id' => $defaultCycleId, ':full_name' => $row[0], ':role_title' => $row[1], ':specialization' => $row[2], ':phone' => $row[3], ':email' => $row[4], ':employment_status' => $row[5], ':notes' => $row[6], ]); } } $assessmentCount = (int) $pdo->query('SELECT COUNT(*) FROM school_assessment_types')->fetchColumn(); if ($assessmentCount === 0) { $assessmentRows = [ ['الاختبار التشخيصي', 'تشخيصي', 'percentage', 100.00, 10.00, 1, 'يقيس مستوى الانطلاق في بداية البرنامج.'], ['المشاركة الصفية', 'مشاركة', 'points', 20.00, 15.00, 1, 'متابعة التفاعل اليومي والانضباط داخل الحلقة أو الصف.'], ['المشروع التطبيقي', 'مشاريع', 'rubric', 4.00, 35.00, 1, 'يقيس التطبيق العملي والابتكار والعمل الجماعي.'], ['الاختبار الختامي', 'ختامي', 'percentage', 100.00, 40.00, 1, 'يعكس المخرجات النهائية ونسبة الإتقان العامة.'], ]; $assessmentStmt = $pdo->prepare( 'INSERT INTO school_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() )' ); foreach ($assessmentRows as $row) { $assessmentStmt->execute([ ':center_application_id' => $approvedId, ':cycle_id' => $defaultCycleId, ':title' => $row[0], ':category' => $row[1], ':scale_type' => $row[2], ':max_score' => $row[3], ':weight_percentage' => $row[4], ':is_active' => $row[5], ':notes' => $row[6], ]); } } $attendanceCount = (int) $pdo->query('SELECT COUNT(*) FROM school_attendance_records')->fetchColumn(); if ($attendanceCount === 0) { $studentStmt = $pdo->prepare( 'SELECT id FROM school_students WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id ORDER BY id ASC' ); $studentStmt->execute([ ':center_application_id' => $approvedId, ':cycle_id' => $defaultCycleId, ]); $studentRows = $studentStmt->fetchAll(); if ($studentRows !== []) { $studentIds = []; foreach ($studentRows as $studentRow) { $studentIds[] = (int) ($studentRow['id'] ?? 0); } $attendanceRows = []; if (isset($studentIds[0])) { $attendanceRows[] = [$studentIds[0], '2026-04-13', 'absent', 'ظرف صحي', 'تم التواصل مع ولي الأمر والمتابعة جارية.']; $attendanceRows[] = [$studentIds[0], '2026-04-15', 'late', 'تأخر في المواصلات', 'دخل بعد بداية الفترة الأولى بـ 15 دقيقة.']; } if (isset($studentIds[1])) { $attendanceRows[] = [$studentIds[1], '2026-04-14', 'excused', 'موعد عائلي مسبق', 'غياب بعذر وتمت مشاركة الواجبات مع الطالبة.']; } if (isset($studentIds[2])) { $attendanceRows[] = [$studentIds[2], '2026-04-15', 'absent', 'لم يتم الرد على الاتصال', 'يحتاج إلى متابعة إضافية بسبب تكرر الغياب.']; } if ($attendanceRows !== []) { $attendanceStmt = $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() )' ); foreach ($attendanceRows as $row) { $attendanceStmt->execute([ ':center_application_id' => $approvedId, ':cycle_id' => $defaultCycleId, ':student_id' => $row[0], ':attendance_date' => $row[1], ':attendance_status' => $row[2], ':absence_reason' => $row[3], ':notes' => $row[4], ]); } } } } } function seed_center_application_demo_data(PDO $pdo): void { $count = (int) $pdo->query('SELECT COUNT(*) FROM center_applications')->fetchColumn(); if ($count > 0) { return; } $rows = [ [ 'مركز النور الصيفي', 'العاصمة', 'بنين', 'طلاب', 'أ. خالد السالمي', '0501234567', 'alnoor@example.com', 180, '2026-06-15', '2026-08-15', 'يركز على القرآن والمهارات الرقمية والأنشطة الرياضية المسائية.', 'submitted', 'بانتظار زيارة ميدانية أولية.', null, ], [ 'مركز الواحة للفتيات', 'الزور', 'بنات', 'طالبات', 'أ. نورة الشيبانية', '0507654321', 'alwaha@example.com', 140, '2026-06-20', '2026-08-10', 'طلب تجهيز معمل حاسب وقاعة أنشطة فنية.', 'under_review', 'تمت مراجعة الوثائق والمطلوب استكمال خطة الأمن والسلامة.', 82, ], [ 'مركز الريادة المجتمعي', 'الساحل', 'مختلط', 'طلاب وطالبات', 'أ. سيف الحارثي', '0509988776', 'riyadah@example.com', 220, '2026-06-18', '2026-08-20', 'يشمل برنامجاً علمياً ومساراً للابتكار ومتابعة أسرية.', 'approved', 'المركز مستوفٍ للاشتراطات ويُنصح ببدء التسجيل.', 94, ], ]; $stmt = $pdo->prepare( 'INSERT INTO center_applications ( center_name, city, center_type, gender_scope, director_name, phone, email, expected_students, start_date, end_date, notes, status, admin_notes, evaluation_score, submitted_at, updated_at ) VALUES ( :center_name, :city, :center_type, :gender_scope, :director_name, :phone, :email, :expected_students, :start_date, :end_date, :notes, :status, :admin_notes, :evaluation_score, NOW(), NOW() )' ); foreach ($rows as $row) { $stmt->execute([ ':center_name' => $row[0], ':city' => $row[1], ':center_type' => $row[2], ':gender_scope' => $row[3], ':director_name' => $row[4], ':phone' => $row[5], ':email' => $row[6], ':expected_students' => $row[7], ':start_date' => $row[8], ':end_date' => $row[9], ':notes' => $row[10], ':status' => $row[11], ':admin_notes' => $row[12], ':evaluation_score' => $row[13], ]); } } function clean_text(string $value, int $limit = 255): string { $normalized = preg_replace('/\s+/u', ' ', trim($value)); if (!is_string($normalized)) { $normalized = trim($value); } if (function_exists('mb_substr')) { return mb_substr($normalized, 0, $limit); } return substr($normalized, 0, $limit); } function application_defaults(): array { return [ 'center_name' => '', 'city' => '', 'center_type' => '', 'gender_scope' => '', 'director_name' => '', 'phone' => '', 'email' => '', 'expected_students' => '', 'start_date' => '', 'end_date' => '', 'notes' => '', 'subjects' => [], ]; } function validate_application_input(array $input): array { $data = application_defaults(); foreach ($data as $key => $_value) { if ($key === 'subjects') { $data[$key] = is_array($input[$key] ?? null) ? $input[$key] : []; continue; } $data[$key] = clean_text((string) ($input[$key] ?? ''), $key === 'notes' ? 1000 : 190); } $errors = []; if ($data['center_name'] === '') { $errors['center_name'] = 'يرجى إدخال اسم المركز.'; } if ($data['city'] === '') { $errors['city'] = 'يرجى اختيار المدينة أو الولاية الفرعية.'; } if ($data['center_type'] === '') { $errors['center_type'] = 'يرجى تحديد نوع المركز.'; } if ($data['gender_scope'] === '') { $errors['gender_scope'] = 'يرجى تحديد الفئة المستهدفة.'; } if ($data['director_name'] === '') { $errors['director_name'] = 'يرجى إدخال اسم مدير أو مديرة المركز.'; } if ($data['phone'] === '') { $errors['phone'] = 'يرجى إدخال رقم الهاتف.'; } if ($data['email'] === '' || !filter_var($data['email'], FILTER_VALIDATE_EMAIL)) { $errors['email'] = 'يرجى إدخال بريد إلكتروني صحيح.'; } $expectedStudents = filter_var($input['expected_students'] ?? null, FILTER_VALIDATE_INT, [ 'options' => ['min_range' => 10, 'max_range' => 2000], ]); if ($expectedStudents === false) { $errors['expected_students'] = 'أدخل عدداً صحيحاً بين 10 و2000.'; } else { $data['expected_students'] = (string) $expectedStudents; } $startDate = clean_text((string) ($input['start_date'] ?? ''), 20); $endDate = clean_text((string) ($input['end_date'] ?? ''), 20); $data['start_date'] = $startDate; $data['end_date'] = $endDate; if ($startDate === '') { $errors['start_date'] = 'حدد تاريخ بداية البرنامج.'; } if ($endDate === '') { $errors['end_date'] = 'حدد تاريخ نهاية البرنامج.'; } if ($startDate !== '' && $endDate !== '' && strtotime($endDate) < strtotime($startDate)) { $errors['end_date'] = 'يجب أن يكون تاريخ النهاية بعد البداية.'; } if (empty($data['subjects'])) { $errors['subjects'] = 'يرجى اختيار مادة واحدة على الأقل.'; } else { $data['subjects'] = array_map('intval', $data['subjects']); } return [$data, $errors]; } function create_application(array $data): int { $pdo = db_connection(); $stmt = $pdo->prepare( 'INSERT INTO center_applications ( center_name, city, center_type, gender_scope, director_name, phone, email, expected_students, start_date, end_date, notes, subjects, status, submitted_at, updated_at ) VALUES ( :center_name, :city, :center_type, :gender_scope, :director_name, :phone, :email, :expected_students, :start_date, :end_date, :notes, :subjects, :status, NOW(), NOW() )' ); $stmt->execute([ ':center_name' => $data['center_name'], ':city' => $data['city'], ':center_type' => $data['center_type'], ':gender_scope' => $data['gender_scope'], ':director_name' => $data['director_name'], ':phone' => $data['phone'], ':email' => $data['email'], ':expected_students' => (int) $data['expected_students'], ':start_date' => $data['start_date'], ':end_date' => $data['end_date'], ':notes' => $data['notes'], ':subjects' => json_encode($data['subjects']), ':status' => 'submitted', ]); return (int) $pdo->lastInsertId(); } function list_applications(string $status = 'all'): array { $pdo = db_connection(); if ($status === 'all' || !array_key_exists($status, status_map())) { $stmt = $pdo->query('SELECT * FROM center_applications ORDER BY submitted_at DESC, id DESC'); return $stmt->fetchAll(); } $stmt = $pdo->prepare('SELECT * FROM center_applications WHERE status = :status ORDER BY submitted_at DESC, id DESC'); $stmt->execute([':status' => $status]); return $stmt->fetchAll(); } function get_application(int $id): ?array { $pdo = db_connection(); $stmt = $pdo->prepare('SELECT * FROM center_applications WHERE id = :id LIMIT 1'); $stmt->execute([':id' => $id]); $row = $stmt->fetch(); return $row ?: null; } function update_application_review(int $id, string $status, string $adminNotes, ?int $evaluationScore): void { $allowed = array_keys(status_map()); if (!in_array($status, $allowed, true)) { throw new InvalidArgumentException('Invalid status value.'); } $pdo = db_connection(); $stmt = $pdo->prepare( 'UPDATE center_applications SET status = :status, admin_notes = :admin_notes, evaluation_score = :evaluation_score, updated_at = NOW() WHERE id = :id' ); $stmt->bindValue(':status', $status, PDO::PARAM_STR); $stmt->bindValue(':admin_notes', $adminNotes !== '' ? $adminNotes : null, $adminNotes !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL); $stmt->bindValue(':evaluation_score', $evaluationScore, $evaluationScore !== null ? PDO::PARAM_INT : PDO::PARAM_NULL); $stmt->bindValue(':id', $id, PDO::PARAM_INT); $stmt->execute(); } function dashboard_metrics(): array { $pdo = db_connection(); $totals = [ 'all' => 0, 'submitted' => 0, 'under_review' => 0, 'approved' => 0, 'rejected' => 0, 'expected_students' => 0, ]; $summary = $pdo->query('SELECT status, COUNT(*) AS total FROM center_applications GROUP BY status')->fetchAll(); foreach ($summary as $row) { $status = (string) ($row['status'] ?? ''); $count = (int) ($row['total'] ?? 0); if (array_key_exists($status, $totals)) { $totals[$status] = $count; $totals['all'] += $count; } } $totals['expected_students'] = (int) $pdo->query('SELECT COALESCE(SUM(expected_students), 0) FROM center_applications')->fetchColumn(); return $totals; } function latest_applications(int $limit = 5): array { $pdo = db_connection(); $stmt = $pdo->prepare('SELECT * FROM center_applications ORDER BY submitted_at DESC, id DESC LIMIT :limit'); $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetchAll(); } function student_defaults(): array { return [ 'student_code' => '', 'full_name' => '', 'gender' => '', 'grade_level' => '', 'guardian_name' => '', 'guardian_phone' => '', 'birth_date' => '', 'enrollment_status' => 'active', 'notes' => '', ]; } function student_gender_options(): array { return ['طالب', 'طالبة']; } function student_grade_options(): array { return [ 'الصف الرابع', 'الصف الخامس', 'الصف السادس', 'الصف السابع', 'الصف الثامن', 'الصف التاسع', 'الصف العاشر', 'الصف الحادي عشر', 'الصف الثاني عشر', ]; } function student_enrollment_status_map(): array { return [ 'active' => ['label' => 'مسجل', 'class' => 'status-approved'], 'waiting' => ['label' => 'قائمة انتظار', 'class' => 'status-review'], 'withdrawn' => ['label' => 'منسحب', 'class' => 'status-muted'], ]; } function student_enrollment_status_badge(string $status): string { $map = student_enrollment_status_map(); $meta = $map[$status] ?? ['label' => 'غير محدد', 'class' => 'status-muted']; return '' . e($meta['label']) . ''; } function validate_student_input(array $input): array { $data = student_defaults(); foreach ($data as $key => $_value) { $limit = $key === 'notes' ? 1000 : ($key === 'student_code' ? 60 : 190); $data[$key] = clean_text((string) ($input[$key] ?? ''), $limit); } $errors = []; if ($data['student_code'] === '') { $errors['student_code'] = 'يرجى إدخال الرقم أو الكود المرجعي للطالب.'; } if ($data['full_name'] === '') { $errors['full_name'] = 'يرجى إدخال اسم الطالب أو الطالبة.'; } if (!in_array($data['gender'], student_gender_options(), true)) { $errors['gender'] = 'يرجى تحديد النوع.'; } if (!in_array($data['grade_level'], student_grade_options(), true)) { $errors['grade_level'] = 'يرجى اختيار الصف الدراسي.'; } if ($data['guardian_name'] === '') { $errors['guardian_name'] = 'يرجى إدخال اسم ولي الأمر.'; } if ($data['guardian_phone'] === '' || strlen($data['guardian_phone']) < 7) { $errors['guardian_phone'] = 'يرجى إدخال رقم هاتف صحيح لولي الأمر.'; } $birthDate = clean_text((string) ($input['birth_date'] ?? ''), 20); $data['birth_date'] = $birthDate; if ($birthDate !== '' && strtotime($birthDate) === false) { $errors['birth_date'] = 'يرجى إدخال تاريخ ميلاد صحيح أو تركه فارغاً.'; } $statusMap = student_enrollment_status_map(); if (!array_key_exists($data['enrollment_status'], $statusMap)) { $errors['enrollment_status'] = 'يرجى اختيار حالة تسجيل صحيحة.'; } return [$data, $errors]; } function create_student(int $centerApplicationId, array $data): int { $pdo = db_connection(); $stmt = $pdo->prepare( 'INSERT INTO school_students ( center_application_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, :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(':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(int $centerApplicationId): array { $pdo = db_connection(); $stmt = $pdo->prepare('SELECT * FROM school_students WHERE center_application_id = :center_application_id ORDER BY created_at DESC, id DESC'); $stmt->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetchAll(); } function school_student_metrics(int $centerApplicationId): array { $pdo = db_connection(); $stmt = $pdo->prepare( "SELECT COUNT(*) AS total, COALESCE(SUM(gender = 'طالب'), 0) AS boys, COALESCE(SUM(gender = 'طالبة'), 0) AS girls, 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" ); $stmt->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT); $stmt->execute(); $row = $stmt->fetch() ?: []; return [ 'total' => (int) ($row['total'] ?? 0), 'boys' => (int) ($row['boys'] ?? 0), 'girls' => (int) ($row['girls'] ?? 0), 'active' => (int) ($row['active_count'] ?? 0), 'waiting' => (int) ($row['waiting_count'] ?? 0), 'withdrawn' => (int) ($row['withdrawn_count'] ?? 0), ]; } function teacher_defaults(): array { return [ 'full_name' => '', 'role_title' => '', 'specialization' => '', 'phone' => '', 'email' => '', 'employment_status' => 'active', 'notes' => '', ]; } function teacher_role_options(): array { return [ 'معلم', 'معلمة', 'مشرف أكاديمي', 'مشرفة أكاديمية', 'منسق أنشطة', 'مسؤول متابعة', 'إداري', ]; } function teacher_employment_status_map(): array { return [ 'active' => ['label' => 'مفعل', 'class' => 'status-approved'], 'pending' => ['label' => 'بانتظار التفعيل', 'class' => 'status-review'], 'inactive' => ['label' => 'غير مفعل', 'class' => 'status-muted'], ]; } function teacher_employment_status_badge(string $status): string { $map = teacher_employment_status_map(); $meta = $map[$status] ?? ['label' => 'غير محدد', 'class' => 'status-muted']; return '' . e($meta['label']) . ''; } function validate_teacher_input(array $input): array { $data = teacher_defaults(); foreach ($data as $key => $_value) { $limit = $key === 'notes' ? 1000 : 190; $data[$key] = clean_text((string) ($input[$key] ?? ''), $limit); } $errors = []; if ($data['full_name'] === '') { $errors['full_name'] = 'يرجى إدخال اسم عضو الفريق.'; } if (!in_array($data['role_title'], teacher_role_options(), true)) { $errors['role_title'] = 'يرجى اختيار الدور الوظيفي.'; } if ($data['phone'] !== '' && strlen($data['phone']) < 7) { $errors['phone'] = 'أدخل رقم هاتف صحيحاً أو اتركه فارغاً.'; } if ($data['email'] !== '' && !filter_var($data['email'], FILTER_VALIDATE_EMAIL)) { $errors['email'] = 'أدخل بريداً إلكترونياً صحيحاً أو اتركه فارغاً.'; } $statusMap = teacher_employment_status_map(); if (!array_key_exists($data['employment_status'], $statusMap)) { $errors['employment_status'] = 'يرجى اختيار حالة توظيف صحيحة.'; } return [$data, $errors]; } function create_teacher(int $centerApplicationId, array $data): int { $pdo = db_connection(); $stmt = $pdo->prepare( 'INSERT INTO school_teachers ( center_application_id, full_name, role_title, specialization, phone, email, employment_status, notes, created_at, updated_at ) VALUES ( :center_application_id, :full_name, :role_title, :specialization, :phone, :email, :employment_status, :notes, NOW(), NOW() )' ); $stmt->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT); $stmt->bindValue(':full_name', $data['full_name'], PDO::PARAM_STR); $stmt->bindValue(':role_title', $data['role_title'], PDO::PARAM_STR); $stmt->bindValue(':specialization', $data['specialization'] !== '' ? $data['specialization'] : null, $data['specialization'] !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL); $stmt->bindValue(':phone', $data['phone'] !== '' ? $data['phone'] : null, $data['phone'] !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL); $stmt->bindValue(':email', $data['email'] !== '' ? $data['email'] : null, $data['email'] !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL); $stmt->bindValue(':employment_status', $data['employment_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_teachers(int $centerApplicationId): array { $pdo = db_connection(); $stmt = $pdo->prepare('SELECT * FROM school_teachers WHERE center_application_id = :center_application_id ORDER BY created_at DESC, id DESC'); $stmt->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetchAll(); } function school_teacher_metrics(int $centerApplicationId): 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(email IS NOT NULL AND email <> ''), 0) AS email_ready_count, COALESCE(SUM(role_title LIKE '%مشرف%'), 0) AS supervisors_count FROM school_teachers WHERE center_application_id = :center_application_id" ); $stmt->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT); $stmt->execute(); $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), 'email_ready' => (int) ($row['email_ready_count'] ?? 0), 'supervisors' => (int) ($row['supervisors_count'] ?? 0), ]; } function assessment_defaults(): array { return [ 'title' => '', 'category' => 'اختبار قصير', 'scale_type' => 'percentage', 'max_score' => '100', 'weight_percentage' => '10', 'is_active' => '1', 'notes' => '', ]; } function assessment_category_options(): array { return [ 'تشخيصي', 'واجب', 'مشاركة', 'مشروع', 'أداء', 'اختبار قصير', 'اختبار نهائي', ]; } function assessment_scale_type_map(): array { return [ 'percentage' => ['label' => 'نسبة مئوية', 'class' => 'status-approved'], 'points' => ['label' => 'نقاط', 'class' => 'status-submitted'], 'rubric_4' => ['label' => 'Rubric / 4', 'class' => 'status-review'], 'rubric_5' => ['label' => 'Rubric / 5', 'class' => 'status-review'], ]; } function assessment_scale_type_label(string $scaleType): string { $map = assessment_scale_type_map(); return (string) ($map[$scaleType]['label'] ?? 'غير محدد'); } function assessment_scale_type_badge(string $scaleType): string { $map = assessment_scale_type_map(); $meta = $map[$scaleType] ?? ['label' => 'غير محدد', 'class' => 'status-muted']; return '' . e($meta['label']) . ''; } function assessment_active_badge(int $isActive): string { $meta = $isActive === 1 ? ['label' => 'مفعل', 'class' => 'status-approved'] : ['label' => 'مؤرشف', 'class' => 'status-muted']; return '' . e($meta['label']) . ''; } function validate_assessment_input(array $input): array { $data = assessment_defaults(); $data['title'] = clean_text((string) ($input['title'] ?? ''), 150); $data['category'] = clean_text((string) ($input['category'] ?? ''), 80); $data['scale_type'] = clean_text((string) ($input['scale_type'] ?? ''), 40); $data['notes'] = clean_text((string) ($input['notes'] ?? ''), 1000); $data['is_active'] = ((string) ($input['is_active'] ?? '1')) === '1' ? '1' : '0'; $errors = []; if ($data['title'] === '') { $errors['title'] = 'يرجى إدخال اسم نوع التقييم.'; } if (!in_array($data['category'], assessment_category_options(), true)) { $errors['category'] = 'يرجى اختيار فئة تقييم صحيحة.'; } $scaleMap = assessment_scale_type_map(); if (!array_key_exists($data['scale_type'], $scaleMap)) { $errors['scale_type'] = 'يرجى اختيار مقياس تقييم صحيح.'; } $maxScoreRaw = str_replace(',', '.', (string) ($input['max_score'] ?? '')); if ($maxScoreRaw === '' || !is_numeric($maxScoreRaw)) { $errors['max_score'] = 'أدخل الدرجة النهائية بصيغة رقمية.'; } else { $maxScore = round((float) $maxScoreRaw, 2); if ($maxScore <= 0 || $maxScore > 1000) { $errors['max_score'] = 'يجب أن تكون الدرجة النهائية بين 0.01 و1000.'; } else { $data['max_score'] = number_format($maxScore, 2, '.', ''); } } $weightRaw = str_replace(',', '.', (string) ($input['weight_percentage'] ?? '')); if ($weightRaw === '' || !is_numeric($weightRaw)) { $errors['weight_percentage'] = 'أدخل وزن التقييم بصيغة رقمية.'; } else { $weight = round((float) $weightRaw, 2); if ($weight < 0 || $weight > 100) { $errors['weight_percentage'] = 'يجب أن يكون الوزن بين 0 و100٪.'; } else { $data['weight_percentage'] = number_format($weight, 2, '.', ''); } } return [$data, $errors]; } function create_assessment_type(int $centerApplicationId, array $data): int { $pdo = db_connection(); $stmt = $pdo->prepare( 'INSERT INTO school_assessment_types ( center_application_id, title, category, scale_type, max_score, weight_percentage, is_active, notes, created_at, updated_at ) VALUES ( :center_application_id, :title, :category, :scale_type, :max_score, :weight_percentage, :is_active, :notes, NOW(), NOW() )' ); $stmt->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT); $stmt->bindValue(':title', $data['title'], PDO::PARAM_STR); $stmt->bindValue(':category', $data['category'], PDO::PARAM_STR); $stmt->bindValue(':scale_type', $data['scale_type'], PDO::PARAM_STR); $stmt->bindValue(':max_score', (float) $data['max_score']); $stmt->bindValue(':weight_percentage', (float) $data['weight_percentage']); $stmt->bindValue(':is_active', (int) $data['is_active'], PDO::PARAM_INT); $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_assessments(int $centerApplicationId): array { $pdo = db_connection(); $stmt = $pdo->prepare( 'SELECT * FROM school_assessment_types WHERE center_application_id = :center_application_id ORDER BY is_active DESC, weight_percentage DESC, created_at DESC, id DESC' ); $stmt->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetchAll(); } function school_assessment_metrics(int $centerApplicationId): 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 IN ('rubric_4', 'rubric_5')), 0) AS rubric_count FROM school_assessment_types WHERE center_application_id = :center_application_id" ); $stmt->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT); $stmt->execute(); $row = $stmt->fetch() ?: []; return [ 'total' => (int) ($row['total'] ?? 0), 'active' => (int) ($row['active_count'] ?? 0), 'inactive' => (int) ($row['inactive_count'] ?? 0), 'total_weight' => round((float) ($row['total_weight'] ?? 0), 2), 'active_weight' => round((float) ($row['active_weight'] ?? 0), 2), 'average_max_score' => round((float) ($row['average_max_score'] ?? 0), 2), 'percentage' => (int) ($row['percentage_count'] ?? 0), 'points' => (int) ($row['points_count'] ?? 0), 'rubric' => (int) ($row['rubric_count'] ?? 0), ]; } function attendance_defaults(): array { return [ 'student_id' => '', 'attendance_date' => date('Y-m-d'), 'attendance_status' => 'absent', 'absence_reason' => '', 'notes' => '', ]; } function attendance_status_map(): array { return [ 'absent' => ['label' => 'غياب', 'class' => 'status-rejected'], 'excused' => ['label' => 'غياب بعذر', 'class' => 'status-review'], 'late' => ['label' => 'تأخر', 'class' => 'status-submitted'], ]; } function attendance_status_badge(string $status): string { $map = attendance_status_map(); $meta = $map[$status] ?? ['label' => 'غير محدد', 'class' => 'status-muted']; return '' . e($meta['label']) . ''; } function school_student_options(int $centerApplicationId): array { $students = list_school_students($centerApplicationId); $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 validate_attendance_input(int $centerApplicationId, 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($centerApplicationId); 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(int $centerApplicationId, array $data): int { $pdo = db_connection(); $stmt = $pdo->prepare( 'INSERT INTO school_attendance_records ( center_application_id, student_id, attendance_date, attendance_status, absence_reason, notes, created_at, updated_at ) VALUES ( :center_application_id, :student_id, :attendance_date, :attendance_status, :absence_reason, :notes, NOW(), NOW() )' ); $stmt->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT); $stmt->bindValue(':student_id', (int) $data['student_id'], PDO::PARAM_INT); $stmt->bindValue(':attendance_date', $data['attendance_date'], PDO::PARAM_STR); $stmt->bindValue(':attendance_status', $data['attendance_status'], PDO::PARAM_STR); $stmt->bindValue(':absence_reason', $data['absence_reason'] !== '' ? $data['absence_reason'] : null, $data['absence_reason'] !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL); $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_attendance_records(int $centerApplicationId): array { $pdo = db_connection(); $stmt = $pdo->prepare( '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 ORDER BY ar.attendance_date DESC, ar.created_at DESC, ar.id DESC' ); $stmt->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetchAll(); } function school_attendance_metrics(int $centerApplicationId): 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" ); $stmt->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT); $stmt->execute(); $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 render_page_start(string $pageTitle, string $active = 'home', string $pageDescription = ''): void { $projectName = project_name(); $description = $pageDescription !== '' ? $pageDescription : project_description(); $projectImageUrl = env_value('PROJECT_IMAGE_URL'); ?> <?= e($pageTitle) ?> | <?= e($projectName) ?>
'text-bg-success', 'error' => 'text-bg-danger', default => 'text-bg-dark', }; ?>
prepare('UPDATE center_applications SET subjects = :subjects, updated_at = NOW() WHERE id = :id'); $stmt->bindValue(':subjects', json_encode(array_values($subjects)), PDO::PARAM_STR); $stmt->bindValue(':id', $id, PDO::PARAM_INT); $stmt->execute(); } function get_enabled_subjects(): array { try { $pdo = db_connection(); $stmt = $pdo->query("SELECT id, name, description FROM subjects WHERE status = 'enabled' ORDER BY name ASC"); $result = $stmt->fetchAll(PDO::FETCH_ASSOC); return $result !== false ? $result : []; } catch (Throwable $e) { return []; } }