From 7937c54a84d457bff9f25e04fe90f941190cc8d0 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Fri, 17 Apr 2026 07:30:11 +0000 Subject: [PATCH] Autosave: 20260417-073012 --- admin.php | 6 + center_assessment_criteria.php | 374 +++++++ center_assessment_report.php | 228 +++++ center_assessment_score_sheet.php | 495 +++++++++ center_assessments.php | 529 ++++++++++ .../20260417_center_assessment_system.sql | 83 ++ includes/app.php | 1 + includes/cycles.php | 950 ++++++++++++++++++ 8 files changed, 2666 insertions(+) create mode 100644 center_assessment_criteria.php create mode 100644 center_assessment_report.php create mode 100644 center_assessment_score_sheet.php create mode 100644 center_assessments.php create mode 100644 db/migrations/20260417_center_assessment_system.sql diff --git a/admin.php b/admin.php index 7ebd78b..816406b 100644 --- a/admin.php +++ b/admin.php @@ -87,6 +87,11 @@ render_flash($flash);

للوصول إلى المراكز الجاهزة للتشغيل ثم الانتقال إلى الطلاب والمعلمين والتقييمات والحضور لكل مركز.

فتح المراكز المعتمدة +
+

تقييم المراكز

+

إعداد تقييمات إشرافية للمراكز المعتمدة داخل كل دورة موسمية، تمهيداً لإضافة البنود والرصد بنفس نمط الطلاب.

+ فتح تقييم المراكز +

هيكل النظام

مرجع سريع لفهم تنظيم الصفحات الحالية ومسار التطوير الإداري داخل التطبيق.

@@ -107,6 +112,7 @@ render_flash($flash); diff --git a/center_assessment_criteria.php b/center_assessment_criteria.php new file mode 100644 index 0000000..b50c051 --- /dev/null +++ b/center_assessment_criteria.php @@ -0,0 +1,374 @@ + 0 ? get_application($applicationId) : null; +$isApprovedCenter = $application && (string) ($application['status'] ?? '') === 'approved'; +$errors = []; +$values = ['criteria' => []]; +$cycleContext = ['cycles' => [], 'selected' => null, 'active' => null, 'read_only' => false]; +$selectedCycle = null; +$selectedCycleId = 0; +$isCycleReadOnly = false; +$cycleLabel = 'لا توجد دورة بعد'; + +$buildCenterAssessmentsUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0, array $extra = []): string { + $params = []; + if ($targetApplicationId > 0) { + $params['id'] = $targetApplicationId; + } + if ($targetCycleId > 0) { + $params['cycle'] = $targetCycleId; + } + foreach ($extra as $key => $value) { + if ($value === '' || $value === null) { + continue; + } + $params[$key] = $value; + } + + return 'center_assessments.php' . ($params !== [] ? '?' . http_build_query($params) : ''); +}; + +$buildCenterAssessmentCriteriaUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0, int $targetAssessmentId = 0, array $extra = []) : string { + $params = []; + if ($targetApplicationId > 0) { + $params['id'] = $targetApplicationId; + } + if ($targetCycleId > 0) { + $params['cycle'] = $targetCycleId; + } + if ($targetAssessmentId > 0) { + $params['assessment_id'] = $targetAssessmentId; + } + foreach ($extra as $key => $value) { + if ($value === '' || $value === null) { + continue; + } + $params[$key] = $value; + } + + return 'center_assessment_criteria.php' . ($params !== [] ? '?' . http_build_query($params) : ''); +}; + +if ($isApprovedCenter) { + $cycleContext = resolve_school_cycle_context((int) $application['id'], $application, $requestedCycleId); + $selectedCycle = $cycleContext['selected']; + $selectedCycleId = $selectedCycle ? (int) ($selectedCycle['id'] ?? 0) : 0; + $isCycleReadOnly = (bool) ($cycleContext['read_only'] ?? false); + $cycleLabel = $selectedCycle ? (string) ($selectedCycle['cycle_name'] ?? $cycleLabel) : $cycleLabel; +} + +$assessmentOptions = $isApprovedCenter && $selectedCycleId > 0 + ? center_assessment_type_options_by_cycle((int) $application['id'], $selectedCycleId, false) + : []; +$selectedAssessmentId = $requestedAssessmentId; +if ($selectedAssessmentId <= 0 && $assessmentOptions !== []) { + $keys = array_keys($assessmentOptions); + $selectedAssessmentId = (int) ($keys[0] ?? 0); +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST' && $application) { + $selectedAssessmentId = filter_input(INPUT_POST, 'assessment_id', FILTER_VALIDATE_INT) ?: $selectedAssessmentId; + if (!$isApprovedCenter) { + $errors['form'] = 'لا يمكن إعداد بنود تقييم المركز قبل اعتماد المركز.'; + } elseif ($selectedCycleId <= 0) { + $errors['form'] = 'يرجى إنشاء دورة موسمية أولاً من صفحة المركز.'; + } elseif ($isCycleReadOnly) { + $errors['form'] = 'هذه الدورة مؤرشفة للقراءة فقط. اختر دورة نشطة لتعديل البنود.'; + } elseif ($selectedAssessmentId <= 0 || !isset($assessmentOptions[$selectedAssessmentId])) { + $errors['form'] = 'يرجى اختيار تقييم مركز صحيح أولاً.'; + } else { + [$values, $errors] = validate_center_assessment_criteria_input((int) $application['id'], $selectedCycleId, $selectedAssessmentId, $_POST); + if ($errors === []) { + try { + $savedRows = save_center_assessment_criteria_in_cycle((int) $application['id'], $selectedCycleId, $selectedAssessmentId, $values); + set_flash('success', 'تم حفظ ' . $savedRows . ' بند/بنود لتقييم المركز.'); + header('Location: ' . $buildCenterAssessmentCriteriaUrl((int) $application['id'], $selectedCycleId, $selectedAssessmentId)); + exit; + } catch (Throwable $exception) { + $errors['form'] = 'تعذر حفظ البنود حالياً. يرجى المحاولة مرة أخرى.'; + } + } + } +} + +$selectedAssessment = $selectedAssessmentId > 0 ? ($assessmentOptions[$selectedAssessmentId] ?? null) : null; +$criteriaRows = $isApprovedCenter && $selectedCycleId > 0 && $selectedAssessmentId > 0 + ? list_center_assessment_criteria_by_assessment((int) $application['id'], $selectedCycleId, $selectedAssessmentId, false) + : []; +$criteriaMetrics = $isApprovedCenter && $selectedCycleId > 0 && $selectedAssessmentId > 0 + ? center_assessment_criteria_metrics((int) $application['id'], $selectedCycleId, $selectedAssessmentId) + : ['total' => 0, 'active' => 0, 'active_max_score' => 0.0]; + +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + $values['criteria'] = []; + foreach ($criteriaRows as $criterion) { + $values['criteria'][] = [ + 'id' => (int) ($criterion['id'] ?? 0), + 'title' => (string) ($criterion['title'] ?? ''), + 'max_score' => rtrim(rtrim(number_format((float) ($criterion['max_score'] ?? 0), 2, '.', ''), '0'), '.'), + 'notes' => (string) ($criterion['notes'] ?? ''), + 'is_active' => ((int) ($criterion['is_active'] ?? 0) === 1) ? '1' : '0', + ]; + } + if ($values['criteria'] === []) { + $values['criteria'][] = ['id' => 0, 'title' => 'الالتزام التشغيلي', 'max_score' => '10', 'notes' => '', 'is_active' => '1']; + $values['criteria'][] = ['id' => 0, 'title' => 'جودة التنفيذ', 'max_score' => '10', 'notes' => '', 'is_active' => '1']; + $values['criteria'][] = ['id' => 0, 'title' => 'التواصل والتقارير', 'max_score' => '10', 'notes' => '', 'is_active' => '1']; + } +} + +$pageTitle = $application && $selectedAssessment + ? 'بنود تقييم المركز: ' . (string) ($selectedAssessment['title'] ?? '') . ' — ' . (string) ($application['center_name'] ?? '') + : 'بنود تقييم المراكز'; +$pageDescription = 'إدارة البنود التفصيلية لكل تقييم مركز داخل الدورة الموسمية، مع تحديث الدرجة القصوى تلقائياً من مجموع البنود النشطة.'; +$assessmentsUrl = $application ? $buildCenterAssessmentsUrl((int) $application['id'], $selectedCycleId) : 'center_assessments.php'; + +if (!$application) { + http_response_code(404); +} + +render_page_start($pageTitle, 'admin', $pageDescription); +render_flash($flash); +?> +
+
+
+
+ +
+
+ +
+
المركز غير موجود
+

تحقق من الرابط أو ارجع إلى صفحة تقييم المراكز.

+ تقييم المراكز +
+ +
+
البنود تُفتح بعد الاعتماد
+

اعتمد المركز أولاً حتى تتمكن من إعداد بنود تقييمه.

+
+ +
+
أنشئ دورة أولاً
+

يجب اختيار دورة موسمية للمركز قبل إعداد بنود التقييم.

+ الرجوع إلى تقييمات المركز +
+ +
+
اختر تقييماً أولاً
+

ابدأ من صفحة تقييمات المراكز ثم افتح البنود للتقييم المطلوب.

+ الرجوع إلى تقييمات المركز +
+ +
+
+
+ مرحلة 2 — بنود تقييم المراكز +

+

أضف البنود التفصيلية التي سيُبنى عليها تقييم المركز، مثل الالتزام وجودة التنفيذ والتقارير. مجموع البنود النشطة سيُحدِّث الدرجة النهائية لهذا التقييم تلقائياً.

+
+ + + + الوزن % +
+
+
+
+
التقييم المحدد
+
+
عدد البنود النشطة حالياً داخل هذا التقييم.
+
+ الرجوع إلى التقييمات + + + + الدرجة الحالية لهذا التقييم: +
+
+
+
+
+ +
+
إجمالي البنود
كل البنود المحفوظة لهذا التقييم.
+
البنود النشطة
هي التي تدخل في الرصد النهائي.
+
مجموع البنود النشطة
يحدّث الدرجة القصوى للتقييم تلقائياً.
+
+ +
+
+ + +
+ + +
+
+
الحالة
+
+
+
+
+ + +
+ + +
هذه الدورة مؤرشفة، لذلك الصفحة معروضة للقراءة فقط.
+ + +
+
+
+
بنود تقييم المركز
+
يمكنك إضافة بنود جديدة أو إخفاء بند قديم دون حذفه من السجل.
+
+ + + +
+ +
+ +
+ + + + + + + + + + + + $criterion): ?> + + + + + + + + + +
اسم البندالدرجةملاحظاتالحالةالإجراء
+ + > + +
+ +
+ > + + > + + + + + قراءة فقط + + أوقفه + + + +
+
+ + +
+
سيتم تحديث الدرجة النهائية لهذا التقييم تلقائياً إلى مجموع البنود النشطة.
+ +
+ +
+
+ +
+
+
+
+ + + + + 0 ? get_application($applicationId) : null; +$isApprovedCenter = $application && (string) ($application['status'] ?? '') === 'approved'; +$cycleContext = ['cycles' => [], 'selected' => null, 'active' => null, 'read_only' => false]; +$selectedCycle = null; +$selectedCycleId = 0; +$cycleLabel = 'لا توجد دورة بعد'; + +$buildCenterAssessmentsUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0): string { + $params = []; + if ($targetApplicationId > 0) { + $params['id'] = $targetApplicationId; + } + if ($targetCycleId > 0) { + $params['cycle'] = $targetCycleId; + } + return 'center_assessments.php' . ($params !== [] ? '?' . http_build_query($params) : ''); +}; + +$buildCenterAssessmentCriteriaUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0, int $targetAssessmentId = 0): string { + $params = []; + if ($targetApplicationId > 0) { + $params['id'] = $targetApplicationId; + } + if ($targetCycleId > 0) { + $params['cycle'] = $targetCycleId; + } + if ($targetAssessmentId > 0) { + $params['assessment_id'] = $targetAssessmentId; + } + return 'center_assessment_criteria.php' . ($params !== [] ? '?' . http_build_query($params) : ''); +}; + +$buildCenterAssessmentScoreUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0, int $targetAssessmentId = 0): string { + $params = []; + if ($targetApplicationId > 0) { + $params['id'] = $targetApplicationId; + } + if ($targetCycleId > 0) { + $params['cycle'] = $targetCycleId; + } + if ($targetAssessmentId > 0) { + $params['assessment_id'] = $targetAssessmentId; + } + return 'center_assessment_score_sheet.php' . ($params !== [] ? '?' . http_build_query($params) : ''); +}; + +if ($isApprovedCenter) { + $cycleContext = resolve_school_cycle_context((int) $application['id'], $application, $requestedCycleId); + $selectedCycle = $cycleContext['selected']; + $selectedCycleId = $selectedCycle ? (int) ($selectedCycle['id'] ?? 0) : 0; + $cycleLabel = $selectedCycle ? (string) ($selectedCycle['cycle_name'] ?? $cycleLabel) : $cycleLabel; +} + +$summary = $isApprovedCenter && $selectedCycleId > 0 + ? center_assessment_summary_by_cycle((int) $application['id'], $selectedCycleId) + : [ + 'assessments' => [], + 'total_assessments' => 0, + 'active_assessments' => 0, + 'recorded_assessments' => 0, + 'completed_assessments' => 0, + 'pending_assessments' => 0, + 'waived_assessments' => 0, + 'missing_assessments' => 0, + 'overall_percentage' => 0.0, + 'score_total' => 0.0, + 'max_score_total' => 0.0, + 'latest_assessed_on' => '', + 'performance' => student_certificate_performance_meta(0.0), + ]; + +$pageTitle = $application && $isApprovedCenter + ? 'تقرير تقييم المراكز: ' . (string) ($application['center_name'] ?? '') . ($selectedCycle ? ' — ' . $cycleLabel : '') + : 'تقرير تقييم المراكز'; +$pageDescription = 'ملخص مجمع لنتائج تقييم المركز داخل الدورة المختارة، مع حالة كل تقييم ونسبة الإنجاز الكلية.'; + +if (!$application) { + http_response_code(404); +} + +$assessmentsUrl = $application ? $buildCenterAssessmentsUrl((int) $application['id'], $selectedCycleId) : 'center_assessments.php'; +render_page_start($pageTitle, 'approved', $pageDescription, (string) ($application['favicon'] ?? '')); +render_flash($flash); +?> +
+
+
+
+ +
+
+ +
+
المركز غير موجود
+

