Autosave: 20260417-025303
This commit is contained in:
parent
a83ac160ed
commit
5849af849c
299
assessment_criteria.php
Normal file
299
assessment_criteria.php
Normal file
@ -0,0 +1,299 @@
|
||||
<?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="row g-4 align-items-start">
|
||||
<div class="col-lg-3">
|
||||
<?php if ($application) { require __DIR__ . '/includes/center_sidebar.php'; } else { require __DIR__ . '/includes/sidebar.php'; } ?>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<?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" data-remove-row>حذف</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" data-remove-row>حذف</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();
|
||||
438
assessment_score_sheet.php
Normal file
438
assessment_score_sheet.php
Normal file
@ -0,0 +1,438 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
function 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;
|
||||
$search = clean_text($_GET['search'] ?? '', 255);
|
||||
$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 = [
|
||||
'assessment_type_id' => '',
|
||||
'teacher_id' => '',
|
||||
'assessed_on' => date('Y-m-d'),
|
||||
'assessment_max_score' => 0.0,
|
||||
'has_criteria' => false,
|
||||
'criteria' => [],
|
||||
'entries' => [],
|
||||
];
|
||||
|
||||
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)
|
||||
: [];
|
||||
$teacherOptions = $isApprovedSchool && $selectedCycleId > 0
|
||||
? school_teacher_options_by_cycle((int) $application['id'], $selectedCycleId, true)
|
||||
: [];
|
||||
$students = $isApprovedSchool && $selectedCycleId > 0
|
||||
? list_school_students_by_cycle((int) $application['id'], $selectedCycleId, $search, 0, 0, ['enrollment_status' => 'active'])
|
||||
: [];
|
||||
|
||||
$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 (!$isApprovedSchool) {
|
||||
$errors['form'] = 'لا يمكن إدخال الدرجات قبل اعتماد المركز.';
|
||||
} elseif ($selectedCycleId <= 0) {
|
||||
$errors['form'] = 'يرجى إنشاء دورة موسمية أولاً من صفحة المركز.';
|
||||
} elseif ($isCycleReadOnly) {
|
||||
$errors['form'] = 'هذه الدورة مؤرشفة للقراءة فقط. افتح دورة جديدة أو اختر دورة نشطة لإدخال درجات جديدة.';
|
||||
} else {
|
||||
[$values, $errors, $selectedAssessmentMeta] = validate_assessment_scores_batch_input((int) $application['id'], $selectedCycleId, $_POST);
|
||||
$selectedAssessmentId = (int) ($values['assessment_type_id'] ?? 0);
|
||||
if ($errors === []) {
|
||||
try {
|
||||
$savedRows = save_assessment_scores_in_cycle((int) $application['id'], $selectedCycleId, $values);
|
||||
set_flash('success', 'تم حفظ درجات ' . $savedRows . ' طالب/طالبة في هذا التقييم.');
|
||||
header('Location: ' . school_page_url('assessment_score_sheet.php', (int) $application['id'], $selectedCycleId) . '&assessment_id=' . urlencode((string) $selectedAssessmentId) . ($search !== '' ? '&search=' . urlencode($search) : ''));
|
||||
exit;
|
||||
} catch (Throwable $exception) {
|
||||
$errors['form'] = 'تعذر حفظ الدرجات حالياً. يرجى المحاولة مرة أخرى.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$selectedAssessment = $selectedAssessmentId > 0 ? ($assessmentOptions[$selectedAssessmentId] ?? null) : null;
|
||||
$criteria = $isApprovedSchool && $selectedCycleId > 0 && $selectedAssessmentId > 0
|
||||
? list_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);
|
||||
}
|
||||
}
|
||||
|
||||
$scoreMap = $isApprovedSchool && $selectedCycleId > 0 && $selectedAssessmentId > 0
|
||||
? school_assessment_score_map_by_assessment((int) $application['id'], $selectedCycleId, $selectedAssessmentId)
|
||||
: [];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' && $scoreMap !== []) {
|
||||
$firstRecord = reset($scoreMap);
|
||||
if (is_array($firstRecord)) {
|
||||
if (!empty($firstRecord['teacher_id'])) {
|
||||
$values['teacher_id'] = (string) ((int) $firstRecord['teacher_id']);
|
||||
}
|
||||
if (!empty($firstRecord['assessed_on'])) {
|
||||
$values['assessed_on'] = (string) $firstRecord['assessed_on'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$criteriaMetrics = $isApprovedSchool && $selectedCycleId > 0 && $selectedAssessmentId > 0
|
||||
? school_assessment_criteria_metrics((int) $application['id'], $selectedCycleId, $selectedAssessmentId)
|
||||
: ['total' => 0, 'active' => 0, 'active_max_score' => 0.0];
|
||||
$scoreMetrics = $isApprovedSchool && $selectedCycleId > 0 && $selectedAssessmentId > 0
|
||||
? school_assessment_score_metrics_by_cycle((int) $application['id'], $selectedCycleId, $selectedAssessmentId)
|
||||
: ['total' => 0, 'present' => 0, 'absent' => 0, 'excused' => 0, 'average_score' => 0.0, 'latest_date' => ''];
|
||||
|
||||
$pageTitle = $application && $selectedAssessment
|
||||
? 'ورقة رصد: ' . (string) $selectedAssessment['title'] . ' — ' . (string) $application['center_name']
|
||||
: 'ورقة رصد الدرجات';
|
||||
$pageDescription = $hasCriteria
|
||||
? 'صفحة مستقلة لرصد درجات الطلاب حسب البنود التفصيلية داخل تقييم واحد.'
|
||||
: 'صفحة مستقلة ومبسطة لإدخال درجات الطلاب داخل تقييم واحد فقط.';
|
||||
$scoreListUrl = $application ? school_page_url('assessment_scores.php', (int) $application['id'], $selectedCycleId) : 'assessment_scores.php';
|
||||
$assessmentsUrl = $application ? school_page_url('assessments.php', (int) $application['id'], $selectedCycleId) : 'assessments.php';
|
||||
$criteriaUrl = $application ? school_page_url('assessment_criteria.php', (int) $application['id'], $selectedCycleId) . '&assessment_id=' . urlencode((string) $selectedAssessmentId) : 'assessment_criteria.php';
|
||||
$approvedSchoolUrl = $application ? school_page_url('approved_school.php', (int) $application['id'], $selectedCycleId) : 'approved_school.php';
|
||||
$maxScoreLabel = score_display((float) ($values['assessment_max_score'] ?? 0.0));
|
||||
$averageScoreLabel = ($selectedAssessment && (int) $scoreMetrics['present'] > 0)
|
||||
? score_display((float) $scoreMetrics['average_score']) . ' / ' . ($maxScoreLabel !== '' ? $maxScoreLabel : '0')
|
||||
: 'لا يوجد';
|
||||
$latestScoreDate = $scoreMetrics['latest_date'] !== '' ? (string) $scoreMetrics['latest_date'] : 'لا يوجد';
|
||||
|
||||
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="row g-4 align-items-start">
|
||||
<div class="col-lg-3">
|
||||
<?php if ($application) { require __DIR__ . '/includes/center_sidebar.php'; } else { require __DIR__ . '/includes/sidebar.php'; } ?>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
<?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-3">اعتمد المركز أولاً حتى تتمكن من فتح ورقة الرصد.</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($scoreListUrl) ?>">الرجوع إلى قائمة التقييمات</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">
|
||||
<?= $hasCriteria
|
||||
? 'هذه الورقة مبنية على بنود تقييم متعددة، لذلك تظهر كل مهارة أو معيار كعمود مستقل ويُحسب المجموع تلقائياً.'
|
||||
: 'هذه الصفحة مخصصة لهذا التقييم فقط داخل دورة ' . '<strong>' . e($cycleLabel) . '</strong>' . ' حتى يتمكن المعلم من إدخال الدرجات بسرعة وبدون عناصر مشتتة.' ?>
|
||||
</p>
|
||||
<div class="hero-meta">
|
||||
<span><?= e((string) ($selectedAssessment['subject_label'] !== '' ? $selectedAssessment['subject_label'] : 'بدون مادة')) ?></span>
|
||||
<span>الدرجة النهائية <?= e($maxScoreLabel !== '' ? $maxScoreLabel : '0') ?></span>
|
||||
<span>الوزن <?= e(score_display((float) $selectedAssessment['weight_percentage'])) ?>%</span>
|
||||
<span><?= e((string) $criteriaMetrics['active']) ?> بنود نشطة</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-outline-secondary" href="<?= e($criteriaUrl) ?>">إعداد بنود التقييم</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($scoreListUrl) ?>">اختيار تقييم آخر</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($approvedSchoolUrl) ?>">صفحة المركز</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 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((string) $scoreMetrics['total']) ?></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($averageScoreLabel) ?></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']) ?></div><div class="section-subtle">مجموعها <?= e($maxScoreLabel !== '' ? $maxScoreLabel : '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.15rem;"><?= e($latestScoreDate) ?></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 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) ?>">
|
||||
<input type="hidden" name="assessment_id" value="<?= e((string) $selectedAssessmentId) ?>">
|
||||
<div class="col-md-8">
|
||||
<input type="text" name="search" class="form-control" placeholder="ابحث باسم الطالب أو رقمه" value="<?= e($search) ?>">
|
||||
</div>
|
||||
<div class="col-md-2 d-grid">
|
||||
<button type="submit" class="btn btn-outline-secondary">بحث</button>
|
||||
</div>
|
||||
<div class="col-md-2 d-grid">
|
||||
<a class="btn btn-light" href="<?= e(school_page_url('assessment_score_sheet.php', (int) $application['id'], $selectedCycleId) . '&assessment_id=' . urlencode((string) $selectedAssessmentId)) ?>">إلغاء</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="app-card">
|
||||
<?php if ($students === []): ?>
|
||||
<div class="empty-state text-center p-4">
|
||||
<div class="empty-title mb-2">لا يوجد طلاب مطابقون</div>
|
||||
<p class="text-muted mb-0">جرّب البحث بكلمة أخرى أو أضف طلاباً من سجل الطلاب.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<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-6 col-xl-4">
|
||||
<label class="form-label" for="teacher_id">المعلم المسؤول</label>
|
||||
<select class="form-select" name="teacher_id" id="teacher_id">
|
||||
<option value="">بدون تحديد</option>
|
||||
<?php foreach ($teacherOptions as $teacherId => $teacher): ?>
|
||||
<option value="<?= e((string) $teacherId) ?>" <?= (string) $values['teacher_id'] === (string) $teacherId ? 'selected' : '' ?>><?= e((string) $teacher['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3 col-xl-3">
|
||||
<label class="form-label" for="assessed_on">تاريخ الرصد</label>
|
||||
<input type="date" class="form-control" id="assessed_on" name="assessed_on" value="<?= e((string) $values['assessed_on']) ?>">
|
||||
</div>
|
||||
<div class="col-md-3 col-xl-3">
|
||||
<div class="school-data-item h-100">
|
||||
<strong>نمط الورقة</strong>
|
||||
<span><?= $hasCriteria ? 'متعددة البنود' : 'درجة واحدة' ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12 col-xl-2">
|
||||
<?php if (!$isCycleReadOnly): ?>
|
||||
<button class="btn btn-primary w-100" type="submit">حفظ الدرجات</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($hasCriteria): ?>
|
||||
<div class="alert alert-info d-flex justify-content-between align-items-center flex-wrap gap-2 mb-4">
|
||||
<div>يجب إدخال جميع البنود النشطة للطالب الحاضر، ثم سيُحسب <strong>المجموع</strong> تلقائياً.</div>
|
||||
<a class="btn btn-sm btn-outline-secondary" href="<?= e($criteriaUrl) ?>">تعديل البنود</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-light border mb-4">هذا التقييم ما يزال بدرجة واحدة. إذا كنت تريد بنوداً مثل الحفظ والطلاقة والتجويد، افتح <a href="<?= e($criteriaUrl) ?>">إعداد بنود التقييم</a>.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table app-table align-middle table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>الطالب</th>
|
||||
<th style="width: 150px;">الحالة</th>
|
||||
<?php if ($hasCriteria): ?>
|
||||
<?php foreach ($criteria as $criterion): ?>
|
||||
<th style="min-width: 140px;">
|
||||
<?= e((string) $criterion['title']) ?>
|
||||
<small class="d-block text-muted">من <?= e(score_display((float) $criterion['max_score'])) ?></small>
|
||||
</th>
|
||||
<?php endforeach; ?>
|
||||
<th style="width: 130px;">المجموع</th>
|
||||
<?php else: ?>
|
||||
<th style="width: 150px;">الدرجة</th>
|
||||
<?php endif; ?>
|
||||
<th>ملاحظة</th>
|
||||
<th style="width: 170px;">آخر حفظ</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($students as $student): ?>
|
||||
<?php
|
||||
$studentId = (int) ($student['id'] ?? 0);
|
||||
$existing = $scoreMap[$studentId] ?? [];
|
||||
$entryValues = $values['entries'][$studentId] ?? [];
|
||||
$statusValue = (string) ($entryValues['status'] ?? ($existing['status'] ?? 'present'));
|
||||
$notesValue = (string) ($entryValues['notes'] ?? ($existing['notes'] ?? ''));
|
||||
$rowError = $errors['entries_' . $studentId] ?? null;
|
||||
$teacherName = (string) ($existing['teacher_name'] ?? '');
|
||||
$assessedOn = (string) ($existing['assessed_on'] ?? '');
|
||||
$existingCriteriaScores = is_array($existing['criteria_scores'] ?? null) ? $existing['criteria_scores'] : [];
|
||||
$postedCriteriaScores = is_array($entryValues['criteria_scores'] ?? null) ? $entryValues['criteria_scores'] : [];
|
||||
$totalValue = $entryValues['total_score'] ?? (($existing && (string) ($existing['status'] ?? '') === 'present' && $existing['score'] !== null) ? (float) $existing['score'] : null);
|
||||
$legacyScoreValue = (string) ($entryValues['score_raw'] ?? (($existing && (string) ($existing['status'] ?? '') === 'present' && $existing['score'] !== null) ? score_display((float) $existing['score']) : ''));
|
||||
?>
|
||||
<tr data-score-row>
|
||||
<td>
|
||||
<strong><?= e((string) $student['full_name']) ?></strong>
|
||||
<small class="d-block text-muted"><?= e((string) $student['student_code']) ?><?= !empty($student['grade_level']) ? ' — ' . e((string) $student['grade_level']) : '' ?></small>
|
||||
</td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm" name="entries[<?= e((string) $studentId) ?>][status]" data-score-status>
|
||||
<?php foreach (assessment_score_status_map() as $statusKey => $statusMeta): ?>
|
||||
<option value="<?= e($statusKey) ?>" <?= $statusValue === $statusKey ? 'selected' : '' ?>><?= e($statusMeta['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
<?php if ($hasCriteria): ?>
|
||||
<?php foreach ($criteria as $criterion): ?>
|
||||
<?php
|
||||
$criterionId = (int) ($criterion['id'] ?? 0);
|
||||
$postedCriterion = $postedCriteriaScores[$criterionId] ?? [];
|
||||
$existingCriterion = $existingCriteriaScores[$criterionId] ?? [];
|
||||
$criterionScoreValue = (string) ($postedCriterion['score_raw'] ?? (($existing && (string) ($existing['status'] ?? '') === 'present' && isset($existingCriterion['score']) && $existingCriterion['score'] !== null) ? score_display((float) $existingCriterion['score']) : ''));
|
||||
?>
|
||||
<td>
|
||||
<input
|
||||
class="form-control form-control-sm<?= $rowError ? ' is-invalid' : '' ?>"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
max="<?= e(score_display((float) $criterion['max_score'])) ?>"
|
||||
name="entries[<?= e((string) $studentId) ?>][criteria][<?= e((string) $criterionId) ?>]"
|
||||
value="<?= e($criterionScoreValue) ?>"
|
||||
placeholder="<?= e(score_display((float) $criterion['max_score'])) ?>"
|
||||
data-criterion-input
|
||||
data-max-score="<?= e((string) $criterion['max_score']) ?>"
|
||||
>
|
||||
</td>
|
||||
<?php endforeach; ?>
|
||||
<td>
|
||||
<div class="fw-semibold" data-row-total><?= e($totalValue !== null ? score_display((float) $totalValue) : '—') ?></div>
|
||||
<small class="text-muted">/ <?= e($maxScoreLabel !== '' ? $maxScoreLabel : '0') ?></small>
|
||||
<?php if ($rowError): ?><div class="small text-danger mt-1"><?= e((string) $rowError) ?></div><?php endif; ?>
|
||||
</td>
|
||||
<?php else: ?>
|
||||
<td>
|
||||
<input class="form-control form-control-sm<?= $rowError ? ' is-invalid' : '' ?>" type="number" step="0.01" min="0" max="<?= e($maxScoreLabel !== '' ? $maxScoreLabel : '0') ?>" name="entries[<?= e((string) $studentId) ?>][score]" value="<?= e($legacyScoreValue) ?>" placeholder="من <?= e($maxScoreLabel !== '' ? $maxScoreLabel : '0') ?>">
|
||||
<?php if ($rowError): ?><div class="invalid-feedback"><?= e((string) $rowError) ?></div><?php endif; ?>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
<td>
|
||||
<textarea class="form-control form-control-sm" rows="2" name="entries[<?= e((string) $studentId) ?>][notes]" placeholder="اختياري"><?= e($notesValue) ?></textarea>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($existing): ?>
|
||||
<?= assessment_score_status_badge((string) ($existing['status'] ?? 'present')) ?>
|
||||
<small class="d-block text-muted"><?= e($assessedOn !== '' ? $assessedOn : '—') ?><?= $teacherName !== '' ? ' — ' . e($teacherName) : '' ?></small>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">—</span>
|
||||
<?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>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php if ($hasCriteria): ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const rows = document.querySelectorAll('[data-score-row]');
|
||||
const updateRow = (row) => {
|
||||
const status = row.querySelector('[data-score-status]');
|
||||
const totalEl = row.querySelector('[data-row-total]');
|
||||
const inputs = row.querySelectorAll('[data-criterion-input]');
|
||||
if (!status || !totalEl || !inputs.length) return;
|
||||
|
||||
if (status.value !== 'present') {
|
||||
totalEl.textContent = '—';
|
||||
inputs.forEach((input) => {
|
||||
input.value = '';
|
||||
input.setAttribute('disabled', 'disabled');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
let hasValue = false;
|
||||
inputs.forEach((input) => {
|
||||
input.removeAttribute('disabled');
|
||||
const raw = input.value.trim();
|
||||
if (raw !== '' && !Number.isNaN(Number(raw))) {
|
||||
total += Number(raw);
|
||||
hasValue = true;
|
||||
}
|
||||
});
|
||||
|
||||
totalEl.textContent = hasValue ? total.toFixed(2).replace(/\.00$/, '').replace(/(\.\d)0$/, '$1') : '—';
|
||||
};
|
||||
|
||||
rows.forEach((row) => {
|
||||
row.querySelectorAll('[data-criterion-input], [data-score-status]').forEach((field) => {
|
||||
field.addEventListener('input', () => updateRow(row));
|
||||
field.addEventListener('change', () => updateRow(row));
|
||||
});
|
||||
updateRow(row);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
<?php render_page_end();
|
||||
@ -5,112 +5,46 @@ 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;
|
||||
$search = clean_text($_GET['search'] ?? '', 255);
|
||||
$application = $applicationId > 0 ? get_application($applicationId) : null;
|
||||
$isApprovedSchool = $application && (string) $application['status'] === 'approved';
|
||||
$errors = [];
|
||||
$search = clean_text($_GET['search'] ?? '', 255);
|
||||
$cycleContext = ['cycles' => [], 'selected' => null, 'active' => null, 'read_only' => false];
|
||||
$selectedCycle = null;
|
||||
$selectedCycleId = 0;
|
||||
$isCycleReadOnly = false;
|
||||
$cycleLabel = 'لا توجد دورة بعد';
|
||||
$values = [
|
||||
'assessment_type_id' => '',
|
||||
'teacher_id' => '',
|
||||
'assessed_on' => date('Y-m-d'),
|
||||
'assessment_max_score' => 0.0,
|
||||
'entries' => [],
|
||||
];
|
||||
|
||||
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, true) : [];
|
||||
$teacherOptions = $isApprovedSchool && $selectedCycleId > 0 ? school_teacher_options_by_cycle((int) $application['id'], $selectedCycleId, true) : [];
|
||||
$studentFilters = ['enrollment_status' => 'active'];
|
||||
$students = $isApprovedSchool && $selectedCycleId > 0 ? list_school_students_by_cycle((int) $application['id'], $selectedCycleId, $search, 0, 0, $studentFilters) : [];
|
||||
|
||||
$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 (!$isApprovedSchool) {
|
||||
$errors['form'] = 'لا يمكن إدخال الدرجات قبل اعتماد المركز.';
|
||||
} elseif ($selectedCycleId <= 0) {
|
||||
$errors['form'] = 'يرجى إنشاء دورة موسمية أولاً من صفحة المركز.';
|
||||
} elseif ($isCycleReadOnly) {
|
||||
$errors['form'] = 'هذه الدورة مؤرشفة للقراءة فقط. افتح دورة جديدة أو اختر دورة نشطة لإدخال درجات جديدة.';
|
||||
} else {
|
||||
[$values, $errors, $selectedAssessmentMeta] = validate_assessment_scores_batch_input((int) $application['id'], $selectedCycleId, $_POST);
|
||||
$selectedAssessmentId = (int) ($values['assessment_type_id'] ?? 0);
|
||||
if ($errors === []) {
|
||||
try {
|
||||
$savedRows = save_assessment_scores_in_cycle((int) $application['id'], $selectedCycleId, $values);
|
||||
set_flash('success', 'تم حفظ درجات ' . $savedRows . ' طالب/طالبة في هذا التقييم.');
|
||||
header('Location: ' . school_page_url('assessment_scores.php', (int) $application['id'], $selectedCycleId) . '&assessment_id=' . urlencode((string) $selectedAssessmentId) . ($search !== '' ? '&search=' . urlencode($search) : ''));
|
||||
exit;
|
||||
} catch (Throwable $exception) {
|
||||
$errors['form'] = 'تعذر حفظ الدرجات حالياً. يرجى المحاولة مرة أخرى.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$selectedAssessment = $selectedAssessmentId > 0 ? ($assessmentOptions[$selectedAssessmentId] ?? null) : null;
|
||||
$scoreMap = $isApprovedSchool && $selectedCycleId > 0 && $selectedAssessmentId > 0
|
||||
? school_assessment_score_map_by_assessment((int) $application['id'], $selectedCycleId, $selectedAssessmentId)
|
||||
$assessmentOptions = $isApprovedSchool && $selectedCycleId > 0
|
||||
? school_assessment_type_options_by_cycle((int) $application['id'], $selectedCycleId, false)
|
||||
: [];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' && $scoreMap !== []) {
|
||||
$firstRecord = reset($scoreMap);
|
||||
if (is_array($firstRecord)) {
|
||||
if (!empty($firstRecord['teacher_id'])) {
|
||||
$values['teacher_id'] = (string) ((int) $firstRecord['teacher_id']);
|
||||
if ($search !== '') {
|
||||
$assessmentOptions = array_filter(
|
||||
$assessmentOptions,
|
||||
static function (array $assessment) use ($search): bool {
|
||||
$haystack = implode(' ', [
|
||||
(string) ($assessment['label'] ?? ''),
|
||||
(string) ($assessment['title'] ?? ''),
|
||||
(string) ($assessment['subject_label'] ?? ''),
|
||||
(string) ($assessment['category'] ?? ''),
|
||||
]);
|
||||
return stripos($haystack, $search) !== false;
|
||||
}
|
||||
if (!empty($firstRecord['assessed_on'])) {
|
||||
$values['assessed_on'] = (string) $firstRecord['assessed_on'];
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
$assessmentMetrics = $isApprovedSchool && $selectedCycleId > 0 ? school_assessment_metrics_by_cycle((int) $application['id'], $selectedCycleId) : [
|
||||
'total' => 0, 'active' => 0, 'inactive' => 0, 'total_weight' => 0.0, 'active_weight' => 0.0,
|
||||
'average_max_score' => 0.0, 'percentage' => 0, 'points' => 0, 'rubric' => 0,
|
||||
];
|
||||
$studentMetrics = $isApprovedSchool && $selectedCycleId > 0 ? school_student_metrics_by_cycle((int) $application['id'], $selectedCycleId) : [
|
||||
'total' => 0, 'boys' => 0, 'girls' => 0, 'active' => 0, 'waiting' => 0, 'withdrawn' => 0,
|
||||
];
|
||||
$teacherMetrics = $isApprovedSchool && $selectedCycleId > 0 ? school_teacher_metrics_by_cycle((int) $application['id'], $selectedCycleId) : [
|
||||
'total' => 0, 'active' => 0, 'pending' => 0, 'inactive' => 0, 'teachers' => 0, 'supervisors' => 0,
|
||||
];
|
||||
$scoreMetrics = $isApprovedSchool && $selectedCycleId > 0 ? school_assessment_score_metrics_by_cycle((int) $application['id'], $selectedCycleId, $selectedAssessmentId) : [
|
||||
'total' => 0, 'present' => 0, 'absent' => 0, 'excused' => 0, 'average_score' => 0.0, 'latest_date' => '',
|
||||
];
|
||||
|
||||
$pageTitle = $application ? 'إدخال درجات الطلاب: ' . (string) $application['center_name'] . ($selectedCycle ? ' — ' . $cycleLabel : '') : 'إدخال درجات الطلاب';
|
||||
$pageDescription = 'صفحة مستقلة تسمح للمعلم أو الإدارة الأكاديمية بإدخال درجات الطلاب لكل تقييم داخل دورة موسمية محددة.';
|
||||
$pageTitle = $application ? 'اختيار ورقة رصد الدرجات: ' . (string) $application['center_name'] . ($selectedCycle ? ' — ' . $cycleLabel : '') : 'اختيار ورقة رصد الدرجات';
|
||||
$pageDescription = 'اختر التقييم أولاً ثم افتح صفحة رصد مستقلة ونظيفة لإدخال درجات الطلاب.';
|
||||
$approvedSchoolUrl = $application ? school_page_url('approved_school.php', (int) $application['id'], $selectedCycleId) : 'approved_school.php';
|
||||
$studentsUrl = $application ? school_page_url('students.php', (int) $application['id'], $selectedCycleId) : 'students.php';
|
||||
$teachersUrl = $application ? school_page_url('teachers.php', (int) $application['id'], $selectedCycleId) : 'teachers.php';
|
||||
$assessmentsUrl = $application ? school_page_url('assessments.php', (int) $application['id'], $selectedCycleId) : 'assessments.php';
|
||||
$attendanceUrl = $application ? school_page_url('attendance.php', (int) $application['id'], $selectedCycleId) : 'attendance.php';
|
||||
$assessmentSwitchBaseUrl = $application ? school_page_url('assessment_scores.php', (int) $application['id'], $selectedCycleId) : 'assessment_scores.php';
|
||||
$latestScoreDate = $scoreMetrics['latest_date'] !== '' ? $scoreMetrics['latest_date'] : 'لا يوجد';
|
||||
$averageScoreDisplay = $selectedAssessment && $scoreMetrics['present'] > 0
|
||||
? number_format((float) $scoreMetrics['average_score'], 2, '.', '') . ' / ' . rtrim(rtrim(number_format((float) $selectedAssessment['max_score'], 2, '.', ''), '0'), '.')
|
||||
: 'لا يوجد';
|
||||
$scoreSheetBaseUrl = $application ? school_page_url('assessment_score_sheet.php', (int) $application['id'], $selectedCycleId) : 'assessment_score_sheet.php';
|
||||
$criteriaBaseUrl = $application ? school_page_url('assessment_criteria.php', (int) $application['id'], $selectedCycleId) : 'assessment_criteria.php';
|
||||
|
||||
if (!$application) {
|
||||
http_response_code(404);
|
||||
@ -126,312 +60,92 @@ render_flash($flash);
|
||||
<?php if ($application) { require __DIR__ . '/includes/center_sidebar.php'; } else { require __DIR__ . '/includes/sidebar.php'; } ?>
|
||||
</div>
|
||||
<div class="col-lg-9">
|
||||
|
||||
<?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="page-banner mb-4 mb-lg-5">
|
||||
<div class="row g-4 align-items-center">
|
||||
<div class="col-lg-8">
|
||||
<span class="eyebrow mb-3">الدرجات تُفتح بعد الاعتماد</span>
|
||||
<h1 class="page-title mb-3"><?= e((string) $application['center_name']) ?></h1>
|
||||
<p class="page-copy mb-3">تم تجهيز صفحة إدخال الدرجات، لكنها لا تعمل إلا بعد اعتماد المركز وفتح الدورة الأكاديمية الخاصة به.</p>
|
||||
<div class="cta-stack mt-4">
|
||||
<a class="btn btn-primary" href="<?= e($approvedSchoolUrl) ?>">صفحة المركز</a>
|
||||
<a class="btn btn-outline-secondary" href="application_detail.php?id=<?= e((string) $application['id']) ?>">ملف الاعتماد</a>
|
||||
</div>
|
||||
<?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>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="page-banner approved-hero mb-4 mb-lg-5">
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-8">
|
||||
<span class="approved-kicker mb-3">خطوة التنفيذ بعد تصميم ورقة التقييم</span>
|
||||
<h1 class="page-title mb-3">إدخال درجات الطلاب — <?= e((string) $application['center_name']) ?></h1>
|
||||
<p class="page-copy mb-3">اختر التقييم، حدّد المعلّم إن رغبت، ثم أدخل درجات الطلاب مباشرة داخل نفس الدورة <strong><?= e($cycleLabel) ?></strong>. كل درجة تُحفَظ على مستوى الطالب والتقييم والموسم الحالي.</p>
|
||||
<div class="hero-meta">
|
||||
<span><?= e((string) $studentMetrics['active']) ?> طلاب نشطون</span>
|
||||
<span><?= e((string) $assessmentMetrics['active']) ?> تقييمات مفعلة</span>
|
||||
<span><?= e((string) $teacherMetrics['active']) ?> معلمين نشطين</span>
|
||||
</div>
|
||||
<div class="cta-stack mt-4">
|
||||
<a class="btn btn-outline-secondary" href="<?= e($assessmentsUrl) ?>">إدارة التقييمات</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($attendanceUrl) ?>">سجلات الغياب</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($approvedSchoolUrl) ?>">صفحة المركز</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-3">اعتمد المركز أولاً حتى تظهر أوراق الرصد الخاصة به.</p>
|
||||
<a class="btn btn-outline-secondary" href="application_detail.php?id=<?= e((string) $application['id']) ?>">ملف الاعتماد</a>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="app-card approved-note h-100">
|
||||
<div class="section-title mb-3">ملخص الدرجة الحالية</div>
|
||||
<div class="summary-stack mb-3">
|
||||
<div class="summary-row"><span>السجلات المحفوظة</span><strong><?= e((string) $scoreMetrics['total']) ?> سجل</strong></div>
|
||||
<div class="summary-row"><span>درجات فعلية</span><strong><?= e((string) $scoreMetrics['present']) ?> طالب</strong></div>
|
||||
<div class="summary-row"><span>آخر تحديث</span><strong><?= e($latestScoreDate) ?></strong></div>
|
||||
</div>
|
||||
<p class="section-subtle mb-0">المتوسط الحالي: <strong><?= e($averageScoreDisplay) ?></strong><?php if ($selectedAssessment): ?> ضمن التقييم <strong><?= e($selectedAssessment['label']) ?></strong><?php endif; ?>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($selectedCycle): ?>
|
||||
<?php $cycleStatusMap = school_cycle_status_map(); ?>
|
||||
<div class="row g-4 mb-4 align-items-start">
|
||||
<div class="col-lg-<?= is_super_admin() ? '7' : '12' ?>">
|
||||
<div class="app-card h-100">
|
||||
<div class="section-head mb-3">
|
||||
<div>
|
||||
<div class="section-title">الدورة الموسمية الحالية</div>
|
||||
<div class="section-copy">كل الدرجات في هذه الصفحة مرتبطة بالدورة <strong><?= e($cycleLabel) ?></strong>. أرشفة الدورة تجعل الصفحة للقراءة فقط بدون خلط النتائج مع الموسم التالي.</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">الخطوة 1</span>
|
||||
<h1 class="page-title mb-3">اختر التقييم ثم افتح صفحة الرصد</h1>
|
||||
<p class="page-copy mb-3">بدلاً من شاشة مزدحمة، أصبحت عملية إدخال الدرجات على خطوتين: <strong>اختيار التقييم</strong> ثم فتح <strong>صفحة مستقلة</strong> لكل ورقة رصد داخل دورة <strong><?= e($cycleLabel) ?></strong>.</p>
|
||||
<div class="hero-meta">
|
||||
<span><?= e((string) count($assessmentOptions)) ?> تقييمات متاحة</span>
|
||||
<span><?= e($cycleLabel) ?></span>
|
||||
</div>
|
||||
<?= school_cycle_status_badge((string) $selectedCycle['status']) ?>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4"><div class="school-data-item"><strong>اسم الدورة</strong><span><?= e($cycleLabel) ?></span></div></div>
|
||||
<div class="col-md-4"><div class="school-data-item"><strong>الفترة</strong><span><?= e((string) $selectedCycle['start_date']) ?> → <?= e((string) $selectedCycle['end_date']) ?></span></div></div>
|
||||
<div class="col-md-4"><div class="school-data-item"><strong>عدد الدورات</strong><span><?= e((string) count($cycleContext['cycles'])) ?> دورة للمركز</span></div></div>
|
||||
</div>
|
||||
<?php if ($isCycleReadOnly): ?>
|
||||
<div class="alert alert-warning mt-3 mb-0">هذه الدورة مؤرشفة، لذلك تبقى صفحة إدخال الدرجات للقراءة فقط حالياً.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (is_super_admin()): ?>
|
||||
<div class="col-lg-5">
|
||||
<div class="app-card sidebar-card h-100">
|
||||
<div class="section-title mb-3">التبديل بين الدورات</div>
|
||||
<p class="section-subtle mb-3">يمكنك فتح دفتر الدرجات لنفس المركز في أي موسم سابق أو حالي من هنا مباشرة.</p>
|
||||
<div class="quick-link-stack">
|
||||
<?php foreach ($cycleContext['cycles'] as $cycle): ?>
|
||||
<?php
|
||||
$isCurrentCycleLink = (int) $cycle['id'] === $selectedCycleId;
|
||||
$isActiveCycleLink = (int) $cycle['id'] === (int) (($cycleContext['active']['id'] ?? 0));
|
||||
$cycleStatusLabel = (string) ($cycleStatusMap[$cycle['status']]['label'] ?? 'غير معروف');
|
||||
$cycleMetaLine = (string) $cycle['start_date'] . ' → ' . (string) $cycle['end_date'] . ' — ' . $cycleStatusLabel . ($isActiveCycleLink ? ' — النشطة حالياً' : '');
|
||||
?>
|
||||
<a class="quick-link-item <?= $isCurrentCycleLink ? 'is-current' : '' ?>" href="<?= e(school_page_url('assessment_scores.php', (int) $application['id'], (int) $cycle['id'])) ?><?= $selectedAssessmentId > 0 ? '&assessment_id=' . e((string) $selectedAssessmentId) : '' ?>">
|
||||
<div>
|
||||
<strong><?= e((string) $cycle['cycle_name']) ?><?= $isCurrentCycleLink ? ' — المعروضة الآن' : '' ?></strong>
|
||||
<span><?= e($cycleMetaLine) ?></span>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-4">
|
||||
<div class="app-card sidebar-card mb-4">
|
||||
<div class="section-title mb-3">اختيار التقييم والمعلّم</div>
|
||||
<p class="section-subtle mb-3">اختر نوع التقييم أولاً، ثم أدخل الدرجات دفعة واحدة لكل الطلاب الظاهرين في القائمة.</p>
|
||||
|
||||
<?php if (isset($errors['form'])): ?>
|
||||
<div class="alert alert-danger"><?= e($errors['form']) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($assessmentOptions === []): ?>
|
||||
<div class="alert alert-warning mb-0">ابدأ أولاً من صفحة التقييمات لإضافة تقييم واحد على الأقل قبل إدخال درجات الطلاب.</div>
|
||||
<?php elseif ($students === []): ?>
|
||||
<div class="alert alert-warning mb-0">لا يوجد طلاب نشطون لعرضهم حالياً. أضف الطلاب أولاً أو غيّر البحث الحالي.</div>
|
||||
<?php else: ?>
|
||||
<?php if ($isCycleReadOnly): ?>
|
||||
<div class="alert alert-warning mb-0">هذه الدورة مؤرشفة. يمكنك مراجعة الدرجات فقط أو فتح دورة جديدة من صفحة المركز.</div>
|
||||
<?php else: ?>
|
||||
<form method="get" class="vstack gap-3 mb-4" novalidate>
|
||||
<input type="hidden" name="id" value="<?= e((string) $application['id']) ?>">
|
||||
<input type="hidden" name="cycle" value="<?= e((string) $selectedCycleId) ?>">
|
||||
<div>
|
||||
<label class="form-label" for="assessment_id_switch">التقييم النشط</label>
|
||||
<select class="form-select" id="assessment_id_switch" name="assessment_id" onchange="this.form.submit()">
|
||||
<?php foreach ($assessmentOptions as $assessmentId => $assessmentMeta): ?>
|
||||
<option value="<?= e((string) $assessmentId) ?>" <?= $selectedAssessmentId === $assessmentId ? 'selected' : '' ?>><?= e($assessmentMeta['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" for="search">بحث داخل كشف الطلاب</label>
|
||||
<input class="form-control" id="search" name="search" value="<?= e($search) ?>" placeholder="ابحث باسم الطالب أو الكود المرجعي">
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button class="btn btn-outline-secondary" type="submit">تحديث الكشف</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="vstack gap-3">
|
||||
<input type="hidden" name="assessment_type_id" value="<?= e((string) $selectedAssessmentId) ?>">
|
||||
<div>
|
||||
<label class="form-label" for="teacher_id">المعلّم / المقيم (اختياري)</label>
|
||||
<select class="form-select <?= isset($errors['teacher_id']) ? 'is-invalid' : '' ?>" id="teacher_id" name="teacher_id" form="scoreBatchForm">
|
||||
<option value="0">بدون تحديد معلم</option>
|
||||
<?php foreach ($teacherOptions as $teacherId => $teacherMeta): ?>
|
||||
<option value="<?= e((string) $teacherId) ?>" <?= $values['teacher_id'] === (string) $teacherId ? 'selected' : '' ?>><?= e($teacherMeta['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php if (isset($errors['teacher_id'])): ?><div class="invalid-feedback"><?= e($errors['teacher_id']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="form-label" for="assessed_on">تاريخ الرصد</label>
|
||||
<input class="form-control <?= isset($errors['assessed_on']) ? 'is-invalid' : '' ?>" type="date" id="assessed_on" name="assessed_on" value="<?= e($values['assessed_on']) ?>" form="scoreBatchForm">
|
||||
<?php if (isset($errors['assessed_on'])): ?><div class="invalid-feedback"><?= e($errors['assessed_on']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="school-data-item">
|
||||
<strong>التقييم المختار</strong>
|
||||
<span><?= e($selectedAssessment['label'] ?? '—') ?></span>
|
||||
</div>
|
||||
<div class="school-data-item">
|
||||
<strong>الدرجة النهائية</strong>
|
||||
<span><?= e($selectedAssessment ? rtrim(rtrim(number_format((float) $selectedAssessment['max_score'], 2, '.', ''), '0'), '.') : '—') ?></span>
|
||||
</div>
|
||||
<div class="school-data-item">
|
||||
<strong>الوزن</strong>
|
||||
<span><?= e($selectedAssessment ? rtrim(rtrim(number_format((float) $selectedAssessment['weight_percentage'], 2, '.', ''), '0'), '.') . '%' : '—') ?></span>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button class="btn btn-primary" type="submit" form="scoreBatchForm">حفظ درجات الطلاب الظاهرين</button>
|
||||
<div class="col-lg-4">
|
||||
<div class="app-card h-100">
|
||||
<div class="section-title mb-2">اختصار العمل</div>
|
||||
<div class="section-copy mb-3">1) افتح التقييم 2) أدخل الدرجات 3) احفظ</div>
|
||||
<div class="cta-stack">
|
||||
<a class="btn btn-outline-secondary" href="<?= e($assessmentsUrl) ?>">إدارة التقييمات</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($approvedSchoolUrl) ?>">صفحة المركز</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-card sidebar-card">
|
||||
<div class="section-title mb-3">قبل البدء</div>
|
||||
<ul class="module-roadmap-list mb-0">
|
||||
<li><strong>1. تعريف التقييم</strong><span class="section-subtle">أنشئ التقييم من صفحة التقييمات وحدد الدرجة النهائية والوزن.</span></li>
|
||||
<li><strong>2. تجهيز الطلاب</strong><span class="section-subtle">هذه الصفحة تعرض الطلاب النشطين فقط لتقليل الأخطاء أثناء الرصد.</span></li>
|
||||
<li><strong>3. حفظ متكرر</strong><span class="section-subtle">يمكنك العودة لاحقاً لنفس التقييم وتعديل درجات أي طالب، وسيتم تحديث سجله بدل إنشاء نسخة جديدة.</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-8">
|
||||
<div class="app-card mb-4">
|
||||
<div class="section-head mb-3">
|
||||
<div>
|
||||
<div class="section-title">كشف إدخال الدرجات</div>
|
||||
<div class="section-copy">كل صف يمثل طالباً واحداً ضمن التقييم المختار. عند حفظ النموذج يتم تحديث الصفوف التي تحتوي على درجة أو حالة خاصة أو ملاحظة.</div>
|
||||
<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-9">
|
||||
<input type="text" name="search" class="form-control" placeholder="ابحث باسم التقييم أو المادة" value="<?= e($search) ?>">
|
||||
</div>
|
||||
<span class="header-chip"><?= e((string) count($students)) ?> طلاب ظاهرون</span>
|
||||
</div>
|
||||
<div class="col-md-3 d-grid">
|
||||
<button type="submit" class="btn btn-primary">بحث</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<?php if ($assessmentOptions === []): ?>
|
||||
<div class="empty-state text-center p-4">
|
||||
<div class="empty-title mb-2">لا توجد تقييمات مفعلة بعد</div>
|
||||
<p class="text-muted mb-0">أنشئ أول تقييم من صفحة التقييمات، ثم عد هنا لبدء رصد النتائج.</p>
|
||||
</div>
|
||||
<?php elseif ($students === []): ?>
|
||||
<div class="empty-state text-center p-4">
|
||||
<div class="empty-title mb-2">لا يوجد طلاب مطابقون لهذا العرض</div>
|
||||
<p class="text-muted mb-0">جرّب إزالة البحث الحالي أو أضف طلاباً نشطين من صفحة الطلاب.</p>
|
||||
<div class="col-12">
|
||||
<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>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<form method="post" id="scoreBatchForm" novalidate>
|
||||
<input type="hidden" name="assessment_type_id" value="<?= e((string) $selectedAssessmentId) ?>">
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-3"><div class="school-data-item"><strong>المحفوظ حالياً</strong><span><?= e((string) $scoreMetrics['total']) ?> سجل</span></div></div>
|
||||
<div class="col-md-3"><div class="school-data-item"><strong>درجات مرصودة</strong><span><?= e((string) $scoreMetrics['present']) ?> طالب</span></div></div>
|
||||
<div class="col-md-3"><div class="school-data-item"><strong>غياب / عذر</strong><span><?= e((string) ($scoreMetrics['absent'] + $scoreMetrics['excused'])) ?> حالة</span></div></div>
|
||||
<div class="col-md-3"><div class="school-data-item"><strong>المتوسط</strong><span><?= e($averageScoreDisplay) ?></span></div></div>
|
||||
<?php foreach ($assessmentOptions as $assessmentId => $assessment): ?>
|
||||
<div class="col-md-6 col-xl-4">
|
||||
<article class="app-card h-100 d-flex flex-column">
|
||||
<div class="d-flex justify-content-between gap-3 align-items-start mb-3">
|
||||
<div>
|
||||
<div class="section-title mb-1"><?= e((string) ($assessment['title'] ?: 'تقييم غير مسمى')) ?></div>
|
||||
<div class="section-subtle"><?= e((string) ($assessment['subject_label'] !== '' ? $assessment['subject_label'] : 'بدون مادة محددة')) ?></div>
|
||||
</div>
|
||||
<?= assessment_active_badge((int) ($assessment['is_active'] ? 1 : 0)) ?>
|
||||
</div>
|
||||
<div class="row g-2 mb-4">
|
||||
<div class="col-6"><div class="school-data-item"><strong>الفئة</strong><span><?= e((string) $assessment['category']) ?></span></div></div>
|
||||
<div class="col-6"><div class="school-data-item"><strong>الدرجة</strong><span><?= e(rtrim(rtrim(number_format((float) $assessment['max_score'], 2, '.', ''), '0'), '.')) ?></span></div></div>
|
||||
<div class="col-6"><div class="school-data-item"><strong>الوزن</strong><span><?= e(rtrim(rtrim(number_format((float) $assessment['weight_percentage'], 2, '.', ''), '0'), '.')) ?>%</span></div></div>
|
||||
<div class="col-6"><div class="school-data-item"><strong>البنود</strong><span><?= e((string) ((int) ($assessment['criteria_count'] ?? 0))) ?></span></div></div>
|
||||
</div>
|
||||
<div class="mt-auto d-grid gap-2">
|
||||
<a class="btn btn-primary" href="<?= e($scoreSheetBaseUrl . '&assessment_id=' . urlencode((string) $assessmentId)) ?>">فتح صفحة الرصد</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($criteriaBaseUrl . '&assessment_id=' . urlencode((string) $assessmentId)) ?>">إعداد البنود</a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table app-table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>الطالب</th>
|
||||
<th style="width: 140px;">الحالة</th>
|
||||
<th style="width: 140px;">الدرجة</th>
|
||||
<th>ملاحظات</th>
|
||||
<th>آخر رصد</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($students as $student): ?>
|
||||
<?php
|
||||
$studentId = (int) ($student['id'] ?? 0);
|
||||
$existing = $scoreMap[$studentId] ?? null;
|
||||
$entryValues = $values['entries'][$studentId] ?? [];
|
||||
$statusValue = (string) ($entryValues['status'] ?? ($existing['status'] ?? 'present'));
|
||||
$scoreValue = (string) ($entryValues['score_raw'] ?? (($existing && (string) ($existing['status'] ?? '') === 'present' && $existing['score'] !== null) ? rtrim(rtrim(number_format((float) $existing['score'], 2, '.', ''), '0'), '.') : ''));
|
||||
$notesValue = (string) ($entryValues['notes'] ?? ($existing['notes'] ?? ''));
|
||||
$rowError = $errors['entries_' . $studentId] ?? null;
|
||||
$teacherName = (string) ($existing['teacher_name'] ?? '');
|
||||
$assessedOn = (string) ($existing['assessed_on'] ?? '');
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<strong><?= e((string) $student['full_name']) ?></strong>
|
||||
<small><?= e((string) $student['student_code']) ?> — <?= e((string) $student['grade_level']) ?></small>
|
||||
</td>
|
||||
<td>
|
||||
<select class="form-select form-select-sm" name="entries[<?= e((string) $studentId) ?>][status]">
|
||||
<?php foreach (assessment_score_status_map() as $statusKey => $statusMeta): ?>
|
||||
<option value="<?= e($statusKey) ?>" <?= $statusValue === $statusKey ? 'selected' : '' ?>><?= e($statusMeta['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input class="form-control form-control-sm <?= $rowError ? 'is-invalid' : '' ?>" type="number" step="0.01" min="0" max="<?= e($selectedAssessment ? (string) $selectedAssessment['max_score'] : '100') ?>" name="entries[<?= e((string) $studentId) ?>][score]" value="<?= e($scoreValue) ?>" placeholder="<?= e($selectedAssessment ? 'من ' . rtrim(rtrim(number_format((float) $selectedAssessment['max_score'], 2, '.', ''), '0'), '.') : 'درجة') ?>">
|
||||
<?php if ($rowError): ?><div class="invalid-feedback"><?= e((string) $rowError) ?></div><?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<textarea class="form-control form-control-sm" rows="2" name="entries[<?= e((string) $studentId) ?>][notes]" placeholder="ملاحظة سريعة عند الحاجة"><?= e($notesValue) ?></textarea>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($existing): ?>
|
||||
<?= assessment_score_status_badge((string) ($existing['status'] ?? 'present')) ?>
|
||||
<small><?= e($assessedOn !== '' ? $assessedOn : '—') ?><?= $teacherName !== '' ? ' — ' . e($teacherName) : '' ?></small>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">—</span>
|
||||
<?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>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="app-card">
|
||||
<div class="section-title mb-3">سياق المدرسة</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6"><div class="school-data-item"><strong>الطلاب النشطون</strong><span><?= e((string) $studentMetrics['active']) ?> طالب/طالبة</span></div></div>
|
||||
<div class="col-md-6"><div class="school-data-item"><strong>المعلمون النشطون</strong><span><?= e((string) $teacherMetrics['active']) ?> عضو</span></div></div>
|
||||
<div class="col-md-6"><div class="school-data-item"><strong>التقييمات المفعلة</strong><span><?= e((string) $assessmentMetrics['active']) ?> نوع</span></div></div>
|
||||
<div class="col-md-6"><div class="school-data-item"><strong>آخر رصد محفوظ</strong><span><?= e($latestScoreDate) ?></span></div></div>
|
||||
</div>
|
||||
<div class="cta-stack mt-4">
|
||||
<a class="btn btn-outline-secondary" href="<?= e($assessmentsUrl) ?>">العودة إلى التقييمات</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($teachersUrl) ?>">العودة إلى المعلمين</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($studentsUrl) ?>">العودة إلى الطلاب</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($approvedSchoolUrl) ?>">صفحة المركز</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -87,6 +87,8 @@ $pageTitle = $application ? 'التقييمات والأوزان: ' . (string) $
|
||||
$pageDescription = 'صفحة مستقلة لتعريف أنواع التقييم، المقاييس، والأوزان لكل مدرسة معتمدة داخل دورة موسمية محددة.';
|
||||
$approvedSchoolUrl = $application ? school_page_url('approved_school.php', (int) $application['id'], $selectedCycleId) : 'approved_school.php';
|
||||
$assessmentScoresUrl = $application ? school_page_url('assessment_scores.php', (int) $application['id'], $selectedCycleId) : 'assessment_scores.php';
|
||||
$assessmentScoreSheetBaseUrl = $application ? school_page_url('assessment_score_sheet.php', (int) $application['id'], $selectedCycleId) : 'assessment_score_sheet.php';
|
||||
$criteriaBaseUrl = $application ? school_page_url('assessment_criteria.php', (int) $application['id'], $selectedCycleId) : 'assessment_criteria.php';
|
||||
|
||||
if (!$application) {
|
||||
http_response_code(404);
|
||||
@ -200,6 +202,7 @@ render_flash($flash);
|
||||
<th>المقياس</th>
|
||||
<th>الدرجة</th>
|
||||
<th>الوزن</th>
|
||||
<th>البنود</th>
|
||||
<th>الحالة</th>
|
||||
<?php if (!$isCycleReadOnly): ?><th>الإجراء</th><?php endif; ?>
|
||||
</tr>
|
||||
@ -223,12 +226,25 @@ render_flash($flash);
|
||||
<td><?= assessment_scale_type_badge((string) $assessment['scale_type']) ?></td>
|
||||
<td><?= e(number_format((float) $assessment['max_score'], 2, '.', '')) ?></td>
|
||||
<td><strong><?= e(number_format((float) $assessment['weight_percentage'], 2, '.', '')) ?>%</strong></td>
|
||||
<td>
|
||||
<?php $criteriaCount = (int) ($assessment['criteria_count'] ?? 0); ?>
|
||||
<?php if ($criteriaCount > 0): ?>
|
||||
<div class="fw-semibold"><?= e((string) $criteriaCount) ?> بنود</div>
|
||||
<small class="text-muted">المجموع <?= e(rtrim(rtrim(number_format((float) ($assessment['criteria_total_max_score'] ?? $assessment['max_score']), 2, '.', ''), '0'), '.')) ?></small>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">بدون بنود</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= assessment_active_badge((int) $assessment['is_active']) ?></td>
|
||||
<?php if (!$isCycleReadOnly):
|
||||
?><td>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="editAssessment(<?= htmlspecialchars(json_encode($assessment), ENT_QUOTES, 'UTF-8') ?>)">
|
||||
تعديل
|
||||
</button>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a class="btn btn-sm btn-primary" href="<?= e($assessmentScoreSheetBaseUrl . '&assessment_id=' . urlencode((string) $assessment['id'])) ?>">رصد الدرجات</a>
|
||||
<a class="btn btn-sm btn-outline-secondary" href="<?= e($criteriaBaseUrl . '&assessment_id=' . urlencode((string) $assessment['id'])) ?>">إعداد البنود</a>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="editAssessment(<?= htmlspecialchars(json_encode($assessment), ENT_QUOTES, 'UTF-8') ?>)">
|
||||
تعديل
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
</tr>
|
||||
@ -287,6 +303,7 @@ render_flash($flash);
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="max_score">الدرجة النهائية</label>
|
||||
<input type="number" step="0.01" min="0" max="1000" class="form-control" id="max_score" name="max_score" value="100">
|
||||
<div class="form-text">إذا أضفت بنوداً تفصيلية لاحقاً فسيتم تحديث هذا الرقم تلقائياً من مجموع البنود النشطة.</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="weight_percentage">الوزن (%)</label>
|
||||
|
||||
46
db/migrations/20260417_school_assessment_criteria.sql
Normal file
46
db/migrations/20260417_school_assessment_criteria.sql
Normal file
@ -0,0 +1,46 @@
|
||||
CREATE TABLE IF NOT EXISTS school_assessment_criteria (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
center_application_id INT UNSIGNED NOT NULL,
|
||||
cycle_id INT UNSIGNED NOT NULL,
|
||||
assessment_type_id INT UNSIGNED NOT NULL,
|
||||
title VARCHAR(190) NOT NULL,
|
||||
max_score DECIMAL(8,2) NOT NULL DEFAULT 0.00,
|
||||
sort_order INT UNSIGNED NOT NULL DEFAULT 1,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
notes TEXT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_school_assessment_criteria_center (center_application_id),
|
||||
INDEX idx_school_assessment_criteria_cycle (cycle_id),
|
||||
INDEX idx_school_assessment_criteria_assessment (assessment_type_id),
|
||||
INDEX idx_school_assessment_criteria_active (assessment_type_id, is_active),
|
||||
CONSTRAINT fk_school_assessment_criteria_center_application FOREIGN KEY (center_application_id) REFERENCES center_applications(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_school_assessment_criteria_cycle FOREIGN KEY (cycle_id) REFERENCES school_cycles(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_school_assessment_criteria_assessment FOREIGN KEY (assessment_type_id) REFERENCES school_assessment_types(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS school_assessment_score_items (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
center_application_id INT UNSIGNED NOT NULL,
|
||||
cycle_id INT UNSIGNED NOT NULL,
|
||||
assessment_score_id INT UNSIGNED NOT NULL,
|
||||
assessment_type_id INT UNSIGNED NOT NULL,
|
||||
criterion_id INT UNSIGNED NOT NULL,
|
||||
student_id INT UNSIGNED NOT NULL,
|
||||
score DECIMAL(8,2) NULL,
|
||||
max_score DECIMAL(8,2) NOT NULL DEFAULT 0.00,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uniq_school_assessment_score_item (assessment_score_id, criterion_id),
|
||||
INDEX idx_school_assessment_score_items_center (center_application_id),
|
||||
INDEX idx_school_assessment_score_items_cycle (cycle_id),
|
||||
INDEX idx_school_assessment_score_items_assessment (assessment_type_id),
|
||||
INDEX idx_school_assessment_score_items_criterion (criterion_id),
|
||||
INDEX idx_school_assessment_score_items_student (student_id),
|
||||
CONSTRAINT fk_school_assessment_score_items_center_application FOREIGN KEY (center_application_id) REFERENCES center_applications(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_school_assessment_score_items_cycle FOREIGN KEY (cycle_id) REFERENCES school_cycles(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_school_assessment_score_items_score FOREIGN KEY (assessment_score_id) REFERENCES school_assessment_scores(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_school_assessment_score_items_assessment FOREIGN KEY (assessment_type_id) REFERENCES school_assessment_types(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_school_assessment_score_items_criterion FOREIGN KEY (criterion_id) REFERENCES school_assessment_criteria(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_school_assessment_score_items_student FOREIGN KEY (student_id) REFERENCES school_students(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@ -110,6 +110,7 @@ function db_connection(): PDO
|
||||
ensure_school_module_schema($pdo);
|
||||
ensure_school_cycle_schema($pdo);
|
||||
ensure_school_assessment_score_schema($pdo);
|
||||
ensure_school_assessment_criteria_schema($pdo);
|
||||
seed_school_module_demo_data($pdo);
|
||||
$bootstrapped = true;
|
||||
}
|
||||
@ -686,7 +687,7 @@ function student_enrollment_status_badge(string $status): string
|
||||
return '<span class="status-badge ' . e($meta['class']) . '">' . e($meta['label']) . '</span>';
|
||||
}
|
||||
|
||||
function validate_student_input(array $input): array
|
||||
function validate_student_input(array $input, bool $requireStudentCode = true): array
|
||||
{
|
||||
$data = student_defaults();
|
||||
foreach ($data as $key => $_value) {
|
||||
@ -696,7 +697,7 @@ function validate_student_input(array $input): array
|
||||
|
||||
$errors = [];
|
||||
|
||||
if ($data['student_code'] === '') {
|
||||
if ($requireStudentCode && $data['student_code'] === '') {
|
||||
$errors['student_code'] = 'يرجى إدخال الرقم أو الكود المرجعي للطالب.';
|
||||
}
|
||||
if ($data['full_name'] === '') {
|
||||
|
||||
@ -5,9 +5,9 @@ $activePage = 'dashboard';
|
||||
if ($script === 'center_profile.php') $activePage = 'profile';
|
||||
if ($script === 'students.php') $activePage = 'students';
|
||||
if ($script === 'teachers.php') $activePage = 'teachers';
|
||||
if ($script === 'assessments.php') $activePage = 'assessments';
|
||||
if ($script === 'assessments.php' || $script === 'assessment_criteria.php') $activePage = 'assessments';
|
||||
if ($script === 'attendance.php') $activePage = 'attendance';
|
||||
if ($script === 'assessment_scores.php') $activePage = 'scores';
|
||||
if ($script === 'assessment_scores.php' || $script === 'assessment_score_sheet.php') $activePage = 'scores';
|
||||
if ($script === 'center_subjects.php') $activePage = 'subjects';
|
||||
|
||||
// We assume $application is available in scope.
|
||||
|
||||
@ -122,6 +122,24 @@ function ensure_school_assessment_score_schema(PDO $pdo): void
|
||||
$done = true;
|
||||
}
|
||||
|
||||
function ensure_school_assessment_criteria_schema(PDO $pdo): void
|
||||
{
|
||||
static $done = false;
|
||||
if ($done) {
|
||||
return;
|
||||
}
|
||||
|
||||
$migrationPath = __DIR__ . '/../db/migrations/20260417_school_assessment_criteria.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(
|
||||
@ -422,25 +440,68 @@ function copy_school_cycle_rollover(PDO $pdo, int $centerApplicationId, int $sou
|
||||
}
|
||||
|
||||
if (!empty($rollover['copy_assessments'])) {
|
||||
$stmt = $pdo->prepare(
|
||||
$sourceStmt = $pdo->prepare(
|
||||
'SELECT * FROM school_assessment_types
|
||||
WHERE center_application_id = :center_application_id AND cycle_id = :source_cycle_id
|
||||
ORDER BY id ASC'
|
||||
);
|
||||
$sourceStmt->execute([
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':source_cycle_id' => $sourceCycleId,
|
||||
]);
|
||||
$sourceAssessments = $sourceStmt->fetchAll();
|
||||
|
||||
$insertAssessmentStmt = $pdo->prepare(
|
||||
'INSERT INTO school_assessment_types (
|
||||
center_application_id, cycle_id, subject_id, title, category, scale_type,
|
||||
max_score, weight_percentage, is_active, notes,
|
||||
created_at, updated_at
|
||||
) VALUES (
|
||||
:center_application_id, :cycle_id, :subject_id, :title, :category, :scale_type,
|
||||
:max_score, :weight_percentage, :is_active, :notes,
|
||||
NOW(), NOW()
|
||||
)'
|
||||
);
|
||||
$copyCriteriaStmt = $pdo->prepare(
|
||||
'INSERT INTO school_assessment_criteria (
|
||||
center_application_id, cycle_id, assessment_type_id, title, max_score,
|
||||
sort_order, is_active, notes, created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
center_application_id, :target_cycle_id, subject_id, title, category, scale_type,
|
||||
max_score, weight_percentage, is_active, notes,
|
||||
NOW(), NOW()
|
||||
FROM school_assessment_types
|
||||
WHERE center_application_id = :center_application_id AND cycle_id = :source_cycle_id'
|
||||
center_application_id, :target_cycle_id, :target_assessment_id, title, max_score,
|
||||
sort_order, is_active, notes, NOW(), NOW()
|
||||
FROM school_assessment_criteria
|
||||
WHERE center_application_id = :center_application_id
|
||||
AND cycle_id = :source_cycle_id
|
||||
AND assessment_type_id = :source_assessment_id'
|
||||
);
|
||||
$stmt->execute([
|
||||
':target_cycle_id' => $targetCycleId,
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':source_cycle_id' => $sourceCycleId,
|
||||
]);
|
||||
$summary['assessments'] = $stmt->rowCount();
|
||||
|
||||
foreach ($sourceAssessments as $sourceAssessment) {
|
||||
$insertAssessmentStmt->execute([
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':cycle_id' => $targetCycleId,
|
||||
':subject_id' => !empty($sourceAssessment['subject_id']) ? (int) $sourceAssessment['subject_id'] : null,
|
||||
':title' => (string) ($sourceAssessment['title'] ?? ''),
|
||||
':category' => (string) ($sourceAssessment['category'] ?? ''),
|
||||
':scale_type' => (string) ($sourceAssessment['scale_type'] ?? 'percentage'),
|
||||
':max_score' => (float) ($sourceAssessment['max_score'] ?? 0),
|
||||
':weight_percentage' => (float) ($sourceAssessment['weight_percentage'] ?? 0),
|
||||
':is_active' => (int) ($sourceAssessment['is_active'] ?? 1),
|
||||
':notes' => !empty($sourceAssessment['notes']) ? (string) $sourceAssessment['notes'] : null,
|
||||
]);
|
||||
$newAssessmentId = (int) $pdo->lastInsertId();
|
||||
if ($newAssessmentId > 0) {
|
||||
$copyCriteriaStmt->execute([
|
||||
':target_cycle_id' => $targetCycleId,
|
||||
':target_assessment_id' => $newAssessmentId,
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':source_cycle_id' => $sourceCycleId,
|
||||
':source_assessment_id' => (int) ($sourceAssessment['id'] ?? 0),
|
||||
]);
|
||||
sync_assessment_total_score_from_criteria($centerApplicationId, $targetCycleId, $newAssessmentId);
|
||||
}
|
||||
}
|
||||
$summary['assessments'] = count($sourceAssessments);
|
||||
}
|
||||
|
||||
if (!empty($rollover['copy_students'])) {
|
||||
@ -605,6 +666,40 @@ function school_page_url(string $page, int $applicationId, ?int $cycleId = null)
|
||||
return $url;
|
||||
}
|
||||
|
||||
function next_student_code_for_cycle(int $centerApplicationId, int $cycleId): string
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT student_code FROM school_students WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id ORDER BY id DESC'
|
||||
);
|
||||
$stmt->execute([
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':cycle_id' => $cycleId,
|
||||
]);
|
||||
|
||||
$codes = $stmt->fetchAll(PDO::FETCH_COLUMN) ?: [];
|
||||
$prefix = 'ST-';
|
||||
$padding = 3;
|
||||
$maxNumber = 0;
|
||||
|
||||
foreach ($codes as $codeValue) {
|
||||
$code = trim((string) $codeValue);
|
||||
if ($code === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('/^(.*?)(\d+)$/', $code, $matches)) {
|
||||
if ($prefix === 'ST-' && trim((string) $matches[1]) !== '') {
|
||||
$prefix = (string) $matches[1];
|
||||
}
|
||||
$padding = max($padding, strlen((string) $matches[2]));
|
||||
$maxNumber = max($maxNumber, (int) $matches[2]);
|
||||
}
|
||||
}
|
||||
|
||||
return $prefix . str_pad((string) ($maxNumber + 1), $padding, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
function create_student_in_cycle(int $centerApplicationId, int $cycleId, array $data): int
|
||||
{
|
||||
$pdo = db_connection();
|
||||
@ -878,7 +973,25 @@ function create_assessment_type_in_cycle(int $centerApplicationId, int $cycleId,
|
||||
function list_school_assessments_by_cycle(int $centerApplicationId, int $cycleId, array $filters = [], int $limit = 0, int $offset = 0): array
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$query = 'SELECT * FROM school_assessment_types WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id';
|
||||
$query = 'SELECT sat.*,
|
||||
(
|
||||
SELECT COUNT(*)
|
||||
FROM school_assessment_criteria criteria
|
||||
WHERE criteria.assessment_type_id = sat.id
|
||||
AND criteria.center_application_id = sat.center_application_id
|
||||
AND criteria.cycle_id = sat.cycle_id
|
||||
AND criteria.is_active = 1
|
||||
) AS criteria_count,
|
||||
(
|
||||
SELECT COALESCE(SUM(criteria.max_score), 0)
|
||||
FROM school_assessment_criteria criteria
|
||||
WHERE criteria.assessment_type_id = sat.id
|
||||
AND criteria.center_application_id = sat.center_application_id
|
||||
AND criteria.cycle_id = sat.cycle_id
|
||||
AND criteria.is_active = 1
|
||||
) AS criteria_total_max_score
|
||||
FROM school_assessment_types sat
|
||||
WHERE sat.center_application_id = :center_application_id AND sat.cycle_id = :cycle_id';
|
||||
$params = [
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':cycle_id' => $cycleId,
|
||||
@ -886,24 +999,24 @@ function list_school_assessments_by_cycle(int $centerApplicationId, int $cycleId
|
||||
|
||||
$search = $filters['search'] ?? '';
|
||||
if ($search !== '') {
|
||||
$query .= ' AND (title LIKE :search1 OR category LIKE :search2)';
|
||||
$query .= ' AND (sat.title LIKE :search1 OR sat.category LIKE :search2)';
|
||||
$params[':search1'] = "%$search%";
|
||||
$params[':search2'] = "%$search%";
|
||||
}
|
||||
|
||||
$subject_id = $filters['subject_id'] ?? '';
|
||||
if ($subject_id !== '') {
|
||||
$query .= ' AND subject_id = :subject_id';
|
||||
$query .= ' AND sat.subject_id = :subject_id';
|
||||
$params[':subject_id'] = (int) $subject_id;
|
||||
}
|
||||
|
||||
$category = $filters['category'] ?? '';
|
||||
if ($category !== '') {
|
||||
$query .= ' AND category = :category';
|
||||
$query .= ' AND sat.category = :category';
|
||||
$params[':category'] = $category;
|
||||
}
|
||||
|
||||
$query .= ' ORDER BY is_active DESC, created_at DESC, id DESC';
|
||||
$query .= ' ORDER BY sat.is_active DESC, sat.created_at DESC, sat.id DESC';
|
||||
|
||||
if ($limit > 0) {
|
||||
$query .= ' LIMIT ' . (int)$limit . ' OFFSET ' . (int)$offset;
|
||||
@ -1064,6 +1177,9 @@ function school_assessment_type_options_by_cycle(int $centerApplicationId, int $
|
||||
$label .= ' — ' . $subjectLabel;
|
||||
}
|
||||
|
||||
$criteriaCount = (int) ($assessment['criteria_count'] ?? 0);
|
||||
$criteriaTotal = (float) ($assessment['criteria_total_max_score'] ?? 0);
|
||||
|
||||
$options[$assessmentId] = [
|
||||
'id' => $assessmentId,
|
||||
'label' => $label,
|
||||
@ -1072,6 +1188,9 @@ function school_assessment_type_options_by_cycle(int $centerApplicationId, int $
|
||||
'category' => (string) ($assessment['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,
|
||||
];
|
||||
}
|
||||
@ -1079,6 +1198,254 @@ function school_assessment_type_options_by_cycle(int $centerApplicationId, int $
|
||||
return $options;
|
||||
}
|
||||
|
||||
function list_assessment_criteria_by_assessment(int $centerApplicationId, int $cycleId, int $assessmentTypeId, bool $onlyActive = false): array
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$query = 'SELECT *
|
||||
FROM school_assessment_criteria
|
||||
WHERE center_application_id = :center_application_id
|
||||
AND cycle_id = :cycle_id
|
||||
AND assessment_type_id = :assessment_type_id';
|
||||
$params = [
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':cycle_id' => $cycleId,
|
||||
':assessment_type_id' => $assessmentTypeId,
|
||||
];
|
||||
|
||||
if ($onlyActive) {
|
||||
$query .= ' AND is_active = 1';
|
||||
}
|
||||
|
||||
$query .= ' ORDER BY sort_order ASC, id ASC';
|
||||
$stmt = $pdo->prepare($query);
|
||||
$stmt->execute($params);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
function school_assessment_criteria_metrics(int $centerApplicationId, int $cycleId, int $assessmentTypeId): array
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT
|
||||
COUNT(*) AS total_count,
|
||||
COALESCE(SUM(is_active = 1), 0) AS active_count,
|
||||
COALESCE(SUM(CASE WHEN is_active = 1 THEN max_score ELSE 0 END), 0) AS active_max_score
|
||||
FROM school_assessment_criteria
|
||||
WHERE center_application_id = :center_application_id
|
||||
AND cycle_id = :cycle_id
|
||||
AND assessment_type_id = :assessment_type_id"
|
||||
);
|
||||
$stmt->execute([
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':cycle_id' => $cycleId,
|
||||
':assessment_type_id' => $assessmentTypeId,
|
||||
]);
|
||||
$row = $stmt->fetch() ?: [];
|
||||
|
||||
return [
|
||||
'total' => (int) ($row['total_count'] ?? 0),
|
||||
'active' => (int) ($row['active_count'] ?? 0),
|
||||
'active_max_score' => (float) ($row['active_max_score'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
function validate_assessment_criteria_input(int $centerApplicationId, int $cycleId, int $assessmentTypeId, array $input): array
|
||||
{
|
||||
$data = ['criteria' => []];
|
||||
$errors = [];
|
||||
$assessmentOptions = school_assessment_type_options_by_cycle($centerApplicationId, $cycleId, false);
|
||||
if (!array_key_exists($assessmentTypeId, $assessmentOptions)) {
|
||||
return [$data, ['form' => 'يرجى اختيار تقييم صحيح من نفس الدورة.']];
|
||||
}
|
||||
|
||||
$postedRows = $input['criteria'] ?? [];
|
||||
if (!is_array($postedRows)) {
|
||||
$postedRows = [];
|
||||
}
|
||||
|
||||
$position = 1;
|
||||
$activeCount = 0;
|
||||
foreach ($postedRows as $rowKey => $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$criterionId = (int) ($row['id'] ?? 0);
|
||||
$title = clean_text((string) ($row['title'] ?? ''), 150);
|
||||
$maxScoreRaw = str_replace(',', '.', clean_text((string) ($row['max_score'] ?? ''), 30));
|
||||
$notes = clean_text((string) ($row['notes'] ?? ''), 500);
|
||||
$isActive = ((string) ($row['is_active'] ?? '1')) === '1' ? 1 : 0;
|
||||
|
||||
if ($criterionId <= 0 && $title === '' && $maxScoreRaw === '' && $notes === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rowErrors = [];
|
||||
if ($title === '') {
|
||||
$rowErrors[] = 'اسم البند مطلوب.';
|
||||
}
|
||||
|
||||
$maxScore = null;
|
||||
if ($maxScoreRaw === '' || !is_numeric($maxScoreRaw)) {
|
||||
$rowErrors[] = 'أدخل درجة رقمية للبند.';
|
||||
} else {
|
||||
$maxScore = round((float) $maxScoreRaw, 2);
|
||||
if ($maxScore <= 0 || $maxScore > 1000) {
|
||||
$rowErrors[] = 'درجة البند يجب أن تكون بين 0.01 و1000.';
|
||||
}
|
||||
}
|
||||
|
||||
if ($rowErrors !== []) {
|
||||
$errors['criteria_' . $rowKey] = implode(' ', $rowErrors);
|
||||
}
|
||||
|
||||
$data['criteria'][] = [
|
||||
'id' => $criterionId,
|
||||
'title' => $title,
|
||||
'max_score' => $maxScore !== null ? number_format($maxScore, 2, '.', '') : '',
|
||||
'notes' => $notes,
|
||||
'is_active' => (string) $isActive,
|
||||
'sort_order' => $position,
|
||||
];
|
||||
|
||||
if ($isActive === 1) {
|
||||
$activeCount++;
|
||||
}
|
||||
$position++;
|
||||
}
|
||||
|
||||
if ($data['criteria'] === []) {
|
||||
$errors['form'] = 'أضف بند تقييم واحد على الأقل قبل الحفظ.';
|
||||
} elseif ($activeCount === 0) {
|
||||
$errors['form'] = 'فعّل بنداً واحداً على الأقل ليظهر في ورقة الرصد.';
|
||||
}
|
||||
|
||||
return [$data, $errors];
|
||||
}
|
||||
|
||||
function sync_assessment_total_score_from_criteria(int $centerApplicationId, int $cycleId, int $assessmentTypeId): void
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$criteria = list_assessment_criteria_by_assessment($centerApplicationId, $cycleId, $assessmentTypeId, true);
|
||||
if ($criteria === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$totalMaxScore = 0.0;
|
||||
foreach ($criteria as $criterion) {
|
||||
$totalMaxScore += (float) ($criterion['max_score'] ?? 0);
|
||||
}
|
||||
$totalMaxScore = round($totalMaxScore, 2);
|
||||
|
||||
$assessmentStmt = $pdo->prepare(
|
||||
'UPDATE school_assessment_types
|
||||
SET max_score = :max_score, updated_at = NOW()
|
||||
WHERE id = :id AND center_application_id = :center_application_id AND cycle_id = :cycle_id'
|
||||
);
|
||||
$assessmentStmt->execute([
|
||||
':max_score' => $totalMaxScore,
|
||||
':id' => $assessmentTypeId,
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':cycle_id' => $cycleId,
|
||||
]);
|
||||
|
||||
$itemStmt = $pdo->prepare(
|
||||
'UPDATE school_assessment_score_items items
|
||||
INNER JOIN school_assessment_criteria criteria ON criteria.id = items.criterion_id
|
||||
SET items.max_score = criteria.max_score,
|
||||
items.updated_at = NOW()
|
||||
WHERE items.center_application_id = :center_application_id
|
||||
AND items.cycle_id = :cycle_id
|
||||
AND items.assessment_type_id = :assessment_type_id'
|
||||
);
|
||||
$itemStmt->execute([
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':cycle_id' => $cycleId,
|
||||
':assessment_type_id' => $assessmentTypeId,
|
||||
]);
|
||||
|
||||
$scoreStmt = $pdo->prepare(
|
||||
'UPDATE school_assessment_scores
|
||||
SET max_score = :max_score,
|
||||
updated_at = NOW()
|
||||
WHERE center_application_id = :center_application_id
|
||||
AND cycle_id = :cycle_id
|
||||
AND assessment_type_id = :assessment_type_id'
|
||||
);
|
||||
$scoreStmt->execute([
|
||||
':max_score' => $totalMaxScore,
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':cycle_id' => $cycleId,
|
||||
':assessment_type_id' => $assessmentTypeId,
|
||||
]);
|
||||
}
|
||||
|
||||
function save_assessment_criteria_in_cycle(int $centerApplicationId, int $cycleId, int $assessmentTypeId, array $data): int
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$existingStmt = $pdo->prepare(
|
||||
'SELECT id FROM school_assessment_criteria
|
||||
WHERE center_application_id = :center_application_id
|
||||
AND cycle_id = :cycle_id
|
||||
AND assessment_type_id = :assessment_type_id'
|
||||
);
|
||||
$existingStmt->execute([
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':cycle_id' => $cycleId,
|
||||
':assessment_type_id' => $assessmentTypeId,
|
||||
]);
|
||||
$existingIds = array_map('intval', $existingStmt->fetchAll(PDO::FETCH_COLUMN));
|
||||
$existingMap = array_fill_keys($existingIds, true);
|
||||
|
||||
$insertStmt = $pdo->prepare(
|
||||
'INSERT INTO school_assessment_criteria (
|
||||
center_application_id, cycle_id, assessment_type_id, title, max_score,
|
||||
sort_order, is_active, notes, created_at, updated_at
|
||||
) VALUES (
|
||||
:center_application_id, :cycle_id, :assessment_type_id, :title, :max_score,
|
||||
:sort_order, :is_active, :notes, NOW(), NOW()
|
||||
)'
|
||||
);
|
||||
$updateStmt = $pdo->prepare(
|
||||
'UPDATE school_assessment_criteria
|
||||
SET title = :title,
|
||||
max_score = :max_score,
|
||||
sort_order = :sort_order,
|
||||
is_active = :is_active,
|
||||
notes = :notes,
|
||||
updated_at = NOW()
|
||||
WHERE id = :id
|
||||
AND center_application_id = :center_application_id
|
||||
AND cycle_id = :cycle_id
|
||||
AND assessment_type_id = :assessment_type_id'
|
||||
);
|
||||
|
||||
$saved = 0;
|
||||
foreach ($data['criteria'] as $criterion) {
|
||||
$params = [
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':cycle_id' => $cycleId,
|
||||
':assessment_type_id' => $assessmentTypeId,
|
||||
':title' => (string) ($criterion['title'] ?? ''),
|
||||
':max_score' => (float) ($criterion['max_score'] ?? 0),
|
||||
':sort_order' => (int) ($criterion['sort_order'] ?? 0),
|
||||
':is_active' => ((string) ($criterion['is_active'] ?? '1')) === '1' ? 1 : 0,
|
||||
':notes' => !empty($criterion['notes']) ? (string) $criterion['notes'] : null,
|
||||
];
|
||||
|
||||
$criterionId = (int) ($criterion['id'] ?? 0);
|
||||
if ($criterionId > 0 && isset($existingMap[$criterionId])) {
|
||||
$updateStmt->execute($params + [':id' => $criterionId]);
|
||||
} else {
|
||||
$insertStmt->execute($params);
|
||||
}
|
||||
$saved++;
|
||||
}
|
||||
|
||||
sync_assessment_total_score_from_criteria($centerApplicationId, $cycleId, $assessmentTypeId);
|
||||
return $saved;
|
||||
}
|
||||
|
||||
function validate_assessment_scores_batch_input(int $centerApplicationId, int $cycleId, array $input): array
|
||||
{
|
||||
$data = [
|
||||
@ -1086,6 +1453,8 @@ function validate_assessment_scores_batch_input(int $centerApplicationId, int $c
|
||||
'teacher_id' => (string) ((int) ($input['teacher_id'] ?? 0)),
|
||||
'assessed_on' => clean_text((string) ($input['assessed_on'] ?? date('Y-m-d')), 20),
|
||||
'assessment_max_score' => 0.0,
|
||||
'has_criteria' => false,
|
||||
'criteria' => [],
|
||||
'entries' => [],
|
||||
];
|
||||
|
||||
@ -1099,7 +1468,25 @@ function validate_assessment_scores_batch_input(int $centerApplicationId, int $c
|
||||
$selectedAssessment = $assessmentOptions[$assessmentId] ?? null;
|
||||
if ($selectedAssessment === null) {
|
||||
$errors['assessment_type_id'] = 'يرجى اختيار تقييم صحيح من نفس الدورة.';
|
||||
} else {
|
||||
}
|
||||
|
||||
$criteriaRows = $assessmentId > 0
|
||||
? list_assessment_criteria_by_assessment($centerApplicationId, $cycleId, $assessmentId, true)
|
||||
: [];
|
||||
$criteriaMap = [];
|
||||
foreach ($criteriaRows as $criterion) {
|
||||
$criteriaId = (int) ($criterion['id'] ?? 0);
|
||||
if ($criteriaId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$criteriaMap[$criteriaId] = $criterion;
|
||||
$data['assessment_max_score'] += (float) ($criterion['max_score'] ?? 0);
|
||||
}
|
||||
$data['assessment_max_score'] = round($data['assessment_max_score'], 2);
|
||||
$data['has_criteria'] = $criteriaMap !== [];
|
||||
$data['criteria'] = $criteriaRows;
|
||||
|
||||
if (!$data['has_criteria'] && $selectedAssessment !== null) {
|
||||
$data['assessment_max_score'] = (float) ($selectedAssessment['max_score'] ?? 0);
|
||||
}
|
||||
|
||||
@ -1133,29 +1520,90 @@ function validate_assessment_scores_batch_input(int $centerApplicationId, int $c
|
||||
$status = 'present';
|
||||
}
|
||||
|
||||
$scoreRaw = clean_text((string) ($row['score'] ?? ''), 30);
|
||||
$notes = clean_text((string) ($row['notes'] ?? ''), 1000);
|
||||
$criteriaScores = [];
|
||||
$score = null;
|
||||
$shouldSave = $status !== 'present' || $scoreRaw !== '' || $notes !== '';
|
||||
$scoreRaw = '';
|
||||
$hasCriteriaInput = false;
|
||||
|
||||
if ($scoreRaw !== '') {
|
||||
if (!is_numeric($scoreRaw)) {
|
||||
$errors['entries_' . $studentId] = 'الدرجة يجب أن تكون رقماً صحيحاً أو عشرياً.';
|
||||
} else {
|
||||
$score = round((float) $scoreRaw, 2);
|
||||
if ($selectedAssessment !== null && ($score < 0 || $score > (float) $selectedAssessment['max_score'])) {
|
||||
$errors['entries_' . $studentId] = 'الدرجة يجب أن تكون بين 0 و ' . rtrim(rtrim(number_format((float) $selectedAssessment['max_score'], 2, '.', ''), '0'), '.');
|
||||
if ($data['has_criteria']) {
|
||||
$postedCriterionScores = $row['criteria'] ?? [];
|
||||
if (!is_array($postedCriterionScores)) {
|
||||
$postedCriterionScores = [];
|
||||
}
|
||||
|
||||
$missingCriteria = [];
|
||||
$totalScore = 0.0;
|
||||
foreach ($criteriaMap as $criterionId => $criterion) {
|
||||
$rawValue = str_replace(',', '.', clean_text((string) ($postedCriterionScores[$criterionId] ?? ''), 30));
|
||||
$criteriaScores[$criterionId] = [
|
||||
'criterion_id' => $criterionId,
|
||||
'score' => null,
|
||||
'score_raw' => $rawValue,
|
||||
'max_score' => (float) ($criterion['max_score'] ?? 0),
|
||||
];
|
||||
|
||||
if ($rawValue === '') {
|
||||
$missingCriteria[] = (string) ($criterion['title'] ?? '');
|
||||
continue;
|
||||
}
|
||||
|
||||
$hasCriteriaInput = true;
|
||||
if (!is_numeric($rawValue)) {
|
||||
$errors['entries_' . $studentId] = 'كل بند يجب أن يحتوي على درجة رقمية صحيحة.';
|
||||
continue;
|
||||
}
|
||||
|
||||
$criterionScore = round((float) $rawValue, 2);
|
||||
$criterionMax = (float) ($criterion['max_score'] ?? 0);
|
||||
if ($criterionScore < 0 || $criterionScore > $criterionMax) {
|
||||
$errors['entries_' . $studentId] = 'درجة البند يجب أن تكون بين 0 و ' . rtrim(rtrim(number_format($criterionMax, 2, '.', ''), '0'), '.') . '.';
|
||||
continue;
|
||||
}
|
||||
|
||||
$criteriaScores[$criterionId]['score'] = $criterionScore;
|
||||
$totalScore += $criterionScore;
|
||||
}
|
||||
|
||||
$shouldSave = $status !== 'present' || $hasCriteriaInput || $notes !== '';
|
||||
if ($status === 'present' && $shouldSave && $missingCriteria !== []) {
|
||||
$errors['entries_' . $studentId] = 'للطالب الحاضر يجب تعبئة جميع البنود النشطة قبل الحفظ.';
|
||||
}
|
||||
if ($status === 'present' && $missingCriteria === [] && !isset($errors['entries_' . $studentId])) {
|
||||
$score = round($totalScore, 2);
|
||||
$scoreRaw = number_format($score, 2, '.', '');
|
||||
}
|
||||
|
||||
if ($status !== 'present') {
|
||||
foreach ($criteriaScores as $criterionId => $criterionScoreData) {
|
||||
$criteriaScores[$criterionId]['score'] = null;
|
||||
$criteriaScores[$criterionId]['score_raw'] = '';
|
||||
}
|
||||
$score = null;
|
||||
$scoreRaw = '';
|
||||
}
|
||||
} else {
|
||||
$scoreRaw = clean_text((string) ($row['score'] ?? ''), 30);
|
||||
$shouldSave = $status !== 'present' || $scoreRaw !== '' || $notes !== '';
|
||||
if ($scoreRaw !== '') {
|
||||
if (!is_numeric($scoreRaw)) {
|
||||
$errors['entries_' . $studentId] = 'الدرجة يجب أن تكون رقماً صحيحاً أو عشرياً.';
|
||||
} else {
|
||||
$score = round((float) $scoreRaw, 2);
|
||||
if ($selectedAssessment !== null && ($score < 0 || $score > (float) $data['assessment_max_score'])) {
|
||||
$errors['entries_' . $studentId] = 'الدرجة يجب أن تكون بين 0 و ' . rtrim(rtrim(number_format((float) $data['assessment_max_score'], 2, '.', ''), '0'), '.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($status === 'present' && $shouldSave && $scoreRaw === '') {
|
||||
$errors['entries_' . $studentId] = 'أدخل الدرجة أو غيّر الحالة إلى غائب أو بعذر.';
|
||||
}
|
||||
if ($status === 'present' && $shouldSave && $scoreRaw === '') {
|
||||
$errors['entries_' . $studentId] = 'أدخل الدرجة أو غيّر الحالة إلى غائب أو بعذر.';
|
||||
}
|
||||
|
||||
if ($status !== 'present') {
|
||||
$score = null;
|
||||
$scoreRaw = '';
|
||||
if ($status !== 'present') {
|
||||
$score = null;
|
||||
$scoreRaw = '';
|
||||
}
|
||||
}
|
||||
|
||||
if ($shouldSave) {
|
||||
@ -1167,13 +1615,17 @@ function validate_assessment_scores_batch_input(int $centerApplicationId, int $c
|
||||
'status' => $status,
|
||||
'score' => $score,
|
||||
'score_raw' => $scoreRaw,
|
||||
'total_score' => $score,
|
||||
'notes' => $notes,
|
||||
'criteria_scores' => $criteriaScores,
|
||||
'should_save' => $shouldSave,
|
||||
];
|
||||
}
|
||||
|
||||
if (!$hasSaveableRow && $errors === []) {
|
||||
$errors['form'] = 'أدخل درجات أو حدّد حالة طالب واحد على الأقل قبل الحفظ.';
|
||||
$errors['form'] = $data['has_criteria']
|
||||
? 'أدخل درجات البنود أو حدّد حالة طالب واحد على الأقل قبل الحفظ.'
|
||||
: 'أدخل درجات أو حدّد حالة طالب واحد على الأقل قبل الحفظ.';
|
||||
}
|
||||
|
||||
return [$data, $errors, $selectedAssessment];
|
||||
@ -1182,6 +1634,17 @@ function validate_assessment_scores_batch_input(int $centerApplicationId, int $c
|
||||
function save_assessment_scores_in_cycle(int $centerApplicationId, int $cycleId, array $data): int
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$criteriaRows = !empty($data['has_criteria'])
|
||||
? list_assessment_criteria_by_assessment($centerApplicationId, $cycleId, (int) $data['assessment_type_id'], true)
|
||||
: [];
|
||||
$criteriaMap = [];
|
||||
foreach ($criteriaRows as $criterion) {
|
||||
$criteriaId = (int) ($criterion['id'] ?? 0);
|
||||
if ($criteriaId > 0) {
|
||||
$criteriaMap[$criteriaId] = $criterion;
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO school_assessment_scores (
|
||||
center_application_id, cycle_id, assessment_type_id, student_id, teacher_id,
|
||||
@ -1191,6 +1654,7 @@ function save_assessment_scores_in_cycle(int $centerApplicationId, int $cycleId,
|
||||
:score, :max_score, :status, :notes, :assessed_on, NOW(), NOW()
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
id = LAST_INSERT_ID(id),
|
||||
teacher_id = VALUES(teacher_id),
|
||||
score = VALUES(score),
|
||||
max_score = VALUES(max_score),
|
||||
@ -1199,6 +1663,16 @@ function save_assessment_scores_in_cycle(int $centerApplicationId, int $cycleId,
|
||||
assessed_on = VALUES(assessed_on),
|
||||
updated_at = NOW()'
|
||||
);
|
||||
$deleteItemsStmt = $pdo->prepare('DELETE FROM school_assessment_score_items WHERE assessment_score_id = :assessment_score_id');
|
||||
$itemStmt = $pdo->prepare(
|
||||
'INSERT INTO school_assessment_score_items (
|
||||
center_application_id, cycle_id, assessment_score_id, assessment_type_id, criterion_id,
|
||||
student_id, score, max_score, created_at, updated_at
|
||||
) VALUES (
|
||||
:center_application_id, :cycle_id, :assessment_score_id, :assessment_type_id, :criterion_id,
|
||||
:student_id, :score, :max_score, NOW(), NOW()
|
||||
)'
|
||||
);
|
||||
|
||||
$saved = 0;
|
||||
foreach ($data['entries'] as $entry) {
|
||||
@ -1219,6 +1693,29 @@ function save_assessment_scores_in_cycle(int $centerApplicationId, int $cycleId,
|
||||
':assessed_on' => $data['assessed_on'],
|
||||
]);
|
||||
|
||||
$assessmentScoreId = (int) $pdo->lastInsertId();
|
||||
if ($assessmentScoreId > 0) {
|
||||
$deleteItemsStmt->execute([':assessment_score_id' => $assessmentScoreId]);
|
||||
if ($criteriaMap !== [] && (string) ($entry['status'] ?? 'present') === 'present') {
|
||||
foreach ($entry['criteria_scores'] as $criterionId => $criterionScoreData) {
|
||||
if (!array_key_exists((int) $criterionId, $criteriaMap) || ($criterionScoreData['score'] ?? null) === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$itemStmt->execute([
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':cycle_id' => $cycleId,
|
||||
':assessment_score_id' => $assessmentScoreId,
|
||||
':assessment_type_id' => (int) $data['assessment_type_id'],
|
||||
':criterion_id' => (int) $criterionId,
|
||||
':student_id' => (int) $entry['student_id'],
|
||||
':score' => (float) $criterionScoreData['score'],
|
||||
':max_score' => (float) ($criteriaMap[(int) $criterionId]['max_score'] ?? 0),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$saved++;
|
||||
}
|
||||
|
||||
@ -1246,6 +1743,25 @@ function list_assessment_scores_by_assessment(int $centerApplicationId, int $cyc
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
function list_assessment_score_items_by_assessment(int $centerApplicationId, int $cycleId, int $assessmentTypeId): array
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT items.*
|
||||
FROM school_assessment_score_items items
|
||||
WHERE items.center_application_id = :center_application_id
|
||||
AND items.cycle_id = :cycle_id
|
||||
AND items.assessment_type_id = :assessment_type_id
|
||||
ORDER BY items.assessment_score_id ASC, items.id ASC'
|
||||
);
|
||||
$stmt->execute([
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':cycle_id' => $cycleId,
|
||||
':assessment_type_id' => $assessmentTypeId,
|
||||
]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
function school_assessment_score_map_by_assessment(int $centerApplicationId, int $cycleId, int $assessmentTypeId): array
|
||||
{
|
||||
$map = [];
|
||||
@ -1254,9 +1770,19 @@ function school_assessment_score_map_by_assessment(int $centerApplicationId, int
|
||||
if ($studentId <= 0 || array_key_exists($studentId, $map)) {
|
||||
continue;
|
||||
}
|
||||
$row['criteria_scores'] = [];
|
||||
$map[$studentId] = $row;
|
||||
}
|
||||
|
||||
foreach (list_assessment_score_items_by_assessment($centerApplicationId, $cycleId, $assessmentTypeId) as $item) {
|
||||
$studentId = (int) ($item['student_id'] ?? 0);
|
||||
$criterionId = (int) ($item['criterion_id'] ?? 0);
|
||||
if ($studentId <= 0 || $criterionId <= 0 || !isset($map[$studentId])) {
|
||||
continue;
|
||||
}
|
||||
$map[$studentId]['criteria_scores'][$criterionId] = $item;
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
|
||||
20
students.php
20
students.php
@ -14,6 +14,7 @@ $selectedCycle = null;
|
||||
$selectedCycleId = 0;
|
||||
$isCycleReadOnly = false;
|
||||
$cycleLabel = 'لا توجد دورة بعد';
|
||||
$nextStudentCode = '';
|
||||
|
||||
if ($application && $isApprovedSchool) {
|
||||
$cycleContext = resolve_school_cycle_context((int) $application['id'], $application, $requestedCycleId);
|
||||
@ -21,12 +22,21 @@ if ($application && $isApprovedSchool) {
|
||||
$selectedCycleId = $selectedCycle ? (int) ($selectedCycle['id'] ?? 0) : 0;
|
||||
$isCycleReadOnly = (bool) $cycleContext['read_only'];
|
||||
$cycleLabel = $selectedCycle ? (string) $selectedCycle['cycle_name'] : $cycleLabel;
|
||||
if ($selectedCycleId > 0) {
|
||||
$nextStudentCode = next_student_code_for_cycle((int) $application['id'], $selectedCycleId);
|
||||
$values['student_code'] = $nextStudentCode;
|
||||
}
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $application) {
|
||||
$action = $_POST['action'] ?? 'add';
|
||||
$studentId = filter_input(INPUT_POST, 'student_id', FILTER_VALIDATE_INT) ?: 0;
|
||||
[$values, $errors] = validate_student_input($_POST);
|
||||
$isEditAction = $action === 'edit' && $studentId > 0;
|
||||
[$values, $errors] = validate_student_input($_POST, $isEditAction);
|
||||
|
||||
if (!$isEditAction && $selectedCycleId > 0 && $values['student_code'] === '') {
|
||||
$values['student_code'] = $nextStudentCode !== '' ? $nextStudentCode : next_student_code_for_cycle((int) $application['id'], $selectedCycleId);
|
||||
}
|
||||
|
||||
if (!$isApprovedSchool) {
|
||||
$errors['form'] = 'لا يمكن فتح تسجيل الطلاب قبل اعتماد المركز.';
|
||||
@ -343,8 +353,9 @@ render_flash($flash);
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="student_code">الرقم / الكود</label>
|
||||
<input class="form-control <?= isset($errors['student_code']) ? 'is-invalid' : '' ?>" id="student_code" name="student_code" value="<?= e($values['student_code']) ?>" placeholder="مثال: ST-401" required>
|
||||
<label class="form-label" for="student_code">رقم الطالب</label>
|
||||
<input class="form-control <?= isset($errors['student_code']) ? 'is-invalid' : '' ?>" id="student_code" name="student_code" value="<?= e($values['student_code'] !== '' ? $values['student_code'] : $nextStudentCode) ?>" readonly>
|
||||
<div class="form-text">يتم توليد رقم الطالب تلقائياً عند إضافة سجل جديد.</div>
|
||||
<?php if (isset($errors['student_code'])): ?><div class="invalid-feedback"><?= e($errors['student_code']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@ -413,6 +424,8 @@ render_flash($flash);
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const defaultStudentCode = <?= json_encode($nextStudentCode !== '' ? $nextStudentCode : ($values['student_code'] ?? '')) ?>;
|
||||
|
||||
function resetStudentModal() {
|
||||
document.getElementById('studentModalLabel').innerText = 'إضافة طالب جديد';
|
||||
document.getElementById('formAction').value = 'add';
|
||||
@ -421,6 +434,7 @@ function resetStudentModal() {
|
||||
// Clear inputs except defaults
|
||||
const form = document.getElementById('studentForm');
|
||||
form.reset();
|
||||
document.getElementById('student_code').value = defaultStudentCode || '';
|
||||
document.getElementById('enrollment_status').value = 'active';
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user