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_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['season'] = clean_text((string) ($input['season'] ?? $defaults['season']), 20); $data['year'] = clean_text((string) ($input['year'] ?? $defaults['year']), 4); $data['start_date'] = clean_text((string) ($input['start_date'] ?? $defaults['start_date']), 20); $data['end_date'] = clean_text((string) ($input['end_date'] ?? $defaults['end_date']), 20); $data['status'] = clean_text((string) ($input['status'] ?? 'active'), 20); $errors = []; if (!array_key_exists($data['season'], school_cycle_season_options())) { $errors['season'] = 'يرجى اختيار موسم صحيح.'; } $year = (int) $data['year']; if ((string) $year !== $data['year'] || $year < 2020 || $year > 2100) { $errors['year'] = 'يرجى إدخال سنة صحيحة مثل 2026.'; } if ($data['start_date'] === '' || strtotime($data['start_date']) === false) { $errors['start_date'] = 'يرجى إدخال تاريخ بداية صحيح.'; } if ($data['end_date'] === '' || strtotime($data['end_date']) === false) { $errors['end_date'] = 'يرجى إدخال تاريخ نهاية صحيح.'; } if (!isset($errors['start_date'], $errors['end_date']) && strtotime($data['end_date']) < strtotime($data['start_date'])) { $errors['end_date'] = 'تاريخ النهاية يجب أن يكون بعد البداية أو مساوياً لها.'; } 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 = detect_school_cycle_season((string) ($application['start_date'] ?? '')); $year = (int) date('Y', strtotime((string) ($application['start_date'] ?? 'now')) ?: time()); $startDate = (string) ($application['start_date'] ?? date('Y-m-d')); $endDate = (string) ($application['end_date'] ?? $startDate); $cycleName = format_school_cycle_name($season, $year); $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 ) VALUES ( :center_application_id, :season, :year, :cycle_name, :start_date, :end_date, :status, NULL, NOW(), NOW() )' ); $insert->execute([ ':center_application_id' => $applicationId, ':season' => $season, ':year' => $year, ':cycle_name' => $cycleName, ':start_date' => $startDate, ':end_date' => $endDate, ':status' => $status, ]); return (int) $pdo->lastInsertId(); } 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, phone, email, employment_status, notes, created_at, updated_at ) SELECT center_application_id, :target_cycle_id, full_name, role_title, specialization, 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'])) { $stmt = $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 ) SELECT center_application_id, :target_cycle_id, title, category, scale_type, max_score, weight_percentage, is_active, notes, NOW(), NOW() FROM school_assessment_types 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['assessments'] = $stmt->rowCount(); } 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']; $year = (int) $data['year']; $cycleName = format_school_cycle_name($season, $year); $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 ) VALUES ( :center_application_id, :season, :year, :cycle_name, :start_date, :end_date, :status, :archived_at, NOW(), NOW() )' ); $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, ]); $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 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): array { $pdo = db_connection(); $stmt = $pdo->prepare( 'SELECT * FROM school_students WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id ORDER BY created_at DESC, id DESC' ); $stmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ]); return $stmt->fetchAll(); } 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, 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() )' ); $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, ':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 { $pdo = db_connection(); $stmt = $pdo->prepare( 'SELECT * FROM school_teachers WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id ORDER BY created_at DESC, id DESC' ); $stmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ]); return $stmt->fetchAll(); } 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, 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_school_assessments_by_cycle(int $centerApplicationId, int $cycleId): array { $pdo = db_connection(); $stmt = $pdo->prepare( 'SELECT * FROM school_assessment_types WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id ORDER BY is_active DESC, created_at DESC, id DESC' ); $stmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ]); return $stmt->fetchAll(); } 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 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 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): 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 AND ar.cycle_id = :cycle_id ORDER BY ar.attendance_date DESC, ar.created_at DESC, ar.id DESC' ); $stmt->execute([ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, ]); return $stmt->fetchAll(); } 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), ]; }