diff --git a/application_detail.php b/application_detail.php index 68ac527..432c039 100644 --- a/application_detail.php +++ b/application_detail.php @@ -127,7 +127,7 @@ render_flash($flash);
رقم الهاتف
البريد الإلكتروني
السعة المتوقعة طالب
-
فترة البرنامج
+
الدورة / الفترة ()
diff --git a/applications.php b/applications.php index 25ed7d9..ea2d07a 100644 --- a/applications.php +++ b/applications.php @@ -20,15 +20,27 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Read list $search = clean_text($_GET['search'] ?? '', 255); +$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT) ?: 1; +$limit = 10; +$offset = ($page - 1) * $limit; + $query = 'SELECT * FROM center_applications'; +$countQuery = 'SELECT COUNT(*) FROM center_applications'; $params = []; if ($search !== '') { - $query .= ' WHERE center_name LIKE ? OR city LIKE ? OR director_name LIKE ?'; + $where = ' WHERE center_name LIKE ? OR city LIKE ? OR director_name LIKE ?'; + $query .= $where; + $countQuery .= $where; $params[] = "%$search%"; $params[] = "%$search%"; $params[] = "%$search%"; } -$query .= ' ORDER BY id DESC'; + +$stmtCount = db()->prepare($countQuery); +$stmtCount->execute($params); +$totalItems = (int)$stmtCount->fetchColumn(); + +$query .= ' ORDER BY id DESC LIMIT ' . $limit . ' OFFSET ' . $offset; $stmt = db()->prepare($query); $stmt->execute($params); $applications = $stmt->fetchAll(); @@ -56,15 +68,7 @@ render_flash($flash); -
-
- - - - إلغاء - -
-
+
@@ -139,6 +143,7 @@ render_flash($flash);
+ diff --git a/approved_school.php b/approved_school.php index 60f592a..8894402 100644 --- a/approved_school.php +++ b/approved_school.php @@ -65,7 +65,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $isApproved) { $cycleCreation = create_school_cycle((int) $application['id'], $cycleValues, $cycleRollover); $newCycleId = (int) ($cycleCreation['cycle_id'] ?? 0); $rolloverSummary = (array) ($cycleCreation['rollover'] ?? []); - $flashMessage = 'تم إنشاء الدورة الموسمية الجديدة بنجاح. يمكنك الآن العمل داخل ' . format_school_cycle_name($cycleValues['season'], (int) $cycleValues['year']) . '.'; + $flashMessage = 'تم إنشاء الدورة الموسمية الجديدة بنجاح. يمكنك الآن العمل داخل ' . e((string)($cycleValues['cycle_name'] ?? 'الدورة الجديدة')) . '.'; $rolloverParts = []; if (!empty($rolloverSummary['teachers'])) { $rolloverParts[] = 'ترحيل ' . (int) $rolloverSummary['teachers'] . ' من أعضاء الفريق'; @@ -134,6 +134,7 @@ $teachersUrl = school_page_url('teachers.php', (int) $application['id'], $select $assessmentsUrl = school_page_url('assessments.php', (int) $application['id'], $selectedCycleId); $attendanceUrl = school_page_url('attendance.php', (int) $application['id'], $selectedCycleId); $approvedSchoolUrl = school_page_url('approved_school.php', (int) $application['id'], $selectedCycleId); +$centerSubjectsUrl = school_page_url('center_subjects.php', (int) $application['id'], $selectedCycleId); $studentCycleMetrics = $isApproved && $selectedCycleId > 0 ? school_student_metrics_by_cycle((int) $application['id'], $selectedCycleId) : ['total' => 0, 'active' => 0]; $teacherCycleMetrics = $isApproved && $selectedCycleId > 0 ? school_teacher_metrics_by_cycle((int) $application['id'], $selectedCycleId) : ['total' => 0, 'active' => 0]; $assessmentCycleMetrics = $isApproved && $selectedCycleId > 0 ? school_assessment_metrics_by_cycle((int) $application['id'], $selectedCycleId) : ['total' => 0, 'active' => 0, 'active_weight' => 0]; @@ -149,6 +150,11 @@ render_flash($flash); ?>
+
0 ? list_school_assessments_by_cycle((int) $application['id'], $selectedCycleId) : []; +$search = clean_text($_GET['search'] ?? '', 255); +$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT) ?: 1; +$limit = 10; +$offset = ($page - 1) * $limit; + +$assessments = $isApprovedSchool && $selectedCycleId > 0 ? list_school_assessments_by_cycle((int) $application['id'], $selectedCycleId, $search, $limit, $offset) : []; +$totalAssessments = $isApprovedSchool && $selectedCycleId > 0 ? count_school_assessments_by_cycle((int) $application['id'], $selectedCycleId, $search) : 0; + $metrics = $isApprovedSchool && $selectedCycleId > 0 ? school_assessment_metrics_by_cycle((int) $application['id'], $selectedCycleId) : [ 'total' => 0, 'active' => 0, @@ -96,7 +103,7 @@ render_flash($flash);
- +
@@ -307,6 +314,7 @@ render_flash($flash);
نسبي / Rubric
+
إجمالي الأنواع نوع
@@ -351,6 +359,7 @@ render_flash($flash);
+
diff --git a/attendance.php b/attendance.php index 1f31d06..770aa08 100644 --- a/attendance.php +++ b/attendance.php @@ -54,7 +54,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $application) { $students = $isApprovedSchool && $selectedCycleId > 0 ? list_school_students_by_cycle((int) $application['id'], $selectedCycleId) : []; $studentOptions = $isApprovedSchool && $selectedCycleId > 0 ? school_student_options_by_cycle((int) $application['id'], $selectedCycleId) : []; -$records = $isApprovedSchool && $selectedCycleId > 0 ? list_school_attendance_records_by_cycle((int) $application['id'], $selectedCycleId) : []; +$search = clean_text($_GET['search'] ?? '', 255); +$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT) ?: 1; +$limit = 10; +$offset = ($page - 1) * $limit; + +$records = $isApprovedSchool && $selectedCycleId > 0 ? list_school_attendance_records_by_cycle((int) $application['id'], $selectedCycleId, $search, $limit, $offset) : []; +$totalRecords = $isApprovedSchool && $selectedCycleId > 0 ? count_school_attendance_records_by_cycle((int) $application['id'], $selectedCycleId, $search) : 0; + $metrics = $isApprovedSchool && $selectedCycleId > 0 ? school_attendance_metrics_by_cycle((int) $application['id'], $selectedCycleId) : [ 'total' => 0, 'absent' => 0, @@ -114,7 +121,7 @@ render_flash($flash);
- +
@@ -309,6 +316,7 @@ render_flash($flash);
سجل / طلاب
+
الغياب حالة
@@ -353,6 +361,7 @@ render_flash($flash);
+
diff --git a/center_application.php b/center_application.php index 2b32e20..eb09a46 100644 --- a/center_application.php +++ b/center_application.php @@ -141,15 +141,17 @@ render_flash($flash);
-
- - -
-
-
- - -
+
+ + +
diff --git a/center_profile.php b/center_profile.php index 3d224e9..8bb1ab7 100644 --- a/center_profile.php +++ b/center_profile.php @@ -15,7 +15,7 @@ if (!$application) {
- +
@@ -111,7 +111,7 @@ render_flash($flash);
- +
diff --git a/center_subjects.php b/center_subjects.php new file mode 100644 index 0000000..98f36a7 --- /dev/null +++ b/center_subjects.php @@ -0,0 +1,124 @@ + 0 ? get_application($applicationId) : null; +$isApproved = $application && (string) $application['status'] === 'approved'; + +$selectedCycleId = 0; +if ($isApproved) { + $cycleContext = resolve_school_cycle_context((int) $application['id'], $application, $requestedCycleId); + $selectedCycle = $cycleContext['selected']; + $selectedCycleId = $selectedCycle ? (int) ($selectedCycle['id'] ?? 0) : 0; +} + +if (!$application || !$isApproved) { + http_response_code(404); + render_page_start('المركز غير موجود', 'profile'); + render_flash($flash); + ?> +
+
+
+
+ +
+
+
+
المركز غير موجود
+ العودة +
+
+
+
+
+ 0 ? '&cycle=' . $selectedCycleId : ''; + header('Location: center_subjects.php?id=' . $application['id'] . $selectedCycleIdStr); + exit; + } catch (Throwable $e) { + $errors['form'] = 'تعذر حفظ البيانات. يرجى المحاولة لاحقاً.'; + } +} + +render_page_start('إدارة المواد الدراسية: ' . (string) $application['center_name'], 'profile', 'تحديد المواد الدراسية التي يتم تدريسها في المركز.'); +render_flash($flash); +?> +
+
+
+
+ +
+
+
+

المواد الدراسية للمركز

+

تحديد وتحديث المواد الدراسية التي يتم تقديمها في هذا المركز. سيتم توفير هذه المواد لاختيارها في التقييمات والجداول.

+
+ +
+ + +
+ +
+ + +
لا توجد مواد دراسية مفعلة في النظام حالياً. يرجى إضافة مواد من الإدارة المركزية أولاً.
+ +
+ +
+
+ > + +
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+
+
+ 0, + "SELECT 1", + CONCAT("ALTER TABLE ", @tablename, " ADD ", @columnname, " INT UNSIGNED NULL AFTER center_application_id;") +)); +PREPARE alterIfNotExists FROM @preparedStatement; +EXECUTE alterIfNotExists; +DEALLOCATE PREPARE alterIfNotExists; + +-- Safely add foreign key +SET @fkname = 'fk_school_cycles_global_cycle'; +SET @preparedStatement = (SELECT IF( + ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS + WHERE + (table_name = @tablename) + AND (table_schema = @dbname) + AND (constraint_name = @fkname) + ) > 0, + "SELECT 1", + CONCAT("ALTER TABLE ", @tablename, " ADD CONSTRAINT ", @fkname, " FOREIGN KEY (", @columnname, ") REFERENCES global_cycles(id) ON DELETE SET NULL;") +)); +PREPARE alterIfNotExists FROM @preparedStatement; +EXECUTE alterIfNotExists; +DEALLOCATE PREPARE alterIfNotExists; + +-- We can make season/year nullable or give defaults, since we will use global cycles instead. +-- We'll just alter them to allow NULL +ALTER TABLE school_cycles MODIFY season VARCHAR(20) NULL; +ALTER TABLE school_cycles MODIFY year SMALLINT UNSIGNED NULL; diff --git a/db/migrations/20260416_global_cycles.sql b/db/migrations/20260416_global_cycles.sql new file mode 100644 index 0000000..ae036ae --- /dev/null +++ b/db/migrations/20260416_global_cycles.sql @@ -0,0 +1,45 @@ +CREATE TABLE IF NOT EXISTS global_cycles ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + cycle_name VARCHAR(150) NOT NULL, + start_date DATE NOT NULL, + end_date DATE NOT NULL, + is_active TINYINT(1) NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- We need to safely add the column. +SET @dbname = DATABASE(); +SET @tablename = 'center_applications'; +SET @columnname = 'global_cycle_id'; +SET @preparedStatement = (SELECT IF( + ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE + (table_name = @tablename) + AND (table_schema = @dbname) + AND (column_name = @columnname) + ) > 0, + "SELECT 1", + CONCAT("ALTER TABLE ", @tablename, " ADD ", @columnname, " INT UNSIGNED NULL AFTER expected_students;") +)); +PREPARE alterIfNotExists FROM @preparedStatement; +EXECUTE alterIfNotExists; +DEALLOCATE PREPARE alterIfNotExists; + +-- Safely add foreign key +SET @fkname = 'fk_center_applications_global_cycle'; +SET @preparedStatement = (SELECT IF( + ( + SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS + WHERE + (table_name = @tablename) + AND (table_schema = @dbname) + AND (constraint_name = @fkname) + ) > 0, + "SELECT 1", + CONCAT("ALTER TABLE ", @tablename, " ADD CONSTRAINT ", @fkname, " FOREIGN KEY (", @columnname, ") REFERENCES global_cycles(id) ON DELETE SET NULL;") +)); +PREPARE alterIfNotExists FROM @preparedStatement; +EXECUTE alterIfNotExists; +DEALLOCATE PREPARE alterIfNotExists; diff --git a/fix_app.php b/fix_app.php new file mode 100644 index 0000000..55f20e7 --- /dev/null +++ b/fix_app.php @@ -0,0 +1,49 @@ +exec(\" + CREATE TABLE IF NOT EXISTS app_settings ( + id INT PRIMARY KEY DEFAULT 1, + app_name VARCHAR(190) NOT NULL DEFAULT 'Central Admin', + app_email VARCHAR(190) DEFAULT NULL, + app_telephone VARCHAR(60) DEFAULT NULL, + app_logo VARCHAR(255) DEFAULT NULL, + app_favicon VARCHAR(255) DEFAULT NULL, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + \"); + \$pdo->exec(\"INSERT IGNORE INTO app_settings (id, app_name) VALUES (1, 'Central Admin')\"); +} + +function get_app_settings(): array +{ + \$pdo = db_connection(); + \$stmt = \$pdo->query('SELECT * FROM app_settings WHERE id = 1'); + \$res = \$stmt->fetch(PDO::FETCH_ASSOC); + if (!\$res) { + return [ + 'app_name' => 'Central Admin', + 'app_email' => '', + 'app_telephone' => '', + 'app_logo' => '', + 'app_favicon' => '' + ]; + } + return \$res; +} + +function ensure_center_application_schema(PDO \$pdo): void"; + +$content = str_replace($search2, $replace2, $content); + +file_put_contents($file, $content); +echo "Done\n"; + diff --git a/fix_app_settings.py b/fix_app_settings.py new file mode 100644 index 0000000..0101dee --- /dev/null +++ b/fix_app_settings.py @@ -0,0 +1,52 @@ +import re + +with open('includes/app.php', 'r') as f: + content = f.read() + +# Add ensure_app_settings_schema call +content = content.replace( + 'ensure_center_application_schema($pdo);', + 'ensure_app_settings_schema($pdo); + ensure_center_application_schema($pdo);' +) + +# Add function definitions before ensure_center_application_schema definition +new_funcs = """function ensure_app_settings_schema(PDO $pdo): void +{ + $pdo->exec(" + CREATE TABLE IF NOT EXISTS app_settings ( + id INT PRIMARY KEY DEFAULT 1, + app_name VARCHAR(190) NOT NULL DEFAULT 'Central Admin', + app_email VARCHAR(190) DEFAULT NULL, + app_telephone VARCHAR(60) DEFAULT NULL, + app_logo VARCHAR(255) DEFAULT NULL, + app_favicon VARCHAR(255) DEFAULT NULL, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + "); + $pdo->exec("INSERT IGNORE INTO app_settings (id, app_name) VALUES (1, 'Central Admin')"); +} + +function get_app_settings(): array +{ + $pdo = db_connection(); + $stmt = $pdo->query('SELECT * FROM app_settings WHERE id = 1'); + $res = $stmt->fetch(PDO::FETCH_ASSOC); + if (!$res) { + return [ + 'app_name' => 'Central Admin', + 'app_email' => '', + 'app_telephone' => '', + 'app_logo' => '', + 'app_favicon' => '' + ]; + } + return $res; +} + +function ensure_center_application_schema(PDO $pdo): void""" + +content = content.replace('function ensure_center_application_schema(PDO $pdo): void', new_funcs) + +with open('includes/app.php', 'w') as f: + f.write(content) \ No newline at end of file diff --git a/global_cycles.php b/global_cycles.php new file mode 100644 index 0000000..a4c1edc --- /dev/null +++ b/global_cycles.php @@ -0,0 +1,297 @@ +prepare('INSERT INTO global_cycles (cycle_name, start_date, end_date) VALUES (?, ?, ?)'); + $stmt->execute([$cycleName, $startDate, $endDate]); + set_flash('success', 'تم إضافة الدورة بنجاح.'); + header('Location: global_cycles.php'); + exit; + } catch (Throwable $e) { + $errors['form'] = 'حدث خطأ أثناء حفظ الدورة.'; + } + } + } elseif ($action === 'toggle') { + $cycleId = filter_input(INPUT_POST, 'cycle_id', FILTER_VALIDATE_INT); + if ($cycleId) { + try { + $stmt = db()->prepare('UPDATE global_cycles SET is_active = NOT is_active WHERE id = ?'); + $stmt->execute([$cycleId]); + set_flash('success', 'تم تغيير حالة الدورة.'); + } catch (Throwable $e) { + set_flash('error', 'حدث خطأ.'); + } + } + header('Location: global_cycles.php'); + exit; + } elseif ($action === 'delete') { + $cycleId = filter_input(INPUT_POST, 'cycle_id', FILTER_VALIDATE_INT); + if ($cycleId) { + try { + $stmt = db()->prepare('DELETE FROM global_cycles WHERE id = ?'); + $stmt->execute([$cycleId]); + set_flash('success', 'تم حذف الدورة بنجاح.'); + } catch (Throwable $e) { + set_flash('error', 'لا يمكن حذف الدورة، ربما تكون مرتبطة بطلبات أخرى.'); + } + } + header('Location: global_cycles.php'); + exit; + } elseif ($action === 'edit') { + $cycleId = filter_input(INPUT_POST, 'cycle_id', FILTER_VALIDATE_INT); + $cycleName = clean_text($_POST['cycle_name'] ?? ''); + $startDate = $_POST['start_date'] ?? ''; + $endDate = $_POST['end_date'] ?? ''; + + if ($cycleId && $cycleName && $startDate && $endDate) { + try { + $stmt = db()->prepare('UPDATE global_cycles SET cycle_name = ?, start_date = ?, end_date = ? WHERE id = ?'); + $stmt->execute([$cycleName, $startDate, $endDate, $cycleId]); + set_flash('success', 'تم تعديل الدورة بنجاح.'); + } catch (Throwable $e) { + set_flash('error', 'حدث خطأ أثناء التعديل.'); + } + } + header('Location: global_cycles.php'); + exit; + } +} + +// Fetch cycles +$search = clean_text($_GET['search'] ?? '', 255); +$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT) ?: 1; +$limit = 10; +$offset = ($page - 1) * $limit; + +$cycles = []; +$totalItems = 0; + +try { + $query = 'SELECT * FROM global_cycles'; + $countQuery = 'SELECT COUNT(*) FROM global_cycles'; + $params = []; + + if ($search !== '') { + $where = ' WHERE cycle_name LIKE ?'; + $query .= $where; + $countQuery .= $where; + $params[] = "%$search%"; + } + + $stmtCount = db()->prepare($countQuery); + $stmtCount->execute($params); + $totalItems = (int)$stmtCount->fetchColumn(); + + $query .= ' ORDER BY start_date DESC LIMIT ' . $limit . ' OFFSET ' . $offset; + $stmt = db()->prepare($query); + $stmt->execute($params); + $cycles = $stmt->fetchAll(PDO::FETCH_ASSOC); +} catch (Throwable $e) {} + +render_page_start('إدارة الدورات الموسمية', 'cycles', 'إضافة وإدارة الدورات التي تتقدم لها المراكز.'); +render_flash($flash); +?> +
+
+
+
+ +
+
+
+
+
+

