Autosave: 20260417-142208
This commit is contained in:
parent
7937c54a84
commit
faff5cf4a0
@ -92,6 +92,11 @@ render_flash($flash);
|
||||
<p>إعداد تقييمات إشرافية للمراكز المعتمدة داخل كل دورة موسمية، تمهيداً لإضافة البنود والرصد بنفس نمط الطلاب.</p>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="center_assessments.php">فتح تقييم المراكز</a>
|
||||
</article>
|
||||
<article class="module-item">
|
||||
<h2>قوالب تقييم المراكز</h2>
|
||||
<p>إنشاء القوالب العامة وبنودها مركزياً حتى يستخدمها المشرف العام أو المقيمون المكلّفون عند تقييم المراكز واحداً تلو الآخر.</p>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="global_center_assessments.php">فتح قوالب التقييم</a>
|
||||
</article>
|
||||
<article class="module-item">
|
||||
<h2>هيكل النظام</h2>
|
||||
<p>مرجع سريع لفهم تنظيم الصفحات الحالية ومسار التطوير الإداري داخل التطبيق.</p>
|
||||
@ -113,6 +118,7 @@ render_flash($flash);
|
||||
<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="global_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>
|
||||
|
||||
@ -56,6 +56,12 @@ $buildCenterAssessmentScoreUrl = static function (int $targetApplicationId = 0,
|
||||
return 'center_assessment_score_sheet.php' . ($params !== [] ? '?' . http_build_query($params) : '');
|
||||
};
|
||||
|
||||
if ($applicationId <= 0 || $requestedCycleId <= 0) {
|
||||
set_flash('error', 'اختر المركز والدورة أولاً ثم افتح تقرير التقييم من شاشة تقييم المراكز.');
|
||||
header('Location: ' . $buildCenterAssessmentsUrl($applicationId, $requestedCycleId));
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($isApprovedCenter) {
|
||||
$cycleContext = resolve_school_cycle_context((int) $application['id'], $application, $requestedCycleId);
|
||||
$selectedCycle = $cycleContext['selected'];
|
||||
@ -86,7 +92,7 @@ $pageTitle = $application && $isApprovedCenter
|
||||
: 'تقرير تقييم المراكز';
|
||||
$pageDescription = 'ملخص مجمع لنتائج تقييم المركز داخل الدورة المختارة، مع حالة كل تقييم ونسبة الإنجاز الكلية.';
|
||||
|
||||
if (!$application) {
|
||||
if (!$application && $applicationId > 0) {
|
||||
http_response_code(404);
|
||||
}
|
||||
|
||||
|
||||
@ -87,6 +87,12 @@ $buildCenterAssessmentReportUrl = static function (int $targetApplicationId = 0,
|
||||
return 'center_assessment_report.php' . ($params !== [] ? '?' . http_build_query($params) : '');
|
||||
};
|
||||
|
||||
if ($applicationId <= 0 || $requestedCycleId <= 0) {
|
||||
set_flash('error', 'اختر المركز والدورة أولاً ثم افتح صفحة رصد التقييم من شاشة تقييم المراكز.');
|
||||
header('Location: ' . $buildCenterAssessmentsUrl($applicationId, $requestedCycleId));
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($isApprovedCenter) {
|
||||
$cycleContext = resolve_school_cycle_context((int) $application['id'], $application, $requestedCycleId);
|
||||
$selectedCycle = $cycleContext['selected'];
|
||||
@ -121,7 +127,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $application) {
|
||||
try {
|
||||
save_center_assessment_score_in_cycle((int) $application['id'], $selectedCycleId, $values);
|
||||
set_flash('success', 'تم حفظ رصد تقييم المركز بنجاح.');
|
||||
header('Location: ' . $buildCenterAssessmentScoreUrl((int) $application['id'], $selectedCycleId, $selectedAssessmentId));
|
||||
$returnUrl = filter_input(INPUT_GET, 'return_url', FILTER_SANITIZE_URL); header('Location: ' . ($returnUrl ?: $buildCenterAssessmentScoreUrl((int) $application['id'], $selectedCycleId, $selectedAssessmentId)));
|
||||
exit;
|
||||
} catch (Throwable $exception) {
|
||||
$errors['form'] = 'تعذر حفظ الرصد حالياً. يرجى المحاولة مرة أخرى.';
|
||||
@ -196,7 +202,7 @@ $pageTitle = $application && $isApprovedCenter
|
||||
: 'رصد تقييم المركز';
|
||||
$pageDescription = 'إدخال درجة تقييم المركز نفسه داخل الدورة المختارة، مع دعم البنود التفصيلية والتقرير النهائي.';
|
||||
|
||||
if (!$application) {
|
||||
if (!$application && $applicationId > 0) {
|
||||
http_response_code(404);
|
||||
}
|
||||
|
||||
@ -248,6 +254,9 @@ render_flash($flash);
|
||||
<div class="app-card h-100">
|
||||
<div class="section-title mb-2">تنقّل سريع</div>
|
||||
<div class="cta-stack">
|
||||
<?php if (!empty($_GET['return_url'])): ?>
|
||||
<a class="btn btn-outline-primary" href="<?= e($_GET['return_url']) ?>">العودة للقائمة</a>
|
||||
<?php endif; ?>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($assessmentsUrl) ?>">كل التقييمات</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($reportUrl) ?>">تقرير الدورة</a>
|
||||
<?php if ($selectedAssessment): ?>
|
||||
@ -274,6 +283,7 @@ render_flash($flash);
|
||||
<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) ?>">
|
||||
<?php if (!empty($_GET['return_url'])): ?><input type="hidden" name="return_url" value="<?= e($_GET['return_url']) ?>"><?php endif; ?>
|
||||
<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()">
|
||||
@ -425,10 +435,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
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 maxScore = <?= json_encode(center_score_display((float) ($values['assessment_max_score'] ?? 0))) ?>;
|
||||
const maxScoreValue = <?= (float) ($values['assessment_max_score'] ?? 0) ?>;
|
||||
|
||||
const parsedMax = <?= json.dumps('MAX_PLACEHOLDER') ?>;
|
||||
const parsedMax = <?= (float) ($values['assessment_max_score'] ?? 0) ?>;
|
||||
|
||||
const updateCriteriaState = () => {
|
||||
if (!statusField || !criterionInputs.length) return;
|
||||
|
||||
501
center_assessment_score_sheet.php.bak
Normal file
501
center_assessment_score_sheet.php.bak
Normal file
@ -0,0 +1,501 @@
|
||||
<?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 ($applicationId <= 0 || $requestedCycleId <= 0) {
|
||||
set_flash('error', 'اختر المركز والدورة أولاً ثم افتح صفحة رصد التقييم من شاشة تقييم المراكز.');
|
||||
header('Location: ' . $buildCenterAssessmentsUrl($applicationId, $requestedCycleId));
|
||||
exit;
|
||||
}
|
||||
|
||||
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 && $applicationId > 0) {
|
||||
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();
|
||||
@ -94,7 +94,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $isApprovedCenter) {
|
||||
|
||||
if ($errors === []) {
|
||||
try {
|
||||
if ($action === 'edit' && $assessmentId > 0) {
|
||||
if ($action === 'import_global') {
|
||||
$globalId = filter_input(INPUT_POST, 'global_assessment_id', FILTER_VALIDATE_INT) ?: 0;
|
||||
if ($globalId > 0) {
|
||||
import_global_center_assessment_to_center($globalId, (int) $application['id'], $selectedCycleId);
|
||||
set_flash('success', 'تم إدراج القالب العام مع بنوده بنجاح.');
|
||||
} else {
|
||||
set_flash('error', 'يجب اختيار قالب صحيح.');
|
||||
}
|
||||
} elseif ($action === 'edit' && $assessmentId > 0) {
|
||||
update_center_assessment_type_in_cycle((int) $application['id'], $selectedCycleId, $assessmentId, $values);
|
||||
set_flash('success', 'تم تحديث تقييم المركز بنجاح.');
|
||||
} else {
|
||||
@ -192,7 +200,8 @@ render_flash($flash);
|
||||
<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>
|
||||
<button class="btn btn-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#centerAssessmentModal">إضافة تقييم جديد</button>
|
||||
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#importGlobalModal">استيراد من القوالب العامة</button>
|
||||
<?php else: ?>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="applications.php?status=approved">اختر مركزاً معتمداً</a>
|
||||
<?php endif; ?>
|
||||
@ -402,7 +411,44 @@ render_flash($flash);
|
||||
</section>
|
||||
|
||||
<?php if ($application && $isApprovedCenter && $selectedCycleId > 0): ?>
|
||||
<div class="modal fade" id="centerAssessmentModal" tabindex="-1" aria-labelledby="centerAssessmentModalLabel" aria-hidden="true">
|
||||
<!-- Import Global Assessment Modal -->
|
||||
<div class="modal fade" id="importGlobalModal" tabindex="-1" aria-labelledby="importGlobalModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post" action="<?= e($buildCenterAssessmentsUrl((int) $application['id'], $selectedCycleId, ['search' => $filters['search'], 'category' => $filters['category'], 'page' => $page])) ?>">
|
||||
<input type="hidden" name="action" value="import_global">
|
||||
<!-- dummy fields to satisfy validate_assessment_input which expects them -->
|
||||
<input type="hidden" name="title" value="import">
|
||||
<input type="hidden" name="category" value="import">
|
||||
<input type="hidden" name="scale_type" value="percentage">
|
||||
<input type="hidden" name="max_score" value="100">
|
||||
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="importGlobalModalLabel">استيراد من القوالب العامة</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="إغلاق"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>سيتم استيراد القالب المحدد مع كافة بنوده وخصائصه إلى هذا المركز في الدورة المحددة.</p>
|
||||
<div class="mb-3">
|
||||
<label for="global_assessment_id" class="form-label">اختر القالب</label>
|
||||
<select class="form-select" id="global_assessment_id" name="global_assessment_id" required>
|
||||
<option value="">-- اختر القالب --</option>
|
||||
<?php foreach (global_center_assessment_type_options(true) as $globalTemplate): ?>
|
||||
<option value="<?= e((string) $globalTemplate['id']) ?>"><?= e((string) $globalTemplate['title']) ?> (الوزن: <?= e(rtrim(rtrim(number_format((float) ($globalTemplate['weight_percentage'] ?? 0), 2, '.', ''), '0'), '.')) ?>٪)</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" class="btn btn-primary">استيراد الآن</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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])) ?>">
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
ALTER TABLE center_assessment_types ADD COLUMN global_template_id INT UNSIGNED NULL AFTER cycle_id;
|
||||
ALTER TABLE center_assessment_types ADD CONSTRAINT fk_center_assessment_types_global_template FOREIGN KEY (global_template_id) REFERENCES global_center_assessment_types(id) ON DELETE SET NULL;
|
||||
29
db/migrations/20260417_global_center_assessments.sql
Normal file
29
db/migrations/20260417_global_center_assessments.sql
Normal file
@ -0,0 +1,29 @@
|
||||
CREATE TABLE IF NOT EXISTS global_center_assessment_types (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
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_global_center_assessment_types_active (is_active),
|
||||
INDEX idx_global_center_assessment_types_category (category)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS global_center_assessment_criteria (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
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_global_center_assessment_criteria_assessment (assessment_type_id),
|
||||
INDEX idx_global_center_assessment_criteria_active (assessment_type_id, is_active),
|
||||
CONSTRAINT fk_global_center_assessment_criteria_assessment FOREIGN KEY (assessment_type_id) REFERENCES global_center_assessment_types(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
220
execute_global_assessment.php
Normal file
220
execute_global_assessment.php
Normal file
@ -0,0 +1,220 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
$templateId = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT) ?: 0;
|
||||
if ($templateId <= 0) {
|
||||
set_flash('error', 'اختر قالب التقييم أولاً.');
|
||||
header('Location: global_center_assessments.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$template = get_global_center_assessment_type($templateId);
|
||||
if (!$template) {
|
||||
set_flash('error', 'القالب غير موجود.');
|
||||
header('Location: global_center_assessments.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$requestedCycleId = filter_input(INPUT_GET, 'cycle', FILTER_VALIDATE_INT) ?: 0;
|
||||
|
||||
// Fetch active cycles
|
||||
$pdo = db();
|
||||
$cyclesStmt = $pdo->query("SELECT id, cycle_name, status FROM school_cycles ORDER BY (status = 'active') DESC, start_date DESC");
|
||||
$cycles = $cyclesStmt->fetchAll() ?: [];
|
||||
|
||||
// Find default cycle if not selected
|
||||
if ($requestedCycleId <= 0 && count($cycles) > 0) {
|
||||
foreach ($cycles as $c) {
|
||||
if ($c['status'] === 'active') {
|
||||
$requestedCycleId = (int)$c['id'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($requestedCycleId <= 0) {
|
||||
$requestedCycleId = (int)$cycles[0]['id'];
|
||||
}
|
||||
}
|
||||
|
||||
// Action: assess a specific center
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'assess') {
|
||||
$centerId = filter_input(INPUT_POST, 'center_id', FILTER_VALIDATE_INT) ?: 0;
|
||||
$cycleId = filter_input(INPUT_POST, 'cycle_id', FILTER_VALIDATE_INT) ?: 0;
|
||||
|
||||
if ($centerId > 0 && $cycleId > 0) {
|
||||
// Check if already imported
|
||||
$stmt = $pdo->prepare('SELECT id FROM center_assessment_types WHERE center_application_id = :center_id AND cycle_id = :cycle_id AND global_template_id = :template_id LIMIT 1');
|
||||
$stmt->execute([
|
||||
':center_id' => $centerId,
|
||||
':cycle_id' => $cycleId,
|
||||
':template_id' => $templateId
|
||||
]);
|
||||
$existingAssessmentId = $stmt->fetchColumn();
|
||||
|
||||
if ($existingAssessmentId) {
|
||||
$assessmentId = (int)$existingAssessmentId;
|
||||
} else {
|
||||
// Import it
|
||||
try {
|
||||
$assessmentId = import_global_center_assessment_to_center($templateId, $centerId, $cycleId);
|
||||
} catch (Exception $e) {
|
||||
set_flash('error', 'فشل استيراد التقييم للمركز.');
|
||||
header("Location: execute_global_assessment.php?id={$templateId}&cycle={$cycleId}");
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$returnUrl = urlencode("execute_global_assessment.php?id={$templateId}&cycle={$cycleId}");
|
||||
header("Location: center_assessment_score_sheet.php?id={$centerId}&cycle={$cycleId}&assessment_id={$assessmentId}&return_url={$returnUrl}");
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch all approved centers and their assessment status for this template
|
||||
$centers = [];
|
||||
if ($requestedCycleId > 0) {
|
||||
// Left join with center_assessment_types and center_assessment_scores
|
||||
$query = "
|
||||
SELECT
|
||||
c.id, c.center_name, c.region,
|
||||
cat.id AS assessment_type_id,
|
||||
cas.status AS score_status,
|
||||
cas.score,
|
||||
cas.max_score
|
||||
FROM center_applications c
|
||||
LEFT JOIN center_assessment_types cat ON cat.center_application_id = c.id
|
||||
AND cat.cycle_id = :cycle_id
|
||||
AND cat.global_template_id = :template_id
|
||||
LEFT JOIN center_assessment_scores cas ON cas.assessment_type_id = cat.id
|
||||
WHERE c.status = 'approved'
|
||||
ORDER BY c.center_name ASC
|
||||
";
|
||||
$stmt = $pdo->prepare($query);
|
||||
$stmt->execute([
|
||||
':cycle_id' => $requestedCycleId,
|
||||
':template_id' => $templateId
|
||||
]);
|
||||
$centers = $stmt->fetchAll() ?: [];
|
||||
}
|
||||
|
||||
$pageTitle = 'تطبيق التقييم: ' . (string) ($template['title'] ?? '');
|
||||
$pageDescription = 'تنفيذ التقييم العام على المراكز المعتمدة.';
|
||||
render_page_start($pageTitle, 'admin', $pageDescription);
|
||||
$flash = consume_flash();
|
||||
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 admin-hero mb-4">
|
||||
<div class="row g-4 align-items-center">
|
||||
<div class="col-lg-8">
|
||||
<span class="admin-kicker mb-3">تطبيق قالب التقييم</span>
|
||||
<h1 class="page-title mb-3"><?= e((string) ($template['title'] ?? '')) ?></h1>
|
||||
<p class="page-copy mb-3">من هذه الشاشة يمكنك المرور على جميع المراكز ورصد درجاتها بناءً على هذا القالب الموحد.</p>
|
||||
<div class="hero-meta">
|
||||
<span>الوزن: <?= e((string) ($template['weight_percentage'] ?? '0')) ?>%</span>
|
||||
<span>الدرجة القصوى: <?= e((string) ($template['max_score'] ?? '0')) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 text-lg-end">
|
||||
<a class="btn btn-outline-secondary" href="global_center_assessments.php">العودة للقوالب</a>
|
||||
</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)$templateId) ?>">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label" for="cycle">الدورة المستهدفة</label>
|
||||
<select class="form-select" id="cycle" name="cycle" onchange="this.form.submit()">
|
||||
<?php if (count($cycles) === 0): ?>
|
||||
<option value="">لا توجد دورات</option>
|
||||
<?php else: ?>
|
||||
<?php foreach ($cycles as $c): ?>
|
||||
<option value="<?= e((string)$c['id']) ?>" <?= $c['id'] == $requestedCycleId ? 'selected' : '' ?>>
|
||||
<?= e((string)$c['cycle_name']) ?> <?= $c['status'] === 'active' ? '(نشطة)' : '' ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 d-grid">
|
||||
<button type="submit" class="btn btn-primary">عرض المراكز</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php if ($requestedCycleId > 0 && count($centers) > 0): ?>
|
||||
<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 class="text-end">الإجراءات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($centers as $center):
|
||||
$status = (string) ($center['score_status'] ?? '');
|
||||
$hasScore = $center['score'] !== null;
|
||||
$scoreVal = $hasScore ? rtrim(rtrim(number_format((float)$center['score'], 2, '.', ''), '0'), '.') : '—';
|
||||
$maxScore = $center['max_score'] !== null ? rtrim(rtrim(number_format((float)$center['max_score'], 2, '.', ''), '0'), '.') : '';
|
||||
|
||||
$statusBadge = '<span class="badge bg-secondary">لم يبدأ الرصد</span>';
|
||||
if ($status === 'draft') $statusBadge = '<span class="badge bg-warning text-dark">مسودة</span>';
|
||||
if ($status === 'completed') $statusBadge = '<span class="badge bg-success">مكتمل</span>';
|
||||
if ($status === 'waived') $statusBadge = '<span class="badge bg-info">مستثنى</span>';
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-semibold"><?= e((string) ($center['center_name'] ?? '')) ?></div>
|
||||
</td>
|
||||
<td><?= e((string) ($center['region'] ?? '—')) ?></td>
|
||||
<td><?= $statusBadge ?></td>
|
||||
<td>
|
||||
<?php if ($hasScore): ?>
|
||||
<span class="fw-bold"><?= e($scoreVal) ?></span> / <?= e($maxScore) ?>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">—</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<form method="post" style="display:inline-block;">
|
||||
<input type="hidden" name="action" value="assess">
|
||||
<input type="hidden" name="center_id" value="<?= e((string)$center['id']) ?>">
|
||||
<input type="hidden" name="cycle_id" value="<?= e((string)$requestedCycleId) ?>">
|
||||
<?php if ($status === 'completed'): ?>
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary">تعديل الرصد</button>
|
||||
<?php else: ?>
|
||||
<button type="submit" class="btn btn-sm btn-primary">رصد</button>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php elseif ($requestedCycleId > 0): ?>
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">لا توجد مراكز معتمدة</div>
|
||||
<p class="text-muted">لم يتم العثور على مراكز معتمدة لتقييمها في هذه الدورة.</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php render_page_end(); ?>
|
||||
68
fix_import.py
Normal file
68
fix_import.py
Normal file
@ -0,0 +1,68 @@
|
||||
import re
|
||||
|
||||
with open('includes/cycles.php', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
new_func = """function import_global_center_assessment_to_center(int $globalAssessmentId, int $centerApplicationId, int $cycleId): int
|
||||
{
|
||||
$pdo = db();
|
||||
$globalAssessment = get_global_center_assessment_type($globalAssessmentId);
|
||||
if (!$globalAssessment) {
|
||||
throw new InvalidArgumentException("Global assessment template not found.");
|
||||
}
|
||||
|
||||
$globalCriteria = list_global_center_assessment_criteria_by_assessment($globalAssessmentId, true);
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
$stmt = $pdo->prepare('INSERT INTO center_assessment_types
|
||||
(center_application_id, cycle_id, global_template_id, title, category, scale_type, max_score, weight_percentage, is_active, notes)
|
||||
VALUES (:center_application_id, :cycle_id, :global_template_id, :title, :category, :scale_type, :max_score, :weight_percentage, :is_active, :notes)');
|
||||
|
||||
$stmt->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':cycle_id', $cycleId, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':global_template_id', $globalAssessmentId, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':title', $globalAssessment['title'], PDO::PARAM_STR);
|
||||
$stmt->bindValue(':category', $globalAssessment['category'], PDO::PARAM_STR);
|
||||
$stmt->bindValue(':scale_type', $globalAssessment['scale_type'], PDO::PARAM_STR);
|
||||
$stmt->bindValue(':max_score', $globalAssessment['max_score'], PDO::PARAM_STR);
|
||||
$stmt->bindValue(':weight_percentage', $globalAssessment['weight_percentage'], PDO::PARAM_STR);
|
||||
$stmt->bindValue(':is_active', $globalAssessment['is_active'], PDO::PARAM_INT);
|
||||
$stmt->bindValue(':notes', $globalAssessment['notes'] ?? null, PDO::PARAM_STR);
|
||||
|
||||
$stmt->execute();
|
||||
$newAssessmentId = (int) $pdo->lastInsertId();
|
||||
|
||||
if ($globalCriteria !== []) {
|
||||
$stmtCrit = $pdo->prepare('INSERT INTO center_assessment_criteria
|
||||
(center_application_id, cycle_id, assessment_type_id, title, notes, max_score, sort_order, is_active)
|
||||
VALUES (:center_application_id, :cycle_id, :assessment_type_id, :title, :notes, :max_score, :sort_order, :is_active)');
|
||||
|
||||
foreach ($globalCriteria as $criteria) {
|
||||
$stmtCrit->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT);
|
||||
$stmtCrit->bindValue(':cycle_id', $cycleId, PDO::PARAM_INT);
|
||||
$stmtCrit->bindValue(':assessment_type_id', $newAssessmentId, PDO::PARAM_INT);
|
||||
$stmtCrit->bindValue(':title', $criteria['title'], PDO::PARAM_STR);
|
||||
$stmtCrit->bindValue(':notes', $criteria['notes'] ?? null, PDO::PARAM_STR);
|
||||
$stmtCrit->bindValue(':max_score', $criteria['max_score'], PDO::PARAM_STR);
|
||||
$stmtCrit->bindValue(':sort_order', $criteria['sort_order'] ?? 0, PDO::PARAM_INT);
|
||||
$stmtCrit->bindValue(':is_active', $criteria['is_active'], PDO::PARAM_INT);
|
||||
$stmtCrit->execute();
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
return $newAssessmentId;
|
||||
} catch (PDOException $e) {
|
||||
$pdo->rollBack();
|
||||
error_log("Failed to import global center assessment: " . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}"""
|
||||
|
||||
content = re.sub(r'function import_global_center_assessment_to_center.*?catch \(PDOException \$e\) \{\s*\$pdo->rollBack\(\);\s*error_log\("Failed to import global center assessment: " \. \$e->getMessage\(\)\);\s*throw \$e;\s*\}\s*\}', new_func, content, flags=re.DOTALL)
|
||||
|
||||
with open('includes/cycles.php', 'w') as f:
|
||||
f.write(content)
|
||||
print("Fixed import function.")
|
||||
222
global_center_assessment_criteria.php
Normal file
222
global_center_assessment_criteria.php
Normal file
@ -0,0 +1,222 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
if (!is_super_admin()) {
|
||||
http_response_code(403);
|
||||
render_page_start('صلاحيات غير كافية', 'admin', 'هذه الصفحة مخصصة للمشرف العام لإدارة بنود قوالب تقييم المراكز.');
|
||||
?>
|
||||
<section class="py-5 text-center">
|
||||
<div class="container-xxl">
|
||||
<h1 class="mb-3">عذراً</h1>
|
||||
<p>هذه الصفحة مخصصة للمشرف العام فقط.</p>
|
||||
<a href="admin.php" class="btn btn-primary mt-3">العودة إلى لوحة الإدارة</a>
|
||||
</div>
|
||||
</section>
|
||||
<?php
|
||||
render_page_end();
|
||||
exit;
|
||||
}
|
||||
|
||||
$assessmentId = filter_input(INPUT_GET, 'assessment_id', FILTER_VALIDATE_INT) ?: 0;
|
||||
if ($assessmentId <= 0) {
|
||||
set_flash('error', 'اختر أولاً قالب تقييم من صفحة القوالب العامة.');
|
||||
header('Location: global_center_assessments.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$assessment = get_global_center_assessment_type($assessmentId);
|
||||
if (!$assessment) {
|
||||
set_flash('error', 'قالب التقييم المطلوب غير موجود أو تم حذفه.');
|
||||
header('Location: global_center_assessments.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$flash = consume_flash();
|
||||
$criteria = list_global_center_assessment_criteria_by_assessment($assessmentId);
|
||||
$criteriaMetrics = global_center_assessment_criteria_metrics($assessmentId);
|
||||
$errors = [];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
[$submittedData, $errors] = validate_global_center_assessment_criteria_input($assessmentId, $_POST);
|
||||
if ($errors === []) {
|
||||
try {
|
||||
save_global_center_assessment_criteria($assessmentId, $submittedData);
|
||||
set_flash('success', 'تم حفظ بنود قالب تقييم المراكز بنجاح.');
|
||||
header('Location: global_center_assessment_criteria.php?assessment_id=' . $assessmentId);
|
||||
exit;
|
||||
} catch (Throwable $exception) {
|
||||
$errors['form'] = 'تعذر حفظ البنود حالياً. يرجى المحاولة مرة أخرى.';
|
||||
$criteria = $submittedData['criteria'];
|
||||
}
|
||||
} else {
|
||||
$criteria = $submittedData['criteria'];
|
||||
}
|
||||
}
|
||||
|
||||
$blankRows = 3;
|
||||
for ($i = 0; $i < $blankRows; $i++) {
|
||||
$criteria[] = [
|
||||
'id' => 0,
|
||||
'title' => '',
|
||||
'max_score' => '',
|
||||
'notes' => '',
|
||||
'is_active' => '1',
|
||||
];
|
||||
}
|
||||
|
||||
render_page_start(
|
||||
'بنود قالب تقييم المراكز',
|
||||
'admin',
|
||||
'إدارة البنود والمعايير التفصيلية للقالب العام الذي سيُستخدم لاحقاً عند تقييم المراكز.'
|
||||
);
|
||||
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">
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-8">
|
||||
<span class="eyebrow mb-3">بناء المعايير</span>
|
||||
<h1 class="page-title mb-3">بنود القالب: <?= e((string) ($assessment['title'] ?? '')) ?></h1>
|
||||
<p class="page-copy mb-3">أضف هنا البنود التي سيعتمد عليها المشرف العام أو المقيم المسؤول عند تقييم المراكز. مجموع البنود النشطة يحدّث الدرجة القصوى تلقائياً.</p>
|
||||
<div class="hero-meta">
|
||||
<span>الفئة <?= e((string) ($assessment['category'] ?? '')) ?></span>
|
||||
<span>المقياس <?= e(assessment_scale_type_label((string) ($assessment['scale_type'] ?? 'percentage'))) ?></span>
|
||||
<span>الوزن <?= e(rtrim(rtrim(number_format((float) ($assessment['weight_percentage'] ?? 0), 2, '.', ''), '0'), '.')) ?>٪</span>
|
||||
</div>
|
||||
<div class="cta-stack mt-4">
|
||||
<a class="btn btn-outline-secondary" href="global_center_assessments.php">العودة إلى القوالب</a>
|
||||
<a class="btn btn-primary" href="center_assessments.php">فتح صفحة تقييم المراكز</a>
|
||||
</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(rtrim(rtrim(number_format((float) ($assessment['max_score'] ?? 0), 2, '.', ''), '0'), '.')) ?></div>
|
||||
<div class="mini-stat-copy mb-3">يتم تحديثها تلقائياً وفق مجموع درجات البنود النشطة فقط.</div>
|
||||
<div><?= assessment_active_badge((int) ($assessment['is_active'] ?? 0)) ?></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">
|
||||
<div class="section-head mb-3">
|
||||
<div>
|
||||
<div class="section-title">إدارة البنود</div>
|
||||
<div class="section-copy">يمكنك تعديل البنود الحالية أو إضافة بنود جديدة. الصفوف الفارغة لن تُحفظ.</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" id="addCriteriaRow">إضافة صف جديد</button>
|
||||
</div>
|
||||
|
||||
<?php if (isset($errors['form'])): ?>
|
||||
<div class="alert alert-danger"><?= e($errors['form']) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php foreach ($errors as $errorKey => $message): ?>
|
||||
<?php if (str_starts_with($errorKey, 'criteria_')): ?>
|
||||
<div class="alert alert-warning py-2 mb-2"><?= e($message) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<form method="post">
|
||||
<div id="criteriaRows" class="d-grid gap-3">
|
||||
<?php foreach ($criteria as $index => $criterion): ?>
|
||||
<div class="border rounded-4 p-3 bg-white">
|
||||
<input type="hidden" name="criteria[<?= e((string) $index) ?>][id]" value="<?= e((string) ($criterion['id'] ?? 0)) ?>">
|
||||
<div class="row g-3 align-items-start">
|
||||
<div class="col-lg-4">
|
||||
<label class="form-label">اسم البند</label>
|
||||
<input class="form-control" name="criteria[<?= e((string) $index) ?>][title]" value="<?= e((string) ($criterion['title'] ?? '')) ?>" placeholder="مثال: الالتزام بالخطة التشغيلية">
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<label class="form-label">الدرجة</label>
|
||||
<input class="form-control" type="number" min="0.01" max="1000" step="0.01" name="criteria[<?= e((string) $index) ?>][max_score]" value="<?= e((string) ($criterion['max_score'] ?? '')) ?>" placeholder="10">
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<label class="form-label">الحالة</label>
|
||||
<select class="form-select" name="criteria[<?= e((string) $index) ?>][is_active]">
|
||||
<option value="1" <?= ((string) ($criterion['is_active'] ?? '1')) === '1' ? 'selected' : '' ?>>مفعّل</option>
|
||||
<option value="0" <?= ((string) ($criterion['is_active'] ?? '1')) === '0' ? 'selected' : '' ?>>مؤرشف</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<label class="form-label">ملاحظات</label>
|
||||
<input class="form-control" name="criteria[<?= e((string) $index) ?>][notes]" value="<?= e((string) ($criterion['notes'] ?? '')) ?>" placeholder="شرح مختصر لما يجب مراجعته في هذا البند">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mt-4">
|
||||
<button class="btn btn-primary" type="submit">حفظ البنود</button>
|
||||
<a class="btn btn-outline-secondary" href="global_center_assessments.php?edit=<?= e((string) $assessmentId) ?>#assessmentFormCard">تحرير القالب</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<template id="criteriaRowTemplate">
|
||||
<div class="border rounded-4 p-3 bg-white">
|
||||
<input type="hidden" data-name="id" value="0">
|
||||
<div class="row g-3 align-items-start">
|
||||
<div class="col-lg-4">
|
||||
<label class="form-label">اسم البند</label>
|
||||
<input class="form-control" data-name="title" placeholder="مثال: الجاهزية الإدارية">
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<label class="form-label">الدرجة</label>
|
||||
<input class="form-control" type="number" min="0.01" max="1000" step="0.01" data-name="max_score" placeholder="10">
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<label class="form-label">الحالة</label>
|
||||
<select class="form-select" data-name="is_active">
|
||||
<option value="1" selected>مفعّل</option>
|
||||
<option value="0">مؤرشف</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<label class="form-label">ملاحظات</label>
|
||||
<input class="form-control" data-name="notes" placeholder="شرح مختصر لما يجب فحصه">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const rowsContainer = document.getElementById('criteriaRows');
|
||||
const addButton = document.getElementById('addCriteriaRow');
|
||||
const template = document.getElementById('criteriaRowTemplate');
|
||||
if (!rowsContainer || !addButton || !template) return;
|
||||
|
||||
let rowIndex = rowsContainer.children.length;
|
||||
addButton.addEventListener('click', function () {
|
||||
const fragment = template.content.cloneNode(true);
|
||||
fragment.querySelectorAll('[data-name]').forEach(function (field) {
|
||||
field.setAttribute('name', 'criteria[' + rowIndex + '][' + field.getAttribute('data-name') + ']');
|
||||
});
|
||||
rowsContainer.appendChild(fragment);
|
||||
rowIndex += 1;
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?php render_page_end(); ?>
|
||||
432
global_center_assessments.php
Normal file
432
global_center_assessments.php
Normal file
@ -0,0 +1,432 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
if (!is_super_admin()) {
|
||||
http_response_code(403);
|
||||
render_page_start('صلاحيات غير كافية', 'admin', 'هذه الصفحة مخصصة للمشرف العام لإدارة قوالب تقييم المراكز.');
|
||||
?>
|
||||
<section class="py-5 text-center">
|
||||
<div class="container-xxl">
|
||||
<h1 class="mb-3">عذراً</h1>
|
||||
<p>هذه الصفحة مخصصة للمشرف العام فقط.</p>
|
||||
<a href="admin.php" class="btn btn-primary mt-3">العودة إلى لوحة الإدارة</a>
|
||||
</div>
|
||||
</section>
|
||||
<?php
|
||||
render_page_end();
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? 'create';
|
||||
$assessmentId = filter_input(INPUT_POST, 'assessment_id', FILTER_VALIDATE_INT) ?: 0;
|
||||
|
||||
if ($action === 'delete') {
|
||||
if ($assessmentId > 0 && delete_global_center_assessment_type($assessmentId)) {
|
||||
set_flash('success', 'تم حذف قالب التقييم بنجاح.');
|
||||
} else {
|
||||
set_flash('error', 'حدث خطأ أثناء محاولة حذف قالب التقييم.');
|
||||
}
|
||||
header('Location: global_center_assessments.php');
|
||||
exit;
|
||||
} else {
|
||||
[$values, $errors] = validate_assessment_input($_POST);
|
||||
|
||||
if ($errors === []) {
|
||||
try {
|
||||
if ($action === 'edit' && $assessmentId > 0) {
|
||||
update_global_center_assessment_type($assessmentId, $values);
|
||||
set_flash('success', 'تم تحديث قالب تقييم المراكز بنجاح.');
|
||||
} else {
|
||||
$assessmentId = create_global_center_assessment_type($values);
|
||||
set_flash('success', 'تم إنشاء قالب تقييم جديد للمراكز. يمكنك الآن إضافة البنود الخاصة به.');
|
||||
}
|
||||
header('Location: global_center_assessments.php' . ($assessmentId > 0 && $action !== 'edit' ? '?highlight=' . $assessmentId : ''));
|
||||
exit;
|
||||
} catch (Throwable $exception) {
|
||||
set_flash('error', 'تعذر حفظ قالب التقييم حالياً. يرجى المحاولة مرة أخرى.');
|
||||
header('Location: global_center_assessments.php');
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
$errorMsg = implode(' ', $errors);
|
||||
set_flash('error', $errorMsg);
|
||||
header('Location: global_center_assessments.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$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 = list_global_center_assessments($filters, $limit, $offset);
|
||||
$totalAssessments = count_global_center_assessments($filters);
|
||||
$metrics = global_center_assessment_metrics();
|
||||
$highlightAssessmentId = filter_input(INPUT_GET, 'highlight', FILTER_VALIDATE_INT) ?: 0;
|
||||
$activeWeight = round((float) $metrics['active_weight'], 2);
|
||||
$weightGap = round(100 - $activeWeight, 2);
|
||||
|
||||
$flash = consume_flash();
|
||||
render_page_start(
|
||||
'قوالب تقييم المراكز',
|
||||
'admin',
|
||||
'إدارة القوالب العامة لتقييم المراكز وبنودها حتى يتمكن المشرف العام أو المقيمون المكلّفون من استخدامها لاحقاً عند تقييم كل مركز.'
|
||||
);
|
||||
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">
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-8">
|
||||
<span class="eyebrow mb-3">مكتبة التقييم الإشرافي</span>
|
||||
<h1 class="page-title mb-3">قوالب تقييم المراكز</h1>
|
||||
<p class="page-copy mb-3">من هنا ينشئ المشرف العام التقييمات العامة للمراكز مرة واحدة، ثم تُستخدم لاحقاً من قبل المشرف العام أو أي مقيّم مكلّف لتقييم المراكز واحداً تلو الآخر.</p>
|
||||
<div class="hero-meta">
|
||||
<span>قوالب نشطة <?= e((string) $metrics['active']) ?></span>
|
||||
<span>بنفس المنهجية السابقة للبنود والرصد</span>
|
||||
<span>إجمالي الوزن النشط <?= e(rtrim(rtrim(number_format($activeWeight, 2, '.', ''), '0'), '.')) ?>٪</span>
|
||||
</div>
|
||||
<div class="cta-stack mt-4">
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createModal">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
|
||||
</svg>
|
||||
إضافة قالب جديد
|
||||
</button>
|
||||
<a class="btn btn-outline-secondary" href="center_assessments.php">فتح تقييم المراكز الحالي</a>
|
||||
</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(rtrim(rtrim(number_format($weightGap, 2, '.', ''), '0'), '.')) ?>٪</div>
|
||||
<div class="mini-stat-copy mb-3">كلما اقتربت القوالب النشطة من 100٪ صار توزيع التقييم أوضح للمقيمين عند تقييم المراكز ميدانياً.</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(rtrim(rtrim(number_format((float) $metrics['average_max_score'], 2, '.', ''), '0'), '.')) ?></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['percentage'] + $metrics['points'] + $metrics['rubric'])) ?></div><div class="mini-stat-copy">نسبة: <?= e((string) $metrics['percentage']) ?> • نقاط: <?= e((string) $metrics['points']) ?> • Rubric: <?= e((string) $metrics['rubric']) ?></div></div></div>
|
||||
</div>
|
||||
|
||||
<div class="app-card mb-4">
|
||||
<div class="section-head mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-0 flex-wrap gap-3">
|
||||
<div>
|
||||
<div class="section-title">إدارة قوالب التقييم</div>
|
||||
<div class="section-copy">كل قالب هنا يمثل نموذج تقييم إشرافي يمكن لاحقاً استخدامه عند تقييم أي مركز.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php render_search_bar($filters['search'], 'ابحث باسم القالب أو الفئة أو الملاحظات...', 'global_center_assessments.php', $_GET); ?>
|
||||
|
||||
<form method="get" class="row g-3 align-items-end mb-4">
|
||||
<input type="hidden" name="search" value="<?= e($filters['search']) ?>">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label" for="categoryFilter">الفئة</label>
|
||||
<select class="form-select" id="categoryFilter" name="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-4 d-grid">
|
||||
<button class="btn btn-outline-secondary" type="submit">تطبيق الفلترة</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table app-table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>القالب</th>
|
||||
<th>الفئة</th>
|
||||
<th>المقياس</th>
|
||||
<th>الوزن</th>
|
||||
<th>البنود</th>
|
||||
<th>الحالة</th>
|
||||
<th>الإجراءات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($assessments === []): ?>
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-4 text-muted">لا توجد قوالب تقييم مسجلة أو لم يتم العثور على نتائج.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($assessments as $assessment): ?>
|
||||
<?php $isHighlighted = (int) ($assessment['id'] ?? 0) === $highlightAssessmentId; ?>
|
||||
<tr<?= $isHighlighted ? ' class="table-warning"' : '' ?>>
|
||||
<td>
|
||||
<div class="fw-semibold"><?= e((string) ($assessment['title'] ?? '')) ?></div>
|
||||
<div class="text-muted small" style="max-width: 250px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="<?= e((string) ($assessment['notes'] ?? '')) ?>"><?= e((string) ($assessment['notes'] ?? 'بدون ملاحظات إضافية')) ?></div>
|
||||
</td>
|
||||
<td><?= e((string) ($assessment['category'] ?? '')) ?></td>
|
||||
<td><?= assessment_scale_type_badge((string) ($assessment['scale_type'] ?? '')) ?></td>
|
||||
<td>
|
||||
<div class="fw-semibold"><?= e(rtrim(rtrim(number_format((float) ($assessment['weight_percentage'] ?? 0), 2, '.', ''), '0'), '.')) ?>٪</div>
|
||||
<div class="text-muted small">الدرجة: <?= e(rtrim(rtrim(number_format((float) ($assessment['max_score'] ?? 0), 2, '.', ''), '0'), '.')) ?></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-semibold"><?= e((string) ((int) ($assessment['criteria_count'] ?? 0))) ?></div>
|
||||
<div class="text-muted small">مجموع النشط: <?= e(rtrim(rtrim(number_format((float) ($assessment['criteria_total_max_score'] ?? 0), 2, '.', ''), '0'), '.')) ?></div>
|
||||
</td>
|
||||
<td><?= assessment_active_badge((int) ($assessment['is_active'] ?? 0)) ?></td>
|
||||
<td>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-secondary" title="تعديل"
|
||||
data-bs-toggle="modal" data-bs-target="#editModal"
|
||||
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(rtrim(rtrim(number_format((float) ($assessment['max_score'] ?? 0), 2, '.', ''), '0'), '.')) ?>"
|
||||
data-weight_percentage="<?= e(rtrim(rtrim(number_format((float) ($assessment['weight_percentage'] ?? 0), 2, '.', ''), '0'), '.')) ?>"
|
||||
data-is_active="<?= (int) ($assessment['is_active'] ?? 0) === 1 ? '1' : '0' ?>"
|
||||
data-notes="<?= e((string) ($assessment['notes'] ?? '')) ?>"
|
||||
onclick="fillEditModal(this)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" title="حذف"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
||||
data-id="<?= e((string) ($assessment['id'] ?? 0)) ?>"
|
||||
data-title="<?= e((string) ($assessment['title'] ?? '')) ?>"
|
||||
onclick="fillDeleteModal(this)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||
<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 .5.5v6a.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 fill-rule="evenodd" 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-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<a class="btn btn-primary btn-sm" href="global_center_assessment_criteria.php?assessment_id=<?= e((string) ($assessment['id'] ?? 0)) ?>" title="البنود">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M14.5 3a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-13a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h13zm-13-1A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2h-13z"/>
|
||||
<path d="M3 5.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zM3 8.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zM3 11.5a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="btn btn-success btn-sm" href="execute_global_assessment.php?id=<?= e((string) ($assessment['id'] ?? 0)) ?>" title="تطبيق التقييم">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
|
||||
</svg> تطبيق
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if ($assessments !== []): ?>
|
||||
<div class="mt-4">
|
||||
<?php render_pagination($totalAssessments, $limit, $page, $_GET); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Create Modal -->
|
||||
<div class="modal fade" id="createModal" tabindex="-1" aria-labelledby="createModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<form method="POST" action="global_center_assessments.php">
|
||||
<input type="hidden" name="action" value="create">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="createModalLabel">إضافة قالب تقييم جديد</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="create_title">اسم التقييم</label>
|
||||
<input class="form-control" id="create_title" name="title" placeholder="مثال: الزيارة الإشرافية العامة" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="create_category">الفئة</label>
|
||||
<select class="form-select" id="create_category" name="category">
|
||||
<?php foreach (assessment_category_options() as $categoryOption): ?>
|
||||
<option value="<?= e($categoryOption) ?>"><?= e($categoryOption) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="create_scale_type">مقياس التقييم</label>
|
||||
<select class="form-select" id="create_scale_type" name="scale_type">
|
||||
<?php foreach (assessment_scale_type_map() as $scaleKey => $scaleMeta): ?>
|
||||
<option value="<?= e($scaleKey) ?>"><?= e((string) $scaleMeta['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="create_max_score">الدرجة القصوى</label>
|
||||
<input class="form-control" id="create_max_score" type="number" min="0.01" max="1000" step="0.01" name="max_score" value="100">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="create_weight_percentage">الوزن النسبي ٪</label>
|
||||
<input class="form-control" id="create_weight_percentage" type="number" min="0" max="100" step="0.01" name="weight_percentage" value="100">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="create_is_active">الحالة</label>
|
||||
<select class="form-select" id="create_is_active" name="is_active">
|
||||
<option value="1" selected>مفعّل</option>
|
||||
<option value="0">مؤرشف</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="create_notes">ملاحظات داخلية</label>
|
||||
<textarea class="form-control" id="create_notes" name="notes" rows="3" placeholder="مثلاً: يستخدم في الزيارات الإشرافية الفصلية..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" class="btn btn-primary">إنشاء القالب</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Modal -->
|
||||
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<form method="POST" action="global_center_assessments.php">
|
||||
<input type="hidden" name="action" value="edit">
|
||||
<input type="hidden" name="assessment_id" id="edit_assessment_id">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editModalLabel">تعديل قالب التقييم</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="edit_title">اسم التقييم</label>
|
||||
<input class="form-control" id="edit_title" name="title" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="edit_category">الفئة</label>
|
||||
<select class="form-select" id="edit_category" name="category">
|
||||
<?php foreach (assessment_category_options() as $categoryOption): ?>
|
||||
<option value="<?= e($categoryOption) ?>"><?= e($categoryOption) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="edit_scale_type">مقياس التقييم</label>
|
||||
<select class="form-select" id="edit_scale_type" name="scale_type">
|
||||
<?php foreach (assessment_scale_type_map() as $scaleKey => $scaleMeta): ?>
|
||||
<option value="<?= e($scaleKey) ?>"><?= e((string) $scaleMeta['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="edit_max_score">الدرجة القصوى</label>
|
||||
<input class="form-control" id="edit_max_score" type="number" min="0.01" max="1000" step="0.01" name="max_score">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="edit_weight_percentage">الوزن النسبي ٪</label>
|
||||
<input class="form-control" id="edit_weight_percentage" type="number" min="0" max="100" step="0.01" name="weight_percentage">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="edit_is_active">الحالة</label>
|
||||
<select class="form-select" id="edit_is_active" name="is_active">
|
||||
<option value="1">مفعّل</option>
|
||||
<option value="0">مؤرشف</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="edit_notes">ملاحظات داخلية</label>
|
||||
<textarea class="form-control" id="edit_notes" name="notes" rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" class="btn btn-primary">حفظ التعديلات</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Modal -->
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="POST" action="global_center_assessments.php">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="assessment_id" id="delete_assessment_id">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteModalLabel">تأكيد حذف القالب</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>هل أنت متأكد أنك تريد حذف القالب <strong id="delete_assessment_title"></strong>؟</p>
|
||||
<p class="text-danger small">ملاحظة: سيتم حذف جميع البنود المرتبطة بهذا القالب أيضاً. هذا الإجراء لا يمكن التراجع عنه.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" class="btn btn-danger">تأكيد الحذف</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function fillEditModal(btn) {
|
||||
document.getElementById('edit_assessment_id').value = btn.getAttribute('data-id');
|
||||
document.getElementById('edit_title').value = btn.getAttribute('data-title');
|
||||
document.getElementById('edit_category').value = btn.getAttribute('data-category');
|
||||
document.getElementById('edit_scale_type').value = btn.getAttribute('data-scale_type');
|
||||
document.getElementById('edit_max_score').value = btn.getAttribute('data-max_score');
|
||||
document.getElementById('edit_weight_percentage').value = btn.getAttribute('data-weight_percentage');
|
||||
document.getElementById('edit_is_active').value = btn.getAttribute('data-is_active');
|
||||
document.getElementById('edit_notes').value = btn.getAttribute('data-notes');
|
||||
}
|
||||
|
||||
function fillDeleteModal(btn) {
|
||||
document.getElementById('delete_assessment_id').value = btn.getAttribute('data-id');
|
||||
document.getElementById('delete_assessment_title').textContent = btn.getAttribute('data-title');
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php render_page_end(); ?>
|
||||
@ -112,6 +112,7 @@ function db_connection(): PDO
|
||||
ensure_school_assessment_score_schema($pdo);
|
||||
ensure_school_assessment_criteria_schema($pdo);
|
||||
ensure_center_assessment_schema($pdo);
|
||||
ensure_global_center_assessment_schema($pdo);
|
||||
seed_school_module_demo_data($pdo);
|
||||
$bootstrapped = true;
|
||||
}
|
||||
|
||||
@ -159,6 +159,24 @@ function ensure_center_assessment_schema(PDO $pdo): void
|
||||
$done = true;
|
||||
}
|
||||
|
||||
function ensure_global_center_assessment_schema(PDO $pdo): void
|
||||
{
|
||||
static $done = false;
|
||||
if ($done) {
|
||||
return;
|
||||
}
|
||||
|
||||
$migrationPath = __DIR__ . '/../db/migrations/20260417_global_center_assessments.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(
|
||||
@ -3218,3 +3236,494 @@ function update_assessment_type_in_cycle(int $centerApplicationId, int $cycleId,
|
||||
':notes' => $data['notes'] !== '' ? $data['notes'] : null,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
function get_global_center_assessment_type(int $assessmentId): ?array
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->prepare('SELECT * FROM global_center_assessment_types WHERE id = :id LIMIT 1');
|
||||
$stmt->execute([':id' => $assessmentId]);
|
||||
$row = $stmt->fetch();
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
function create_global_center_assessment_type(array $data): int
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO global_center_assessment_types (
|
||||
title, category, scale_type, max_score, weight_percentage,
|
||||
is_active, notes, created_at, updated_at
|
||||
) VALUES (
|
||||
:title, :category, :scale_type, :max_score, :weight_percentage,
|
||||
:is_active, :notes, NOW(), NOW()
|
||||
)'
|
||||
);
|
||||
|
||||
$stmt->execute([
|
||||
':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 update_global_center_assessment_type(int $assessmentId, array $data): bool
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->prepare(
|
||||
'UPDATE global_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'
|
||||
);
|
||||
|
||||
return $stmt->execute([
|
||||
':id' => $assessmentId,
|
||||
':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 list_global_center_assessments(array $filters = [], int $limit = 0, int $offset = 0): array
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$query = 'SELECT gcat.*,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM global_center_assessment_criteria criteria
|
||||
WHERE criteria.assessment_type_id = gcat.id
|
||||
AND criteria.is_active = 1
|
||||
) AS criteria_count,
|
||||
(
|
||||
SELECT COALESCE(SUM(criteria.max_score), 0)
|
||||
FROM global_center_assessment_criteria criteria
|
||||
WHERE criteria.assessment_type_id = gcat.id
|
||||
AND criteria.is_active = 1
|
||||
) AS criteria_total_max_score
|
||||
FROM global_center_assessment_types gcat
|
||||
WHERE 1 = 1';
|
||||
$params = [];
|
||||
|
||||
$search = trim((string) ($filters['search'] ?? ''));
|
||||
if ($search !== '') {
|
||||
$query .= ' AND (gcat.title LIKE :search1 OR gcat.category LIKE :search2 OR gcat.notes LIKE :search3)';
|
||||
$params[':search1'] = "%{$search}%";
|
||||
$params[':search2'] = "%{$search}%";
|
||||
$params[':search3'] = "%{$search}%";
|
||||
}
|
||||
|
||||
$category = trim((string) ($filters['category'] ?? ''));
|
||||
if ($category !== '') {
|
||||
$query .= ' AND gcat.category = :category';
|
||||
$params[':category'] = $category;
|
||||
}
|
||||
|
||||
$query .= ' ORDER BY gcat.is_active DESC, gcat.updated_at DESC, gcat.id DESC';
|
||||
|
||||
if ($limit > 0) {
|
||||
$query .= ' LIMIT ' . (int) $limit . ' OFFSET ' . (int) $offset;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare($query);
|
||||
$stmt->execute($params);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
function count_global_center_assessments(array $filters = []): int
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$query = 'SELECT COUNT(*) FROM global_center_assessment_types WHERE 1 = 1';
|
||||
$params = [];
|
||||
|
||||
$search = trim((string) ($filters['search'] ?? ''));
|
||||
if ($search !== '') {
|
||||
$query .= ' AND (title LIKE :search1 OR category LIKE :search2 OR notes LIKE :search3)';
|
||||
$params[':search1'] = "%{$search}%";
|
||||
$params[':search2'] = "%{$search}%";
|
||||
$params[':search3'] = "%{$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 global_center_assessment_metrics(): array
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->query(
|
||||
"SELECT
|
||||
COUNT(*) AS total_count,
|
||||
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 global_center_assessment_types"
|
||||
);
|
||||
$row = $stmt ? ($stmt->fetch() ?: []) : [];
|
||||
|
||||
return [
|
||||
'total' => (int) ($row['total_count'] ?? 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 global_center_assessment_type_options(bool $onlyActive = false): array
|
||||
{
|
||||
$rows = list_global_center_assessments();
|
||||
$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 : ('قالب #' . $assessmentId);
|
||||
$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 ? 1 : 0,
|
||||
];
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
function list_global_center_assessment_criteria_by_assessment(int $assessmentTypeId, bool $onlyActive = false): array
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$query = 'SELECT *
|
||||
FROM global_center_assessment_criteria
|
||||
WHERE assessment_type_id = :assessment_type_id';
|
||||
if ($onlyActive) {
|
||||
$query .= ' AND is_active = 1';
|
||||
}
|
||||
$query .= ' ORDER BY sort_order ASC, id ASC';
|
||||
|
||||
$stmt = $pdo->prepare($query);
|
||||
$stmt->execute([':assessment_type_id' => $assessmentTypeId]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
function global_center_assessment_criteria_metrics(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 global_center_assessment_criteria
|
||||
WHERE assessment_type_id = :assessment_type_id"
|
||||
);
|
||||
$stmt->execute([':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_global_center_assessment_criteria_input(int $assessmentTypeId, array $input): array
|
||||
{
|
||||
$data = ['criteria' => []];
|
||||
$errors = [];
|
||||
$assessment = get_global_center_assessment_type($assessmentTypeId);
|
||||
if (!$assessment) {
|
||||
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_global_center_assessment_total_score_from_criteria(int $assessmentTypeId): void
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$criteria = list_global_center_assessment_criteria_by_assessment($assessmentTypeId, true);
|
||||
if ($criteria === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$totalMaxScore = 0.0;
|
||||
foreach ($criteria as $criterion) {
|
||||
$totalMaxScore += (float) ($criterion['max_score'] ?? 0);
|
||||
}
|
||||
$totalMaxScore = round($totalMaxScore, 2);
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
'UPDATE global_center_assessment_types
|
||||
SET max_score = :max_score, updated_at = NOW()
|
||||
WHERE id = :id'
|
||||
);
|
||||
$stmt->execute([
|
||||
':max_score' => $totalMaxScore,
|
||||
':id' => $assessmentTypeId,
|
||||
]);
|
||||
}
|
||||
|
||||
function save_global_center_assessment_criteria(int $assessmentTypeId, array $data): int
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$existingStmt = $pdo->prepare(
|
||||
'SELECT id FROM global_center_assessment_criteria WHERE assessment_type_id = :assessment_type_id'
|
||||
);
|
||||
$existingStmt->execute([':assessment_type_id' => $assessmentTypeId]);
|
||||
$existingIds = array_map('intval', $existingStmt->fetchAll(PDO::FETCH_COLUMN));
|
||||
$existingMap = array_fill_keys($existingIds, true);
|
||||
|
||||
$insertStmt = $pdo->prepare(
|
||||
'INSERT INTO global_center_assessment_criteria (
|
||||
assessment_type_id, title, max_score, sort_order, is_active, notes, created_at, updated_at
|
||||
) VALUES (
|
||||
:assessment_type_id, :title, :max_score, :sort_order, :is_active, :notes, NOW(), NOW()
|
||||
)'
|
||||
);
|
||||
$updateStmt = $pdo->prepare(
|
||||
'UPDATE global_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 assessment_type_id = :assessment_type_id'
|
||||
);
|
||||
|
||||
$saved = 0;
|
||||
foreach ($data['criteria'] as $criterion) {
|
||||
$params = [
|
||||
':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]);
|
||||
unset($existingMap[$criterionId]);
|
||||
} else {
|
||||
$insertStmt->execute($params);
|
||||
}
|
||||
$saved++;
|
||||
}
|
||||
|
||||
if ($existingMap !== []) {
|
||||
$deleteStmt = $pdo->prepare(
|
||||
'DELETE FROM global_center_assessment_criteria
|
||||
WHERE assessment_type_id = :assessment_type_id AND id = :id'
|
||||
);
|
||||
foreach (array_keys($existingMap) as $obsoleteId) {
|
||||
$deleteStmt->execute([
|
||||
':assessment_type_id' => $assessmentTypeId,
|
||||
':id' => (int) $obsoleteId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
sync_global_center_assessment_total_score_from_criteria($assessmentTypeId);
|
||||
return $saved;
|
||||
}
|
||||
|
||||
|
||||
function delete_global_center_assessment_type(int $assessmentId): bool
|
||||
{
|
||||
$pdo = db();
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
$stmt = $pdo->prepare('DELETE FROM global_center_assessment_criteria WHERE assessment_type_id = :assessment_type_id');
|
||||
$stmt->bindValue(':assessment_type_id', $assessmentId, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
|
||||
$stmt = $pdo->prepare('DELETE FROM global_center_assessment_types WHERE id = :id');
|
||||
$stmt->bindValue(':id', $assessmentId, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
|
||||
$pdo->commit();
|
||||
return true;
|
||||
} catch (PDOException $e) {
|
||||
$pdo->rollBack();
|
||||
error_log("Failed to delete global_center_assessment_type ID $assessmentId: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function import_global_center_assessment_to_center(int $globalAssessmentId, int $centerApplicationId, int $cycleId): int
|
||||
{
|
||||
$pdo = db();
|
||||
$globalAssessment = get_global_center_assessment_type($globalAssessmentId);
|
||||
if (!$globalAssessment) {
|
||||
throw new InvalidArgumentException("Global assessment template not found.");
|
||||
}
|
||||
|
||||
$globalCriteria = list_global_center_assessment_criteria_by_assessment($globalAssessmentId, true);
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
$stmt = $pdo->prepare('INSERT INTO center_assessment_types
|
||||
(center_application_id, cycle_id, global_template_id, title, category, scale_type, max_score, weight_percentage, is_active, notes)
|
||||
VALUES (:center_application_id, :cycle_id, :global_template_id, :title, :category, :scale_type, :max_score, :weight_percentage, :is_active, :notes)');
|
||||
|
||||
$stmt->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':cycle_id', $cycleId, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':global_template_id', $globalAssessmentId, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':title', $globalAssessment['title'], PDO::PARAM_STR);
|
||||
$stmt->bindValue(':category', $globalAssessment['category'], PDO::PARAM_STR);
|
||||
$stmt->bindValue(':scale_type', $globalAssessment['scale_type'], PDO::PARAM_STR);
|
||||
$stmt->bindValue(':max_score', $globalAssessment['max_score'], PDO::PARAM_STR);
|
||||
$stmt->bindValue(':weight_percentage', $globalAssessment['weight_percentage'], PDO::PARAM_STR);
|
||||
$stmt->bindValue(':is_active', $globalAssessment['is_active'], PDO::PARAM_INT);
|
||||
$stmt->bindValue(':notes', $globalAssessment['notes'] ?? null, PDO::PARAM_STR);
|
||||
|
||||
$stmt->execute();
|
||||
$newAssessmentId = (int) $pdo->lastInsertId();
|
||||
|
||||
if ($globalCriteria !== []) {
|
||||
$stmtCrit = $pdo->prepare('INSERT INTO center_assessment_criteria
|
||||
(center_application_id, cycle_id, assessment_type_id, title, notes, max_score, sort_order, is_active)
|
||||
VALUES (:center_application_id, :cycle_id, :assessment_type_id, :title, :notes, :max_score, :sort_order, :is_active)');
|
||||
|
||||
foreach ($globalCriteria as $criteria) {
|
||||
$stmtCrit->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT);
|
||||
$stmtCrit->bindValue(':cycle_id', $cycleId, PDO::PARAM_INT);
|
||||
$stmtCrit->bindValue(':assessment_type_id', $newAssessmentId, PDO::PARAM_INT);
|
||||
$stmtCrit->bindValue(':title', $criteria['title'], PDO::PARAM_STR);
|
||||
$stmtCrit->bindValue(':notes', $criteria['notes'] ?? null, PDO::PARAM_STR);
|
||||
$stmtCrit->bindValue(':max_score', $criteria['max_score'], PDO::PARAM_STR);
|
||||
$stmtCrit->bindValue(':sort_order', $criteria['sort_order'] ?? 0, PDO::PARAM_INT);
|
||||
$stmtCrit->bindValue(':is_active', $criteria['is_active'], PDO::PARAM_INT);
|
||||
$stmtCrit->execute();
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
return $newAssessmentId;
|
||||
} catch (PDOException $e) {
|
||||
$pdo->rollBack();
|
||||
error_log("Failed to import global center assessment: " . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,8 @@ if ($script === 'global_cycles.php') $activePage = 'global_cycles';
|
||||
if ($script === 'dashboard.php') $activePage = 'dashboard';
|
||||
if ($script === 'applications.php') $activePage = ($statusQuery === 'approved') ? 'approved' : 'applications';
|
||||
if (in_array($script, ['approved_school.php', 'center_profile.php', 'students.php', 'teachers.php', 'assessments.php', 'attendance.php'], true)) $activePage = 'approved';
|
||||
if (in_array($script, ['center_assessments.php', 'center_assessment_criteria.php', 'center_assessment_score_sheet.php', 'center_assessment_report.php'], true)) $activePage = 'center_assessments';
|
||||
if (in_array($script, ['global_center_assessments.php', 'global_center_assessment_criteria.php'], true)) $activePage = 'global_center_assessments';
|
||||
if ($script === 'application_detail.php') $activePage = 'applications';
|
||||
if ($script === 'modules.php') $activePage = 'modules';
|
||||
|
||||
@ -32,6 +34,14 @@ if (!isset($recentApproved)) {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M14.763.075A.5.5 0 0 1 15 .5v15a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5V14h-1v1.5a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5V10a.5.5 0 0 1 .342-.474L6 7.64V4.5a.5.5 0 0 1 .276-.447l8-4a.5.5 0 0 1 .487.022zM6 8.694 1 10.36V15h5V8.694zM7 15h2v-1.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5V15h2V1.309l-7 3.5V15z"/><path d="M2 11h1v1H2v-1zm2 0h1v1H4v-1zm-2 2h1v1H2v-1zm2 0h1v1H4v-1zm4-4h1v1H8V9zm2 0h1v1h-1V9zm-2 2h1v1H8v-1zm2 0h1v1h-1v-1zm2-2h1v1h-1V9zm0 2h1v1h-1v-1zM8 7h1v1H8V7zm2 0h1v1h-1V7zm2 0h1v1h-1V7zM8 5h1v1H8V5zm2 0h1v1h-1V5zm2 0h1v1h-1V5zm0-2h1v1h-1V3z"/></svg>
|
||||
المراكز المعتمدة
|
||||
</a>
|
||||
<a href="center_assessments.php" class="sidebar-link <?= $activePage === 'center_assessments' ? 'active' : '' ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path d="M3 14s-1 0-1-1 1-4 5-4 5 3 5 4-1 1-1 1H3Zm5-5a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"/><path d="M12.5 8a.5.5 0 0 1 .5.5V10h1.5a.5.5 0 0 1 0 1H13v1.5a.5.5 0 0 1-1 0V11h-1.5a.5.5 0 0 1 0-1H12V8.5a.5.5 0 0 1 .5-.5Z"/><path d="M1 2a2 2 0 0 1 2-2h7.5a.5.5 0 0 1 0 1H3a1 1 0 0 0-1 1v7.256A4.493 4.493 0 0 1 4.528 8h.471a4 4 0 1 1 6.002 0h.471c.506 0 .992.084 1.446.238A1.999 1.999 0 0 0 15 6.5V4a.5.5 0 0 1 1 0v2.5A3 3 0 0 1 13 9.47V14a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V2Z"/></svg>
|
||||
تقييم المراكز
|
||||
</a>
|
||||
<a href="global_center_assessments.php" class="sidebar-link <?= $activePage === 'global_center_assessments' ? 'active' : '' ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path d="M8 1a3 3 0 0 0-3 3v1H4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-1V4a3 3 0 0 0-3-3Zm2 4H6V4a2 2 0 1 1 4 0v1Z"/><path d="M5.5 8a.5.5 0 0 1 .5.5V9h4v-.5a.5.5 0 0 1 1 0V9h.5a.5.5 0 0 1 0 1H11v.5a.5.5 0 0 1-1 0V10H6v.5a.5.5 0 0 1-1 0V10h-.5a.5.5 0 0 1 0-1H5v-.5a.5.5 0 0 1 .5-.5Z"/></svg>
|
||||
قوالب تقييم المراكز
|
||||
</a>
|
||||
<a href="center_application.php" class="sidebar-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path d="M8 6.5a.5.5 0 0 1 .5.5v1.5H10a.5.5 0 0 1 0 1H8.5V11a.5.5 0 0 1-1 0V9.5H6a.5.5 0 0 1 0-1h1.5V7a.5.5 0 0 1 .5-.5z"/><path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/></svg>
|
||||
طلب فتح مركز
|
||||
|
||||
21
patch_global.py
Normal file
21
patch_global.py
Normal file
@ -0,0 +1,21 @@
|
||||
import re
|
||||
|
||||
with open('global_center_assessments.php', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
replacement = """<a class="btn btn-primary btn-sm" href="global_center_assessment_criteria.php?assessment_id=<?= e((string) ($assessment['id'] ?? 0)) ?>" title="البنود">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M14.5 3a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-13a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h13zm-13-1A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2h-13z"/>
|
||||
<path d="M3 5.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zM3 8.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zM3 11.5a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="btn btn-success btn-sm" href="execute_global_assessment.php?id=<?= e((string) ($assessment['id'] ?? 0)) ?>" title="تطبيق التقييم">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
|
||||
</svg> تطبيق
|
||||
</a>"""
|
||||
|
||||
content = re.sub(r'<a class="btn btn-primary btn-sm" href="global_center_assessment_criteria\.php.*?</svg>\s*</a>', replacement, content, flags=re.DOTALL)
|
||||
|
||||
with open('global_center_assessments.php', 'w') as f:
|
||||
f.write(content)
|
||||
7
run_migration.php
Normal file
7
run_migration.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
require 'db/config.php';
|
||||
$pdo = db();
|
||||
$sql = file_get_contents('db/migrations/20260417_add_global_template_id_to_assessment.sql');
|
||||
$pdo->exec($sql);
|
||||
echo "Migration applied.\n";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user