تحقق من الرابط أو ارجع إلى قائمة المراكز المعتمدة.

+ المراكز المعتمدة +
+ +
+
التقرير يُفتح بعد الاعتماد
+

اعتمد المركز أولاً حتى يظهر تقرير التقييم الإشرافي.

+ ملف الاعتماد +
+ +
+
+
+ تقرير الدورة +

ملخص تقييم المركز

+

هذا التقرير يجمع حالة كل تقييم إشرافي للمركز داخل دورة ويعرض النسبة الكلية الحالية.

+
+ تقييمات نشطة + % + +
+
+
+
+
إجراءات سريعة
+ +
+
+
+
+ + +
لا توجد دورة متاحة لهذا المركز بعد. أنشئ دورة أولاً من صفحة المركز.
+ +
+
لا توجد تقييمات لعرض التقرير
+

أضف تقييمات مركز أولاً، ثم استخدم الرصد لإظهار النتيجة هنا.

+ إضافة تقييم +
+ +
+
النسبة الكلية
%
+
المرصود
من تقييم نشط
+
المكتمل
المؤجل
+
آخر تحديث
آخر تاريخ رصد
+
+ +
+ طريقة الحساب الحالية: + مجموع الدرجات المكتملة + من أصل + للتقييمات المكتملة فقط. +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
التقييمالحالةالنتيجةالنسبةالبنودآخر رصدالإجراء
+
+
٪
+
+ + غير مرصود + + + + +
+
+
+ +
+
+
+ + +
+
+
+
+ 0 ? get_application($applicationId) : null; +$isApprovedCenter = $application && (string) ($application['status'] ?? '') === 'approved'; +$errors = []; +$cycleContext = ['cycles' => [], 'selected' => null, 'active' => null, 'read_only' => false]; +$selectedCycle = null; +$selectedCycleId = 0; +$isCycleReadOnly = false; +$cycleLabel = 'لا توجد دورة بعد'; +$values = [ + 'assessment_type_id' => '', + 'assessed_on' => date('Y-m-d'), + 'status' => 'completed', + 'assessment_max_score' => 0.0, + 'has_criteria' => false, + 'criteria' => [], + 'criteria_scores' => [], + 'score' => null, + 'score_raw' => '', + 'notes' => '', + 'should_save' => false, +]; + +$buildCenterAssessmentsUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0): string { + $params = []; + if ($targetApplicationId > 0) { + $params['id'] = $targetApplicationId; + } + if ($targetCycleId > 0) { + $params['cycle'] = $targetCycleId; + } + return 'center_assessments.php' . ($params !== [] ? '?' . http_build_query($params) : ''); +}; + +$buildCenterAssessmentCriteriaUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0, int $targetAssessmentId = 0): string { + $params = []; + if ($targetApplicationId > 0) { + $params['id'] = $targetApplicationId; + } + if ($targetCycleId > 0) { + $params['cycle'] = $targetCycleId; + } + if ($targetAssessmentId > 0) { + $params['assessment_id'] = $targetAssessmentId; + } + return 'center_assessment_criteria.php' . ($params !== [] ? '?' . http_build_query($params) : ''); +}; + +$buildCenterAssessmentScoreUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0, int $targetAssessmentId = 0): string { + $params = []; + if ($targetApplicationId > 0) { + $params['id'] = $targetApplicationId; + } + if ($targetCycleId > 0) { + $params['cycle'] = $targetCycleId; + } + if ($targetAssessmentId > 0) { + $params['assessment_id'] = $targetAssessmentId; + } + return 'center_assessment_score_sheet.php' . ($params !== [] ? '?' . http_build_query($params) : ''); +}; + +$buildCenterAssessmentReportUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0): string { + $params = []; + if ($targetApplicationId > 0) { + $params['id'] = $targetApplicationId; + } + if ($targetCycleId > 0) { + $params['cycle'] = $targetCycleId; + } + return 'center_assessment_report.php' . ($params !== [] ? '?' . http_build_query($params) : ''); +}; + +if ($isApprovedCenter) { + $cycleContext = resolve_school_cycle_context((int) $application['id'], $application, $requestedCycleId); + $selectedCycle = $cycleContext['selected']; + $selectedCycleId = $selectedCycle ? (int) ($selectedCycle['id'] ?? 0) : 0; + $isCycleReadOnly = (bool) ($cycleContext['read_only'] ?? false); + $cycleLabel = $selectedCycle ? (string) ($selectedCycle['cycle_name'] ?? $cycleLabel) : $cycleLabel; +} + +$assessmentOptions = $isApprovedCenter && $selectedCycleId > 0 + ? center_assessment_type_options_by_cycle((int) $application['id'], $selectedCycleId, false) + : []; +$selectedAssessmentId = $requestedAssessmentId; +if ($selectedAssessmentId <= 0 && $assessmentOptions !== []) { + $keys = array_keys($assessmentOptions); + $selectedAssessmentId = (int) ($keys[0] ?? 0); +} +if ($selectedAssessmentId > 0) { + $values['assessment_type_id'] = (string) $selectedAssessmentId; +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST' && $application) { + if (!$isApprovedCenter) { + $errors['form'] = 'لا يمكن رصد تقييم المركز قبل اعتماد المركز.'; + } elseif ($selectedCycleId <= 0) { + $errors['form'] = 'يرجى إنشاء دورة موسمية أولاً من صفحة المركز.'; + } elseif ($isCycleReadOnly) { + $errors['form'] = 'هذه الدورة مؤرشفة للقراءة فقط. اختر دورة نشطة لإدخال تقييم جديد.'; + } else { + [$values, $errors, $selectedAssessmentMeta] = validate_center_assessment_score_input((int) $application['id'], $selectedCycleId, $_POST); + $selectedAssessmentId = (int) ($values['assessment_type_id'] ?? 0); + if ($errors === []) { + try { + save_center_assessment_score_in_cycle((int) $application['id'], $selectedCycleId, $values); + set_flash('success', 'تم حفظ رصد تقييم المركز بنجاح.'); + header('Location: ' . $buildCenterAssessmentScoreUrl((int) $application['id'], $selectedCycleId, $selectedAssessmentId)); + exit; + } catch (Throwable $exception) { + $errors['form'] = 'تعذر حفظ الرصد حالياً. يرجى المحاولة مرة أخرى.'; + } + } + } +} + +$selectedAssessment = $selectedAssessmentId > 0 ? ($assessmentOptions[$selectedAssessmentId] ?? null) : null; +$criteria = $isApprovedCenter && $selectedCycleId > 0 && $selectedAssessmentId > 0 + ? list_center_assessment_criteria_by_assessment((int) $application['id'], $selectedCycleId, $selectedAssessmentId, true) + : []; +$hasCriteria = $criteria !== []; +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + $values['has_criteria'] = $hasCriteria; + $values['criteria'] = $criteria; + if ($hasCriteria) { + $values['assessment_max_score'] = round(array_reduce($criteria, static function (float $carry, array $criterion): float { + return $carry + (float) ($criterion['max_score'] ?? 0); + }, 0.0), 2); + } elseif ($selectedAssessment) { + $values['assessment_max_score'] = (float) ($selectedAssessment['max_score'] ?? 0); + } +} + +$existingBundle = $isApprovedCenter && $selectedCycleId > 0 && $selectedAssessmentId > 0 + ? center_assessment_score_bundle_by_assessment((int) $application['id'], $selectedCycleId, $selectedAssessmentId) + : ['score' => null, 'criteria_scores' => []]; +$existingScore = $existingBundle['score'] ?? null; +$existingCriteriaScores = $existingBundle['criteria_scores'] ?? []; + +if ($_SERVER['REQUEST_METHOD'] !== 'POST' && is_array($existingScore) && $existingScore !== []) { + if (!empty($existingScore['assessed_on'])) { + $values['assessed_on'] = (string) $existingScore['assessed_on']; + } + $values['status'] = center_assessment_normalize_status((string) ($existingScore['status'] ?? 'completed')); + $values['notes'] = (string) ($existingScore['notes'] ?? ''); + + if ($hasCriteria) { + foreach ($criteria as $criterion) { + $criterionId = (int) ($criterion['id'] ?? 0); + if ($criterionId <= 0) { + continue; + } + $existingCriterion = $existingCriteriaScores[$criterionId] ?? []; + $values['criteria_scores'][$criterionId] = [ + 'criterion_id' => $criterionId, + 'score' => isset($existingCriterion['score']) ? (float) $existingCriterion['score'] : null, + 'score_raw' => isset($existingCriterion['score']) && $existingCriterion['score'] !== null + ? center_score_display((float) $existingCriterion['score']) + : '', + 'max_score' => (float) ($criterion['max_score'] ?? 0), + ]; + } + } else { + $values['score'] = isset($existingScore['score']) ? (float) $existingScore['score'] : null; + $values['score_raw'] = isset($existingScore['score']) && $existingScore['score'] !== null + ? center_score_display((float) $existingScore['score']) + : ''; + } +} + +$criteriaMetrics = $isApprovedCenter && $selectedCycleId > 0 && $selectedAssessmentId > 0 + ? center_assessment_criteria_metrics((int) $application['id'], $selectedCycleId, $selectedAssessmentId) + : ['total' => 0, 'active' => 0, 'active_max_score' => 0.0]; +$scoreMetrics = $isApprovedCenter && $selectedCycleId > 0 && $selectedAssessmentId > 0 + ? center_assessment_score_metrics_by_cycle((int) $application['id'], $selectedCycleId, $selectedAssessmentId) + : ['total' => 0, 'completed' => 0, 'pending' => 0, 'waived' => 0, 'average_score' => 0.0, 'latest_date' => '']; + +$pageTitle = $application && $isApprovedCenter + ? 'رصد تقييم المركز: ' . (string) ($application['center_name'] ?? '') . ($selectedAssessment ? ' — ' . (string) ($selectedAssessment['title'] ?? '') : '') + : 'رصد تقييم المركز'; +$pageDescription = 'إدخال درجة تقييم المركز نفسه داخل الدورة المختارة، مع دعم البنود التفصيلية والتقرير النهائي.'; + +if (!$application) { + http_response_code(404); +} + +$assessmentsUrl = $application ? $buildCenterAssessmentsUrl((int) $application['id'], $selectedCycleId) : 'center_assessments.php'; +$criteriaUrl = $application && $selectedAssessmentId > 0 ? $buildCenterAssessmentCriteriaUrl((int) $application['id'], $selectedCycleId, $selectedAssessmentId) : 'center_assessment_criteria.php'; +$reportUrl = $application ? $buildCenterAssessmentReportUrl((int) $application['id'], $selectedCycleId) : 'center_assessment_report.php'; +$maxScoreLabel = center_score_display((float) ($values['assessment_max_score'] ?? 0)); +$existingStatus = is_array($existingScore) ? center_assessment_normalize_status((string) ($existingScore['status'] ?? 'pending')) : ''; +$existingPercentage = (is_array($existingScore) && isset($existingScore['score']) && $existingScore['score'] !== null && (float) ($existingScore['max_score'] ?? 0) > 0) + ? round(((float) $existingScore['score'] / (float) $existingScore['max_score']) * 100, 2) + : null; + +render_page_start($pageTitle, 'approved', $pageDescription, (string) ($application['favicon'] ?? '')); +render_flash($flash); +?> +
+
+
+
+ +
+
+ +
+
المركز غير موجود
+

تحقق من الرابط أو ارجع إلى قائمة المراكز المعتمدة.

+ المراكز المعتمدة +
+ +
+
الرصد يُفتح بعد الاعتماد
+

اعتمد المركز أولاً حتى يظهر رصد تقييمه الإشرافي.

+ ملف الاعتماد +
+ +
+
+
+ رصد المركز +

إدخال تقييم إشرافي للمركز

+

اختر التقييم، ثم أدخل النتيجة مباشرة أو عبر البنود التفصيلية داخل دورة .

+
+ تقييمات متاحة + + +
+
+
+ +
+
+
+ + +
+
لا توجد دورة متاحة لهذا المركز بعد. أنشئ دورة أولاً من صفحة المركز.
+
+ +
+
لا توجد تقييمات مراكز جاهزة للرصد
+

أضف نوع تقييم أولاً من صفحة تقييم المراكز ثم ارجع هنا لإدخال الدرجة.

+ إضافة تقييم +
+ +
+
+ + +
+ + +
+
+ +
+
+
+ + +
+
الدرجة النهائية
المجموع المعتمد
+
البنود النشطة
من أصل
+
حالة الرصد
غير مرصود' ?>
آخر حفظ
+
آخر نسبة
للتقييم الحالي
+
+ + + +
+ + +
هذه الدورة مؤرشفة، لذلك الصفحة معروضة للقراءة فقط.
+ + + +
+
+ +
+
+ + > +
+
+
+ + +
+
+
+ الوزن + ٪ +
+
+
+ + +
+ + + +
+ + + + + + + + + + + + + + + + + + +
البندالدرجة القصوىالدرجة المرصودة
+
+
+
+ + > +
+
+
+
المجموع الحالي
+
المجموع الكلي
+
نسبة الإنجاز 0) ? center_score_display(((float) $values['score_raw'] / (float) $values['assessment_max_score']) * 100) . '%' : '—') ?>
+
+ +
+ + > +
+
+ + +
+ + +
+ + +
+ آخر حفظ: + + بتاريخ + % +
+ + + +
+
يمكنك تعديل نفس التقييم لاحقاً، وسيتم تحديث الدرجة والبنود الحالية بدلاً من إنشاء نسخة جديدة.
+ +
+ +
+
+ + + +
+
+
+
+ + + + 0 ? get_application($applicationId) : null; +$isApprovedCenter = $application && (string) ($application['status'] ?? '') === 'approved'; +$values = assessment_defaults(); +$errors = []; +$cycleContext = ['cycles' => [], 'selected' => null, 'active' => null, 'read_only' => false]; +$selectedCycle = null; +$selectedCycleId = 0; +$isCycleReadOnly = false; +$cycleLabel = 'لا توجد دورة بعد'; + +$buildCenterAssessmentsUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0, array $extra = []): string { + $params = []; + if ($targetApplicationId > 0) { + $params['id'] = $targetApplicationId; + } + if ($targetCycleId > 0) { + $params['cycle'] = $targetCycleId; + } + foreach ($extra as $key => $value) { + if ($value === '' || $value === null) { + continue; + } + $params[$key] = $value; + } + return 'center_assessments.php' . ($params !== [] ? '?' . http_build_query($params) : ''); +}; + + +$buildCenterAssessmentScoreUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0, int $targetAssessmentId = 0): string { + $params = []; + if ($targetApplicationId > 0) { + $params['id'] = $targetApplicationId; + } + if ($targetCycleId > 0) { + $params['cycle'] = $targetCycleId; + } + if ($targetAssessmentId > 0) { + $params['assessment_id'] = $targetAssessmentId; + } + return 'center_assessment_score_sheet.php' . ($params !== [] ? '?' . http_build_query($params) : ''); +}; + +$buildCenterAssessmentReportUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0): string { + $params = []; + if ($targetApplicationId > 0) { + $params['id'] = $targetApplicationId; + } + if ($targetCycleId > 0) { + $params['cycle'] = $targetCycleId; + } + return 'center_assessment_report.php' . ($params !== [] ? '?' . http_build_query($params) : ''); +}; + +$buildCenterAssessmentCriteriaUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0, int $targetAssessmentId = 0): string { + $params = []; + if ($targetApplicationId > 0) { + $params['id'] = $targetApplicationId; + } + if ($targetCycleId > 0) { + $params['cycle'] = $targetCycleId; + } + if ($targetAssessmentId > 0) { + $params['assessment_id'] = $targetAssessmentId; + } + return 'center_assessment_criteria.php' . ($params !== [] ? '?' . http_build_query($params) : ''); +}; + +if ($isApprovedCenter) { + $cycleContext = resolve_school_cycle_context((int) $application['id'], $application, $requestedCycleId); + $selectedCycle = $cycleContext['selected']; + $selectedCycleId = $selectedCycle ? (int) ($selectedCycle['id'] ?? 0) : 0; + $isCycleReadOnly = (bool) ($cycleContext['read_only'] ?? false); + $cycleLabel = $selectedCycle ? (string) ($selectedCycle['cycle_name'] ?? $cycleLabel) : $cycleLabel; +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST' && $isApprovedCenter) { + $action = $_POST['action'] ?? 'add'; + $assessmentId = filter_input(INPUT_POST, 'assessment_id', FILTER_VALIDATE_INT) ?: 0; + [$values, $errors] = validate_assessment_input($_POST); + + if ($selectedCycleId <= 0) { + $errors['form'] = 'يجب إنشاء دورة موسمية لهذا المركز قبل إعداد تقييمات المراكز.'; + } elseif ($isCycleReadOnly) { + $errors['form'] = 'الدورة الحالية مؤرشفة للقراءة فقط. اختر دورة نشطة أو أنشئ دورة جديدة.'; + } + + if ($errors === []) { + try { + if ($action === 'edit' && $assessmentId > 0) { + update_center_assessment_type_in_cycle((int) $application['id'], $selectedCycleId, $assessmentId, $values); + set_flash('success', 'تم تحديث تقييم المركز بنجاح.'); + } else { + create_center_assessment_type_in_cycle((int) $application['id'], $selectedCycleId, $values); + set_flash('success', 'تم حفظ نوع تقييم جديد للمركز داخل الدورة المحددة.'); + } + + $redirectParams = array_intersect_key($_GET, array_flip(['search', 'category', 'page'])); + header('Location: ' . $buildCenterAssessmentsUrl((int) $application['id'], $selectedCycleId, $redirectParams)); + exit; + } catch (Throwable $exception) { + $errors['form'] = 'تعذر حفظ تقييم المركز حالياً. يرجى المحاولة مرة أخرى.'; + } + } +} + +$filters = [ + 'search' => clean_text($_GET['search'] ?? '', 255), + 'category' => clean_text($_GET['category'] ?? '', 80), +]; +$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT) ?: 1; +$limit = 20; +$offset = ($page - 1) * $limit; + +$assessments = $isApprovedCenter && $selectedCycleId > 0 ? list_center_assessments_by_cycle((int) $application['id'], $selectedCycleId, $filters, $limit, $offset) : []; +$totalAssessments = $isApprovedCenter && $selectedCycleId > 0 ? count_center_assessments_by_cycle((int) $application['id'], $selectedCycleId, $filters) : 0; +$metrics = $isApprovedCenter && $selectedCycleId > 0 ? center_assessment_metrics_by_cycle((int) $application['id'], $selectedCycleId) : [ + 'total' => 0, + 'active' => 0, + 'inactive' => 0, + 'active_weight' => 0.0, + 'average_max_score' => 0.0, + 'percentage' => 0, + 'points' => 0, + 'rubric' => 0, +]; + +$approvedCenterCards = []; +foreach ($approvedCenters as $center) { + $centerId = (int) ($center['id'] ?? 0); + if ($centerId <= 0) { + continue; + } + + $centerCycleContext = resolve_school_cycle_context($centerId, $center, 0); + $centerSelectedCycle = $centerCycleContext['selected'] ?? null; + $centerCycleId = $centerSelectedCycle ? (int) ($centerSelectedCycle['id'] ?? 0) : 0; + $approvedCenterCards[] = [ + 'application' => $center, + 'selected_cycle' => $centerSelectedCycle, + 'selected_cycle_id' => $centerCycleId, + 'url' => $buildCenterAssessmentsUrl($centerId, $centerCycleId), + ]; +} + +$pageTitle = $application && $isApprovedCenter + ? 'تقييم المراكز: ' . (string) ($application['center_name'] ?? '') . ($selectedCycle ? ' — ' . $cycleLabel : '') + : 'تقييم المراكز'; +$pageDescription = 'إدارة تقييمات إشرافية مستقلة للمراكز المعتمدة حسب الدورة الموسمية، مع البنود، الرصد، والتقرير المجمع.'; + +render_page_start($pageTitle, 'admin', $pageDescription); +render_flash($flash); +?> +
+
+
+
+ +
+
+ +
+
+
+ مرحلة 2 — التقييم + البنود +

إدارة تقييمات المراكز

+

هذه الصفحة تضيف طبقة مستقلة لتقييم المراكز المعتمدة حسب الدورة الموسمية، بدون خلطها مع تقييمات الطلاب. يمكنك الآن تعريف أنواع تقييم المراكز وأوزانها ثم فتح بنود كل تقييم بنفس النمط المستخدم في تقييم الطلاب، تمهيداً لصفحة الرصد الفعلي.

+
+ المراكز المعتمدة + الإعداد الحالي تقييمات نشطة + الدورة المختارة +
+ +
+
+
+
نطاق العمل
+
+
أنواع تقييم مركزية معرفة حالياً للمركز/الدورة المحددين.
+
+ 0 && !$isCycleReadOnly): ?> + + + اختر مركزاً معتمداً + + المعيار هنا: لكل مركز معتمد ولكل دورة بشكل مستقل. +
+
+
+
+
+ +
+
إجمالي التقييمات
كل الأنواع المعرفة للمركز المحدد.
+
تقييمات نشطة
جاهزة لربط البنود ثم الرصد.
+
إجمالي الوزن النشط
٪
للمراجعة قبل تفعيل الرصد.
+
متوسط الدرجة القصوى
متوسط السقف لكل تقييم مركز.
+
+ +
+
+
+ +
+
+
اختر مركزاً معتمداً للبدء
+
التقييمات المركزية لا تظهر إلا للمراكز المعتمدة، وكل مركز يُدار داخل دورته الموسمية الخاصة.
+
+
+ +
+
لا توجد مراكز معتمدة بعد
+