الدورات الموسمية

+

يمكنك هنا تعريف الدورات الجديدة ليتاح للمراكز التقديم عليها.

+
+
+ +
+
+
+ + +
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
اسم الدورةالفترة الزمنيةالحالةالإجراءات
لا توجد دورات مسجلة حالياً.
+ من: + إلى: + + + متاحة للتقديم + + مغلقة + + +
+ +
+ + + +
+
+ + + +
+
+
+
+ +
+
+
+
+ + + + + + + + + + diff --git a/includes/app.php b/includes/app.php index a57e7f0..5d97d29 100644 --- a/includes/app.php +++ b/includes/app.php @@ -96,6 +96,7 @@ function db_connection(): PDO } if (!$bootstrapped) { + ensure_app_settings_schema($pdo); ensure_center_application_schema($pdo); seed_center_application_demo_data($pdo); ensure_school_module_schema($pdo); @@ -107,6 +108,39 @@ function db_connection(): PDO return $pdo; } +function ensure_app_settings_schema(PDO $pdo): void +{ + $pdo->exec(" + CREATE TABLE IF NOT EXISTS app_settings ( + id INT PRIMARY KEY DEFAULT 1, + app_name VARCHAR(190) NOT NULL DEFAULT 'Central Admin', + app_email VARCHAR(190) DEFAULT NULL, + app_telephone VARCHAR(60) DEFAULT NULL, + app_logo VARCHAR(255) DEFAULT NULL, + app_favicon VARCHAR(255) DEFAULT NULL, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + "); + $pdo->exec("INSERT IGNORE INTO app_settings (id, app_name) VALUES (1, 'Central Admin')"); +} + +function get_app_settings(): array +{ + $pdo = db_connection(); + $stmt = $pdo->query('SELECT * FROM app_settings WHERE id = 1'); + $res = $stmt->fetch(PDO::FETCH_ASSOC); + if (!$res) { + return [ + 'app_name' => 'Central Admin', + 'app_email' => '', + 'app_telephone' => '', + 'app_logo' => '', + 'app_favicon' => '' + ]; + } + return $res; +} + function ensure_center_application_schema(PDO $pdo): void { static $done = false; @@ -406,7 +440,7 @@ function application_defaults(): array 'phone' => '', 'email' => '', 'expected_students' => '', - 'start_date' => '', + 'start_date' => '', 'global_cycle_id' => '', 'end_date' => '', 'notes' => '', 'subjects' => [], @@ -1294,6 +1328,75 @@ function render_page_start(string $pageTitle, string $active = 'home', string $p '; + echo '
    '; + + // Prev + $prevClass = $page <= 1 ? 'disabled' : ''; + $prevUrl = '?'; + $params = $queryParams; + $params['page'] = $page - 1; + $prevUrl .= http_build_query($params); + + echo '
  • '; + echo 'السابق'; + echo '
  • '; + + // Pages + for ($i = 1; $i <= $totalPages; $i++) { + $activeClass = $i === $page ? 'active' : ''; + $params['page'] = $i; + $url = '?' . http_build_query($params); + echo '
  • '; + echo '' . $i . ''; + echo '
  • '; + } + + // Next + $nextClass = $page >= $totalPages ? 'disabled' : ''; + $params['page'] = $page + 1; + $nextUrl = '?' . http_build_query($params); + + echo '
  • '; + echo 'التالي'; + echo '
  • '; + + echo '
'; + echo ''; +} + +function render_search_bar(string $currentSearch, string $placeholder = "بحث...", string $action = "", array $hiddenParams = []): void { + echo '
'; + foreach ($hiddenParams as $k => $v) { + if ($k === 'search' || $k === 'page') continue; + echo ''; + } + echo '
'; + echo ''; + echo ''; + if ($currentSearch !== '') { + $clearParams = $hiddenParams; + unset($clearParams['search']); + unset($clearParams['page']); + $clearUrl = $action; + if (!empty($clearParams)) { + $clearUrl .= '?' . http_build_query($clearParams); + } + echo 'إلغاء'; + } + echo '
'; + echo '
'; +} + function render_flash(?array $flash): void { if (!$flash || empty($flash['message'])) { @@ -1368,3 +1471,14 @@ function get_enabled_subjects(): array return []; } } + + +function get_active_global_cycles(): array { + try { + $stmt = db()->query('SELECT * FROM global_cycles WHERE is_active = 1 ORDER BY start_date DESC'); + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } catch (Throwable $e) { + return []; + } +} + diff --git a/includes/center_sidebar.php b/includes/center_sidebar.php new file mode 100644 index 0000000..dc29627 --- /dev/null +++ b/includes/center_sidebar.php @@ -0,0 +1,73 @@ + 0, 'center_name' => 'المركز غير متوفر']; +} + +// Same for selected cycle +$selectedCycleIdStr = isset($selectedCycleId) && $selectedCycleId > 0 ? '&cycle=' . $selectedCycleId : ''; +$baseQuery = '?id=' . e((string) $application['id']) . $selectedCycleIdStr; + +?> + diff --git a/includes/cycles.php b/includes/cycles.php index b93480f..34a84d2 100644 --- a/includes/cycles.php +++ b/includes/cycles.php @@ -203,30 +203,26 @@ function validate_school_cycle_input(array $input, ?array $application = null): { $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); + $data['global_cycle_id'] = filter_var($input['global_cycle_id'] ?? null, FILTER_VALIDATE_INT) ?: null; $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 (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)) { @@ -260,20 +256,32 @@ function ensure_default_school_cycle_record(PDO $pdo, array $application): int return (int) $existing['id']; } - $season = detect_school_cycle_season((string) ($application['start_date'] ?? '')); - $year = (int) date('Y', strtotime((string) ($application['start_date'] ?? 'now')) ?: time()); + $season = null; + $year = null; $startDate = (string) ($application['start_date'] ?? date('Y-m-d')); $endDate = (string) ($application['end_date'] ?? $startDate); - $cycleName = format_school_cycle_name($season, $year); + $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 + 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() + :status, NULL, NOW(), NOW(), :global_cycle_id )' ); $insert->execute([ @@ -284,6 +292,7 @@ function ensure_default_school_cycle_record(PDO $pdo, array $application): int ':start_date' => $startDate, ':end_date' => $endDate, ':status' => $status, + ':global_cycle_id' => $globalCycleId ]); return (int) $pdo->lastInsertId(); @@ -420,9 +429,10 @@ function copy_school_cycle_rollover(PDO $pdo, int $centerApplicationId, int $sou 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); + $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); @@ -455,10 +465,10 @@ function create_school_cycle(int $centerApplicationId, array $data, array $rollo $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 + 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() + :status, :archived_at, NOW(), NOW(), :global_cycle_id )' ); $insert->execute([ @@ -470,6 +480,7 @@ function create_school_cycle(int $centerApplicationId, array $data, array $rollo ':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(); @@ -581,21 +592,54 @@ function create_student_in_cycle(int $centerApplicationId, int $cycleId, array $ return (int) $pdo->lastInsertId(); } -function list_school_students_by_cycle(int $centerApplicationId, int $cycleId): array +function list_school_students_by_cycle(int $centerApplicationId, int $cycleId, string $search = '', int $limit = 0, int $offset = 0): 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([ + $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%"; + } + + $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 = ''): 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%"; + } + + $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(); @@ -654,21 +698,54 @@ function create_teacher_in_cycle(int $centerApplicationId, int $cycleId, array $ return (int) $pdo->lastInsertId(); } -function list_school_teachers_by_cycle(int $centerApplicationId, int $cycleId): array +function list_school_teachers_by_cycle(int $centerApplicationId, int $cycleId, string $search = '', int $limit = 0, int $offset = 0): 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([ + $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, - ]); + ]; + + 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%"; + } + + $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, string $search = ''): 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, + ]; + + 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%"; + } + + $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(); @@ -729,21 +806,52 @@ function create_assessment_type_in_cycle(int $centerApplicationId, int $cycleId, return (int) $pdo->lastInsertId(); } -function list_school_assessments_by_cycle(int $centerApplicationId, int $cycleId): array +function list_school_assessments_by_cycle(int $centerApplicationId, int $cycleId, string $search = '', int $limit = 0, int $offset = 0): 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([ + $query = 'SELECT * FROM school_assessment_types WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id'; + $params = [ ':center_application_id' => $centerApplicationId, ':cycle_id' => $cycleId, - ]); + ]; + + if ($search !== '') { + $query .= ' AND (assessment_name LIKE :search1 OR assessment_category LIKE :search2)'; + $params[':search1'] = "%$search%"; + $params[':search2'] = "%$search%"; + } + + $query .= ' ORDER BY is_active DESC, 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_assessments_by_cycle(int $centerApplicationId, int $cycleId, string $search = ''): 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, + ]; + + if ($search !== '') { + $query .= ' AND (assessment_name LIKE :search1 OR assessment_category LIKE :search2)'; + $params[':search1'] = "%$search%"; + $params[':search2'] = "%$search%"; + } + + $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(); @@ -860,23 +968,58 @@ function create_attendance_record_in_cycle(int $centerApplicationId, int $cycleI return (int) $pdo->lastInsertId(); } -function list_school_attendance_records_by_cycle(int $centerApplicationId, int $cycleId): array +function list_school_attendance_records_by_cycle(int $centerApplicationId, int $cycleId, string $search = '', int $limit = 0, int $offset = 0): array { $pdo = db_connection(); - $stmt = $pdo->prepare( - 'SELECT ar.*, s.student_code, s.full_name, s.grade_level, s.guardian_phone + $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 - ORDER BY ar.attendance_date DESC, ar.created_at DESC, ar.id DESC' - ); - $stmt->execute([ + 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(); diff --git a/includes/sidebar.php b/includes/sidebar.php index 63f80b3..93cb17c 100644 --- a/includes/sidebar.php +++ b/includes/sidebar.php @@ -5,6 +5,7 @@ $statusQuery = $_GET['status'] ?? ''; $activePage = 'admin'; if ($script === 'app_settings.php') $activePage = 'app_settings'; if ($script === 'subjects.php') $activePage = 'subjects'; +if ($script === 'global_cycles.php') $activePage = 'global_cycles'; if ($script === 'dashboard.php') $activePage = 'dashboard'; if ($script === 'applications.php') $activePage = ($statusQuery === 'approved') ? 'approved' : 'applications'; if (in_array($script, ['approved_school.php', 'center_profile.php', 'students.php', 'teachers.php', 'assessments.php', 'attendance.php'], true)) $activePage = 'approved'; @@ -38,6 +39,10 @@ if (!isset($recentApproved)) { + + + الدورات الموسمية + الإعدادات العامة diff --git a/patch_approved_school.py b/patch_approved_school.py new file mode 100644 index 0000000..2dce8cd --- /dev/null +++ b/patch_approved_school.py @@ -0,0 +1,38 @@ +import re + +with open('approved_school.php', 'r') as f: + content = f.read() + +# Replace the layout start +layout_start_replacement = """
+
+
+
+ +
+
+ """ + +content = re.sub(r'
\s*
\s*<\?php if \(!\$isApproved\): \?>', layout_start_replacement, content) + +# Replace the layout end. Wait, currently it is: +#
+#
+# +#
+#
+# Let's just do string replacement for the end. +end_str = """
+
+ +
+""" +new_end_str = """
+
+
+
+""" +content = content.replace(end_str, new_end_str) + +with open('approved_school.php', 'w') as f: + f.write(content) diff --git a/students.php b/students.php index 0af762d..7c454e5 100644 --- a/students.php +++ b/students.php @@ -53,7 +53,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $application) { } } -$students = $isApprovedSchool && $selectedCycleId > 0 ? list_school_students_by_cycle((int) $application['id'], $selectedCycleId) : []; +$search = clean_text($_GET['search'] ?? '', 255); +$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT) ?: 1; +$limit = 10; +$offset = ($page - 1) * $limit; + +$students = $isApprovedSchool && $selectedCycleId > 0 ? list_school_students_by_cycle((int) $application['id'], $selectedCycleId, $search, $limit, $offset) : []; +$totalStudents = $isApprovedSchool && $selectedCycleId > 0 ? count_school_students_by_cycle((int) $application['id'], $selectedCycleId, $search) : 0; + $metrics = $isApprovedSchool && $selectedCycleId > 0 ? school_student_metrics_by_cycle((int) $application['id'], $selectedCycleId) : [ 'total' => 0, 'boys' => 0, @@ -84,7 +91,7 @@ render_flash($flash);
- +
@@ -322,6 +329,7 @@ render_flash($flash);
طلاب / طالبات
+
إجمالي القيد طالب
@@ -369,6 +377,7 @@ render_flash($flash);
+
diff --git a/subjects.php b/subjects.php index 36bd467..7025923 100644 --- a/subjects.php +++ b/subjects.php @@ -53,14 +53,26 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { // Read list $search = clean_text($_GET['search'] ?? '', 255); +$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT) ?: 1; +$limit = 10; +$offset = ($page - 1) * $limit; + $query = 'SELECT * FROM subjects'; +$countQuery = 'SELECT COUNT(*) FROM subjects'; $params = []; if ($search !== '') { - $query .= ' WHERE name LIKE ? OR description LIKE ?'; + $where = ' WHERE name LIKE ? OR description LIKE ?'; + $query .= $where; + $countQuery .= $where; $params[] = "%$search%"; $params[] = "%$search%"; } -$query .= ' ORDER BY id DESC'; + +$stmtCount = db()->prepare($countQuery); +$stmtCount->execute($params); +$totalItems = (int)$stmtCount->fetchColumn(); + +$query .= ' ORDER BY id DESC LIMIT ' . $limit . ' OFFSET ' . $offset; $stmt = db()->prepare($query); $stmt->execute($params); $subjects = $stmt->fetchAll(); @@ -88,15 +100,7 @@ render_flash($flash);
-
- -
+
@@ -160,6 +164,7 @@ render_flash($flash);
+
diff --git a/teachers.php b/teachers.php index 0d12a4c..6e75bc0 100644 --- a/teachers.php +++ b/teachers.php @@ -46,7 +46,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $application) { } } -$teachers = $isApprovedSchool && $selectedCycleId > 0 ? list_school_teachers_by_cycle((int) $application['id'], $selectedCycleId) : []; +$search = clean_text($_GET['search'] ?? '', 255); +$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT) ?: 1; +$limit = 10; +$offset = ($page - 1) * $limit; + +$teachers = $isApprovedSchool && $selectedCycleId > 0 ? list_school_teachers_by_cycle((int) $application['id'], $selectedCycleId, $search, $limit, $offset) : []; +$totalTeachers = $isApprovedSchool && $selectedCycleId > 0 ? count_school_teachers_by_cycle((int) $application['id'], $selectedCycleId, $search) : 0; + $metrics = $isApprovedSchool && $selectedCycleId > 0 ? school_teacher_metrics_by_cycle((int) $application['id'], $selectedCycleId) : [ 'total' => 0, 'active' => 0, @@ -83,7 +90,7 @@ render_flash($flash);
- +
@@ -296,6 +303,7 @@ render_flash($flash);
بريد جاهز / بانتظار التفعيل
+
إجمالي الفريق عضو
@@ -341,6 +349,7 @@ render_flash($flash);
+