Autosave: 20260417-073012
This commit is contained in:
parent
b80640c59c
commit
7937c54a84
@ -87,6 +87,11 @@ render_flash($flash);
|
||||
<p>للوصول إلى المراكز الجاهزة للتشغيل ثم الانتقال إلى الطلاب والمعلمين والتقييمات والحضور لكل مركز.</p>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="applications.php?status=approved">فتح المراكز المعتمدة</a>
|
||||
</article>
|
||||
<article class="module-item">
|
||||
<h2>تقييم المراكز</h2>
|
||||
<p>إعداد تقييمات إشرافية للمراكز المعتمدة داخل كل دورة موسمية، تمهيداً لإضافة البنود والرصد بنفس نمط الطلاب.</p>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="center_assessments.php">فتح تقييم المراكز</a>
|
||||
</article>
|
||||
<article class="module-item">
|
||||
<h2>هيكل النظام</h2>
|
||||
<p>مرجع سريع لفهم تنظيم الصفحات الحالية ومسار التطوير الإداري داخل التطبيق.</p>
|
||||
@ -107,6 +112,7 @@ render_flash($flash);
|
||||
<div class="quick-link-stack">
|
||||
<a class="quick-link-item" href="applications.php?status=submitted"><div><strong>طلبات جديدة</strong><span>ابدأ مباشرة بالطلبات التي لم تُراجع بعد.</span></div></a>
|
||||
<a class="quick-link-item" href="applications.php?status=under_review"><div><strong>طلبات تحت المراجعة</strong><span>تابع الملفات المفتوحة حالياً حتى قرار نهائي.</span></div></a>
|
||||
<a class="quick-link-item" href="center_assessments.php"><div><strong>تقييم المراكز</strong><span>ابدأ بإعداد تقييمات إشرافية للمراكز المعتمدة حسب الدورة.</span></div></a>
|
||||
<a class="quick-link-item" href="center_application.php"><div><strong>فتح طلب جديد</strong><span>اختبار أو إنشاء طلب جديد من نموذج التقديم.</span></div></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
374
center_assessment_criteria.php
Normal file
374
center_assessment_criteria.php
Normal file
@ -0,0 +1,374 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
$flash = consume_flash();
|
||||
$applicationId = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT) ?: 0;
|
||||
$requestedCycleId = filter_input(INPUT_GET, 'cycle', FILTER_VALIDATE_INT) ?: 0;
|
||||
$requestedAssessmentId = filter_input(INPUT_GET, 'assessment_id', FILTER_VALIDATE_INT) ?: 0;
|
||||
$application = $applicationId > 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);
|
||||
?>
|
||||
<section class="py-4 py-lg-5">
|
||||
<div class="container-xxl">
|
||||
<div class="admin-layout row g-4 align-items-start">
|
||||
<div class="col-lg-3 layout-sidebar-column">
|
||||
<?php require __DIR__ . '/includes/sidebar.php'; ?>
|
||||
</div>
|
||||
<div class="col-lg-9 layout-content-column">
|
||||
<?php if (!$application): ?>
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">المركز غير موجود</div>
|
||||
<p class="text-muted mb-3">تحقق من الرابط أو ارجع إلى صفحة تقييم المراكز.</p>
|
||||
<a class="btn btn-primary" href="center_assessments.php">تقييم المراكز</a>
|
||||
</div>
|
||||
<?php elseif (!$isApprovedCenter): ?>
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">البنود تُفتح بعد الاعتماد</div>
|
||||
<p class="text-muted mb-0">اعتمد المركز أولاً حتى تتمكن من إعداد بنود تقييمه.</p>
|
||||
</div>
|
||||
<?php elseif ($selectedCycleId <= 0): ?>
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">أنشئ دورة أولاً</div>
|
||||
<p class="text-muted mb-3">يجب اختيار دورة موسمية للمركز قبل إعداد بنود التقييم.</p>
|
||||
<a class="btn btn-primary" href="<?= e($assessmentsUrl) ?>">الرجوع إلى تقييمات المركز</a>
|
||||
</div>
|
||||
<?php elseif ($selectedAssessment === null): ?>
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">اختر تقييماً أولاً</div>
|
||||
<p class="text-muted mb-3">ابدأ من صفحة تقييمات المراكز ثم افتح البنود للتقييم المطلوب.</p>
|
||||
<a class="btn btn-primary" href="<?= e($assessmentsUrl) ?>">الرجوع إلى تقييمات المركز</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="page-banner mb-4 mb-lg-5">
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-8">
|
||||
<span class="eyebrow mb-3">مرحلة 2 — بنود تقييم المراكز</span>
|
||||
<h1 class="page-title mb-3"><?= e((string) ($selectedAssessment['title'] ?? '')) ?></h1>
|
||||
<p class="page-copy mb-3">أضف البنود التفصيلية التي سيُبنى عليها تقييم المركز، مثل <strong>الالتزام</strong> و<strong>جودة التنفيذ</strong> و<strong>التقارير</strong>. مجموع البنود النشطة سيُحدِّث الدرجة النهائية لهذا التقييم تلقائياً.</p>
|
||||
<div class="hero-meta">
|
||||
<span><?= e((string) ($application['center_name'] ?? '')) ?></span>
|
||||
<span><?= e($cycleLabel) ?></span>
|
||||
<span><?= e((string) ($selectedAssessment['category'] ?? '')) ?></span>
|
||||
<span>الوزن <?= e(rtrim(rtrim(number_format((float) ($selectedAssessment['weight_percentage'] ?? 0), 2, '.', ''), '0'), '.')) ?>%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="page-banner-panel h-100">
|
||||
<div class="mini-stat-label">التقييم المحدد</div>
|
||||
<div class="mini-stat-value"><?= e((string) $criteriaMetrics['active']) ?></div>
|
||||
<div class="mini-stat-copy mb-3">عدد البنود النشطة حالياً داخل هذا التقييم.</div>
|
||||
<div class="d-grid gap-2">
|
||||
<a class="btn btn-primary btn-sm" href="<?= e($assessmentsUrl) ?>">الرجوع إلى التقييمات</a>
|
||||
<?php if (!$isCycleReadOnly): ?>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" id="addCriterionRow">إضافة بند</button>
|
||||
<?php endif; ?>
|
||||
<span class="small text-muted">الدرجة الحالية لهذا التقييم: <strong><?= e(rtrim(rtrim(number_format((float) ($selectedAssessment['max_score'] ?? 0), 2, '.', ''), '0'), '.')) ?></strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4"><div class="app-card stat-tile"><div class="mini-stat-label">إجمالي البنود</div><div class="mini-stat-value"><?= e((string) $criteriaMetrics['total']) ?></div><div class="mini-stat-copy">كل البنود المحفوظة لهذا التقييم.</div></div></div>
|
||||
<div class="col-md-4"><div class="app-card stat-tile"><div class="mini-stat-label">البنود النشطة</div><div class="mini-stat-value"><?= e((string) $criteriaMetrics['active']) ?></div><div class="mini-stat-copy">هي التي تدخل في الرصد النهائي.</div></div></div>
|
||||
<div class="col-md-4"><div class="app-card stat-tile"><div class="mini-stat-label">مجموع البنود النشطة</div><div class="mini-stat-value"><?= e(rtrim(rtrim(number_format((float) $criteriaMetrics['active_max_score'], 2, '.', ''), '0'), '.')) ?></div><div class="mini-stat-copy">يحدّث الدرجة القصوى للتقييم تلقائياً.</div></div></div>
|
||||
</div>
|
||||
|
||||
<div class="app-card mb-4">
|
||||
<form method="get" class="row g-3 align-items-end">
|
||||
<input type="hidden" name="id" value="<?= e((string) $application['id']) ?>">
|
||||
<input type="hidden" name="cycle" value="<?= e((string) $selectedCycleId) ?>">
|
||||
<div class="col-lg-8">
|
||||
<label class="form-label" for="assessmentSelect">التقييم</label>
|
||||
<select class="form-select" id="assessmentSelect" name="assessment_id" onchange="this.form.submit()">
|
||||
<?php foreach ($assessmentOptions as $assessmentOption): ?>
|
||||
<option value="<?= e((string) ($assessmentOption['id'] ?? 0)) ?>" <?= (int) ($assessmentOption['id'] ?? 0) === $selectedAssessmentId ? 'selected' : '' ?>><?= e((string) ($assessmentOption['label'] ?? '')) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="section-subtle mb-2">الحالة</div>
|
||||
<div><?= assessment_active_badge((int) ($selectedAssessment['is_active'] ?? 0)) ?></div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($errors['form'])): ?>
|
||||
<div class="alert alert-danger mb-4"><?= e($errors['form']) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($isCycleReadOnly): ?>
|
||||
<div class="alert alert-warning mb-4">هذه الدورة مؤرشفة، لذلك الصفحة معروضة للقراءة فقط.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="app-card">
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-4">
|
||||
<div>
|
||||
<div class="section-title mb-1">بنود تقييم المركز</div>
|
||||
<div class="section-subtle">يمكنك إضافة بنود جديدة أو إخفاء بند قديم دون حذفه من السجل.</div>
|
||||
</div>
|
||||
<?php if (!$isCycleReadOnly): ?>
|
||||
<button type="button" class="btn btn-outline-secondary" id="addCriterionRowSecondary">إضافة بند</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<form method="post" novalidate>
|
||||
<input type="hidden" name="assessment_id" value="<?= e((string) $selectedAssessmentId) ?>">
|
||||
<div class="table-responsive">
|
||||
<table class="table app-table align-middle" id="criteriaTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 32%">اسم البند</th>
|
||||
<th style="width: 18%">الدرجة</th>
|
||||
<th style="width: 30%">ملاحظات</th>
|
||||
<th style="width: 12%">الحالة</th>
|
||||
<th style="width: 8%">الإجراء</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($values['criteria'] as $index => $criterion): ?>
|
||||
<tr data-criterion-row data-existing="<?= !empty($criterion['id']) ? '1' : '0' ?>">
|
||||
<td>
|
||||
<input type="hidden" name="criteria[<?= e((string) $index) ?>][id]" value="<?= e((string) ($criterion['id'] ?? 0)) ?>">
|
||||
<input class="form-control" type="text" name="criteria[<?= e((string) $index) ?>][title]" value="<?= e((string) ($criterion['title'] ?? '')) ?>" placeholder="مثال: الالتزام التشغيلي" <?= $isCycleReadOnly ? 'disabled' : '' ?>>
|
||||
<?php if (isset($errors['criteria_' . $index])): ?>
|
||||
<div class="text-danger small mt-1"><?= e($errors['criteria_' . $index]) ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<input class="form-control" type="number" step="0.01" min="0" max="1000" name="criteria[<?= e((string) $index) ?>][max_score]" value="<?= e((string) ($criterion['max_score'] ?? '')) ?>" placeholder="10" <?= $isCycleReadOnly ? 'disabled' : '' ?>>
|
||||
</td>
|
||||
<td>
|
||||
<input class="form-control" type="text" name="criteria[<?= e((string) $index) ?>][notes]" value="<?= e((string) ($criterion['notes'] ?? '')) ?>" placeholder="اختياري" <?= $isCycleReadOnly ? 'disabled' : '' ?>>
|
||||
</td>
|
||||
<td>
|
||||
<select class="form-select" name="criteria[<?= e((string) $index) ?>][is_active]" <?= $isCycleReadOnly ? 'disabled' : '' ?>>
|
||||
<option value="1" <?= (string) ($criterion['is_active'] ?? '1') === '1' ? 'selected' : '' ?>>نشط</option>
|
||||
<option value="0" <?= (string) ($criterion['is_active'] ?? '1') === '0' ? 'selected' : '' ?>>مخفي</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($isCycleReadOnly): ?>
|
||||
<span class="text-muted small">قراءة فقط</span>
|
||||
<?php elseif (!empty($criterion['id'])): ?>
|
||||
<span class="text-muted small">أوقفه</span>
|
||||
<?php else: ?>
|
||||
<button type="button" class="btn btn-sm btn-light icon-action" data-remove-row title="حذف البند" aria-label="حذف البند">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0A.5.5 0 0 1 8.5 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z"/><path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1 0-2H5.5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1H13.5a1 1 0 0 1 1 1ZM4 4v9a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4H4Zm2.5-2a.5.5 0 0 0-.5.5V3h4v-.5a.5.5 0 0 0-.5-.5h-3Z"/></svg>
|
||||
<span class="visually-hidden">حذف البند</span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php if (!$isCycleReadOnly): ?>
|
||||
<div class="d-flex justify-content-between align-items-center pt-3 border-top mt-3 flex-wrap gap-2">
|
||||
<div class="section-subtle">سيتم تحديث الدرجة النهائية لهذا التقييم تلقائياً إلى مجموع البنود النشطة.</div>
|
||||
<button class="btn btn-primary" type="submit">حفظ البنود</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php if ($application && !$isCycleReadOnly && $selectedAssessment): ?>
|
||||
<template id="criterionRowTemplate">
|
||||
<tr data-criterion-row data-existing="0">
|
||||
<td>
|
||||
<input type="hidden" data-row-field="id" value="0">
|
||||
<input class="form-control" type="text" data-row-field="title" placeholder="مثال: الالتزام التشغيلي">
|
||||
</td>
|
||||
<td>
|
||||
<input class="form-control" type="number" step="0.01" min="0" max="1000" data-row-field="max_score" placeholder="10">
|
||||
</td>
|
||||
<td>
|
||||
<input class="form-control" type="text" data-row-field="notes" placeholder="اختياري">
|
||||
</td>
|
||||
<td>
|
||||
<select class="form-select" data-row-field="is_active">
|
||||
<option value="1" selected>نشط</option>
|
||||
<option value="0">مخفي</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-light icon-action" data-remove-row title="حذف البند" aria-label="حذف البند">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0A.5.5 0 0 1 8.5 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z"/><path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1 0-2H5.5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1H13.5a1 1 0 0 1 1 1ZM4 4v9a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4H4Zm2.5-2a.5.5 0 0 0-.5.5V3h4v-.5a.5.5 0 0 0-.5-.5h-3Z"/></svg>
|
||||
<span class="visually-hidden">حذف البند</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const buttons = [document.getElementById('addCriterionRow'), document.getElementById('addCriterionRowSecondary')].filter(Boolean);
|
||||
const tableBody = document.querySelector('#criteriaTable tbody');
|
||||
const template = document.getElementById('criterionRowTemplate');
|
||||
if (buttons.length === 0 || !tableBody || !template) return;
|
||||
|
||||
const wireRow = (row) => {
|
||||
const removeButton = row.querySelector('[data-remove-row]');
|
||||
if (removeButton) {
|
||||
removeButton.addEventListener('click', () => row.remove());
|
||||
}
|
||||
};
|
||||
|
||||
tableBody.querySelectorAll('[data-criterion-row]').forEach(wireRow);
|
||||
|
||||
const renumberRows = () => {
|
||||
Array.from(tableBody.querySelectorAll('[data-criterion-row]')).forEach((row, index) => {
|
||||
row.querySelectorAll('[data-row-field]').forEach((field) => {
|
||||
const key = field.getAttribute('data-row-field');
|
||||
field.name = `criteria[${index}][${key}]`;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
buttons.forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const fragment = template.content.cloneNode(true);
|
||||
tableBody.appendChild(fragment);
|
||||
wireRow(tableBody.lastElementChild);
|
||||
renumberRows();
|
||||
});
|
||||
});
|
||||
|
||||
renumberRows();
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
<?php render_page_end();
|
||||
228
center_assessment_report.php
Normal file
228
center_assessment_report.php
Normal file
@ -0,0 +1,228 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
function center_report_number(float $value): string
|
||||
{
|
||||
return rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.');
|
||||
}
|
||||
|
||||
$flash = consume_flash();
|
||||
$applicationId = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT) ?: 0;
|
||||
$requestedCycleId = filter_input(INPUT_GET, 'cycle', FILTER_VALIDATE_INT) ?: 0;
|
||||
$application = $applicationId > 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);
|
||||
?>
|
||||
<section class="py-4 py-lg-5">
|
||||
<div class="container-xxl">
|
||||
<div class="admin-layout row g-4 align-items-start">
|
||||
<div class="col-lg-3 layout-sidebar-column">
|
||||
<?php require __DIR__ . '/includes/sidebar.php'; ?>
|
||||
</div>
|
||||
<div class="col-lg-9 layout-content-column">
|
||||
<?php if (!$application): ?>
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">المركز غير موجود</div>
|
||||
<p class="text-muted mb-3">تحقق من الرابط أو ارجع إلى قائمة المراكز المعتمدة.</p>
|
||||
<a class="btn btn-primary" href="applications.php?status=approved">المراكز المعتمدة</a>
|
||||
</div>
|
||||
<?php elseif (!$isApprovedCenter): ?>
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">التقرير يُفتح بعد الاعتماد</div>
|
||||
<p class="text-muted mb-3">اعتمد المركز أولاً حتى يظهر تقرير التقييم الإشرافي.</p>
|
||||
<a class="btn btn-outline-secondary" href="application_detail.php?id=<?= e((string) $application['id']) ?>">ملف الاعتماد</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="page-banner approved-hero mb-4">
|
||||
<div class="row g-4 align-items-center">
|
||||
<div class="col-lg-8">
|
||||
<span class="approved-kicker mb-3">تقرير الدورة</span>
|
||||
<h1 class="page-title mb-3">ملخص تقييم المركز</h1>
|
||||
<p class="page-copy mb-3">هذا التقرير يجمع حالة كل تقييم إشرافي للمركز داخل دورة <strong><?= e($cycleLabel) ?></strong> ويعرض النسبة الكلية الحالية.</p>
|
||||
<div class="hero-meta">
|
||||
<span><?= e((string) ($summary['active_assessments'] ?? 0)) ?> تقييمات نشطة</span>
|
||||
<span><?= e(center_report_number((float) ($summary['overall_percentage'] ?? 0))) ?>%</span>
|
||||
<span><?= e((string) (($summary['performance']['label_ar'] ?? ''))) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="app-card h-100">
|
||||
<div class="section-title mb-2">إجراءات سريعة</div>
|
||||
<div class="cta-stack">
|
||||
<a class="btn btn-outline-secondary" href="<?= e($assessmentsUrl) ?>">إدارة التقييمات</a>
|
||||
<?php if (!empty($summary['assessments'][0]['id'])): ?>
|
||||
<a class="btn btn-primary" href="<?= e($buildCenterAssessmentScoreUrl((int) $application['id'], $selectedCycleId, (int) $summary['assessments'][0]['id'])) ?>">فتح أول رصد</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($selectedCycleId <= 0): ?>
|
||||
<div class="app-card"><div class="alert alert-warning mb-0">لا توجد دورة متاحة لهذا المركز بعد. أنشئ دورة أولاً من صفحة المركز.</div></div>
|
||||
<?php elseif ($summary['assessments'] === []): ?>
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">لا توجد تقييمات لعرض التقرير</div>
|
||||
<p class="text-muted mb-3">أضف تقييمات مركز أولاً، ثم استخدم الرصد لإظهار النتيجة هنا.</p>
|
||||
<a class="btn btn-primary" href="<?= e($assessmentsUrl) ?>">إضافة تقييم</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-3"><div class="app-card h-100"><div class="section-title mb-2">النسبة الكلية</div><div class="display-6 mb-1"><?= e(center_report_number((float) ($summary['overall_percentage'] ?? 0))) ?>%</div><div class="section-subtle"><?= e((string) (($summary['performance']['label_ar'] ?? ''))) ?></div></div></div>
|
||||
<div class="col-md-3"><div class="app-card h-100"><div class="section-title mb-2">المرصود</div><div class="display-6 mb-1"><?= e((string) ($summary['recorded_assessments'] ?? 0)) ?></div><div class="section-subtle">من <?= e((string) ($summary['active_assessments'] ?? 0)) ?> تقييم نشط</div></div></div>
|
||||
<div class="col-md-3"><div class="app-card h-100"><div class="section-title mb-2">المكتمل</div><div class="display-6 mb-1"><?= e((string) ($summary['completed_assessments'] ?? 0)) ?></div><div class="section-subtle">المؤجل <?= e((string) ($summary['pending_assessments'] ?? 0)) ?></div></div></div>
|
||||
<div class="col-md-3"><div class="app-card h-100"><div class="section-title mb-2">آخر تحديث</div><div class="display-6 mb-1" style="font-size:1.05rem;"><?= e((string) (($summary['latest_assessed_on'] ?: '—'))) ?></div><div class="section-subtle">آخر تاريخ رصد</div></div></div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-light border mb-4">
|
||||
<strong>طريقة الحساب الحالية:</strong>
|
||||
مجموع الدرجات المكتملة <?= e(center_report_number((float) ($summary['score_total'] ?? 0))) ?>
|
||||
من أصل <?= e(center_report_number((float) ($summary['max_score_total'] ?? 0))) ?>
|
||||
للتقييمات المكتملة فقط.
|
||||
</div>
|
||||
|
||||
<div class="app-card">
|
||||
<div class="table-responsive">
|
||||
<table class="table app-table align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>التقييم</th>
|
||||
<th>الحالة</th>
|
||||
<th>النتيجة</th>
|
||||
<th>النسبة</th>
|
||||
<th>البنود</th>
|
||||
<th>آخر رصد</th>
|
||||
<th>الإجراء</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($summary['assessments'] as $assessment): ?>
|
||||
<?php
|
||||
$assessmentId = (int) ($assessment['id'] ?? 0);
|
||||
$scoreText = '—';
|
||||
if (($assessment['status'] ?? '') === 'completed' && isset($assessment['score']) && $assessment['score'] !== null) {
|
||||
$scoreText = center_report_number((float) $assessment['score']) . ' / ' . center_report_number((float) ($assessment['saved_max_score'] ?? 0));
|
||||
} elseif (($assessment['status'] ?? '') === 'missing') {
|
||||
$scoreText = 'غير مرصود';
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-semibold"><?= e((string) ($assessment['title'] ?? '')) ?></div>
|
||||
<div class="text-muted small"><?= e((string) ($assessment['category'] ?? '')) ?> — <?= e(center_report_number((float) ($assessment['weight_percentage'] ?? 0))) ?>٪</div>
|
||||
</td>
|
||||
<td>
|
||||
<?php if (($assessment['status'] ?? '') === 'missing'): ?>
|
||||
<span class="text-muted">غير مرصود</span>
|
||||
<?php else: ?>
|
||||
<?= center_assessment_status_badge((string) ($assessment['status'] ?? 'pending')) ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= e($scoreText) ?></td>
|
||||
<td><?= e(isset($assessment['percentage']) && $assessment['percentage'] !== null ? center_report_number((float) $assessment['percentage']) . '%' : '—') ?></td>
|
||||
<td><?= e((string) ($assessment['criteria_count'] ?? 0)) ?></td>
|
||||
<td>
|
||||
<div><?= e((string) (($assessment['assessed_on'] ?? '') !== '' ? $assessment['assessed_on'] : '—')) ?></div>
|
||||
<?php if (!empty($assessment['notes'])): ?><div class="text-muted small"><?= e((string) $assessment['notes']) ?></div><?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a class="btn btn-primary btn-sm" href="<?= e($buildCenterAssessmentScoreUrl((int) $application['id'], $selectedCycleId, $assessmentId)) ?>">رصد</a>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="<?= e($buildCenterAssessmentCriteriaUrl((int) $application['id'], $selectedCycleId, $assessmentId)) ?>">البنود</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php render_page_end();
|
||||
495
center_assessment_score_sheet.php
Normal file
495
center_assessment_score_sheet.php
Normal file
@ -0,0 +1,495 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
function center_score_display(?float $value): string
|
||||
{
|
||||
if ($value === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.');
|
||||
}
|
||||
|
||||
$flash = consume_flash();
|
||||
$applicationId = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT) ?: 0;
|
||||
$requestedCycleId = filter_input(INPUT_GET, 'cycle', FILTER_VALIDATE_INT) ?: 0;
|
||||
$requestedAssessmentId = filter_input(INPUT_GET, 'assessment_id', FILTER_VALIDATE_INT) ?: 0;
|
||||
$application = $applicationId > 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);
|
||||
?>
|
||||
<section class="py-4 py-lg-5">
|
||||
<div class="container-xxl">
|
||||
<div class="admin-layout row g-4 align-items-start">
|
||||
<div class="col-lg-3 layout-sidebar-column">
|
||||
<?php require __DIR__ . '/includes/sidebar.php'; ?>
|
||||
</div>
|
||||
<div class="col-lg-9 layout-content-column">
|
||||
<?php if (!$application): ?>
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">المركز غير موجود</div>
|
||||
<p class="text-muted mb-3">تحقق من الرابط أو ارجع إلى قائمة المراكز المعتمدة.</p>
|
||||
<a class="btn btn-primary" href="applications.php?status=approved">المراكز المعتمدة</a>
|
||||
</div>
|
||||
<?php elseif (!$isApprovedCenter): ?>
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">الرصد يُفتح بعد الاعتماد</div>
|
||||
<p class="text-muted mb-3">اعتمد المركز أولاً حتى يظهر رصد تقييمه الإشرافي.</p>
|
||||
<a class="btn btn-outline-secondary" href="application_detail.php?id=<?= e((string) $application['id']) ?>">ملف الاعتماد</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="page-banner approved-hero mb-4">
|
||||
<div class="row g-4 align-items-center">
|
||||
<div class="col-lg-8">
|
||||
<span class="approved-kicker mb-3">رصد المركز</span>
|
||||
<h1 class="page-title mb-3">إدخال تقييم إشرافي للمركز</h1>
|
||||
<p class="page-copy mb-3">اختر التقييم، ثم أدخل النتيجة مباشرة أو عبر البنود التفصيلية داخل دورة <strong><?= e($cycleLabel) ?></strong>.</p>
|
||||
<div class="hero-meta">
|
||||
<span><?= e((string) count($assessmentOptions)) ?> تقييمات متاحة</span>
|
||||
<span><?= e($cycleLabel) ?></span>
|
||||
<?php if ($selectedAssessment): ?><span><?= e((string) ($selectedAssessment['title'] ?? '')) ?></span><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="app-card h-100">
|
||||
<div class="section-title mb-2">تنقّل سريع</div>
|
||||
<div class="cta-stack">
|
||||
<a class="btn btn-outline-secondary" href="<?= e($assessmentsUrl) ?>">كل التقييمات</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($reportUrl) ?>">تقرير الدورة</a>
|
||||
<?php if ($selectedAssessment): ?>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($criteriaUrl) ?>">إدارة البنود</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($selectedCycleId <= 0): ?>
|
||||
<div class="app-card">
|
||||
<div class="alert alert-warning mb-0">لا توجد دورة متاحة لهذا المركز بعد. أنشئ دورة أولاً من صفحة المركز.</div>
|
||||
</div>
|
||||
<?php elseif ($assessmentOptions === []): ?>
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">لا توجد تقييمات مراكز جاهزة للرصد</div>
|
||||
<p class="text-muted mb-3">أضف نوع تقييم أولاً من صفحة تقييم المراكز ثم ارجع هنا لإدخال الدرجة.</p>
|
||||
<a class="btn btn-primary" href="<?= e($assessmentsUrl) ?>">إضافة تقييم</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="app-card mb-4">
|
||||
<form method="get" class="row g-2 align-items-center">
|
||||
<input type="hidden" name="id" value="<?= e((string) $application['id']) ?>">
|
||||
<input type="hidden" name="cycle" value="<?= e((string) $selectedCycleId) ?>">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label" for="assessment_id">التقييم</label>
|
||||
<select class="form-select" name="assessment_id" id="assessment_id" onchange="this.form.submit()">
|
||||
<?php foreach ($assessmentOptions as $assessmentId => $assessment): ?>
|
||||
<option value="<?= e((string) $assessmentId) ?>" <?= $selectedAssessmentId === (int) $assessmentId ? 'selected' : '' ?>><?= e((string) ($assessment['label'] ?? '')) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 d-grid">
|
||||
<button type="submit" class="btn btn-primary">فتح الرصد</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php if ($selectedAssessment): ?>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-3"><div class="app-card h-100"><div class="section-title mb-2">الدرجة النهائية</div><div class="display-6 mb-1"><?= e($maxScoreLabel !== '' ? $maxScoreLabel : '0') ?></div><div class="section-subtle">المجموع المعتمد</div></div></div>
|
||||
<div class="col-md-3"><div class="app-card h-100"><div class="section-title mb-2">البنود النشطة</div><div class="display-6 mb-1"><?= e((string) ($criteriaMetrics['active'] ?? 0)) ?></div><div class="section-subtle">من أصل <?= e((string) ($criteriaMetrics['total'] ?? 0)) ?></div></div></div>
|
||||
<div class="col-md-3"><div class="app-card h-100"><div class="section-title mb-2">حالة الرصد</div><div class="display-6 mb-1" style="font-size:1.05rem;"><?= $existingStatus !== '' ? center_assessment_status_badge($existingStatus) : '<span class="text-muted">غير مرصود</span>' ?></div><div class="section-subtle">آخر حفظ <?= e($scoreMetrics['latest_date'] !== '' ? (string) $scoreMetrics['latest_date'] : '—') ?></div></div></div>
|
||||
<div class="col-md-3"><div class="app-card h-100"><div class="section-title mb-2">آخر نسبة</div><div class="display-6 mb-1"><?= e($existingPercentage !== null ? center_score_display((float) $existingPercentage) . '%' : '—') ?></div><div class="section-subtle">للتقييم الحالي</div></div></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($errors['form'])): ?>
|
||||
<div class="alert alert-danger mb-4"><?= e($errors['form']) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($isCycleReadOnly): ?>
|
||||
<div class="alert alert-warning mb-4">هذه الدورة مؤرشفة، لذلك الصفحة معروضة للقراءة فقط.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($selectedAssessment): ?>
|
||||
<div class="app-card">
|
||||
<form method="post" novalidate>
|
||||
<input type="hidden" name="assessment_type_id" value="<?= e((string) $selectedAssessmentId) ?>">
|
||||
<div class="row g-3 align-items-end mb-4">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="assessed_on">تاريخ الرصد</label>
|
||||
<input type="date" class="form-control <?= isset($errors['assessed_on']) ? 'is-invalid' : '' ?>" id="assessed_on" name="assessed_on" value="<?= e((string) $values['assessed_on']) ?>" <?= $isCycleReadOnly ? 'disabled' : '' ?>>
|
||||
<?php if (isset($errors['assessed_on'])): ?><div class="invalid-feedback"><?= e($errors['assessed_on']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="status">الحالة</label>
|
||||
<select class="form-select" id="status" name="status" data-center-status <?= $isCycleReadOnly ? 'disabled' : '' ?>>
|
||||
<?php foreach (center_assessment_status_map() as $statusKey => $statusMeta): ?>
|
||||
<option value="<?= e($statusKey) ?>" <?= (string) $values['status'] === (string) $statusKey ? 'selected' : '' ?>><?= e((string) $statusMeta['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="school-data-item h-100">
|
||||
<strong>الوزن</strong>
|
||||
<span><?= e(center_score_display((float) ($selectedAssessment['weight_percentage'] ?? 0)) ?: '0') ?>٪</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($errors['score'])): ?>
|
||||
<div class="alert alert-danger mb-4"><?= e($errors['score']) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($hasCriteria): ?>
|
||||
<div class="table-responsive mb-4">
|
||||
<table class="table app-table align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>البند</th>
|
||||
<th>الدرجة القصوى</th>
|
||||
<th>الدرجة المرصودة</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($criteria as $criterion): ?>
|
||||
<?php
|
||||
$criterionId = (int) ($criterion['id'] ?? 0);
|
||||
$criterionValue = (string) (($values['criteria_scores'][$criterionId]['score_raw'] ?? ''));
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-semibold"><?= e((string) ($criterion['title'] ?? '')) ?></div>
|
||||
<?php if (!empty($criterion['notes'])): ?><div class="text-muted small"><?= e((string) ($criterion['notes'] ?? '')) ?></div><?php endif; ?>
|
||||
</td>
|
||||
<td><?= e(center_score_display((float) ($criterion['max_score'] ?? 0))) ?></td>
|
||||
<td>
|
||||
<input
|
||||
class="form-control"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
max="<?= e(center_score_display((float) ($criterion['max_score'] ?? 0))) ?>"
|
||||
name="criteria[<?= e((string) $criterionId) ?>]"
|
||||
value="<?= e($criterionValue) ?>"
|
||||
placeholder="<?= e(center_score_display((float) ($criterion['max_score'] ?? 0))) ?>"
|
||||
data-center-criterion
|
||||
<?= $isCycleReadOnly ? 'disabled' : '' ?>
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4"><div class="school-data-item"><strong>المجموع الحالي</strong><span data-center-total><?= e($values['score_raw'] !== '' ? center_score_display((float) $values['score_raw']) : '—') ?></span></div></div>
|
||||
<div class="col-md-4"><div class="school-data-item"><strong>المجموع الكلي</strong><span><?= e($maxScoreLabel !== '' ? $maxScoreLabel : '0') ?></span></div></div>
|
||||
<div class="col-md-4"><div class="school-data-item"><strong>نسبة الإنجاز</strong><span data-center-percentage><?= e(($values['score_raw'] !== '' && (float) ($values['assessment_max_score'] ?? 0) > 0) ? center_score_display(((float) $values['score_raw'] / (float) $values['assessment_max_score']) * 100) . '%' : '—') ?></span></div></div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="score">الدرجة المرصودة</label>
|
||||
<input class="form-control <?= isset($errors['score']) ? 'is-invalid' : '' ?>" type="number" step="0.01" min="0" max="<?= e($maxScoreLabel !== '' ? $maxScoreLabel : '0') ?>" id="score" name="score" value="<?= e((string) $values['score_raw']) ?>" placeholder="من <?= e($maxScoreLabel !== '' ? $maxScoreLabel : '0') ?>" <?= $isCycleReadOnly ? 'disabled' : '' ?>>
|
||||
<?php if (isset($errors['score'])): ?><div class="invalid-feedback"><?= e($errors['score']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="notes">ملاحظات</label>
|
||||
<textarea class="form-control" id="notes" name="notes" rows="4" placeholder="اختياري" <?= $isCycleReadOnly ? 'disabled' : '' ?>><?= e((string) $values['notes']) ?></textarea>
|
||||
</div>
|
||||
|
||||
<?php if (is_array($existingScore) && $existingScore !== []): ?>
|
||||
<div class="alert alert-light border mb-4">
|
||||
<strong>آخر حفظ:</strong>
|
||||
<?= center_assessment_status_badge((string) ($existingScore['status'] ?? 'pending')) ?>
|
||||
<span class="ms-2">بتاريخ <?= e((string) ($existingScore['assessed_on'] ?? '—')) ?></span>
|
||||
<?php if ($existingPercentage !== null): ?><span class="ms-2">— <?= e(center_score_display((float) $existingPercentage)) ?>%</span><?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!$isCycleReadOnly): ?>
|
||||
<div class="d-flex justify-content-between align-items-center pt-3 border-top mt-3 flex-wrap gap-2">
|
||||
<div class="section-subtle">يمكنك تعديل نفس التقييم لاحقاً، وسيتم تحديث الدرجة والبنود الحالية بدلاً من إنشاء نسخة جديدة.</div>
|
||||
<button class="btn btn-primary" type="submit">حفظ الرصد</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php if (!$isCycleReadOnly): ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const statusField = document.querySelector('[data-center-status]');
|
||||
const criterionInputs = Array.from(document.querySelectorAll('[data-center-criterion]'));
|
||||
const totalEl = document.querySelector('[data-center-total]');
|
||||
const percentageEl = document.querySelector('[data-center-percentage]');
|
||||
const directScore = document.getElementById('score');
|
||||
const maxScore = <?= json.dumps(0) ?>;
|
||||
const maxScoreValue = <?= (float) 0 ?>;
|
||||
|
||||
const parsedMax = <?= json.dumps('MAX_PLACEHOLDER') ?>;
|
||||
|
||||
const updateCriteriaState = () => {
|
||||
if (!statusField || !criterionInputs.length) return;
|
||||
const enabled = statusField.value === 'completed';
|
||||
let total = 0;
|
||||
let hasValue = false;
|
||||
criterionInputs.forEach((input) => {
|
||||
if (enabled) {
|
||||
input.removeAttribute('disabled');
|
||||
} else {
|
||||
input.setAttribute('disabled', 'disabled');
|
||||
input.value = '';
|
||||
}
|
||||
const raw = input.value.trim();
|
||||
if (enabled && raw !== '' && !Number.isNaN(Number(raw))) {
|
||||
total += Number(raw);
|
||||
hasValue = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (totalEl) {
|
||||
totalEl.textContent = enabled && hasValue ? total.toFixed(2).replace(/\.00$/, '').replace(/(\.\d)0$/, '$1') : '—';
|
||||
}
|
||||
if (percentageEl) {
|
||||
const maxValue = Number(percentageEl.getAttribute('data-max-score') || '0');
|
||||
percentageEl.textContent = enabled && hasValue && maxValue > 0
|
||||
? ((total / maxValue) * 100).toFixed(2).replace(/\.00$/, '').replace(/(\.\d)0$/, '$1') + '%'
|
||||
: '—';
|
||||
}
|
||||
};
|
||||
|
||||
if (percentageEl) {
|
||||
percentageEl.setAttribute('data-max-score', '<?= e((string) ((float) ($values['assessment_max_score'] ?? 0))) ?>');
|
||||
}
|
||||
|
||||
const updateDirectScoreState = () => {
|
||||
if (!statusField || !directScore) return;
|
||||
const enabled = statusField.value === 'completed';
|
||||
if (enabled) {
|
||||
directScore.removeAttribute('disabled');
|
||||
} else {
|
||||
directScore.setAttribute('disabled', 'disabled');
|
||||
directScore.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
if (statusField) {
|
||||
statusField.addEventListener('change', () => {
|
||||
updateDirectScoreState();
|
||||
updateCriteriaState();
|
||||
});
|
||||
}
|
||||
|
||||
criterionInputs.forEach((input) => {
|
||||
input.addEventListener('input', updateCriteriaState);
|
||||
input.addEventListener('change', updateCriteriaState);
|
||||
});
|
||||
|
||||
updateDirectScoreState();
|
||||
updateCriteriaState();
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
<?php render_page_end();
|
||||
529
center_assessments.php
Normal file
529
center_assessments.php
Normal file
@ -0,0 +1,529 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
$flash = consume_flash();
|
||||
$approvedCenters = list_applications('approved');
|
||||
$applicationId = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT) ?: 0;
|
||||
$requestedCycleId = filter_input(INPUT_GET, 'cycle', FILTER_VALIDATE_INT) ?: 0;
|
||||
$application = $applicationId > 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);
|
||||
?>
|
||||
<section class="py-4 py-lg-5">
|
||||
<div class="container-xxl">
|
||||
<div class="admin-layout row g-4 align-items-start">
|
||||
<div class="col-lg-3 layout-sidebar-column">
|
||||
<?php require __DIR__ . '/includes/sidebar.php'; ?>
|
||||
</div>
|
||||
<div class="col-lg-9 layout-content-column">
|
||||
|
||||
<div class="page-banner mb-4 mb-lg-5">
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-8">
|
||||
<span class="eyebrow mb-3">مرحلة 2 — التقييم + البنود</span>
|
||||
<h1 class="page-title mb-3">إدارة تقييمات المراكز</h1>
|
||||
<p class="page-copy mb-3">هذه الصفحة تضيف طبقة مستقلة لتقييم <strong>المراكز المعتمدة</strong> حسب <strong>الدورة الموسمية</strong>، بدون خلطها مع تقييمات الطلاب. يمكنك الآن تعريف <strong>أنواع تقييم المراكز وأوزانها</strong> ثم فتح <strong>بنود كل تقييم</strong> بنفس النمط المستخدم في تقييم الطلاب، تمهيداً لصفحة الرصد الفعلي.</p>
|
||||
<div class="hero-meta">
|
||||
<span>المراكز المعتمدة <?= e((string) count($approvedCenterCards)) ?></span>
|
||||
<span>الإعداد الحالي <?= e((string) $metrics['active']) ?> تقييمات نشطة</span>
|
||||
<span>الدورة المختارة <?= e($cycleLabel) ?></span>
|
||||
</div>
|
||||
<div class="cta-stack mt-4">
|
||||
<a class="btn btn-primary" href="admin.php">العودة إلى لوحة الإدارة</a>
|
||||
<a class="btn btn-outline-secondary" href="applications.php?status=approved">المراكز المعتمدة</a>
|
||||
<?php if ($application && $isApprovedCenter): ?>
|
||||
<a class="btn btn-outline-secondary" href="approved_school.php?id=<?= e((string) $application['id']) ?>&cycle=<?= e((string) $selectedCycleId) ?>">صفحة المركز</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="page-banner-panel h-100">
|
||||
<div class="mini-stat-label">نطاق العمل</div>
|
||||
<div class="mini-stat-value"><?= e((string) $metrics['total']) ?></div>
|
||||
<div class="mini-stat-copy mb-3">أنواع تقييم مركزية معرفة حالياً للمركز/الدورة المحددين.</div>
|
||||
<div class="d-grid gap-2">
|
||||
<?php if ($application && $isApprovedCenter && $selectedCycleId > 0 && !$isCycleReadOnly): ?>
|
||||
<button class="btn btn-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#centerAssessmentModal">إضافة تقييم مركز</button>
|
||||
<?php else: ?>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="applications.php?status=approved">اختر مركزاً معتمداً</a>
|
||||
<?php endif; ?>
|
||||
<span class="small text-muted">المعيار هنا: <strong>لكل مركز معتمد</strong> و<strong>لكل دورة</strong> بشكل مستقل.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6 col-xl-3"><div class="app-card stat-tile"><div class="mini-stat-label">إجمالي التقييمات</div><div class="mini-stat-value"><?= e((string) $metrics['total']) ?></div><div class="mini-stat-copy">كل الأنواع المعرفة للمركز المحدد.</div></div></div>
|
||||
<div class="col-md-6 col-xl-3"><div class="app-card stat-tile"><div class="mini-stat-label">تقييمات نشطة</div><div class="mini-stat-value"><?= e((string) $metrics['active']) ?></div><div class="mini-stat-copy">جاهزة لربط البنود ثم الرصد.</div></div></div>
|
||||
<div class="col-md-6 col-xl-3"><div class="app-card stat-tile"><div class="mini-stat-label">إجمالي الوزن النشط</div><div class="mini-stat-value"><?= e((string) round((float) $metrics['active_weight'], 2)) ?>٪</div><div class="mini-stat-copy">للمراجعة قبل تفعيل الرصد.</div></div></div>
|
||||
<div class="col-md-6 col-xl-3"><div class="app-card stat-tile"><div class="mini-stat-label">متوسط الدرجة القصوى</div><div class="mini-stat-value"><?= e((string) round((float) $metrics['average_max_score'], 2)) ?></div><div class="mini-stat-copy">متوسط السقف لكل تقييم مركز.</div></div></div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-8">
|
||||
<div class="app-card h-100">
|
||||
<?php if (!$application || !$isApprovedCenter): ?>
|
||||
<div class="section-head mb-3">
|
||||
<div>
|
||||
<div class="section-title">اختر مركزاً معتمداً للبدء</div>
|
||||
<div class="section-copy">التقييمات المركزية لا تظهر إلا للمراكز المعتمدة، وكل مركز يُدار داخل دورته الموسمية الخاصة.</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($approvedCenterCards === []): ?>
|
||||
<div class="empty-state text-center py-5">
|
||||
<div class="empty-title mb-2">لا توجد مراكز معتمدة بعد</div>
|
||||
<p class="text-muted mb-3">اعتمد مركزاً أولاً من لوحة الطلبات حتى تتمكن من إعداد تقييمات المراكز.</p>
|
||||
<a class="btn btn-primary" href="applications.php">العودة إلى الطلبات</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="row g-3">
|
||||
<?php foreach ($approvedCenterCards as $card): ?>
|
||||
<?php $center = $card['application']; ?>
|
||||
<div class="col-md-6">
|
||||
<article class="app-card h-100 border-0 shadow-sm">
|
||||
<div class="section-title mb-2"><?= e((string) ($center['center_name'] ?? '')) ?></div>
|
||||
<p class="text-muted mb-3"><?= e((string) ($center['city'] ?? '')) ?> — <?= e((string) ($center['director_name'] ?? '')) ?></p>
|
||||
<div class="hero-meta mb-3">
|
||||
<span>الدورة <?= e((string) (($card['selected_cycle']['cycle_name'] ?? 'غير متاحة'))) ?></span>
|
||||
<span>السعة <?= e((string) ($center['expected_students'] ?? '0')) ?> طالب</span>
|
||||
</div>
|
||||
<?php if ((int) $card['selected_cycle_id'] > 0): ?>
|
||||
<a class="btn btn-primary btn-sm" href="<?= e($card['url']) ?>">فتح تقييم هذا المركز</a>
|
||||
<?php else: ?>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="approved_school.php?id=<?= e((string) ($center['id'] ?? 0)) ?>">أنشئ دورة أولاً</a>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<div class="section-head mb-3">
|
||||
<div>
|
||||
<div class="section-title">تقييمات <?= e((string) $application['center_name']) ?></div>
|
||||
<div class="section-copy">الدورة الحالية: <strong><?= e($cycleLabel) ?></strong>. يمكنك هنا إدارة أنواع التقييم، ثم فتح البنود، الرصد، والتقرير المجمع لكل دورة من عمود الإجراءات.</div>
|
||||
</div>
|
||||
<?php if (!$isCycleReadOnly && $selectedCycleId > 0): ?>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<a class="btn btn-outline-secondary btn-sm" href="<?= e($buildCenterAssessmentReportUrl((int) $application['id'], $selectedCycleId)) ?>">تقرير الدورة</a>
|
||||
<button class="btn btn-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#centerAssessmentModal">إضافة تقييم</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($selectedCycleId <= 0): ?>
|
||||
<div class="alert alert-warning mb-0">لا توجد دورة متاحة لهذا المركز بعد. أنشئ دورة موسمية أولاً من صفحة المركز.</div>
|
||||
<?php else: ?>
|
||||
<?php if ($isCycleReadOnly): ?>
|
||||
<div class="alert alert-warning mb-4">هذه الدورة مؤرشفة للقراءة فقط. يمكنك المراجعة، لكن لا يمكن إضافة أو تعديل تقييمات جديدة هنا.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="get" class="row g-3 align-items-end mb-4">
|
||||
<input type="hidden" name="id" value="<?= e((string) $application['id']) ?>">
|
||||
<input type="hidden" name="cycle" value="<?= e((string) $selectedCycleId) ?>">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label" for="search">بحث</label>
|
||||
<input type="text" name="search" id="search" class="form-control" placeholder="ابحث باسم التقييم..." value="<?= e($filters['search']) ?>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="category">الفئة</label>
|
||||
<select class="form-select" name="category" id="category">
|
||||
<option value="">كل الفئات</option>
|
||||
<?php foreach (assessment_category_options() as $categoryOption): ?>
|
||||
<option value="<?= e($categoryOption) ?>" <?= $filters['category'] === $categoryOption ? 'selected' : '' ?>><?= e($categoryOption) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3 d-grid gap-2">
|
||||
<button class="btn btn-outline-secondary" type="submit">تصفية</button>
|
||||
<a class="btn btn-link" href="<?= e($buildCenterAssessmentsUrl((int) $application['id'], $selectedCycleId)) ?>">إعادة تعيين</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="alert alert-light border mb-4">أصبحت دورة تقييم المراكز مكتملة: <strong>تعريف التقييم</strong> ثم <strong>البنود</strong> ثم <strong>الرصد</strong> ثم <strong>تقرير ملخّص للدورة</strong>.</div>
|
||||
|
||||
<?php if ($assessments === []): ?>
|
||||
<div class="empty-state text-center py-5">
|
||||
<div class="empty-title mb-2">لا توجد تقييمات مراكز بعد</div>
|
||||
<p class="text-muted mb-3">ابدأ بتعريف أول نوع تقييم للمركز مثل: الالتزام الإداري، جودة البيئة التعليمية، أو متابعة الخطة.</p>
|
||||
<?php if (!$isCycleReadOnly): ?>
|
||||
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#centerAssessmentModal">إضافة أول تقييم</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table app-table align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>التقييم</th>
|
||||
<th>الفئة</th>
|
||||
<th>المقياس</th>
|
||||
<th>الدرجة</th>
|
||||
<th>الوزن</th>
|
||||
<th>البنود</th>
|
||||
<th>الحالة</th>
|
||||
<th>الإجراء</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($assessments as $assessment): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-semibold"><?= e((string) ($assessment['title'] ?? '')) ?></div>
|
||||
<?php if (!empty($assessment['notes'])): ?>
|
||||
<div class="text-muted small"><?= e((string) $assessment['notes']) ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= e((string) ($assessment['category'] ?? '')) ?></td>
|
||||
<td><?= assessment_scale_type_badge((string) ($assessment['scale_type'] ?? '')) ?></td>
|
||||
<td><?= e((string) round((float) ($assessment['max_score'] ?? 0), 2)) ?></td>
|
||||
<td><?= e((string) round((float) ($assessment['weight_percentage'] ?? 0), 2)) ?>٪</td>
|
||||
<td>
|
||||
<div class="fw-semibold"><?= e((string) ((int) ($assessment['criteria_count'] ?? 0))) ?></div>
|
||||
<div class="text-muted small">المجموع <?= e((string) round((float) ($assessment['criteria_total_max_score'] ?? 0), 2)) ?></div>
|
||||
</td>
|
||||
<td><?= assessment_active_badge((int) ($assessment['is_active'] ?? 0)) ?></td>
|
||||
<td>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a class="btn btn-outline-primary btn-sm" href="<?= e($buildCenterAssessmentCriteriaUrl((int) $application['id'], $selectedCycleId, (int) ($assessment['id'] ?? 0))) ?>">البنود</a>
|
||||
<a class="btn btn-primary btn-sm" href="<?= e($buildCenterAssessmentScoreUrl((int) $application['id'], $selectedCycleId, (int) ($assessment['id'] ?? 0))) ?>">رصد</a>
|
||||
<?php if (!$isCycleReadOnly): ?>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#centerAssessmentModal"
|
||||
data-action="edit"
|
||||
data-id="<?= e((string) ($assessment['id'] ?? 0)) ?>"
|
||||
data-title="<?= e((string) ($assessment['title'] ?? '')) ?>"
|
||||
data-category="<?= e((string) ($assessment['category'] ?? '')) ?>"
|
||||
data-scale-type="<?= e((string) ($assessment['scale_type'] ?? '')) ?>"
|
||||
data-max-score="<?= e((string) round((float) ($assessment['max_score'] ?? 0), 2)) ?>"
|
||||
data-weight-percentage="<?= e((string) round((float) ($assessment['weight_percentage'] ?? 0), 2)) ?>"
|
||||
data-is-active="<?= e((string) ($assessment['is_active'] ?? 0)) ?>"
|
||||
data-notes="<?= e((string) ($assessment['notes'] ?? '')) ?>"
|
||||
>تعديل</button>
|
||||
<?php else: ?>
|
||||
<span class="text-muted small align-self-center">قراءة فقط</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php render_pagination($totalAssessments, $limit, $page, ['id' => (int) $application['id'], 'cycle' => $selectedCycleId, 'search' => $filters['search'], 'category' => $filters['category']]); ?>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="app-card h-100 sidebar-card">
|
||||
<div class="section-title mb-3">المراكز المعتمدة</div>
|
||||
<?php if ($approvedCenterCards === []): ?>
|
||||
<div class="empty-state text-center py-4">
|
||||
<div class="empty-title mb-2">لا توجد مراكز معتمدة</div>
|
||||
<p class="text-muted mb-0">ستظهر هنا بمجرد اعتماد أول مركز.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="quick-link-stack">
|
||||
<?php foreach ($approvedCenterCards as $card): ?>
|
||||
<?php $center = $card['application']; ?>
|
||||
<a class="quick-link-item <?= $applicationId === (int) ($center['id'] ?? 0) ? 'active' : '' ?>" href="<?= e($card['url']) ?>">
|
||||
<div>
|
||||
<strong><?= e((string) ($center['center_name'] ?? '')) ?></strong>
|
||||
<span><?= e((string) ($card['selected_cycle']['cycle_name'] ?? 'بدون دورة')) ?> — <?= e((string) ($center['city'] ?? '')) ?></span>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php if ($application && $isApprovedCenter && $selectedCycleId > 0): ?>
|
||||
<div class="modal fade" id="centerAssessmentModal" tabindex="-1" aria-labelledby="centerAssessmentModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<form method="post" action="<?= e($buildCenterAssessmentsUrl((int) $application['id'], $selectedCycleId, ['search' => $filters['search'], 'category' => $filters['category'], 'page' => $page])) ?>">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title fs-5" id="centerAssessmentModalLabel">إضافة تقييم مركز</h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="إغلاق"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<?php if (isset($errors['form'])): ?>
|
||||
<div class="alert alert-danger"><?= e($errors['form']) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<input type="hidden" name="action" id="centerAssessmentAction" value="add">
|
||||
<input type="hidden" name="assessment_id" id="centerAssessmentId" value="0">
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="title">اسم التقييم</label>
|
||||
<input type="text" class="form-control <?= isset($errors['title']) ? 'is-invalid' : '' ?>" id="title" name="title" value="<?= e($values['title']) ?>" required>
|
||||
<?php if (isset($errors['title'])): ?><div class="invalid-feedback"><?= e($errors['title']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="category">الفئة</label>
|
||||
<select class="form-select <?= isset($errors['category']) ? 'is-invalid' : '' ?>" id="categoryField" name="category" required>
|
||||
<?php foreach (assessment_category_options() as $categoryOption): ?>
|
||||
<option value="<?= e($categoryOption) ?>" <?= $values['category'] === $categoryOption ? 'selected' : '' ?>><?= e($categoryOption) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php if (isset($errors['category'])): ?><div class="invalid-feedback"><?= e($errors['category']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="scaleTypeField">المقياس</label>
|
||||
<select class="form-select <?= isset($errors['scale_type']) ? 'is-invalid' : '' ?>" id="scaleTypeField" name="scale_type" required>
|
||||
<?php foreach (assessment_scale_type_map() as $scaleKey => $scaleMeta): ?>
|
||||
<option value="<?= e($scaleKey) ?>" <?= $values['scale_type'] === $scaleKey ? 'selected' : '' ?>><?= e((string) $scaleMeta['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php if (isset($errors['scale_type'])): ?><div class="invalid-feedback"><?= e($errors['scale_type']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="maxScoreField">الدرجة النهائية</label>
|
||||
<input type="number" step="0.01" min="0.01" max="1000" class="form-control <?= isset($errors['max_score']) ? 'is-invalid' : '' ?>" id="maxScoreField" name="max_score" value="<?= e($values['max_score']) ?>" required>
|
||||
<?php if (isset($errors['max_score'])): ?><div class="invalid-feedback"><?= e($errors['max_score']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="weightField">الوزن ٪</label>
|
||||
<input type="number" step="0.01" min="0" max="100" class="form-control <?= isset($errors['weight_percentage']) ? 'is-invalid' : '' ?>" id="weightField" name="weight_percentage" value="<?= e($values['weight_percentage']) ?>" required>
|
||||
<?php if (isset($errors['weight_percentage'])): ?><div class="invalid-feedback"><?= e($errors['weight_percentage']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="notesField">ملاحظات</label>
|
||||
<textarea class="form-control" id="notesField" name="notes" rows="3"><?= e($values['notes']) ?></textarea>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<input type="hidden" name="is_active" value="0">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="isActiveField" name="is_active" value="1" <?= $values['is_active'] === '1' ? 'checked' : '' ?>>
|
||||
<label class="form-check-label" for="isActiveField">تفعيل هذا التقييم داخل الدورة الحالية</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" class="btn btn-primary">حفظ التقييم</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const modal = document.getElementById('centerAssessmentModal');
|
||||
if (!modal) return;
|
||||
|
||||
modal.addEventListener('show.bs.modal', function (event) {
|
||||
const trigger = event.relatedTarget;
|
||||
const actionField = document.getElementById('centerAssessmentAction');
|
||||
const idField = document.getElementById('centerAssessmentId');
|
||||
const titleField = document.getElementById('title');
|
||||
const categoryField = document.getElementById('categoryField');
|
||||
const scaleTypeField = document.getElementById('scaleTypeField');
|
||||
const maxScoreField = document.getElementById('maxScoreField');
|
||||
const weightField = document.getElementById('weightField');
|
||||
const isActiveField = document.getElementById('isActiveField');
|
||||
const notesField = document.getElementById('notesField');
|
||||
const modalTitle = document.getElementById('centerAssessmentModalLabel');
|
||||
|
||||
if (!trigger || !trigger.dataset || !trigger.dataset.action) {
|
||||
actionField.value = 'add';
|
||||
idField.value = '0';
|
||||
modalTitle.innerText = 'إضافة تقييم مركز';
|
||||
titleField.value = '<?= e($values['title']) ?>';
|
||||
categoryField.value = '<?= e($values['category']) ?>';
|
||||
scaleTypeField.value = '<?= e($values['scale_type']) ?>';
|
||||
maxScoreField.value = '<?= e($values['max_score']) ?>';
|
||||
weightField.value = '<?= e($values['weight_percentage']) ?>';
|
||||
isActiveField.checked = <?= $values['is_active'] === '1' ? 'true' : 'false' ?>;
|
||||
notesField.value = '<?= e($values['notes']) ?>';
|
||||
return;
|
||||
}
|
||||
|
||||
actionField.value = trigger.dataset.action || 'edit';
|
||||
idField.value = trigger.dataset.id || '0';
|
||||
modalTitle.innerText = 'تعديل تقييم المركز';
|
||||
titleField.value = trigger.dataset.title || '';
|
||||
categoryField.value = trigger.dataset.category || 'اختبار قصير';
|
||||
scaleTypeField.value = trigger.dataset.scaleType || 'percentage';
|
||||
maxScoreField.value = trigger.dataset.maxScore || '100';
|
||||
weightField.value = trigger.dataset.weightPercentage || '0';
|
||||
isActiveField.checked = (trigger.dataset.isActive || '0') === '1';
|
||||
notesField.value = trigger.dataset.notes || '';
|
||||
});
|
||||
|
||||
<?php if ($errors !== []): ?>
|
||||
const bootstrapModal = new bootstrap.Modal(modal);
|
||||
bootstrapModal.show();
|
||||
<?php endif; ?>
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php render_page_end(); ?>
|
||||
83
db/migrations/20260417_center_assessment_system.sql
Normal file
83
db/migrations/20260417_center_assessment_system.sql
Normal file
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 '<span class="status-badge ' . e($meta['class']) . '">' . e($meta['label']) . '</span>';
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user