439 lines
26 KiB
PHP
439 lines
26 KiB
PHP
<?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="admin-layout row g-4 align-items-start">
|
|
<div class="col-lg-3 layout-sidebar-column">
|
|
<?php if ($application) { require __DIR__ . '/includes/center_sidebar.php'; } else { require __DIR__ . '/includes/sidebar.php'; } ?>
|
|
</div>
|
|
<div class="col-lg-9 layout-content-column">
|
|
<?php if (!$application): ?>
|
|
<div class="app-card text-center py-5">
|
|
<div class="empty-title mb-2">المدرسة غير موجودة</div>
|
|
<p class="text-muted mb-3">تحقق من رابط المدرسة أو ارجع إلى قائمة المراكز المعتمدة.</p>
|
|
<a class="btn btn-primary" href="applications.php?status=approved">المراكز المعتمدة</a>
|
|
</div>
|
|
<?php elseif (!$isApprovedSchool): ?>
|
|
<div class="app-card text-center py-5">
|
|
<div class="empty-title mb-2">الدرجات تُفتح بعد الاعتماد</div>
|
|
<p class="text-muted mb-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();
|