306 lines
19 KiB
PHP
306 lines
19 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
require_once __DIR__ . '/includes/app.php';
|
|
|
|
$flash = consume_flash();
|
|
$applicationId = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT) ?: 0;
|
|
$requestedCycleId = filter_input(INPUT_GET, 'cycle', FILTER_VALIDATE_INT) ?: 0;
|
|
$requestedAssessmentId = filter_input(INPUT_GET, 'assessment_id', FILTER_VALIDATE_INT) ?: 0;
|
|
$application = $applicationId > 0 ? get_application($applicationId) : null;
|
|
$isApprovedSchool = $application && (string) $application['status'] === 'approved';
|
|
$errors = [];
|
|
$cycleContext = ['cycles' => [], 'selected' => null, 'active' => null, 'read_only' => false];
|
|
$selectedCycle = null;
|
|
$selectedCycleId = 0;
|
|
$isCycleReadOnly = false;
|
|
$cycleLabel = 'لا توجد دورة بعد';
|
|
$values = ['criteria' => []];
|
|
|
|
if ($application && $isApprovedSchool) {
|
|
$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'];
|
|
$cycleLabel = $selectedCycle ? (string) $selectedCycle['cycle_name'] : $cycleLabel;
|
|
}
|
|
|
|
$assessmentOptions = $isApprovedSchool && $selectedCycleId > 0
|
|
? school_assessment_type_options_by_cycle((int) $application['id'], $selectedCycleId, false)
|
|
: [];
|
|
$selectedAssessmentId = $requestedAssessmentId;
|
|
if ($selectedAssessmentId <= 0 && $assessmentOptions !== []) {
|
|
$keys = array_keys($assessmentOptions);
|
|
$selectedAssessmentId = (int) ($keys[0] ?? 0);
|
|
}
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $application) {
|
|
$selectedAssessmentId = filter_input(INPUT_POST, 'assessment_id', FILTER_VALIDATE_INT) ?: $selectedAssessmentId;
|
|
if (!$isApprovedSchool) {
|
|
$errors['form'] = 'لا يمكن إعداد بنود التقييم قبل اعتماد المركز.';
|
|
} elseif ($selectedCycleId <= 0) {
|
|
$errors['form'] = 'يرجى إنشاء دورة موسمية أولاً من صفحة المركز.';
|
|
} elseif ($isCycleReadOnly) {
|
|
$errors['form'] = 'هذه الدورة مؤرشفة للقراءة فقط. افتح دورة جديدة أو اختر دورة نشطة لتعديل البنود.';
|
|
} elseif ($selectedAssessmentId <= 0 || !isset($assessmentOptions[$selectedAssessmentId])) {
|
|
$errors['form'] = 'يرجى اختيار تقييم صحيح أولاً.';
|
|
} else {
|
|
[$values, $errors] = validate_assessment_criteria_input((int) $application['id'], $selectedCycleId, $selectedAssessmentId, $_POST);
|
|
if ($errors === []) {
|
|
try {
|
|
$savedRows = save_assessment_criteria_in_cycle((int) $application['id'], $selectedCycleId, $selectedAssessmentId, $values);
|
|
set_flash('success', 'تم حفظ ' . $savedRows . ' بند/بنود لهذا التقييم.');
|
|
header('Location: ' . school_page_url('assessment_criteria.php', (int) $application['id'], $selectedCycleId) . '&assessment_id=' . urlencode((string) $selectedAssessmentId));
|
|
exit;
|
|
} catch (Throwable $exception) {
|
|
$errors['form'] = 'تعذر حفظ البنود حالياً. يرجى المحاولة مرة أخرى.';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$selectedAssessment = $selectedAssessmentId > 0 ? ($assessmentOptions[$selectedAssessmentId] ?? null) : null;
|
|
$criteriaRows = $isApprovedSchool && $selectedCycleId > 0 && $selectedAssessmentId > 0
|
|
? list_assessment_criteria_by_assessment((int) $application['id'], $selectedCycleId, $selectedAssessmentId, false)
|
|
: [];
|
|
$criteriaMetrics = $isApprovedSchool && $selectedCycleId > 0 && $selectedAssessmentId > 0
|
|
? school_assessment_criteria_metrics((int) $application['id'], $selectedCycleId, $selectedAssessmentId)
|
|
: ['total' => 0, 'active' => 0, 'active_max_score' => 0.0];
|
|
|
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
$values['criteria'] = [];
|
|
foreach ($criteriaRows as $criterion) {
|
|
$values['criteria'][] = [
|
|
'id' => (int) ($criterion['id'] ?? 0),
|
|
'title' => (string) ($criterion['title'] ?? ''),
|
|
'max_score' => rtrim(rtrim(number_format((float) ($criterion['max_score'] ?? 0), 2, '.', ''), '0'), '.'),
|
|
'notes' => (string) ($criterion['notes'] ?? ''),
|
|
'is_active' => ((int) ($criterion['is_active'] ?? 0) === 1) ? '1' : '0',
|
|
];
|
|
}
|
|
if ($values['criteria'] === []) {
|
|
$values['criteria'][] = ['id' => 0, 'title' => 'الحفظ', 'max_score' => '10', 'notes' => '', 'is_active' => '1'];
|
|
$values['criteria'][] = ['id' => 0, 'title' => 'الطلاقة', 'max_score' => '10', 'notes' => '', 'is_active' => '1'];
|
|
$values['criteria'][] = ['id' => 0, 'title' => 'التجويد / النطق', 'max_score' => '10', 'notes' => '', 'is_active' => '1'];
|
|
}
|
|
}
|
|
|
|
$pageTitle = $application && $selectedAssessment
|
|
? 'بنود التقييم: ' . (string) $selectedAssessment['title'] . ' — ' . (string) $application['center_name']
|
|
: 'إعداد بنود التقييم';
|
|
$pageDescription = 'صفحة مستقلة لبناء ورقة تقييم متعددة البنود مثل الحفظ والطلاقة والنطق لكل تقييم.';
|
|
$assessmentsUrl = $application ? school_page_url('assessments.php', (int) $application['id'], $selectedCycleId) : 'assessments.php';
|
|
$scoreSheetUrl = $application ? school_page_url('assessment_score_sheet.php', (int) $application['id'], $selectedCycleId) . '&assessment_id=' . urlencode((string) $selectedAssessmentId) : 'assessment_score_sheet.php';
|
|
$scoreListUrl = $application ? school_page_url('assessment_scores.php', (int) $application['id'], $selectedCycleId) : 'assessment_scores.php';
|
|
|
|
if (!$application) {
|
|
http_response_code(404);
|
|
}
|
|
|
|
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 if ($application) { require __DIR__ . '/includes/center_sidebar.php'; } else { 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 (!$isApprovedSchool): ?>
|
|
<div class="app-card text-center py-5">
|
|
<div class="empty-title mb-2">البنود تُفتح بعد الاعتماد</div>
|
|
<p class="text-muted mb-0">اعتمد المركز أولاً حتى تتمكن من بناء أوراق تقييم تفصيلية.</p>
|
|
</div>
|
|
<?php elseif (!$selectedAssessment): ?>
|
|
<div class="app-card text-center py-5">
|
|
<div class="empty-title mb-2">اختر تقييماً أولاً</div>
|
|
<p class="text-muted mb-3">ابدأ من صفحة التقييمات ثم افتح إعداد البنود للتقييم المطلوب.</p>
|
|
<a class="btn btn-primary" href="<?= e($assessmentsUrl) ?>">الرجوع إلى التقييمات</a>
|
|
</div>
|
|
<?php else: ?>
|
|
<div class="page-banner 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"><?= e((string) $selectedAssessment['title']) ?></h1>
|
|
<p class="page-copy mb-3">أضف البنود التي تريد للمعلم أن يرصدها بشكل مستقل، مثل <strong>الحفظ</strong> و<strong>الطلاقة</strong> و<strong>النطق</strong>. سيظهر كل بند كعمود مستقل في صفحة الرصد، ويُحسب المجموع تلقائياً.</p>
|
|
<div class="hero-meta">
|
|
<span><?= e((string) ($selectedAssessment['subject_label'] !== '' ? $selectedAssessment['subject_label'] : 'بدون مادة')) ?></span>
|
|
<span><?= e($cycleLabel) ?></span>
|
|
<span>الوزن <?= e(rtrim(rtrim(number_format((float) $selectedAssessment['weight_percentage'], 2, '.', ''), '0'), '.')) ?>%</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-4">
|
|
<div class="app-card h-100">
|
|
<div class="section-title mb-3">تنقّل سريع</div>
|
|
<div class="cta-stack">
|
|
<a class="btn btn-primary" href="<?= e($scoreSheetUrl) ?>">فتح ورقة الرصد</a>
|
|
<a class="btn btn-outline-secondary" href="<?= e($assessmentsUrl) ?>">الرجوع إلى التقييمات</a>
|
|
<a class="btn btn-outline-secondary" href="<?= e($scoreListUrl) ?>">قائمة أوراق الرصد</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-4 mb-4">
|
|
<div class="col-md-4"><div class="app-card h-100"><div class="section-title mb-2">إجمالي البنود</div><div class="display-6 mb-1"><?= e((string) $criteriaMetrics['total']) ?></div><div class="section-subtle">كل البنود المحفوظة</div></div></div>
|
|
<div class="col-md-4"><div class="app-card h-100"><div class="section-title mb-2">البنود النشطة</div><div class="display-6 mb-1"><?= e((string) $criteriaMetrics['active']) ?></div><div class="section-subtle">هي التي تظهر في ورقة الرصد</div></div></div>
|
|
<div class="col-md-4"><div class="app-card h-100"><div class="section-title mb-2">المجموع النهائي</div><div class="display-6 mb-1"><?= e(rtrim(rtrim(number_format((float) $criteriaMetrics['active_max_score'], 2, '.', ''), '0'), '.')) ?></div><div class="section-subtle">يُحدَّث تلقائياً داخل التقييم</div></div></div>
|
|
</div>
|
|
|
|
<?php if (!empty($errors['form'])): ?>
|
|
<div class="alert alert-danger mb-4"><?= e($errors['form']) ?></div>
|
|
<?php endif; ?>
|
|
<?php if ($isCycleReadOnly): ?>
|
|
<div class="alert alert-warning mb-4">هذه الدورة مؤرشفة، لذلك الصفحة معروضة للقراءة فقط.</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="app-card">
|
|
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-4">
|
|
<div>
|
|
<div class="section-title mb-1">بنود التقييم</div>
|
|
<div class="section-subtle">يمكنك إضافة بنود جديدة، أو إيقاف بند قديم عن الظهور في ورقة الرصد.</div>
|
|
</div>
|
|
<?php if (!$isCycleReadOnly): ?>
|
|
<button type="button" class="btn btn-outline-secondary" id="addCriterionRow">إضافة بند</button>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<form method="post" novalidate>
|
|
<input type="hidden" name="assessment_id" value="<?= e((string) $selectedAssessmentId) ?>">
|
|
<div class="table-responsive">
|
|
<table class="table app-table align-middle" id="criteriaTable">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 32%;">اسم البند</th>
|
|
<th style="width: 16%;">الدرجة</th>
|
|
<th>ملاحظة داخلية</th>
|
|
<th style="width: 12%;">الحالة</th>
|
|
<th style="width: 12%;">إجراء</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($values['criteria'] as $index => $criterion): ?>
|
|
<?php $rowError = $errors['criteria_' . $index] ?? null; ?>
|
|
<tr data-criterion-row data-existing="<?= !empty($criterion['id']) ? '1' : '0' ?>">
|
|
<td>
|
|
<input type="hidden" name="criteria[<?= e((string) $index) ?>][id]" value="<?= e((string) ($criterion['id'] ?? 0)) ?>">
|
|
<input class="form-control<?= $rowError ? ' is-invalid' : '' ?>" type="text" name="criteria[<?= e((string) $index) ?>][title]" value="<?= e((string) ($criterion['title'] ?? '')) ?>" placeholder="مثال: الحفظ" <?= $isCycleReadOnly ? 'disabled' : '' ?>>
|
|
<?php if ($rowError): ?><div class="invalid-feedback d-block"><?= e((string) $rowError) ?></div><?php endif; ?>
|
|
</td>
|
|
<td>
|
|
<input class="form-control" type="number" step="0.01" min="0" max="1000" name="criteria[<?= e((string) $index) ?>][max_score]" value="<?= e((string) ($criterion['max_score'] ?? '')) ?>" placeholder="10" <?= $isCycleReadOnly ? 'disabled' : '' ?>>
|
|
</td>
|
|
<td>
|
|
<input class="form-control" type="text" name="criteria[<?= e((string) $index) ?>][notes]" value="<?= e((string) ($criterion['notes'] ?? '')) ?>" placeholder="اختياري" <?= $isCycleReadOnly ? 'disabled' : '' ?>>
|
|
</td>
|
|
<td>
|
|
<select class="form-select" name="criteria[<?= e((string) $index) ?>][is_active]" <?= $isCycleReadOnly ? 'disabled' : '' ?>>
|
|
<option value="1" <?= (string) ($criterion['is_active'] ?? '1') === '1' ? 'selected' : '' ?>>نشط</option>
|
|
<option value="0" <?= (string) ($criterion['is_active'] ?? '1') === '0' ? 'selected' : '' ?>>مخفي</option>
|
|
</select>
|
|
</td>
|
|
<td>
|
|
<?php if ($isCycleReadOnly): ?>
|
|
<span class="text-muted small">قراءة فقط</span>
|
|
<?php elseif (!empty($criterion['id'])): ?>
|
|
<span class="text-muted small">أوقف التفعيل لإخفائه</span>
|
|
<?php else: ?>
|
|
<button type="button" class="btn btn-sm btn-light icon-action" data-remove-row title="حذف البند" aria-label="حذف البند">
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0A.5.5 0 0 1 8.5 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z"/><path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1 0-2H5.5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1H13.5a1 1 0 0 1 1 1ZM4 4v9a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4H4Zm2.5-2a.5.5 0 0 0-.5.5V3h4v-.5a.5.5 0 0 0-.5-.5h-3Z"/></svg>
|
|
<span class="visually-hidden">حذف البند</span>
|
|
</button>
|
|
<?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<?php if (!$isCycleReadOnly): ?>
|
|
<div class="d-flex justify-content-between align-items-center pt-3 border-top mt-3 flex-wrap gap-2">
|
|
<div class="section-subtle">سيتم تحديث الدرجة النهائية للتقييم تلقائياً إلى مجموع البنود النشطة.</div>
|
|
<button class="btn btn-primary" type="submit">حفظ البنود</button>
|
|
</div>
|
|
<?php endif; ?>
|
|
</form>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<?php if ($application && !$isCycleReadOnly && $selectedAssessment): ?>
|
|
<template id="criterionRowTemplate">
|
|
<tr data-criterion-row data-existing="0">
|
|
<td>
|
|
<input type="hidden" data-row-field="id" value="0">
|
|
<input class="form-control" type="text" data-row-field="title" placeholder="مثال: الطلاقة">
|
|
</td>
|
|
<td>
|
|
<input class="form-control" type="number" step="0.01" min="0" max="1000" data-row-field="max_score" placeholder="10">
|
|
</td>
|
|
<td>
|
|
<input class="form-control" type="text" data-row-field="notes" placeholder="اختياري">
|
|
</td>
|
|
<td>
|
|
<select class="form-select" data-row-field="is_active">
|
|
<option value="1" selected>نشط</option>
|
|
<option value="0">مخفي</option>
|
|
</select>
|
|
</td>
|
|
<td>
|
|
<button type="button" class="btn btn-sm btn-light icon-action" data-remove-row title="حذف البند" aria-label="حذف البند">
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0A.5.5 0 0 1 8.5 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z"/><path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1 0-2H5.5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1H13.5a1 1 0 0 1 1 1ZM4 4v9a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4H4Zm2.5-2a.5.5 0 0 0-.5.5V3h4v-.5a.5.5 0 0 0-.5-.5h-3Z"/></svg>
|
|
<span class="visually-hidden">حذف البند</span>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const addButton = document.getElementById('addCriterionRow');
|
|
const tableBody = document.querySelector('#criteriaTable tbody');
|
|
const template = document.getElementById('criterionRowTemplate');
|
|
if (!addButton || !tableBody || !template) return;
|
|
|
|
const wireRow = (row) => {
|
|
const removeButton = row.querySelector('[data-remove-row]');
|
|
if (removeButton) {
|
|
removeButton.addEventListener('click', () => row.remove());
|
|
}
|
|
};
|
|
|
|
tableBody.querySelectorAll('[data-criterion-row]').forEach(wireRow);
|
|
|
|
const renumberRows = () => {
|
|
Array.from(tableBody.querySelectorAll('[data-criterion-row]')).forEach((row, index) => {
|
|
row.querySelectorAll('[data-row-field]').forEach((field) => {
|
|
const key = field.getAttribute('data-row-field');
|
|
field.name = `criteria[${index}][${key}]`;
|
|
});
|
|
});
|
|
};
|
|
|
|
addButton.addEventListener('click', () => {
|
|
const fragment = template.content.cloneNode(true);
|
|
const row = fragment.querySelector('[data-criterion-row]');
|
|
tableBody.appendChild(fragment);
|
|
wireRow(tableBody.lastElementChild);
|
|
renumberRows();
|
|
});
|
|
|
|
renumberRows();
|
|
});
|
|
</script>
|
|
<?php endif; ?>
|
|
<?php render_page_end();
|