اعتمد مركزاً أولاً من لوحة الطلبات حتى تتمكن من إعداد تقييمات المراكز.

+ العودة إلى الطلبات +
+ +
+ + +
+ +
+ +
+ + +
+
+
تقييمات
+
الدورة الحالية: . يمكنك هنا إدارة أنواع التقييم، ثم فتح البنود، الرصد، والتقرير المجمع لكل دورة من عمود الإجراءات.
+
+ 0): ?> +
+ تقرير الدورة + +
+ +
+ + +
لا توجد دورة متاحة لهذا المركز بعد. أنشئ دورة موسمية أولاً من صفحة المركز.
+ + +
هذه الدورة مؤرشفة للقراءة فقط. يمكنك المراجعة، لكن لا يمكن إضافة أو تعديل تقييمات جديدة هنا.
+ + +
+ + +
+ + +
+
+ + +
+
+ + إعادة تعيين +
+
+ +
أصبحت دورة تقييم المراكز مكتملة: تعريف التقييم ثم البنود ثم الرصد ثم تقرير ملخّص للدورة.
+ + +
+
لا توجد تقييمات مراكز بعد
+

ابدأ بتعريف أول نوع تقييم للمركز مثل: الالتزام الإداري، جودة البيئة التعليمية، أو متابعة الخطة.

+ + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
التقييمالفئةالمقياسالدرجةالوزنالبنودالحالةالإجراء
+
+ +
+ +
٪ +
+
المجموع
+
+
+ البنود + رصد + + + + قراءة فقط + +
+
+
+ (int) $application['id'], 'cycle' => $selectedCycleId, 'search' => $filters['search'], 'category' => $filters['category']]); ?> + + + +
+
+ +
+ +
+
+ +
+
+
+
+ + 0): ?> + + + + + + diff --git a/db/migrations/20260417_center_assessment_system.sql b/db/migrations/20260417_center_assessment_system.sql new file mode 100644 index 0000000..fb9d122 --- /dev/null +++ b/db/migrations/20260417_center_assessment_system.sql @@ -0,0 +1,83 @@ +CREATE TABLE IF NOT EXISTS center_assessment_types ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + center_application_id INT UNSIGNED NOT NULL, + cycle_id INT UNSIGNED NOT NULL, + title VARCHAR(190) NOT NULL, + category VARCHAR(80) NOT NULL DEFAULT 'أداء', + scale_type VARCHAR(40) NOT NULL DEFAULT 'percentage', + max_score DECIMAL(8,2) NOT NULL DEFAULT 100.00, + weight_percentage DECIMAL(5,2) NOT NULL DEFAULT 0.00, + is_active TINYINT(1) NOT NULL DEFAULT 1, + notes TEXT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_center_assessment_types_center (center_application_id), + INDEX idx_center_assessment_types_cycle (cycle_id), + INDEX idx_center_assessment_types_active (center_application_id, cycle_id, is_active), + CONSTRAINT fk_center_assessment_types_center_application FOREIGN KEY (center_application_id) REFERENCES center_applications(id) ON DELETE CASCADE, + CONSTRAINT fk_center_assessment_types_cycle FOREIGN KEY (cycle_id) REFERENCES school_cycles(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS center_assessment_criteria ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + center_application_id INT UNSIGNED NOT NULL, + cycle_id INT UNSIGNED NOT NULL, + assessment_type_id INT UNSIGNED NOT NULL, + title VARCHAR(190) NOT NULL, + max_score DECIMAL(8,2) NOT NULL DEFAULT 0.00, + sort_order INT UNSIGNED NOT NULL DEFAULT 1, + is_active TINYINT(1) NOT NULL DEFAULT 1, + notes TEXT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_center_assessment_criteria_center (center_application_id), + INDEX idx_center_assessment_criteria_cycle (cycle_id), + INDEX idx_center_assessment_criteria_assessment (assessment_type_id), + CONSTRAINT fk_center_assessment_criteria_center_application FOREIGN KEY (center_application_id) REFERENCES center_applications(id) ON DELETE CASCADE, + CONSTRAINT fk_center_assessment_criteria_cycle FOREIGN KEY (cycle_id) REFERENCES school_cycles(id) ON DELETE CASCADE, + CONSTRAINT fk_center_assessment_criteria_assessment FOREIGN KEY (assessment_type_id) REFERENCES center_assessment_types(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS center_assessment_scores ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + center_application_id INT UNSIGNED NOT NULL, + cycle_id INT UNSIGNED NOT NULL, + assessment_type_id INT UNSIGNED NOT NULL, + score DECIMAL(8,2) NULL, + max_score DECIMAL(8,2) NOT NULL DEFAULT 100.00, + status VARCHAR(20) NOT NULL DEFAULT 'draft', + notes TEXT NULL, + assessed_on DATE NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY uniq_center_assessment_score (center_application_id, cycle_id, assessment_type_id), + INDEX idx_center_assessment_scores_cycle (cycle_id), + INDEX idx_center_assessment_scores_assessment (assessment_type_id), + INDEX idx_center_assessment_scores_status (status), + CONSTRAINT fk_center_assessment_scores_center_application FOREIGN KEY (center_application_id) REFERENCES center_applications(id) ON DELETE CASCADE, + CONSTRAINT fk_center_assessment_scores_cycle FOREIGN KEY (cycle_id) REFERENCES school_cycles(id) ON DELETE CASCADE, + CONSTRAINT fk_center_assessment_scores_assessment FOREIGN KEY (assessment_type_id) REFERENCES center_assessment_types(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS center_assessment_score_items ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + center_application_id INT UNSIGNED NOT NULL, + cycle_id INT UNSIGNED NOT NULL, + assessment_score_id INT UNSIGNED NOT NULL, + assessment_type_id INT UNSIGNED NOT NULL, + criterion_id INT UNSIGNED NOT NULL, + score DECIMAL(8,2) NULL, + max_score DECIMAL(8,2) NOT NULL DEFAULT 0.00, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY uniq_center_assessment_score_item (assessment_score_id, criterion_id), + INDEX idx_center_assessment_score_items_center (center_application_id), + INDEX idx_center_assessment_score_items_cycle (cycle_id), + INDEX idx_center_assessment_score_items_assessment (assessment_type_id), + INDEX idx_center_assessment_score_items_criterion (criterion_id), + CONSTRAINT fk_center_assessment_score_items_center_application FOREIGN KEY (center_application_id) REFERENCES center_applications(id) ON DELETE CASCADE, + CONSTRAINT fk_center_assessment_score_items_cycle FOREIGN KEY (cycle_id) REFERENCES school_cycles(id) ON DELETE CASCADE, + CONSTRAINT fk_center_assessment_score_items_score FOREIGN KEY (assessment_score_id) REFERENCES center_assessment_scores(id) ON DELETE CASCADE, + CONSTRAINT fk_center_assessment_score_items_assessment FOREIGN KEY (assessment_type_id) REFERENCES center_assessment_types(id) ON DELETE CASCADE, + CONSTRAINT fk_center_assessment_score_items_criterion FOREIGN KEY (criterion_id) REFERENCES center_assessment_criteria(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/includes/app.php b/includes/app.php index b2fab68..f3c8de5 100644 --- a/includes/app.php +++ b/includes/app.php @@ -111,6 +111,7 @@ function db_connection(): PDO ensure_school_cycle_schema($pdo); ensure_school_assessment_score_schema($pdo); ensure_school_assessment_criteria_schema($pdo); + ensure_center_assessment_schema($pdo); seed_school_module_demo_data($pdo); $bootstrapped = true; } diff --git a/includes/cycles.php b/includes/cycles.php index a839e82..2fc671d 100644 --- a/includes/cycles.php +++ b/includes/cycles.php @@ -140,6 +140,25 @@ function ensure_school_assessment_criteria_schema(PDO $pdo): void $done = true; } + +function ensure_center_assessment_schema(PDO $pdo): void +{ + static $done = false; + if ($done) { + return; + } + + $migrationPath = __DIR__ . '/../db/migrations/20260417_center_assessment_system.sql'; + if (is_file($migrationPath)) { + $sql = file_get_contents($migrationPath); + if (is_string($sql) && trim($sql) !== '') { + $pdo->exec($sql); + } + } + + $done = true; +} + function ensure_school_cycle_backfill(PDO $pdo): void { $applicationRows = $pdo->query( @@ -1151,6 +1170,470 @@ function school_teacher_options_by_cycle(int $centerApplicationId, int $cycleId, return $options; } + +function create_center_assessment_type_in_cycle(int $centerApplicationId, int $cycleId, array $data): int +{ + $pdo = db_connection(); + $stmt = $pdo->prepare( + 'INSERT INTO center_assessment_types ( + center_application_id, cycle_id, title, category, scale_type, + max_score, weight_percentage, is_active, notes, + created_at, updated_at + ) VALUES ( + :center_application_id, :cycle_id, :title, :category, :scale_type, + :max_score, :weight_percentage, :is_active, :notes, + NOW(), NOW() + )' + ); + $stmt->execute([ + ':center_application_id' => $centerApplicationId, + ':cycle_id' => $cycleId, + ':title' => $data['title'], + ':category' => $data['category'], + ':scale_type' => $data['scale_type'], + ':max_score' => (float) $data['max_score'], + ':weight_percentage' => (float) $data['weight_percentage'], + ':is_active' => (int) $data['is_active'], + ':notes' => $data['notes'] !== '' ? $data['notes'] : null, + ]); + + return (int) $pdo->lastInsertId(); +} + +function list_center_assessments_by_cycle(int $centerApplicationId, int $cycleId, array $filters = [], int $limit = 0, int $offset = 0): array +{ + $pdo = db_connection(); + $query = 'SELECT cat.*, + ( + SELECT COUNT(*) + FROM center_assessment_criteria criteria + WHERE criteria.assessment_type_id = cat.id + AND criteria.center_application_id = cat.center_application_id + AND criteria.cycle_id = cat.cycle_id + AND criteria.is_active = 1 + ) AS criteria_count, + ( + SELECT COALESCE(SUM(criteria.max_score), 0) + FROM center_assessment_criteria criteria + WHERE criteria.assessment_type_id = cat.id + AND criteria.center_application_id = cat.center_application_id + AND criteria.cycle_id = cat.cycle_id + AND criteria.is_active = 1 + ) AS criteria_total_max_score + FROM center_assessment_types cat + WHERE cat.center_application_id = :center_application_id AND cat.cycle_id = :cycle_id'; + $params = [ + ':center_application_id' => $centerApplicationId, + ':cycle_id' => $cycleId, + ]; + + $search = trim((string) ($filters['search'] ?? '')); + if ($search !== '') { + $query .= ' AND (cat.title LIKE :search1 OR cat.category LIKE :search2)'; + $params[':search1'] = "%{$search}%"; + $params[':search2'] = "%{$search}%"; + } + + $category = trim((string) ($filters['category'] ?? '')); + if ($category !== '') { + $query .= ' AND cat.category = :category'; + $params[':category'] = $category; + } + + $query .= ' ORDER BY cat.is_active DESC, cat.updated_at DESC, cat.id DESC'; + + if ($limit > 0) { + $query .= ' LIMIT ' . (int) $limit . ' OFFSET ' . (int) $offset; + } + + $stmt = $pdo->prepare($query); + $stmt->execute($params); + + return $stmt->fetchAll(); +} + +function count_center_assessments_by_cycle(int $centerApplicationId, int $cycleId, array $filters = []): int +{ + $pdo = db_connection(); + $query = 'SELECT COUNT(*) FROM center_assessment_types WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id'; + $params = [ + ':center_application_id' => $centerApplicationId, + ':cycle_id' => $cycleId, + ]; + + $search = trim((string) ($filters['search'] ?? '')); + if ($search !== '') { + $query .= ' AND (title LIKE :search1 OR category LIKE :search2)'; + $params[':search1'] = "%{$search}%"; + $params[':search2'] = "%{$search}%"; + } + + $category = trim((string) ($filters['category'] ?? '')); + if ($category !== '') { + $query .= ' AND category = :category'; + $params[':category'] = $category; + } + + $stmt = $pdo->prepare($query); + $stmt->execute($params); + + return (int) $stmt->fetchColumn(); +} + +function center_assessment_metrics_by_cycle(int $centerApplicationId, int $cycleId): array +{ + $pdo = db_connection(); + $stmt = $pdo->prepare( + "SELECT + COUNT(*) AS total, + COALESCE(SUM(is_active = 1), 0) AS active_count, + COALESCE(SUM(is_active = 0), 0) AS inactive_count, + COALESCE(SUM(CASE WHEN is_active = 1 THEN weight_percentage ELSE 0 END), 0) AS active_weight, + COALESCE(AVG(max_score), 0) AS average_max_score, + COALESCE(SUM(scale_type = 'percentage'), 0) AS percentage_count, + COALESCE(SUM(scale_type = 'points'), 0) AS points_count, + COALESCE(SUM(scale_type LIKE 'rubric_%'), 0) AS rubric_count + FROM center_assessment_types + WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id" + ); + $stmt->execute([ + ':center_application_id' => $centerApplicationId, + ':cycle_id' => $cycleId, + ]); + $row = $stmt->fetch() ?: []; + + return [ + 'total' => (int) ($row['total'] ?? 0), + 'active' => (int) ($row['active_count'] ?? 0), + 'inactive' => (int) ($row['inactive_count'] ?? 0), + 'active_weight' => (float) ($row['active_weight'] ?? 0), + 'average_max_score' => (float) ($row['average_max_score'] ?? 0), + 'percentage' => (int) ($row['percentage_count'] ?? 0), + 'points' => (int) ($row['points_count'] ?? 0), + 'rubric' => (int) ($row['rubric_count'] ?? 0), + ]; +} + +function update_center_assessment_type_in_cycle(int $centerApplicationId, int $cycleId, int $assessmentId, array $data): bool +{ + $pdo = db_connection(); + $stmt = $pdo->prepare( + 'UPDATE center_assessment_types SET + title = :title, + category = :category, + scale_type = :scale_type, + max_score = :max_score, + weight_percentage = :weight_percentage, + is_active = :is_active, + notes = :notes, + updated_at = NOW() + WHERE id = :id AND center_application_id = :center_application_id AND cycle_id = :cycle_id' + ); + + return $stmt->execute([ + ':id' => $assessmentId, + ':center_application_id' => $centerApplicationId, + ':cycle_id' => $cycleId, + ':title' => $data['title'], + ':category' => $data['category'], + ':scale_type' => $data['scale_type'], + ':max_score' => (float) $data['max_score'], + ':weight_percentage' => (float) $data['weight_percentage'], + ':is_active' => (int) $data['is_active'], + ':notes' => $data['notes'] !== '' ? $data['notes'] : null, + ]); +} + +function center_assessment_type_options_by_cycle(int $centerApplicationId, int $cycleId, bool $onlyActive = false): array +{ + $rows = list_center_assessments_by_cycle($centerApplicationId, $cycleId); + $options = []; + foreach ($rows as $assessment) { + $assessmentId = (int) ($assessment['id'] ?? 0); + if ($assessmentId <= 0) { + continue; + } + + $isActive = (int) ($assessment['is_active'] ?? 0) === 1; + if ($onlyActive && !$isActive) { + continue; + } + + $title = trim((string) ($assessment['title'] ?? '')); + $label = $title !== '' ? $title : 'تقييم غير مسمى'; + $category = trim((string) ($assessment['category'] ?? '')); + if ($category !== '') { + $label .= ' — ' . $category; + } + + $criteriaCount = (int) ($assessment['criteria_count'] ?? 0); + $criteriaTotal = (float) ($assessment['criteria_total_max_score'] ?? 0); + + $options[$assessmentId] = [ + 'id' => $assessmentId, + 'label' => $label, + 'title' => $title, + 'category' => $category, + 'max_score' => (float) ($assessment['max_score'] ?? 0), + 'weight_percentage' => (float) ($assessment['weight_percentage'] ?? 0), + 'criteria_count' => $criteriaCount, + 'criteria_total_max_score' => $criteriaTotal, + 'has_criteria' => $criteriaCount > 0, + 'is_active' => $isActive, + ]; + } + + return $options; +} + +function list_center_assessment_criteria_by_assessment(int $centerApplicationId, int $cycleId, int $assessmentTypeId, bool $onlyActive = false): array +{ + $pdo = db_connection(); + $query = 'SELECT * + FROM center_assessment_criteria + WHERE center_application_id = :center_application_id + AND cycle_id = :cycle_id + AND assessment_type_id = :assessment_type_id'; + $params = [ + ':center_application_id' => $centerApplicationId, + ':cycle_id' => $cycleId, + ':assessment_type_id' => $assessmentTypeId, + ]; + + if ($onlyActive) { + $query .= ' AND is_active = 1'; + } + + $query .= ' ORDER BY sort_order ASC, id ASC'; + $stmt = $pdo->prepare($query); + $stmt->execute($params); + return $stmt->fetchAll(); +} + +function center_assessment_criteria_metrics(int $centerApplicationId, int $cycleId, int $assessmentTypeId): array +{ + $pdo = db_connection(); + $stmt = $pdo->prepare( + "SELECT + COUNT(*) AS total_count, + COALESCE(SUM(is_active = 1), 0) AS active_count, + COALESCE(SUM(CASE WHEN is_active = 1 THEN max_score ELSE 0 END), 0) AS active_max_score + FROM center_assessment_criteria + WHERE center_application_id = :center_application_id + AND cycle_id = :cycle_id + AND assessment_type_id = :assessment_type_id" + ); + $stmt->execute([ + ':center_application_id' => $centerApplicationId, + ':cycle_id' => $cycleId, + ':assessment_type_id' => $assessmentTypeId, + ]); + $row = $stmt->fetch() ?: []; + + return [ + 'total' => (int) ($row['total_count'] ?? 0), + 'active' => (int) ($row['active_count'] ?? 0), + 'active_max_score' => (float) ($row['active_max_score'] ?? 0), + ]; +} + +function validate_center_assessment_criteria_input(int $centerApplicationId, int $cycleId, int $assessmentTypeId, array $input): array +{ + $data = ['criteria' => []]; + $errors = []; + $assessmentOptions = center_assessment_type_options_by_cycle($centerApplicationId, $cycleId, false); + if (!array_key_exists($assessmentTypeId, $assessmentOptions)) { + return [$data, ['form' => 'يرجى اختيار تقييم مركز صحيح من نفس الدورة.']]; + } + + $postedRows = $input['criteria'] ?? []; + if (!is_array($postedRows)) { + $postedRows = []; + } + + $position = 1; + $activeCount = 0; + foreach ($postedRows as $rowKey => $row) { + if (!is_array($row)) { + continue; + } + + $criterionId = (int) ($row['id'] ?? 0); + $title = clean_text((string) ($row['title'] ?? ''), 150); + $maxScoreRaw = str_replace(',', '.', clean_text((string) ($row['max_score'] ?? ''), 30)); + $notes = clean_text((string) ($row['notes'] ?? ''), 500); + $isActive = ((string) ($row['is_active'] ?? '1')) === '1' ? 1 : 0; + + if ($criterionId <= 0 && $title === '' && $maxScoreRaw === '' && $notes === '') { + continue; + } + + $rowErrors = []; + if ($title === '') { + $rowErrors[] = 'اسم البند مطلوب.'; + } + + $maxScore = null; + if ($maxScoreRaw === '' || !is_numeric($maxScoreRaw)) { + $rowErrors[] = 'أدخل درجة رقمية للبند.'; + } else { + $maxScore = round((float) $maxScoreRaw, 2); + if ($maxScore <= 0 || $maxScore > 1000) { + $rowErrors[] = 'درجة البند يجب أن تكون بين 0.01 و1000.'; + } + } + + if ($rowErrors !== []) { + $errors['criteria_' . $rowKey] = implode(' ', $rowErrors); + } + + $data['criteria'][] = [ + 'id' => $criterionId, + 'title' => $title, + 'max_score' => $maxScore !== null ? number_format($maxScore, 2, '.', '') : '', + 'notes' => $notes, + 'is_active' => (string) $isActive, + 'sort_order' => $position, + ]; + + if ($isActive === 1) { + $activeCount++; + } + $position++; + } + + if ($data['criteria'] === []) { + $errors['form'] = 'أضف بند تقييم واحداً على الأقل قبل الحفظ.'; + } elseif ($activeCount === 0) { + $errors['form'] = 'فعّل بنداً واحداً على الأقل ليظهر في رصد تقييم المركز.'; + } + + return [$data, $errors]; +} + +function sync_center_assessment_total_score_from_criteria(int $centerApplicationId, int $cycleId, int $assessmentTypeId): void +{ + $pdo = db_connection(); + $criteria = list_center_assessment_criteria_by_assessment($centerApplicationId, $cycleId, $assessmentTypeId, true); + if ($criteria === []) { + return; + } + + $totalMaxScore = 0.0; + foreach ($criteria as $criterion) { + $totalMaxScore += (float) ($criterion['max_score'] ?? 0); + } + $totalMaxScore = round($totalMaxScore, 2); + + $assessmentStmt = $pdo->prepare( + 'UPDATE center_assessment_types + SET max_score = :max_score, updated_at = NOW() + WHERE id = :id AND center_application_id = :center_application_id AND cycle_id = :cycle_id' + ); + $assessmentStmt->execute([ + ':max_score' => $totalMaxScore, + ':id' => $assessmentTypeId, + ':center_application_id' => $centerApplicationId, + ':cycle_id' => $cycleId, + ]); + + $itemStmt = $pdo->prepare( + 'UPDATE center_assessment_score_items items + INNER JOIN center_assessment_criteria criteria ON criteria.id = items.criterion_id + SET items.max_score = criteria.max_score, + items.updated_at = NOW() + WHERE items.center_application_id = :center_application_id + AND items.cycle_id = :cycle_id + AND items.assessment_type_id = :assessment_type_id' + ); + $itemStmt->execute([ + ':center_application_id' => $centerApplicationId, + ':cycle_id' => $cycleId, + ':assessment_type_id' => $assessmentTypeId, + ]); + + $scoreStmt = $pdo->prepare( + 'UPDATE center_assessment_scores + SET max_score = :max_score, + updated_at = NOW() + WHERE center_application_id = :center_application_id + AND cycle_id = :cycle_id + AND assessment_type_id = :assessment_type_id' + ); + $scoreStmt->execute([ + ':max_score' => $totalMaxScore, + ':center_application_id' => $centerApplicationId, + ':cycle_id' => $cycleId, + ':assessment_type_id' => $assessmentTypeId, + ]); +} + +function save_center_assessment_criteria_in_cycle(int $centerApplicationId, int $cycleId, int $assessmentTypeId, array $data): int +{ + $pdo = db_connection(); + $existingStmt = $pdo->prepare( + 'SELECT id FROM center_assessment_criteria + WHERE center_application_id = :center_application_id + AND cycle_id = :cycle_id + AND assessment_type_id = :assessment_type_id' + ); + $existingStmt->execute([ + ':center_application_id' => $centerApplicationId, + ':cycle_id' => $cycleId, + ':assessment_type_id' => $assessmentTypeId, + ]); + $existingIds = array_map('intval', $existingStmt->fetchAll(PDO::FETCH_COLUMN)); + $existingMap = array_fill_keys($existingIds, true); + + $insertStmt = $pdo->prepare( + 'INSERT INTO center_assessment_criteria ( + center_application_id, cycle_id, assessment_type_id, title, max_score, + sort_order, is_active, notes, created_at, updated_at + ) VALUES ( + :center_application_id, :cycle_id, :assessment_type_id, :title, :max_score, + :sort_order, :is_active, :notes, NOW(), NOW() + )' + ); + $updateStmt = $pdo->prepare( + 'UPDATE center_assessment_criteria + SET title = :title, + max_score = :max_score, + sort_order = :sort_order, + is_active = :is_active, + notes = :notes, + updated_at = NOW() + WHERE id = :id + AND center_application_id = :center_application_id + AND cycle_id = :cycle_id + AND assessment_type_id = :assessment_type_id' + ); + + $saved = 0; + foreach ($data['criteria'] as $criterion) { + $params = [ + ':center_application_id' => $centerApplicationId, + ':cycle_id' => $cycleId, + ':assessment_type_id' => $assessmentTypeId, + ':title' => (string) ($criterion['title'] ?? ''), + ':max_score' => (float) ($criterion['max_score'] ?? 0), + ':sort_order' => (int) ($criterion['sort_order'] ?? 0), + ':is_active' => ((string) ($criterion['is_active'] ?? '1')) === '1' ? 1 : 0, + ':notes' => !empty($criterion['notes']) ? (string) $criterion['notes'] : null, + ]; + + $criterionId = (int) ($criterion['id'] ?? 0); + if ($criterionId > 0 && isset($existingMap[$criterionId])) { + $updateStmt->execute($params + [':id' => $criterionId]); + } else { + $insertStmt->execute($params); + } + $saved++; + } + + sync_center_assessment_total_score_from_criteria($centerApplicationId, $cycleId, $assessmentTypeId); + return $saved; +} + function school_assessment_type_options_by_cycle(int $centerApplicationId, int $cycleId, bool $onlyActive = false): array { $rows = list_school_assessments_by_cycle($centerApplicationId, $cycleId); @@ -1824,6 +2307,473 @@ function school_assessment_score_metrics_by_cycle(int $centerApplicationId, int ]; } + +function center_assessment_normalize_status(string $status): string +{ + return match ($status) { + 'present' => 'completed', + 'absent', 'draft' => 'pending', + 'excused' => 'waived', + 'completed', 'pending', 'waived' => $status, + default => 'pending', + }; +} + +function center_assessment_status_map(): array +{ + return [ + 'completed' => ['label' => 'مكتمل', 'class' => 'status-approved'], + 'pending' => ['label' => 'مؤجل', 'class' => 'status-review'], + 'waived' => ['label' => 'معفى', 'class' => 'status-muted'], + ]; +} + +function center_assessment_status_badge(string $status): string +{ + $normalizedStatus = center_assessment_normalize_status($status); + $map = center_assessment_status_map(); + $meta = $map[$normalizedStatus] ?? ['label' => 'غير محدد', 'class' => 'status-muted']; + return '' . e($meta['label']) . ''; +} + +function validate_center_assessment_score_input(int $centerApplicationId, int $cycleId, array $input): array +{ + $data = [ + 'assessment_type_id' => (string) ((int) ($input['assessment_type_id'] ?? 0)), + 'assessed_on' => clean_text((string) ($input['assessed_on'] ?? date('Y-m-d')), 20), + 'status' => center_assessment_normalize_status(clean_text((string) ($input['status'] ?? 'completed'), 20)), + 'assessment_max_score' => 0.0, + 'has_criteria' => false, + 'criteria' => [], + 'criteria_scores' => [], + 'score' => null, + 'score_raw' => clean_text((string) ($input['score'] ?? ''), 30), + 'notes' => clean_text((string) ($input['notes'] ?? ''), 1000), + 'should_save' => false, + ]; + + $errors = []; + $assessmentOptions = center_assessment_type_options_by_cycle($centerApplicationId, $cycleId, false); + $statusMap = center_assessment_status_map(); + + $assessmentId = (int) $data['assessment_type_id']; + $selectedAssessment = $assessmentOptions[$assessmentId] ?? null; + if ($selectedAssessment === null) { + $errors['assessment_type_id'] = 'يرجى اختيار تقييم مركز صحيح من نفس الدورة.'; + } + + if (!array_key_exists($data['status'], $statusMap)) { + $data['status'] = 'completed'; + } + + if ($data['assessed_on'] === '' || strtotime($data['assessed_on']) === false) { + $errors['assessed_on'] = 'يرجى إدخال تاريخ تقييم صحيح.'; + } + + $criteriaRows = $assessmentId > 0 + ? list_center_assessment_criteria_by_assessment($centerApplicationId, $cycleId, $assessmentId, true) + : []; + $criteriaMap = []; + foreach ($criteriaRows as $criterion) { + $criterionId = (int) ($criterion['id'] ?? 0); + if ($criterionId <= 0) { + continue; + } + $criteriaMap[$criterionId] = $criterion; + $data['assessment_max_score'] += (float) ($criterion['max_score'] ?? 0); + } + $data['assessment_max_score'] = round($data['assessment_max_score'], 2); + $data['has_criteria'] = $criteriaMap !== []; + $data['criteria'] = $criteriaRows; + + if (!$data['has_criteria'] && $selectedAssessment !== null) { + $data['assessment_max_score'] = (float) ($selectedAssessment['max_score'] ?? 0); + } + + if ($data['has_criteria']) { + $postedCriterionScores = $input['criteria'] ?? []; + if (!is_array($postedCriterionScores)) { + $postedCriterionScores = []; + } + + $missingCriteria = []; + $totalScore = 0.0; + $hasCriteriaInput = false; + + foreach ($criteriaMap as $criterionId => $criterion) { + $rawValue = str_replace(',', '.', clean_text((string) ($postedCriterionScores[$criterionId] ?? ''), 30)); + $data['criteria_scores'][$criterionId] = [ + 'criterion_id' => $criterionId, + 'score' => null, + 'score_raw' => $rawValue, + 'max_score' => (float) ($criterion['max_score'] ?? 0), + ]; + + if ($rawValue == '') { + $missingCriteria[] = (string) ($criterion['title'] ?? ''); + continue; + } + + $hasCriteriaInput = true; + if (!is_numeric($rawValue)) { + $errors['score'] = 'كل بند يجب أن يحتوي على درجة رقمية صحيحة.'; + continue; + } + + $criterionScore = round((float) $rawValue, 2); + $criterionMax = (float) ($criterion['max_score'] ?? 0); + if ($criterionScore < 0 || $criterionScore > $criterionMax) { + $errors['score'] = 'درجة كل بند يجب أن تكون بين 0 و ' . rtrim(rtrim(number_format($criterionMax, 2, '.', ''), '0'), '.') . '.'; + continue; + } + + $data['criteria_scores'][$criterionId]['score'] = $criterionScore; + $totalScore += $criterionScore; + } + + $data['should_save'] = $data['status'] !== 'completed' || $hasCriteriaInput || $data['notes'] !== ''; + if ($data['status'] === 'completed' && $data['should_save'] && $missingCriteria !== []) { + $errors['score'] = 'عند اعتماد التقييم كمكتمل يجب تعبئة جميع البنود النشطة.'; + } + + if ($data['status'] === 'completed' && $missingCriteria === [] && !isset($errors['score'])) { + $data['score'] = round($totalScore, 2); + $data['score_raw'] = number_format($data['score'], 2, '.', ''); + } + + if ($data['status'] !== 'completed') { + foreach ($data['criteria_scores'] as $criterionId => $criterionScoreData) { + $data['criteria_scores'][$criterionId]['score'] = null; + $data['criteria_scores'][$criterionId]['score_raw'] = ''; + } + $data['score'] = null; + $data['score_raw'] = ''; + } + } else { + $scoreRaw = str_replace(',', '.', clean_text((string) ($input['score'] ?? ''), 30)); + $data['score_raw'] = $scoreRaw; + $data['should_save'] = $data['status'] !== 'completed' || $scoreRaw !== '' || $data['notes'] !== ''; + + if ($scoreRaw !== '') { + if (!is_numeric($scoreRaw)) { + $errors['score'] = 'الدرجة يجب أن تكون رقماً صحيحاً أو عشرياً.'; + } else { + $data['score'] = round((float) $scoreRaw, 2); + if ($selectedAssessment !== null && ($data['score'] < 0 || $data['score'] > (float) $data['assessment_max_score'])) { + $errors['score'] = 'الدرجة يجب أن تكون بين 0 و ' . rtrim(rtrim(number_format((float) $data['assessment_max_score'], 2, '.', ''), '0'), '.'); + } + } + } + + if ($data['status'] === 'completed' && $data['should_save'] && $scoreRaw === '') { + $errors['score'] = 'أدخل الدرجة أو غيّر الحالة إلى مؤجل أو معفى.'; + } + + if ($data['status'] !== 'completed') { + $data['score'] = null; + $data['score_raw'] = ''; + } + } + + if (!$data['should_save'] && $errors === []) { + $errors['form'] = $data['has_criteria'] + ? 'أدخل درجات البنود أو حدّد حالة التقييم قبل الحفظ.' + : 'أدخل الدرجة أو حدّد حالة التقييم قبل الحفظ.'; + } + + return [$data, $errors, $selectedAssessment]; +} + +function save_center_assessment_score_in_cycle(int $centerApplicationId, int $cycleId, array $data): int +{ + $pdo = db_connection(); + $criteriaRows = !empty($data['has_criteria']) + ? list_center_assessment_criteria_by_assessment($centerApplicationId, $cycleId, (int) $data['assessment_type_id'], true) + : []; + $criteriaMap = []; + foreach ($criteriaRows as $criterion) { + $criteriaId = (int) ($criterion['id'] ?? 0); + if ($criteriaId > 0) { + $criteriaMap[$criteriaId] = $criterion; + } + } + + $stmt = $pdo->prepare( + 'INSERT INTO center_assessment_scores ( + center_application_id, cycle_id, assessment_type_id, + score, max_score, status, notes, assessed_on, created_at, updated_at + ) VALUES ( + :center_application_id, :cycle_id, :assessment_type_id, + :score, :max_score, :status, :notes, :assessed_on, NOW(), NOW() + ) + ON DUPLICATE KEY UPDATE + id = LAST_INSERT_ID(id), + score = VALUES(score), + max_score = VALUES(max_score), + status = VALUES(status), + notes = VALUES(notes), + assessed_on = VALUES(assessed_on), + updated_at = NOW()' + ); + $deleteItemsStmt = $pdo->prepare('DELETE FROM center_assessment_score_items WHERE assessment_score_id = :assessment_score_id'); + $itemStmt = $pdo->prepare( + 'INSERT INTO center_assessment_score_items ( + center_application_id, cycle_id, assessment_score_id, assessment_type_id, criterion_id, + score, max_score, created_at, updated_at + ) VALUES ( + :center_application_id, :cycle_id, :assessment_score_id, :assessment_type_id, :criterion_id, + :score, :max_score, NOW(), NOW() + )' + ); + + $stmt->execute([ + ':center_application_id' => $centerApplicationId, + ':cycle_id' => $cycleId, + ':assessment_type_id' => (int) $data['assessment_type_id'], + ':score' => $data['score'], + ':max_score' => (float) ($data['assessment_max_score'] ?? 0), + ':status' => center_assessment_normalize_status((string) ($data['status'] ?? 'completed')), + ':notes' => $data['notes'] !== '' ? $data['notes'] : null, + ':assessed_on' => $data['assessed_on'], + ]); + + $assessmentScoreId = (int) $pdo->lastInsertId(); + if ($assessmentScoreId > 0) { + $deleteItemsStmt->execute([':assessment_score_id' => $assessmentScoreId]); + if ($criteriaMap !== [] && center_assessment_normalize_status((string) ($data['status'] ?? 'completed')) === 'completed') { + foreach ($data['criteria_scores'] as $criterionId => $criterionScoreData) { + if (!array_key_exists((int) $criterionId, $criteriaMap) || ($criterionScoreData['score'] ?? null) === null) { + continue; + } + + $itemStmt->execute([ + ':center_application_id' => $centerApplicationId, + ':cycle_id' => $cycleId, + ':assessment_score_id' => $assessmentScoreId, + ':assessment_type_id' => (int) $data['assessment_type_id'], + ':criterion_id' => (int) $criterionId, + ':score' => (float) $criterionScoreData['score'], + ':max_score' => (float) ($criteriaMap[(int) $criterionId]['max_score'] ?? 0), + ]); + } + } + } + + return 1; +} + +function center_assessment_score_by_assessment(int $centerApplicationId, int $cycleId, int $assessmentTypeId): ?array +{ + $pdo = db_connection(); + $stmt = $pdo->prepare( + 'SELECT scores.* + FROM center_assessment_scores scores + WHERE scores.center_application_id = :center_application_id + AND scores.cycle_id = :cycle_id + AND scores.assessment_type_id = :assessment_type_id + LIMIT 1' + ); + $stmt->execute([ + ':center_application_id' => $centerApplicationId, + ':cycle_id' => $cycleId, + ':assessment_type_id' => $assessmentTypeId, + ]); + $row = $stmt->fetch(); + + return $row ?: null; +} + +function list_center_assessment_score_items_by_assessment(int $centerApplicationId, int $cycleId, int $assessmentTypeId): array +{ + $pdo = db_connection(); + $stmt = $pdo->prepare( + 'SELECT items.*, criteria.title AS criterion_title + FROM center_assessment_score_items items + LEFT JOIN center_assessment_criteria criteria ON criteria.id = items.criterion_id + WHERE items.center_application_id = :center_application_id + AND items.cycle_id = :cycle_id + AND items.assessment_type_id = :assessment_type_id + ORDER BY items.id ASC' + ); + $stmt->execute([ + ':center_application_id' => $centerApplicationId, + ':cycle_id' => $cycleId, + ':assessment_type_id' => $assessmentTypeId, + ]); + + return $stmt->fetchAll(); +} + +function center_assessment_score_bundle_by_assessment(int $centerApplicationId, int $cycleId, int $assessmentTypeId): array +{ + $score = center_assessment_score_by_assessment($centerApplicationId, $cycleId, $assessmentTypeId); + $criteriaScores = []; + foreach (list_center_assessment_score_items_by_assessment($centerApplicationId, $cycleId, $assessmentTypeId) as $item) { + $criterionId = (int) ($item['criterion_id'] ?? 0); + if ($criterionId <= 0) { + continue; + } + $criteriaScores[$criterionId] = $item; + } + + return [ + 'score' => $score, + 'criteria_scores' => $criteriaScores, + ]; +} + +function center_assessment_score_metrics_by_cycle(int $centerApplicationId, int $cycleId, int $assessmentTypeId = 0): array +{ + $pdo = db_connection(); + $query = "SELECT + COUNT(*) AS total, + COALESCE(SUM(status IN ('completed', 'present')), 0) AS completed_count, + COALESCE(SUM(status IN ('pending', 'absent', 'draft')), 0) AS pending_count, + COALESCE(SUM(status IN ('waived', 'excused')), 0) AS waived_count, + COALESCE(AVG(CASE WHEN status IN ('completed', 'present') THEN score END), 0) AS average_score, + MAX(assessed_on) AS latest_date + FROM center_assessment_scores + WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id"; + $params = [ + ':center_application_id' => $centerApplicationId, + ':cycle_id' => $cycleId, + ]; + + if ($assessmentTypeId > 0) { + $query .= ' AND assessment_type_id = :assessment_type_id'; + $params[':assessment_type_id'] = $assessmentTypeId; + } + + $stmt = $pdo->prepare($query); + $stmt->execute($params); + $row = $stmt->fetch() ?: []; + + return [ + 'total' => (int) ($row['total'] ?? 0), + 'completed' => (int) ($row['completed_count'] ?? 0), + 'pending' => (int) ($row['pending_count'] ?? 0), + 'waived' => (int) ($row['waived_count'] ?? 0), + 'average_score' => (float) ($row['average_score'] ?? 0), + 'latest_date' => (string) ($row['latest_date'] ?? ''), + ]; +} + +function center_assessment_summary_by_cycle(int $centerApplicationId, int $cycleId): array +{ + $summary = [ + 'assessments' => [], + 'total_assessments' => 0, + 'active_assessments' => 0, + 'recorded_assessments' => 0, + 'completed_assessments' => 0, + 'pending_assessments' => 0, + 'waived_assessments' => 0, + 'missing_assessments' => 0, + 'overall_percentage' => 0.0, + 'score_total' => 0.0, + 'max_score_total' => 0.0, + 'latest_assessed_on' => '', + 'performance' => student_certificate_performance_meta(0.0), + ]; + + $pdo = db_connection(); + $stmt = $pdo->prepare( + 'SELECT + assessments.*, + scores.id AS score_id, + scores.score, + scores.max_score AS saved_max_score, + scores.status AS score_status, + scores.notes AS score_notes, + scores.assessed_on, + ( + SELECT COUNT(*) + FROM center_assessment_criteria criteria + WHERE criteria.center_application_id = assessments.center_application_id + AND criteria.cycle_id = assessments.cycle_id + AND criteria.assessment_type_id = assessments.id + AND criteria.is_active = 1 + ) AS criteria_count + FROM center_assessment_types assessments + LEFT JOIN center_assessment_scores scores + ON scores.center_application_id = assessments.center_application_id + AND scores.cycle_id = assessments.cycle_id + AND scores.assessment_type_id = assessments.id + WHERE assessments.center_application_id = :center_application_id + AND assessments.cycle_id = :cycle_id + ORDER BY assessments.is_active DESC, assessments.updated_at DESC, assessments.id DESC' + ); + $stmt->execute([ + ':center_application_id' => $centerApplicationId, + ':cycle_id' => $cycleId, + ]); + $rows = $stmt->fetchAll(); + + foreach ($rows as $row) { + $summary['total_assessments']++; + $isActive = (int) ($row['is_active'] ?? 0) === 1; + if ($isActive) { + $summary['active_assessments']++; + } + + $hasSavedScore = !empty($row['score_id']); + $status = $hasSavedScore ? center_assessment_normalize_status((string) ($row['score_status'] ?? 'pending')) : 'missing'; + $score = isset($row['score']) ? (float) $row['score'] : null; + $maxScore = $hasSavedScore && (float) ($row['saved_max_score'] ?? 0) > 0 + ? (float) ($row['saved_max_score'] ?? 0) + : (float) ($row['max_score'] ?? 0); + $percentage = ($status === 'completed' && $score !== null && $maxScore > 0) + ? round(($score / $maxScore) * 100, 2) + : null; + + if ($hasSavedScore) { + $summary['recorded_assessments']++; + if ($status === 'completed' && $score !== null && $maxScore > 0) { + $summary['completed_assessments']++; + $summary['score_total'] += $score; + $summary['max_score_total'] += $maxScore; + } elseif ($status === 'pending') { + $summary['pending_assessments']++; + } elseif ($status === 'waived') { + $summary['waived_assessments']++; + } + + $assessedOn = (string) ($row['assessed_on'] ?? ''); + if ($assessedOn !== '' && ($summary['latest_assessed_on'] === '' || strtotime($assessedOn) > strtotime($summary['latest_assessed_on']))) { + $summary['latest_assessed_on'] = $assessedOn; + } + } elseif ($isActive) { + $summary['missing_assessments']++; + } + + $summary['assessments'][] = [ + 'id' => (int) ($row['id'] ?? 0), + 'title' => (string) ($row['title'] ?? ''), + 'category' => (string) ($row['category'] ?? ''), + 'scale_type' => (string) ($row['scale_type'] ?? ''), + 'weight_percentage' => (float) ($row['weight_percentage'] ?? 0), + 'max_score' => (float) ($row['max_score'] ?? 0), + 'score' => $score, + 'saved_max_score' => $maxScore, + 'status' => $status, + 'status_label' => $status === 'missing' ? 'غير مرصود' : (center_assessment_status_map()[$status]['label'] ?? 'غير محدد'), + 'notes' => (string) ($row['score_notes'] ?? ''), + 'assessed_on' => (string) ($row['assessed_on'] ?? ''), + 'criteria_count' => (int) ($row['criteria_count'] ?? 0), + 'is_active' => $isActive, + 'percentage' => $percentage, + 'has_saved_score' => $hasSavedScore, + ]; + } + + if ($summary['max_score_total'] > 0) { + $summary['overall_percentage'] = round(($summary['score_total'] / $summary['max_score_total']) * 100, 2); + } + $summary['performance'] = student_certificate_performance_meta((float) $summary['overall_percentage']); + + return $summary; +} + function school_student_options_by_cycle(int $centerApplicationId, int $cycleId): array { $students = list_school_students_by_cycle($centerApplicationId, $cycleId);