add students assessments
This commit is contained in:
parent
6d8ede20a0
commit
a83ac160ed
@ -8,12 +8,14 @@ $settings = get_app_settings();
|
||||
$errors = [];
|
||||
$values = [
|
||||
'app_name' => $settings['app_name'] ?? '',
|
||||
'app_slogan' => $settings['app_slogan'] ?? '',
|
||||
'app_email' => $settings['app_email'] ?? '',
|
||||
'app_telephone' => $settings['app_telephone'] ?? '',
|
||||
];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$values['app_name'] = clean_text($_POST['app_name'] ?? '', 190);
|
||||
$values['app_slogan'] = clean_text($_POST['app_slogan'] ?? '', 190);
|
||||
$values['app_email'] = clean_text($_POST['app_email'] ?? '', 190);
|
||||
$values['app_telephone'] = clean_text($_POST['app_telephone'] ?? '', 60);
|
||||
|
||||
@ -54,9 +56,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
if (empty($errors)) {
|
||||
try {
|
||||
$stmt = db()->prepare('UPDATE app_settings SET app_name = ?, app_email = ?, app_telephone = ?, app_logo = ?, app_favicon = ?, updated_at = NOW() WHERE id = 1');
|
||||
$stmt = db()->prepare('UPDATE app_settings SET app_name = ?, app_slogan = ?, app_email = ?, app_telephone = ?, app_logo = ?, app_favicon = ?, updated_at = NOW() WHERE id = 1');
|
||||
$stmt->execute([
|
||||
$values['app_name'],
|
||||
$values['app_slogan'],
|
||||
$values['app_email'],
|
||||
$values['app_telephone'],
|
||||
$logoPath,
|
||||
@ -93,11 +96,16 @@ render_flash($flash);
|
||||
|
||||
<form method="post" enctype="multipart/form-data" novalidate>
|
||||
<div class="row g-4">
|
||||
<div class="col-md-12">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">اسم النظام (المنصة)</label>
|
||||
<input class="form-control <?= isset($errors['app_name']) ? 'is-invalid' : '' ?>" name="app_name" value="<?= e($values['app_name']) ?>">
|
||||
<?php if (isset($errors['app_name'])): ?><div class="invalid-feedback"><?= e($errors['app_name']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">الشعار اللفظي (Slogan)</label>
|
||||
<input class="form-control <?= isset($errors['app_slogan']) ? 'is-invalid' : '' ?>" name="app_slogan" value="<?= e($values['app_slogan']) ?>">
|
||||
<?php if (isset($errors['app_slogan'])): ?><div class="invalid-feedback"><?= e($errors['app_slogan']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">البريد الإلكتروني العام</label>
|
||||
<input type="email" class="form-control <?= isset($errors['app_email']) ? 'is-invalid' : '' ?>" name="app_email" value="<?= e($values['app_email']) ?>">
|
||||
|
||||
@ -133,6 +133,7 @@ $studentsUrl = school_page_url('students.php', (int) $application['id'], $select
|
||||
$teachersUrl = school_page_url('teachers.php', (int) $application['id'], $selectedCycleId);
|
||||
$assessmentsUrl = school_page_url('assessments.php', (int) $application['id'], $selectedCycleId);
|
||||
$attendanceUrl = school_page_url('attendance.php', (int) $application['id'], $selectedCycleId);
|
||||
$assessmentScoresUrl = school_page_url('assessment_scores.php', (int) $application['id'], $selectedCycleId);
|
||||
$approvedSchoolUrl = school_page_url('approved_school.php', (int) $application['id'], $selectedCycleId);
|
||||
$centerSubjectsUrl = school_page_url('center_subjects.php', (int) $application['id'], $selectedCycleId);
|
||||
$studentCycleMetrics = $isApproved && $selectedCycleId > 0 ? school_student_metrics_by_cycle((int) $application['id'], $selectedCycleId) : ['total' => 0, 'active' => 0];
|
||||
@ -185,6 +186,7 @@ render_flash($flash);
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 mt-md-0 d-flex gap-2">
|
||||
<a class="btn btn-sm btn-outline-secondary" href="<?= e($assessmentScoresUrl) ?>">إدخال الدرجات</a>
|
||||
<a class="btn btn-sm btn-outline-secondary" href="center_profile.php?id=<?= e((string) $application['id']) ?>">إعدادات المركز</a>
|
||||
<a class="btn btn-sm btn-outline-secondary" href="application_detail.php?id=<?= e((string) $application['id']) ?>">ملف الاعتماد</a>
|
||||
<a class="btn btn-sm btn-primary" href="admin.php">لوحة الإدارة</a>
|
||||
|
||||
439
assessment_scores.php
Normal file
439
assessment_scores.php
Normal file
@ -0,0 +1,439 @@
|
||||
<?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 = [];
|
||||
$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)
|
||||
: [];
|
||||
|
||||
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'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$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 = 'صفحة مستقلة تسمح للمعلم أو الإدارة الأكاديمية بإدخال درجات الطلاب لكل تقييم داخل دورة موسمية محددة.';
|
||||
$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'), '.')
|
||||
: 'لا يوجد';
|
||||
|
||||
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="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>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</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>
|
||||
</div>
|
||||
<span class="header-chip"><?= e((string) count($students)) ?> طلاب ظاهرون</span>
|
||||
</div>
|
||||
|
||||
<?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>
|
||||
<?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>
|
||||
</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 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; ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php render_page_end();
|
||||
@ -86,6 +86,7 @@ $weightGap = round(100 - $activeWeight, 2);
|
||||
$pageTitle = $application ? 'التقييمات والأوزان: ' . (string) $application['center_name'] . ($selectedCycle ? ' — ' . $cycleLabel : '') : 'التقييمات والأوزان';
|
||||
$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';
|
||||
|
||||
if (!$application) {
|
||||
http_response_code(404);
|
||||
@ -128,11 +129,14 @@ render_flash($flash);
|
||||
<p class="page-copy mb-0">إدارة أنواع التقييم وتوزيع الدرجات ضمن الخطة الأكاديمية للدورة <strong><?= e($cycleLabel) ?></strong>.</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-md-end">
|
||||
<?php if (!$isCycleReadOnly): ?>
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#assessmentModal" onclick="resetAssessmentForm()">
|
||||
إضافة تقييم جديد
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<div class="d-flex flex-wrap justify-content-md-end gap-2">
|
||||
<a class="btn btn-outline-secondary" href="<?= e($assessmentScoresUrl) ?>">إدخال الدرجات</a>
|
||||
<?php if (!$isCycleReadOnly): ?>
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#assessmentModal" onclick="resetAssessmentForm()">
|
||||
إضافة تقييم جديد
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -51,7 +51,7 @@ render_flash($flash);
|
||||
<div class="section-title">بيانات الطلب الأساسية</div>
|
||||
<div class="section-copy">قسّمنا النموذج إلى مجموعات واضحة حتى لا تختلط بيانات المركز مع بيانات التشغيل والتواصل.</div>
|
||||
</div>
|
||||
<span class="header-chip">نموذج منظم</span>
|
||||
|
||||
</div>
|
||||
|
||||
<?php if (!empty($errors['form'])): ?>
|
||||
@ -221,23 +221,23 @@ render_flash($flash);
|
||||
<div class="d-flex align-items-center justify-content-between mb-3 mt-4">
|
||||
<h3 class="h6 mb-0 fw-bold text-dark">روابط سريعة</h3>
|
||||
</div>
|
||||
<div class="d-flex flex-column gap-3">
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<a href="applications.php" class="text-decoration-none">
|
||||
<article class="app-card link-card p-3 bg-white rounded-3 border-start border-primary border-4 shadow-sm" style="transition: transform 0.2s;">
|
||||
<h4 class="h6 fw-bold text-dark mb-1">لوحة الطلبات</h4>
|
||||
<p class="text-muted small mb-0">متابعة جميع الطلبات بعد الإرسال.</p>
|
||||
<article class="app-card link-card p-2 bg-white rounded-2 border-start border-primary border-4 shadow-sm" style="transition: transform 0.2s;">
|
||||
<h4 class="small fw-bold text-dark mb-1">لوحة الطلبات</h4>
|
||||
<p class="text-muted mb-0" style="font-size: 0.75rem;">متابعة جميع الطلبات بعد الإرسال.</p>
|
||||
</article>
|
||||
</a>
|
||||
<a href="dashboard.php" class="text-decoration-none">
|
||||
<article class="app-card link-card p-3 bg-white rounded-3 border-start border-warning border-4 shadow-sm" style="transition: transform 0.2s;">
|
||||
<h4 class="h6 fw-bold text-dark mb-1">لوحة القيادة</h4>
|
||||
<p class="text-muted small mb-0">مراجعة المؤشرات العامة للولاية.</p>
|
||||
<article class="app-card link-card p-2 bg-white rounded-2 border-start border-warning border-4 shadow-sm" style="transition: transform 0.2s;">
|
||||
<h4 class="small fw-bold text-dark mb-1">لوحة القيادة</h4>
|
||||
<p class="text-muted mb-0" style="font-size: 0.75rem;">مراجعة المؤشرات العامة للولاية.</p>
|
||||
</article>
|
||||
</a>
|
||||
<a href="modules.php" class="text-decoration-none">
|
||||
<article class="app-card link-card p-3 bg-white rounded-3 border-start border-success border-4 shadow-sm" style="transition: transform 0.2s;">
|
||||
<h4 class="h6 fw-bold text-dark mb-1">هيكل النظام</h4>
|
||||
<p class="text-muted small mb-0">فهم حدود النسخة الحالية ومسار العمل.</p>
|
||||
<article class="app-card link-card p-2 bg-white rounded-2 border-start border-success border-4 shadow-sm" style="transition: transform 0.2s;">
|
||||
<h4 class="small fw-bold text-dark mb-1">هيكل النظام</h4>
|
||||
<p class="text-muted mb-0" style="font-size: 0.75rem;">فهم حدود النسخة الحالية ومسار العمل.</p>
|
||||
</article>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
1
db/migrations/20260416_alter_app_settings_slogan.sql
Normal file
1
db/migrations/20260416_alter_app_settings_slogan.sql
Normal file
@ -0,0 +1 @@
|
||||
ALTER TABLE app_settings ADD COLUMN app_slogan VARCHAR(255);
|
||||
27
db/migrations/20260417_school_assessment_scores.sql
Normal file
27
db/migrations/20260417_school_assessment_scores.sql
Normal file
@ -0,0 +1,27 @@
|
||||
CREATE TABLE IF NOT EXISTS school_assessment_scores (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
center_application_id INT UNSIGNED NOT NULL,
|
||||
cycle_id INT UNSIGNED NOT NULL,
|
||||
assessment_type_id INT UNSIGNED NOT NULL,
|
||||
student_id INT UNSIGNED NOT NULL,
|
||||
teacher_id INT UNSIGNED NULL,
|
||||
score DECIMAL(8,2) NULL,
|
||||
max_score DECIMAL(8,2) NOT NULL DEFAULT 100.00,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'present',
|
||||
notes TEXT NULL,
|
||||
assessed_on DATE NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uniq_school_assessment_score (cycle_id, assessment_type_id, student_id),
|
||||
INDEX idx_school_assessment_scores_center (center_application_id),
|
||||
INDEX idx_school_assessment_scores_cycle (cycle_id),
|
||||
INDEX idx_school_assessment_scores_assessment (assessment_type_id),
|
||||
INDEX idx_school_assessment_scores_student (student_id),
|
||||
INDEX idx_school_assessment_scores_teacher (teacher_id),
|
||||
INDEX idx_school_assessment_scores_status (status),
|
||||
CONSTRAINT fk_school_assessment_scores_center_application FOREIGN KEY (center_application_id) REFERENCES center_applications(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_school_assessment_scores_cycle FOREIGN KEY (cycle_id) REFERENCES school_cycles(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_school_assessment_scores_assessment FOREIGN KEY (assessment_type_id) REFERENCES school_assessment_types(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_school_assessment_scores_student FOREIGN KEY (student_id) REFERENCES school_students(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_school_assessment_scores_teacher FOREIGN KEY (teacher_id) REFERENCES school_teachers(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@ -109,6 +109,7 @@ function db_connection(): PDO
|
||||
seed_center_application_demo_data($pdo);
|
||||
ensure_school_module_schema($pdo);
|
||||
ensure_school_cycle_schema($pdo);
|
||||
ensure_school_assessment_score_schema($pdo);
|
||||
seed_school_module_demo_data($pdo);
|
||||
$bootstrapped = true;
|
||||
}
|
||||
@ -143,7 +144,8 @@ function get_app_settings(): array
|
||||
'app_email' => '',
|
||||
'app_telephone' => '',
|
||||
'app_logo' => '',
|
||||
'app_favicon' => ''
|
||||
'app_favicon' => '',
|
||||
'app_slogan' => ''
|
||||
];
|
||||
}
|
||||
return $res;
|
||||
@ -1272,7 +1274,10 @@ function school_attendance_metrics(int $centerApplicationId): array
|
||||
|
||||
function render_page_start(string $pageTitle, string $active = 'home', string $pageDescription = ''): void
|
||||
{
|
||||
$projectName = project_name();
|
||||
$settings = get_app_settings();
|
||||
$projectName = !empty($settings['app_name']) ? $settings['app_name'] : project_name();
|
||||
$projectLogo = !empty($settings['app_logo']) ? $settings['app_logo'] : '';
|
||||
$projectSlogan = !empty($settings['app_slogan']) ? $settings['app_slogan'] : 'منصة الولاية لإدارة المراكز الصيفية';
|
||||
$description = $pageDescription !== '' ? $pageDescription : project_description();
|
||||
$projectImageUrl = env_value('PROJECT_IMAGE_URL');
|
||||
?>
|
||||
@ -1305,10 +1310,14 @@ function render_page_start(string $pageTitle, string $active = 'home', string $p
|
||||
<nav class="navbar navbar-expand-lg p-0">
|
||||
<div class="container-fluid p-0 align-items-center gap-3">
|
||||
<a class="navbar-brand brand-mark d-flex align-items-center gap-3 m-0" href="index.php">
|
||||
<span class="brand-badge">م</span>
|
||||
<?php if ($projectLogo !== ''): ?>
|
||||
<img src="<?= e(asset_url($projectLogo)) ?>" alt="<?= e($projectName) ?>" class="brand-logo" style="max-height: 48px; object-fit: contain; border-radius: 8px;">
|
||||
<?php else: ?>
|
||||
<span class="brand-badge"><?= mb_substr($projectName, 0, 1) ?></span>
|
||||
<?php endif; ?>
|
||||
<span>
|
||||
<span class="d-block brand-title"><?= e($projectName) ?></span>
|
||||
<span class="d-block brand-subtitle">منصة الولاية لإدارة المراكز الصيفية</span>
|
||||
<span class="d-block brand-subtitle text-warning"><?= e($projectSlogan) ?></span>
|
||||
</span>
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav" aria-controls="mainNav" aria-expanded="false" aria-label="تبديل التنقل">
|
||||
@ -1324,21 +1333,7 @@ function render_page_start(string $pageTitle, string $active = 'home', string $p
|
||||
<li class="nav-item"><a class="nav-link <?= $active === 'approved' ? 'active' : '' ?>" href="applications.php?status=approved">المراكز المعتمدة</a></li>
|
||||
<li class="nav-item"><a class="nav-link <?= $active === 'modules' ? 'active' : '' ?>" href="modules.php">هيكل النظام</a></li>
|
||||
</ul>
|
||||
<div class="d-flex align-items-center gap-2 header-actions">
|
||||
<?php
|
||||
$isSuperAdmin = is_super_admin();
|
||||
$roleName = $isSuperAdmin ? 'المشرف العام' : 'مدير المركز';
|
||||
$nextRole = $isSuperAdmin ? 'center_admin' : 'super_admin';
|
||||
$currentUrl = $_SERVER['REQUEST_URI'];
|
||||
$roleSwitchUrl = strpos($currentUrl, '?') !== false
|
||||
? preg_replace('/([?&])role=[^&]*(&|$)/', '$1', $currentUrl)
|
||||
: $currentUrl;
|
||||
$roleSwitchUrl = rtrim($roleSwitchUrl, '?&');
|
||||
$roleSwitchUrl .= (strpos($roleSwitchUrl, '?') !== false ? '&' : '?') . 'role=' . $nextRole;
|
||||
?>
|
||||
<a href="<?= e($roleSwitchUrl) ?>" class="header-chip text-decoration-none bg-primary text-white" style="cursor:pointer;" title="اضغط للتبديل">صلاحية: <?= e($roleName) ?> ⟳</a>
|
||||
<a class="btn btn-primary btn-sm px-3" href="applications.php?status=submitted">مراجعة سريعة</a>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2 header-actions"></div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@ -7,6 +7,7 @@ if ($script === 'students.php') $activePage = 'students';
|
||||
if ($script === 'teachers.php') $activePage = 'teachers';
|
||||
if ($script === 'assessments.php') $activePage = 'assessments';
|
||||
if ($script === 'attendance.php') $activePage = 'attendance';
|
||||
if ($script === 'assessment_scores.php') $activePage = 'scores';
|
||||
if ($script === 'center_subjects.php') $activePage = 'subjects';
|
||||
|
||||
// We assume $application is available in scope.
|
||||
@ -53,6 +54,11 @@ $baseQuery = '?id=' . e((string) $application['id']) . $selectedCycleIdStr;
|
||||
التقييمات
|
||||
</a>
|
||||
|
||||
<a href="assessment_scores.php<?= $baseQuery ?>" class="sidebar-link <?= $activePage === 'scores' ? 'active' : '' ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path d="M8 12a.5.5 0 0 1-.374-.168l-2.5-2.75a.5.5 0 1 1 .748-.664L8 10.765l2.126-2.347a.5.5 0 1 1 .748.664l-2.5 2.75A.5.5 0 0 1 8 12z"/><path d="M4 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V5.5L9.5 1H4zm5 1.5V5h2.5L9 2.5zM4 2h4v3a1 1 0 0 0 1 1h3v7a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z"/></svg>
|
||||
إدخال الدرجات
|
||||
</a>
|
||||
|
||||
<a href="attendance.php<?= $baseQuery ?>" class="sidebar-link <?= $activePage === 'attendance' ? 'active' : '' ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path d="M11 2a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v12h.5a.5.5 0 0 1 0 1H.5a.5.5 0 0 1 0-1H1v-3a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v3h1V7a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v7h1V2zm1 12h2V2h-2v12zm-3 0V7H7v7h2zm-5 0v-3H2v3h2z"/></svg>
|
||||
سجلات الغياب
|
||||
|
||||
@ -104,6 +104,24 @@ function ensure_school_cycle_schema(PDO $pdo): void
|
||||
$done = true;
|
||||
}
|
||||
|
||||
function ensure_school_assessment_score_schema(PDO $pdo): void
|
||||
{
|
||||
static $done = false;
|
||||
if ($done) {
|
||||
return;
|
||||
}
|
||||
|
||||
$migrationPath = __DIR__ . '/../db/migrations/20260417_school_assessment_scores.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(
|
||||
@ -965,6 +983,319 @@ function school_assessment_metrics_by_cycle(int $centerApplicationId, int $cycle
|
||||
];
|
||||
}
|
||||
|
||||
function assessment_score_status_map(): array
|
||||
{
|
||||
return [
|
||||
'present' => ['label' => 'حاضر', 'class' => 'status-approved'],
|
||||
'absent' => ['label' => 'غائب', 'class' => 'status-review'],
|
||||
'excused' => ['label' => 'بعذر', 'class' => 'status-muted'],
|
||||
];
|
||||
}
|
||||
|
||||
function assessment_score_status_badge(string $status): string
|
||||
{
|
||||
$map = assessment_score_status_map();
|
||||
$meta = $map[$status] ?? ['label' => 'غير محدد', 'class' => 'status-muted'];
|
||||
return '<span class="status-badge ' . e($meta['class']) . '">' . e($meta['label']) . '</span>';
|
||||
}
|
||||
|
||||
function school_teacher_options_by_cycle(int $centerApplicationId, int $cycleId, bool $onlyActive = false): array
|
||||
{
|
||||
$filters = [];
|
||||
if ($onlyActive) {
|
||||
$filters['employment_status'] = 'active';
|
||||
}
|
||||
|
||||
$teachers = list_school_teachers_by_cycle($centerApplicationId, $cycleId, $filters);
|
||||
$options = [];
|
||||
foreach ($teachers as $teacher) {
|
||||
$teacherId = (int) ($teacher['id'] ?? 0);
|
||||
if ($teacherId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$roleTitle = trim((string) ($teacher['role_title'] ?? ''));
|
||||
$specialization = trim((string) ($teacher['specialization'] ?? ''));
|
||||
$label = trim((string) ($teacher['full_name'] ?? ''));
|
||||
if ($roleTitle !== '') {
|
||||
$label .= ' — ' . $roleTitle;
|
||||
}
|
||||
if ($specialization !== '') {
|
||||
$label .= ' (' . $specialization . ')';
|
||||
}
|
||||
|
||||
$options[$teacherId] = [
|
||||
'label' => $label,
|
||||
'full_name' => trim((string) ($teacher['full_name'] ?? '')),
|
||||
'role_title' => $roleTitle,
|
||||
'specialization' => $specialization,
|
||||
'employment_status' => (string) ($teacher['employment_status'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
function school_assessment_type_options_by_cycle(int $centerApplicationId, int $cycleId, bool $onlyActive = false): array
|
||||
{
|
||||
$rows = list_school_assessments_by_cycle($centerApplicationId, $cycleId);
|
||||
$subjects = [];
|
||||
foreach (get_enabled_subjects() as $subject) {
|
||||
$subjects[(int) ($subject['id'] ?? 0)] = (string) ($subject['name'] ?? '');
|
||||
}
|
||||
|
||||
$options = [];
|
||||
foreach ($rows as $assessment) {
|
||||
$assessmentId = (int) ($assessment['id'] ?? 0);
|
||||
if ($assessmentId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$isActive = (int) ($assessment['is_active'] ?? 0) === 1;
|
||||
if ($onlyActive && !$isActive) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$subjectId = (int) ($assessment['subject_id'] ?? 0);
|
||||
$subjectLabel = $subjectId > 0 ? ($subjects[$subjectId] ?? '') : '';
|
||||
$title = trim((string) ($assessment['title'] ?? ''));
|
||||
$label = $title !== '' ? $title : 'تقييم غير مسمى';
|
||||
if ($subjectLabel !== '') {
|
||||
$label .= ' — ' . $subjectLabel;
|
||||
}
|
||||
|
||||
$options[$assessmentId] = [
|
||||
'id' => $assessmentId,
|
||||
'label' => $label,
|
||||
'title' => $title,
|
||||
'subject_label' => $subjectLabel,
|
||||
'category' => (string) ($assessment['category'] ?? ''),
|
||||
'max_score' => (float) ($assessment['max_score'] ?? 0),
|
||||
'weight_percentage' => (float) ($assessment['weight_percentage'] ?? 0),
|
||||
'is_active' => $isActive,
|
||||
];
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
function validate_assessment_scores_batch_input(int $centerApplicationId, int $cycleId, array $input): array
|
||||
{
|
||||
$data = [
|
||||
'assessment_type_id' => (string) ((int) ($input['assessment_type_id'] ?? 0)),
|
||||
'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,
|
||||
'entries' => [],
|
||||
];
|
||||
|
||||
$errors = [];
|
||||
$assessmentOptions = school_assessment_type_options_by_cycle($centerApplicationId, $cycleId, false);
|
||||
$teacherOptions = school_teacher_options_by_cycle($centerApplicationId, $cycleId, false);
|
||||
$studentOptions = school_student_options_by_cycle($centerApplicationId, $cycleId);
|
||||
$statusMap = assessment_score_status_map();
|
||||
|
||||
$assessmentId = (int) $data['assessment_type_id'];
|
||||
$selectedAssessment = $assessmentOptions[$assessmentId] ?? null;
|
||||
if ($selectedAssessment === null) {
|
||||
$errors['assessment_type_id'] = 'يرجى اختيار تقييم صحيح من نفس الدورة.';
|
||||
} else {
|
||||
$data['assessment_max_score'] = (float) ($selectedAssessment['max_score'] ?? 0);
|
||||
}
|
||||
|
||||
$teacherId = (int) $data['teacher_id'];
|
||||
if ($teacherId > 0 && !array_key_exists($teacherId, $teacherOptions)) {
|
||||
$errors['teacher_id'] = 'يرجى اختيار معلم صحيح من نفس الدورة.';
|
||||
}
|
||||
|
||||
if ($data['assessed_on'] === '' || strtotime($data['assessed_on']) === false) {
|
||||
$errors['assessed_on'] = 'يرجى إدخال تاريخ تقييم صحيح.';
|
||||
}
|
||||
|
||||
$postedEntries = $input['entries'] ?? [];
|
||||
if (!is_array($postedEntries)) {
|
||||
$postedEntries = [];
|
||||
}
|
||||
|
||||
$hasSaveableRow = false;
|
||||
foreach ($postedEntries as $studentKey => $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$studentId = (int) $studentKey;
|
||||
if ($studentId <= 0 || !array_key_exists($studentId, $studentOptions)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$status = clean_text((string) ($row['status'] ?? 'present'), 20);
|
||||
if (!array_key_exists($status, $statusMap)) {
|
||||
$status = 'present';
|
||||
}
|
||||
|
||||
$scoreRaw = clean_text((string) ($row['score'] ?? ''), 30);
|
||||
$notes = clean_text((string) ($row['notes'] ?? ''), 1000);
|
||||
$score = null;
|
||||
$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) $selectedAssessment['max_score'])) {
|
||||
$errors['entries_' . $studentId] = 'الدرجة يجب أن تكون بين 0 و ' . rtrim(rtrim(number_format((float) $selectedAssessment['max_score'], 2, '.', ''), '0'), '.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($status === 'present' && $shouldSave && $scoreRaw === '') {
|
||||
$errors['entries_' . $studentId] = 'أدخل الدرجة أو غيّر الحالة إلى غائب أو بعذر.';
|
||||
}
|
||||
|
||||
if ($status !== 'present') {
|
||||
$score = null;
|
||||
$scoreRaw = '';
|
||||
}
|
||||
|
||||
if ($shouldSave) {
|
||||
$hasSaveableRow = true;
|
||||
}
|
||||
|
||||
$data['entries'][$studentId] = [
|
||||
'student_id' => $studentId,
|
||||
'status' => $status,
|
||||
'score' => $score,
|
||||
'score_raw' => $scoreRaw,
|
||||
'notes' => $notes,
|
||||
'should_save' => $shouldSave,
|
||||
];
|
||||
}
|
||||
|
||||
if (!$hasSaveableRow && $errors === []) {
|
||||
$errors['form'] = 'أدخل درجات أو حدّد حالة طالب واحد على الأقل قبل الحفظ.';
|
||||
}
|
||||
|
||||
return [$data, $errors, $selectedAssessment];
|
||||
}
|
||||
|
||||
function save_assessment_scores_in_cycle(int $centerApplicationId, int $cycleId, array $data): int
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO school_assessment_scores (
|
||||
center_application_id, cycle_id, assessment_type_id, student_id, teacher_id,
|
||||
score, max_score, status, notes, assessed_on, created_at, updated_at
|
||||
) VALUES (
|
||||
:center_application_id, :cycle_id, :assessment_type_id, :student_id, :teacher_id,
|
||||
:score, :max_score, :status, :notes, :assessed_on, NOW(), NOW()
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
teacher_id = VALUES(teacher_id),
|
||||
score = VALUES(score),
|
||||
max_score = VALUES(max_score),
|
||||
status = VALUES(status),
|
||||
notes = VALUES(notes),
|
||||
assessed_on = VALUES(assessed_on),
|
||||
updated_at = NOW()'
|
||||
);
|
||||
|
||||
$saved = 0;
|
||||
foreach ($data['entries'] as $entry) {
|
||||
if (empty($entry['should_save'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$stmt->execute([
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':cycle_id' => $cycleId,
|
||||
':assessment_type_id' => (int) $data['assessment_type_id'],
|
||||
':student_id' => (int) $entry['student_id'],
|
||||
':teacher_id' => !empty($data['teacher_id']) ? (int) $data['teacher_id'] : null,
|
||||
':score' => $entry['score'],
|
||||
':max_score' => (float) ($data['assessment_max_score'] ?? 0),
|
||||
':status' => $entry['status'],
|
||||
':notes' => $entry['notes'] !== '' ? $entry['notes'] : null,
|
||||
':assessed_on' => $data['assessed_on'],
|
||||
]);
|
||||
|
||||
$saved++;
|
||||
}
|
||||
|
||||
return $saved;
|
||||
}
|
||||
|
||||
function list_assessment_scores_by_assessment(int $centerApplicationId, int $cycleId, int $assessmentTypeId): array
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT scores.*, teachers.full_name AS teacher_name
|
||||
FROM school_assessment_scores scores
|
||||
LEFT JOIN school_teachers teachers ON teachers.id = scores.teacher_id
|
||||
WHERE scores.center_application_id = :center_application_id
|
||||
AND scores.cycle_id = :cycle_id
|
||||
AND scores.assessment_type_id = :assessment_type_id
|
||||
ORDER BY scores.assessed_on DESC, scores.updated_at DESC, scores.id DESC'
|
||||
);
|
||||
$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 = [];
|
||||
foreach (list_assessment_scores_by_assessment($centerApplicationId, $cycleId, $assessmentTypeId) as $row) {
|
||||
$studentId = (int) ($row['student_id'] ?? 0);
|
||||
if ($studentId <= 0 || array_key_exists($studentId, $map)) {
|
||||
continue;
|
||||
}
|
||||
$map[$studentId] = $row;
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
function school_assessment_score_metrics_by_cycle(int $centerApplicationId, int $cycleId, int $assessmentTypeId = 0): array
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$query = "SELECT
|
||||
COUNT(*) AS total,
|
||||
COALESCE(SUM(status = 'present'), 0) AS present_count,
|
||||
COALESCE(SUM(status = 'absent'), 0) AS absent_count,
|
||||
COALESCE(SUM(status = 'excused'), 0) AS excused_count,
|
||||
COALESCE(AVG(CASE WHEN status = 'present' THEN score END), 0) AS average_score,
|
||||
MAX(assessed_on) AS latest_date
|
||||
FROM school_assessment_scores
|
||||
WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id";
|
||||
$params = [
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':cycle_id' => $cycleId,
|
||||
];
|
||||
|
||||
if ($assessmentTypeId > 0) {
|
||||
$query .= ' AND assessment_type_id = :assessment_type_id';
|
||||
$params[':assessment_type_id'] = $assessmentTypeId;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare($query);
|
||||
$stmt->execute($params);
|
||||
$row = $stmt->fetch() ?: [];
|
||||
|
||||
return [
|
||||
'total' => (int) ($row['total'] ?? 0),
|
||||
'present' => (int) ($row['present_count'] ?? 0),
|
||||
'absent' => (int) ($row['absent_count'] ?? 0),
|
||||
'excused' => (int) ($row['excused_count'] ?? 0),
|
||||
'average_score' => (float) ($row['average_score'] ?? 0),
|
||||
'latest_date' => (string) ($row['latest_date'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
function school_student_options_by_cycle(int $centerApplicationId, int $cycleId): array
|
||||
{
|
||||
$students = list_school_students_by_cycle($centerApplicationId, $cycleId);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user