Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f04e1c8d5 | ||
|
|
faff5cf4a0 | ||
|
|
7937c54a84 | ||
|
|
b80640c59c | ||
|
|
7fc46facca | ||
|
|
11de03c65b | ||
|
|
5849af849c | ||
|
|
a83ac160ed | ||
|
|
6d8ede20a0 | ||
|
|
61e8c6788b | ||
|
|
a55b4a5394 | ||
|
|
371b07bffa | ||
|
|
e5366bdd58 | ||
|
|
d70f2ad2ca | ||
|
|
b7b7995909 | ||
|
|
f43e1b9751 | ||
|
|
c9ac266bd6 | ||
|
|
c7366417c4 | ||
|
|
18026059c0 |
15
add_logo_to_hero.py
Normal file
15
add_logo_to_hero.py
Normal file
@ -0,0 +1,15 @@
|
||||
with open('approved_school.php', 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
replacement = """
|
||||
<div class="d-flex align-items-center gap-3 mb-3">
|
||||
<?php if ($application['logo']): ?>
|
||||
<img src="<?= e((string)$application['logo']) ?>" alt="Logo" style="width: 64px; height: 64px; border-radius: 12px; object-fit: cover; box-shadow: 0 4px 12px rgba(0,0,0,0.1);">
|
||||
<?php endif; ?>
|
||||
<h1 class="page-title mb-0"><?= e((string) $application['center_name']) ?></h1>
|
||||
</div>"""
|
||||
|
||||
content = content.replace('<h1 class="page-title mb-3"><?= e((string) $application["center_name"]) ?></h1>', replacement)
|
||||
|
||||
with open('approved_school.php', 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
211
admin.php
Normal file
211
admin.php
Normal file
@ -0,0 +1,211 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
$flash = consume_flash();
|
||||
$stats = dashboard_metrics();
|
||||
$recentApplications = latest_applications(6);
|
||||
$approvedCenters = array_slice(list_applications('approved'), 0, 6);
|
||||
$reviewBacklog = $stats['submitted'] + $stats['under_review'];
|
||||
$approvalRate = $stats['all'] > 0 ? (int) round(($stats['approved'] / $stats['all']) * 100) : 0;
|
||||
$recentApproved = $approvedCenters[0] ?? null;
|
||||
|
||||
render_page_start('لوحة الإدارة', 'admin', 'لوحة إدارة مركزية تجمع صفحات المتابعة التشغيلية، الطلبات، والمراكز المعتمدة في نقطة دخول واحدة للمشرف العام.');
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="py-4 py-lg-5">
|
||||
<div class="container-xxl">
|
||||
|
||||
<div class="admin-layout row g-4 align-items-start">
|
||||
<div class="col-lg-3 layout-sidebar-column">
|
||||
<?php require __DIR__ . '/includes/sidebar.php'; ?>
|
||||
</div>
|
||||
<div class="col-lg-9 layout-content-column">
|
||||
|
||||
<div class="page-banner mb-4 mb-lg-5">
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-8">
|
||||
<span class="eyebrow mb-3">بوابة الإدارة المركزية</span>
|
||||
<h1 class="page-title mb-3">لوحة الإدارة الرئيسية</h1>
|
||||
<p class="page-copy mb-3">هذه الصفحة أصبحت نقطة الدخول الواحدة لكل الصفحات الإدارية: مؤشرات عامة، مراجعة الطلبات، الوصول إلى المراكز المعتمدة، ثم الانتقال إلى الطلاب والمعلمين والتقييمات والغياب حسب المركز والدورة.</p>
|
||||
<div class="hero-meta">
|
||||
<span>إجمالي الطلبات <?= e((string) $stats['all']) ?></span>
|
||||
<span>العمل المفتوح الآن <?= e((string) $reviewBacklog) ?></span>
|
||||
<span>نسبة الاعتماد <?= e((string) $approvalRate) ?>٪</span>
|
||||
</div>
|
||||
<div class="cta-stack mt-4">
|
||||
<a class="btn btn-primary" href="dashboard.php">لوحة القيادة</a>
|
||||
<a class="btn btn-outline-secondary" href="applications.php">لوحة الطلبات</a>
|
||||
<a class="btn btn-outline-secondary" href="applications.php?status=approved">المراكز المعتمدة</a>
|
||||
<a class="btn btn-outline-secondary" href="modules.php">خريطة الصفحات</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="page-banner-panel h-100">
|
||||
<div class="mini-stat-label">التركيز الإداري الحالي</div>
|
||||
<div class="mini-stat-value"><?= e((string) $reviewBacklog) ?></div>
|
||||
<div class="mini-stat-copy mb-3">طلبات تحتاج متابعة مباشرة الآن بين الاستلام والمراجعة، مع <?= e((string) $stats['approved']) ?> مركزاً معتمداً جاهزاً للتشغيل.</div>
|
||||
<?php if ($recentApproved): ?>
|
||||
<a class="btn btn-primary btn-sm" href="approved_school.php?id=<?= e((string) $recentApproved['id']) ?>">فتح أحدث مركز معتمد</a>
|
||||
<?php else: ?>
|
||||
<a class="btn btn-primary btn-sm" href="applications.php">بدء مراجعة الطلبات</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6 col-xl-3"><div class="app-card stat-tile"><div class="mini-stat-label">إجمالي الطلبات</div><div class="mini-stat-value"><?= e((string) $stats['all']) ?></div><div class="mini-stat-copy">كل طلبات فتح المراكز المسجلة.</div></div></div>
|
||||
<div class="col-md-6 col-xl-3"><div class="app-card stat-tile"><div class="mini-stat-label">بانتظار الاستلام</div><div class="mini-stat-value"><?= e((string) $stats['submitted']) ?></div><div class="mini-stat-copy">طلبات جديدة تحتاج بدء المعالجة.</div></div></div>
|
||||
<div class="col-md-6 col-xl-3"><div class="app-card stat-tile"><div class="mini-stat-label">تحت المراجعة</div><div class="mini-stat-value"><?= e((string) $stats['under_review']) ?></div><div class="mini-stat-copy">ملفات نشطة داخل مسار القرار.</div></div></div>
|
||||
<div class="col-md-6 col-xl-3"><div class="app-card stat-tile"><div class="mini-stat-label">معتمد</div><div class="mini-stat-value"><?= e((string) $stats['approved']) ?></div><div class="mini-stat-copy">مراكز جاهزة للإدارة التشغيلية.</div></div></div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-4 align-items-start">
|
||||
<div class="col-lg-7">
|
||||
<div class="app-card h-100">
|
||||
<div class="section-head mb-3">
|
||||
<div>
|
||||
<div class="section-title">المسارات الإدارية الأساسية</div>
|
||||
<div class="section-copy">كل صفحة إدارية أصبحت مرتبطة بهذه اللوحة حتى يعود المشرف العام دائماً إلى نقطة تحكم واضحة.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="module-list">
|
||||
<article class="module-item">
|
||||
<h2>لوحة القيادة التشغيلية</h2>
|
||||
<p>للمؤشرات اليومية، آخر الطلبات، والحمل التشغيلي الحالي على مستوى الولاية.</p>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="dashboard.php">فتح لوحة القيادة</a>
|
||||
</article>
|
||||
<article class="module-item">
|
||||
<h2>لوحة الطلبات</h2>
|
||||
<p>لفرز الطلبات حسب الحالة وفتح ملف كل مركز واتخاذ القرار الإداري.</p>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="applications.php">فتح لوحة الطلبات</a>
|
||||
</article>
|
||||
<article class="module-item">
|
||||
<h2>المراكز المعتمدة</h2>
|
||||
<p>للوصول إلى المراكز الجاهزة للتشغيل ثم الانتقال إلى الطلاب والمعلمين والتقييمات والحضور لكل مركز.</p>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="applications.php?status=approved">فتح المراكز المعتمدة</a>
|
||||
</article>
|
||||
<article class="module-item">
|
||||
<h2>تقييم المراكز</h2>
|
||||
<p>إعداد تقييمات إشرافية للمراكز المعتمدة داخل كل دورة موسمية، تمهيداً لإضافة البنود والرصد بنفس نمط الطلاب.</p>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="center_assessments.php">فتح تقييم المراكز</a>
|
||||
</article>
|
||||
<article class="module-item">
|
||||
<h2>قوالب تقييم المراكز</h2>
|
||||
<p>إنشاء القوالب العامة وبنودها مركزياً حتى يستخدمها المشرف العام أو المقيمون المكلّفون عند تقييم المراكز واحداً تلو الآخر.</p>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="global_center_assessments.php">فتح قوالب التقييم</a>
|
||||
</article>
|
||||
<article class="module-item">
|
||||
<h2>هيكل النظام</h2>
|
||||
<p>مرجع سريع لفهم تنظيم الصفحات الحالية ومسار التطوير الإداري داخل التطبيق.</p>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="modules.php">عرض خريطة الصفحات</a>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<div class="app-card h-100 sidebar-card">
|
||||
<div class="section-title mb-3">كيف تستخدم لوحة الإدارة؟</div>
|
||||
<div class="process-list mb-4">
|
||||
<div class="process-item active"><span>1</span><div><strong>ابدأ من هنا</strong><small>راجع المؤشرات العامة وافتح المسار الإداري المناسب من نفس الشاشة.</small></div></div>
|
||||
<div class="process-item"><span>2</span><div><strong>افتح مركزاً معتمداً</strong><small>من قائمة المراكز المعتمدة اختر المركز الذي تريد تشغيله أو مراجعته.</small></div></div>
|
||||
<div class="process-item"><span>3</span><div><strong>أدر الوحدات التشغيلية</strong><small>من داخل المركز انتقل إلى الطلاب أو الفريق أو التقييمات أو الغياب ثم ارجع إلى لوحة الإدارة عند الحاجة.</small></div></div>
|
||||
</div>
|
||||
<div class="section-title mb-2">الوصول السريع</div>
|
||||
<div class="quick-link-stack">
|
||||
<a class="quick-link-item" href="applications.php?status=submitted"><div><strong>طلبات جديدة</strong><span>ابدأ مباشرة بالطلبات التي لم تُراجع بعد.</span></div></a>
|
||||
<a class="quick-link-item" href="applications.php?status=under_review"><div><strong>طلبات تحت المراجعة</strong><span>تابع الملفات المفتوحة حالياً حتى قرار نهائي.</span></div></a>
|
||||
<a class="quick-link-item" href="center_assessments.php"><div><strong>تقييم المراكز</strong><span>ابدأ بإعداد تقييمات إشرافية للمراكز المعتمدة حسب الدورة.</span></div></a>
|
||||
<a class="quick-link-item" href="global_center_assessments.php"><div><strong>قوالب تقييم المراكز</strong><span>أنشئ التقييم العام وبنوده قبل بدء تقييم المراكز ميدانياً.</span></div></a>
|
||||
<a class="quick-link-item" href="center_application.php"><div><strong>فتح طلب جديد</strong><span>اختبار أو إنشاء طلب جديد من نموذج التقديم.</span></div></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-7">
|
||||
<div class="app-card h-100">
|
||||
<div class="section-head mb-3">
|
||||
<div>
|
||||
<div class="section-title">آخر الطلبات الواردة</div>
|
||||
<div class="section-copy">نظرة سريعة من نفس لوحة الإدارة دون الحاجة لفتح لوحة الطلبات أولاً.</div>
|
||||
</div>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="applications.php">فتح القائمة الكاملة</a>
|
||||
</div>
|
||||
<?php if ($recentApplications === []): ?>
|
||||
<div class="empty-state text-center py-5">
|
||||
<div class="empty-title mb-2">لا توجد طلبات بعد</div>
|
||||
<p class="text-muted mb-3">يمكنك إنشاء أول طلب أو العودة لاحقاً عند وصول طلبات جديدة.</p>
|
||||
<a class="btn btn-primary" href="center_application.php">إنشاء طلب جديد</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table app-table align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>المرجع</th>
|
||||
<th>المركز</th>
|
||||
<th>المدينة</th>
|
||||
<th>الحالة</th>
|
||||
<th>الإجراء</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($recentApplications as $application): ?>
|
||||
<tr>
|
||||
<td><a class="table-link" href="application_detail.php?id=<?= e((string) $application['id']) ?>">#<?= e((string) $application['id']) ?></a></td>
|
||||
<td>
|
||||
<div class="fw-semibold"><?= e((string) $application['center_name']) ?></div>
|
||||
<div class="text-muted small"><?= e((string) $application['director_name']) ?></div>
|
||||
</td>
|
||||
<td><?= e((string) $application['city']) ?></td>
|
||||
<td><?= status_badge((string) $application['status']) ?></td>
|
||||
<td>
|
||||
<?php if ((string) $application['status'] === 'approved'): ?>
|
||||
<a class="btn btn-primary btn-sm px-3" href="approved_school.php?id=<?= e((string) $application['id']) ?>">صفحة المركز</a>
|
||||
<?php else: ?>
|
||||
<a class="btn btn-outline-secondary btn-sm px-3" href="application_detail.php?id=<?= e((string) $application['id']) ?>">فتح الملف</a>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<div class="app-card h-100 sidebar-card">
|
||||
<div class="section-title mb-3">المراكز المعتمدة الجاهزة للإدارة</div>
|
||||
<?php if ($approvedCenters === []): ?>
|
||||
<div class="empty-state 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="applications.php">العودة إلى الطلبات</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="quick-link-stack">
|
||||
<?php foreach ($approvedCenters as $center): ?>
|
||||
<a class="quick-link-item" href="approved_school.php?id=<?= e((string) $center['id']) ?>">
|
||||
<div>
|
||||
<strong><?= e((string) $center['center_name']) ?></strong>
|
||||
<span><?= e((string) $center['city']) ?> — <?= e((string) $center['director_name']) ?> — سعة <?= e((string) $center['expected_students']) ?> طالب</span>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
<?php render_page_end(); ?>
|
||||
182
app_settings.php
Normal file
182
app_settings.php
Normal file
@ -0,0 +1,182 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
$flash = consume_flash();
|
||||
$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'] ?? '',
|
||||
'completion_certificate_template' => $settings['completion_certificate_template'] ?? 'modern',
|
||||
'completion_certificate_tagline' => $settings['completion_certificate_tagline'] ?? 'شهادة إتمام وتكريم',
|
||||
'completion_certificate_message' => $settings['completion_certificate_message'] ?? '',
|
||||
];
|
||||
|
||||
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);
|
||||
$values['completion_certificate_template'] = clean_text($_POST['completion_certificate_template'] ?? 'modern', 40);
|
||||
$values['completion_certificate_tagline'] = clean_text($_POST['completion_certificate_tagline'] ?? '', 255);
|
||||
$values['completion_certificate_message'] = clean_text($_POST['completion_certificate_message'] ?? '', 1200);
|
||||
|
||||
if ($values['app_name'] === '') $errors['app_name'] = 'مطلوب';
|
||||
if (!in_array($values['completion_certificate_template'], ['modern', 'classic'], true)) {
|
||||
$values['completion_certificate_template'] = 'modern';
|
||||
}
|
||||
if ($values['completion_certificate_tagline'] === '') {
|
||||
$values['completion_certificate_tagline'] = 'شهادة إتمام وتكريم';
|
||||
}
|
||||
|
||||
$logoPath = $settings['app_logo'] ?? '';
|
||||
$faviconPath = $settings['app_favicon'] ?? '';
|
||||
|
||||
// Handle Uploads
|
||||
$uploadDir = __DIR__ . '/assets/images/uploads/';
|
||||
if (!is_dir($uploadDir)) {
|
||||
mkdir($uploadDir, 0755, true);
|
||||
}
|
||||
|
||||
if (isset($_FILES['logo']) && $_FILES['logo']['error'] === UPLOAD_ERR_OK) {
|
||||
$logoExt = strtolower(pathinfo($_FILES['logo']['name'], PATHINFO_EXTENSION));
|
||||
if (in_array($logoExt, ['png', 'jpg', 'jpeg', 'svg', 'gif'])) {
|
||||
$logoName = 'app_logo_' . time() . '.' . $logoExt;
|
||||
if (move_uploaded_file($_FILES['logo']['tmp_name'], $uploadDir . $logoName)) {
|
||||
$logoPath = 'assets/images/uploads/' . $logoName;
|
||||
}
|
||||
} else {
|
||||
$errors['logo'] = 'صيغة غير مدعومة';
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_FILES['favicon']) && $_FILES['favicon']['error'] === UPLOAD_ERR_OK) {
|
||||
$faviconExt = strtolower(pathinfo($_FILES['favicon']['name'], PATHINFO_EXTENSION));
|
||||
if (in_array($faviconExt, ['png', 'ico', 'svg'])) {
|
||||
$faviconName = 'app_favicon_' . time() . '.' . $faviconExt;
|
||||
if (move_uploaded_file($_FILES['favicon']['tmp_name'], $uploadDir . $faviconName)) {
|
||||
$faviconPath = 'assets/images/uploads/' . $faviconName;
|
||||
}
|
||||
} else {
|
||||
$errors['favicon'] = 'صيغة غير مدعومة';
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($errors)) {
|
||||
try {
|
||||
$stmt = db()->prepare('UPDATE app_settings SET app_name = ?, app_slogan = ?, app_email = ?, app_telephone = ?, app_logo = ?, app_favicon = ?, completion_certificate_template = ?, completion_certificate_tagline = ?, completion_certificate_message = ?, updated_at = NOW() WHERE id = 1');
|
||||
$stmt->execute([
|
||||
$values['app_name'],
|
||||
$values['app_slogan'],
|
||||
$values['app_email'],
|
||||
$values['app_telephone'],
|
||||
$logoPath,
|
||||
$faviconPath,
|
||||
$values['completion_certificate_template'],
|
||||
$values['completion_certificate_tagline'],
|
||||
$values['completion_certificate_message']
|
||||
]);
|
||||
set_flash('success', 'تم تحديث الإعدادات العامة بنجاح.');
|
||||
header('Location: app_settings.php');
|
||||
exit;
|
||||
} catch (Throwable $e) {
|
||||
$errors['form'] = 'تعذر الحفظ.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render_page_start('إعدادات النظام', 'app_settings', 'إعدادات النظام العامة');
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="py-4 py-lg-5">
|
||||
<div class="container-xxl">
|
||||
<div class="admin-layout row g-4 align-items-start">
|
||||
<div class="col-lg-3 layout-sidebar-column">
|
||||
<?php require __DIR__ . '/includes/sidebar.php'; ?>
|
||||
</div>
|
||||
<div class="col-lg-9 layout-content-column">
|
||||
<div class="page-banner mb-4">
|
||||
<h1 class="page-title mb-2">الإعدادات العامة للنظام</h1>
|
||||
<p class="page-copy mb-0">تعديل اسم النظام، الشعار (Logo)، الأيقونة (Favicon)، وبيانات التواصل.</p>
|
||||
</div>
|
||||
|
||||
<div class="app-card form-card">
|
||||
<?php if (!empty($errors['form'])): ?>
|
||||
<div class="alert alert-danger mb-4"><?= e($errors['form']) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" enctype="multipart/form-data" novalidate>
|
||||
<div class="row g-4">
|
||||
<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']) ?>">
|
||||
<?php if (isset($errors['app_email'])): ?><div class="invalid-feedback"><?= e($errors['app_email']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">رقم الهاتف العام</label>
|
||||
<input class="form-control <?= isset($errors['app_telephone']) ? 'is-invalid' : '' ?>" name="app_telephone" value="<?= e($values['app_telephone']) ?>">
|
||||
<?php if (isset($errors['app_telephone'])): ?><div class="invalid-feedback"><?= e($errors['app_telephone']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">الشعار (Logo)</label>
|
||||
<?php if (!empty($settings['app_logo'])): ?>
|
||||
<div class="mb-2"><img src="<?= e((string)$settings['app_logo']) ?>" alt="Logo" style="max-height: 80px; max-width: 100%; border-radius: 8px;"></div>
|
||||
<?php endif; ?>
|
||||
<input type="file" class="form-control <?= isset($errors['logo']) ? 'is-invalid' : '' ?>" name="logo" accept="image/*">
|
||||
<?php if (isset($errors['logo'])): ?><div class="invalid-feedback"><?= e($errors['logo']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">الأيقونة (Favicon)</label>
|
||||
<?php if (!empty($settings['app_favicon'])): ?>
|
||||
<div class="mb-2"><img src="<?= e((string)$settings['app_favicon']) ?>" alt="Favicon" style="max-height: 40px; max-width: 100%; border-radius: 4px;"></div>
|
||||
<?php endif; ?>
|
||||
<input type="file" class="form-control <?= isset($errors['favicon']) ? 'is-invalid' : '' ?>" name="favicon" accept=".ico,.png,.svg">
|
||||
<?php if (isset($errors['favicon'])): ?><div class="invalid-feedback"><?= e($errors['favicon']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="col-12"><hr class="my-1"></div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">قالب شهادة الإتمام</label>
|
||||
<select class="form-select" name="completion_certificate_template">
|
||||
<option value="modern" <?= $values['completion_certificate_template'] === 'modern' ? 'selected' : '' ?>>Modern / عصري</option>
|
||||
<option value="classic" <?= $values['completion_certificate_template'] === 'classic' ? 'selected' : '' ?>>Classic / رسمي</option>
|
||||
</select>
|
||||
<div class="form-text">يغيّر شكل شهادة الإتمام والتكريم فقط، بدون التأثير على كشف الدرجات.</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">عنوان الشهادة</label>
|
||||
<input class="form-control" name="completion_certificate_tagline" value="<?= e($values['completion_certificate_tagline']) ?>">
|
||||
<div class="form-text">مثال: شهادة إتمام وتكريم أو شهادة تفوق.</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">نص شهادة الإتمام</label>
|
||||
<textarea class="form-control" name="completion_certificate_message" rows="4"><?= e($values['completion_certificate_message']) ?></textarea>
|
||||
<div class="form-text">يمكنك استخدام المتغيرات: {student} {center} {cycle} {performance} {honor} {percentage}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions mt-4">
|
||||
<button class="btn btn-primary px-4" type="submit">حفظ التغييرات</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php render_page_end(); ?>
|
||||
274
application_detail.php
Normal file
274
application_detail.php
Normal file
@ -0,0 +1,274 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
$flash = consume_flash();
|
||||
$applicationId = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT) ?: 0;
|
||||
$application = $applicationId > 0 ? get_application($applicationId) : null;
|
||||
|
||||
if (!$application) {
|
||||
http_response_code(404);
|
||||
render_page_start('طلب غير موجود', 'applications', 'لم يتم العثور على طلب فتح المركز المطلوب.');
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="py-5">
|
||||
<div class="container-xxl">
|
||||
<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">العودة إلى القائمة</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php
|
||||
render_page_end();
|
||||
exit;
|
||||
}
|
||||
|
||||
$reviewErrors = [];
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
|
||||
if (isset($_POST['action']) && $_POST['action'] === 'update_subjects') {
|
||||
$selected_subjects = isset($_POST['subjects']) && is_array($_POST['subjects']) ? $_POST['subjects'] : [];
|
||||
$valid_subjects = [];
|
||||
foreach ($selected_subjects as $sid) {
|
||||
$sid = filter_var($sid, FILTER_VALIDATE_INT);
|
||||
if ($sid) $valid_subjects[] = $sid;
|
||||
}
|
||||
update_application_subjects($applicationId, $valid_subjects);
|
||||
set_flash('success', 'تم تحديث المواد الدراسية بنجاح.');
|
||||
header('Location: application_detail.php?id=' . urlencode((string) $applicationId));
|
||||
exit;
|
||||
}
|
||||
$status = clean_text((string) ($_POST['status'] ?? 'submitted'), 30);
|
||||
$adminNotes = clean_text((string) ($_POST['admin_notes'] ?? ''), 1000);
|
||||
$evaluationScore = filter_var($_POST['evaluation_score'] ?? null, FILTER_VALIDATE_INT, [
|
||||
'options' => ['min_range' => 0, 'max_range' => 100],
|
||||
]);
|
||||
|
||||
if (($_POST['evaluation_score'] ?? '') !== '' && $evaluationScore === false) {
|
||||
$reviewErrors['evaluation_score'] = 'أدخل درجة بين 0 و100.';
|
||||
}
|
||||
|
||||
if ($reviewErrors === []) {
|
||||
try {
|
||||
update_application_review($applicationId, $status, $adminNotes, $evaluationScore === false ? null : $evaluationScore);
|
||||
if ($status === 'approved') {
|
||||
set_flash('success', 'تم اعتماد المركز بنجاح، وهذه هي صفحة الهبوط الخاصة به بعد الموافقة.');
|
||||
header('Location: approved_school.php?id=' . urlencode((string) $applicationId));
|
||||
exit;
|
||||
}
|
||||
set_flash('success', 'تم تحديث حالة الطلب وملاحظات التقييم بنجاح.');
|
||||
header('Location: application_detail.php?id=' . urlencode((string) $applicationId));
|
||||
exit;
|
||||
} catch (Throwable $exception) {
|
||||
$reviewErrors['form'] = 'تعذر حفظ التحديث الآن. حاول مرة أخرى.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$application = get_application($applicationId);
|
||||
$statusMeta = status_meta((string) $application['status']);
|
||||
$scoreValue = $application['evaluation_score'] !== null ? max(0, min(100, (int) $application['evaluation_score'])) : null;
|
||||
|
||||
render_page_start('تفاصيل الطلب #' . $applicationId, 'applications', 'عرض تفصيلي لطلب فتح المركز مع نموذج المراجعة للمشرف العام.');
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="py-4 py-lg-5">
|
||||
<div class="container-xxl">
|
||||
<div class="admin-layout row g-4 align-items-start">
|
||||
<div class="col-lg-3 layout-sidebar-column">
|
||||
<?php require __DIR__ . '/includes/sidebar.php'; ?>
|
||||
</div>
|
||||
<div class="col-lg-9 layout-content-column">
|
||||
|
||||
<div class="page-banner mb-4 mb-lg-5">
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-8">
|
||||
<span class="eyebrow mb-3">ملف مستقل للمراجعة</span>
|
||||
<h1 class="page-title mb-2"><?= e((string) $application['center_name']) ?></h1>
|
||||
<p class="page-copy mb-3">مرجع الطلب #<?= e((string) $application['id']) ?> — مقدم الطلب <?= e((string) $application['director_name']) ?> من <?= e((string) $application['city']) ?>.</p>
|
||||
<div class="hero-meta">
|
||||
<span>تاريخ الإرسال: <?= e((string) $application['submitted_at']) ?></span>
|
||||
<span>آخر تحديث: <?= e((string) $application['updated_at']) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="page-banner-panel h-100 status-panel">
|
||||
<div class="mini-stat-label">الحالة الحالية</div>
|
||||
<div class="mb-3"><?= status_badge((string) $application['status']) ?></div>
|
||||
<div class="mini-stat-copy">هذا الملف مخصص لاتخاذ القرار وتوثيق الملاحظات بدلاً من خلطه مع قائمة الطلبات.</div>
|
||||
<a class="btn btn-outline-secondary btn-sm mt-3" href="admin.php">العودة إلى لوحة الإدارة</a>
|
||||
<?php if ((string) $application['status'] === 'approved'): ?>
|
||||
<a class="btn btn-primary btn-sm mt-3" href="approved_school.php?id=<?= e((string) $application['id']) ?>">فتح صفحة المركز المعتمد</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-8">
|
||||
<div class="app-card detail-card mb-4">
|
||||
<div class="section-head mb-4">
|
||||
<div>
|
||||
<div class="section-title">بيانات المركز</div>
|
||||
<div class="section-copy">عرض منظم لجميع البيانات المرسلة من صاحب الطلب في بطاقة واحدة.</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a class="btn btn-outline-secondary btn-sm px-3" href="admin.php">لوحة الإدارة</a>
|
||||
<a class="btn btn-outline-secondary btn-sm px-3" href="applications.php">العودة إلى القائمة</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 detail-grid">
|
||||
<div class="col-md-6"><div class="detail-item"><span>نوع المركز</span><strong><?= e((string) $application['center_type']) ?></strong></div></div>
|
||||
<div class="col-md-6"><div class="detail-item"><span>الفئة المستهدفة</span><strong><?= e((string) $application['gender_scope']) ?></strong></div></div>
|
||||
<div class="col-md-6"><div class="detail-item"><span>رقم الهاتف</span><strong><?= e((string) $application['phone']) ?></strong></div></div>
|
||||
<div class="col-md-6"><div class="detail-item"><span>البريد الإلكتروني</span><strong><?= e((string) $application['email']) ?></strong></div></div>
|
||||
<div class="col-md-6"><div class="detail-item"><span>السعة المتوقعة</span><strong><?= e((string) $application['expected_students']) ?> طالب</strong></div></div>
|
||||
<div class="col-md-6"><div class="detail-item"><span>الدورة / الفترة</span><strong><?= e((string) ($application['cycle_name'] ?? 'دورة مخصصة')) ?> (<?= e((string) $application['start_date']) ?> — <?= e((string) $application['end_date']) ?>)</strong></div></div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="notes-block mt-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="section-title small-title mb-0">المواد الدراسية المطلوبة</div>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#editSubjectsModal">تعديل المواد</button>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<?php
|
||||
$selected_subjects = json_decode((string) ($application['subjects'] ?? '[]'), true) ?: [];
|
||||
$all_subjects = get_enabled_subjects();
|
||||
$subject_map = [];
|
||||
foreach ($all_subjects as $sub) {
|
||||
$subject_map[$sub['id']] = $sub['name'];
|
||||
}
|
||||
if (empty($selected_subjects)):
|
||||
?>
|
||||
<span class="text-muted small">لم يتم اختيار أي مواد.</span>
|
||||
<?php else: ?>
|
||||
<?php foreach ($selected_subjects as $sub_id): ?>
|
||||
<span class="badge bg-secondary"><?= e($subject_map[$sub_id] ?? 'مادة غير معروفة') ?></span>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="notes-block mt-4">
|
||||
<div class="section-title small-title mb-2">ملخص البرنامج والاحتياجات</div>
|
||||
<p class="mb-0 text-muted"><?= nl2br(e((string) ($application['notes'] ?: 'لا توجد ملاحظات إضافية.'))) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-card detail-card">
|
||||
<div class="section-title mb-3">سجل المراجعة</div>
|
||||
<div class="timeline-list">
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-title">تم استلام الطلب</div>
|
||||
<div class="timeline-copy text-muted">أُرسل الطلب بتاريخ <?= e((string) $application['submitted_at']) ?>.</div>
|
||||
</div>
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-title">الحالة الحالية: <?= e($statusMeta['label']) ?></div>
|
||||
<div class="timeline-copy text-muted">آخر تحديث موثق بتاريخ <?= e((string) $application['updated_at']) ?>.</div>
|
||||
</div>
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-title">ملاحظات المشرف العام</div>
|
||||
<div class="timeline-copy text-muted"><?= nl2br(e((string) ($application['admin_notes'] ?: 'لم تُسجل ملاحظات بعد.'))) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="app-card sidebar-card mb-4">
|
||||
<div class="section-title mb-3">ملخص القرار</div>
|
||||
<?php if ($scoreValue !== null): ?>
|
||||
<div class="score-bar mb-3" aria-label="درجة التقييم الأولي">
|
||||
<span style="width: <?= e((string) $scoreValue) ?>%"></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="summary-stack">
|
||||
<div class="summary-row"><span>الحالة</span><strong><?= e($statusMeta['label']) ?></strong></div>
|
||||
<div class="summary-row"><span>درجة الاستعداد</span><strong><?= $scoreValue !== null ? e((string) $scoreValue) . '%' : 'غير محددة' ?></strong></div>
|
||||
<div class="summary-row"><span>آخر تحديث</span><strong><?= e((string) $application['updated_at']) ?></strong></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-card sidebar-card">
|
||||
<div class="section-title mb-3">إجراء المشرف العام</div>
|
||||
<div class="section-copy mb-3">هذه البطاقة مخصصة فقط لتغيير الحالة وتسجيل قرار المراجعة على هذا الطلب.</div>
|
||||
<?php if (!empty($reviewErrors['form'])): ?>
|
||||
<div class="alert alert-danger"><?= e($reviewErrors['form']) ?></div>
|
||||
<?php endif; ?>
|
||||
<form method="post" novalidate>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="status">حالة الطلب</label>
|
||||
<select class="form-select" id="status" name="status">
|
||||
<?php foreach (status_map() as $value => $meta): ?>
|
||||
<option value="<?= e($value) ?>" <?= (string) $application['status'] === $value ? 'selected' : '' ?>><?= e($meta['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="evaluation_score">درجة التقييم الأولي</label>
|
||||
<input type="number" min="0" max="100" class="form-control <?= isset($reviewErrors['evaluation_score']) ? 'is-invalid' : '' ?>" id="evaluation_score" name="evaluation_score" value="<?= e((string) ($application['evaluation_score'] ?? '')) ?>" placeholder="مثال: 85">
|
||||
<?php if (isset($reviewErrors['evaluation_score'])): ?><div class="invalid-feedback"><?= e($reviewErrors['evaluation_score']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="admin_notes">ملاحظات التقييم</label>
|
||||
<textarea class="form-control" id="admin_notes" name="admin_notes" rows="6" placeholder="سجّل نقاط القوة أو النواقص المطلوب استكمالها."><?= e((string) ($application['admin_notes'] ?? '')) ?></textarea>
|
||||
</div>
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-primary" type="submit">حفظ التحديث</button>
|
||||
<?php if ((string) $application['status'] === 'approved'): ?>
|
||||
<a class="btn btn-outline-dark" href="approved_school.php?id=<?= e((string) $application['id']) ?>">معاينة صفحة المركز</a>
|
||||
<?php endif; ?>
|
||||
<a class="btn btn-outline-secondary" href="admin.php">لوحة الإدارة</a>
|
||||
<a class="btn btn-outline-secondary" href="applications.php">العودة إلى القائمة</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Modal for Editing Subjects -->
|
||||
<div class="modal fade" id="editSubjectsModal" tabindex="-1" aria-labelledby="editSubjectsModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="update_subjects">
|
||||
<div class="modal-header border-bottom-0">
|
||||
<h5 class="modal-title" id="editSubjectsModalLabel">تعديل المواد الدراسية</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="إغلاق"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row g-2">
|
||||
<?php foreach ($all_subjects as $sub): ?>
|
||||
<?php $isChecked = in_array($sub['id'], $selected_subjects); ?>
|
||||
<div class="col-md-6">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="subjects[]" value="<?= e((string)$sub['id']) ?>" id="subject_modal_<?= e((string)$sub['id']) ?>" <?= $isChecked ? 'checked' : '' ?>/>
|
||||
<label class="form-check-label" for="subject_modal_<?= e((string)$sub['id']) ?>">
|
||||
<?= e($sub['name']) ?>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer border-top-0">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" class="btn btn-primary">حفظ التغييرات</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php render_page_end(); ?>
|
||||
152
applications.php
Normal file
152
applications.php
Normal file
@ -0,0 +1,152 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
// Handle POST actions (e.g. Delete)
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
if ($action === 'delete') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id > 0) {
|
||||
$stmt = db()->prepare('DELETE FROM center_applications WHERE id = ?');
|
||||
$stmt->execute([$id]);
|
||||
set_flash('success', 'تم حذف الطلب بنجاح.');
|
||||
}
|
||||
header('Location: applications.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Read list
|
||||
$search = clean_text($_GET['search'] ?? '', 255);
|
||||
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT) ?: 1;
|
||||
$limit = 10;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$query = 'SELECT * FROM center_applications';
|
||||
$countQuery = 'SELECT COUNT(*) FROM center_applications';
|
||||
$params = [];
|
||||
if ($search !== '') {
|
||||
$where = ' WHERE center_name LIKE ? OR city LIKE ? OR director_name LIKE ?';
|
||||
$query .= $where;
|
||||
$countQuery .= $where;
|
||||
$params[] = "%$search%";
|
||||
$params[] = "%$search%";
|
||||
$params[] = "%$search%";
|
||||
}
|
||||
|
||||
$stmtCount = db()->prepare($countQuery);
|
||||
$stmtCount->execute($params);
|
||||
$totalItems = (int)$stmtCount->fetchColumn();
|
||||
|
||||
$query .= ' ORDER BY id DESC LIMIT ' . $limit . ' OFFSET ' . $offset;
|
||||
$stmt = db()->prepare($query);
|
||||
$stmt->execute($params);
|
||||
$applications = $stmt->fetchAll();
|
||||
|
||||
$flash = consume_flash();
|
||||
render_page_start('إدارة الطلبات', 'applications', 'قائمة بطلبات فتح المراكز');
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="py-4 py-lg-5">
|
||||
<div class="container-xxl">
|
||||
<div class="admin-layout row g-4 align-items-start">
|
||||
<div class="col-lg-3 layout-sidebar-column">
|
||||
<?php require __DIR__ . '/includes/sidebar.php'; ?>
|
||||
</div>
|
||||
<div class="col-lg-9 layout-content-column">
|
||||
<div class="app-card mb-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-3">
|
||||
<div class="section-title mb-0">إدارة طلبات فتح المراكز</div>
|
||||
<a class="btn btn-primary" href="center_application.php">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
|
||||
</svg>
|
||||
إضافة طلب
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Search Bar -->
|
||||
<?php render_search_bar($search, "ابحث باسم المركز، المدينة، أو المسؤول...", "applications.php", $_GET); ?>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table app-table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>المرجع</th>
|
||||
<th>المركز</th>
|
||||
<th>المدينة</th>
|
||||
<th>المسؤول</th>
|
||||
<th>الفترة</th>
|
||||
<th>الحالة</th>
|
||||
<th>الإجراءات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (count($applications) === 0): ?>
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-4 text-muted">لا توجد طلبات مسجلة أو لم يتم العثور على نتائج.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($applications as $application): ?>
|
||||
<tr>
|
||||
<td><a class="table-link" href="application_detail.php?id=<?= e((string) $application['id']) ?>">#<?= e((string) $application['id']) ?></a></td>
|
||||
<td>
|
||||
<div class="fw-semibold"><?= e((string) $application['center_name']) ?></div>
|
||||
<div class="text-muted small">سعة متوقعة: <?= e((string) $application['expected_students']) ?> طالب</div>
|
||||
</td>
|
||||
<td><?= e((string) $application['city']) ?></td>
|
||||
<td>
|
||||
<div><?= e((string) $application['director_name']) ?></div>
|
||||
<div class="text-muted small"><?= e((string) $application['phone']) ?></div>
|
||||
</td>
|
||||
<td>
|
||||
<div><?= e((string) $application['start_date']) ?></div>
|
||||
<div class="text-muted small">حتى <?= e((string) $application['end_date']) ?></div>
|
||||
</td>
|
||||
<td><?= status_badge((string) $application['status']) ?></td>
|
||||
<td>
|
||||
<div class="d-flex gap-2">
|
||||
<!-- View/Edit btn -->
|
||||
<?php if ((string) $application['status'] === 'approved'): ?>
|
||||
<a href="approved_school.php?id=<?= e((string) $application['id']) ?>" class="btn btn-sm btn-outline-secondary" title="صفحة المركز">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M10.5 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z"/>
|
||||
<path d="M0 8s3-5.5 8-5.5S16 8 16 8s-3 5.5-8 5.5S0 8 0 8zm8 3.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<a href="application_detail.php?id=<?= e((string) $application['id']) ?>" class="btn btn-sm btn-outline-secondary" title="تعديل">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Delete btn -->
|
||||
<form method="POST" action="applications.php" onsubmit="return confirm('هل أنت متأكد من حذف هذا الطلب؟');">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="id" value="<?= e((string)$application['id']) ?>">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="حذف">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
||||
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php render_pagination($totalItems, $limit, $page, $_GET); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php render_page_end(); ?>
|
||||
349
approved_school.php
Normal file
349
approved_school.php
Normal file
@ -0,0 +1,349 @@
|
||||
<?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;
|
||||
$application = $applicationId > 0 ? get_application($applicationId) : null;
|
||||
$cycleValues = school_cycle_defaults($application ?: null);
|
||||
$cycleErrors = [];
|
||||
$cycleRollover = school_cycle_rollover_defaults();
|
||||
|
||||
if (!$application) {
|
||||
http_response_code(404);
|
||||
render_page_start('صفحة المركز غير موجودة', 'approved', 'تعذر العثور على المركز المطلوب لعرض صفحة الهبوط بعد الاعتماد.');
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="py-5">
|
||||
<div class="container-xxl">
|
||||
<div class="admin-layout row g-4 align-items-start">
|
||||
<div class="col-lg-3 layout-sidebar-column">
|
||||
<?php require __DIR__ . '/includes/sidebar.php'; ?>
|
||||
</div>
|
||||
<div class="col-lg-9 layout-content-column">
|
||||
|
||||
<div class="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>
|
||||
</section>
|
||||
<?php
|
||||
render_page_end();
|
||||
exit;
|
||||
}
|
||||
|
||||
$isApproved = (string) $application['status'] === 'approved';
|
||||
$cycleContext = ['cycles' => [], 'selected' => null, 'active' => null, 'read_only' => false];
|
||||
$selectedCycle = null;
|
||||
$selectedCycleId = 0;
|
||||
$isCycleReadOnly = false;
|
||||
$cycleLabel = 'لا توجد دورة بعد';
|
||||
|
||||
if ($isApproved) {
|
||||
$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;
|
||||
$cycleRollover = school_cycle_rollover_defaults($selectedCycle);
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $isApproved) {
|
||||
$cycleAction = clean_text((string) ($_POST['cycle_action'] ?? ''), 30);
|
||||
if ($cycleAction === 'create_cycle') {
|
||||
[$cycleValues, $cycleErrors] = validate_school_cycle_input($_POST, $application);
|
||||
$cycleRollover = school_cycle_rollover_input((int) $application['id'], $_POST, $selectedCycle);
|
||||
$wantsRollover = !empty($cycleRollover['copy_teachers']) || !empty($cycleRollover['copy_assessments']) || !empty($cycleRollover['copy_students']);
|
||||
if ($wantsRollover && (int) $cycleRollover['source_cycle_id'] <= 0) {
|
||||
$cycleErrors['form'] = 'اختر دورة مصدر إذا كنت تريد نسخ الفريق أو التقييمات أو الطلاب إلى الدورة الجديدة.';
|
||||
}
|
||||
if ($cycleErrors === []) {
|
||||
try {
|
||||
$cycleCreation = create_school_cycle((int) $application['id'], $cycleValues, $cycleRollover);
|
||||
$newCycleId = (int) ($cycleCreation['cycle_id'] ?? 0);
|
||||
$rolloverSummary = (array) ($cycleCreation['rollover'] ?? []);
|
||||
$flashMessage = 'تم إنشاء الدورة الموسمية الجديدة بنجاح. يمكنك الآن العمل داخل ' . e((string)($cycleValues['cycle_name'] ?? 'الدورة الجديدة')) . '.';
|
||||
$rolloverParts = [];
|
||||
if (!empty($rolloverSummary['teachers'])) {
|
||||
$rolloverParts[] = 'ترحيل ' . (int) $rolloverSummary['teachers'] . ' من أعضاء الفريق';
|
||||
}
|
||||
if (!empty($rolloverSummary['assessments'])) {
|
||||
$rolloverParts[] = 'نسخ ' . (int) $rolloverSummary['assessments'] . ' من بنود التقييم';
|
||||
}
|
||||
if (!empty($rolloverSummary['students'])) {
|
||||
$rolloverParts[] = 'نقل ' . (int) $rolloverSummary['students'] . ' من الطلاب المستمرين';
|
||||
}
|
||||
if ($rolloverParts !== []) {
|
||||
$sourceCycleName = (string) ($cycleCreation['source_cycle_name'] ?? 'الدورة السابقة');
|
||||
$flashMessage .= ' تم أيضاً ' . implode('، ', $rolloverParts) . ' من ' . $sourceCycleName . '.';
|
||||
}
|
||||
set_flash('success', $flashMessage);
|
||||
header('Location: ' . school_page_url('approved_school.php', (int) $application['id'], $newCycleId));
|
||||
exit;
|
||||
} catch (PDOException $exception) {
|
||||
$duplicateCode = isset($exception->errorInfo[1]) && (int) $exception->errorInfo[1] === 1062;
|
||||
if ($duplicateCode) {
|
||||
$cycleErrors['year'] = 'هذه الدورة موجودة مسبقاً لهذا المركز. اختر موسماً أو سنة مختلفة.';
|
||||
} else {
|
||||
$cycleErrors['form'] = 'تعذر إنشاء الدورة حالياً. يرجى المحاولة مرة أخرى.';
|
||||
}
|
||||
} catch (InvalidArgumentException $exception) {
|
||||
$cycleErrors['form'] = 'تعذر ترحيل البيانات من الدورة المحددة. اختر دورة صحيحة ثم حاول مرة أخرى.';
|
||||
} catch (Throwable $exception) {
|
||||
$cycleErrors['form'] = 'تعذر إنشاء الدورة حالياً. يرجى المحاولة مرة أخرى.';
|
||||
}
|
||||
}
|
||||
} elseif ($cycleAction === 'archive_cycle') {
|
||||
$postedCycleId = (int) ($_POST['cycle_id'] ?? 0);
|
||||
$cycleToArchive = $postedCycleId > 0 ? get_school_cycle((int) $application['id'], $postedCycleId) : null;
|
||||
if (!$cycleToArchive) {
|
||||
$cycleErrors['form'] = 'تعذر العثور على الدورة المطلوب أرشفتها.';
|
||||
} elseif ((string) $cycleToArchive['status'] === 'archived') {
|
||||
$cycleErrors['form'] = 'هذه الدورة مؤرشفة بالفعل.';
|
||||
} else {
|
||||
archive_school_cycle((int) $application['id'], $postedCycleId);
|
||||
set_flash('success', 'تمت أرشفة الدورة ' . (string) $cycleToArchive['cycle_name'] . ' بنجاح. يمكنك فتح دورة جديدة متى شئت.');
|
||||
header('Location: ' . school_page_url('approved_school.php', (int) $application['id']));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$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;
|
||||
if ($cycleAction !== 'create_cycle') {
|
||||
$cycleRollover = school_cycle_rollover_defaults($selectedCycle);
|
||||
}
|
||||
}
|
||||
|
||||
$scoreValue = $application['evaluation_score'] !== null ? max(0, min(100, (int) $application['evaluation_score'])) : null;
|
||||
$durationDays = 0;
|
||||
$startTs = strtotime((string) $application['start_date']);
|
||||
$endTs = strtotime((string) $application['end_date']);
|
||||
if ($startTs !== false && $endTs !== false && $endTs >= $startTs) {
|
||||
$durationDays = (int) floor(($endTs - $startTs) / 86400) + 1;
|
||||
}
|
||||
|
||||
$studentsUrl = school_page_url('students.php', (int) $application['id'], $selectedCycleId);
|
||||
$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];
|
||||
$teacherCycleMetrics = $isApproved && $selectedCycleId > 0 ? school_teacher_metrics_by_cycle((int) $application['id'], $selectedCycleId) : ['total' => 0, 'active' => 0];
|
||||
$assessmentCycleMetrics = $isApproved && $selectedCycleId > 0 ? school_assessment_metrics_by_cycle((int) $application['id'], $selectedCycleId) : ['total' => 0, 'active' => 0, 'active_weight' => 0];
|
||||
$attendanceCycleMetrics = $isApproved && $selectedCycleId > 0 ? school_attendance_metrics_by_cycle((int) $application['id'], $selectedCycleId) : ['total' => 0, 'today_count' => 0, 'latest_date' => ''];
|
||||
|
||||
$pageTitle = $isApproved ? 'صفحة المركز المعتمد: ' . (string) $application['center_name'] . ($selectedCycle ? ' — ' . $cycleLabel : '') : 'صفحة المركز بعد الاعتماد';
|
||||
$pageDescription = $isApproved
|
||||
? 'صفحة تشغيلية للمركز المعتمد تعرض الجاهزية، الدورات الموسمية، والخطوات التالية بعد الموافقة.'
|
||||
: 'هذه الصفحة تصبح متاحة بعد اعتماد طلب المركز من المشرف العام.';
|
||||
|
||||
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 (!$isApproved): ?>
|
||||
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-4">
|
||||
<div>
|
||||
<span class="badge bg-warning text-primary mb-2">هذه الصفحة تُفتح بعد الاعتماد فقط</span>
|
||||
<h1 class="h3 mb-2"><?= e((string) $application['center_name']) ?></h1>
|
||||
<div class="text-muted small">
|
||||
الحالة الحالية: <?= e(status_meta((string) $application['status'])['label']) ?> • مرجع الطلب #<?= e((string) $application['id']) ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 mt-md-0 d-flex gap-2">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">في انتظار الاعتماد</div>
|
||||
<p class="text-muted mb-3">تم تجهيز صفحة الهبوط لهذا المركز، لكنها ستصبح الصفحة التشغيلية الرسمية فقط بعد تغيير الحالة إلى <strong>معتمد</strong>.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-4">
|
||||
<div>
|
||||
<span class="badge bg-success mb-2">مركز معتمد وجاهز للانطلاق</span>
|
||||
<h1 class="h3 mb-2"><?= e((string) $application['center_name']) ?></h1>
|
||||
<div class="text-muted small">
|
||||
<?= e((string) $application['city']) ?> • <?= e((string) $application['center_type']) ?> • <?= e((string) $application['gender_scope']) ?> • من <?= e((string) $application['start_date']) ?> إلى <?= e((string) $application['end_date']) ?>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-md-6 col-xl-3"><div class="app-card stat-tile p-3"><div class="mini-stat-label mb-1 text-muted small">المقاعد المتوقعة</div><div class="mini-stat-value fs-4 fw-bold"><?= e((string) $application['expected_students']) ?></div></div></div>
|
||||
<div class="col-md-6 col-xl-3"><div class="app-card stat-tile p-3"><div class="mini-stat-label mb-1 text-muted small">مدة البرنامج</div><div class="mini-stat-value fs-4 fw-bold"><?= e((string) ($durationDays > 0 ? $durationDays : '—')) ?> يوم</div></div></div>
|
||||
<div class="col-md-6 col-xl-3"><div class="app-card stat-tile p-3"><div class="mini-stat-label mb-1 text-muted small">مرجع التشغيل</div><div class="mini-stat-value fs-4 fw-bold">#<?= e((string) $application['id']) ?></div></div></div>
|
||||
<div class="col-md-6 col-xl-3"><div class="app-card stat-tile p-3"><div class="mini-stat-label mb-1 text-muted small">درجة التقييم</div><div class="mini-stat-value fs-4 fw-bold"><?= $scoreValue !== null ? e((string) $scoreValue) . '%' : '—' ?></div></div></div>
|
||||
</div>
|
||||
|
||||
<?php if ($selectedCycle): ?>
|
||||
<div class="row g-4 mb-4" id="cycles">
|
||||
<div class="<?= is_super_admin() ? 'col-lg-8' : 'col-lg-12' ?>">
|
||||
<div class="app-card h-100 p-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h5 class="mb-0">الدورة الموسمية الحالية</h5>
|
||||
<?= school_cycle_status_badge((string) $selectedCycle['status']) ?>
|
||||
</div>
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-md-4"><div class="school-data-item"><div class="text-muted small mb-1">الدورة المحددة</div><div class="fw-semibold"><?= e($cycleLabel) ?></div></div></div>
|
||||
<div class="col-md-4"><div class="school-data-item"><div class="text-muted small mb-1">الفترة</div><div class="fw-semibold" dir="ltr"><?= e((string) $selectedCycle['start_date']) ?> <br> <?= e((string) $selectedCycle['end_date']) ?></div></div></div>
|
||||
<div class="col-md-4"><div class="school-data-item"><div class="text-muted small mb-1">حالة الدورة</div><div class="fw-semibold"><?= e((string) school_cycle_status_map()[$selectedCycle['status']]['label']) ?></div></div></div>
|
||||
<div class="col-md-3"><div class="school-data-item"><div class="text-muted small mb-1">الطلاب</div><div class="fw-semibold"><?= e((string) $studentCycleMetrics['total']) ?></div></div></div>
|
||||
<div class="col-md-3"><div class="school-data-item"><div class="text-muted small mb-1">الفريق</div><div class="fw-semibold"><?= e((string) $teacherCycleMetrics['total']) ?></div></div></div>
|
||||
<div class="col-md-3"><div class="school-data-item"><div class="text-muted small mb-1">التقييمات</div><div class="fw-semibold"><?= e((string) $assessmentCycleMetrics['active']) ?></div></div></div>
|
||||
<div class="col-md-3"><div class="school-data-item"><div class="text-muted small mb-1">سجلات الغياب</div><div class="fw-semibold"><?= e((string) $attendanceCycleMetrics['total']) ?></div></div></div>
|
||||
</div>
|
||||
|
||||
<?php if (!$isCycleReadOnly && is_super_admin()): ?>
|
||||
<div class="text-end border-top pt-3 mt-2">
|
||||
<form method="post" class="d-inline">
|
||||
<input type="hidden" name="cycle_action" value="archive_cycle">
|
||||
<input type="hidden" name="cycle_id" value="<?= e((string) $selectedCycleId) ?>">
|
||||
<button class="btn btn-sm btn-outline-danger" type="submit">أرشفة هذه الدورة</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?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-4">
|
||||
<div class="app-card sidebar-card mb-4 p-4">
|
||||
<h6 class="mb-3">كل دورات المركز</h6>
|
||||
<div class="vstack gap-2">
|
||||
<?php foreach ($cycleContext['cycles'] as $cycle): ?>
|
||||
<a class="text-decoration-none bg-light rounded px-3 py-2 text-primary" href="<?= e(school_page_url('approved_school.php', (int) $application['id'], (int) $cycle['id'])) ?>#cycles">
|
||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||
<strong class="small"><?= e((string) $cycle['cycle_name']) ?></strong>
|
||||
<span class="badge bg-white text-primary border" style="font-size: 0.7rem;"><?= e((string) school_cycle_status_map()[$cycle['status']]['label']) ?></span>
|
||||
</div>
|
||||
<div class="text-muted" style="font-size: 0.75rem;" dir="ltr"><?= e((string) $cycle['start_date']) ?> → <?= e((string) $cycle['end_date']) ?></div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="app-card sidebar-card p-4">
|
||||
<h6 class="mb-3">فتح دورة جديدة</h6>
|
||||
<?php if (isset($cycleErrors['form'])): ?><div class="alert alert-danger py-2 small mb-3"><?= e($cycleErrors['form']) ?></div><?php endif; ?>
|
||||
<form method="post" class="vstack gap-3" novalidate>
|
||||
<input type="hidden" name="cycle_action" value="create_cycle">
|
||||
<div>
|
||||
<label class="form-label small text-muted mb-1" for="global_cycle_id">اختر الدورة</label>
|
||||
<select class="form-select form-select-sm <?= isset($cycleErrors['global_cycle_id']) ? 'is-invalid' : '' ?>" id="global_cycle_id" name="global_cycle_id">
|
||||
<option value="">اختر دورة مسجلة مسبقاً</option>
|
||||
<?php foreach (get_active_global_cycles() as $cycle): ?>
|
||||
<option value="<?= e((string)$cycle['id']) ?>"><?= e((string)$cycle['cycle_name']) ?> (<?= e((string)$cycle['start_date']) ?> - <?= e((string)$cycle['end_date']) ?>)</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php if (isset($cycleErrors['global_cycle_id'])): ?><div class="invalid-feedback"><?= e($cycleErrors['global_cycle_id']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label small text-muted mb-1" for="status">حالة البداية</label>
|
||||
<select class="form-select form-select-sm <?= isset($cycleErrors['status']) ? 'is-invalid' : '' ?>" id="status" name="status">
|
||||
<option value="active" <?= $cycleValues['status'] === 'active' ? 'selected' : '' ?>>نشطة مباشرة</option>
|
||||
<option value="upcoming" <?= $cycleValues['status'] === 'upcoming' ? 'selected' : '' ?>>قادمة</option>
|
||||
</select>
|
||||
<?php if (isset($cycleErrors['status'])): ?><div class="invalid-feedback"><?= e($cycleErrors['status']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<?php if (!empty($cycleContext['cycles'])): ?>
|
||||
<div class="border rounded-3 p-3 bg-light-subtle">
|
||||
<div class="fw-semibold mb-2" style="font-size: 0.8rem;">ترحيل البيانات من دورة سابقة</div>
|
||||
<div class="mb-3">
|
||||
<select class="form-select form-select-sm" id="source_cycle_id" name="source_cycle_id">
|
||||
<option value="0">بدون نسخ مسبق</option>
|
||||
<?php foreach ($cycleContext['cycles'] as $cycle): ?>
|
||||
<option value="<?= e((string) $cycle['id']) ?>" <?= (int) $cycleRollover['source_cycle_id'] === (int) $cycle['id'] ? 'selected' : '' ?>><?= e((string) $cycle['cycle_name']) ?> — <?= e((string) school_cycle_status_map()[$cycle['status']]['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="vstack gap-2" style="font-size: 0.8rem;">
|
||||
<label class="form-check mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="copy_teachers" value="1" <?= !empty($cycleRollover['copy_teachers']) ? 'checked' : '' ?>>
|
||||
<span class="form-check-label">الفريق التعليمي والوظائف</span>
|
||||
</label>
|
||||
<label class="form-check mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="copy_assessments" value="1" <?= !empty($cycleRollover['copy_assessments']) ? 'checked' : '' ?>>
|
||||
<span class="form-check-label">خطة التقييم والأوزان</span>
|
||||
</label>
|
||||
<label class="form-check mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="copy_students" value="1" <?= !empty($cycleRollover['copy_students']) ? 'checked' : '' ?>>
|
||||
<span class="form-check-label">الطلاب المستمرين فقط</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<button class="btn btn-sm btn-primary w-100" type="submit">إنشاء الدورة الجديدة</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-8">
|
||||
<div class="app-card mb-4 p-4">
|
||||
<h6 class="mb-3">نبذة المدرسة / المركز</h6>
|
||||
<p class="mb-0 text-muted small lh-lg"><?= nl2br(e((string) ($application['notes'] ?: 'مركز صيفي معتمد لتقديم برنامج منظم يجمع بين الأنشطة التعليمية والتربوية والمهارية تحت إشراف الولاية.'))) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="app-card sidebar-card mb-4 p-4">
|
||||
<h6 class="mb-3">بيانات التواصل</h6>
|
||||
<div class="vstack gap-2 small">
|
||||
<div class="d-flex justify-content-between border-bottom pb-2">
|
||||
<strong class="text-primary">المدير:</strong>
|
||||
<span class="text-muted"><?= e((string) $application['director_name']) ?></span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between border-bottom pb-2">
|
||||
<strong class="text-primary">الهاتف:</strong>
|
||||
<span class="text-muted" dir="ltr"><?= e((string) $application['phone']) ?></span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between border-bottom pb-2">
|
||||
<strong class="text-primary">الإيميل:</strong>
|
||||
<span class="text-muted"><?= e((string) $application['email']) ?></span>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<strong class="text-primary">المدينة:</strong>
|
||||
<span class="text-muted"><?= e((string) $application['city']) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php render_page_end();
|
||||
273
assessment_categories.php
Normal file
273
assessment_categories.php
Normal file
@ -0,0 +1,273 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
if (!is_super_admin()) {
|
||||
http_response_code(403);
|
||||
render_page_start('صلاحيات غير كافية', '');
|
||||
?>
|
||||
<section class="py-5 text-center">
|
||||
<div class="container-xxl">
|
||||
<h1 class="mb-3">عذراً</h1>
|
||||
<p>هذه الصفحة مخصصة للمشرف العام فقط.</p>
|
||||
<a href="index.php" class="btn btn-primary mt-3">العودة للرئيسية</a>
|
||||
</div>
|
||||
</section>
|
||||
<?php
|
||||
render_page_end();
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Handle POST actions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
if ($action === 'create') {
|
||||
$name = clean_text($_POST['name'] ?? '', 255);
|
||||
$description = clean_text($_POST['description'] ?? '', 1000);
|
||||
$status = in_array($_POST['status'] ?? '', ['enabled', 'disabled']) ? $_POST['status'] : 'enabled';
|
||||
|
||||
if ($name !== '') {
|
||||
$stmt = db()->prepare('INSERT INTO assessment_categories (name, description, status) VALUES (?, ?, ?)');
|
||||
$stmt->execute([$name, $description, $status]);
|
||||
set_flash('success', 'تمت إضافة الفئة بنجاح.');
|
||||
} else {
|
||||
set_flash('error', 'اسم الفئة مطلوب.');
|
||||
}
|
||||
header('Location: assessment_categories.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'edit') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$name = clean_text($_POST['name'] ?? '', 255);
|
||||
$description = clean_text($_POST['description'] ?? '', 1000);
|
||||
$status = in_array($_POST['status'] ?? '', ['enabled', 'disabled']) ? $_POST['status'] : 'enabled';
|
||||
|
||||
if ($id > 0 && $name !== '') {
|
||||
$stmt = db()->prepare('UPDATE assessment_categories SET name = ?, description = ?, status = ? WHERE id = ?');
|
||||
$stmt->execute([$name, $description, $status, $id]);
|
||||
set_flash('success', 'تم تحديث الفئة بنجاح.');
|
||||
} else {
|
||||
set_flash('error', 'تأكد من إدخال اسم الفئة.');
|
||||
}
|
||||
header('Location: assessment_categories.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'delete') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id > 0) {
|
||||
$stmt = db()->prepare('DELETE FROM assessment_categories WHERE id = ?');
|
||||
$stmt->execute([$id]);
|
||||
set_flash('success', 'تم حذف الفئة بنجاح.');
|
||||
}
|
||||
header('Location: assessment_categories.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Read list
|
||||
$search = clean_text($_GET['search'] ?? '', 255);
|
||||
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT) ?: 1;
|
||||
$limit = 10;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$query = 'SELECT * FROM assessment_categories';
|
||||
$countQuery = 'SELECT COUNT(*) FROM assessment_categories';
|
||||
$params = [];
|
||||
if ($search !== '') {
|
||||
$where = ' WHERE name LIKE ? OR description LIKE ?';
|
||||
$query .= $where;
|
||||
$countQuery .= $where;
|
||||
$params[] = "%$search%";
|
||||
$params[] = "%$search%";
|
||||
}
|
||||
|
||||
$stmtCount = db()->prepare($countQuery);
|
||||
$stmtCount->execute($params);
|
||||
$totalItems = (int)$stmtCount->fetchColumn();
|
||||
|
||||
$query .= ' ORDER BY id DESC LIMIT ' . $limit . ' OFFSET ' . $offset;
|
||||
$stmt = db()->prepare($query);
|
||||
$stmt->execute($params);
|
||||
$assessment_categories = $stmt->fetchAll();
|
||||
|
||||
$flash = consume_flash();
|
||||
render_page_start('فئات التقييم', 'assessment_categories', 'إدارة فئات التقييم الخاصة بالمراكز');
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="py-4 py-lg-5">
|
||||
<div class="container-xxl">
|
||||
<div class="admin-layout row g-4 align-items-start">
|
||||
<div class="col-lg-3 layout-sidebar-column">
|
||||
<?php require __DIR__ . '/includes/sidebar.php'; ?>
|
||||
</div>
|
||||
<div class="col-lg-9 layout-content-column">
|
||||
<div class="app-card mb-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-3">
|
||||
<div class="section-title mb-0">إدارة فئات التقييم</div>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createModal">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
|
||||
</svg>
|
||||
إضافة مادة
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Search Bar -->
|
||||
<?php render_search_bar($search, "ابحث باسم الفئة أو الوصف...", "assessment_categories.php", $_GET); ?>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table app-table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>الاسم</th>
|
||||
<th>الوصف</th>
|
||||
<th>الحالة</th>
|
||||
<th>الإجراءات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (count($assessment_categories) === 0): ?>
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-4 text-muted">لا توجد مواد مسجلة أو لم يتم العثور على نتائج.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($assessment_categories as $assessment_category): ?>
|
||||
<tr>
|
||||
<td><?= e((string)$assessment_category['id']) ?></td>
|
||||
<td class="fw-semibold"><?= e($assessment_category['name']) ?></td>
|
||||
<td class="text-muted" style="max-width: 250px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="<?= e($assessment_category['description']) ?>"><?= e($assessment_category['description'] ?: '—') ?></td>
|
||||
<td>
|
||||
<?php if ($assessment_category['status'] === 'enabled'): ?>
|
||||
<span class="badge bg-success-subtle text-success">مفعل</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-secondary-subtle text-secondary">معطل</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex gap-2">
|
||||
<!-- Edit btn -->
|
||||
<button class="btn btn-sm btn-outline-secondary" title="تعديل"
|
||||
data-bs-toggle="modal" data-bs-target="#editModal"
|
||||
data-id="<?= e((string)$assessment_category['id']) ?>"
|
||||
data-name="<?= e($assessment_category['name']) ?>"
|
||||
data-description="<?= e($assessment_category['description']) ?>"
|
||||
data-status="<?= e($assessment_category['status']) ?>"
|
||||
onclick="fillEditModal(this)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Delete btn -->
|
||||
<form method="POST" action="assessment_categories.php" onsubmit="return confirm('هل أنت متأكد من حذف هذه الفئة؟');">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="id" value="<?= e((string)$assessment_category['id']) ?>">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="حذف">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
||||
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php render_pagination($totalItems, $limit, $page, $_GET); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Create Modal -->
|
||||
<div class="modal fade" id="createModal" tabindex="-1" aria-labelledby="createModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="POST" action="assessment_categories.php">
|
||||
<input type="hidden" name="action" value="create">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="createModalLabel">إضافة مادة دراسية</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">اسم الفئة</label>
|
||||
<input type="text" name="name" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">الوصف</label>
|
||||
<textarea name="description" class="form-control" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">الحالة</label>
|
||||
<select name="status" class="form-select">
|
||||
<option value="enabled">مفعل</option>
|
||||
<option value="disabled">معطل</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" class="btn btn-primary">إضافة</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Modal -->
|
||||
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="POST" action="assessment_categories.php">
|
||||
<input type="hidden" name="action" value="edit">
|
||||
<input type="hidden" name="id" id="edit_id">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editModalLabel">تعديل مادة دراسية</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">اسم الفئة</label>
|
||||
<input type="text" name="name" id="edit_name" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">الوصف</label>
|
||||
<textarea name="description" id="edit_description" class="form-control" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">الحالة</label>
|
||||
<select name="status" id="edit_status" class="form-select">
|
||||
<option value="enabled">مفعل</option>
|
||||
<option value="disabled">معطل</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" class="btn btn-primary">حفظ التعديلات</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function fillEditModal(btn) {
|
||||
document.getElementById('edit_id').value = btn.getAttribute('data-id');
|
||||
document.getElementById('edit_name').value = btn.getAttribute('data-name');
|
||||
document.getElementById('edit_description').value = btn.getAttribute('data-description');
|
||||
document.getElementById('edit_status').value = btn.getAttribute('data-status');
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php render_page_end(); ?>
|
||||
305
assessment_criteria.php
Normal file
305
assessment_criteria.php
Normal file
@ -0,0 +1,305 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
$flash = consume_flash();
|
||||
$applicationId = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT) ?: 0;
|
||||
$requestedCycleId = filter_input(INPUT_GET, 'cycle', FILTER_VALIDATE_INT) ?: 0;
|
||||
$requestedAssessmentId = filter_input(INPUT_GET, 'assessment_id', FILTER_VALIDATE_INT) ?: 0;
|
||||
$application = $applicationId > 0 ? get_application($applicationId) : null;
|
||||
$isApprovedSchool = $application && (string) $application['status'] === 'approved';
|
||||
$errors = [];
|
||||
$cycleContext = ['cycles' => [], 'selected' => null, 'active' => null, 'read_only' => false];
|
||||
$selectedCycle = null;
|
||||
$selectedCycleId = 0;
|
||||
$isCycleReadOnly = false;
|
||||
$cycleLabel = 'لا توجد دورة بعد';
|
||||
$values = ['criteria' => []];
|
||||
|
||||
if ($application && $isApprovedSchool) {
|
||||
$cycleContext = resolve_school_cycle_context((int) $application['id'], $application, $requestedCycleId);
|
||||
$selectedCycle = $cycleContext['selected'];
|
||||
$selectedCycleId = $selectedCycle ? (int) ($selectedCycle['id'] ?? 0) : 0;
|
||||
$isCycleReadOnly = (bool) $cycleContext['read_only'];
|
||||
$cycleLabel = $selectedCycle ? (string) $selectedCycle['cycle_name'] : $cycleLabel;
|
||||
}
|
||||
|
||||
$assessmentOptions = $isApprovedSchool && $selectedCycleId > 0
|
||||
? school_assessment_type_options_by_cycle((int) $application['id'], $selectedCycleId, false)
|
||||
: [];
|
||||
$selectedAssessmentId = $requestedAssessmentId;
|
||||
if ($selectedAssessmentId <= 0 && $assessmentOptions !== []) {
|
||||
$keys = array_keys($assessmentOptions);
|
||||
$selectedAssessmentId = (int) ($keys[0] ?? 0);
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $application) {
|
||||
$selectedAssessmentId = filter_input(INPUT_POST, 'assessment_id', FILTER_VALIDATE_INT) ?: $selectedAssessmentId;
|
||||
if (!$isApprovedSchool) {
|
||||
$errors['form'] = 'لا يمكن إعداد بنود التقييم قبل اعتماد المركز.';
|
||||
} elseif ($selectedCycleId <= 0) {
|
||||
$errors['form'] = 'يرجى إنشاء دورة موسمية أولاً من صفحة المركز.';
|
||||
} elseif ($isCycleReadOnly) {
|
||||
$errors['form'] = 'هذه الدورة مؤرشفة للقراءة فقط. افتح دورة جديدة أو اختر دورة نشطة لتعديل البنود.';
|
||||
} elseif ($selectedAssessmentId <= 0 || !isset($assessmentOptions[$selectedAssessmentId])) {
|
||||
$errors['form'] = 'يرجى اختيار تقييم صحيح أولاً.';
|
||||
} else {
|
||||
[$values, $errors] = validate_assessment_criteria_input((int) $application['id'], $selectedCycleId, $selectedAssessmentId, $_POST);
|
||||
if ($errors === []) {
|
||||
try {
|
||||
$savedRows = save_assessment_criteria_in_cycle((int) $application['id'], $selectedCycleId, $selectedAssessmentId, $values);
|
||||
set_flash('success', 'تم حفظ ' . $savedRows . ' بند/بنود لهذا التقييم.');
|
||||
header('Location: ' . school_page_url('assessment_criteria.php', (int) $application['id'], $selectedCycleId) . '&assessment_id=' . urlencode((string) $selectedAssessmentId));
|
||||
exit;
|
||||
} catch (Throwable $exception) {
|
||||
$errors['form'] = 'تعذر حفظ البنود حالياً. يرجى المحاولة مرة أخرى.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$selectedAssessment = $selectedAssessmentId > 0 ? ($assessmentOptions[$selectedAssessmentId] ?? null) : null;
|
||||
$criteriaRows = $isApprovedSchool && $selectedCycleId > 0 && $selectedAssessmentId > 0
|
||||
? list_assessment_criteria_by_assessment((int) $application['id'], $selectedCycleId, $selectedAssessmentId, false)
|
||||
: [];
|
||||
$criteriaMetrics = $isApprovedSchool && $selectedCycleId > 0 && $selectedAssessmentId > 0
|
||||
? school_assessment_criteria_metrics((int) $application['id'], $selectedCycleId, $selectedAssessmentId)
|
||||
: ['total' => 0, 'active' => 0, 'active_max_score' => 0.0];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$values['criteria'] = [];
|
||||
foreach ($criteriaRows as $criterion) {
|
||||
$values['criteria'][] = [
|
||||
'id' => (int) ($criterion['id'] ?? 0),
|
||||
'title' => (string) ($criterion['title'] ?? ''),
|
||||
'max_score' => rtrim(rtrim(number_format((float) ($criterion['max_score'] ?? 0), 2, '.', ''), '0'), '.'),
|
||||
'notes' => (string) ($criterion['notes'] ?? ''),
|
||||
'is_active' => ((int) ($criterion['is_active'] ?? 0) === 1) ? '1' : '0',
|
||||
];
|
||||
}
|
||||
if ($values['criteria'] === []) {
|
||||
$values['criteria'][] = ['id' => 0, 'title' => 'الحفظ', 'max_score' => '10', 'notes' => '', 'is_active' => '1'];
|
||||
$values['criteria'][] = ['id' => 0, 'title' => 'الطلاقة', 'max_score' => '10', 'notes' => '', 'is_active' => '1'];
|
||||
$values['criteria'][] = ['id' => 0, 'title' => 'التجويد / النطق', 'max_score' => '10', 'notes' => '', 'is_active' => '1'];
|
||||
}
|
||||
}
|
||||
|
||||
$pageTitle = $application && $selectedAssessment
|
||||
? 'بنود التقييم: ' . (string) $selectedAssessment['title'] . ' — ' . (string) $application['center_name']
|
||||
: 'إعداد بنود التقييم';
|
||||
$pageDescription = 'صفحة مستقلة لبناء ورقة تقييم متعددة البنود مثل الحفظ والطلاقة والنطق لكل تقييم.';
|
||||
$assessmentsUrl = $application ? school_page_url('assessments.php', (int) $application['id'], $selectedCycleId) : 'assessments.php';
|
||||
$scoreSheetUrl = $application ? school_page_url('assessment_score_sheet.php', (int) $application['id'], $selectedCycleId) . '&assessment_id=' . urlencode((string) $selectedAssessmentId) : 'assessment_score_sheet.php';
|
||||
$scoreListUrl = $application ? school_page_url('assessment_scores.php', (int) $application['id'], $selectedCycleId) : 'assessment_scores.php';
|
||||
|
||||
if (!$application) {
|
||||
http_response_code(404);
|
||||
}
|
||||
|
||||
render_page_start($pageTitle, 'approved', $pageDescription, (string) ($application['favicon'] ?? ''));
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="py-4 py-lg-5">
|
||||
<div class="container-xxl">
|
||||
<div class="admin-layout row g-4 align-items-start">
|
||||
<div class="col-lg-3 layout-sidebar-column">
|
||||
<?php if ($application) { require __DIR__ . '/includes/center_sidebar.php'; } else { require __DIR__ . '/includes/sidebar.php'; } ?>
|
||||
</div>
|
||||
<div class="col-lg-9 layout-content-column">
|
||||
<?php if (!$application): ?>
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">المدرسة غير موجودة</div>
|
||||
<p class="text-muted mb-3">تحقق من رابط المدرسة أو ارجع إلى قائمة المراكز المعتمدة.</p>
|
||||
<a class="btn btn-primary" href="applications.php?status=approved">المراكز المعتمدة</a>
|
||||
</div>
|
||||
<?php elseif (!$isApprovedSchool): ?>
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">البنود تُفتح بعد الاعتماد</div>
|
||||
<p class="text-muted mb-0">اعتمد المركز أولاً حتى تتمكن من بناء أوراق تقييم تفصيلية.</p>
|
||||
</div>
|
||||
<?php elseif (!$selectedAssessment): ?>
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">اختر تقييماً أولاً</div>
|
||||
<p class="text-muted mb-3">ابدأ من صفحة التقييمات ثم افتح إعداد البنود للتقييم المطلوب.</p>
|
||||
<a class="btn btn-primary" href="<?= e($assessmentsUrl) ?>">الرجوع إلى التقييمات</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="page-banner approved-hero mb-4">
|
||||
<div class="row g-4 align-items-center">
|
||||
<div class="col-lg-8">
|
||||
<span class="approved-kicker mb-3">ورقة تقييم متعددة البنود</span>
|
||||
<h1 class="page-title mb-3"><?= e((string) $selectedAssessment['title']) ?></h1>
|
||||
<p class="page-copy mb-3">أضف البنود التي تريد للمعلم أن يرصدها بشكل مستقل، مثل <strong>الحفظ</strong> و<strong>الطلاقة</strong> و<strong>النطق</strong>. سيظهر كل بند كعمود مستقل في صفحة الرصد، ويُحسب المجموع تلقائياً.</p>
|
||||
<div class="hero-meta">
|
||||
<span><?= e((string) ($selectedAssessment['subject_label'] !== '' ? $selectedAssessment['subject_label'] : 'بدون مادة')) ?></span>
|
||||
<span><?= e($cycleLabel) ?></span>
|
||||
<span>الوزن <?= e(rtrim(rtrim(number_format((float) $selectedAssessment['weight_percentage'], 2, '.', ''), '0'), '.')) ?>%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="app-card h-100">
|
||||
<div class="section-title mb-3">تنقّل سريع</div>
|
||||
<div class="cta-stack">
|
||||
<a class="btn btn-primary" href="<?= e($scoreSheetUrl) ?>">فتح ورقة الرصد</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($assessmentsUrl) ?>">الرجوع إلى التقييمات</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($scoreListUrl) ?>">قائمة أوراق الرصد</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-4">
|
||||
<div class="col-md-4"><div class="app-card h-100"><div class="section-title mb-2">إجمالي البنود</div><div class="display-6 mb-1"><?= e((string) $criteriaMetrics['total']) ?></div><div class="section-subtle">كل البنود المحفوظة</div></div></div>
|
||||
<div class="col-md-4"><div class="app-card h-100"><div class="section-title mb-2">البنود النشطة</div><div class="display-6 mb-1"><?= e((string) $criteriaMetrics['active']) ?></div><div class="section-subtle">هي التي تظهر في ورقة الرصد</div></div></div>
|
||||
<div class="col-md-4"><div class="app-card h-100"><div class="section-title mb-2">المجموع النهائي</div><div class="display-6 mb-1"><?= e(rtrim(rtrim(number_format((float) $criteriaMetrics['active_max_score'], 2, '.', ''), '0'), '.')) ?></div><div class="section-subtle">يُحدَّث تلقائياً داخل التقييم</div></div></div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($errors['form'])): ?>
|
||||
<div class="alert alert-danger mb-4"><?= e($errors['form']) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($isCycleReadOnly): ?>
|
||||
<div class="alert alert-warning mb-4">هذه الدورة مؤرشفة، لذلك الصفحة معروضة للقراءة فقط.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="app-card">
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-4">
|
||||
<div>
|
||||
<div class="section-title mb-1">بنود التقييم</div>
|
||||
<div class="section-subtle">يمكنك إضافة بنود جديدة، أو إيقاف بند قديم عن الظهور في ورقة الرصد.</div>
|
||||
</div>
|
||||
<?php if (!$isCycleReadOnly): ?>
|
||||
<button type="button" class="btn btn-outline-secondary" id="addCriterionRow">إضافة بند</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<form method="post" novalidate>
|
||||
<input type="hidden" name="assessment_id" value="<?= e((string) $selectedAssessmentId) ?>">
|
||||
<div class="table-responsive">
|
||||
<table class="table app-table align-middle" id="criteriaTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 32%;">اسم البند</th>
|
||||
<th style="width: 16%;">الدرجة</th>
|
||||
<th>ملاحظة داخلية</th>
|
||||
<th style="width: 12%;">الحالة</th>
|
||||
<th style="width: 12%;">إجراء</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($values['criteria'] as $index => $criterion): ?>
|
||||
<?php $rowError = $errors['criteria_' . $index] ?? null; ?>
|
||||
<tr data-criterion-row data-existing="<?= !empty($criterion['id']) ? '1' : '0' ?>">
|
||||
<td>
|
||||
<input type="hidden" name="criteria[<?= e((string) $index) ?>][id]" value="<?= e((string) ($criterion['id'] ?? 0)) ?>">
|
||||
<input class="form-control<?= $rowError ? ' is-invalid' : '' ?>" type="text" name="criteria[<?= e((string) $index) ?>][title]" value="<?= e((string) ($criterion['title'] ?? '')) ?>" placeholder="مثال: الحفظ" <?= $isCycleReadOnly ? 'disabled' : '' ?>>
|
||||
<?php if ($rowError): ?><div class="invalid-feedback d-block"><?= e((string) $rowError) ?></div><?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<input class="form-control" type="number" step="0.01" min="0" max="1000" name="criteria[<?= e((string) $index) ?>][max_score]" value="<?= e((string) ($criterion['max_score'] ?? '')) ?>" placeholder="10" <?= $isCycleReadOnly ? 'disabled' : '' ?>>
|
||||
</td>
|
||||
<td>
|
||||
<input class="form-control" type="text" name="criteria[<?= e((string) $index) ?>][notes]" value="<?= e((string) ($criterion['notes'] ?? '')) ?>" placeholder="اختياري" <?= $isCycleReadOnly ? 'disabled' : '' ?>>
|
||||
</td>
|
||||
<td>
|
||||
<select class="form-select" name="criteria[<?= e((string) $index) ?>][is_active]" <?= $isCycleReadOnly ? 'disabled' : '' ?>>
|
||||
<option value="1" <?= (string) ($criterion['is_active'] ?? '1') === '1' ? 'selected' : '' ?>>نشط</option>
|
||||
<option value="0" <?= (string) ($criterion['is_active'] ?? '1') === '0' ? 'selected' : '' ?>>مخفي</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($isCycleReadOnly): ?>
|
||||
<span class="text-muted small">قراءة فقط</span>
|
||||
<?php elseif (!empty($criterion['id'])): ?>
|
||||
<span class="text-muted small">أوقف التفعيل لإخفائه</span>
|
||||
<?php else: ?>
|
||||
<button type="button" class="btn btn-sm btn-light icon-action" data-remove-row title="حذف البند" aria-label="حذف البند">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0A.5.5 0 0 1 8.5 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z"/><path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1 0-2H5.5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1H13.5a1 1 0 0 1 1 1ZM4 4v9a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4H4Zm2.5-2a.5.5 0 0 0-.5.5V3h4v-.5a.5.5 0 0 0-.5-.5h-3Z"/></svg>
|
||||
<span class="visually-hidden">حذف البند</span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php if (!$isCycleReadOnly): ?>
|
||||
<div class="d-flex justify-content-between align-items-center pt-3 border-top mt-3 flex-wrap gap-2">
|
||||
<div class="section-subtle">سيتم تحديث الدرجة النهائية للتقييم تلقائياً إلى مجموع البنود النشطة.</div>
|
||||
<button class="btn btn-primary" type="submit">حفظ البنود</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php if ($application && !$isCycleReadOnly && $selectedAssessment): ?>
|
||||
<template id="criterionRowTemplate">
|
||||
<tr data-criterion-row data-existing="0">
|
||||
<td>
|
||||
<input type="hidden" data-row-field="id" value="0">
|
||||
<input class="form-control" type="text" data-row-field="title" placeholder="مثال: الطلاقة">
|
||||
</td>
|
||||
<td>
|
||||
<input class="form-control" type="number" step="0.01" min="0" max="1000" data-row-field="max_score" placeholder="10">
|
||||
</td>
|
||||
<td>
|
||||
<input class="form-control" type="text" data-row-field="notes" placeholder="اختياري">
|
||||
</td>
|
||||
<td>
|
||||
<select class="form-select" data-row-field="is_active">
|
||||
<option value="1" selected>نشط</option>
|
||||
<option value="0">مخفي</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-light icon-action" data-remove-row title="حذف البند" aria-label="حذف البند">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0A.5.5 0 0 1 8.5 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z"/><path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1 0-2H5.5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1H13.5a1 1 0 0 1 1 1ZM4 4v9a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4H4Zm2.5-2a.5.5 0 0 0-.5.5V3h4v-.5a.5.5 0 0 0-.5-.5h-3Z"/></svg>
|
||||
<span class="visually-hidden">حذف البند</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const addButton = document.getElementById('addCriterionRow');
|
||||
const tableBody = document.querySelector('#criteriaTable tbody');
|
||||
const template = document.getElementById('criterionRowTemplate');
|
||||
if (!addButton || !tableBody || !template) return;
|
||||
|
||||
const wireRow = (row) => {
|
||||
const removeButton = row.querySelector('[data-remove-row]');
|
||||
if (removeButton) {
|
||||
removeButton.addEventListener('click', () => row.remove());
|
||||
}
|
||||
};
|
||||
|
||||
tableBody.querySelectorAll('[data-criterion-row]').forEach(wireRow);
|
||||
|
||||
const renumberRows = () => {
|
||||
Array.from(tableBody.querySelectorAll('[data-criterion-row]')).forEach((row, index) => {
|
||||
row.querySelectorAll('[data-row-field]').forEach((field) => {
|
||||
const key = field.getAttribute('data-row-field');
|
||||
field.name = `criteria[${index}][${key}]`;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
addButton.addEventListener('click', () => {
|
||||
const fragment = template.content.cloneNode(true);
|
||||
const row = fragment.querySelector('[data-criterion-row]');
|
||||
tableBody.appendChild(fragment);
|
||||
wireRow(tableBody.lastElementChild);
|
||||
renumberRows();
|
||||
});
|
||||
|
||||
renumberRows();
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
<?php render_page_end();
|
||||
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="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();
|
||||
153
assessment_scores.php
Normal file
153
assessment_scores.php
Normal file
@ -0,0 +1,153 @@
|
||||
<?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;
|
||||
$search = clean_text($_GET['search'] ?? '', 255);
|
||||
$application = $applicationId > 0 ? get_application($applicationId) : null;
|
||||
$isApprovedSchool = $application && (string) $application['status'] === 'approved';
|
||||
$cycleContext = ['cycles' => [], 'selected' => null, 'active' => null, 'read_only' => false];
|
||||
$selectedCycle = null;
|
||||
$selectedCycleId = 0;
|
||||
$cycleLabel = 'لا توجد دورة بعد';
|
||||
|
||||
if ($application && $isApprovedSchool) {
|
||||
$cycleContext = resolve_school_cycle_context((int) $application['id'], $application, $requestedCycleId);
|
||||
$selectedCycle = $cycleContext['selected'];
|
||||
$selectedCycleId = $selectedCycle ? (int) ($selectedCycle['id'] ?? 0) : 0;
|
||||
$cycleLabel = $selectedCycle ? (string) $selectedCycle['cycle_name'] : $cycleLabel;
|
||||
}
|
||||
|
||||
$assessmentOptions = $isApprovedSchool && $selectedCycleId > 0
|
||||
? school_assessment_type_options_by_cycle((int) $application['id'], $selectedCycleId, false)
|
||||
: [];
|
||||
|
||||
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;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
$pageTitle = $application ? 'اختيار ورقة رصد الدرجات: ' . (string) $application['center_name'] . ($selectedCycle ? ' — ' . $cycleLabel : '') : 'اختيار ورقة رصد الدرجات';
|
||||
$pageDescription = 'اختر التقييم أولاً ثم افتح صفحة رصد مستقلة ونظيفة لإدخال درجات الطلاب.';
|
||||
$approvedSchoolUrl = $application ? school_page_url('approved_school.php', (int) $application['id'], $selectedCycleId) : 'approved_school.php';
|
||||
$assessmentsUrl = $application ? school_page_url('assessments.php', (int) $application['id'], $selectedCycleId) : 'assessments.php';
|
||||
$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);
|
||||
}
|
||||
|
||||
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>
|
||||
<a class="btn btn-outline-secondary" href="application_detail.php?id=<?= e((string) $application['id']) ?>">ملف الاعتماد</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="page-banner approved-hero mb-4">
|
||||
<div class="row g-4 align-items-center">
|
||||
<div class="col-lg-8">
|
||||
<span class="approved-kicker mb-3">الخطوة 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>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-card mb-4">
|
||||
<form method="get" class="row g-2 align-items-center">
|
||||
<input type="hidden" name="id" value="<?= e((string) $application['id']) ?>">
|
||||
<input type="hidden" name="cycle" value="<?= e((string) $selectedCycleId) ?>">
|
||||
<div class="col-md-9">
|
||||
<input type="text" name="search" class="form-control" placeholder="ابحث باسم التقييم أو المادة" value="<?= e($search) ?>">
|
||||
</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="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: ?>
|
||||
<?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>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php render_page_end();
|
||||
398
assessments.php
Normal file
398
assessments.php
Normal file
@ -0,0 +1,398 @@
|
||||
<?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;
|
||||
$application = $applicationId > 0 ? get_application($applicationId) : null;
|
||||
$isApprovedSchool = $application && (string) $application['status'] === 'approved';
|
||||
$values = assessment_defaults();
|
||||
$errors = [];
|
||||
$cycleContext = ['cycles' => [], 'selected' => null, 'active' => null, 'read_only' => false];
|
||||
$selectedCycle = null;
|
||||
$selectedCycleId = 0;
|
||||
$isCycleReadOnly = false;
|
||||
$cycleLabel = 'لا توجد دورة بعد';
|
||||
|
||||
$available_subjects = [];
|
||||
if ($application) {
|
||||
$center_subjects_ids = is_string($application['subjects']) ? json_decode($application['subjects'], true) : [];
|
||||
if (!is_array($center_subjects_ids)) $center_subjects_ids = [];
|
||||
$center_subjects_ids = array_map('strval', $center_subjects_ids);
|
||||
$all_subjects = get_enabled_subjects();
|
||||
$available_subjects = array_filter($all_subjects, function($s) use ($center_subjects_ids) { return in_array((string)$s["id"], $center_subjects_ids, true); });
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $application) {
|
||||
$action = $_POST['action'] ?? 'add';
|
||||
$assessmentId = filter_input(INPUT_POST, 'assessment_id', FILTER_VALIDATE_INT) ?: 0;
|
||||
|
||||
[$values, $errors] = validate_assessment_input($_POST);
|
||||
|
||||
if (!$isApprovedSchool) {
|
||||
$errors['form'] = 'لا يمكن إعداد التقييمات قبل اعتماد المركز.';
|
||||
} elseif ($selectedCycleId <= 0) {
|
||||
$errors['form'] = 'يرجى إنشاء دورة موسمية أولاً من صفحة المركز.';
|
||||
} elseif ($isCycleReadOnly) {
|
||||
$errors['form'] = 'هذه الدورة مؤرشفة للقراءة فقط. افتح دورة جديدة أو اختر دورة نشطة لإضافة تقييمات جديدة.';
|
||||
}
|
||||
|
||||
if ($errors === []) {
|
||||
try {
|
||||
if ($action === 'edit' && $assessmentId > 0) {
|
||||
update_assessment_type_in_cycle((int) $application['id'], $selectedCycleId, $assessmentId, $values);
|
||||
set_flash('success', 'تم تحديث التقييم بنجاح.');
|
||||
} else {
|
||||
create_assessment_type_in_cycle((int) $application['id'], $selectedCycleId, $values);
|
||||
set_flash('success', 'تم حفظ التقييم داخل الدورة الموسمية المحددة.');
|
||||
}
|
||||
header('Location: ' . school_page_url('assessments.php', (int) $application['id'], $selectedCycleId) . '&' . http_build_query(array_intersect_key($_GET, array_flip(['search', 'category', 'subject_id', 'page']))));
|
||||
exit;
|
||||
} catch (Throwable $exception) {
|
||||
$errors['form'] = 'تعذر حفظ التقييم حالياً. يرجى المحاولة مرة أخرى.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$filters = [
|
||||
'search' => clean_text($_GET['search'] ?? '', 255),
|
||||
'category' => clean_text($_GET['category'] ?? '', 80),
|
||||
'subject_id' => clean_text($_GET['subject_id'] ?? '', 20)
|
||||
];
|
||||
|
||||
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT) ?: 1;
|
||||
$limit = 20;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$assessments = $isApprovedSchool && $selectedCycleId > 0 ? list_school_assessments_by_cycle((int) $application['id'], $selectedCycleId, $filters, $limit, $offset) : [];
|
||||
$totalAssessments = $isApprovedSchool && $selectedCycleId > 0 ? count_school_assessments_by_cycle((int) $application['id'], $selectedCycleId, $filters) : 0;
|
||||
|
||||
$metrics = $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,
|
||||
];
|
||||
|
||||
$activeWeight = round((float) $metrics['active_weight'], 2);
|
||||
$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';
|
||||
$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);
|
||||
}
|
||||
|
||||
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="page-banner mb-4">
|
||||
<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>
|
||||
<a class="btn btn-primary" href="<?= e($approvedSchoolUrl) ?>">صفحة المركز</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
|
||||
<div class="page-banner mb-4">
|
||||
<div class="row g-4 align-items-center">
|
||||
<div class="col-md-8">
|
||||
<h1 class="page-title mb-2">التقييمات: <?= e((string) $application['center_name']) ?></h1>
|
||||
<p class="page-copy mb-0">إدارة أنواع التقييم وتوزيع الدرجات ضمن الخطة الأكاديمية للدورة <strong><?= e($cycleLabel) ?></strong>.</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-md-end">
|
||||
<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>
|
||||
|
||||
<?php if (isset($errors['form'])): ?>
|
||||
<div class="alert alert-danger"><?= 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) ?>">
|
||||
<div class="col-md-4">
|
||||
<input type="text" name="search" class="form-control" placeholder="ابحث باسم التقييم..." value="<?= e($filters['search']) ?>">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select name="subject_id" class="form-select">
|
||||
<option value="">كل المواد</option>
|
||||
<?php foreach ($available_subjects as $subj):
|
||||
?><option value="<?= e((string)$subj['id']) ?>" <?= $filters['subject_id'] === (string)$subj['id'] ? 'selected' : '' ?>><?= e($subj['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select name="category" class="form-select">
|
||||
<option value="">كل الفئات</option>
|
||||
<?php foreach (assessment_category_options() as $cat):
|
||||
?><option value="<?= e($cat) ?>" <?= $filters['category'] === $cat ? 'selected' : '' ?>><?= e($cat) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-primary w-100">تصفية</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="app-card mb-4">
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-4"><div class="school-data-item"><strong>إجمالي الأنواع</strong><span><?= e((string) $metrics['total']) ?> نوع</span></div></div>
|
||||
<div class="col-md-4"><div class="school-data-item"><strong>المفعّل الآن</strong><span><?= e((string) $metrics['active']) ?> نوع</span></div></div>
|
||||
<div class="col-md-4"><div class="school-data-item"><strong>الأوزان المفعلة</strong><span><?= e(number_format($activeWeight, 2, '.', '')) ?>%</span></div></div>
|
||||
</div>
|
||||
|
||||
<?php if ($assessments === []): ?>
|
||||
<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: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table app-table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>التقييم</th>
|
||||
<th>المادة</th>
|
||||
<th>الفئة</th>
|
||||
<th>المقياس</th>
|
||||
<th>الدرجة</th>
|
||||
<th>الوزن</th>
|
||||
<th>البنود</th>
|
||||
<th>الحالة</th>
|
||||
<?php if (!$isCycleReadOnly): ?><th>الإجراء</th><?php endif; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($assessments as $assessment):
|
||||
$subjName = 'عام';
|
||||
foreach ($available_subjects as $s) {
|
||||
if ((string)$s['id'] === (string)$assessment['subject_id']) {
|
||||
$subjName = $s['name']; break;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<strong><?= e((string) $assessment['title']) ?></strong>
|
||||
<?php if (!empty($assessment['notes'])): ?><div class="small text-muted"><?= e((string) $assessment['notes']) ?></div><?php endif; ?>
|
||||
</td>
|
||||
<td><span class="badge bg-secondary"><?= e($subjName) ?></span></td>
|
||||
<td><?= e((string) $assessment['category']) ?></td>
|
||||
<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 class="table-action-cell">
|
||||
<div class="table-icon-actions">
|
||||
<a
|
||||
class="btn btn-sm btn-primary icon-action"
|
||||
href="<?= e($assessmentScoreSheetBaseUrl . '&assessment_id=' . urlencode((string) $assessment['id'])) ?>"
|
||||
title="رصد الدرجات"
|
||||
aria-label="رصد الدرجات"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true"><path d="M2.5 1A1.5 1.5 0 0 0 1 2.5v11A1.5 1.5 0 0 0 2.5 15h11a1.5 1.5 0 0 0 1.5-1.5v-11A1.5 1.5 0 0 0 13.5 1h-11ZM2 2.5a.5.5 0 0 1 .5-.5H4v12H2.5a.5.5 0 0 1-.5-.5v-11Zm3 11.5V2h8.5a.5.5 0 0 1 .5.5v11a.5.5 0 0 1-.5.5H5Zm2.5-8.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1H8a.5.5 0 0 1-.5-.5Zm0 2.5A.5.5 0 0 1 8 7.5h4a.5.5 0 0 1 0 1H8a.5.5 0 0 1-.5-.5Zm0 2.5A.5.5 0 0 1 8 10h2.5a.5.5 0 0 1 0 1H8a.5.5 0 0 1-.5-.5Z"/></svg>
|
||||
<span class="visually-hidden">رصد الدرجات</span>
|
||||
</a>
|
||||
<a
|
||||
class="btn btn-sm btn-outline-secondary icon-action"
|
||||
href="<?= e($criteriaBaseUrl . '&assessment_id=' . urlencode((string) $assessment['id'])) ?>"
|
||||
title="إعداد البنود"
|
||||
aria-label="إعداد البنود"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true"><path d="M6 10.117V16l4-2.5 4 2.5v-5.883l-4 2.5-4-2.5Z"/><path d="M10 0a4 4 0 1 0 0 8 4 4 0 0 0 0-8ZM7 4a3 3 0 1 1 6 0 3 3 0 0 1-6 0Z"/><path d="M2.5 1A1.5 1.5 0 0 0 1 2.5v9A1.5 1.5 0 0 0 2.5 13H5v-1H2.5a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h7.55a5.02 5.02 0 0 0-.497-1H2.5Z"/></svg>
|
||||
<span class="visually-hidden">إعداد البنود</span>
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-secondary icon-action"
|
||||
onclick="editAssessment(<?= htmlspecialchars(json_encode($assessment), ENT_QUOTES, 'UTF-8') ?>)"
|
||||
title="تعديل التقييم"
|
||||
aria-label="تعديل التقييم"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true"><path d="M12.854.146a.5.5 0 0 1 .707 0l2.586 2.586a.5.5 0 0 1 0 .707l-9.793 9.793a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168L12.854.146ZM11.207 2 3 10.207V13h2.793L14 4.793 11.207 2Z"/></svg>
|
||||
<span class="visually-hidden">تعديل التقييم</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php render_pagination($totalAssessments, $limit, $page, $_GET); ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<?php if (!$isCycleReadOnly):
|
||||
?><div class="modal fade" id="assessmentModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<form method="post" id="assessmentForm" novalidate>
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="assessmentModalLabel">إضافة نوع تقييم</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="إغلاق"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="action" id="formAction" value="add">
|
||||
<input type="hidden" name="assessment_id" id="formAssessmentId" value="">
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label" for="title">اسم التقييم</label>
|
||||
<input class="form-control" id="title" name="title" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="subject_id">المادة الدراسية</label>
|
||||
<select class="form-select" id="subject_id" name="subject_id">
|
||||
<option value="">عام (لا يخص مادة معينة)</option>
|
||||
<?php foreach ($available_subjects as $subj):
|
||||
?><option value="<?= e((string)$subj['id']) ?>"><?= e($subj['name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="category">الفئة</label>
|
||||
<select class="form-select" id="category" name="category">
|
||||
<?php foreach (assessment_category_options() as $option):
|
||||
?><option value="<?= e($option) ?>"><?= e($option) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="scale_type">المقياس</label>
|
||||
<select class="form-select" id="scale_type" name="scale_type">
|
||||
<?php foreach (assessment_scale_type_map() as $key => $meta):
|
||||
?><option value="<?= e($key) ?>"><?= e((string) $meta['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<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>
|
||||
<input type="number" step="0.01" min="0" max="100" class="form-control" id="weight_percentage" name="weight_percentage" value="10">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="is_active">حالة التفعيل</label>
|
||||
<select class="form-select" id="is_active" name="is_active">
|
||||
<option value="1">مفعل داخل الخطة</option>
|
||||
<option value="0">مؤرشف / تحضيري</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label" for="notes">ملاحظات</label>
|
||||
<textarea class="form-control" id="notes" name="notes" rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" class="btn btn-primary">حفظ التقييم</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
let assessmentModal;
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const m = document.getElementById('assessmentModal');
|
||||
if (m) assessmentModal = new bootstrap.Modal(m);
|
||||
});
|
||||
function resetAssessmentForm() {
|
||||
document.getElementById('assessmentForm').reset();
|
||||
document.getElementById('formAction').value = 'add';
|
||||
document.getElementById('formAssessmentId').value = '';
|
||||
document.getElementById('assessmentModalLabel').innerText = 'إضافة نوع تقييم';
|
||||
}
|
||||
function editAssessment(data) {
|
||||
resetAssessmentForm();
|
||||
document.getElementById('formAction').value = 'edit';
|
||||
document.getElementById('formAssessmentId').value = data.id;
|
||||
document.getElementById('assessmentModalLabel').innerText = 'تعديل التقييم';
|
||||
|
||||
document.getElementById('title').value = data.title || '';
|
||||
document.getElementById('subject_id').value = data.subject_id || '';
|
||||
document.getElementById('category').value = data.category || '';
|
||||
document.getElementById('scale_type').value = data.scale_type || '';
|
||||
document.getElementById('max_score').value = data.max_score || '100';
|
||||
document.getElementById('weight_percentage').value = data.weight_percentage || '0';
|
||||
document.getElementById('is_active').value = data.is_active;
|
||||
document.getElementById('notes').value = data.notes || '';
|
||||
|
||||
if (assessmentModal) assessmentModal.show();
|
||||
}
|
||||
<?php if (!empty($errors) && isset($_POST['action'])):
|
||||
?>document.addEventListener("DOMContentLoaded", () => {
|
||||
if (assessmentModal) assessmentModal.show();
|
||||
});
|
||||
<?php endif; ?>
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php render_page_end(); ?>
|
||||
File diff suppressed because it is too large
Load Diff
BIN
assets/images/pexels/dashboard-analytics.jpg
Normal file
BIN
assets/images/pexels/dashboard-analytics.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 138 KiB |
BIN
assets/images/pexels/hero-office.jpg
Normal file
BIN
assets/images/pexels/hero-office.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 239 KiB |
@ -1,39 +1,12 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const chatForm = document.getElementById('chat-form');
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
const chatMessages = document.getElementById('chat-messages');
|
||||
const toastElement = document.getElementById('appToast');
|
||||
if (toastElement && window.bootstrap) {
|
||||
const toast = new bootstrap.Toast(toastElement);
|
||||
toast.show();
|
||||
}
|
||||
|
||||
const appendMessage = (text, sender) => {
|
||||
const msgDiv = document.createElement('div');
|
||||
msgDiv.classList.add('message', sender);
|
||||
msgDiv.textContent = text;
|
||||
chatMessages.appendChild(msgDiv);
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
};
|
||||
|
||||
chatForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const message = chatInput.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
appendMessage(message, 'visitor');
|
||||
chatInput.value = '';
|
||||
|
||||
try {
|
||||
const response = await fetch('api/chat.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message })
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
// Artificial delay for realism
|
||||
setTimeout(() => {
|
||||
appendMessage(data.reply, 'bot');
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
appendMessage("Sorry, something went wrong. Please try again.", 'bot');
|
||||
}
|
||||
});
|
||||
const invalidField = document.querySelector('.is-invalid');
|
||||
if (invalidField) {
|
||||
invalidField.focus({ preventScroll: false });
|
||||
}
|
||||
});
|
||||
|
||||
394
attendance.php
Normal file
394
attendance.php
Normal file
@ -0,0 +1,394 @@
|
||||
<?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;
|
||||
$application = $applicationId > 0 ? get_application($applicationId) : null;
|
||||
$isApprovedSchool = $application && (string) $application['status'] === 'approved';
|
||||
$values = attendance_defaults();
|
||||
$errors = [];
|
||||
$cycleContext = ['cycles' => [], 'selected' => null, 'active' => null, 'read_only' => false];
|
||||
$selectedCycle = null;
|
||||
$selectedCycleId = 0;
|
||||
$isCycleReadOnly = false;
|
||||
$cycleLabel = 'لا توجد دورة بعد';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $application) {
|
||||
if (!$isApprovedSchool) {
|
||||
$errors['form'] = 'لا يمكن فتح سجل الغياب قبل اعتماد المركز.';
|
||||
} elseif ($selectedCycleId <= 0) {
|
||||
$errors['form'] = 'يرجى إنشاء دورة موسمية أولاً من صفحة المركز.';
|
||||
} elseif ($isCycleReadOnly) {
|
||||
$errors['form'] = 'هذه الدورة مؤرشفة للقراءة فقط. افتح دورة جديدة أو اختر دورة نشطة لإضافة غياب جديد.';
|
||||
} else {
|
||||
[$values, $errors] = validate_attendance_input_for_cycle((int) $application['id'], $selectedCycleId, $_POST);
|
||||
if ($errors === []) {
|
||||
try {
|
||||
create_attendance_record_in_cycle((int) $application['id'], $selectedCycleId, $values);
|
||||
set_flash('success', 'تم حفظ سجل الغياب داخل الدورة الموسمية المحددة.');
|
||||
header('Location: ' . school_page_url('attendance.php', (int) $application['id'], $selectedCycleId));
|
||||
exit;
|
||||
} catch (PDOException $exception) {
|
||||
$duplicateCode = isset($exception->errorInfo[1]) && (int) $exception->errorInfo[1] === 1062;
|
||||
if ($duplicateCode) {
|
||||
$errors['attendance_date'] = 'يوجد سجل غياب سابق لهذا الطالب في نفس التاريخ داخل هذه الدورة.';
|
||||
} else {
|
||||
$errors['form'] = 'تعذر حفظ سجل الغياب حالياً. يرجى المحاولة مرة أخرى.';
|
||||
}
|
||||
} catch (Throwable $exception) {
|
||||
$errors['form'] = 'تعذر حفظ سجل الغياب حالياً. يرجى المحاولة مرة أخرى.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$students = $isApprovedSchool && $selectedCycleId > 0 ? list_school_students_by_cycle((int) $application['id'], $selectedCycleId) : [];
|
||||
$studentOptions = $isApprovedSchool && $selectedCycleId > 0 ? school_student_options_by_cycle((int) $application['id'], $selectedCycleId) : [];
|
||||
$search = clean_text($_GET['search'] ?? '', 255);
|
||||
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT) ?: 1;
|
||||
$limit = 10;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$records = $isApprovedSchool && $selectedCycleId > 0 ? list_school_attendance_records_by_cycle((int) $application['id'], $selectedCycleId, $search, $limit, $offset) : [];
|
||||
$totalRecords = $isApprovedSchool && $selectedCycleId > 0 ? count_school_attendance_records_by_cycle((int) $application['id'], $selectedCycleId, $search) : 0;
|
||||
|
||||
$metrics = $isApprovedSchool && $selectedCycleId > 0 ? school_attendance_metrics_by_cycle((int) $application['id'], $selectedCycleId) : [
|
||||
'total' => 0,
|
||||
'absent' => 0,
|
||||
'excused' => 0,
|
||||
'late' => 0,
|
||||
'affected_students' => 0,
|
||||
'latest_date' => '',
|
||||
'today_count' => 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,
|
||||
];
|
||||
$assessmentMetrics = $isApprovedSchool && $selectedCycleId > 0 ? school_assessment_metrics_by_cycle((int) $application['id'], $selectedCycleId) : [
|
||||
'total' => 0,
|
||||
'active' => 0,
|
||||
'inactive' => 0,
|
||||
'total_weight' => 0,
|
||||
'active_weight' => 0,
|
||||
'average_max_score' => 0,
|
||||
'percentage' => 0,
|
||||
'points' => 0,
|
||||
'rubric' => 0,
|
||||
];
|
||||
|
||||
$trackedStudents = max(1, $studentMetrics['active']);
|
||||
$incidentRate = $metrics['total'] > 0 ? round(($metrics['affected_students'] / $trackedStudents) * 100, 1) : 0.0;
|
||||
$latestDateLabel = $metrics['latest_date'] !== '' ? $metrics['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';
|
||||
$applicationDetailUrl = $application ? 'application_detail.php?id=' . urlencode((string) $application['id']) : 'application_detail.php';
|
||||
|
||||
if (!$application) {
|
||||
http_response_code(404);
|
||||
}
|
||||
|
||||
render_page_start($pageTitle, 'approved', $pageDescription, (string) ($application['favicon'] ?? ''));
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="py-4 py-lg-5">
|
||||
<div class="container-xxl">
|
||||
<div class="admin-layout row g-4 align-items-start">
|
||||
<div class="col-lg-3 layout-sidebar-column">
|
||||
<?php if ($application) { require __DIR__ . '/includes/center_sidebar.php'; } else { require __DIR__ . '/includes/sidebar.php'; } ?>
|
||||
</div>
|
||||
<div class="col-lg-9 layout-content-column">
|
||||
|
||||
<?php if (!$application): ?>
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">المدرسة غير موجودة</div>
|
||||
<p class="text-muted mb-3">تحقق من رابط المدرسة أو ارجع إلى قائمة المراكز المعتمدة.</p>
|
||||
<a class="btn btn-primary" href="applications.php?status=approved">المراكز المعتمدة</a>
|
||||
</div>
|
||||
<?php elseif (!$isApprovedSchool): ?>
|
||||
<div class="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">تم تجهيز صفحة غياب الطلاب، لكن استخدامها مرتبط بتحويل حالة المركز إلى <strong>معتمد</strong> أولاً حتى يبقى السجل اليومي محصوراً بالمراكز الجاهزة للتشغيل.</p>
|
||||
<div class="hero-meta">
|
||||
<span>الحالة الحالية: <?= e(status_meta((string) $application['status'])['label']) ?></span>
|
||||
<span>المدينة: <?= e((string) $application['city']) ?></span>
|
||||
</div>
|
||||
<div class="cta-stack mt-4">
|
||||
<a class="btn btn-primary" href="<?= e($applicationDetailUrl) ?>">العودة لملف الاعتماد</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($approvedSchoolUrl) ?>">صفحة المركز</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">هنا يتم تسجيل الغياب اليومي، الأعذار، وحالات التأخر في صفحة تشغيلية منفصلة. كل سجل يرتبط مباشرة بطالب محدد داخل هذا المركز فقط.</p>
|
||||
<div class="hero-meta">
|
||||
<span><?= e((string) $application['city']) ?></span>
|
||||
<span><?= e((string) $metrics['affected_students']) ?> طلاب عليهم ملاحظات حضور</span>
|
||||
<span>آخر تحديث <?= e($latestDateLabel) ?></span>
|
||||
</div>
|
||||
<div class="cta-stack mt-4">
|
||||
<a class="btn btn-primary" href="admin.php">لوحة الإدارة</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($approvedSchoolUrl) ?>">العودة لصفحة المركز</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($studentsUrl) ?>">تسجيل الطلاب</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($teachersUrl) ?>">فريق المعلمين</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($assessmentsUrl) ?>">التقييمات والأوزان</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($applicationDetailUrl) ?>">ملف الاعتماد</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="app-card h-100 approved-note">
|
||||
<div class="section-title mb-3">ملخص الانضباط اليومي</div>
|
||||
<?php if ($studentMetrics['active'] > 0): ?>
|
||||
<div class="score-display mb-3"><strong><?= e(number_format($incidentRate, 1, '.', '')) ?>%</strong><span>نسبة الطلاب المتأثرين</span></div>
|
||||
<div class="score-bar mb-3" aria-label="نسبة الطلاب المتأثرين"><span style="width: <?= e((string) min(100, max(0, $incidentRate))) ?>%"></span></div>
|
||||
<?php endif; ?>
|
||||
<div class="summary-stack">
|
||||
<div class="summary-row"><span>السجلات اليوم</span><strong><?= e((string) $metrics['today_count']) ?></strong></div>
|
||||
<div class="summary-row"><span>الغياب بدون عذر</span><strong><?= e((string) $metrics['absent']) ?></strong></div>
|
||||
<div class="summary-row"><span>الغياب بعذر / تأخر</span><strong><?= e((string) ($metrics['excused'] + $metrics['late'])) ?></strong></div>
|
||||
</div>
|
||||
</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>
|
||||
<div class="cta-stack mt-3">
|
||||
<?php if (is_super_admin()): ?><a class="btn btn-outline-secondary" href="<?= e($approvedSchoolUrl) ?>#cycles">إدارة الدورات الموسمية</a><?php endif; ?>
|
||||
</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('attendance.php', (int) $application['id'], (int) $cycle['id'])) ?>">
|
||||
<div>
|
||||
<strong><?= e((string) $cycle['cycle_name']) ?><?= $isCurrentCycleLink ? ' — المعروضة الآن' : '' ?></strong>
|
||||
<span><?= e($cycleMetaLine) ?></span>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</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 ($isCycleReadOnly): ?>
|
||||
<div class="alert alert-warning mb-0">هذه الدورة مؤرشفة. يمكنك مراجعة السجل فقط، أو فتح دورة جديدة من صفحة المركز.</div>
|
||||
<?php elseif ($students === []): ?>
|
||||
<div class="alert alert-warning mb-0">ابدأ أولاً من صفحة الطلاب لإضافة الطلاب قبل تسجيل أي غياب.</div>
|
||||
<?php else: ?>
|
||||
<?php if (isset($errors['form'])): ?>
|
||||
<div class="alert alert-danger"><?= e($errors['form']) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" class="vstack gap-3" novalidate>
|
||||
<div>
|
||||
<label class="form-label" for="student_id">الطالب</label>
|
||||
<select class="form-select <?= isset($errors['student_id']) ? 'is-invalid' : '' ?>" id="student_id" name="student_id">
|
||||
<option value="">اختر الطالب</option>
|
||||
<?php foreach ($studentOptions as $studentId => $studentMeta): ?>
|
||||
<option value="<?= e((string) $studentId) ?>" <?= $values['student_id'] === (string) $studentId ? 'selected' : '' ?>><?= e($studentMeta['label'] . ' — ' . $studentMeta['grade_level']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php if (isset($errors['student_id'])): ?><div class="invalid-feedback"><?= e($errors['student_id']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="form-label" for="attendance_date">التاريخ</label>
|
||||
<input class="form-control <?= isset($errors['attendance_date']) ? 'is-invalid' : '' ?>" type="date" id="attendance_date" name="attendance_date" value="<?= e($values['attendance_date']) ?>">
|
||||
<?php if (isset($errors['attendance_date'])): ?><div class="invalid-feedback"><?= e($errors['attendance_date']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="form-label" for="attendance_status">الحالة</label>
|
||||
<select class="form-select <?= isset($errors['attendance_status']) ? 'is-invalid' : '' ?>" id="attendance_status" name="attendance_status">
|
||||
<?php foreach (attendance_status_map() as $statusKey => $statusMeta): ?>
|
||||
<option value="<?= e($statusKey) ?>" <?= $values['attendance_status'] === $statusKey ? 'selected' : '' ?>><?= e($statusMeta['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php if (isset($errors['attendance_status'])): ?><div class="invalid-feedback"><?= e($errors['attendance_status']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="form-label" for="absence_reason">سبب الغياب / التأخر</label>
|
||||
<input class="form-control <?= isset($errors['absence_reason']) ? 'is-invalid' : '' ?>" id="absence_reason" name="absence_reason" value="<?= e($values['absence_reason']) ?>" placeholder="مثال: ظرف صحي أو تأخر في المواصلات">
|
||||
<?php if (isset($errors['absence_reason'])): ?><div class="invalid-feedback"><?= e($errors['absence_reason']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="form-label" for="notes">ملاحظات المتابعة</label>
|
||||
<textarea class="form-control" id="notes" name="notes" rows="4" placeholder="مثال: تم التواصل مع ولي الأمر أو تحويل الحالة للمرشد."><?= e($values['notes']) ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button class="btn btn-primary" type="submit">حفظ سجل الغياب</button>
|
||||
</div>
|
||||
</form>
|
||||
<?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>التواصل مع أولياء الأمور</strong><span class="section-subtle">يمكن لاحقاً إضافة إشعارات أو رسائل عند تكرار الغياب لنفس الطالب.</span></li>
|
||||
<li><strong>تقارير أسبوعية</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) $metrics['total']) ?> سجل / <?= e((string) $metrics['affected_students']) ?> طلاب</span>
|
||||
</div>
|
||||
<?php render_search_bar($search, 'ابحث باسم الطالب أو الكود...', school_page_url('attendance.php', (int)$application['id'], $selectedCycleId), $_GET); ?>
|
||||
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-3"><div class="school-data-item"><strong>الغياب</strong><span><?= e((string) $metrics['absent']) ?> حالة</span></div></div>
|
||||
<div class="col-md-3"><div class="school-data-item"><strong>بعذر</strong><span><?= e((string) $metrics['excused']) ?> حالة</span></div></div>
|
||||
<div class="col-md-3"><div class="school-data-item"><strong>تأخر</strong><span><?= e((string) $metrics['late']) ?> حالة</span></div></div>
|
||||
<div class="col-md-3"><div class="school-data-item"><strong>السجلات اليوم</strong><span><?= e((string) $metrics['today_count']) ?> حالة</span></div></div>
|
||||
</div>
|
||||
|
||||
<?php if ($records === []): ?>
|
||||
<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: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table app-table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>الطالب</th>
|
||||
<th>التاريخ</th>
|
||||
<th>الحالة</th>
|
||||
<th>السبب</th>
|
||||
<th>المتابعة</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($records as $record): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<strong><?= e((string) $record['full_name']) ?></strong>
|
||||
<small><?= e((string) $record['student_code']) ?> — <?= e((string) $record['grade_level']) ?></small>
|
||||
</td>
|
||||
<td><?= e((string) $record['attendance_date']) ?></td>
|
||||
<td><?= attendance_status_badge((string) $record['attendance_status']) ?></td>
|
||||
<td><?= e((string) ($record['absence_reason'] ?: '—')) ?></td>
|
||||
<td>
|
||||
<strong><?= e((string) ($record['guardian_phone'] ?: '—')) ?></strong>
|
||||
<?php if (!empty($record['notes'])): ?><small><?= e((string) $record['notes']) ?></small><?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php render_pagination($totalRecords, $limit, $page, $_GET); ?>
|
||||
<?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['total']) ?> عضو</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($latestDateLabel) ?></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();
|
||||
249
center_application.php
Normal file
249
center_application.php
Normal file
@ -0,0 +1,249 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
$flash = consume_flash();
|
||||
$values = application_defaults();
|
||||
$errors = [];
|
||||
$stats = dashboard_metrics();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
[$values, $errors] = validate_application_input($_POST);
|
||||
|
||||
if ($errors === []) {
|
||||
try {
|
||||
$applicationId = create_application($values);
|
||||
set_flash('success', 'تم إرسال طلب فتح المركز بنجاح وهو الآن ظاهر في لوحة المشرف العام.');
|
||||
header('Location: application_detail.php?id=' . urlencode((string) $applicationId));
|
||||
exit;
|
||||
} catch (Throwable $exception) {
|
||||
$errors['form'] = 'تعذر حفظ الطلب حالياً. يرجى المحاولة مرة أخرى.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render_page_start('طلب فتح مركز', 'apply', 'بوابة تقديم طلبات فتح المراكز الصيفية مع التحقق من بيانات التشغيل الأساسية.');
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="py-4 py-lg-5">
|
||||
<div class="container-xxl">
|
||||
|
||||
<div class="row align-items-center mb-4 bg-white rounded-4 shadow-sm overflow-hidden" style="border: 1px solid var(--border-color);">
|
||||
<div class="col-lg-8 p-4 p-lg-5">
|
||||
<span class="badge bg-primary bg-opacity-10 text-primary mb-3 px-3 py-2 rounded-pill border border-primary border-opacity-25">نموذج التسجيل</span>
|
||||
<h1 class="h3 fw-bold mb-3 text-primary">طلب فتح مركز صيفي</h1>
|
||||
<p class="text-muted mb-0" style="line-height: 1.8; font-size: 0.95rem;">
|
||||
هذه الصفحة مخصصة بالكامل لبيانات طلب المركز: تعريف الجهة، المسؤول المباشر، الطاقة التشغيلية، وملاحظات التنفيذ قبل انتقال الملف إلى لوحة المراجعة.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-lg-4 p-4 p-lg-5 bg-light border-start d-flex flex-column justify-content-center h-100">
|
||||
<div class="text-muted small fw-bold mb-2">حالة سير الطلبات الحالية</div>
|
||||
<div class="fs-2 fw-bold text-dark mb-1"><?= e((string) $stats['all']) ?></div>
|
||||
<div class="text-secondary small" style="line-height: 1.6;">إجمالي الطلبات المسجلة، منها <span class="fw-bold text-warning"><?= e((string) $stats['submitted']) ?></span> بانتظار الاستلام و<span class="fw-bold text-info"><?= e((string) $stats['under_review']) ?></span> تحت المراجعة.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-8">
|
||||
<div class="app-card form-card shadow-sm border-0">
|
||||
<div class="section-head mb-4">
|
||||
<div>
|
||||
<div class="section-title">بيانات الطلب الأساسية</div>
|
||||
<div class="section-copy">قسّمنا النموذج إلى مجموعات واضحة حتى لا تختلط بيانات المركز مع بيانات التشغيل والتواصل.</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<?php if (!empty($errors['form'])): ?>
|
||||
<div class="alert alert-danger mb-4"><?= e($errors['form']) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" novalidate>
|
||||
<div class="form-section-block mb-4">
|
||||
<div class="form-section-heading">
|
||||
<div>
|
||||
<h2 class="form-section-title">تعريف المركز</h2>
|
||||
<p class="form-section-copy">معلومات الهوية الرسمية للمركز ومكان تشغيله والفئة التي سيخدمها.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label" for="center_name">اسم المركز</label>
|
||||
<input class="form-control <?= isset($errors['center_name']) ? 'is-invalid' : '' ?>" id="center_name" name="center_name" value="<?= e($values['center_name']) ?>" placeholder="مثال: مركز الريادة الصيفي">
|
||||
<?php if (isset($errors['center_name'])): ?><div class="invalid-feedback"><?= e($errors['center_name']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="city">المدينة / المنطقة</label>
|
||||
<input class="form-control <?= isset($errors['city']) ? 'is-invalid' : '' ?>" id="city" name="city" value="<?= e($values['city']) ?>" placeholder="العاصمة">
|
||||
<?php if (isset($errors['city'])): ?><div class="invalid-feedback"><?= e($errors['city']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="center_type">نوع المركز</label>
|
||||
<select class="form-select <?= isset($errors['center_type']) ? 'is-invalid' : '' ?>" id="center_type" name="center_type">
|
||||
<option value="">اختر</option>
|
||||
<?php foreach (['بنين', 'بنات', 'مختلط'] as $option): ?>
|
||||
<option value="<?= e($option) ?>" <?= $values['center_type'] === $option ? 'selected' : '' ?>><?= e($option) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php if (isset($errors['center_type'])): ?><div class="invalid-feedback"><?= e($errors['center_type']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="gender_scope">الفئة المستهدفة</label>
|
||||
<select class="form-select <?= isset($errors['gender_scope']) ? 'is-invalid' : '' ?>" id="gender_scope" name="gender_scope">
|
||||
<option value="">اختر</option>
|
||||
<?php foreach (['طلاب', 'طالبات', 'طلاب وطالبات'] as $option): ?>
|
||||
<option value="<?= e($option) ?>" <?= $values['gender_scope'] === $option ? 'selected' : '' ?>><?= e($option) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php if (isset($errors['gender_scope'])): ?><div class="invalid-feedback"><?= e($errors['gender_scope']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section-block mb-4">
|
||||
<div class="form-section-heading">
|
||||
<div>
|
||||
<h2 class="form-section-title">بيانات المسؤول والتواصل</h2>
|
||||
<p class="form-section-copy">تُستخدم هذه البيانات للتواصل السريع أثناء التحقق واستكمال الملاحظات.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="director_name">اسم مدير / مديرة المركز</label>
|
||||
<input class="form-control <?= isset($errors['director_name']) ? 'is-invalid' : '' ?>" id="director_name" name="director_name" value="<?= e($values['director_name']) ?>" placeholder="الاسم الرباعي">
|
||||
<?php if (isset($errors['director_name'])): ?><div class="invalid-feedback"><?= e($errors['director_name']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="phone">رقم الهاتف</label>
|
||||
<input class="form-control <?= isset($errors['phone']) ? 'is-invalid' : '' ?>" id="phone" name="phone" value="<?= e($values['phone']) ?>" placeholder="0500000000">
|
||||
<?php if (isset($errors['phone'])): ?><div class="invalid-feedback"><?= e($errors['phone']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="email">البريد الإلكتروني</label>
|
||||
<input type="email" class="form-control <?= isset($errors['email']) ? 'is-invalid' : '' ?>" id="email" name="email" value="<?= e($values['email']) ?>" placeholder="name@example.com">
|
||||
<?php if (isset($errors['email'])): ?><div class="invalid-feedback"><?= e($errors['email']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section-block mb-4">
|
||||
<div class="form-section-heading">
|
||||
<div>
|
||||
<h2 class="form-section-title">التشغيل والجدول الزمني</h2>
|
||||
<p class="form-section-copy">قدّم تقديراً للطاقة الاستيعابية والفترة الزمنية واحتياجات التنفيذ المباشرة.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="expected_students">الطاقة الاستيعابية المتوقعة</label>
|
||||
<input type="number" min="10" max="2000" class="form-control <?= isset($errors['expected_students']) ? 'is-invalid' : '' ?>" id="expected_students" name="expected_students" value="<?= e($values['expected_students']) ?>" placeholder="150">
|
||||
<?php if (isset($errors['expected_students'])): ?><div class="invalid-feedback"><?= e($errors['expected_students']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label class="form-label" for="global_cycle_id">الدورة المطلوبة</label>
|
||||
<select class="form-select <?= isset($errors['global_cycle_id']) ? 'is-invalid' : '' ?>" id="global_cycle_id" name="global_cycle_id">
|
||||
<option value="">اختر الدورة...</option>
|
||||
<?php foreach (get_active_global_cycles() as $cycle): ?>
|
||||
<option value="<?= e((string)$cycle['id']) ?>" <?= ((string)($values['global_cycle_id'] ?? '')) === ((string)$cycle['id']) ? 'selected' : '' ?>>
|
||||
<?= e((string)$cycle['cycle_name']) ?> (<?= e((string)$cycle['start_date']) ?> - <?= e((string)$cycle['end_date']) ?>)
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php if (isset($errors['global_cycle_id'])): ?><div class="invalid-feedback"><?= e($errors['global_cycle_id']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="notes">ملخص البرنامج والاحتياجات</label>
|
||||
<textarea class="form-control" id="notes" name="notes" rows="5" placeholder="مثال: المسارات التعليمية، عدد القاعات، الشركاء، أو أي متطلبات تشغيلية."><?= e($values['notes']) ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-section-block mb-4">
|
||||
<div class="form-section-heading">
|
||||
<div>
|
||||
<h2 class="form-section-title">المواد الدراسية</h2>
|
||||
<p class="form-section-copy">اختر المواد الدراسية المطلوب تدريسها في هذا المركز.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<?php $available_subjects = get_enabled_subjects(); ?>
|
||||
<?php if (empty($available_subjects)): ?>
|
||||
<p class="text-muted small">لا توجد مواد دراسية مفعلة حالياً.</p>
|
||||
<?php else: ?>
|
||||
<div class="d-flex flex-wrap gap-3 <?= isset($errors['subjects']) ? 'is-invalid' : '' ?>">
|
||||
<?php foreach ($available_subjects as $subject): ?>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="subjects[]" value="<?= e((string) $subject['id']) ?>" id="subject_<?= e((string) $subject['id']) ?>" <?= in_array((string) $subject['id'], array_map('strval', $values['subjects'] ?? []), true) ? 'checked' : '' ?>>
|
||||
<label class="form-check-label" for="subject_<?= e((string) $subject['id']) ?>">
|
||||
<?= e((string) $subject['name']) ?>
|
||||
</label>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php if (isset($errors['subjects'])): ?><div class="invalid-feedback d-block"><?= e($errors['subjects']) ?></div><?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions d-flex flex-wrap gap-2 pt-2">
|
||||
<button class="btn btn-primary px-4" type="submit">إرسال الطلب</button>
|
||||
<a class="btn btn-outline-secondary px-4" href="applications.php">العودة إلى لوحة الطلبات</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="app-card sidebar-card mb-4 shadow-sm border-0">
|
||||
<div class="section-title mb-3">مسار هذه الصفحة</div>
|
||||
<div class="process-list">
|
||||
<div class="process-item active"><span>1</span><div><strong>إدخال البيانات</strong><small>تجميع بيانات الهوية والتشغيل في صفحة واحدة.</small></div></div>
|
||||
<div class="process-item"><span>2</span><div><strong>التحقق والحفظ</strong><small>الحقول الإلزامية تُراجع قبل إنشاء المرجع.</small></div></div>
|
||||
<div class="process-item"><span>3</span><div><strong>الانتقال للمراجعة</strong><small>يظهر الطلب فوراً داخل لوحة المشرف العام.</small></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-card sidebar-card mb-4 shadow-sm border-0">
|
||||
<div class="section-title mb-3">الحد الأدنى المطلوب</div>
|
||||
<ul class="check-list mb-0">
|
||||
<li>اسم المركز والمنطقة التشغيلية.</li>
|
||||
<li>المسؤول المباشر ووسائل التواصل.</li>
|
||||
<li>نوع المركز والفئة المستهدفة.</li>
|
||||
<li>الطاقة التشغيلية ومواعيد البرنامج.</li>
|
||||
<li>ملخص احتياجات التنفيذ أو الشراكات.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<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-2">
|
||||
<a href="applications.php" class="text-decoration-none">
|
||||
<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-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-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>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php render_page_end(); ?>
|
||||
374
center_assessment_criteria.php
Normal file
374
center_assessment_criteria.php
Normal file
@ -0,0 +1,374 @@
|
||||
<?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;
|
||||
$isApprovedCenter = $application && (string) ($application['status'] ?? '') === 'approved';
|
||||
$errors = [];
|
||||
$values = ['criteria' => []];
|
||||
$cycleContext = ['cycles' => [], 'selected' => null, 'active' => null, 'read_only' => false];
|
||||
$selectedCycle = null;
|
||||
$selectedCycleId = 0;
|
||||
$isCycleReadOnly = false;
|
||||
$cycleLabel = 'لا توجد دورة بعد';
|
||||
|
||||
$buildCenterAssessmentsUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0, array $extra = []): string {
|
||||
$params = [];
|
||||
if ($targetApplicationId > 0) {
|
||||
$params['id'] = $targetApplicationId;
|
||||
}
|
||||
if ($targetCycleId > 0) {
|
||||
$params['cycle'] = $targetCycleId;
|
||||
}
|
||||
foreach ($extra as $key => $value) {
|
||||
if ($value === '' || $value === null) {
|
||||
continue;
|
||||
}
|
||||
$params[$key] = $value;
|
||||
}
|
||||
|
||||
return 'center_assessments.php' . ($params !== [] ? '?' . http_build_query($params) : '');
|
||||
};
|
||||
|
||||
$buildCenterAssessmentCriteriaUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0, int $targetAssessmentId = 0, array $extra = []) : string {
|
||||
$params = [];
|
||||
if ($targetApplicationId > 0) {
|
||||
$params['id'] = $targetApplicationId;
|
||||
}
|
||||
if ($targetCycleId > 0) {
|
||||
$params['cycle'] = $targetCycleId;
|
||||
}
|
||||
if ($targetAssessmentId > 0) {
|
||||
$params['assessment_id'] = $targetAssessmentId;
|
||||
}
|
||||
foreach ($extra as $key => $value) {
|
||||
if ($value === '' || $value === null) {
|
||||
continue;
|
||||
}
|
||||
$params[$key] = $value;
|
||||
}
|
||||
|
||||
return 'center_assessment_criteria.php' . ($params !== [] ? '?' . http_build_query($params) : '');
|
||||
};
|
||||
|
||||
if ($isApprovedCenter) {
|
||||
$cycleContext = resolve_school_cycle_context((int) $application['id'], $application, $requestedCycleId);
|
||||
$selectedCycle = $cycleContext['selected'];
|
||||
$selectedCycleId = $selectedCycle ? (int) ($selectedCycle['id'] ?? 0) : 0;
|
||||
$isCycleReadOnly = (bool) ($cycleContext['read_only'] ?? false);
|
||||
$cycleLabel = $selectedCycle ? (string) ($selectedCycle['cycle_name'] ?? $cycleLabel) : $cycleLabel;
|
||||
}
|
||||
|
||||
$assessmentOptions = $isApprovedCenter && $selectedCycleId > 0
|
||||
? center_assessment_type_options_by_cycle((int) $application['id'], $selectedCycleId, false)
|
||||
: [];
|
||||
$selectedAssessmentId = $requestedAssessmentId;
|
||||
if ($selectedAssessmentId <= 0 && $assessmentOptions !== []) {
|
||||
$keys = array_keys($assessmentOptions);
|
||||
$selectedAssessmentId = (int) ($keys[0] ?? 0);
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $application) {
|
||||
$selectedAssessmentId = filter_input(INPUT_POST, 'assessment_id', FILTER_VALIDATE_INT) ?: $selectedAssessmentId;
|
||||
if (!$isApprovedCenter) {
|
||||
$errors['form'] = 'لا يمكن إعداد بنود تقييم المركز قبل اعتماد المركز.';
|
||||
} elseif ($selectedCycleId <= 0) {
|
||||
$errors['form'] = 'يرجى إنشاء دورة موسمية أولاً من صفحة المركز.';
|
||||
} elseif ($isCycleReadOnly) {
|
||||
$errors['form'] = 'هذه الدورة مؤرشفة للقراءة فقط. اختر دورة نشطة لتعديل البنود.';
|
||||
} elseif ($selectedAssessmentId <= 0 || !isset($assessmentOptions[$selectedAssessmentId])) {
|
||||
$errors['form'] = 'يرجى اختيار تقييم مركز صحيح أولاً.';
|
||||
} else {
|
||||
[$values, $errors] = validate_center_assessment_criteria_input((int) $application['id'], $selectedCycleId, $selectedAssessmentId, $_POST);
|
||||
if ($errors === []) {
|
||||
try {
|
||||
$savedRows = save_center_assessment_criteria_in_cycle((int) $application['id'], $selectedCycleId, $selectedAssessmentId, $values);
|
||||
set_flash('success', 'تم حفظ ' . $savedRows . ' بند/بنود لتقييم المركز.');
|
||||
header('Location: ' . $buildCenterAssessmentCriteriaUrl((int) $application['id'], $selectedCycleId, $selectedAssessmentId));
|
||||
exit;
|
||||
} catch (Throwable $exception) {
|
||||
$errors['form'] = 'تعذر حفظ البنود حالياً. يرجى المحاولة مرة أخرى.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$selectedAssessment = $selectedAssessmentId > 0 ? ($assessmentOptions[$selectedAssessmentId] ?? null) : null;
|
||||
$criteriaRows = $isApprovedCenter && $selectedCycleId > 0 && $selectedAssessmentId > 0
|
||||
? list_center_assessment_criteria_by_assessment((int) $application['id'], $selectedCycleId, $selectedAssessmentId, false)
|
||||
: [];
|
||||
$criteriaMetrics = $isApprovedCenter && $selectedCycleId > 0 && $selectedAssessmentId > 0
|
||||
? center_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 ? $buildCenterAssessmentsUrl((int) $application['id'], $selectedCycleId) : 'center_assessments.php';
|
||||
|
||||
if (!$application) {
|
||||
http_response_code(404);
|
||||
}
|
||||
|
||||
render_page_start($pageTitle, 'admin', $pageDescription);
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="py-4 py-lg-5">
|
||||
<div class="container-xxl">
|
||||
<div class="admin-layout row g-4 align-items-start">
|
||||
<div class="col-lg-3 layout-sidebar-column">
|
||||
<?php require __DIR__ . '/includes/sidebar.php'; ?>
|
||||
</div>
|
||||
<div class="col-lg-9 layout-content-column">
|
||||
<?php if (!$application): ?>
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">المركز غير موجود</div>
|
||||
<p class="text-muted mb-3">تحقق من الرابط أو ارجع إلى صفحة تقييم المراكز.</p>
|
||||
<a class="btn btn-primary" href="center_assessments.php">تقييم المراكز</a>
|
||||
</div>
|
||||
<?php elseif (!$isApprovedCenter): ?>
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">البنود تُفتح بعد الاعتماد</div>
|
||||
<p class="text-muted mb-0">اعتمد المركز أولاً حتى تتمكن من إعداد بنود تقييمه.</p>
|
||||
</div>
|
||||
<?php elseif ($selectedCycleId <= 0): ?>
|
||||
<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 elseif ($selectedAssessment === null): ?>
|
||||
<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 mb-4 mb-lg-5">
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-8">
|
||||
<span class="eyebrow mb-3">مرحلة 2 — بنود تقييم المراكز</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) ($application['center_name'] ?? '')) ?></span>
|
||||
<span><?= e($cycleLabel) ?></span>
|
||||
<span><?= e((string) ($selectedAssessment['category'] ?? '')) ?></span>
|
||||
<span>الوزن <?= e(rtrim(rtrim(number_format((float) ($selectedAssessment['weight_percentage'] ?? 0), 2, '.', ''), '0'), '.')) ?>%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="page-banner-panel h-100">
|
||||
<div class="mini-stat-label">التقييم المحدد</div>
|
||||
<div class="mini-stat-value"><?= e((string) $criteriaMetrics['active']) ?></div>
|
||||
<div class="mini-stat-copy mb-3">عدد البنود النشطة حالياً داخل هذا التقييم.</div>
|
||||
<div class="d-grid gap-2">
|
||||
<a class="btn btn-primary btn-sm" href="<?= e($assessmentsUrl) ?>">الرجوع إلى التقييمات</a>
|
||||
<?php if (!$isCycleReadOnly): ?>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" id="addCriterionRow">إضافة بند</button>
|
||||
<?php endif; ?>
|
||||
<span class="small text-muted">الدرجة الحالية لهذا التقييم: <strong><?= e(rtrim(rtrim(number_format((float) ($selectedAssessment['max_score'] ?? 0), 2, '.', ''), '0'), '.')) ?></strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4"><div class="app-card stat-tile"><div class="mini-stat-label">إجمالي البنود</div><div class="mini-stat-value"><?= e((string) $criteriaMetrics['total']) ?></div><div class="mini-stat-copy">كل البنود المحفوظة لهذا التقييم.</div></div></div>
|
||||
<div class="col-md-4"><div class="app-card stat-tile"><div class="mini-stat-label">البنود النشطة</div><div class="mini-stat-value"><?= e((string) $criteriaMetrics['active']) ?></div><div class="mini-stat-copy">هي التي تدخل في الرصد النهائي.</div></div></div>
|
||||
<div class="col-md-4"><div class="app-card stat-tile"><div class="mini-stat-label">مجموع البنود النشطة</div><div class="mini-stat-value"><?= e(rtrim(rtrim(number_format((float) $criteriaMetrics['active_max_score'], 2, '.', ''), '0'), '.')) ?></div><div class="mini-stat-copy">يحدّث الدرجة القصوى للتقييم تلقائياً.</div></div></div>
|
||||
</div>
|
||||
|
||||
<div class="app-card mb-4">
|
||||
<form method="get" class="row g-3 align-items-end">
|
||||
<input type="hidden" name="id" value="<?= e((string) $application['id']) ?>">
|
||||
<input type="hidden" name="cycle" value="<?= e((string) $selectedCycleId) ?>">
|
||||
<div class="col-lg-8">
|
||||
<label class="form-label" for="assessmentSelect">التقييم</label>
|
||||
<select class="form-select" id="assessmentSelect" name="assessment_id" onchange="this.form.submit()">
|
||||
<?php foreach ($assessmentOptions as $assessmentOption): ?>
|
||||
<option value="<?= e((string) ($assessmentOption['id'] ?? 0)) ?>" <?= (int) ($assessmentOption['id'] ?? 0) === $selectedAssessmentId ? 'selected' : '' ?>><?= e((string) ($assessmentOption['label'] ?? '')) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="section-subtle mb-2">الحالة</div>
|
||||
<div><?= assessment_active_badge((int) ($selectedAssessment['is_active'] ?? 0)) ?></div>
|
||||
</div>
|
||||
</form>
|
||||
</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="addCriterionRowSecondary">إضافة بند</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: 18%">الدرجة</th>
|
||||
<th style="width: 30%">ملاحظات</th>
|
||||
<th style="width: 12%">الحالة</th>
|
||||
<th style="width: 8%">الإجراء</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($values['criteria'] as $index => $criterion): ?>
|
||||
<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" type="text" name="criteria[<?= e((string) $index) ?>][title]" value="<?= e((string) ($criterion['title'] ?? '')) ?>" placeholder="مثال: الالتزام التشغيلي" <?= $isCycleReadOnly ? 'disabled' : '' ?>>
|
||||
<?php if (isset($errors['criteria_' . $index])): ?>
|
||||
<div class="text-danger small mt-1"><?= e($errors['criteria_' . $index]) ?></div>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<input class="form-control" type="number" step="0.01" min="0" max="1000" name="criteria[<?= e((string) $index) ?>][max_score]" value="<?= e((string) ($criterion['max_score'] ?? '')) ?>" placeholder="10" <?= $isCycleReadOnly ? 'disabled' : '' ?>>
|
||||
</td>
|
||||
<td>
|
||||
<input class="form-control" type="text" name="criteria[<?= e((string) $index) ?>][notes]" value="<?= e((string) ($criterion['notes'] ?? '')) ?>" placeholder="اختياري" <?= $isCycleReadOnly ? 'disabled' : '' ?>>
|
||||
</td>
|
||||
<td>
|
||||
<select class="form-select" name="criteria[<?= e((string) $index) ?>][is_active]" <?= $isCycleReadOnly ? 'disabled' : '' ?>>
|
||||
<option value="1" <?= (string) ($criterion['is_active'] ?? '1') === '1' ? 'selected' : '' ?>>نشط</option>
|
||||
<option value="0" <?= (string) ($criterion['is_active'] ?? '1') === '0' ? 'selected' : '' ?>>مخفي</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($isCycleReadOnly): ?>
|
||||
<span class="text-muted small">قراءة فقط</span>
|
||||
<?php elseif (!empty($criterion['id'])): ?>
|
||||
<span class="text-muted small">أوقفه</span>
|
||||
<?php else: ?>
|
||||
<button type="button" class="btn btn-sm btn-light icon-action" data-remove-row title="حذف البند" aria-label="حذف البند">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0A.5.5 0 0 1 8.5 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z"/><path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1 0-2H5.5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1H13.5a1 1 0 0 1 1 1ZM4 4v9a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4H4Zm2.5-2a.5.5 0 0 0-.5.5V3h4v-.5a.5.5 0 0 0-.5-.5h-3Z"/></svg>
|
||||
<span class="visually-hidden">حذف البند</span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<?php if (!$isCycleReadOnly): ?>
|
||||
<div class="d-flex justify-content-between align-items-center pt-3 border-top mt-3 flex-wrap gap-2">
|
||||
<div class="section-subtle">سيتم تحديث الدرجة النهائية لهذا التقييم تلقائياً إلى مجموع البنود النشطة.</div>
|
||||
<button class="btn btn-primary" type="submit">حفظ البنود</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php if ($application && !$isCycleReadOnly && $selectedAssessment): ?>
|
||||
<template id="criterionRowTemplate">
|
||||
<tr data-criterion-row data-existing="0">
|
||||
<td>
|
||||
<input type="hidden" data-row-field="id" value="0">
|
||||
<input class="form-control" type="text" data-row-field="title" placeholder="مثال: الالتزام التشغيلي">
|
||||
</td>
|
||||
<td>
|
||||
<input class="form-control" type="number" step="0.01" min="0" max="1000" data-row-field="max_score" placeholder="10">
|
||||
</td>
|
||||
<td>
|
||||
<input class="form-control" type="text" data-row-field="notes" placeholder="اختياري">
|
||||
</td>
|
||||
<td>
|
||||
<select class="form-select" data-row-field="is_active">
|
||||
<option value="1" selected>نشط</option>
|
||||
<option value="0">مخفي</option>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-light icon-action" data-remove-row title="حذف البند" aria-label="حذف البند">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm2.5 0A.5.5 0 0 1 8.5 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5Zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6Z"/><path d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1 0-2H5.5a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1H13.5a1 1 0 0 1 1 1ZM4 4v9a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4H4Zm2.5-2a.5.5 0 0 0-.5.5V3h4v-.5a.5.5 0 0 0-.5-.5h-3Z"/></svg>
|
||||
<span class="visually-hidden">حذف البند</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const buttons = [document.getElementById('addCriterionRow'), document.getElementById('addCriterionRowSecondary')].filter(Boolean);
|
||||
const tableBody = document.querySelector('#criteriaTable tbody');
|
||||
const template = document.getElementById('criterionRowTemplate');
|
||||
if (buttons.length === 0 || !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}]`;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
buttons.forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const fragment = template.content.cloneNode(true);
|
||||
tableBody.appendChild(fragment);
|
||||
wireRow(tableBody.lastElementChild);
|
||||
renumberRows();
|
||||
});
|
||||
});
|
||||
|
||||
renumberRows();
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
<?php render_page_end();
|
||||
785
center_assessment_print_sheet.php
Normal file
785
center_assessment_print_sheet.php
Normal file
@ -0,0 +1,785 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
function print_sheet_score_display(?float $value): string
|
||||
{
|
||||
if ($value === null) {
|
||||
return '—';
|
||||
}
|
||||
|
||||
return rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.');
|
||||
}
|
||||
|
||||
$applicationId = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT) ?: 0;
|
||||
$requestedCycleId = filter_input(INPUT_GET, 'cycle', FILTER_VALIDATE_INT) ?: 0;
|
||||
$requestedAssessmentId = filter_input(INPUT_GET, 'assessment_id', FILTER_VALIDATE_INT) ?: 0;
|
||||
$application = $applicationId > 0 ? get_application($applicationId) : null;
|
||||
$isApprovedCenter = $application && (string) ($application['status'] ?? '') === 'approved';
|
||||
$cycleContext = ['cycles' => [], 'selected' => null, 'active' => null, 'read_only' => false];
|
||||
$selectedCycle = null;
|
||||
$selectedCycleId = 0;
|
||||
$cycleLabel = 'لا توجد دورة بعد';
|
||||
|
||||
$buildCenterAssessmentsUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0): string {
|
||||
$params = [];
|
||||
if ($targetApplicationId > 0) {
|
||||
$params['id'] = $targetApplicationId;
|
||||
}
|
||||
if ($targetCycleId > 0) {
|
||||
$params['cycle'] = $targetCycleId;
|
||||
}
|
||||
return 'center_assessments.php' . ($params !== [] ? '?' . http_build_query($params) : '');
|
||||
};
|
||||
|
||||
$buildCenterAssessmentScoreUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0, int $targetAssessmentId = 0): string {
|
||||
$params = [];
|
||||
if ($targetApplicationId > 0) {
|
||||
$params['id'] = $targetApplicationId;
|
||||
}
|
||||
if ($targetCycleId > 0) {
|
||||
$params['cycle'] = $targetCycleId;
|
||||
}
|
||||
if ($targetAssessmentId > 0) {
|
||||
$params['assessment_id'] = $targetAssessmentId;
|
||||
}
|
||||
return 'center_assessment_score_sheet.php' . ($params !== [] ? '?' . http_build_query($params) : '');
|
||||
};
|
||||
|
||||
if (!$application || !$isApprovedCenter || $requestedCycleId <= 0 || $requestedAssessmentId <= 0) {
|
||||
set_flash('error', 'اختر مركزاً ودورة وتقييماً صحيحاً لفتح ورقة الطباعة.');
|
||||
header('Location: ' . $buildCenterAssessmentsUrl($applicationId, $requestedCycleId));
|
||||
exit;
|
||||
}
|
||||
|
||||
$cycleContext = resolve_school_cycle_context((int) $application['id'], $application, $requestedCycleId);
|
||||
$selectedCycle = $cycleContext['selected'];
|
||||
$selectedCycleId = $selectedCycle ? (int) ($selectedCycle['id'] ?? 0) : 0;
|
||||
$cycleLabel = $selectedCycle ? (string) ($selectedCycle['cycle_name'] ?? $cycleLabel) : $cycleLabel;
|
||||
|
||||
if ($selectedCycleId <= 0) {
|
||||
set_flash('error', 'تعذر تحديد الدورة المطلوبة لورقة الطباعة.');
|
||||
header('Location: ' . $buildCenterAssessmentsUrl((int) $application['id'], $requestedCycleId));
|
||||
exit;
|
||||
}
|
||||
|
||||
$selectedAssessment = null;
|
||||
foreach (list_center_assessments_by_cycle((int) $application['id'], $selectedCycleId) as $assessment) {
|
||||
if ((int) ($assessment['id'] ?? 0) === $requestedAssessmentId) {
|
||||
$selectedAssessment = $assessment;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$selectedAssessment) {
|
||||
set_flash('error', 'تعذر العثور على التقييم المطلوب لطباعة النموذج.');
|
||||
header('Location: ' . $buildCenterAssessmentsUrl((int) $application['id'], $selectedCycleId));
|
||||
exit;
|
||||
}
|
||||
|
||||
$criteria = list_center_assessment_criteria_by_assessment((int) $application['id'], $selectedCycleId, $requestedAssessmentId, true);
|
||||
$scoreBundle = center_assessment_score_bundle_by_assessment((int) $application['id'], $selectedCycleId, $requestedAssessmentId);
|
||||
$scoreRow = $scoreBundle['score'] ?? null;
|
||||
$criteriaScores = $scoreBundle['criteria_scores'] ?? [];
|
||||
$statusKey = center_assessment_normalize_status((string) ($scoreRow['status'] ?? 'pending'));
|
||||
$statusMeta = center_assessment_status_map()[$statusKey] ?? ['label' => 'غير محدد'];
|
||||
$totalScore = isset($scoreRow['score']) && $scoreRow['score'] !== null ? (float) $scoreRow['score'] : null;
|
||||
$maxScore = isset($scoreRow['max_score']) && $scoreRow['max_score'] !== null
|
||||
? (float) $scoreRow['max_score']
|
||||
: (float) ($selectedAssessment['max_score'] ?? 0);
|
||||
if ($criteria !== []) {
|
||||
$criteriaMax = 0.0;
|
||||
foreach ($criteria as $criterion) {
|
||||
$criteriaMax += (float) ($criterion['max_score'] ?? 0);
|
||||
}
|
||||
$maxScore = $criteriaMax > 0 ? $criteriaMax : $maxScore;
|
||||
}
|
||||
$percentage = $totalScore !== null && $maxScore > 0 ? round(($totalScore / $maxScore) * 100, 2) : null;
|
||||
$notes = trim((string) ($scoreRow['notes'] ?? ''));
|
||||
$assessmentDate = (string) ($scoreRow['assessed_on'] ?? '');
|
||||
$settings = get_app_settings();
|
||||
$projectName = (string) (!empty($settings['app_name']) ? $settings['app_name'] : project_name());
|
||||
$projectLogo = (string) ($settings['app_logo'] ?? '');
|
||||
$pageTitle = 'ورقة متابعة تقييم المركز — ' . (string) ($selectedAssessment['title'] ?? '');
|
||||
$pageDescription = 'نموذج رسمي قابل للطباعة لتوثيق تقييم المركز والتوقيعات وخطة المتابعة الإدارية.';
|
||||
$generatedAt = date('Y-m-d H:i');
|
||||
$scoreSheetUrl = $buildCenterAssessmentScoreUrl((int) $application['id'], $selectedCycleId, $requestedAssessmentId);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="ar" dir="rtl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title><?= e($pageTitle) ?> | <?= e($projectName) ?></title>
|
||||
<meta name="description" content="<?= e($pageDescription) ?>">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700;800&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--ink: #17324d;
|
||||
--ink-soft: #48637d;
|
||||
--line: #cfd9e5;
|
||||
--line-strong: #8da6be;
|
||||
--panel: #ffffff;
|
||||
--panel-soft: #f4f7fb;
|
||||
--accent: #0f4c81;
|
||||
--accent-soft: #e9f2fb;
|
||||
--success-soft: #edf7ef;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
background: #eef3f8;
|
||||
color: var(--ink);
|
||||
font-family: 'Cairo', system-ui, sans-serif;
|
||||
line-height: 1.65;
|
||||
}
|
||||
.screen-toolbar {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 1.25rem 1rem 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.toolbar-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.45rem;
|
||||
padding: 0.8rem 1.15rem;
|
||||
border-radius: 999px;
|
||||
text-decoration: none;
|
||||
border: 1px solid rgba(15, 76, 129, 0.18);
|
||||
background: rgba(255,255,255,0.92);
|
||||
color: var(--ink);
|
||||
font-weight: 700;
|
||||
box-shadow: 0 14px 35px rgba(15, 76, 129, 0.08);
|
||||
cursor: pointer;
|
||||
}
|
||||
.toolbar-btn.primary {
|
||||
background: linear-gradient(135deg, #0f4c81, #2f7bc4);
|
||||
color: #fff;
|
||||
border-color: transparent;
|
||||
}
|
||||
.sheet-wrap {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
.sheet {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 28px;
|
||||
box-shadow: 0 25px 60px rgba(17, 43, 70, 0.08);
|
||||
padding: 2rem;
|
||||
}
|
||||
.sheet-header {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 1.5rem;
|
||||
align-items: start;
|
||||
padding-bottom: 1.5rem;
|
||||
border-bottom: 2px solid var(--line);
|
||||
}
|
||||
.sheet-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
.sheet-brand img {
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
object-fit: contain;
|
||||
border-radius: 18px;
|
||||
border: 1px solid var(--line);
|
||||
padding: 0.5rem;
|
||||
background: #fff;
|
||||
}
|
||||
.brand-mark {
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
border-radius: 18px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #0f4c81, #2f7bc4);
|
||||
color: #fff;
|
||||
font-size: 1.8rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
.eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.3rem 0.75rem;
|
||||
border-radius: 999px;
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 0.35rem;
|
||||
font-size: 2rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.subhead {
|
||||
margin: 0;
|
||||
color: var(--ink-soft);
|
||||
font-size: 1rem;
|
||||
}
|
||||
.document-meta {
|
||||
min-width: 250px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 20px;
|
||||
background: var(--panel-soft);
|
||||
padding: 1rem 1.15rem;
|
||||
}
|
||||
.document-meta .meta-row,
|
||||
.info-grid .info-item,
|
||||
.summary-grid .summary-card {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
.meta-row + .meta-row { margin-top: 0.45rem; }
|
||||
.meta-label,
|
||||
.info-label,
|
||||
.summary-label {
|
||||
color: var(--ink-soft);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
.meta-value,
|
||||
.info-value,
|
||||
.summary-value {
|
||||
font-weight: 700;
|
||||
}
|
||||
.section {
|
||||
margin-top: 1.35rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 22px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
.section-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
padding: 0.95rem 1.2rem;
|
||||
background: linear-gradient(180deg, #f8fbff, #edf4fb);
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.section-title {
|
||||
margin: 0;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
.section-note {
|
||||
color: var(--ink-soft);
|
||||
font-size: 0.9rem;
|
||||
margin: 0;
|
||||
}
|
||||
.section-body { padding: 1.15rem 1.2rem 1.3rem; }
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.85rem 1rem;
|
||||
}
|
||||
.info-item {
|
||||
padding: 0.75rem 0.9rem;
|
||||
background: var(--panel-soft);
|
||||
border-radius: 16px;
|
||||
min-height: 56px;
|
||||
}
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 0.85rem;
|
||||
}
|
||||
.print-top-grid,
|
||||
.print-bottom-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
.summary-card {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
border-radius: 18px;
|
||||
border: 1px solid var(--line);
|
||||
background: linear-gradient(180deg, #fff, #f8fbff);
|
||||
min-height: 108px;
|
||||
}
|
||||
.summary-card.status-card { background: var(--success-soft); }
|
||||
.summary-value.big {
|
||||
font-size: 1.45rem;
|
||||
font-weight: 800;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid var(--line);
|
||||
padding: 0.8rem 0.7rem;
|
||||
vertical-align: top;
|
||||
text-align: right;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
thead th {
|
||||
background: #f3f7fb;
|
||||
font-weight: 800;
|
||||
}
|
||||
td.center, th.center { text-align: center; }
|
||||
.muted { color: var(--ink-soft); }
|
||||
.notes-box,
|
||||
.lines-box {
|
||||
min-height: 110px;
|
||||
border: 1px dashed var(--line-strong);
|
||||
border-radius: 18px;
|
||||
padding: 0.95rem 1rem;
|
||||
background: linear-gradient(180deg, #fff, #fbfdff);
|
||||
}
|
||||
.notes-box.empty::after {
|
||||
content: '................................................................................................................................................................................';
|
||||
color: var(--line-strong);
|
||||
word-break: break-word;
|
||||
line-height: 2;
|
||||
}
|
||||
.follow-up-table td {
|
||||
height: 54px;
|
||||
}
|
||||
.signature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
.signature-card {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 18px;
|
||||
padding: 1rem;
|
||||
background: #fff;
|
||||
min-height: 155px;
|
||||
}
|
||||
.signature-role {
|
||||
font-weight: 800;
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.signature-line {
|
||||
border-bottom: 1px solid var(--line-strong);
|
||||
margin: 1rem 0 0.45rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
.print-footnote {
|
||||
margin-top: 1.25rem;
|
||||
color: var(--ink-soft);
|
||||
font-size: 0.86rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.sheet-header,
|
||||
.info-grid,
|
||||
.summary-grid,
|
||||
.signature-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.document-meta { min-width: 0; }
|
||||
}
|
||||
@media print {
|
||||
@page { size: A4; margin: 8mm; }
|
||||
body {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
font-size: 11px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.screen-toolbar { display: none !important; }
|
||||
.sheet-wrap { max-width: none; padding: 0; }
|
||||
.sheet {
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.sheet-header {
|
||||
gap: 0.75rem;
|
||||
padding-bottom: 0.5rem;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.sheet-brand { gap: 0.7rem; }
|
||||
.sheet-brand img,
|
||||
.brand-mark {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 12px;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
.eyebrow {
|
||||
margin-bottom: 0.3rem;
|
||||
padding: 0.12rem 0.5rem;
|
||||
font-size: 0.66rem;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.15rem;
|
||||
margin-bottom: 0.1rem;
|
||||
}
|
||||
.subhead {
|
||||
font-size: 0.74rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.document-meta {
|
||||
min-width: 205px;
|
||||
padding: 0.5rem 0.65rem;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.meta-row + .meta-row { margin-top: 0.18rem; }
|
||||
.meta-label,
|
||||
.info-label,
|
||||
.summary-label,
|
||||
.section-note,
|
||||
.muted,
|
||||
.print-footnote {
|
||||
font-size: 0.68rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.print-top-grid {
|
||||
grid-template-columns: 1.25fr 0.95fr;
|
||||
gap: 0.45rem;
|
||||
margin-top: 0.45rem;
|
||||
}
|
||||
.print-bottom-grid {
|
||||
grid-template-columns: 0.95fr 1.05fr;
|
||||
gap: 0.45rem;
|
||||
margin-top: 0.45rem;
|
||||
}
|
||||
.section {
|
||||
margin-top: 0.45rem;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.section-head {
|
||||
padding: 0.45rem 0.65rem;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.section-body {
|
||||
padding: 0.5rem 0.65rem 0.6rem;
|
||||
}
|
||||
.info-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.35rem 0.45rem;
|
||||
}
|
||||
.info-item {
|
||||
min-height: 0;
|
||||
padding: 0.35rem 0.45rem;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.summary-grid {
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.summary-card {
|
||||
min-height: 0;
|
||||
padding: 0.45rem 0.5rem;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.summary-value.big {
|
||||
font-size: 1rem;
|
||||
margin-top: 0.05rem;
|
||||
}
|
||||
table { table-layout: fixed; }
|
||||
th, td {
|
||||
padding: 0.28rem 0.32rem;
|
||||
font-size: 0.68rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
td > div + div { margin-top: 0.08rem; }
|
||||
.notes-box,
|
||||
.lines-box {
|
||||
min-height: 54px;
|
||||
padding: 0.45rem 0.55rem;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.notes-box.empty::after {
|
||||
line-height: 1.55;
|
||||
font-size: 0.66rem;
|
||||
}
|
||||
.follow-up-table td {
|
||||
height: 28px;
|
||||
}
|
||||
.follow-up-table tbody tr:nth-child(n+3) {
|
||||
display: none;
|
||||
}
|
||||
.criterion-desc {
|
||||
display: none;
|
||||
}
|
||||
.signature-grid {
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.signature-card {
|
||||
min-height: 82px;
|
||||
padding: 0.45rem 0.5rem;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.signature-role {
|
||||
font-size: 0.74rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.signature-line {
|
||||
margin: 0.5rem 0 0.2rem;
|
||||
height: 0.65rem;
|
||||
}
|
||||
.print-footnote {
|
||||
margin-top: 0.45rem;
|
||||
}
|
||||
.section,
|
||||
.document-meta,
|
||||
.summary-card,
|
||||
.signature-card,
|
||||
.info-item,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
break-inside: avoid;
|
||||
}
|
||||
a { color: inherit; text-decoration: none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="screen-toolbar">
|
||||
<div>
|
||||
<div style="font-weight:800; font-size:1.05rem; color:#17324d;">ورقة تقييم قابلة للطباعة</div>
|
||||
<div style="color:#48637d; font-size:0.92rem;">نموذج رسمي للتوقيعات وخطة المتابعة الإدارية.</div>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<a class="toolbar-btn" href="<?= e($scoreSheetUrl) ?>">العودة إلى صفحة الرصد</a>
|
||||
<a class="toolbar-btn" href="<?= e($buildCenterAssessmentsUrl((int) $application['id'], $selectedCycleId)) ?>">كل تقييمات المركز</a>
|
||||
<button class="toolbar-btn primary" type="button" onclick="window.print()">طباعة / حفظ PDF</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sheet-wrap">
|
||||
<article class="sheet">
|
||||
<header class="sheet-header">
|
||||
<div>
|
||||
<div class="sheet-brand">
|
||||
<?php if ($projectLogo !== ''): ?>
|
||||
<img src="<?= e(asset_url($projectLogo)) ?>" alt="<?= e($projectName) ?>">
|
||||
<?php else: ?>
|
||||
<span class="brand-mark"><?= e(mb_substr($projectName, 0, 1)) ?></span>
|
||||
<?php endif; ?>
|
||||
<div>
|
||||
<div class="eyebrow">نموذج رسمي معتمد للمتابعة</div>
|
||||
<h1>ورقة تقييم ومتابعة إدارية للمركز</h1>
|
||||
<p class="subhead"><?= e($projectName) ?> — توثيق نتيجة التقييم وخطوات المتابعة والتوقيعات.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="document-meta">
|
||||
<div class="meta-row"><span class="meta-label">رقم المركز</span><span class="meta-value">#<?= e((string) $application['id']) ?></span></div>
|
||||
<div class="meta-row"><span class="meta-label">الدورة</span><span class="meta-value"><?= e($cycleLabel) ?></span></div>
|
||||
<div class="meta-row"><span class="meta-label">تاريخ التقييم</span><span class="meta-value"><?= e($assessmentDate !== '' ? $assessmentDate : '................') ?></span></div>
|
||||
<div class="meta-row"><span class="meta-label">تاريخ الطباعة</span><span class="meta-value"><?= e($generatedAt) ?></span></div>
|
||||
<div class="meta-row"><span class="meta-label">الحالة</span><span class="meta-value"><?= e((string) ($statusMeta['label'] ?? 'غير محدد')) ?></span></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="print-top-grid">
|
||||
<section class="section">
|
||||
<div class="section-head">
|
||||
<h2 class="section-title">بيانات المركز والتقييم</h2>
|
||||
<p class="section-note">يمكن اعتماد هذه الصفحة في الملف الورقي أو حفظها بصيغة PDF.</p>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="info-grid">
|
||||
<div class="info-item"><span class="info-label">اسم المركز</span><span class="info-value"><?= e((string) ($application['center_name'] ?? '')) ?></span></div>
|
||||
<div class="info-item"><span class="info-label">المدينة</span><span class="info-value"><?= e((string) (($application['city'] ?? '') !== '' ? $application['city'] : '—')) ?></span></div>
|
||||
<div class="info-item"><span class="info-label">مدير المركز</span><span class="info-value"><?= e((string) (($application['director_name'] ?? '') !== '' ? $application['director_name'] : '—')) ?></span></div>
|
||||
<div class="info-item"><span class="info-label">التقييم</span><span class="info-value"><?= e((string) ($selectedAssessment['title'] ?? '')) ?></span></div>
|
||||
<div class="info-item"><span class="info-label">الفئة</span><span class="info-value"><?= e((string) (($selectedAssessment['category'] ?? '') !== '' ? $selectedAssessment['category'] : '—')) ?></span></div>
|
||||
<div class="info-item"><span class="info-label">نوع المقياس</span><span class="info-value"><?= e((string) assessment_scale_type_label((string) ($selectedAssessment['scale_type'] ?? ''))) ?></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-head">
|
||||
<h2 class="section-title">ملخص النتيجة</h2>
|
||||
<p class="section-note">يعرض هذا القسم النتيجة الحالية إن كانت مرصودة، أو يترك مساحة للتعبئة اليدوية.</p>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="summary-grid">
|
||||
<div class="summary-card">
|
||||
<div class="summary-label">الدرجة المحققة</div>
|
||||
<div class="summary-value big"><?= e(print_sheet_score_display($totalScore)) ?></div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-label">الدرجة القصوى</div>
|
||||
<div class="summary-value big"><?= e(print_sheet_score_display($maxScore)) ?></div>
|
||||
</div>
|
||||
<div class="summary-card">
|
||||
<div class="summary-label">النسبة المئوية</div>
|
||||
<div class="summary-value big"><?= e($percentage !== null ? print_sheet_score_display($percentage) . '%' : '—') ?></div>
|
||||
</div>
|
||||
<div class="summary-card status-card">
|
||||
<div class="summary-label">وزن التقييم</div>
|
||||
<div class="summary-value big"><?= e(print_sheet_score_display((float) ($selectedAssessment['weight_percentage'] ?? 0))) ?>%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-head">
|
||||
<h2 class="section-title">تفاصيل البنود</h2>
|
||||
<p class="section-note"><?= $criteria !== [] ? 'تفصيل البنود النشطة لهذا التقييم.' : 'لا توجد بنود معرفة حالياً، لذا يظهر سطر موحد للتقييم العام.' ?></p>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="center" style="width:64px;">#</th>
|
||||
<th>البند / المعيار</th>
|
||||
<th class="center" style="width:120px;">الدرجة القصوى</th>
|
||||
<th class="center" style="width:140px;">الدرجة المرصودة</th>
|
||||
<th>ملاحظات مختصرة</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($criteria !== []): ?>
|
||||
<?php foreach ($criteria as $index => $criterion): ?>
|
||||
<?php
|
||||
$criterionId = (int) ($criterion['id'] ?? 0);
|
||||
$criterionScore = $criteriaScores[$criterionId] ?? null;
|
||||
$criterionNote = trim((string) ($criterionScore['notes'] ?? ''));
|
||||
?>
|
||||
<tr>
|
||||
<td class="center"><?= e((string) ($index + 1)) ?></td>
|
||||
<td>
|
||||
<div style="font-weight:700;"><?= e((string) ($criterion['title'] ?? '')) ?></div>
|
||||
<?php if (!empty($criterion['description'])): ?><div class="muted criterion-desc"><?= e((string) $criterion['description']) ?></div><?php endif; ?>
|
||||
</td>
|
||||
<td class="center"><?= e(print_sheet_score_display((float) ($criterion['max_score'] ?? 0))) ?></td>
|
||||
<td class="center"><?= e(isset($criterionScore['score']) && $criterionScore['score'] !== null ? print_sheet_score_display((float) $criterionScore['score']) : '................') ?></td>
|
||||
<td><?= e($criterionNote !== '' ? $criterionNote : '................................................................') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td class="center">1</td>
|
||||
<td><?= e((string) ($selectedAssessment['title'] ?? 'التقييم العام')) ?></td>
|
||||
<td class="center"><?= e(print_sheet_score_display($maxScore)) ?></td>
|
||||
<td class="center"><?= e($totalScore !== null ? print_sheet_score_display($totalScore) : '................') ?></td>
|
||||
<td>................................................................</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="print-bottom-grid">
|
||||
<section class="section">
|
||||
<div class="section-head">
|
||||
<h2 class="section-title">ملاحظات المقيم</h2>
|
||||
<p class="section-note">أبرز الملاحظات أو أسباب القرار أو التوصية العامة.</p>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="notes-box<?= $notes === '' ? ' empty' : '' ?>"><?= $notes !== '' ? nl2br(e($notes)) : '' ?></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-head">
|
||||
<h2 class="section-title">خطة المتابعة الإدارية</h2>
|
||||
<p class="section-note">تُعبّأ عند وجود ملاحظات تتطلب معالجة أو متابعة لاحقة.</p>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<table class="follow-up-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:48%;">الإجراء المطلوب</th>
|
||||
<th style="width:18%;">المسؤول</th>
|
||||
<th style="width:18%;">الموعد المستهدف</th>
|
||||
<th style="width:16%;">حالة المتابعة</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td></td><td></td><td></td><td></td></tr>
|
||||
<tr><td></td><td></td><td></td><td></td></tr>
|
||||
<tr><td></td><td></td><td></td><td></td></tr>
|
||||
<tr><td></td><td></td><td></td><td></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-head">
|
||||
<h2 class="section-title">التوقيعات والاعتماد</h2>
|
||||
<p class="section-note">للاستخدام الرسمي بين المقيم وإدارة المركز والإدارة المشرفة.</p>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="signature-grid">
|
||||
<div class="signature-card">
|
||||
<div class="signature-role">المقيم / المشرف</div>
|
||||
<div class="signature-line"></div>
|
||||
<div class="muted">الاسم والتوقيع</div>
|
||||
<div class="signature-line"></div>
|
||||
<div class="muted">التاريخ</div>
|
||||
</div>
|
||||
<div class="signature-card">
|
||||
<div class="signature-role">مدير المركز</div>
|
||||
<div class="signature-line"></div>
|
||||
<div class="muted">الاسم والتوقيع</div>
|
||||
<div class="signature-line"></div>
|
||||
<div class="muted">التاريخ</div>
|
||||
</div>
|
||||
<div class="signature-card">
|
||||
<div class="signature-role">اعتماد الإدارة</div>
|
||||
<div class="signature-line"></div>
|
||||
<div class="muted">الاسم والتوقيع</div>
|
||||
<div class="signature-line"></div>
|
||||
<div class="muted">التاريخ</div>
|
||||
</div>
|
||||
<div class="signature-card">
|
||||
<div class="signature-role">متابعة الإغلاق</div>
|
||||
<div class="signature-line"></div>
|
||||
<div class="muted">تمت المراجعة / أغلقت الملاحظات</div>
|
||||
<div class="signature-line"></div>
|
||||
<div class="muted">التاريخ</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="print-footnote">
|
||||
<span>هذه الورقة ناتجة من نظام إدارة المراكز ويمكن أرشفتها ضمن ملف المركز.</span>
|
||||
<span>مرجع التقييم: #<?= e((string) ($selectedAssessment['id'] ?? 0)) ?> — <?= e((string) ($selectedAssessment['title'] ?? '')) ?></span>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
234
center_assessment_report.php
Normal file
234
center_assessment_report.php
Normal file
@ -0,0 +1,234 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
function center_report_number(float $value): string
|
||||
{
|
||||
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;
|
||||
$application = $applicationId > 0 ? get_application($applicationId) : null;
|
||||
$isApprovedCenter = $application && (string) ($application['status'] ?? '') === 'approved';
|
||||
$cycleContext = ['cycles' => [], 'selected' => null, 'active' => null, 'read_only' => false];
|
||||
$selectedCycle = null;
|
||||
$selectedCycleId = 0;
|
||||
$cycleLabel = 'لا توجد دورة بعد';
|
||||
|
||||
$buildCenterAssessmentsUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0): string {
|
||||
$params = [];
|
||||
if ($targetApplicationId > 0) {
|
||||
$params['id'] = $targetApplicationId;
|
||||
}
|
||||
if ($targetCycleId > 0) {
|
||||
$params['cycle'] = $targetCycleId;
|
||||
}
|
||||
return 'center_assessments.php' . ($params !== [] ? '?' . http_build_query($params) : '');
|
||||
};
|
||||
|
||||
$buildCenterAssessmentCriteriaUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0, int $targetAssessmentId = 0): string {
|
||||
$params = [];
|
||||
if ($targetApplicationId > 0) {
|
||||
$params['id'] = $targetApplicationId;
|
||||
}
|
||||
if ($targetCycleId > 0) {
|
||||
$params['cycle'] = $targetCycleId;
|
||||
}
|
||||
if ($targetAssessmentId > 0) {
|
||||
$params['assessment_id'] = $targetAssessmentId;
|
||||
}
|
||||
return 'center_assessment_criteria.php' . ($params !== [] ? '?' . http_build_query($params) : '');
|
||||
};
|
||||
|
||||
$buildCenterAssessmentScoreUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0, int $targetAssessmentId = 0): string {
|
||||
$params = [];
|
||||
if ($targetApplicationId > 0) {
|
||||
$params['id'] = $targetApplicationId;
|
||||
}
|
||||
if ($targetCycleId > 0) {
|
||||
$params['cycle'] = $targetCycleId;
|
||||
}
|
||||
if ($targetAssessmentId > 0) {
|
||||
$params['assessment_id'] = $targetAssessmentId;
|
||||
}
|
||||
return 'center_assessment_score_sheet.php' . ($params !== [] ? '?' . http_build_query($params) : '');
|
||||
};
|
||||
|
||||
if ($applicationId <= 0 || $requestedCycleId <= 0) {
|
||||
set_flash('error', 'اختر المركز والدورة أولاً ثم افتح تقرير التقييم من شاشة تقييم المراكز.');
|
||||
header('Location: ' . $buildCenterAssessmentsUrl($applicationId, $requestedCycleId));
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($isApprovedCenter) {
|
||||
$cycleContext = resolve_school_cycle_context((int) $application['id'], $application, $requestedCycleId);
|
||||
$selectedCycle = $cycleContext['selected'];
|
||||
$selectedCycleId = $selectedCycle ? (int) ($selectedCycle['id'] ?? 0) : 0;
|
||||
$cycleLabel = $selectedCycle ? (string) ($selectedCycle['cycle_name'] ?? $cycleLabel) : $cycleLabel;
|
||||
}
|
||||
|
||||
$summary = $isApprovedCenter && $selectedCycleId > 0
|
||||
? center_assessment_summary_by_cycle((int) $application['id'], $selectedCycleId)
|
||||
: [
|
||||
'assessments' => [],
|
||||
'total_assessments' => 0,
|
||||
'active_assessments' => 0,
|
||||
'recorded_assessments' => 0,
|
||||
'completed_assessments' => 0,
|
||||
'pending_assessments' => 0,
|
||||
'waived_assessments' => 0,
|
||||
'missing_assessments' => 0,
|
||||
'overall_percentage' => 0.0,
|
||||
'score_total' => 0.0,
|
||||
'max_score_total' => 0.0,
|
||||
'latest_assessed_on' => '',
|
||||
'performance' => student_certificate_performance_meta(0.0),
|
||||
];
|
||||
|
||||
$pageTitle = $application && $isApprovedCenter
|
||||
? 'تقرير تقييم المراكز: ' . (string) ($application['center_name'] ?? '') . ($selectedCycle ? ' — ' . $cycleLabel : '')
|
||||
: 'تقرير تقييم المراكز';
|
||||
$pageDescription = 'ملخص مجمع لنتائج تقييم المركز داخل الدورة المختارة، مع حالة كل تقييم ونسبة الإنجاز الكلية.';
|
||||
|
||||
if (!$application && $applicationId > 0) {
|
||||
http_response_code(404);
|
||||
}
|
||||
|
||||
$assessmentsUrl = $application ? $buildCenterAssessmentsUrl((int) $application['id'], $selectedCycleId) : 'center_assessments.php';
|
||||
render_page_start($pageTitle, 'approved', $pageDescription, (string) ($application['favicon'] ?? ''));
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="py-4 py-lg-5">
|
||||
<div class="container-xxl">
|
||||
<div class="admin-layout row g-4 align-items-start">
|
||||
<div class="col-lg-3 layout-sidebar-column">
|
||||
<?php require __DIR__ . '/includes/sidebar.php'; ?>
|
||||
</div>
|
||||
<div class="col-lg-9 layout-content-column">
|
||||
<?php if (!$application): ?>
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">المركز غير موجود</div>
|
||||
<p class="text-muted mb-3">تحقق من الرابط أو ارجع إلى قائمة المراكز المعتمدة.</p>
|
||||
<a class="btn btn-primary" href="applications.php?status=approved">المراكز المعتمدة</a>
|
||||
</div>
|
||||
<?php elseif (!$isApprovedCenter): ?>
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">التقرير يُفتح بعد الاعتماد</div>
|
||||
<p class="text-muted mb-3">اعتمد المركز أولاً حتى يظهر تقرير التقييم الإشرافي.</p>
|
||||
<a class="btn btn-outline-secondary" href="application_detail.php?id=<?= e((string) $application['id']) ?>">ملف الاعتماد</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="page-banner approved-hero mb-4">
|
||||
<div class="row g-4 align-items-center">
|
||||
<div class="col-lg-8">
|
||||
<span class="approved-kicker mb-3">تقرير الدورة</span>
|
||||
<h1 class="page-title mb-3">ملخص تقييم المركز</h1>
|
||||
<p class="page-copy mb-3">هذا التقرير يجمع حالة كل تقييم إشرافي للمركز داخل دورة <strong><?= e($cycleLabel) ?></strong> ويعرض النسبة الكلية الحالية.</p>
|
||||
<div class="hero-meta">
|
||||
<span><?= e((string) ($summary['active_assessments'] ?? 0)) ?> تقييمات نشطة</span>
|
||||
<span><?= e(center_report_number((float) ($summary['overall_percentage'] ?? 0))) ?>%</span>
|
||||
<span><?= e((string) (($summary['performance']['label_ar'] ?? ''))) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="app-card h-100">
|
||||
<div class="section-title mb-2">إجراءات سريعة</div>
|
||||
<div class="cta-stack">
|
||||
<a class="btn btn-outline-secondary" href="<?= e($assessmentsUrl) ?>">إدارة التقييمات</a>
|
||||
<?php if (!empty($summary['assessments'][0]['id'])): ?>
|
||||
<a class="btn btn-primary" href="<?= e($buildCenterAssessmentScoreUrl((int) $application['id'], $selectedCycleId, (int) $summary['assessments'][0]['id'])) ?>">فتح أول رصد</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($selectedCycleId <= 0): ?>
|
||||
<div class="app-card"><div class="alert alert-warning mb-0">لا توجد دورة متاحة لهذا المركز بعد. أنشئ دورة أولاً من صفحة المركز.</div></div>
|
||||
<?php elseif ($summary['assessments'] === []): ?>
|
||||
<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="row g-3 mb-4">
|
||||
<div class="col-md-3"><div class="app-card h-100"><div class="section-title mb-2">النسبة الكلية</div><div class="display-6 mb-1"><?= e(center_report_number((float) ($summary['overall_percentage'] ?? 0))) ?>%</div><div class="section-subtle"><?= e((string) (($summary['performance']['label_ar'] ?? ''))) ?></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) ($summary['recorded_assessments'] ?? 0)) ?></div><div class="section-subtle">من <?= e((string) ($summary['active_assessments'] ?? 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"><?= e((string) ($summary['completed_assessments'] ?? 0)) ?></div><div class="section-subtle">المؤجل <?= e((string) ($summary['pending_assessments'] ?? 0)) ?></div></div></div>
|
||||
<div class="col-md-3"><div class="app-card h-100"><div class="section-title mb-2">آخر تحديث</div><div class="display-6 mb-1" style="font-size:1.05rem;"><?= e((string) (($summary['latest_assessed_on'] ?: '—'))) ?></div><div class="section-subtle">آخر تاريخ رصد</div></div></div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-light border mb-4">
|
||||
<strong>طريقة الحساب الحالية:</strong>
|
||||
مجموع الدرجات المكتملة <?= e(center_report_number((float) ($summary['score_total'] ?? 0))) ?>
|
||||
من أصل <?= e(center_report_number((float) ($summary['max_score_total'] ?? 0))) ?>
|
||||
للتقييمات المكتملة فقط.
|
||||
</div>
|
||||
|
||||
<div class="app-card">
|
||||
<div class="table-responsive">
|
||||
<table class="table app-table align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>التقييم</th>
|
||||
<th>الحالة</th>
|
||||
<th>النتيجة</th>
|
||||
<th>النسبة</th>
|
||||
<th>البنود</th>
|
||||
<th>آخر رصد</th>
|
||||
<th>الإجراء</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($summary['assessments'] as $assessment): ?>
|
||||
<?php
|
||||
$assessmentId = (int) ($assessment['id'] ?? 0);
|
||||
$scoreText = '—';
|
||||
if (($assessment['status'] ?? '') === 'completed' && isset($assessment['score']) && $assessment['score'] !== null) {
|
||||
$scoreText = center_report_number((float) $assessment['score']) . ' / ' . center_report_number((float) ($assessment['saved_max_score'] ?? 0));
|
||||
} elseif (($assessment['status'] ?? '') === 'missing') {
|
||||
$scoreText = 'غير مرصود';
|
||||
}
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-semibold"><?= e((string) ($assessment['title'] ?? '')) ?></div>
|
||||
<div class="text-muted small"><?= e((string) ($assessment['category'] ?? '')) ?> — <?= e(center_report_number((float) ($assessment['weight_percentage'] ?? 0))) ?>٪</div>
|
||||
</td>
|
||||
<td>
|
||||
<?php if (($assessment['status'] ?? '') === 'missing'): ?>
|
||||
<span class="text-muted">غير مرصود</span>
|
||||
<?php else: ?>
|
||||
<?= center_assessment_status_badge((string) ($assessment['status'] ?? 'pending')) ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= e($scoreText) ?></td>
|
||||
<td><?= e(isset($assessment['percentage']) && $assessment['percentage'] !== null ? center_report_number((float) $assessment['percentage']) . '%' : '—') ?></td>
|
||||
<td><?= e((string) ($assessment['criteria_count'] ?? 0)) ?></td>
|
||||
<td>
|
||||
<div><?= e((string) (($assessment['assessed_on'] ?? '') !== '' ? $assessment['assessed_on'] : '—')) ?></div>
|
||||
<?php if (!empty($assessment['notes'])): ?><div class="text-muted small"><?= e((string) $assessment['notes']) ?></div><?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a class="btn btn-primary btn-sm" href="<?= e($buildCenterAssessmentScoreUrl((int) $application['id'], $selectedCycleId, $assessmentId)) ?>">رصد</a>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="<?= e($buildCenterAssessmentCriteriaUrl((int) $application['id'], $selectedCycleId, $assessmentId)) ?>">البنود</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php render_page_end();
|
||||
520
center_assessment_score_sheet.php
Normal file
520
center_assessment_score_sheet.php
Normal file
@ -0,0 +1,520 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
function center_score_display(?float $value): string
|
||||
{
|
||||
if ($value === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.');
|
||||
}
|
||||
|
||||
$flash = consume_flash();
|
||||
$applicationId = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT) ?: 0;
|
||||
$requestedCycleId = filter_input(INPUT_GET, 'cycle', FILTER_VALIDATE_INT) ?: 0;
|
||||
$requestedAssessmentId = filter_input(INPUT_GET, 'assessment_id', FILTER_VALIDATE_INT) ?: 0;
|
||||
$application = $applicationId > 0 ? get_application($applicationId) : null;
|
||||
$isApprovedCenter = $application && (string) ($application['status'] ?? '') === 'approved';
|
||||
$errors = [];
|
||||
$cycleContext = ['cycles' => [], 'selected' => null, 'active' => null, 'read_only' => false];
|
||||
$selectedCycle = null;
|
||||
$selectedCycleId = 0;
|
||||
$isCycleReadOnly = false;
|
||||
$cycleLabel = 'لا توجد دورة بعد';
|
||||
$values = [
|
||||
'assessment_type_id' => '',
|
||||
'assessed_on' => date('Y-m-d'),
|
||||
'status' => 'completed',
|
||||
'assessment_max_score' => 0.0,
|
||||
'has_criteria' => false,
|
||||
'criteria' => [],
|
||||
'criteria_scores' => [],
|
||||
'score' => null,
|
||||
'score_raw' => '',
|
||||
'notes' => '',
|
||||
'should_save' => false,
|
||||
];
|
||||
|
||||
$buildCenterAssessmentsUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0): string {
|
||||
$params = [];
|
||||
if ($targetApplicationId > 0) {
|
||||
$params['id'] = $targetApplicationId;
|
||||
}
|
||||
if ($targetCycleId > 0) {
|
||||
$params['cycle'] = $targetCycleId;
|
||||
}
|
||||
return 'center_assessments.php' . ($params !== [] ? '?' . http_build_query($params) : '');
|
||||
};
|
||||
|
||||
$buildCenterAssessmentCriteriaUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0, int $targetAssessmentId = 0): string {
|
||||
$params = [];
|
||||
if ($targetApplicationId > 0) {
|
||||
$params['id'] = $targetApplicationId;
|
||||
}
|
||||
if ($targetCycleId > 0) {
|
||||
$params['cycle'] = $targetCycleId;
|
||||
}
|
||||
if ($targetAssessmentId > 0) {
|
||||
$params['assessment_id'] = $targetAssessmentId;
|
||||
}
|
||||
return 'center_assessment_criteria.php' . ($params !== [] ? '?' . http_build_query($params) : '');
|
||||
};
|
||||
|
||||
$buildCenterAssessmentScoreUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0, int $targetAssessmentId = 0): string {
|
||||
$params = [];
|
||||
if ($targetApplicationId > 0) {
|
||||
$params['id'] = $targetApplicationId;
|
||||
}
|
||||
if ($targetCycleId > 0) {
|
||||
$params['cycle'] = $targetCycleId;
|
||||
}
|
||||
if ($targetAssessmentId > 0) {
|
||||
$params['assessment_id'] = $targetAssessmentId;
|
||||
}
|
||||
return 'center_assessment_score_sheet.php' . ($params !== [] ? '?' . http_build_query($params) : '');
|
||||
};
|
||||
|
||||
$buildCenterAssessmentReportUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0): string {
|
||||
$params = [];
|
||||
if ($targetApplicationId > 0) {
|
||||
$params['id'] = $targetApplicationId;
|
||||
}
|
||||
if ($targetCycleId > 0) {
|
||||
$params['cycle'] = $targetCycleId;
|
||||
}
|
||||
return 'center_assessment_report.php' . ($params !== [] ? '?' . http_build_query($params) : '');
|
||||
};
|
||||
|
||||
$buildCenterAssessmentPrintUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0, int $targetAssessmentId = 0): string {
|
||||
$params = [];
|
||||
if ($targetApplicationId > 0) {
|
||||
$params['id'] = $targetApplicationId;
|
||||
}
|
||||
if ($targetCycleId > 0) {
|
||||
$params['cycle'] = $targetCycleId;
|
||||
}
|
||||
if ($targetAssessmentId > 0) {
|
||||
$params['assessment_id'] = $targetAssessmentId;
|
||||
}
|
||||
return 'center_assessment_print_sheet.php' . ($params !== [] ? '?' . http_build_query($params) : '');
|
||||
};
|
||||
|
||||
if ($applicationId <= 0 || $requestedCycleId <= 0) {
|
||||
set_flash('error', 'اختر المركز والدورة أولاً ثم افتح صفحة رصد التقييم من شاشة تقييم المراكز.');
|
||||
header('Location: ' . $buildCenterAssessmentsUrl($applicationId, $requestedCycleId));
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($isApprovedCenter) {
|
||||
$cycleContext = resolve_school_cycle_context((int) $application['id'], $application, $requestedCycleId);
|
||||
$selectedCycle = $cycleContext['selected'];
|
||||
$selectedCycleId = $selectedCycle ? (int) ($selectedCycle['id'] ?? 0) : 0;
|
||||
$isCycleReadOnly = (bool) ($cycleContext['read_only'] ?? false);
|
||||
$cycleLabel = $selectedCycle ? (string) ($selectedCycle['cycle_name'] ?? $cycleLabel) : $cycleLabel;
|
||||
}
|
||||
|
||||
$assessmentOptions = $isApprovedCenter && $selectedCycleId > 0
|
||||
? center_assessment_type_options_by_cycle((int) $application['id'], $selectedCycleId, false)
|
||||
: [];
|
||||
$selectedAssessmentId = $requestedAssessmentId;
|
||||
if ($selectedAssessmentId <= 0 && $assessmentOptions !== []) {
|
||||
$keys = array_keys($assessmentOptions);
|
||||
$selectedAssessmentId = (int) ($keys[0] ?? 0);
|
||||
}
|
||||
if ($selectedAssessmentId > 0) {
|
||||
$values['assessment_type_id'] = (string) $selectedAssessmentId;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $application) {
|
||||
if (!$isApprovedCenter) {
|
||||
$errors['form'] = 'لا يمكن رصد تقييم المركز قبل اعتماد المركز.';
|
||||
} elseif ($selectedCycleId <= 0) {
|
||||
$errors['form'] = 'يرجى إنشاء دورة موسمية أولاً من صفحة المركز.';
|
||||
} elseif ($isCycleReadOnly) {
|
||||
$errors['form'] = 'هذه الدورة مؤرشفة للقراءة فقط. اختر دورة نشطة لإدخال تقييم جديد.';
|
||||
} else {
|
||||
[$values, $errors, $selectedAssessmentMeta] = validate_center_assessment_score_input((int) $application['id'], $selectedCycleId, $_POST);
|
||||
$selectedAssessmentId = (int) ($values['assessment_type_id'] ?? 0);
|
||||
if ($errors === []) {
|
||||
try {
|
||||
save_center_assessment_score_in_cycle((int) $application['id'], $selectedCycleId, $values);
|
||||
set_flash('success', 'تم حفظ رصد تقييم المركز بنجاح.');
|
||||
$returnUrl = filter_input(INPUT_GET, 'return_url', FILTER_SANITIZE_URL); header('Location: ' . ($returnUrl ?: $buildCenterAssessmentScoreUrl((int) $application['id'], $selectedCycleId, $selectedAssessmentId)));
|
||||
exit;
|
||||
} catch (Throwable $exception) {
|
||||
$errors['form'] = 'تعذر حفظ الرصد حالياً. يرجى المحاولة مرة أخرى.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$selectedAssessment = $selectedAssessmentId > 0 ? ($assessmentOptions[$selectedAssessmentId] ?? null) : null;
|
||||
$criteria = $isApprovedCenter && $selectedCycleId > 0 && $selectedAssessmentId > 0
|
||||
? list_center_assessment_criteria_by_assessment((int) $application['id'], $selectedCycleId, $selectedAssessmentId, true)
|
||||
: [];
|
||||
$hasCriteria = $criteria !== [];
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$values['has_criteria'] = $hasCriteria;
|
||||
$values['criteria'] = $criteria;
|
||||
if ($hasCriteria) {
|
||||
$values['assessment_max_score'] = round(array_reduce($criteria, static function (float $carry, array $criterion): float {
|
||||
return $carry + (float) ($criterion['max_score'] ?? 0);
|
||||
}, 0.0), 2);
|
||||
} elseif ($selectedAssessment) {
|
||||
$values['assessment_max_score'] = (float) ($selectedAssessment['max_score'] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
$existingBundle = $isApprovedCenter && $selectedCycleId > 0 && $selectedAssessmentId > 0
|
||||
? center_assessment_score_bundle_by_assessment((int) $application['id'], $selectedCycleId, $selectedAssessmentId)
|
||||
: ['score' => null, 'criteria_scores' => []];
|
||||
$existingScore = $existingBundle['score'] ?? null;
|
||||
$existingCriteriaScores = $existingBundle['criteria_scores'] ?? [];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' && is_array($existingScore) && $existingScore !== []) {
|
||||
if (!empty($existingScore['assessed_on'])) {
|
||||
$values['assessed_on'] = (string) $existingScore['assessed_on'];
|
||||
}
|
||||
$values['status'] = center_assessment_normalize_status((string) ($existingScore['status'] ?? 'completed'));
|
||||
$values['notes'] = (string) ($existingScore['notes'] ?? '');
|
||||
|
||||
if ($hasCriteria) {
|
||||
foreach ($criteria as $criterion) {
|
||||
$criterionId = (int) ($criterion['id'] ?? 0);
|
||||
if ($criterionId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$existingCriterion = $existingCriteriaScores[$criterionId] ?? [];
|
||||
$values['criteria_scores'][$criterionId] = [
|
||||
'criterion_id' => $criterionId,
|
||||
'score' => isset($existingCriterion['score']) ? (float) $existingCriterion['score'] : null,
|
||||
'score_raw' => isset($existingCriterion['score']) && $existingCriterion['score'] !== null
|
||||
? center_score_display((float) $existingCriterion['score'])
|
||||
: '',
|
||||
'max_score' => (float) ($criterion['max_score'] ?? 0),
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$values['score'] = isset($existingScore['score']) ? (float) $existingScore['score'] : null;
|
||||
$values['score_raw'] = isset($existingScore['score']) && $existingScore['score'] !== null
|
||||
? center_score_display((float) $existingScore['score'])
|
||||
: '';
|
||||
}
|
||||
}
|
||||
|
||||
$criteriaMetrics = $isApprovedCenter && $selectedCycleId > 0 && $selectedAssessmentId > 0
|
||||
? center_assessment_criteria_metrics((int) $application['id'], $selectedCycleId, $selectedAssessmentId)
|
||||
: ['total' => 0, 'active' => 0, 'active_max_score' => 0.0];
|
||||
$scoreMetrics = $isApprovedCenter && $selectedCycleId > 0 && $selectedAssessmentId > 0
|
||||
? center_assessment_score_metrics_by_cycle((int) $application['id'], $selectedCycleId, $selectedAssessmentId)
|
||||
: ['total' => 0, 'completed' => 0, 'pending' => 0, 'waived' => 0, 'average_score' => 0.0, 'latest_date' => ''];
|
||||
|
||||
$pageTitle = $application && $isApprovedCenter
|
||||
? 'رصد تقييم المركز: ' . (string) ($application['center_name'] ?? '') . ($selectedAssessment ? ' — ' . (string) ($selectedAssessment['title'] ?? '') : '')
|
||||
: 'رصد تقييم المركز';
|
||||
$pageDescription = 'إدخال درجة تقييم المركز نفسه داخل الدورة المختارة، مع دعم البنود التفصيلية والتقرير النهائي.';
|
||||
|
||||
if (!$application && $applicationId > 0) {
|
||||
http_response_code(404);
|
||||
}
|
||||
|
||||
$assessmentsUrl = $application ? $buildCenterAssessmentsUrl((int) $application['id'], $selectedCycleId) : 'center_assessments.php';
|
||||
$criteriaUrl = $application && $selectedAssessmentId > 0 ? $buildCenterAssessmentCriteriaUrl((int) $application['id'], $selectedCycleId, $selectedAssessmentId) : 'center_assessment_criteria.php';
|
||||
$reportUrl = $application ? $buildCenterAssessmentReportUrl((int) $application['id'], $selectedCycleId) : 'center_assessment_report.php';
|
||||
$maxScoreLabel = center_score_display((float) ($values['assessment_max_score'] ?? 0));
|
||||
$existingStatus = is_array($existingScore) ? center_assessment_normalize_status((string) ($existingScore['status'] ?? 'pending')) : '';
|
||||
$existingPercentage = (is_array($existingScore) && isset($existingScore['score']) && $existingScore['score'] !== null && (float) ($existingScore['max_score'] ?? 0) > 0)
|
||||
? round(((float) $existingScore['score'] / (float) $existingScore['max_score']) * 100, 2)
|
||||
: null;
|
||||
|
||||
render_page_start($pageTitle, 'approved', $pageDescription, (string) ($application['favicon'] ?? ''));
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="py-4 py-lg-5">
|
||||
<div class="container-xxl">
|
||||
<div class="admin-layout row g-4 align-items-start">
|
||||
<div class="col-lg-3 layout-sidebar-column">
|
||||
<?php require __DIR__ . '/includes/sidebar.php'; ?>
|
||||
</div>
|
||||
<div class="col-lg-9 layout-content-column">
|
||||
<?php if (!$application): ?>
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">المركز غير موجود</div>
|
||||
<p class="text-muted mb-3">تحقق من الرابط أو ارجع إلى قائمة المراكز المعتمدة.</p>
|
||||
<a class="btn btn-primary" href="applications.php?status=approved">المراكز المعتمدة</a>
|
||||
</div>
|
||||
<?php elseif (!$isApprovedCenter): ?>
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">الرصد يُفتح بعد الاعتماد</div>
|
||||
<p class="text-muted mb-3">اعتمد المركز أولاً حتى يظهر رصد تقييمه الإشرافي.</p>
|
||||
<a class="btn btn-outline-secondary" href="application_detail.php?id=<?= e((string) $application['id']) ?>">ملف الاعتماد</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="page-banner approved-hero mb-4">
|
||||
<div class="row g-4 align-items-center">
|
||||
<div class="col-lg-8">
|
||||
<span class="approved-kicker mb-3">رصد المركز</span>
|
||||
<h1 class="page-title mb-3">إدخال تقييم إشرافي للمركز</h1>
|
||||
<p class="page-copy mb-3">اختر التقييم، ثم أدخل النتيجة مباشرة أو عبر البنود التفصيلية داخل دورة <strong><?= e($cycleLabel) ?></strong>.</p>
|
||||
<div class="hero-meta">
|
||||
<span><?= e((string) count($assessmentOptions)) ?> تقييمات متاحة</span>
|
||||
<span><?= e($cycleLabel) ?></span>
|
||||
<?php if ($selectedAssessment): ?><span><?= e((string) ($selectedAssessment['title'] ?? '')) ?></span><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="app-card h-100">
|
||||
<div class="section-title mb-2">تنقّل سريع</div>
|
||||
<div class="cta-stack">
|
||||
<?php if (!empty($_GET['return_url'])): ?>
|
||||
<a class="btn btn-outline-primary" href="<?= e($_GET['return_url']) ?>">العودة للقائمة</a>
|
||||
<?php endif; ?>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($assessmentsUrl) ?>">كل التقييمات</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($reportUrl) ?>">تقرير الدورة</a>
|
||||
<?php if ($selectedAssessment): ?>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($buildCenterAssessmentPrintUrl((int) $application['id'], $selectedCycleId, (int) ($selectedAssessment['id'] ?? 0))) ?>" target="_blank" rel="noopener">طباعة النموذج الرسمي</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($criteriaUrl) ?>">إدارة البنود</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($selectedCycleId <= 0): ?>
|
||||
<div class="app-card">
|
||||
<div class="alert alert-warning mb-0">لا توجد دورة متاحة لهذا المركز بعد. أنشئ دورة أولاً من صفحة المركز.</div>
|
||||
</div>
|
||||
<?php elseif ($assessmentOptions === []): ?>
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">لا توجد تقييمات مراكز جاهزة للرصد</div>
|
||||
<p class="text-muted mb-3">أضف نوع تقييم أولاً من صفحة تقييم المراكز ثم ارجع هنا لإدخال الدرجة.</p>
|
||||
<a class="btn btn-primary" href="<?= e($assessmentsUrl) ?>">إضافة تقييم</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="app-card mb-4">
|
||||
<form method="get" class="row g-2 align-items-center">
|
||||
<input type="hidden" name="id" value="<?= e((string) $application['id']) ?>">
|
||||
<input type="hidden" name="cycle" value="<?= e((string) $selectedCycleId) ?>">
|
||||
<?php if (!empty($_GET['return_url'])): ?><input type="hidden" name="return_url" value="<?= e($_GET['return_url']) ?>"><?php endif; ?>
|
||||
<div class="col-md-8">
|
||||
<label class="form-label" for="assessment_id">التقييم</label>
|
||||
<select class="form-select" name="assessment_id" id="assessment_id" onchange="this.form.submit()">
|
||||
<?php foreach ($assessmentOptions as $assessmentId => $assessment): ?>
|
||||
<option value="<?= e((string) $assessmentId) ?>" <?= $selectedAssessmentId === (int) $assessmentId ? 'selected' : '' ?>><?= e((string) ($assessment['label'] ?? '')) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 d-grid">
|
||||
<button type="submit" class="btn btn-primary">فتح الرصد</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php if ($selectedAssessment): ?>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-3"><div class="app-card h-100"><div class="section-title mb-2">الدرجة النهائية</div><div class="display-6 mb-1"><?= e($maxScoreLabel !== '' ? $maxScoreLabel : '0') ?></div><div class="section-subtle">المجموع المعتمد</div></div></div>
|
||||
<div class="col-md-3"><div class="app-card h-100"><div class="section-title mb-2">البنود النشطة</div><div class="display-6 mb-1"><?= e((string) ($criteriaMetrics['active'] ?? 0)) ?></div><div class="section-subtle">من أصل <?= e((string) ($criteriaMetrics['total'] ?? 0)) ?></div></div></div>
|
||||
<div class="col-md-3"><div class="app-card h-100"><div class="section-title mb-2">حالة الرصد</div><div class="display-6 mb-1" style="font-size:1.05rem;"><?= $existingStatus !== '' ? center_assessment_status_badge($existingStatus) : '<span class="text-muted">غير مرصود</span>' ?></div><div class="section-subtle">آخر حفظ <?= e($scoreMetrics['latest_date'] !== '' ? (string) $scoreMetrics['latest_date'] : '—') ?></div></div></div>
|
||||
<div class="col-md-3"><div class="app-card h-100"><div class="section-title mb-2">آخر نسبة</div><div class="display-6 mb-1"><?= e($existingPercentage !== null ? center_score_display((float) $existingPercentage) . '%' : '—') ?></div><div class="section-subtle">للتقييم الحالي</div></div></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($errors['form'])): ?>
|
||||
<div class="alert alert-danger mb-4"><?= e($errors['form']) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($isCycleReadOnly): ?>
|
||||
<div class="alert alert-warning mb-4">هذه الدورة مؤرشفة، لذلك الصفحة معروضة للقراءة فقط.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($selectedAssessment): ?>
|
||||
<div class="app-card">
|
||||
<form method="post" novalidate>
|
||||
<input type="hidden" name="assessment_type_id" value="<?= e((string) $selectedAssessmentId) ?>">
|
||||
<div class="row g-3 align-items-end mb-4">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="assessed_on">تاريخ الرصد</label>
|
||||
<input type="date" class="form-control <?= isset($errors['assessed_on']) ? 'is-invalid' : '' ?>" id="assessed_on" name="assessed_on" value="<?= e((string) $values['assessed_on']) ?>" <?= $isCycleReadOnly ? 'disabled' : '' ?>>
|
||||
<?php if (isset($errors['assessed_on'])): ?><div class="invalid-feedback"><?= e($errors['assessed_on']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="status">الحالة</label>
|
||||
<select class="form-select" id="status" name="status" data-center-status <?= $isCycleReadOnly ? 'disabled' : '' ?>>
|
||||
<?php foreach (center_assessment_status_map() as $statusKey => $statusMeta): ?>
|
||||
<option value="<?= e($statusKey) ?>" <?= (string) $values['status'] === (string) $statusKey ? 'selected' : '' ?>><?= e((string) $statusMeta['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="school-data-item h-100">
|
||||
<strong>الوزن</strong>
|
||||
<span><?= e(center_score_display((float) ($selectedAssessment['weight_percentage'] ?? 0)) ?: '0') ?>٪</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($errors['score'])): ?>
|
||||
<div class="alert alert-danger mb-4"><?= e($errors['score']) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($hasCriteria): ?>
|
||||
<div class="table-responsive mb-4">
|
||||
<table class="table app-table align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>البند</th>
|
||||
<th>الدرجة القصوى</th>
|
||||
<th>الدرجة المرصودة</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($criteria as $criterion): ?>
|
||||
<?php
|
||||
$criterionId = (int) ($criterion['id'] ?? 0);
|
||||
$criterionValue = (string) (($values['criteria_scores'][$criterionId]['score_raw'] ?? ''));
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-semibold"><?= e((string) ($criterion['title'] ?? '')) ?></div>
|
||||
<?php if (!empty($criterion['notes'])): ?><div class="text-muted small"><?= e((string) ($criterion['notes'] ?? '')) ?></div><?php endif; ?>
|
||||
</td>
|
||||
<td><?= e(center_score_display((float) ($criterion['max_score'] ?? 0))) ?></td>
|
||||
<td>
|
||||
<input
|
||||
class="form-control"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
max="<?= e(center_score_display((float) ($criterion['max_score'] ?? 0))) ?>"
|
||||
name="criteria[<?= e((string) $criterionId) ?>]"
|
||||
value="<?= e($criterionValue) ?>"
|
||||
placeholder="<?= e(center_score_display((float) ($criterion['max_score'] ?? 0))) ?>"
|
||||
data-center-criterion
|
||||
<?= $isCycleReadOnly ? 'disabled' : '' ?>
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4"><div class="school-data-item"><strong>المجموع الحالي</strong><span data-center-total><?= e($values['score_raw'] !== '' ? center_score_display((float) $values['score_raw']) : '—') ?></span></div></div>
|
||||
<div class="col-md-4"><div class="school-data-item"><strong>المجموع الكلي</strong><span><?= e($maxScoreLabel !== '' ? $maxScoreLabel : '0') ?></span></div></div>
|
||||
<div class="col-md-4"><div class="school-data-item"><strong>نسبة الإنجاز</strong><span data-center-percentage><?= e(($values['score_raw'] !== '' && (float) ($values['assessment_max_score'] ?? 0) > 0) ? center_score_display(((float) $values['score_raw'] / (float) $values['assessment_max_score']) * 100) . '%' : '—') ?></span></div></div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="score">الدرجة المرصودة</label>
|
||||
<input class="form-control <?= isset($errors['score']) ? 'is-invalid' : '' ?>" type="number" step="0.01" min="0" max="<?= e($maxScoreLabel !== '' ? $maxScoreLabel : '0') ?>" id="score" name="score" value="<?= e((string) $values['score_raw']) ?>" placeholder="من <?= e($maxScoreLabel !== '' ? $maxScoreLabel : '0') ?>" <?= $isCycleReadOnly ? 'disabled' : '' ?>>
|
||||
<?php if (isset($errors['score'])): ?><div class="invalid-feedback"><?= e($errors['score']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="notes">ملاحظات</label>
|
||||
<textarea class="form-control" id="notes" name="notes" rows="4" placeholder="اختياري" <?= $isCycleReadOnly ? 'disabled' : '' ?>><?= e((string) $values['notes']) ?></textarea>
|
||||
</div>
|
||||
|
||||
<?php if (is_array($existingScore) && $existingScore !== []): ?>
|
||||
<div class="alert alert-light border mb-4">
|
||||
<strong>آخر حفظ:</strong>
|
||||
<?= center_assessment_status_badge((string) ($existingScore['status'] ?? 'pending')) ?>
|
||||
<span class="ms-2">بتاريخ <?= e((string) ($existingScore['assessed_on'] ?? '—')) ?></span>
|
||||
<?php if ($existingPercentage !== null): ?><span class="ms-2">— <?= e(center_score_display((float) $existingPercentage)) ?>%</span><?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!$isCycleReadOnly): ?>
|
||||
<div class="d-flex justify-content-between align-items-center pt-3 border-top mt-3 flex-wrap gap-2">
|
||||
<div class="section-subtle">يمكنك تعديل نفس التقييم لاحقاً، وسيتم تحديث الدرجة والبنود الحالية بدلاً من إنشاء نسخة جديدة.</div>
|
||||
<button class="btn btn-primary" type="submit">حفظ الرصد</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php if (!$isCycleReadOnly): ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const statusField = document.querySelector('[data-center-status]');
|
||||
const criterionInputs = Array.from(document.querySelectorAll('[data-center-criterion]'));
|
||||
const totalEl = document.querySelector('[data-center-total]');
|
||||
const percentageEl = document.querySelector('[data-center-percentage]');
|
||||
const directScore = document.getElementById('score');
|
||||
const maxScore = <?= json_encode(center_score_display((float) ($values['assessment_max_score'] ?? 0))) ?>;
|
||||
const maxScoreValue = <?= (float) ($values['assessment_max_score'] ?? 0) ?>;
|
||||
|
||||
const parsedMax = <?= (float) ($values['assessment_max_score'] ?? 0) ?>;
|
||||
|
||||
const updateCriteriaState = () => {
|
||||
if (!statusField || !criterionInputs.length) return;
|
||||
const enabled = statusField.value === 'completed';
|
||||
let total = 0;
|
||||
let hasValue = false;
|
||||
criterionInputs.forEach((input) => {
|
||||
if (enabled) {
|
||||
input.removeAttribute('disabled');
|
||||
} else {
|
||||
input.setAttribute('disabled', 'disabled');
|
||||
input.value = '';
|
||||
}
|
||||
const raw = input.value.trim();
|
||||
if (enabled && raw !== '' && !Number.isNaN(Number(raw))) {
|
||||
total += Number(raw);
|
||||
hasValue = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (totalEl) {
|
||||
totalEl.textContent = enabled && hasValue ? total.toFixed(2).replace(/\.00$/, '').replace(/(\.\d)0$/, '$1') : '—';
|
||||
}
|
||||
if (percentageEl) {
|
||||
const maxValue = Number(percentageEl.getAttribute('data-max-score') || '0');
|
||||
percentageEl.textContent = enabled && hasValue && maxValue > 0
|
||||
? ((total / maxValue) * 100).toFixed(2).replace(/\.00$/, '').replace(/(\.\d)0$/, '$1') + '%'
|
||||
: '—';
|
||||
}
|
||||
};
|
||||
|
||||
if (percentageEl) {
|
||||
percentageEl.setAttribute('data-max-score', '<?= e((string) ((float) ($values['assessment_max_score'] ?? 0))) ?>');
|
||||
}
|
||||
|
||||
const updateDirectScoreState = () => {
|
||||
if (!statusField || !directScore) return;
|
||||
const enabled = statusField.value === 'completed';
|
||||
if (enabled) {
|
||||
directScore.removeAttribute('disabled');
|
||||
} else {
|
||||
directScore.setAttribute('disabled', 'disabled');
|
||||
directScore.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
if (statusField) {
|
||||
statusField.addEventListener('change', () => {
|
||||
updateDirectScoreState();
|
||||
updateCriteriaState();
|
||||
});
|
||||
}
|
||||
|
||||
criterionInputs.forEach((input) => {
|
||||
input.addEventListener('input', updateCriteriaState);
|
||||
input.addEventListener('change', updateCriteriaState);
|
||||
});
|
||||
|
||||
updateDirectScoreState();
|
||||
updateCriteriaState();
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
<?php render_page_end();
|
||||
501
center_assessment_score_sheet.php.bak
Normal file
501
center_assessment_score_sheet.php.bak
Normal file
@ -0,0 +1,501 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
function center_score_display(?float $value): string
|
||||
{
|
||||
if ($value === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.');
|
||||
}
|
||||
|
||||
$flash = consume_flash();
|
||||
$applicationId = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT) ?: 0;
|
||||
$requestedCycleId = filter_input(INPUT_GET, 'cycle', FILTER_VALIDATE_INT) ?: 0;
|
||||
$requestedAssessmentId = filter_input(INPUT_GET, 'assessment_id', FILTER_VALIDATE_INT) ?: 0;
|
||||
$application = $applicationId > 0 ? get_application($applicationId) : null;
|
||||
$isApprovedCenter = $application && (string) ($application['status'] ?? '') === 'approved';
|
||||
$errors = [];
|
||||
$cycleContext = ['cycles' => [], 'selected' => null, 'active' => null, 'read_only' => false];
|
||||
$selectedCycle = null;
|
||||
$selectedCycleId = 0;
|
||||
$isCycleReadOnly = false;
|
||||
$cycleLabel = 'لا توجد دورة بعد';
|
||||
$values = [
|
||||
'assessment_type_id' => '',
|
||||
'assessed_on' => date('Y-m-d'),
|
||||
'status' => 'completed',
|
||||
'assessment_max_score' => 0.0,
|
||||
'has_criteria' => false,
|
||||
'criteria' => [],
|
||||
'criteria_scores' => [],
|
||||
'score' => null,
|
||||
'score_raw' => '',
|
||||
'notes' => '',
|
||||
'should_save' => false,
|
||||
];
|
||||
|
||||
$buildCenterAssessmentsUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0): string {
|
||||
$params = [];
|
||||
if ($targetApplicationId > 0) {
|
||||
$params['id'] = $targetApplicationId;
|
||||
}
|
||||
if ($targetCycleId > 0) {
|
||||
$params['cycle'] = $targetCycleId;
|
||||
}
|
||||
return 'center_assessments.php' . ($params !== [] ? '?' . http_build_query($params) : '');
|
||||
};
|
||||
|
||||
$buildCenterAssessmentCriteriaUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0, int $targetAssessmentId = 0): string {
|
||||
$params = [];
|
||||
if ($targetApplicationId > 0) {
|
||||
$params['id'] = $targetApplicationId;
|
||||
}
|
||||
if ($targetCycleId > 0) {
|
||||
$params['cycle'] = $targetCycleId;
|
||||
}
|
||||
if ($targetAssessmentId > 0) {
|
||||
$params['assessment_id'] = $targetAssessmentId;
|
||||
}
|
||||
return 'center_assessment_criteria.php' . ($params !== [] ? '?' . http_build_query($params) : '');
|
||||
};
|
||||
|
||||
$buildCenterAssessmentScoreUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0, int $targetAssessmentId = 0): string {
|
||||
$params = [];
|
||||
if ($targetApplicationId > 0) {
|
||||
$params['id'] = $targetApplicationId;
|
||||
}
|
||||
if ($targetCycleId > 0) {
|
||||
$params['cycle'] = $targetCycleId;
|
||||
}
|
||||
if ($targetAssessmentId > 0) {
|
||||
$params['assessment_id'] = $targetAssessmentId;
|
||||
}
|
||||
return 'center_assessment_score_sheet.php' . ($params !== [] ? '?' . http_build_query($params) : '');
|
||||
};
|
||||
|
||||
$buildCenterAssessmentReportUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0): string {
|
||||
$params = [];
|
||||
if ($targetApplicationId > 0) {
|
||||
$params['id'] = $targetApplicationId;
|
||||
}
|
||||
if ($targetCycleId > 0) {
|
||||
$params['cycle'] = $targetCycleId;
|
||||
}
|
||||
return 'center_assessment_report.php' . ($params !== [] ? '?' . http_build_query($params) : '');
|
||||
};
|
||||
|
||||
if ($applicationId <= 0 || $requestedCycleId <= 0) {
|
||||
set_flash('error', 'اختر المركز والدورة أولاً ثم افتح صفحة رصد التقييم من شاشة تقييم المراكز.');
|
||||
header('Location: ' . $buildCenterAssessmentsUrl($applicationId, $requestedCycleId));
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($isApprovedCenter) {
|
||||
$cycleContext = resolve_school_cycle_context((int) $application['id'], $application, $requestedCycleId);
|
||||
$selectedCycle = $cycleContext['selected'];
|
||||
$selectedCycleId = $selectedCycle ? (int) ($selectedCycle['id'] ?? 0) : 0;
|
||||
$isCycleReadOnly = (bool) ($cycleContext['read_only'] ?? false);
|
||||
$cycleLabel = $selectedCycle ? (string) ($selectedCycle['cycle_name'] ?? $cycleLabel) : $cycleLabel;
|
||||
}
|
||||
|
||||
$assessmentOptions = $isApprovedCenter && $selectedCycleId > 0
|
||||
? center_assessment_type_options_by_cycle((int) $application['id'], $selectedCycleId, false)
|
||||
: [];
|
||||
$selectedAssessmentId = $requestedAssessmentId;
|
||||
if ($selectedAssessmentId <= 0 && $assessmentOptions !== []) {
|
||||
$keys = array_keys($assessmentOptions);
|
||||
$selectedAssessmentId = (int) ($keys[0] ?? 0);
|
||||
}
|
||||
if ($selectedAssessmentId > 0) {
|
||||
$values['assessment_type_id'] = (string) $selectedAssessmentId;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $application) {
|
||||
if (!$isApprovedCenter) {
|
||||
$errors['form'] = 'لا يمكن رصد تقييم المركز قبل اعتماد المركز.';
|
||||
} elseif ($selectedCycleId <= 0) {
|
||||
$errors['form'] = 'يرجى إنشاء دورة موسمية أولاً من صفحة المركز.';
|
||||
} elseif ($isCycleReadOnly) {
|
||||
$errors['form'] = 'هذه الدورة مؤرشفة للقراءة فقط. اختر دورة نشطة لإدخال تقييم جديد.';
|
||||
} else {
|
||||
[$values, $errors, $selectedAssessmentMeta] = validate_center_assessment_score_input((int) $application['id'], $selectedCycleId, $_POST);
|
||||
$selectedAssessmentId = (int) ($values['assessment_type_id'] ?? 0);
|
||||
if ($errors === []) {
|
||||
try {
|
||||
save_center_assessment_score_in_cycle((int) $application['id'], $selectedCycleId, $values);
|
||||
set_flash('success', 'تم حفظ رصد تقييم المركز بنجاح.');
|
||||
header('Location: ' . $buildCenterAssessmentScoreUrl((int) $application['id'], $selectedCycleId, $selectedAssessmentId));
|
||||
exit;
|
||||
} catch (Throwable $exception) {
|
||||
$errors['form'] = 'تعذر حفظ الرصد حالياً. يرجى المحاولة مرة أخرى.';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$selectedAssessment = $selectedAssessmentId > 0 ? ($assessmentOptions[$selectedAssessmentId] ?? null) : null;
|
||||
$criteria = $isApprovedCenter && $selectedCycleId > 0 && $selectedAssessmentId > 0
|
||||
? list_center_assessment_criteria_by_assessment((int) $application['id'], $selectedCycleId, $selectedAssessmentId, true)
|
||||
: [];
|
||||
$hasCriteria = $criteria !== [];
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
$values['has_criteria'] = $hasCriteria;
|
||||
$values['criteria'] = $criteria;
|
||||
if ($hasCriteria) {
|
||||
$values['assessment_max_score'] = round(array_reduce($criteria, static function (float $carry, array $criterion): float {
|
||||
return $carry + (float) ($criterion['max_score'] ?? 0);
|
||||
}, 0.0), 2);
|
||||
} elseif ($selectedAssessment) {
|
||||
$values['assessment_max_score'] = (float) ($selectedAssessment['max_score'] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
$existingBundle = $isApprovedCenter && $selectedCycleId > 0 && $selectedAssessmentId > 0
|
||||
? center_assessment_score_bundle_by_assessment((int) $application['id'], $selectedCycleId, $selectedAssessmentId)
|
||||
: ['score' => null, 'criteria_scores' => []];
|
||||
$existingScore = $existingBundle['score'] ?? null;
|
||||
$existingCriteriaScores = $existingBundle['criteria_scores'] ?? [];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST' && is_array($existingScore) && $existingScore !== []) {
|
||||
if (!empty($existingScore['assessed_on'])) {
|
||||
$values['assessed_on'] = (string) $existingScore['assessed_on'];
|
||||
}
|
||||
$values['status'] = center_assessment_normalize_status((string) ($existingScore['status'] ?? 'completed'));
|
||||
$values['notes'] = (string) ($existingScore['notes'] ?? '');
|
||||
|
||||
if ($hasCriteria) {
|
||||
foreach ($criteria as $criterion) {
|
||||
$criterionId = (int) ($criterion['id'] ?? 0);
|
||||
if ($criterionId <= 0) {
|
||||
continue;
|
||||
}
|
||||
$existingCriterion = $existingCriteriaScores[$criterionId] ?? [];
|
||||
$values['criteria_scores'][$criterionId] = [
|
||||
'criterion_id' => $criterionId,
|
||||
'score' => isset($existingCriterion['score']) ? (float) $existingCriterion['score'] : null,
|
||||
'score_raw' => isset($existingCriterion['score']) && $existingCriterion['score'] !== null
|
||||
? center_score_display((float) $existingCriterion['score'])
|
||||
: '',
|
||||
'max_score' => (float) ($criterion['max_score'] ?? 0),
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$values['score'] = isset($existingScore['score']) ? (float) $existingScore['score'] : null;
|
||||
$values['score_raw'] = isset($existingScore['score']) && $existingScore['score'] !== null
|
||||
? center_score_display((float) $existingScore['score'])
|
||||
: '';
|
||||
}
|
||||
}
|
||||
|
||||
$criteriaMetrics = $isApprovedCenter && $selectedCycleId > 0 && $selectedAssessmentId > 0
|
||||
? center_assessment_criteria_metrics((int) $application['id'], $selectedCycleId, $selectedAssessmentId)
|
||||
: ['total' => 0, 'active' => 0, 'active_max_score' => 0.0];
|
||||
$scoreMetrics = $isApprovedCenter && $selectedCycleId > 0 && $selectedAssessmentId > 0
|
||||
? center_assessment_score_metrics_by_cycle((int) $application['id'], $selectedCycleId, $selectedAssessmentId)
|
||||
: ['total' => 0, 'completed' => 0, 'pending' => 0, 'waived' => 0, 'average_score' => 0.0, 'latest_date' => ''];
|
||||
|
||||
$pageTitle = $application && $isApprovedCenter
|
||||
? 'رصد تقييم المركز: ' . (string) ($application['center_name'] ?? '') . ($selectedAssessment ? ' — ' . (string) ($selectedAssessment['title'] ?? '') : '')
|
||||
: 'رصد تقييم المركز';
|
||||
$pageDescription = 'إدخال درجة تقييم المركز نفسه داخل الدورة المختارة، مع دعم البنود التفصيلية والتقرير النهائي.';
|
||||
|
||||
if (!$application && $applicationId > 0) {
|
||||
http_response_code(404);
|
||||
}
|
||||
|
||||
$assessmentsUrl = $application ? $buildCenterAssessmentsUrl((int) $application['id'], $selectedCycleId) : 'center_assessments.php';
|
||||
$criteriaUrl = $application && $selectedAssessmentId > 0 ? $buildCenterAssessmentCriteriaUrl((int) $application['id'], $selectedCycleId, $selectedAssessmentId) : 'center_assessment_criteria.php';
|
||||
$reportUrl = $application ? $buildCenterAssessmentReportUrl((int) $application['id'], $selectedCycleId) : 'center_assessment_report.php';
|
||||
$maxScoreLabel = center_score_display((float) ($values['assessment_max_score'] ?? 0));
|
||||
$existingStatus = is_array($existingScore) ? center_assessment_normalize_status((string) ($existingScore['status'] ?? 'pending')) : '';
|
||||
$existingPercentage = (is_array($existingScore) && isset($existingScore['score']) && $existingScore['score'] !== null && (float) ($existingScore['max_score'] ?? 0) > 0)
|
||||
? round(((float) $existingScore['score'] / (float) $existingScore['max_score']) * 100, 2)
|
||||
: null;
|
||||
|
||||
render_page_start($pageTitle, 'approved', $pageDescription, (string) ($application['favicon'] ?? ''));
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="py-4 py-lg-5">
|
||||
<div class="container-xxl">
|
||||
<div class="admin-layout row g-4 align-items-start">
|
||||
<div class="col-lg-3 layout-sidebar-column">
|
||||
<?php require __DIR__ . '/includes/sidebar.php'; ?>
|
||||
</div>
|
||||
<div class="col-lg-9 layout-content-column">
|
||||
<?php if (!$application): ?>
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">المركز غير موجود</div>
|
||||
<p class="text-muted mb-3">تحقق من الرابط أو ارجع إلى قائمة المراكز المعتمدة.</p>
|
||||
<a class="btn btn-primary" href="applications.php?status=approved">المراكز المعتمدة</a>
|
||||
</div>
|
||||
<?php elseif (!$isApprovedCenter): ?>
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">الرصد يُفتح بعد الاعتماد</div>
|
||||
<p class="text-muted mb-3">اعتمد المركز أولاً حتى يظهر رصد تقييمه الإشرافي.</p>
|
||||
<a class="btn btn-outline-secondary" href="application_detail.php?id=<?= e((string) $application['id']) ?>">ملف الاعتماد</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="page-banner approved-hero mb-4">
|
||||
<div class="row g-4 align-items-center">
|
||||
<div class="col-lg-8">
|
||||
<span class="approved-kicker mb-3">رصد المركز</span>
|
||||
<h1 class="page-title mb-3">إدخال تقييم إشرافي للمركز</h1>
|
||||
<p class="page-copy mb-3">اختر التقييم، ثم أدخل النتيجة مباشرة أو عبر البنود التفصيلية داخل دورة <strong><?= e($cycleLabel) ?></strong>.</p>
|
||||
<div class="hero-meta">
|
||||
<span><?= e((string) count($assessmentOptions)) ?> تقييمات متاحة</span>
|
||||
<span><?= e($cycleLabel) ?></span>
|
||||
<?php if ($selectedAssessment): ?><span><?= e((string) ($selectedAssessment['title'] ?? '')) ?></span><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="app-card h-100">
|
||||
<div class="section-title mb-2">تنقّل سريع</div>
|
||||
<div class="cta-stack">
|
||||
<a class="btn btn-outline-secondary" href="<?= e($assessmentsUrl) ?>">كل التقييمات</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($reportUrl) ?>">تقرير الدورة</a>
|
||||
<?php if ($selectedAssessment): ?>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($criteriaUrl) ?>">إدارة البنود</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($selectedCycleId <= 0): ?>
|
||||
<div class="app-card">
|
||||
<div class="alert alert-warning mb-0">لا توجد دورة متاحة لهذا المركز بعد. أنشئ دورة أولاً من صفحة المركز.</div>
|
||||
</div>
|
||||
<?php elseif ($assessmentOptions === []): ?>
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">لا توجد تقييمات مراكز جاهزة للرصد</div>
|
||||
<p class="text-muted mb-3">أضف نوع تقييم أولاً من صفحة تقييم المراكز ثم ارجع هنا لإدخال الدرجة.</p>
|
||||
<a class="btn btn-primary" href="<?= e($assessmentsUrl) ?>">إضافة تقييم</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="app-card mb-4">
|
||||
<form method="get" class="row g-2 align-items-center">
|
||||
<input type="hidden" name="id" value="<?= e((string) $application['id']) ?>">
|
||||
<input type="hidden" name="cycle" value="<?= e((string) $selectedCycleId) ?>">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label" for="assessment_id">التقييم</label>
|
||||
<select class="form-select" name="assessment_id" id="assessment_id" onchange="this.form.submit()">
|
||||
<?php foreach ($assessmentOptions as $assessmentId => $assessment): ?>
|
||||
<option value="<?= e((string) $assessmentId) ?>" <?= $selectedAssessmentId === (int) $assessmentId ? 'selected' : '' ?>><?= e((string) ($assessment['label'] ?? '')) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 d-grid">
|
||||
<button type="submit" class="btn btn-primary">فتح الرصد</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php if ($selectedAssessment): ?>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-3"><div class="app-card h-100"><div class="section-title mb-2">الدرجة النهائية</div><div class="display-6 mb-1"><?= e($maxScoreLabel !== '' ? $maxScoreLabel : '0') ?></div><div class="section-subtle">المجموع المعتمد</div></div></div>
|
||||
<div class="col-md-3"><div class="app-card h-100"><div class="section-title mb-2">البنود النشطة</div><div class="display-6 mb-1"><?= e((string) ($criteriaMetrics['active'] ?? 0)) ?></div><div class="section-subtle">من أصل <?= e((string) ($criteriaMetrics['total'] ?? 0)) ?></div></div></div>
|
||||
<div class="col-md-3"><div class="app-card h-100"><div class="section-title mb-2">حالة الرصد</div><div class="display-6 mb-1" style="font-size:1.05rem;"><?= $existingStatus !== '' ? center_assessment_status_badge($existingStatus) : '<span class="text-muted">غير مرصود</span>' ?></div><div class="section-subtle">آخر حفظ <?= e($scoreMetrics['latest_date'] !== '' ? (string) $scoreMetrics['latest_date'] : '—') ?></div></div></div>
|
||||
<div class="col-md-3"><div class="app-card h-100"><div class="section-title mb-2">آخر نسبة</div><div class="display-6 mb-1"><?= e($existingPercentage !== null ? center_score_display((float) $existingPercentage) . '%' : '—') ?></div><div class="section-subtle">للتقييم الحالي</div></div></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($errors['form'])): ?>
|
||||
<div class="alert alert-danger mb-4"><?= e($errors['form']) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($isCycleReadOnly): ?>
|
||||
<div class="alert alert-warning mb-4">هذه الدورة مؤرشفة، لذلك الصفحة معروضة للقراءة فقط.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($selectedAssessment): ?>
|
||||
<div class="app-card">
|
||||
<form method="post" novalidate>
|
||||
<input type="hidden" name="assessment_type_id" value="<?= e((string) $selectedAssessmentId) ?>">
|
||||
<div class="row g-3 align-items-end mb-4">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="assessed_on">تاريخ الرصد</label>
|
||||
<input type="date" class="form-control <?= isset($errors['assessed_on']) ? 'is-invalid' : '' ?>" id="assessed_on" name="assessed_on" value="<?= e((string) $values['assessed_on']) ?>" <?= $isCycleReadOnly ? 'disabled' : '' ?>>
|
||||
<?php if (isset($errors['assessed_on'])): ?><div class="invalid-feedback"><?= e($errors['assessed_on']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="status">الحالة</label>
|
||||
<select class="form-select" id="status" name="status" data-center-status <?= $isCycleReadOnly ? 'disabled' : '' ?>>
|
||||
<?php foreach (center_assessment_status_map() as $statusKey => $statusMeta): ?>
|
||||
<option value="<?= e($statusKey) ?>" <?= (string) $values['status'] === (string) $statusKey ? 'selected' : '' ?>><?= e((string) $statusMeta['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="school-data-item h-100">
|
||||
<strong>الوزن</strong>
|
||||
<span><?= e(center_score_display((float) ($selectedAssessment['weight_percentage'] ?? 0)) ?: '0') ?>٪</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($errors['score'])): ?>
|
||||
<div class="alert alert-danger mb-4"><?= e($errors['score']) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($hasCriteria): ?>
|
||||
<div class="table-responsive mb-4">
|
||||
<table class="table app-table align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>البند</th>
|
||||
<th>الدرجة القصوى</th>
|
||||
<th>الدرجة المرصودة</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($criteria as $criterion): ?>
|
||||
<?php
|
||||
$criterionId = (int) ($criterion['id'] ?? 0);
|
||||
$criterionValue = (string) (($values['criteria_scores'][$criterionId]['score_raw'] ?? ''));
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-semibold"><?= e((string) ($criterion['title'] ?? '')) ?></div>
|
||||
<?php if (!empty($criterion['notes'])): ?><div class="text-muted small"><?= e((string) ($criterion['notes'] ?? '')) ?></div><?php endif; ?>
|
||||
</td>
|
||||
<td><?= e(center_score_display((float) ($criterion['max_score'] ?? 0))) ?></td>
|
||||
<td>
|
||||
<input
|
||||
class="form-control"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
max="<?= e(center_score_display((float) ($criterion['max_score'] ?? 0))) ?>"
|
||||
name="criteria[<?= e((string) $criterionId) ?>]"
|
||||
value="<?= e($criterionValue) ?>"
|
||||
placeholder="<?= e(center_score_display((float) ($criterion['max_score'] ?? 0))) ?>"
|
||||
data-center-criterion
|
||||
<?= $isCycleReadOnly ? 'disabled' : '' ?>
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4"><div class="school-data-item"><strong>المجموع الحالي</strong><span data-center-total><?= e($values['score_raw'] !== '' ? center_score_display((float) $values['score_raw']) : '—') ?></span></div></div>
|
||||
<div class="col-md-4"><div class="school-data-item"><strong>المجموع الكلي</strong><span><?= e($maxScoreLabel !== '' ? $maxScoreLabel : '0') ?></span></div></div>
|
||||
<div class="col-md-4"><div class="school-data-item"><strong>نسبة الإنجاز</strong><span data-center-percentage><?= e(($values['score_raw'] !== '' && (float) ($values['assessment_max_score'] ?? 0) > 0) ? center_score_display(((float) $values['score_raw'] / (float) $values['assessment_max_score']) * 100) . '%' : '—') ?></span></div></div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="score">الدرجة المرصودة</label>
|
||||
<input class="form-control <?= isset($errors['score']) ? 'is-invalid' : '' ?>" type="number" step="0.01" min="0" max="<?= e($maxScoreLabel !== '' ? $maxScoreLabel : '0') ?>" id="score" name="score" value="<?= e((string) $values['score_raw']) ?>" placeholder="من <?= e($maxScoreLabel !== '' ? $maxScoreLabel : '0') ?>" <?= $isCycleReadOnly ? 'disabled' : '' ?>>
|
||||
<?php if (isset($errors['score'])): ?><div class="invalid-feedback"><?= e($errors['score']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label" for="notes">ملاحظات</label>
|
||||
<textarea class="form-control" id="notes" name="notes" rows="4" placeholder="اختياري" <?= $isCycleReadOnly ? 'disabled' : '' ?>><?= e((string) $values['notes']) ?></textarea>
|
||||
</div>
|
||||
|
||||
<?php if (is_array($existingScore) && $existingScore !== []): ?>
|
||||
<div class="alert alert-light border mb-4">
|
||||
<strong>آخر حفظ:</strong>
|
||||
<?= center_assessment_status_badge((string) ($existingScore['status'] ?? 'pending')) ?>
|
||||
<span class="ms-2">بتاريخ <?= e((string) ($existingScore['assessed_on'] ?? '—')) ?></span>
|
||||
<?php if ($existingPercentage !== null): ?><span class="ms-2">— <?= e(center_score_display((float) $existingPercentage)) ?>%</span><?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!$isCycleReadOnly): ?>
|
||||
<div class="d-flex justify-content-between align-items-center pt-3 border-top mt-3 flex-wrap gap-2">
|
||||
<div class="section-subtle">يمكنك تعديل نفس التقييم لاحقاً، وسيتم تحديث الدرجة والبنود الحالية بدلاً من إنشاء نسخة جديدة.</div>
|
||||
<button class="btn btn-primary" type="submit">حفظ الرصد</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php if (!$isCycleReadOnly): ?>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const statusField = document.querySelector('[data-center-status]');
|
||||
const criterionInputs = Array.from(document.querySelectorAll('[data-center-criterion]'));
|
||||
const totalEl = document.querySelector('[data-center-total]');
|
||||
const percentageEl = document.querySelector('[data-center-percentage]');
|
||||
const directScore = document.getElementById('score');
|
||||
const maxScore = <?= json.dumps(0) ?>;
|
||||
const maxScoreValue = <?= (float) 0 ?>;
|
||||
|
||||
const parsedMax = <?= json.dumps('MAX_PLACEHOLDER') ?>;
|
||||
|
||||
const updateCriteriaState = () => {
|
||||
if (!statusField || !criterionInputs.length) return;
|
||||
const enabled = statusField.value === 'completed';
|
||||
let total = 0;
|
||||
let hasValue = false;
|
||||
criterionInputs.forEach((input) => {
|
||||
if (enabled) {
|
||||
input.removeAttribute('disabled');
|
||||
} else {
|
||||
input.setAttribute('disabled', 'disabled');
|
||||
input.value = '';
|
||||
}
|
||||
const raw = input.value.trim();
|
||||
if (enabled && raw !== '' && !Number.isNaN(Number(raw))) {
|
||||
total += Number(raw);
|
||||
hasValue = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (totalEl) {
|
||||
totalEl.textContent = enabled && hasValue ? total.toFixed(2).replace(/\.00$/, '').replace(/(\.\d)0$/, '$1') : '—';
|
||||
}
|
||||
if (percentageEl) {
|
||||
const maxValue = Number(percentageEl.getAttribute('data-max-score') || '0');
|
||||
percentageEl.textContent = enabled && hasValue && maxValue > 0
|
||||
? ((total / maxValue) * 100).toFixed(2).replace(/\.00$/, '').replace(/(\.\d)0$/, '$1') + '%'
|
||||
: '—';
|
||||
}
|
||||
};
|
||||
|
||||
if (percentageEl) {
|
||||
percentageEl.setAttribute('data-max-score', '<?= e((string) ((float) ($values['assessment_max_score'] ?? 0))) ?>');
|
||||
}
|
||||
|
||||
const updateDirectScoreState = () => {
|
||||
if (!statusField || !directScore) return;
|
||||
const enabled = statusField.value === 'completed';
|
||||
if (enabled) {
|
||||
directScore.removeAttribute('disabled');
|
||||
} else {
|
||||
directScore.setAttribute('disabled', 'disabled');
|
||||
directScore.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
if (statusField) {
|
||||
statusField.addEventListener('change', () => {
|
||||
updateDirectScoreState();
|
||||
updateCriteriaState();
|
||||
});
|
||||
}
|
||||
|
||||
criterionInputs.forEach((input) => {
|
||||
input.addEventListener('input', updateCriteriaState);
|
||||
input.addEventListener('change', updateCriteriaState);
|
||||
});
|
||||
|
||||
updateDirectScoreState();
|
||||
updateCriteriaState();
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
<?php render_page_end();
|
||||
676
center_assessments.php
Normal file
676
center_assessments.php
Normal file
@ -0,0 +1,676 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
$flash = consume_flash();
|
||||
$approvedCenters = list_applications('approved');
|
||||
$applicationId = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT) ?: 0;
|
||||
$requestedCycleId = filter_input(INPUT_GET, 'cycle', FILTER_VALIDATE_INT) ?: 0;
|
||||
$application = $applicationId > 0 ? get_application($applicationId) : null;
|
||||
$isApprovedCenter = $application && (string) ($application['status'] ?? '') === 'approved';
|
||||
$values = assessment_defaults();
|
||||
$errors = [];
|
||||
$cycleContext = ['cycles' => [], 'selected' => null, 'active' => null, 'read_only' => false];
|
||||
$selectedCycle = null;
|
||||
$selectedCycleId = 0;
|
||||
$isCycleReadOnly = false;
|
||||
$cycleLabel = 'لا توجد دورة بعد';
|
||||
|
||||
$buildCenterAssessmentsUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0, array $extra = []): string {
|
||||
$params = [];
|
||||
if ($targetApplicationId > 0) {
|
||||
$params['id'] = $targetApplicationId;
|
||||
}
|
||||
if ($targetCycleId > 0) {
|
||||
$params['cycle'] = $targetCycleId;
|
||||
}
|
||||
foreach ($extra as $key => $value) {
|
||||
if ($value === '' || $value === null) {
|
||||
continue;
|
||||
}
|
||||
$params[$key] = $value;
|
||||
}
|
||||
return 'center_assessments.php' . ($params !== [] ? '?' . http_build_query($params) : '');
|
||||
};
|
||||
|
||||
|
||||
$buildCenterAssessmentScoreUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0, int $targetAssessmentId = 0): string {
|
||||
$params = [];
|
||||
if ($targetApplicationId > 0) {
|
||||
$params['id'] = $targetApplicationId;
|
||||
}
|
||||
if ($targetCycleId > 0) {
|
||||
$params['cycle'] = $targetCycleId;
|
||||
}
|
||||
if ($targetAssessmentId > 0) {
|
||||
$params['assessment_id'] = $targetAssessmentId;
|
||||
}
|
||||
return 'center_assessment_score_sheet.php' . ($params !== [] ? '?' . http_build_query($params) : '');
|
||||
};
|
||||
|
||||
$buildCenterAssessmentReportUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0): string {
|
||||
$params = [];
|
||||
if ($targetApplicationId > 0) {
|
||||
$params['id'] = $targetApplicationId;
|
||||
}
|
||||
if ($targetCycleId > 0) {
|
||||
$params['cycle'] = $targetCycleId;
|
||||
}
|
||||
return 'center_assessment_report.php' . ($params !== [] ? '?' . http_build_query($params) : '');
|
||||
};
|
||||
|
||||
$buildCenterAssessmentPrintUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0, int $targetAssessmentId = 0): string {
|
||||
$params = [];
|
||||
if ($targetApplicationId > 0) {
|
||||
$params['id'] = $targetApplicationId;
|
||||
}
|
||||
if ($targetCycleId > 0) {
|
||||
$params['cycle'] = $targetCycleId;
|
||||
}
|
||||
if ($targetAssessmentId > 0) {
|
||||
$params['assessment_id'] = $targetAssessmentId;
|
||||
}
|
||||
return 'center_assessment_print_sheet.php' . ($params !== [] ? '?' . http_build_query($params) : '');
|
||||
};
|
||||
|
||||
$buildCenterAssessmentCriteriaUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0, int $targetAssessmentId = 0): string {
|
||||
$params = [];
|
||||
if ($targetApplicationId > 0) {
|
||||
$params['id'] = $targetApplicationId;
|
||||
}
|
||||
if ($targetCycleId > 0) {
|
||||
$params['cycle'] = $targetCycleId;
|
||||
}
|
||||
if ($targetAssessmentId > 0) {
|
||||
$params['assessment_id'] = $targetAssessmentId;
|
||||
}
|
||||
return 'center_assessment_criteria.php' . ($params !== [] ? '?' . http_build_query($params) : '');
|
||||
};
|
||||
|
||||
if ($isApprovedCenter) {
|
||||
$cycleContext = resolve_school_cycle_context((int) $application['id'], $application, $requestedCycleId);
|
||||
$selectedCycle = $cycleContext['selected'];
|
||||
$selectedCycleId = $selectedCycle ? (int) ($selectedCycle['id'] ?? 0) : 0;
|
||||
$isCycleReadOnly = (bool) ($cycleContext['read_only'] ?? false);
|
||||
$cycleLabel = $selectedCycle ? (string) ($selectedCycle['cycle_name'] ?? $cycleLabel) : $cycleLabel;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $isApprovedCenter) {
|
||||
$action = $_POST['action'] ?? 'add';
|
||||
$assessmentId = filter_input(INPUT_POST, 'assessment_id', FILTER_VALIDATE_INT) ?: 0;
|
||||
[$values, $errors] = validate_assessment_input($_POST);
|
||||
|
||||
if ($selectedCycleId <= 0) {
|
||||
$errors['form'] = 'يجب إنشاء دورة موسمية لهذا المركز قبل إعداد تقييمات المراكز.';
|
||||
} elseif ($isCycleReadOnly) {
|
||||
$errors['form'] = 'الدورة الحالية مؤرشفة للقراءة فقط. اختر دورة نشطة أو أنشئ دورة جديدة.';
|
||||
}
|
||||
|
||||
if ($errors === []) {
|
||||
try {
|
||||
if ($action === 'import_global') {
|
||||
$globalId = filter_input(INPUT_POST, 'global_assessment_id', FILTER_VALIDATE_INT) ?: 0;
|
||||
if ($globalId > 0) {
|
||||
import_global_center_assessment_to_center($globalId, (int) $application['id'], $selectedCycleId);
|
||||
set_flash('success', 'تم إدراج القالب العام مع بنوده بنجاح.');
|
||||
} else {
|
||||
set_flash('error', 'يجب اختيار قالب صحيح.');
|
||||
}
|
||||
} elseif ($action === 'edit' && $assessmentId > 0) {
|
||||
update_center_assessment_type_in_cycle((int) $application['id'], $selectedCycleId, $assessmentId, $values);
|
||||
set_flash('success', 'تم تحديث تقييم المركز بنجاح.');
|
||||
} else {
|
||||
create_center_assessment_type_in_cycle((int) $application['id'], $selectedCycleId, $values);
|
||||
set_flash('success', 'تم حفظ نوع تقييم جديد للمركز داخل الدورة المحددة.');
|
||||
}
|
||||
|
||||
$redirectParams = array_intersect_key($_GET, array_flip(['search', 'category', 'page']));
|
||||
header('Location: ' . $buildCenterAssessmentsUrl((int) $application['id'], $selectedCycleId, $redirectParams));
|
||||
exit;
|
||||
} catch (Throwable $exception) {
|
||||
$errors['form'] = 'تعذر حفظ تقييم المركز حالياً. يرجى المحاولة مرة أخرى.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$filters = [
|
||||
'search' => clean_text($_GET['search'] ?? '', 255),
|
||||
'category' => clean_text($_GET['category'] ?? '', 80),
|
||||
];
|
||||
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT) ?: 1;
|
||||
$limit = 20;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$assessments = $isApprovedCenter && $selectedCycleId > 0 ? list_center_assessments_by_cycle((int) $application['id'], $selectedCycleId, $filters, $limit, $offset) : [];
|
||||
$totalAssessments = $isApprovedCenter && $selectedCycleId > 0 ? count_center_assessments_by_cycle((int) $application['id'], $selectedCycleId, $filters) : 0;
|
||||
$metrics = $isApprovedCenter && $selectedCycleId > 0 ? center_assessment_metrics_by_cycle((int) $application['id'], $selectedCycleId) : [
|
||||
'total' => 0,
|
||||
'active' => 0,
|
||||
'inactive' => 0,
|
||||
'active_weight' => 0.0,
|
||||
'average_max_score' => 0.0,
|
||||
'percentage' => 0,
|
||||
'points' => 0,
|
||||
'rubric' => 0,
|
||||
];
|
||||
|
||||
$approvedCenterCards = [];
|
||||
foreach ($approvedCenters as $center) {
|
||||
$centerId = (int) ($center['id'] ?? 0);
|
||||
if ($centerId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$centerCycleContext = resolve_school_cycle_context($centerId, $center, 0);
|
||||
$centerSelectedCycle = $centerCycleContext['selected'] ?? null;
|
||||
$centerCycleId = $centerSelectedCycle ? (int) ($centerSelectedCycle['id'] ?? 0) : 0;
|
||||
$approvedCenterCards[] = [
|
||||
'application' => $center,
|
||||
'selected_cycle' => $centerSelectedCycle,
|
||||
'selected_cycle_id' => $centerCycleId,
|
||||
'url' => $buildCenterAssessmentsUrl($centerId, $centerCycleId),
|
||||
];
|
||||
}
|
||||
|
||||
$pageTitle = $application && $isApprovedCenter
|
||||
? 'تقييم المراكز: ' . (string) ($application['center_name'] ?? '') . ($selectedCycle ? ' — ' . $cycleLabel : '')
|
||||
: 'تقييم المراكز';
|
||||
$pageDescription = 'إدارة تقييمات إشرافية مستقلة للمراكز المعتمدة حسب الدورة الموسمية، مع البنود، الرصد، والتقرير المجمع.';
|
||||
|
||||
render_page_start($pageTitle, 'admin', $pageDescription);
|
||||
render_flash($flash);
|
||||
?>
|
||||
<style>
|
||||
.icon-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.icon-action {
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid rgba(15, 23, 42, 0.12);
|
||||
border-radius: 0.85rem;
|
||||
color: #0f4c81;
|
||||
background: #ffffff;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease, background-color 0.18s ease;
|
||||
}
|
||||
|
||||
.icon-action:hover,
|
||||
.icon-action:focus-visible {
|
||||
color: #0a2f57;
|
||||
border-color: rgba(15, 76, 129, 0.28);
|
||||
background: #f4f9ff;
|
||||
box-shadow: 0 10px 24px rgba(15, 76, 129, 0.12);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.icon-action svg {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.table-meta {
|
||||
font-size: 0.85rem;
|
||||
color: var(--bs-secondary-color, #6c757d);
|
||||
}
|
||||
</style>
|
||||
<section class="py-4 py-lg-5">
|
||||
<div class="container-xxl">
|
||||
<div class="admin-layout row g-4 align-items-start">
|
||||
<div class="col-lg-3 layout-sidebar-column">
|
||||
<?php require __DIR__ . '/includes/sidebar.php'; ?>
|
||||
</div>
|
||||
<div class="col-lg-9 layout-content-column">
|
||||
|
||||
<div class="page-banner mb-4 mb-lg-5">
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-8">
|
||||
<h1 class="page-title mb-3">إدارة تقييمات المراكز</h1>
|
||||
<p class="page-copy mb-3">هذه الصفحة تضيف طبقة مستقلة لتقييم <strong>المراكز المعتمدة</strong> حسب <strong>الدورة الموسمية</strong>، بدون خلطها مع تقييمات الطلاب. يمكنك الآن تعريف <strong>أنواع تقييم المراكز وأوزانها</strong> ثم فتح <strong>بنود كل تقييم</strong> بنفس النمط المستخدم في تقييم الطلاب، تمهيداً لصفحة الرصد الفعلي.</p>
|
||||
<div class="hero-meta">
|
||||
<span>المراكز المعتمدة <?= e((string) count($approvedCenterCards)) ?></span>
|
||||
<span>الإعداد الحالي <?= e((string) $metrics['active']) ?> تقييمات نشطة</span>
|
||||
<span>الدورة المختارة <?= e($cycleLabel) ?></span>
|
||||
</div>
|
||||
<div class="cta-stack mt-4">
|
||||
<a class="btn btn-primary" href="admin.php">العودة إلى لوحة الإدارة</a>
|
||||
<a class="btn btn-outline-secondary" href="applications.php?status=approved">المراكز المعتمدة</a>
|
||||
<?php if ($application && $isApprovedCenter): ?>
|
||||
<a class="btn btn-outline-secondary" href="approved_school.php?id=<?= e((string) $application['id']) ?>&cycle=<?= e((string) $selectedCycleId) ?>">صفحة المركز</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="page-banner-panel h-100">
|
||||
<div class="mini-stat-label">نطاق العمل</div>
|
||||
<div class="mini-stat-value"><?= e((string) $metrics['total']) ?></div>
|
||||
<div class="mini-stat-copy mb-3">أنواع تقييم مركزية معرفة حالياً للمركز/الدورة المحددين.</div>
|
||||
<div class="d-grid gap-2">
|
||||
<?php if ($application && $isApprovedCenter && $selectedCycleId > 0 && !$isCycleReadOnly): ?>
|
||||
<button class="btn btn-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#centerAssessmentModal">إضافة تقييم جديد</button>
|
||||
<button class="btn btn-outline-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#importGlobalModal">استيراد من القوالب العامة</button>
|
||||
<?php else: ?>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="applications.php?status=approved">اختر مركزاً معتمداً</a>
|
||||
<?php endif; ?>
|
||||
<span class="small text-muted">المعيار هنا: <strong>لكل مركز معتمد</strong> و<strong>لكل دورة</strong> بشكل مستقل.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-8">
|
||||
<div class="app-card h-100">
|
||||
<?php if (!$application || !$isApprovedCenter): ?>
|
||||
<div class="section-head mb-3">
|
||||
<div>
|
||||
<div class="section-title">اختر مركزاً معتمداً للبدء</div>
|
||||
<div class="section-copy">التقييمات المركزية لا تظهر إلا للمراكز المعتمدة، وكل مركز يُدار داخل دورته الموسمية الخاصة.</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($approvedCenterCards === []): ?>
|
||||
<div class="empty-state 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">العودة إلى الطلبات</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="row g-3">
|
||||
<?php foreach ($approvedCenterCards as $card): ?>
|
||||
<?php $center = $card['application']; ?>
|
||||
<div class="col-md-6">
|
||||
<article class="app-card h-100 border-0 shadow-sm">
|
||||
<div class="section-title mb-2"><?= e((string) ($center['center_name'] ?? '')) ?></div>
|
||||
<p class="text-muted mb-3"><?= e((string) ($center['city'] ?? '')) ?> — <?= e((string) ($center['director_name'] ?? '')) ?></p>
|
||||
<div class="hero-meta mb-3">
|
||||
<span>الدورة <?= e((string) (($card['selected_cycle']['cycle_name'] ?? 'غير متاحة'))) ?></span>
|
||||
<span>السعة <?= e((string) ($center['expected_students'] ?? '0')) ?> طالب</span>
|
||||
</div>
|
||||
<?php if ((int) $card['selected_cycle_id'] > 0): ?>
|
||||
<a class="btn btn-primary btn-sm" href="<?= e($card['url']) ?>">فتح تقييم هذا المركز</a>
|
||||
<?php else: ?>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="approved_school.php?id=<?= e((string) ($center['id'] ?? 0)) ?>">أنشئ دورة أولاً</a>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php else: ?>
|
||||
<div class="section-head mb-3">
|
||||
<div>
|
||||
<div class="section-title">تقييمات <?= e((string) $application['center_name']) ?></div>
|
||||
<div class="section-copy">الدورة الحالية: <strong><?= e($cycleLabel) ?></strong>. يمكنك هنا إدارة أنواع التقييم، ثم فتح البنود، الرصد، والتقرير المجمع لكل دورة من عمود الإجراءات.</div>
|
||||
</div>
|
||||
<?php if (!$isCycleReadOnly && $selectedCycleId > 0): ?>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<a class="btn btn-outline-secondary btn-sm" href="<?= e($buildCenterAssessmentReportUrl((int) $application['id'], $selectedCycleId)) ?>">تقرير الدورة</a>
|
||||
<button class="btn btn-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#centerAssessmentModal">إضافة تقييم</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($selectedCycleId <= 0): ?>
|
||||
<div class="alert alert-warning mb-0">لا توجد دورة متاحة لهذا المركز بعد. أنشئ دورة موسمية أولاً من صفحة المركز.</div>
|
||||
<?php else: ?>
|
||||
<?php if ($isCycleReadOnly): ?>
|
||||
<div class="alert alert-warning mb-4">هذه الدورة مؤرشفة للقراءة فقط. يمكنك المراجعة، لكن لا يمكن إضافة أو تعديل تقييمات جديدة هنا.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="get" class="row g-3 align-items-end mb-4">
|
||||
<input type="hidden" name="id" value="<?= e((string) $application['id']) ?>">
|
||||
<input type="hidden" name="cycle" value="<?= e((string) $selectedCycleId) ?>">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label" for="search">بحث</label>
|
||||
<input type="text" name="search" id="search" class="form-control" placeholder="ابحث باسم التقييم..." value="<?= e($filters['search']) ?>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="category">الفئة</label>
|
||||
<select class="form-select" name="category" id="category">
|
||||
<option value="">كل الفئات</option>
|
||||
<?php foreach (assessment_category_options() as $categoryOption): ?>
|
||||
<option value="<?= e($categoryOption) ?>" <?= $filters['category'] === $categoryOption ? 'selected' : '' ?>><?= e($categoryOption) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3 d-grid gap-2">
|
||||
<button class="btn btn-outline-secondary" type="submit">تصفية</button>
|
||||
<a class="btn btn-link" href="<?= e($buildCenterAssessmentsUrl((int) $application['id'], $selectedCycleId)) ?>">إعادة تعيين</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php if ($assessments === []): ?>
|
||||
<div class="empty-state text-center py-5">
|
||||
<div class="empty-title mb-2">لا توجد تقييمات مراكز بعد</div>
|
||||
<p class="text-muted mb-3">ابدأ بتعريف أول نوع تقييم للمركز مثل: الالتزام الإداري، جودة البيئة التعليمية، أو متابعة الخطة.</p>
|
||||
<?php if (!$isCycleReadOnly): ?>
|
||||
<button class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#centerAssessmentModal">إضافة أول تقييم</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table app-table align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>التقييم</th>
|
||||
<th>الفئة</th>
|
||||
<th>الدرجة والوزن</th>
|
||||
<th>البنود</th>
|
||||
<th>الحالة</th>
|
||||
<th>الإجراء</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($assessments as $assessment): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-semibold"><?= e((string) ($assessment['title'] ?? '')) ?></div>
|
||||
</td>
|
||||
<td>
|
||||
<div><?= e((string) ($assessment['category'] ?? '')) ?></div>
|
||||
<div class="table-meta mt-1"><?= assessment_scale_type_badge((string) ($assessment['scale_type'] ?? '')) ?></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-semibold"><?= e((string) round((float) ($assessment['max_score'] ?? 0), 2)) ?></div>
|
||||
<div class="table-meta mt-1">وزن <?= e((string) round((float) ($assessment['weight_percentage'] ?? 0), 2)) ?>٪</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="fw-semibold"><?= e((string) ((int) ($assessment['criteria_count'] ?? 0))) ?></span>
|
||||
</td>
|
||||
<td><?= assessment_active_badge((int) ($assessment['is_active'] ?? 0)) ?></td>
|
||||
<td>
|
||||
<div class="icon-actions">
|
||||
<a
|
||||
class="icon-action"
|
||||
href="<?= e($buildCenterAssessmentCriteriaUrl((int) $application['id'], $selectedCycleId, (int) ($assessment['id'] ?? 0))) ?>"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
data-bs-title="إدارة البنود"
|
||||
title="إدارة البنود"
|
||||
aria-label="إدارة البنود"
|
||||
>
|
||||
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
||||
<path d="M2.5 3A1.5 1.5 0 0 1 4 1.5h8A1.5 1.5 0 0 1 13.5 3v10A1.5 1.5 0 0 1 12 14.5H4A1.5 1.5 0 0 1 2.5 13V3Zm1.5-.5a.5.5 0 0 0-.5.5v10a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5V3a.5.5 0 0 0-.5-.5H4Zm1.25 2.25a.75.75 0 0 1 .75-.75h4a.75.75 0 0 1 0 1.5h-4a.75.75 0 0 1-.75-.75Zm0 3a.75.75 0 0 1 .75-.75h4a.75.75 0 0 1 0 1.5h-4a.75.75 0 0 1-.75-.75Zm0 3a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"/>
|
||||
</svg>
|
||||
<span class="visually-hidden">إدارة البنود</span>
|
||||
</a>
|
||||
<a
|
||||
class="icon-action"
|
||||
href="<?= e($buildCenterAssessmentScoreUrl((int) $application['id'], $selectedCycleId, (int) ($assessment['id'] ?? 0))) ?>"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
data-bs-title="رصد الدرجات"
|
||||
title="رصد الدرجات"
|
||||
aria-label="رصد الدرجات"
|
||||
>
|
||||
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
||||
<path d="M11 1.5a1.5 1.5 0 0 1 1.5 1.5v9.293l-2-2V3a.5.5 0 0 0-.5-.5H4A.5.5 0 0 0 3.5 3v10a.5.5 0 0 0 .5.5h4.5l2 2H4A1.5 1.5 0 0 1 2.5 14V3A1.5 1.5 0 0 1 4 1.5h7Zm2.354 9.146a.5.5 0 0 1 .11.54l-1 3a.5.5 0 0 1-.316.316l-3 1a.5.5 0 0 1-.65-.65l1-3a.5.5 0 0 1 .12-.195l2.5-2.5a.5.5 0 0 1 .707 0l.53.53Zm-6.104-4.396a.75.75 0 0 1 .75-.75h1.5a.75.75 0 0 1 0 1.5H8a.75.75 0 0 1-.75-.75Zm0 3a.75.75 0 0 1 .75-.75h1.5a.75.75 0 0 1 0 1.5H8a.75.75 0 0 1-.75-.75Z"/>
|
||||
</svg>
|
||||
<span class="visually-hidden">رصد الدرجات</span>
|
||||
</a>
|
||||
<a
|
||||
class="icon-action"
|
||||
href="<?= e($buildCenterAssessmentPrintUrl((int) $application['id'], $selectedCycleId, (int) ($assessment['id'] ?? 0))) ?>"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
data-bs-title="طباعة النموذج الرسمي"
|
||||
title="طباعة النموذج الرسمي"
|
||||
aria-label="طباعة النموذج الرسمي"
|
||||
>
|
||||
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
||||
<path d="M4 1.5A1.5 1.5 0 0 0 2.5 3v2.5a.5.5 0 0 0 1 0V3a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v2.5a.5.5 0 0 0 1 0V3A1.5 1.5 0 0 0 12 1.5H4ZM3 6a2 2 0 0 0-2 2v3a2 2 0 0 0 2 2h1v1.5A1.5 1.5 0 0 0 5.5 16h5A1.5 1.5 0 0 0 12 14.5V13h1a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2H3Zm2 7v1.5a.5.5 0 0 0 .5.5h5a.5.5 0 0 0 .5-.5V13H5Zm6-1H5V8h6v4Zm1-3.25a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Z"/>
|
||||
</svg>
|
||||
<span class="visually-hidden">طباعة النموذج الرسمي</span>
|
||||
</a>
|
||||
<?php if (!$isCycleReadOnly): ?>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-action"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#centerAssessmentModal"
|
||||
data-bs-placement="top"
|
||||
data-bs-title="تعديل التقييم"
|
||||
title="تعديل التقييم"
|
||||
aria-label="تعديل التقييم"
|
||||
data-action="edit"
|
||||
data-id="<?= e((string) ($assessment['id'] ?? 0)) ?>"
|
||||
data-title="<?= e((string) ($assessment['title'] ?? '')) ?>"
|
||||
data-category="<?= e((string) ($assessment['category'] ?? '')) ?>"
|
||||
data-scale-type="<?= e((string) ($assessment['scale_type'] ?? '')) ?>"
|
||||
data-max-score="<?= e((string) round((float) ($assessment['max_score'] ?? 0), 2)) ?>"
|
||||
data-weight-percentage="<?= e((string) round((float) ($assessment['weight_percentage'] ?? 0), 2)) ?>"
|
||||
data-is-active="<?= e((string) ($assessment['is_active'] ?? 0)) ?>"
|
||||
data-notes="<?= e((string) ($assessment['notes'] ?? '')) ?>"
|
||||
>
|
||||
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
||||
<path d="M12.854.146a.5.5 0 0 1 .707 0l1.586 1.586a.5.5 0 0 1 0 .707l-9.5 9.5a.5.5 0 0 1-.168.11l-3.5 1.25a.5.5 0 0 1-.64-.64l1.25-3.5a.5.5 0 0 1 .11-.168l9.5-9.5ZM11.207 2 3.354 9.854l-.793 2.22 2.22-.793L12.646 3.414 11.207 2Z"/>
|
||||
</svg>
|
||||
<span class="visually-hidden">تعديل التقييم</span>
|
||||
</button>
|
||||
<?php else: ?>
|
||||
<span class="table-meta align-self-center">قراءة فقط</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php render_pagination($totalAssessments, $limit, $page, ['id' => (int) $application['id'], 'cycle' => $selectedCycleId, 'search' => $filters['search'], 'category' => $filters['category']]); ?>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="app-card h-100 sidebar-card">
|
||||
<div class="section-title mb-3">المراكز المعتمدة</div>
|
||||
<?php if ($approvedCenterCards === []): ?>
|
||||
<div class="empty-state text-center py-4">
|
||||
<div class="empty-title mb-2">لا توجد مراكز معتمدة</div>
|
||||
<p class="text-muted mb-0">ستظهر هنا بمجرد اعتماد أول مركز.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="quick-link-stack">
|
||||
<?php foreach ($approvedCenterCards as $card): ?>
|
||||
<?php $center = $card['application']; ?>
|
||||
<a class="quick-link-item <?= $applicationId === (int) ($center['id'] ?? 0) ? 'active' : '' ?>" href="<?= e($card['url']) ?>">
|
||||
<div>
|
||||
<strong><?= e((string) ($center['center_name'] ?? '')) ?></strong>
|
||||
<span><?= e((string) ($card['selected_cycle']['cycle_name'] ?? 'بدون دورة')) ?> — <?= e((string) ($center['city'] ?? '')) ?></span>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php if ($application && $isApprovedCenter && $selectedCycleId > 0): ?>
|
||||
<!-- Import Global Assessment Modal -->
|
||||
<div class="modal fade" id="importGlobalModal" tabindex="-1" aria-labelledby="importGlobalModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post" action="<?= e($buildCenterAssessmentsUrl((int) $application['id'], $selectedCycleId, ['search' => $filters['search'], 'category' => $filters['category'], 'page' => $page])) ?>">
|
||||
<input type="hidden" name="action" value="import_global">
|
||||
<!-- dummy fields to satisfy validate_assessment_input which expects them -->
|
||||
<input type="hidden" name="title" value="import">
|
||||
<input type="hidden" name="category" value="import">
|
||||
<input type="hidden" name="scale_type" value="percentage">
|
||||
<input type="hidden" name="max_score" value="100">
|
||||
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="importGlobalModalLabel">استيراد من القوالب العامة</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="إغلاق"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>سيتم استيراد القالب المحدد مع كافة بنوده وخصائصه إلى هذا المركز في الدورة المحددة.</p>
|
||||
<div class="mb-3">
|
||||
<label for="global_assessment_id" class="form-label">اختر القالب</label>
|
||||
<select class="form-select" id="global_assessment_id" name="global_assessment_id" required>
|
||||
<option value="">-- اختر القالب --</option>
|
||||
<?php foreach (global_center_assessment_type_options(true) as $globalTemplate): ?>
|
||||
<option value="<?= e((string) $globalTemplate['id']) ?>"><?= e((string) $globalTemplate['title']) ?> (الوزن: <?= e(rtrim(rtrim(number_format((float) ($globalTemplate['weight_percentage'] ?? 0), 2, '.', ''), '0'), '.')) ?>٪)</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" class="btn btn-primary">استيراد الآن</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="centerAssessmentModal" tabindex="-1" aria-labelledby="centerAssessmentModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<form method="post" action="<?= e($buildCenterAssessmentsUrl((int) $application['id'], $selectedCycleId, ['search' => $filters['search'], 'category' => $filters['category'], 'page' => $page])) ?>">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title fs-5" id="centerAssessmentModalLabel">إضافة تقييم مركز</h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="إغلاق"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<?php if (isset($errors['form'])): ?>
|
||||
<div class="alert alert-danger"><?= e($errors['form']) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<input type="hidden" name="action" id="centerAssessmentAction" value="add">
|
||||
<input type="hidden" name="assessment_id" id="centerAssessmentId" value="0">
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="title">اسم التقييم</label>
|
||||
<input type="text" class="form-control <?= isset($errors['title']) ? 'is-invalid' : '' ?>" id="title" name="title" value="<?= e($values['title']) ?>" required>
|
||||
<?php if (isset($errors['title'])): ?><div class="invalid-feedback"><?= e($errors['title']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="category">الفئة</label>
|
||||
<select class="form-select <?= isset($errors['category']) ? 'is-invalid' : '' ?>" id="categoryField" name="category" required>
|
||||
<?php foreach (assessment_category_options() as $categoryOption): ?>
|
||||
<option value="<?= e($categoryOption) ?>" <?= $values['category'] === $categoryOption ? 'selected' : '' ?>><?= e($categoryOption) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php if (isset($errors['category'])): ?><div class="invalid-feedback"><?= e($errors['category']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="scaleTypeField">المقياس</label>
|
||||
<select class="form-select <?= isset($errors['scale_type']) ? 'is-invalid' : '' ?>" id="scaleTypeField" name="scale_type" required>
|
||||
<?php foreach (assessment_scale_type_map() as $scaleKey => $scaleMeta): ?>
|
||||
<option value="<?= e($scaleKey) ?>" <?= $values['scale_type'] === $scaleKey ? 'selected' : '' ?>><?= e((string) $scaleMeta['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php if (isset($errors['scale_type'])): ?><div class="invalid-feedback"><?= e($errors['scale_type']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="maxScoreField">الدرجة النهائية</label>
|
||||
<input type="number" step="0.01" min="0.01" max="1000" class="form-control <?= isset($errors['max_score']) ? 'is-invalid' : '' ?>" id="maxScoreField" name="max_score" value="<?= e($values['max_score']) ?>" required>
|
||||
<?php if (isset($errors['max_score'])): ?><div class="invalid-feedback"><?= e($errors['max_score']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="weightField">الوزن ٪</label>
|
||||
<input type="number" step="0.01" min="0" max="100" class="form-control <?= isset($errors['weight_percentage']) ? 'is-invalid' : '' ?>" id="weightField" name="weight_percentage" value="<?= e($values['weight_percentage']) ?>" required>
|
||||
<?php if (isset($errors['weight_percentage'])): ?><div class="invalid-feedback"><?= e($errors['weight_percentage']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="notesField">ملاحظات</label>
|
||||
<textarea class="form-control" id="notesField" name="notes" rows="3"><?= e($values['notes']) ?></textarea>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<input type="hidden" name="is_active" value="0">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="isActiveField" name="is_active" value="1" <?= $values['is_active'] === '1' ? 'checked' : '' ?>>
|
||||
<label class="form-check-label" for="isActiveField">تفعيل هذا التقييم داخل الدورة الحالية</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" class="btn btn-primary">حفظ التقييم</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(function (element) {
|
||||
new bootstrap.Tooltip(element);
|
||||
});
|
||||
|
||||
const modal = document.getElementById('centerAssessmentModal');
|
||||
if (!modal) return;
|
||||
|
||||
modal.addEventListener('show.bs.modal', function (event) {
|
||||
const trigger = event.relatedTarget;
|
||||
const actionField = document.getElementById('centerAssessmentAction');
|
||||
const idField = document.getElementById('centerAssessmentId');
|
||||
const titleField = document.getElementById('title');
|
||||
const categoryField = document.getElementById('categoryField');
|
||||
const scaleTypeField = document.getElementById('scaleTypeField');
|
||||
const maxScoreField = document.getElementById('maxScoreField');
|
||||
const weightField = document.getElementById('weightField');
|
||||
const isActiveField = document.getElementById('isActiveField');
|
||||
const notesField = document.getElementById('notesField');
|
||||
const modalTitle = document.getElementById('centerAssessmentModalLabel');
|
||||
|
||||
if (!trigger || !trigger.dataset || !trigger.dataset.action) {
|
||||
actionField.value = 'add';
|
||||
idField.value = '0';
|
||||
modalTitle.innerText = 'إضافة تقييم مركز';
|
||||
titleField.value = '<?= e($values['title']) ?>';
|
||||
categoryField.value = '<?= e($values['category']) ?>';
|
||||
scaleTypeField.value = '<?= e($values['scale_type']) ?>';
|
||||
maxScoreField.value = '<?= e($values['max_score']) ?>';
|
||||
weightField.value = '<?= e($values['weight_percentage']) ?>';
|
||||
isActiveField.checked = <?= $values['is_active'] === '1' ? 'true' : 'false' ?>;
|
||||
notesField.value = '<?= e($values['notes']) ?>';
|
||||
return;
|
||||
}
|
||||
|
||||
actionField.value = trigger.dataset.action || 'edit';
|
||||
idField.value = trigger.dataset.id || '0';
|
||||
modalTitle.innerText = 'تعديل تقييم المركز';
|
||||
titleField.value = trigger.dataset.title || '';
|
||||
categoryField.value = trigger.dataset.category || 'اختبار قصير';
|
||||
scaleTypeField.value = trigger.dataset.scaleType || 'percentage';
|
||||
maxScoreField.value = trigger.dataset.maxScore || '100';
|
||||
weightField.value = trigger.dataset.weightPercentage || '0';
|
||||
isActiveField.checked = (trigger.dataset.isActive || '0') === '1';
|
||||
notesField.value = trigger.dataset.notes || '';
|
||||
});
|
||||
|
||||
<?php if ($errors !== []): ?>
|
||||
const bootstrapModal = new bootstrap.Modal(modal);
|
||||
bootstrapModal.show();
|
||||
<?php endif; ?>
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php render_page_end(); ?>
|
||||
173
center_profile.php
Normal file
173
center_profile.php
Normal file
@ -0,0 +1,173 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
$flash = consume_flash();
|
||||
$applicationId = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT) ?: 0;
|
||||
$application = $applicationId > 0 ? get_application($applicationId) : null;
|
||||
|
||||
if (!$application) {
|
||||
http_response_code(404);
|
||||
render_page_start('ملف المركز غير موجود', 'approved', 'تعذر العثور على المركز.');
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="py-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">
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">المركز غير موجود</div>
|
||||
<a class="btn btn-primary" href="applications.php?status=approved">العودة</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php
|
||||
render_page_end();
|
||||
exit;
|
||||
}
|
||||
|
||||
$isApproved = (string) $application['status'] === 'approved';
|
||||
if (!$isApproved) {
|
||||
set_flash('error', 'هذه الصفحة متاحة فقط للمراكز المعتمدة.');
|
||||
header('Location: application_detail.php?id=' . $applicationId);
|
||||
exit;
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
$values = [
|
||||
'center_name' => $application['center_name'],
|
||||
'email' => $application['email'],
|
||||
'phone' => $application['phone'],
|
||||
];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$values['center_name'] = clean_text($_POST['center_name'] ?? '', 190);
|
||||
$values['email'] = clean_text($_POST['email'] ?? '', 190);
|
||||
$values['phone'] = clean_text($_POST['phone'] ?? '', 60);
|
||||
|
||||
if ($values['center_name'] === '') $errors['center_name'] = 'مطلوب';
|
||||
if ($values['email'] === '') $errors['email'] = 'مطلوب';
|
||||
if ($values['phone'] === '') $errors['phone'] = 'مطلوب';
|
||||
|
||||
$logoPath = $application['logo'];
|
||||
$faviconPath = $application['favicon'];
|
||||
|
||||
// Handle Uploads
|
||||
$uploadDir = __DIR__ . '/assets/images/uploads/';
|
||||
|
||||
if (isset($_FILES['logo']) && $_FILES['logo']['error'] === UPLOAD_ERR_OK) {
|
||||
$logoExt = strtolower(pathinfo($_FILES['logo']['name'], PATHINFO_EXTENSION));
|
||||
if (in_array($logoExt, ['png', 'jpg', 'jpeg', 'svg', 'gif'])) {
|
||||
$logoName = 'logo_' . $applicationId . '_' . time() . '.' . $logoExt;
|
||||
if (move_uploaded_file($_FILES['logo']['tmp_name'], $uploadDir . $logoName)) {
|
||||
$logoPath = 'assets/images/uploads/' . $logoName;
|
||||
}
|
||||
} else {
|
||||
$errors['logo'] = 'صيغة غير مدعومة';
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_FILES['favicon']) && $_FILES['favicon']['error'] === UPLOAD_ERR_OK) {
|
||||
$faviconExt = strtolower(pathinfo($_FILES['favicon']['name'], PATHINFO_EXTENSION));
|
||||
if (in_array($faviconExt, ['png', 'ico', 'svg'])) {
|
||||
$faviconName = 'favicon_' . $applicationId . '_' . time() . '.' . $faviconExt;
|
||||
if (move_uploaded_file($_FILES['favicon']['tmp_name'], $uploadDir . $faviconName)) {
|
||||
$faviconPath = 'assets/images/uploads/' . $faviconName;
|
||||
}
|
||||
} else {
|
||||
$errors['favicon'] = 'صيغة غير مدعومة';
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($errors)) {
|
||||
try {
|
||||
$stmt = db()->prepare('UPDATE center_applications SET center_name = ?, email = ?, phone = ?, logo = ?, favicon = ?, updated_at = NOW() WHERE id = ?');
|
||||
$stmt->execute([
|
||||
$values['center_name'],
|
||||
$values['email'],
|
||||
$values['phone'],
|
||||
$logoPath,
|
||||
$faviconPath,
|
||||
$applicationId
|
||||
]);
|
||||
set_flash('success', 'تم تحديث بيانات المركز بنجاح.');
|
||||
header('Location: center_profile.php?id=' . $applicationId);
|
||||
exit;
|
||||
} catch (Throwable $e) {
|
||||
$errors['form'] = 'تعذر الحفظ.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render_page_start('إعدادات المركز', 'approved', 'تعديل بيانات وهوية المركز.', (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">
|
||||
<div class="page-banner mb-4">
|
||||
<h1 class="page-title mb-2">إعدادات وهوية المركز</h1>
|
||||
<p class="page-copy mb-0">تعديل اسم المركز، الشعار (Logo)، الأيقونة (Favicon)، وبيانات التواصل.</p>
|
||||
</div>
|
||||
|
||||
<div class="app-card form-card">
|
||||
<?php if (!empty($errors['form'])): ?>
|
||||
<div class="alert alert-danger mb-4"><?= e($errors['form']) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" enctype="multipart/form-data" novalidate>
|
||||
<div class="row g-4">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">اسم المركز</label>
|
||||
<input class="form-control <?= isset($errors['center_name']) ? 'is-invalid' : '' ?>" name="center_name" value="<?= e($values['center_name']) ?>">
|
||||
<?php if (isset($errors['center_name'])): ?><div class="invalid-feedback"><?= e($errors['center_name']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">البريد الإلكتروني</label>
|
||||
<input type="email" class="form-control <?= isset($errors['email']) ? 'is-invalid' : '' ?>" name="email" value="<?= e($values['email']) ?>">
|
||||
<?php if (isset($errors['email'])): ?><div class="invalid-feedback"><?= e($errors['email']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">رقم الهاتف</label>
|
||||
<input class="form-control <?= isset($errors['phone']) ? 'is-invalid' : '' ?>" name="phone" value="<?= e($values['phone']) ?>">
|
||||
<?php if (isset($errors['phone'])): ?><div class="invalid-feedback"><?= e($errors['phone']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">الشعار (Logo)</label>
|
||||
<?php if ($application['logo']): ?>
|
||||
<div class="mb-2"><img src="<?= e((string)$application['logo']) ?>" alt="Logo" style="max-height: 80px; max-width: 100%; border-radius: 8px;"></div>
|
||||
<?php endif; ?>
|
||||
<input type="file" class="form-control <?= isset($errors['logo']) ? 'is-invalid' : '' ?>" name="logo" accept="image/*">
|
||||
<?php if (isset($errors['logo'])): ?><div class="invalid-feedback"><?= e($errors['logo']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">الأيقونة (Favicon)</label>
|
||||
<?php if ($application['favicon']): ?>
|
||||
<div class="mb-2"><img src="<?= e((string)$application['favicon']) ?>" alt="Favicon" style="max-height: 40px; max-width: 100%; border-radius: 4px;"></div>
|
||||
<?php endif; ?>
|
||||
<input type="file" class="form-control <?= isset($errors['favicon']) ? 'is-invalid' : '' ?>" name="favicon" accept=".ico,.png,.svg">
|
||||
<?php if (isset($errors['favicon'])): ?><div class="invalid-feedback"><?= e($errors['favicon']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions mt-4">
|
||||
<button class="btn btn-primary px-4" type="submit">حفظ التغييرات</button>
|
||||
<a class="btn btn-outline-secondary px-4" href="approved_school.php?id=<?= e((string)$applicationId) ?>">إلغاء</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php render_page_end(); ?>
|
||||
128
center_subjects.php
Normal file
128
center_subjects.php
Normal file
@ -0,0 +1,128 @@
|
||||
<?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;
|
||||
$application = $applicationId > 0 ? get_application($applicationId) : null;
|
||||
$isApproved = $application && (string) $application['status'] === 'approved';
|
||||
|
||||
$selectedCycleId = 0;
|
||||
if ($isApproved) {
|
||||
$cycleContext = resolve_school_cycle_context((int) $application['id'], $application, $requestedCycleId);
|
||||
$selectedCycle = $cycleContext['selected'];
|
||||
$selectedCycleId = $selectedCycle ? (int) ($selectedCycle['id'] ?? 0) : 0;
|
||||
}
|
||||
|
||||
if (!$application || !$isApproved) {
|
||||
http_response_code(404);
|
||||
render_page_start('المركز غير موجود', 'profile');
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="py-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">
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">المركز غير موجود</div>
|
||||
<a class="btn btn-primary" href="applications.php?status=approved">العودة</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php
|
||||
render_page_end();
|
||||
exit;
|
||||
}
|
||||
|
||||
$available_subjects = get_enabled_subjects();
|
||||
$current_subjects = is_string($application['subjects']) ? json_decode($application['subjects'], true) : [];
|
||||
if (!is_array($current_subjects)) $current_subjects = [];
|
||||
$current_subjects_ids = array_map('strval', $current_subjects);
|
||||
|
||||
$errors = [];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'update_subjects') {
|
||||
$selected_subjects = $_POST['subjects'] ?? [];
|
||||
if (!is_array($selected_subjects)) {
|
||||
$selected_subjects = [];
|
||||
}
|
||||
|
||||
// We can allow 0 subjects, but typically at least 1.
|
||||
// Let's allow saving any selection.
|
||||
$selected_subjects_ids = array_map('intval', $selected_subjects);
|
||||
|
||||
try {
|
||||
update_application_subjects((int) $application['id'], $selected_subjects_ids);
|
||||
set_flash('success', 'تم تحديث المواد الدراسية للمركز بنجاح.');
|
||||
$selectedCycleIdStr = $selectedCycleId > 0 ? '&cycle=' . $selectedCycleId : '';
|
||||
header('Location: center_subjects.php?id=' . $application['id'] . $selectedCycleIdStr);
|
||||
exit;
|
||||
} catch (Throwable $e) {
|
||||
$errors['form'] = 'تعذر حفظ البيانات. يرجى المحاولة لاحقاً.';
|
||||
}
|
||||
}
|
||||
|
||||
render_page_start('إدارة المواد الدراسية: ' . (string) $application['center_name'], 'profile', 'تحديد المواد الدراسية التي يتم تدريسها في المركز.');
|
||||
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">
|
||||
<div class="page-banner mb-4">
|
||||
<h1 class="page-title mb-2">المواد الدراسية للمركز</h1>
|
||||
<p class="page-copy mb-0">تحديد وتحديث المواد الدراسية التي يتم تقديمها في هذا المركز. سيتم توفير هذه المواد لاختيارها في التقييمات والجداول.</p>
|
||||
</div>
|
||||
|
||||
<div class="app-card mb-4">
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="update_subjects">
|
||||
<?php if (isset($errors['form'])): ?><div class="alert alert-danger mb-3"><?= e($errors['form']) ?></div><?php endif; ?>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="form-label d-block fw-bold mb-3">اختر المواد المتاحة في المركز</label>
|
||||
<?php if (empty($available_subjects)): ?>
|
||||
<div class="alert alert-info">لا توجد مواد دراسية مفعلة في النظام حالياً. يرجى إضافة مواد من الإدارة المركزية أولاً.</div>
|
||||
<?php else: ?>
|
||||
<div class="row g-3">
|
||||
<?php foreach ($available_subjects as $subject): ?>
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="form-check border rounded-3 p-3 h-100 bg-light-subtle">
|
||||
<input class="form-check-input ms-0 me-2 mt-1 float-end" type="checkbox" name="subjects[]" <?= is_super_admin() ? "" : "disabled" ?> value="<?= e((string) $subject['id']) ?>" id="subject_<?= e((string) $subject['id']) ?>" <?= in_array((string) $subject['id'], $current_subjects_ids, true) ? 'checked' : '' ?>>
|
||||
<label class="form-check-label d-block me-4 pe-2" for="subject_<?= e((string) $subject['id']) ?>">
|
||||
<span class="fw-bold d-block text-primary"><?= e((string) $subject['name']) ?></span>
|
||||
<?php if (!empty($subject['description'])): ?>
|
||||
<small class="text-muted d-block mt-1"><?= e((string) $subject['description']) ?></small>
|
||||
<?php endif; ?>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="text-start">
|
||||
<?php if (is_super_admin()): ?>
|
||||
<button type="submit" class="btn btn-primary px-4">حفظ المواد الدراسية</button>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-warning mb-0">يمكن للمشرف العام فقط تعديل هذه القائمة.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php render_page_end();
|
||||
140
dashboard.php
Normal file
140
dashboard.php
Normal file
@ -0,0 +1,140 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
$flash = consume_flash();
|
||||
$stats = dashboard_metrics();
|
||||
$recentApplications = latest_applications(5);
|
||||
|
||||
render_page_start('المتابعة التشغيلية', 'dashboard', 'لوحة متابعة تشغيلية لعمليات النظام.');
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="py-4 py-lg-5">
|
||||
<div class="container-xxl">
|
||||
<div class="admin-layout row g-4 align-items-start">
|
||||
<div class="col-lg-3 layout-sidebar-column">
|
||||
<?php require __DIR__ . '/includes/sidebar.php'; ?>
|
||||
</div>
|
||||
<div class="col-lg-9 layout-content-column">
|
||||
|
||||
<div class="row align-items-center mb-4 bg-white rounded-4 shadow-sm overflow-hidden" style="border: 1px solid var(--border-color);">
|
||||
<div class="col-lg-7 p-4 p-lg-5">
|
||||
<h1 class="h3 fw-bold mb-3 text-primary">المتابعة التشغيلية</h1>
|
||||
<p class="text-muted mb-4" style="line-height: 1.8; font-size: 0.95rem;">
|
||||
مرحباً بك في واجهة المتابعة التشغيلية. هنا يمكنك مراقبة مؤشرات الأداء الحيوية، استعراض أحدث الطلبات الواردة، واتخاذ إجراءات سريعة لتيسير العمليات اليومية.
|
||||
</p>
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
<a class="btn btn-primary px-4 py-2" href="admin.php">لوحة الإدارة</a>
|
||||
<a class="btn btn-outline-secondary px-4 py-2" href="applications.php?status=submitted">طلبات جديدة</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5 p-0 d-none d-lg-block" style="background-color: var(--bg-color);">
|
||||
<img src="assets/images/pexels/dashboard-analytics.jpg" alt="تحليلات" class="img-fluid w-100 h-100 object-fit-cover" style="min-height: 250px;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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="row g-3 mb-4">
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="app-card stat-tile p-3 h-100 d-flex flex-column justify-content-center shadow-sm">
|
||||
<div class="mini-stat-label text-muted mb-2">إجمالي الطلبات</div>
|
||||
<div class="mini-stat-value fs-3 fw-bold text-primary mb-1"><?= e($stats['all']) ?></div>
|
||||
<div class="mini-stat-copy small text-secondary mt-auto">كافة الطلبات المسجلة</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="app-card stat-tile p-3 h-100 d-flex flex-column justify-content-center shadow-sm">
|
||||
<div class="mini-stat-label text-muted mb-2">قيد الاستلام</div>
|
||||
<div class="mini-stat-value fs-3 fw-bold text-warning mb-1"><?= e($stats['submitted']) ?></div>
|
||||
<div class="mini-stat-copy small text-secondary mt-auto">طلبات جديدة للمراجعة</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="app-card stat-tile p-3 h-100 d-flex flex-column justify-content-center shadow-sm">
|
||||
<div class="mini-stat-label text-muted mb-2">تحت التقييم</div>
|
||||
<div class="mini-stat-value fs-3 fw-bold text-info mb-1"><?= e($stats['under_review']) ?></div>
|
||||
<div class="mini-stat-copy small text-secondary mt-auto">نشطة داخل المسار</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6 col-xl-3">
|
||||
<div class="app-card stat-tile p-3 h-100 d-flex flex-column justify-content-center shadow-sm">
|
||||
<div class="mini-stat-label text-muted mb-2">معتمد</div>
|
||||
<div class="mini-stat-value fs-3 fw-bold text-success mb-1"><?= e($stats['approved']) ?></div>
|
||||
<div class="mini-stat-copy small text-secondary mt-auto">مراكز جاهزة للتشغيل</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-8">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3 mt-2">
|
||||
<h3 class="h6 mb-0 fw-bold text-dark">أحدث الطلبات</h3>
|
||||
<a class="btn btn-link btn-sm text-decoration-none px-0" href="applications.php">عرض الكل ←</a>
|
||||
</div>
|
||||
<div class="app-card p-0 overflow-hidden shadow-sm">
|
||||
<div class="table-responsive">
|
||||
<table class="table app-table align-middle mb-0">
|
||||
<thead class="bg-light">
|
||||
<tr>
|
||||
<th class="ps-4">المرجع</th>
|
||||
<th>المركز</th>
|
||||
<th>المدينة</th>
|
||||
<th>الحالة</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($recentApplications as $application): ?>
|
||||
<tr>
|
||||
<td class="ps-4"><a class="text-decoration-none fw-bold" href="application_detail.php?id=<?= e((string) $application['id']) ?>">#<?= e((string) $application['id']) ?></a></td>
|
||||
<td>
|
||||
<div class="fw-semibold text-dark"><?= e((string) $application['center_name']) ?></div>
|
||||
<div class="text-muted small"><?= e((string) $application['director_name']) ?></div>
|
||||
</td>
|
||||
<td><?= e((string) $application['city']) ?></td>
|
||||
<td><?= status_badge((string) $application['status']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (empty($recentApplications)): ?>
|
||||
<tr><td colspan="4" class="text-center text-muted py-4">لا توجد طلبات حديثة</td></tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="d-flex align-items-center justify-content-between mb-3 mt-2">
|
||||
<h3 class="h6 mb-0 fw-bold text-dark">إجراءات سريعة</h3>
|
||||
</div>
|
||||
<div class="d-flex flex-column gap-3">
|
||||
<a href="center_application.php" class="text-decoration-none">
|
||||
<article class="app-card link-card p-3 border-start border-primary border-4 shadow-sm" style="min-height: auto; transition: transform 0.2s;">
|
||||
<h4 class="h6 fw-bold text-dark mb-1">تقديم طلب جديد</h4>
|
||||
<p class="text-muted small mb-0">فتح نموذج التسجيل لمركز جديد.</p>
|
||||
</article>
|
||||
</a>
|
||||
<a href="applications.php" class="text-decoration-none">
|
||||
<article class="app-card link-card p-3 border-start border-warning border-4 shadow-sm" style="min-height: auto; transition: transform 0.2s;">
|
||||
<h4 class="h6 fw-bold text-dark mb-1">مراجعة الطلبات</h4>
|
||||
<p class="text-muted small mb-0">الذهاب إلى قائمة جميع الطلبات.</p>
|
||||
</article>
|
||||
</a>
|
||||
<a href="modules.php" class="text-decoration-none">
|
||||
<article class="app-card link-card p-3 border-start border-success border-4 shadow-sm" style="min-height: auto; transition: transform 0.2s;">
|
||||
<h4 class="h6 fw-bold text-dark mb-1">هيكل النظام</h4>
|
||||
<p class="text-muted small mb-0">نظرة على بنية الوحدات والميزات.</p>
|
||||
</article>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php render_page_end(); ?>
|
||||
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);
|
||||
18
db/migrations/20260416_alter_assessments_subject.sql
Normal file
18
db/migrations/20260416_alter_assessments_subject.sql
Normal file
@ -0,0 +1,18 @@
|
||||
-- Safely add the column.
|
||||
SET @dbname = DATABASE();
|
||||
SET @tablename = 'school_assessment_types';
|
||||
SET @columnname = 'subject_id';
|
||||
SET @preparedStatement = (SELECT IF(
|
||||
(
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
(table_name = @tablename)
|
||||
AND (table_schema = @dbname)
|
||||
AND (column_name = @columnname)
|
||||
) > 0,
|
||||
"SELECT 1",
|
||||
CONCAT("ALTER TABLE ", @tablename, " ADD ", @columnname, " INT UNSIGNED NULL AFTER cycle_id;")
|
||||
));
|
||||
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||
EXECUTE alterIfNotExists;
|
||||
DEALLOCATE PREPARE alterIfNotExists;
|
||||
@ -0,0 +1 @@
|
||||
ALTER TABLE center_applications ADD COLUMN subjects JSON NULL AFTER notes;
|
||||
40
db/migrations/20260416_alter_school_cycles.sql
Normal file
40
db/migrations/20260416_alter_school_cycles.sql
Normal file
@ -0,0 +1,40 @@
|
||||
-- Safely add the column.
|
||||
SET @dbname = DATABASE();
|
||||
SET @tablename = 'school_cycles';
|
||||
SET @columnname = 'global_cycle_id';
|
||||
SET @preparedStatement = (SELECT IF(
|
||||
(
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
(table_name = @tablename)
|
||||
AND (table_schema = @dbname)
|
||||
AND (column_name = @columnname)
|
||||
) > 0,
|
||||
"SELECT 1",
|
||||
CONCAT("ALTER TABLE ", @tablename, " ADD ", @columnname, " INT UNSIGNED NULL AFTER center_application_id;")
|
||||
));
|
||||
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||
EXECUTE alterIfNotExists;
|
||||
DEALLOCATE PREPARE alterIfNotExists;
|
||||
|
||||
-- Safely add foreign key
|
||||
SET @fkname = 'fk_school_cycles_global_cycle';
|
||||
SET @preparedStatement = (SELECT IF(
|
||||
(
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
|
||||
WHERE
|
||||
(table_name = @tablename)
|
||||
AND (table_schema = @dbname)
|
||||
AND (constraint_name = @fkname)
|
||||
) > 0,
|
||||
"SELECT 1",
|
||||
CONCAT("ALTER TABLE ", @tablename, " ADD CONSTRAINT ", @fkname, " FOREIGN KEY (", @columnname, ") REFERENCES global_cycles(id) ON DELETE SET NULL;")
|
||||
));
|
||||
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||
EXECUTE alterIfNotExists;
|
||||
DEALLOCATE PREPARE alterIfNotExists;
|
||||
|
||||
-- We can make season/year nullable or give defaults, since we will use global cycles instead.
|
||||
-- We'll just alter them to allow NULL
|
||||
ALTER TABLE school_cycles MODIFY season VARCHAR(20) NULL;
|
||||
ALTER TABLE school_cycles MODIFY year SMALLINT UNSIGNED NULL;
|
||||
10
db/migrations/20260416_app_settings.sql
Normal file
10
db/migrations/20260416_app_settings.sql
Normal file
@ -0,0 +1,10 @@
|
||||
CREATE TABLE IF NOT EXISTS app_settings (
|
||||
id INT PRIMARY KEY DEFAULT 1,
|
||||
app_name VARCHAR(255),
|
||||
app_email VARCHAR(255),
|
||||
app_telephone VARCHAR(255),
|
||||
app_logo VARCHAR(255),
|
||||
app_favicon VARCHAR(255),
|
||||
updated_at DATETIME
|
||||
);
|
||||
INSERT IGNORE INTO app_settings (id, app_name) VALUES (1, 'Central Admin');
|
||||
21
db/migrations/20260416_center_applications.sql
Normal file
21
db/migrations/20260416_center_applications.sql
Normal file
@ -0,0 +1,21 @@
|
||||
CREATE TABLE IF NOT EXISTS center_applications (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
center_name VARCHAR(190) NOT NULL,
|
||||
city VARCHAR(120) NOT NULL,
|
||||
center_type VARCHAR(60) NOT NULL,
|
||||
gender_scope VARCHAR(30) NOT NULL,
|
||||
director_name VARCHAR(150) NOT NULL,
|
||||
phone VARCHAR(60) NOT NULL,
|
||||
email VARCHAR(190) NOT NULL,
|
||||
expected_students INT UNSIGNED NOT NULL,
|
||||
start_date DATE NOT NULL,
|
||||
end_date DATE NOT NULL,
|
||||
notes TEXT NULL,
|
||||
status VARCHAR(30) NOT NULL DEFAULT 'submitted',
|
||||
admin_notes TEXT NULL,
|
||||
evaluation_score TINYINT UNSIGNED NULL,
|
||||
submitted_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_center_applications_status (status),
|
||||
INDEX idx_center_applications_submitted_at (submitted_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
45
db/migrations/20260416_global_cycles.sql
Normal file
45
db/migrations/20260416_global_cycles.sql
Normal file
@ -0,0 +1,45 @@
|
||||
CREATE TABLE IF NOT EXISTS global_cycles (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
cycle_name VARCHAR(150) NOT NULL,
|
||||
start_date DATE NOT NULL,
|
||||
end_date DATE NOT NULL,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- We need to safely add the column.
|
||||
SET @dbname = DATABASE();
|
||||
SET @tablename = 'center_applications';
|
||||
SET @columnname = 'global_cycle_id';
|
||||
SET @preparedStatement = (SELECT IF(
|
||||
(
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
(table_name = @tablename)
|
||||
AND (table_schema = @dbname)
|
||||
AND (column_name = @columnname)
|
||||
) > 0,
|
||||
"SELECT 1",
|
||||
CONCAT("ALTER TABLE ", @tablename, " ADD ", @columnname, " INT UNSIGNED NULL AFTER expected_students;")
|
||||
));
|
||||
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||
EXECUTE alterIfNotExists;
|
||||
DEALLOCATE PREPARE alterIfNotExists;
|
||||
|
||||
-- Safely add foreign key
|
||||
SET @fkname = 'fk_center_applications_global_cycle';
|
||||
SET @preparedStatement = (SELECT IF(
|
||||
(
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
|
||||
WHERE
|
||||
(table_name = @tablename)
|
||||
AND (table_schema = @dbname)
|
||||
AND (constraint_name = @fkname)
|
||||
) > 0,
|
||||
"SELECT 1",
|
||||
CONCAT("ALTER TABLE ", @tablename, " ADD CONSTRAINT ", @fkname, " FOREIGN KEY (", @columnname, ") REFERENCES global_cycles(id) ON DELETE SET NULL;")
|
||||
));
|
||||
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||
EXECUTE alterIfNotExists;
|
||||
DEALLOCATE PREPARE alterIfNotExists;
|
||||
18
db/migrations/20260416_school_cycles.sql
Normal file
18
db/migrations/20260416_school_cycles.sql
Normal file
@ -0,0 +1,18 @@
|
||||
CREATE TABLE IF NOT EXISTS school_cycles (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
center_application_id INT UNSIGNED NOT NULL,
|
||||
season VARCHAR(20) NOT NULL,
|
||||
year SMALLINT UNSIGNED NOT NULL,
|
||||
cycle_name VARCHAR(80) NOT NULL,
|
||||
start_date DATE NOT NULL,
|
||||
end_date DATE NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'upcoming',
|
||||
archived_at DATETIME 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_cycle_center_season_year (center_application_id, season, year),
|
||||
INDEX idx_school_cycles_center (center_application_id),
|
||||
INDEX idx_school_cycles_status (status),
|
||||
INDEX idx_school_cycles_dates (start_date, end_date),
|
||||
CONSTRAINT fk_school_cycles_center_application FOREIGN KEY (center_application_id) REFERENCES center_applications(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
70
db/migrations/20260416_school_modules.sql
Normal file
70
db/migrations/20260416_school_modules.sql
Normal file
@ -0,0 +1,70 @@
|
||||
CREATE TABLE IF NOT EXISTS school_students (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
center_application_id INT UNSIGNED NOT NULL,
|
||||
student_code VARCHAR(60) NOT NULL,
|
||||
full_name VARCHAR(190) NOT NULL,
|
||||
gender VARCHAR(20) NOT NULL,
|
||||
grade_level VARCHAR(80) NOT NULL,
|
||||
guardian_name VARCHAR(150) NOT NULL,
|
||||
guardian_phone VARCHAR(60) NOT NULL,
|
||||
birth_date DATE NULL,
|
||||
enrollment_status VARCHAR(30) NOT NULL DEFAULT 'active',
|
||||
notes TEXT 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_student_code (center_application_id, student_code),
|
||||
INDEX idx_school_students_center (center_application_id),
|
||||
INDEX idx_school_students_status (enrollment_status),
|
||||
CONSTRAINT fk_school_students_center_application FOREIGN KEY (center_application_id) REFERENCES center_applications(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS school_teachers (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
center_application_id INT UNSIGNED NOT NULL,
|
||||
full_name VARCHAR(190) NOT NULL,
|
||||
role_title VARCHAR(120) NOT NULL,
|
||||
specialization VARCHAR(150) NULL,
|
||||
phone VARCHAR(60) NULL,
|
||||
email VARCHAR(190) NULL,
|
||||
employment_status VARCHAR(30) NOT NULL DEFAULT 'active',
|
||||
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_teachers_center (center_application_id),
|
||||
INDEX idx_school_teachers_status (employment_status),
|
||||
CONSTRAINT fk_school_teachers_center_application FOREIGN KEY (center_application_id) REFERENCES center_applications(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS school_assessment_types (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
center_application_id INT UNSIGNED NOT NULL,
|
||||
title VARCHAR(150) NOT NULL,
|
||||
category VARCHAR(80) NOT NULL,
|
||||
scale_type VARCHAR(40) NOT NULL DEFAULT 'percentage',
|
||||
max_score DECIMAL(6,2) NOT NULL DEFAULT 100.00,
|
||||
weight_percentage DECIMAL(5,2) NOT NULL DEFAULT 0.00,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
notes TEXT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_school_assessments_center (center_application_id),
|
||||
INDEX idx_school_assessments_active (is_active),
|
||||
CONSTRAINT fk_school_assessments_center_application FOREIGN KEY (center_application_id) REFERENCES center_applications(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS school_attendance_records (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
center_application_id INT UNSIGNED NOT NULL,
|
||||
student_id INT UNSIGNED NOT NULL,
|
||||
attendance_date DATE NOT NULL,
|
||||
attendance_status VARCHAR(30) NOT NULL DEFAULT 'absent',
|
||||
absence_reason VARCHAR(190) NULL,
|
||||
notes TEXT 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_attendance_student_date (student_id, attendance_date),
|
||||
INDEX idx_school_attendance_center (center_application_id),
|
||||
INDEX idx_school_attendance_date (attendance_date),
|
||||
CONSTRAINT fk_school_attendance_center_application FOREIGN KEY (center_application_id) REFERENCES center_applications(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_school_attendance_student FOREIGN KEY (student_id) REFERENCES school_students(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
8
db/migrations/20260416_subjects.sql
Normal file
8
db/migrations/20260416_subjects.sql
Normal file
@ -0,0 +1,8 @@
|
||||
CREATE TABLE IF NOT EXISTS subjects (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
status ENUM('enabled', 'disabled') DEFAULT 'enabled',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@ -0,0 +1,2 @@
|
||||
ALTER TABLE center_assessment_types ADD COLUMN global_template_id INT UNSIGNED NULL AFTER cycle_id;
|
||||
ALTER TABLE center_assessment_types ADD CONSTRAINT fk_center_assessment_types_global_template FOREIGN KEY (global_template_id) REFERENCES global_center_assessment_types(id) ON DELETE SET NULL;
|
||||
@ -0,0 +1,4 @@
|
||||
ALTER TABLE app_settings
|
||||
ADD COLUMN completion_certificate_template VARCHAR(40) NOT NULL DEFAULT 'modern' AFTER app_slogan,
|
||||
ADD COLUMN completion_certificate_tagline VARCHAR(255) DEFAULT 'شهادة إتمام وتكريم' AFTER completion_certificate_template,
|
||||
ADD COLUMN completion_certificate_message TEXT DEFAULT NULL AFTER completion_certificate_tagline;
|
||||
@ -0,0 +1,2 @@
|
||||
ALTER TABLE school_teachers
|
||||
ADD COLUMN subject_ids TEXT NULL AFTER specialization;
|
||||
22
db/migrations/20260417_assessment_categories.sql
Normal file
22
db/migrations/20260417_assessment_categories.sql
Normal file
@ -0,0 +1,22 @@
|
||||
CREATE TABLE IF NOT EXISTS `assessment_categories` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`description` TEXT,
|
||||
`status` ENUM('enabled', 'disabled') NOT NULL DEFAULT 'enabled',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY `idx_name` (`name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT IGNORE INTO `assessment_categories` (`name`, `status`) VALUES
|
||||
('تشخيصي', 'enabled'),
|
||||
('واجب', 'enabled'),
|
||||
('مشاركة', 'enabled'),
|
||||
('تقييم', 'enabled'),
|
||||
('تلاوة', 'enabled'),
|
||||
('حفظ', 'enabled'),
|
||||
('مراجعة', 'enabled'),
|
||||
('اختبار قصير', 'enabled'),
|
||||
('اختبار دوري', 'enabled'),
|
||||
('اختبار نهائي', 'enabled'),
|
||||
('أخرى', 'enabled');
|
||||
83
db/migrations/20260417_center_assessment_system.sql
Normal file
83
db/migrations/20260417_center_assessment_system.sql
Normal file
@ -0,0 +1,83 @@
|
||||
CREATE TABLE IF NOT EXISTS center_assessment_types (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
center_application_id INT UNSIGNED NOT NULL,
|
||||
cycle_id INT UNSIGNED NOT NULL,
|
||||
title VARCHAR(190) NOT NULL,
|
||||
category VARCHAR(80) NOT NULL DEFAULT 'أداء',
|
||||
scale_type VARCHAR(40) NOT NULL DEFAULT 'percentage',
|
||||
max_score DECIMAL(8,2) NOT NULL DEFAULT 100.00,
|
||||
weight_percentage DECIMAL(5,2) NOT NULL DEFAULT 0.00,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
notes TEXT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_center_assessment_types_center (center_application_id),
|
||||
INDEX idx_center_assessment_types_cycle (cycle_id),
|
||||
INDEX idx_center_assessment_types_active (center_application_id, cycle_id, is_active),
|
||||
CONSTRAINT fk_center_assessment_types_center_application FOREIGN KEY (center_application_id) REFERENCES center_applications(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_center_assessment_types_cycle FOREIGN KEY (cycle_id) REFERENCES school_cycles(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS center_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_center_assessment_criteria_center (center_application_id),
|
||||
INDEX idx_center_assessment_criteria_cycle (cycle_id),
|
||||
INDEX idx_center_assessment_criteria_assessment (assessment_type_id),
|
||||
CONSTRAINT fk_center_assessment_criteria_center_application FOREIGN KEY (center_application_id) REFERENCES center_applications(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_center_assessment_criteria_cycle FOREIGN KEY (cycle_id) REFERENCES school_cycles(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_center_assessment_criteria_assessment FOREIGN KEY (assessment_type_id) REFERENCES center_assessment_types(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS center_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,
|
||||
score DECIMAL(8,2) NULL,
|
||||
max_score DECIMAL(8,2) NOT NULL DEFAULT 100.00,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'draft',
|
||||
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_center_assessment_score (center_application_id, cycle_id, assessment_type_id),
|
||||
INDEX idx_center_assessment_scores_cycle (cycle_id),
|
||||
INDEX idx_center_assessment_scores_assessment (assessment_type_id),
|
||||
INDEX idx_center_assessment_scores_status (status),
|
||||
CONSTRAINT fk_center_assessment_scores_center_application FOREIGN KEY (center_application_id) REFERENCES center_applications(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_center_assessment_scores_cycle FOREIGN KEY (cycle_id) REFERENCES school_cycles(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_center_assessment_scores_assessment FOREIGN KEY (assessment_type_id) REFERENCES center_assessment_types(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS center_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,
|
||||
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_center_assessment_score_item (assessment_score_id, criterion_id),
|
||||
INDEX idx_center_assessment_score_items_center (center_application_id),
|
||||
INDEX idx_center_assessment_score_items_cycle (cycle_id),
|
||||
INDEX idx_center_assessment_score_items_assessment (assessment_type_id),
|
||||
INDEX idx_center_assessment_score_items_criterion (criterion_id),
|
||||
CONSTRAINT fk_center_assessment_score_items_center_application FOREIGN KEY (center_application_id) REFERENCES center_applications(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_center_assessment_score_items_cycle FOREIGN KEY (cycle_id) REFERENCES school_cycles(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_center_assessment_score_items_score FOREIGN KEY (assessment_score_id) REFERENCES center_assessment_scores(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_center_assessment_score_items_assessment FOREIGN KEY (assessment_type_id) REFERENCES center_assessment_types(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_center_assessment_score_items_criterion FOREIGN KEY (criterion_id) REFERENCES center_assessment_criteria(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
29
db/migrations/20260417_global_center_assessments.sql
Normal file
29
db/migrations/20260417_global_center_assessments.sql
Normal file
@ -0,0 +1,29 @@
|
||||
CREATE TABLE IF NOT EXISTS global_center_assessment_types (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
title VARCHAR(190) NOT NULL,
|
||||
category VARCHAR(80) NOT NULL DEFAULT 'أداء',
|
||||
scale_type VARCHAR(40) NOT NULL DEFAULT 'percentage',
|
||||
max_score DECIMAL(8,2) NOT NULL DEFAULT 100.00,
|
||||
weight_percentage DECIMAL(5,2) NOT NULL DEFAULT 0.00,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
notes TEXT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_global_center_assessment_types_active (is_active),
|
||||
INDEX idx_global_center_assessment_types_category (category)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS global_center_assessment_criteria (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
assessment_type_id INT UNSIGNED NOT NULL,
|
||||
title VARCHAR(190) NOT NULL,
|
||||
max_score DECIMAL(8,2) NOT NULL DEFAULT 0.00,
|
||||
sort_order INT UNSIGNED NOT NULL DEFAULT 1,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
notes TEXT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_global_center_assessment_criteria_assessment (assessment_type_id),
|
||||
INDEX idx_global_center_assessment_criteria_active (assessment_type_id, is_active),
|
||||
CONSTRAINT fk_global_center_assessment_criteria_assessment FOREIGN KEY (assessment_type_id) REFERENCES global_center_assessment_types(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
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;
|
||||
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;
|
||||
216
execute_global_assessment.php
Normal file
216
execute_global_assessment.php
Normal file
@ -0,0 +1,216 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
$templateId = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT) ?: 0;
|
||||
if ($templateId <= 0) {
|
||||
set_flash('error', 'اختر قالب التقييم أولاً.');
|
||||
header('Location: global_center_assessments.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$template = get_global_center_assessment_type($templateId);
|
||||
if (!$template) {
|
||||
set_flash('error', 'القالب غير موجود.');
|
||||
header('Location: global_center_assessments.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$requestedCycleName = $_GET['cycle'] ?? '';
|
||||
|
||||
// Fetch distinct cycle names from active cycles
|
||||
$pdo = db();
|
||||
$cyclesStmt = $pdo->query("SELECT DISTINCT cycle_name FROM school_cycles WHERE status != 'archived' ORDER BY cycle_name ASC");
|
||||
$cycles = $cyclesStmt->fetchAll(PDO::FETCH_COLUMN) ?: [];
|
||||
|
||||
// Find default cycle if not selected
|
||||
if (empty($requestedCycleName) && count($cycles) > 0) {
|
||||
$requestedCycleName = $cycles[0];
|
||||
}
|
||||
|
||||
// Action: assess a specific center
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'assess') {
|
||||
$centerId = filter_input(INPUT_POST, 'center_id', FILTER_VALIDATE_INT) ?: 0;
|
||||
$cycleId = filter_input(INPUT_POST, 'cycle_id', FILTER_VALIDATE_INT) ?: 0;
|
||||
$cycleName = $_POST['cycle_name'] ?? '';
|
||||
|
||||
if ($centerId > 0 && $cycleId > 0) {
|
||||
// Check if already imported
|
||||
$stmt = $pdo->prepare('SELECT id FROM center_assessment_types WHERE center_application_id = :center_id AND cycle_id = :cycle_id AND global_template_id = :template_id LIMIT 1');
|
||||
$stmt->execute([
|
||||
':center_id' => $centerId,
|
||||
':cycle_id' => $cycleId,
|
||||
':template_id' => $templateId
|
||||
]);
|
||||
$existingAssessmentId = $stmt->fetchColumn();
|
||||
|
||||
if ($existingAssessmentId) {
|
||||
$assessmentId = (int)$existingAssessmentId;
|
||||
} else {
|
||||
// Import it
|
||||
try {
|
||||
$assessmentId = import_global_center_assessment_to_center($templateId, $centerId, $cycleId);
|
||||
} catch (Exception $e) {
|
||||
set_flash('error', 'فشل استيراد التقييم للمركز.');
|
||||
header("Location: execute_global_assessment.php?id={$templateId}&cycle=" . urlencode($cycleName));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$returnUrl = urlencode("execute_global_assessment.php?id={$templateId}&cycle=" . urlencode($cycleName));
|
||||
header("Location: center_assessment_score_sheet.php?id={$centerId}&cycle={$cycleId}&assessment_id={$assessmentId}&return_url={$returnUrl}");
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch all approved centers and their assessment status for this template
|
||||
$centers = [];
|
||||
if (!empty($requestedCycleName)) {
|
||||
// Join with school_cycles by cycle_name, then left join with center_assessment_types and center_assessment_scores
|
||||
$query = "
|
||||
SELECT
|
||||
c.id, c.center_name, c.city,
|
||||
sc.id AS center_cycle_id,
|
||||
cat.id AS assessment_type_id,
|
||||
cas.status AS score_status,
|
||||
cas.score,
|
||||
cas.max_score
|
||||
FROM center_applications c
|
||||
JOIN school_cycles sc ON sc.center_application_id = c.id AND sc.cycle_name = :cycle_name
|
||||
LEFT JOIN center_assessment_types cat ON cat.center_application_id = c.id
|
||||
AND cat.cycle_id = sc.id
|
||||
AND cat.global_template_id = :template_id
|
||||
LEFT JOIN center_assessment_scores cas ON cas.assessment_type_id = cat.id
|
||||
WHERE c.status = 'approved'
|
||||
ORDER BY c.center_name ASC
|
||||
";
|
||||
$stmt = $pdo->prepare($query);
|
||||
$stmt->execute([
|
||||
':cycle_name' => $requestedCycleName,
|
||||
':template_id' => $templateId
|
||||
]);
|
||||
$centers = $stmt->fetchAll() ?: [];
|
||||
}
|
||||
|
||||
$pageTitle = 'تطبيق التقييم: ' . (string) ($template['title'] ?? '');
|
||||
$pageDescription = 'تنفيذ التقييم العام على المراكز المعتمدة.';
|
||||
render_page_start($pageTitle, 'admin', $pageDescription);
|
||||
$flash = consume_flash();
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="py-4 py-lg-5">
|
||||
<div class="container-xxl">
|
||||
<div class="admin-layout row g-4 align-items-start">
|
||||
<div class="col-lg-3 layout-sidebar-column">
|
||||
<?php require __DIR__ . '/includes/sidebar.php'; ?>
|
||||
</div>
|
||||
<div class="col-lg-9 layout-content-column">
|
||||
<div class="page-banner admin-hero mb-4">
|
||||
<div class="row g-4 align-items-center">
|
||||
<div class="col-lg-8">
|
||||
<span class="admin-kicker mb-3">تطبيق قالب التقييم</span>
|
||||
<h1 class="page-title mb-3"><?= e((string) ($template['title'] ?? '')) ?></h1>
|
||||
<p class="page-copy mb-3">من هذه الشاشة يمكنك المرور على جميع المراكز ورصد درجاتها بناءً على هذا القالب الموحد.</p>
|
||||
<div class="hero-meta">
|
||||
<span>الوزن: <?= e((string) ($template['weight_percentage'] ?? '0')) ?>%</span>
|
||||
<span>الدرجة القصوى: <?= e((string) ($template['max_score'] ?? '0')) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 text-lg-end">
|
||||
<a class="btn btn-outline-secondary" href="global_center_assessments.php">العودة للقوالب</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-card mb-4">
|
||||
<form method="get" class="row g-3 align-items-end">
|
||||
<input type="hidden" name="id" value="<?= e((string)$templateId) ?>">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label" for="cycle">الدورة المستهدفة</label>
|
||||
<select class="form-select" id="cycle" name="cycle" onchange="this.form.submit()">
|
||||
<?php if (count($cycles) === 0): ?>
|
||||
<option value="">لا توجد دورات</option>
|
||||
<?php else: ?>
|
||||
<?php foreach ($cycles as $c): ?>
|
||||
<option value="<?= e((string)$c) ?>" <?= $c === $requestedCycleName ? 'selected' : '' ?>>
|
||||
<?= e((string)$c) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 d-grid">
|
||||
<button type="submit" class="btn btn-primary">عرض المراكز</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($requestedCycleName) && count($centers) > 0): ?>
|
||||
<div class="app-card">
|
||||
<div class="table-responsive">
|
||||
<table class="table app-table align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>المركز</th>
|
||||
<th>المنطقة</th>
|
||||
<th>الحالة</th>
|
||||
<th>الدرجة</th>
|
||||
<th class="text-end">الإجراءات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($centers as $center):
|
||||
$status = (string) ($center['score_status'] ?? '');
|
||||
$hasScore = $center['score'] !== null;
|
||||
$scoreVal = $hasScore ? rtrim(rtrim(number_format((float)$center['score'], 2, '.', ''), '0'), '.') : '—';
|
||||
$maxScore = $center['max_score'] !== null ? rtrim(rtrim(number_format((float)$center['max_score'], 2, '.', ''), '0'), '.') : '';
|
||||
|
||||
$statusBadge = '<span class="badge bg-secondary">لم يبدأ الرصد</span>';
|
||||
if ($status === 'draft') $statusBadge = '<span class="badge bg-warning text-dark">مسودة</span>';
|
||||
if ($status === 'completed') $statusBadge = '<span class="badge bg-success">مكتمل</span>';
|
||||
if ($status === 'waived') $statusBadge = '<span class="badge bg-info">مستثنى</span>';
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="fw-semibold"><?= e((string) ($center['center_name'] ?? '')) ?></div>
|
||||
</td>
|
||||
<td><?= e((string) ($center['city'] ?? '—')) ?></td>
|
||||
<td><?= $statusBadge ?></td>
|
||||
<td>
|
||||
<?php if ($hasScore): ?>
|
||||
<span class="fw-bold"><?= e($scoreVal) ?></span> / <?= e($maxScore) ?>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">—</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<form method="post" style="display:inline-block;">
|
||||
<input type="hidden" name="action" value="assess">
|
||||
<input type="hidden" name="center_id" value="<?= e((string)$center['id']) ?>">
|
||||
<input type="hidden" name="cycle_id" value="<?= e((string)$center['center_cycle_id']) ?>">
|
||||
<input type="hidden" name="cycle_name" value="<?= e((string)$requestedCycleName) ?>">
|
||||
<?php if ($status === 'completed'): ?>
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary">تعديل الرصد</button>
|
||||
<?php else: ?>
|
||||
<button type="submit" class="btn btn-sm btn-primary">رصد</button>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php elseif (!empty($requestedCycleName)): ?>
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">لا توجد مراكز معتمدة</div>
|
||||
<p class="text-muted">لم يتم العثور على مراكز معتمدة لتقييمها في هذه الدورة.</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<?php render_page_end(); ?>
|
||||
49
fix_app.php
Normal file
49
fix_app.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
$file = 'includes/app.php';
|
||||
$content = file_get_contents($file);
|
||||
|
||||
$search1 = "ensure_center_application_schema(\$pdo);";
|
||||
$replace1 = "ensure_app_settings_schema(\$pdo);\n ensure_center_application_schema(\$pdo);";
|
||||
$content = str_replace($search1, $replace1, $content);
|
||||
|
||||
$search2 = "function ensure_center_application_schema(PDO \$pdo): void";
|
||||
$replace2 = "function ensure_app_settings_schema(PDO \$pdo): void
|
||||
{
|
||||
\$pdo->exec(\"
|
||||
CREATE TABLE IF NOT EXISTS app_settings (
|
||||
id INT PRIMARY KEY DEFAULT 1,
|
||||
app_name VARCHAR(190) NOT NULL DEFAULT 'Central Admin',
|
||||
app_email VARCHAR(190) DEFAULT NULL,
|
||||
app_telephone VARCHAR(60) DEFAULT NULL,
|
||||
app_logo VARCHAR(255) DEFAULT NULL,
|
||||
app_favicon VARCHAR(255) DEFAULT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
\");
|
||||
\$pdo->exec(\"INSERT IGNORE INTO app_settings (id, app_name) VALUES (1, 'Central Admin')\");
|
||||
}
|
||||
|
||||
function get_app_settings(): array
|
||||
{
|
||||
\$pdo = db_connection();
|
||||
\$stmt = \$pdo->query('SELECT * FROM app_settings WHERE id = 1');
|
||||
\$res = \$stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!\$res) {
|
||||
return [
|
||||
'app_name' => 'Central Admin',
|
||||
'app_email' => '',
|
||||
'app_telephone' => '',
|
||||
'app_logo' => '',
|
||||
'app_favicon' => ''
|
||||
];
|
||||
}
|
||||
return \$res;
|
||||
}
|
||||
|
||||
function ensure_center_application_schema(PDO \$pdo): void";
|
||||
|
||||
$content = str_replace($search2, $replace2, $content);
|
||||
|
||||
file_put_contents($file, $content);
|
||||
echo "Done\n";
|
||||
|
||||
52
fix_app_settings.py
Normal file
52
fix_app_settings.py
Normal file
@ -0,0 +1,52 @@
|
||||
import re
|
||||
|
||||
with open('includes/app.php', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Add ensure_app_settings_schema call
|
||||
content = content.replace(
|
||||
'ensure_center_application_schema($pdo);',
|
||||
'ensure_app_settings_schema($pdo);
|
||||
ensure_center_application_schema($pdo);'
|
||||
)
|
||||
|
||||
# Add function definitions before ensure_center_application_schema definition
|
||||
new_funcs = """function ensure_app_settings_schema(PDO $pdo): void
|
||||
{
|
||||
$pdo->exec("
|
||||
CREATE TABLE IF NOT EXISTS app_settings (
|
||||
id INT PRIMARY KEY DEFAULT 1,
|
||||
app_name VARCHAR(190) NOT NULL DEFAULT 'Central Admin',
|
||||
app_email VARCHAR(190) DEFAULT NULL,
|
||||
app_telephone VARCHAR(60) DEFAULT NULL,
|
||||
app_logo VARCHAR(255) DEFAULT NULL,
|
||||
app_favicon VARCHAR(255) DEFAULT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
");
|
||||
$pdo->exec("INSERT IGNORE INTO app_settings (id, app_name) VALUES (1, 'Central Admin')");
|
||||
}
|
||||
|
||||
function get_app_settings(): array
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->query('SELECT * FROM app_settings WHERE id = 1');
|
||||
$res = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$res) {
|
||||
return [
|
||||
'app_name' => 'Central Admin',
|
||||
'app_email' => '',
|
||||
'app_telephone' => '',
|
||||
'app_logo' => '',
|
||||
'app_favicon' => ''
|
||||
];
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
function ensure_center_application_schema(PDO $pdo): void"""
|
||||
|
||||
content = content.replace('function ensure_center_application_schema(PDO $pdo): void', new_funcs)
|
||||
|
||||
with open('includes/app.php', 'w') as f:
|
||||
f.write(content)
|
||||
68
fix_import.py
Normal file
68
fix_import.py
Normal file
@ -0,0 +1,68 @@
|
||||
import re
|
||||
|
||||
with open('includes/cycles.php', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
new_func = """function import_global_center_assessment_to_center(int $globalAssessmentId, int $centerApplicationId, int $cycleId): int
|
||||
{
|
||||
$pdo = db();
|
||||
$globalAssessment = get_global_center_assessment_type($globalAssessmentId);
|
||||
if (!$globalAssessment) {
|
||||
throw new InvalidArgumentException("Global assessment template not found.");
|
||||
}
|
||||
|
||||
$globalCriteria = list_global_center_assessment_criteria_by_assessment($globalAssessmentId, true);
|
||||
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
|
||||
$stmt = $pdo->prepare('INSERT INTO center_assessment_types
|
||||
(center_application_id, cycle_id, global_template_id, title, category, scale_type, max_score, weight_percentage, is_active, notes)
|
||||
VALUES (:center_application_id, :cycle_id, :global_template_id, :title, :category, :scale_type, :max_score, :weight_percentage, :is_active, :notes)');
|
||||
|
||||
$stmt->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':cycle_id', $cycleId, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':global_template_id', $globalAssessmentId, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':title', $globalAssessment['title'], PDO::PARAM_STR);
|
||||
$stmt->bindValue(':category', $globalAssessment['category'], PDO::PARAM_STR);
|
||||
$stmt->bindValue(':scale_type', $globalAssessment['scale_type'], PDO::PARAM_STR);
|
||||
$stmt->bindValue(':max_score', $globalAssessment['max_score'], PDO::PARAM_STR);
|
||||
$stmt->bindValue(':weight_percentage', $globalAssessment['weight_percentage'], PDO::PARAM_STR);
|
||||
$stmt->bindValue(':is_active', $globalAssessment['is_active'], PDO::PARAM_INT);
|
||||
$stmt->bindValue(':notes', $globalAssessment['notes'] ?? null, PDO::PARAM_STR);
|
||||
|
||||
$stmt->execute();
|
||||
$newAssessmentId = (int) $pdo->lastInsertId();
|
||||
|
||||
if ($globalCriteria !== []) {
|
||||
$stmtCrit = $pdo->prepare('INSERT INTO center_assessment_criteria
|
||||
(center_application_id, cycle_id, assessment_type_id, title, notes, max_score, sort_order, is_active)
|
||||
VALUES (:center_application_id, :cycle_id, :assessment_type_id, :title, :notes, :max_score, :sort_order, :is_active)');
|
||||
|
||||
foreach ($globalCriteria as $criteria) {
|
||||
$stmtCrit->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT);
|
||||
$stmtCrit->bindValue(':cycle_id', $cycleId, PDO::PARAM_INT);
|
||||
$stmtCrit->bindValue(':assessment_type_id', $newAssessmentId, PDO::PARAM_INT);
|
||||
$stmtCrit->bindValue(':title', $criteria['title'], PDO::PARAM_STR);
|
||||
$stmtCrit->bindValue(':notes', $criteria['notes'] ?? null, PDO::PARAM_STR);
|
||||
$stmtCrit->bindValue(':max_score', $criteria['max_score'], PDO::PARAM_STR);
|
||||
$stmtCrit->bindValue(':sort_order', $criteria['sort_order'] ?? 0, PDO::PARAM_INT);
|
||||
$stmtCrit->bindValue(':is_active', $criteria['is_active'], PDO::PARAM_INT);
|
||||
$stmtCrit->execute();
|
||||
}
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
return $newAssessmentId;
|
||||
} catch (PDOException $e) {
|
||||
$pdo->rollBack();
|
||||
error_log("Failed to import global center assessment: " . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}"""
|
||||
|
||||
content = re.sub(r'function import_global_center_assessment_to_center.*?catch \(PDOException \$e\) \{\s*\$pdo->rollBack\(\);\s*error_log\("Failed to import global center assessment: " \. \$e->getMessage\(\)\);\s*throw \$e;\s*\}\s*\}', new_func, content, flags=re.DOTALL)
|
||||
|
||||
with open('includes/cycles.php', 'w') as f:
|
||||
f.write(content)
|
||||
print("Fixed import function.")
|
||||
7
fix_sidebar.py
Normal file
7
fix_sidebar.py
Normal file
@ -0,0 +1,7 @@
|
||||
with open('includes/sidebar.php', 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
content = content.replace("href=\"approved_school.php', 'center_profile.php?id=", "href=\"approved_school.php?id=")
|
||||
|
||||
with open('includes/sidebar.php', 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
222
global_center_assessment_criteria.php
Normal file
222
global_center_assessment_criteria.php
Normal file
@ -0,0 +1,222 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
if (!is_super_admin()) {
|
||||
http_response_code(403);
|
||||
render_page_start('صلاحيات غير كافية', 'admin', 'هذه الصفحة مخصصة للمشرف العام لإدارة بنود قوالب تقييم المراكز.');
|
||||
?>
|
||||
<section class="py-5 text-center">
|
||||
<div class="container-xxl">
|
||||
<h1 class="mb-3">عذراً</h1>
|
||||
<p>هذه الصفحة مخصصة للمشرف العام فقط.</p>
|
||||
<a href="admin.php" class="btn btn-primary mt-3">العودة إلى لوحة الإدارة</a>
|
||||
</div>
|
||||
</section>
|
||||
<?php
|
||||
render_page_end();
|
||||
exit;
|
||||
}
|
||||
|
||||
$assessmentId = filter_input(INPUT_GET, 'assessment_id', FILTER_VALIDATE_INT) ?: 0;
|
||||
if ($assessmentId <= 0) {
|
||||
set_flash('error', 'اختر أولاً قالب تقييم من صفحة القوالب العامة.');
|
||||
header('Location: global_center_assessments.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$assessment = get_global_center_assessment_type($assessmentId);
|
||||
if (!$assessment) {
|
||||
set_flash('error', 'قالب التقييم المطلوب غير موجود أو تم حذفه.');
|
||||
header('Location: global_center_assessments.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$flash = consume_flash();
|
||||
$criteria = list_global_center_assessment_criteria_by_assessment($assessmentId);
|
||||
$criteriaMetrics = global_center_assessment_criteria_metrics($assessmentId);
|
||||
$errors = [];
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
[$submittedData, $errors] = validate_global_center_assessment_criteria_input($assessmentId, $_POST);
|
||||
if ($errors === []) {
|
||||
try {
|
||||
save_global_center_assessment_criteria($assessmentId, $submittedData);
|
||||
set_flash('success', 'تم حفظ بنود قالب تقييم المراكز بنجاح.');
|
||||
header('Location: global_center_assessment_criteria.php?assessment_id=' . $assessmentId);
|
||||
exit;
|
||||
} catch (Throwable $exception) {
|
||||
$errors['form'] = 'تعذر حفظ البنود حالياً. يرجى المحاولة مرة أخرى.';
|
||||
$criteria = $submittedData['criteria'];
|
||||
}
|
||||
} else {
|
||||
$criteria = $submittedData['criteria'];
|
||||
}
|
||||
}
|
||||
|
||||
$blankRows = 3;
|
||||
for ($i = 0; $i < $blankRows; $i++) {
|
||||
$criteria[] = [
|
||||
'id' => 0,
|
||||
'title' => '',
|
||||
'max_score' => '',
|
||||
'notes' => '',
|
||||
'is_active' => '1',
|
||||
];
|
||||
}
|
||||
|
||||
render_page_start(
|
||||
'بنود قالب تقييم المراكز',
|
||||
'admin',
|
||||
'إدارة البنود والمعايير التفصيلية للقالب العام الذي سيُستخدم لاحقاً عند تقييم المراكز.'
|
||||
);
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="py-4 py-lg-5">
|
||||
<div class="container-xxl">
|
||||
<div class="admin-layout row g-4 align-items-start">
|
||||
<div class="col-lg-3 layout-sidebar-column">
|
||||
<?php require __DIR__ . '/includes/sidebar.php'; ?>
|
||||
</div>
|
||||
<div class="col-lg-9 layout-content-column">
|
||||
|
||||
<div class="page-banner mb-4">
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-8">
|
||||
<span class="eyebrow mb-3">بناء المعايير</span>
|
||||
<h1 class="page-title mb-3">بنود القالب: <?= e((string) ($assessment['title'] ?? '')) ?></h1>
|
||||
<p class="page-copy mb-3">أضف هنا البنود التي سيعتمد عليها المشرف العام أو المقيم المسؤول عند تقييم المراكز. مجموع البنود النشطة يحدّث الدرجة القصوى تلقائياً.</p>
|
||||
<div class="hero-meta">
|
||||
<span>الفئة <?= e((string) ($assessment['category'] ?? '')) ?></span>
|
||||
<span>المقياس <?= e(assessment_scale_type_label((string) ($assessment['scale_type'] ?? 'percentage'))) ?></span>
|
||||
<span>الوزن <?= e(rtrim(rtrim(number_format((float) ($assessment['weight_percentage'] ?? 0), 2, '.', ''), '0'), '.')) ?>٪</span>
|
||||
</div>
|
||||
<div class="cta-stack mt-4">
|
||||
<a class="btn btn-outline-secondary" href="global_center_assessments.php">العودة إلى القوالب</a>
|
||||
<a class="btn btn-primary" href="center_assessments.php">فتح صفحة تقييم المراكز</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="page-banner-panel h-100">
|
||||
<div class="mini-stat-label">الدرجة القصوى الحالية</div>
|
||||
<div class="mini-stat-value"><?= e(rtrim(rtrim(number_format((float) ($assessment['max_score'] ?? 0), 2, '.', ''), '0'), '.')) ?></div>
|
||||
<div class="mini-stat-copy mb-3">يتم تحديثها تلقائياً وفق مجموع درجات البنود النشطة فقط.</div>
|
||||
<div><?= assessment_active_badge((int) ($assessment['is_active'] ?? 0)) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4"><div class="app-card stat-tile"><div class="mini-stat-label">إجمالي البنود</div><div class="mini-stat-value"><?= e((string) $criteriaMetrics['total']) ?></div><div class="mini-stat-copy">كل البنود المحفوظة لهذا القالب.</div></div></div>
|
||||
<div class="col-md-4"><div class="app-card stat-tile"><div class="mini-stat-label">البنود النشطة</div><div class="mini-stat-value"><?= e((string) $criteriaMetrics['active']) ?></div><div class="mini-stat-copy">هي التي يعتمد عليها التقييم الفعلي.</div></div></div>
|
||||
<div class="col-md-4"><div class="app-card stat-tile"><div class="mini-stat-label">مجموع الدرجات النشطة</div><div class="mini-stat-value"><?= e(rtrim(rtrim(number_format((float) $criteriaMetrics['active_max_score'], 2, '.', ''), '0'), '.')) ?></div><div class="mini-stat-copy">يعيد ضبط الدرجة القصوى للقالب تلقائياً.</div></div></div>
|
||||
</div>
|
||||
|
||||
<div class="app-card">
|
||||
<div class="section-head mb-3">
|
||||
<div>
|
||||
<div class="section-title">إدارة البنود</div>
|
||||
<div class="section-copy">يمكنك تعديل البنود الحالية أو إضافة بنود جديدة. الصفوف الفارغة لن تُحفظ.</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" id="addCriteriaRow">إضافة صف جديد</button>
|
||||
</div>
|
||||
|
||||
<?php if (isset($errors['form'])): ?>
|
||||
<div class="alert alert-danger"><?= e($errors['form']) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php foreach ($errors as $errorKey => $message): ?>
|
||||
<?php if (str_starts_with($errorKey, 'criteria_')): ?>
|
||||
<div class="alert alert-warning py-2 mb-2"><?= e($message) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<form method="post">
|
||||
<div id="criteriaRows" class="d-grid gap-3">
|
||||
<?php foreach ($criteria as $index => $criterion): ?>
|
||||
<div class="border rounded-4 p-3 bg-white">
|
||||
<input type="hidden" name="criteria[<?= e((string) $index) ?>][id]" value="<?= e((string) ($criterion['id'] ?? 0)) ?>">
|
||||
<div class="row g-3 align-items-start">
|
||||
<div class="col-lg-4">
|
||||
<label class="form-label">اسم البند</label>
|
||||
<input class="form-control" name="criteria[<?= e((string) $index) ?>][title]" value="<?= e((string) ($criterion['title'] ?? '')) ?>" placeholder="مثال: الالتزام بالخطة التشغيلية">
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<label class="form-label">الدرجة</label>
|
||||
<input class="form-control" type="number" min="0.01" max="1000" step="0.01" name="criteria[<?= e((string) $index) ?>][max_score]" value="<?= e((string) ($criterion['max_score'] ?? '')) ?>" placeholder="10">
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<label class="form-label">الحالة</label>
|
||||
<select class="form-select" name="criteria[<?= e((string) $index) ?>][is_active]">
|
||||
<option value="1" <?= ((string) ($criterion['is_active'] ?? '1')) === '1' ? 'selected' : '' ?>>مفعّل</option>
|
||||
<option value="0" <?= ((string) ($criterion['is_active'] ?? '1')) === '0' ? 'selected' : '' ?>>مؤرشف</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<label class="form-label">ملاحظات</label>
|
||||
<input class="form-control" name="criteria[<?= e((string) $index) ?>][notes]" value="<?= e((string) ($criterion['notes'] ?? '')) ?>" placeholder="شرح مختصر لما يجب مراجعته في هذا البند">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 mt-4">
|
||||
<button class="btn btn-primary" type="submit">حفظ البنود</button>
|
||||
<a class="btn btn-outline-secondary" href="global_center_assessments.php?edit=<?= e((string) $assessmentId) ?>#assessmentFormCard">تحرير القالب</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<template id="criteriaRowTemplate">
|
||||
<div class="border rounded-4 p-3 bg-white">
|
||||
<input type="hidden" data-name="id" value="0">
|
||||
<div class="row g-3 align-items-start">
|
||||
<div class="col-lg-4">
|
||||
<label class="form-label">اسم البند</label>
|
||||
<input class="form-control" data-name="title" placeholder="مثال: الجاهزية الإدارية">
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<label class="form-label">الدرجة</label>
|
||||
<input class="form-control" type="number" min="0.01" max="1000" step="0.01" data-name="max_score" placeholder="10">
|
||||
</div>
|
||||
<div class="col-lg-2">
|
||||
<label class="form-label">الحالة</label>
|
||||
<select class="form-select" data-name="is_active">
|
||||
<option value="1" selected>مفعّل</option>
|
||||
<option value="0">مؤرشف</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<label class="form-label">ملاحظات</label>
|
||||
<input class="form-control" data-name="notes" placeholder="شرح مختصر لما يجب فحصه">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const rowsContainer = document.getElementById('criteriaRows');
|
||||
const addButton = document.getElementById('addCriteriaRow');
|
||||
const template = document.getElementById('criteriaRowTemplate');
|
||||
if (!rowsContainer || !addButton || !template) return;
|
||||
|
||||
let rowIndex = rowsContainer.children.length;
|
||||
addButton.addEventListener('click', function () {
|
||||
const fragment = template.content.cloneNode(true);
|
||||
fragment.querySelectorAll('[data-name]').forEach(function (field) {
|
||||
field.setAttribute('name', 'criteria[' + rowIndex + '][' + field.getAttribute('data-name') + ']');
|
||||
});
|
||||
rowsContainer.appendChild(fragment);
|
||||
rowIndex += 1;
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?php render_page_end(); ?>
|
||||
432
global_center_assessments.php
Normal file
432
global_center_assessments.php
Normal file
@ -0,0 +1,432 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
if (!is_super_admin()) {
|
||||
http_response_code(403);
|
||||
render_page_start('صلاحيات غير كافية', 'admin', 'هذه الصفحة مخصصة للمشرف العام لإدارة قوالب تقييم المراكز.');
|
||||
?>
|
||||
<section class="py-5 text-center">
|
||||
<div class="container-xxl">
|
||||
<h1 class="mb-3">عذراً</h1>
|
||||
<p>هذه الصفحة مخصصة للمشرف العام فقط.</p>
|
||||
<a href="admin.php" class="btn btn-primary mt-3">العودة إلى لوحة الإدارة</a>
|
||||
</div>
|
||||
</section>
|
||||
<?php
|
||||
render_page_end();
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? 'create';
|
||||
$assessmentId = filter_input(INPUT_POST, 'assessment_id', FILTER_VALIDATE_INT) ?: 0;
|
||||
|
||||
if ($action === 'delete') {
|
||||
if ($assessmentId > 0 && delete_global_center_assessment_type($assessmentId)) {
|
||||
set_flash('success', 'تم حذف قالب التقييم بنجاح.');
|
||||
} else {
|
||||
set_flash('error', 'حدث خطأ أثناء محاولة حذف قالب التقييم.');
|
||||
}
|
||||
header('Location: global_center_assessments.php');
|
||||
exit;
|
||||
} else {
|
||||
[$values, $errors] = validate_assessment_input($_POST);
|
||||
|
||||
if ($errors === []) {
|
||||
try {
|
||||
if ($action === 'edit' && $assessmentId > 0) {
|
||||
update_global_center_assessment_type($assessmentId, $values);
|
||||
set_flash('success', 'تم تحديث قالب تقييم المراكز بنجاح.');
|
||||
} else {
|
||||
$assessmentId = create_global_center_assessment_type($values);
|
||||
set_flash('success', 'تم إنشاء قالب تقييم جديد للمراكز. يمكنك الآن إضافة البنود الخاصة به.');
|
||||
}
|
||||
header('Location: global_center_assessments.php' . ($assessmentId > 0 && $action !== 'edit' ? '?highlight=' . $assessmentId : ''));
|
||||
exit;
|
||||
} catch (Throwable $exception) {
|
||||
set_flash('error', 'تعذر حفظ قالب التقييم حالياً. يرجى المحاولة مرة أخرى.');
|
||||
header('Location: global_center_assessments.php');
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
$errorMsg = implode(' ', $errors);
|
||||
set_flash('error', $errorMsg);
|
||||
header('Location: global_center_assessments.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$filters = [
|
||||
'search' => clean_text($_GET['search'] ?? '', 255),
|
||||
'category' => clean_text($_GET['category'] ?? '', 80),
|
||||
];
|
||||
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT) ?: 1;
|
||||
$limit = 20;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$assessments = list_global_center_assessments($filters, $limit, $offset);
|
||||
$totalAssessments = count_global_center_assessments($filters);
|
||||
$metrics = global_center_assessment_metrics();
|
||||
$highlightAssessmentId = filter_input(INPUT_GET, 'highlight', FILTER_VALIDATE_INT) ?: 0;
|
||||
$activeWeight = round((float) $metrics['active_weight'], 2);
|
||||
$weightGap = round(100 - $activeWeight, 2);
|
||||
|
||||
$flash = consume_flash();
|
||||
render_page_start(
|
||||
'قوالب تقييم المراكز',
|
||||
'admin',
|
||||
'إدارة القوالب العامة لتقييم المراكز وبنودها حتى يتمكن المشرف العام أو المقيمون المكلّفون من استخدامها لاحقاً عند تقييم كل مركز.'
|
||||
);
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="py-4 py-lg-5">
|
||||
<div class="container-xxl">
|
||||
<div class="admin-layout row g-4 align-items-start">
|
||||
<div class="col-lg-3 layout-sidebar-column">
|
||||
<?php require __DIR__ . '/includes/sidebar.php'; ?>
|
||||
</div>
|
||||
<div class="col-lg-9 layout-content-column">
|
||||
|
||||
<div class="page-banner mb-4">
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-8">
|
||||
<span class="eyebrow mb-3">مكتبة التقييم الإشرافي</span>
|
||||
<h1 class="page-title mb-3">قوالب تقييم المراكز</h1>
|
||||
<p class="page-copy mb-3">من هنا ينشئ المشرف العام التقييمات العامة للمراكز مرة واحدة، ثم تُستخدم لاحقاً من قبل المشرف العام أو أي مقيّم مكلّف لتقييم المراكز واحداً تلو الآخر.</p>
|
||||
<div class="hero-meta">
|
||||
<span>قوالب نشطة <?= e((string) $metrics['active']) ?></span>
|
||||
<span>بنفس المنهجية السابقة للبنود والرصد</span>
|
||||
<span>إجمالي الوزن النشط <?= e(rtrim(rtrim(number_format($activeWeight, 2, '.', ''), '0'), '.')) ?>٪</span>
|
||||
</div>
|
||||
<div class="cta-stack mt-4">
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createModal">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
|
||||
</svg>
|
||||
إضافة قالب جديد
|
||||
</button>
|
||||
<a class="btn btn-outline-secondary" href="center_assessments.php">فتح تقييم المراكز الحالي</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="page-banner-panel h-100">
|
||||
<div class="mini-stat-label">فجوة الأوزان الحالية</div>
|
||||
<div class="mini-stat-value"><?= e(rtrim(rtrim(number_format($weightGap, 2, '.', ''), '0'), '.')) ?>٪</div>
|
||||
<div class="mini-stat-copy mb-3">كلما اقتربت القوالب النشطة من 100٪ صار توزيع التقييم أوضح للمقيمين عند تقييم المراكز ميدانياً.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6 col-xl-3"><div class="app-card stat-tile"><div class="mini-stat-label">إجمالي القوالب</div><div class="mini-stat-value"><?= e((string) $metrics['total']) ?></div><div class="mini-stat-copy">كل قوالب تقييم المراكز المعرفة مركزياً.</div></div></div>
|
||||
<div class="col-md-6 col-xl-3"><div class="app-card stat-tile"><div class="mini-stat-label">النشطة</div><div class="mini-stat-value"><?= e((string) $metrics['active']) ?></div><div class="mini-stat-copy">المتاحة للمقيمين عند بدء التقييم.</div></div></div>
|
||||
<div class="col-md-6 col-xl-3"><div class="app-card stat-tile"><div class="mini-stat-label">متوسط الدرجة القصوى</div><div class="mini-stat-value"><?= e(rtrim(rtrim(number_format((float) $metrics['average_max_score'], 2, '.', ''), '0'), '.')) ?></div><div class="mini-stat-copy">يتحدث تلقائياً حسب البنود النشطة.</div></div></div>
|
||||
<div class="col-md-6 col-xl-3"><div class="app-card stat-tile"><div class="mini-stat-label">أنواع المقاييس</div><div class="mini-stat-value"><?= e((string) ($metrics['percentage'] + $metrics['points'] + $metrics['rubric'])) ?></div><div class="mini-stat-copy">نسبة: <?= e((string) $metrics['percentage']) ?> • نقاط: <?= e((string) $metrics['points']) ?> • Rubric: <?= e((string) $metrics['rubric']) ?></div></div></div>
|
||||
</div>
|
||||
|
||||
<div class="app-card mb-4">
|
||||
<div class="section-head mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-0 flex-wrap gap-3">
|
||||
<div>
|
||||
<div class="section-title">إدارة قوالب التقييم</div>
|
||||
<div class="section-copy">كل قالب هنا يمثل نموذج تقييم إشرافي يمكن لاحقاً استخدامه عند تقييم أي مركز.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php render_search_bar($filters['search'], 'ابحث باسم القالب أو الفئة أو الملاحظات...', 'global_center_assessments.php', $_GET); ?>
|
||||
|
||||
<form method="get" class="row g-3 align-items-end mb-4">
|
||||
<input type="hidden" name="search" value="<?= e($filters['search']) ?>">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label" for="categoryFilter">الفئة</label>
|
||||
<select class="form-select" id="categoryFilter" name="category">
|
||||
<option value="">كل الفئات</option>
|
||||
<?php foreach (assessment_category_options() as $categoryOption): ?>
|
||||
<option value="<?= e($categoryOption) ?>" <?= $filters['category'] === $categoryOption ? 'selected' : '' ?>><?= e($categoryOption) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 d-grid">
|
||||
<button class="btn btn-outline-secondary" type="submit">تطبيق الفلترة</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table app-table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>القالب</th>
|
||||
<th>الفئة</th>
|
||||
<th>المقياس</th>
|
||||
<th>الوزن</th>
|
||||
<th>البنود</th>
|
||||
<th>الحالة</th>
|
||||
<th>الإجراءات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($assessments === []): ?>
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-4 text-muted">لا توجد قوالب تقييم مسجلة أو لم يتم العثور على نتائج.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($assessments as $assessment): ?>
|
||||
<?php $isHighlighted = (int) ($assessment['id'] ?? 0) === $highlightAssessmentId; ?>
|
||||
<tr<?= $isHighlighted ? ' class="table-warning"' : '' ?>>
|
||||
<td>
|
||||
<div class="fw-semibold"><?= e((string) ($assessment['title'] ?? '')) ?></div>
|
||||
<div class="text-muted small" style="max-width: 250px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="<?= e((string) ($assessment['notes'] ?? '')) ?>"><?= e((string) ($assessment['notes'] ?? 'بدون ملاحظات إضافية')) ?></div>
|
||||
</td>
|
||||
<td><?= e((string) ($assessment['category'] ?? '')) ?></td>
|
||||
<td><?= assessment_scale_type_badge((string) ($assessment['scale_type'] ?? '')) ?></td>
|
||||
<td>
|
||||
<div class="fw-semibold"><?= e(rtrim(rtrim(number_format((float) ($assessment['weight_percentage'] ?? 0), 2, '.', ''), '0'), '.')) ?>٪</div>
|
||||
<div class="text-muted small">الدرجة: <?= e(rtrim(rtrim(number_format((float) ($assessment['max_score'] ?? 0), 2, '.', ''), '0'), '.')) ?></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="fw-semibold"><?= e((string) ((int) ($assessment['criteria_count'] ?? 0))) ?></div>
|
||||
<div class="text-muted small">مجموع النشط: <?= e(rtrim(rtrim(number_format((float) ($assessment['criteria_total_max_score'] ?? 0), 2, '.', ''), '0'), '.')) ?></div>
|
||||
</td>
|
||||
<td><?= assessment_active_badge((int) ($assessment['is_active'] ?? 0)) ?></td>
|
||||
<td>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-secondary" title="تعديل"
|
||||
data-bs-toggle="modal" data-bs-target="#editModal"
|
||||
data-id="<?= e((string) ($assessment['id'] ?? 0)) ?>"
|
||||
data-title="<?= e((string) ($assessment['title'] ?? '')) ?>"
|
||||
data-category="<?= e((string) ($assessment['category'] ?? '')) ?>"
|
||||
data-scale_type="<?= e((string) ($assessment['scale_type'] ?? '')) ?>"
|
||||
data-max_score="<?= e(rtrim(rtrim(number_format((float) ($assessment['max_score'] ?? 0), 2, '.', ''), '0'), '.')) ?>"
|
||||
data-weight_percentage="<?= e(rtrim(rtrim(number_format((float) ($assessment['weight_percentage'] ?? 0), 2, '.', ''), '0'), '.')) ?>"
|
||||
data-is_active="<?= (int) ($assessment['is_active'] ?? 0) === 1 ? '1' : '0' ?>"
|
||||
data-notes="<?= e((string) ($assessment['notes'] ?? '')) ?>"
|
||||
onclick="fillEditModal(this)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" title="حذف"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
||||
data-id="<?= e((string) ($assessment['id'] ?? 0)) ?>"
|
||||
data-title="<?= e((string) ($assessment['title'] ?? '')) ?>"
|
||||
onclick="fillDeleteModal(this)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
||||
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<a class="btn btn-primary btn-sm" href="global_center_assessment_criteria.php?assessment_id=<?= e((string) ($assessment['id'] ?? 0)) ?>" title="البنود">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M14.5 3a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-13a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h13zm-13-1A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2h-13z"/>
|
||||
<path d="M3 5.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zM3 8.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zM3 11.5a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="btn btn-success btn-sm" href="execute_global_assessment.php?id=<?= e((string) ($assessment['id'] ?? 0)) ?>" title="تطبيق التقييم">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
|
||||
</svg> تطبيق
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if ($assessments !== []): ?>
|
||||
<div class="mt-4">
|
||||
<?php render_pagination($totalAssessments, $limit, $page, $_GET); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Create Modal -->
|
||||
<div class="modal fade" id="createModal" tabindex="-1" aria-labelledby="createModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<form method="POST" action="global_center_assessments.php">
|
||||
<input type="hidden" name="action" value="create">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="createModalLabel">إضافة قالب تقييم جديد</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="create_title">اسم التقييم</label>
|
||||
<input class="form-control" id="create_title" name="title" placeholder="مثال: الزيارة الإشرافية العامة" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="create_category">الفئة</label>
|
||||
<select class="form-select" id="create_category" name="category">
|
||||
<?php foreach (assessment_category_options() as $categoryOption): ?>
|
||||
<option value="<?= e($categoryOption) ?>"><?= e($categoryOption) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="create_scale_type">مقياس التقييم</label>
|
||||
<select class="form-select" id="create_scale_type" name="scale_type">
|
||||
<?php foreach (assessment_scale_type_map() as $scaleKey => $scaleMeta): ?>
|
||||
<option value="<?= e($scaleKey) ?>"><?= e((string) $scaleMeta['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="create_max_score">الدرجة القصوى</label>
|
||||
<input class="form-control" id="create_max_score" type="number" min="0.01" max="1000" step="0.01" name="max_score" value="100">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="create_weight_percentage">الوزن النسبي ٪</label>
|
||||
<input class="form-control" id="create_weight_percentage" type="number" min="0" max="100" step="0.01" name="weight_percentage" value="100">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="create_is_active">الحالة</label>
|
||||
<select class="form-select" id="create_is_active" name="is_active">
|
||||
<option value="1" selected>مفعّل</option>
|
||||
<option value="0">مؤرشف</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="create_notes">ملاحظات داخلية</label>
|
||||
<textarea class="form-control" id="create_notes" name="notes" rows="3" placeholder="مثلاً: يستخدم في الزيارات الإشرافية الفصلية..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" class="btn btn-primary">إنشاء القالب</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Modal -->
|
||||
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<form method="POST" action="global_center_assessments.php">
|
||||
<input type="hidden" name="action" value="edit">
|
||||
<input type="hidden" name="assessment_id" id="edit_assessment_id">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editModalLabel">تعديل قالب التقييم</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="edit_title">اسم التقييم</label>
|
||||
<input class="form-control" id="edit_title" name="title" required>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="edit_category">الفئة</label>
|
||||
<select class="form-select" id="edit_category" name="category">
|
||||
<?php foreach (assessment_category_options() as $categoryOption): ?>
|
||||
<option value="<?= e($categoryOption) ?>"><?= e($categoryOption) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="edit_scale_type">مقياس التقييم</label>
|
||||
<select class="form-select" id="edit_scale_type" name="scale_type">
|
||||
<?php foreach (assessment_scale_type_map() as $scaleKey => $scaleMeta): ?>
|
||||
<option value="<?= e($scaleKey) ?>"><?= e((string) $scaleMeta['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="edit_max_score">الدرجة القصوى</label>
|
||||
<input class="form-control" id="edit_max_score" type="number" min="0.01" max="1000" step="0.01" name="max_score">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="edit_weight_percentage">الوزن النسبي ٪</label>
|
||||
<input class="form-control" id="edit_weight_percentage" type="number" min="0" max="100" step="0.01" name="weight_percentage">
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="edit_is_active">الحالة</label>
|
||||
<select class="form-select" id="edit_is_active" name="is_active">
|
||||
<option value="1">مفعّل</option>
|
||||
<option value="0">مؤرشف</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="edit_notes">ملاحظات داخلية</label>
|
||||
<textarea class="form-control" id="edit_notes" name="notes" rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" class="btn btn-primary">حفظ التعديلات</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Modal -->
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="POST" action="global_center_assessments.php">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="assessment_id" id="delete_assessment_id">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteModalLabel">تأكيد حذف القالب</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>هل أنت متأكد أنك تريد حذف القالب <strong id="delete_assessment_title"></strong>؟</p>
|
||||
<p class="text-danger small">ملاحظة: سيتم حذف جميع البنود المرتبطة بهذا القالب أيضاً. هذا الإجراء لا يمكن التراجع عنه.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" class="btn btn-danger">تأكيد الحذف</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function fillEditModal(btn) {
|
||||
document.getElementById('edit_assessment_id').value = btn.getAttribute('data-id');
|
||||
document.getElementById('edit_title').value = btn.getAttribute('data-title');
|
||||
document.getElementById('edit_category').value = btn.getAttribute('data-category');
|
||||
document.getElementById('edit_scale_type').value = btn.getAttribute('data-scale_type');
|
||||
document.getElementById('edit_max_score').value = btn.getAttribute('data-max_score');
|
||||
document.getElementById('edit_weight_percentage').value = btn.getAttribute('data-weight_percentage');
|
||||
document.getElementById('edit_is_active').value = btn.getAttribute('data-is_active');
|
||||
document.getElementById('edit_notes').value = btn.getAttribute('data-notes');
|
||||
}
|
||||
|
||||
function fillDeleteModal(btn) {
|
||||
document.getElementById('delete_assessment_id').value = btn.getAttribute('data-id');
|
||||
document.getElementById('delete_assessment_title').textContent = btn.getAttribute('data-title');
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php render_page_end(); ?>
|
||||
314
global_cycles.php
Normal file
314
global_cycles.php
Normal file
@ -0,0 +1,314 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
if (!is_super_admin()) {
|
||||
http_response_code(403);
|
||||
render_page_start('صلاحيات غير كافية', '');
|
||||
?>
|
||||
<section class="py-5 text-center">
|
||||
<div class="container-xxl">
|
||||
<h1 class="mb-3">عذراً</h1>
|
||||
<p>هذه الصفحة مخصصة للمشرف العام فقط.</p>
|
||||
<a href="index.php" class="btn btn-primary mt-3">العودة للرئيسية</a>
|
||||
</div>
|
||||
</section>
|
||||
<?php
|
||||
render_page_end();
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
$flash = consume_flash();
|
||||
$errors = [];
|
||||
|
||||
// Handle form submission
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
if ($action === 'add') {
|
||||
$cycleName = clean_text($_POST['cycle_name'] ?? '');
|
||||
$startDate = $_POST['start_date'] ?? '';
|
||||
$endDate = $_POST['end_date'] ?? '';
|
||||
|
||||
if ($cycleName === '') $errors['cycle_name'] = 'اسم الدورة مطلوب';
|
||||
if ($startDate === '') $errors['start_date'] = 'تاريخ البداية مطلوب';
|
||||
if ($endDate === '') $errors['end_date'] = 'تاريخ النهاية مطلوب';
|
||||
|
||||
if (empty($errors)) {
|
||||
try {
|
||||
$stmt = db()->prepare('INSERT INTO global_cycles (cycle_name, start_date, end_date) VALUES (?, ?, ?)');
|
||||
$stmt->execute([$cycleName, $startDate, $endDate]);
|
||||
set_flash('success', 'تم إضافة الدورة بنجاح.');
|
||||
header('Location: global_cycles.php');
|
||||
exit;
|
||||
} catch (Throwable $e) {
|
||||
$errors['form'] = 'حدث خطأ أثناء حفظ الدورة.';
|
||||
}
|
||||
}
|
||||
} elseif ($action === 'toggle') {
|
||||
$cycleId = filter_input(INPUT_POST, 'cycle_id', FILTER_VALIDATE_INT);
|
||||
if ($cycleId) {
|
||||
try {
|
||||
$stmt = db()->prepare('UPDATE global_cycles SET is_active = NOT is_active WHERE id = ?');
|
||||
$stmt->execute([$cycleId]);
|
||||
set_flash('success', 'تم تغيير حالة الدورة.');
|
||||
} catch (Throwable $e) {
|
||||
set_flash('error', 'حدث خطأ.');
|
||||
}
|
||||
}
|
||||
header('Location: global_cycles.php');
|
||||
exit;
|
||||
} elseif ($action === 'delete') {
|
||||
$cycleId = filter_input(INPUT_POST, 'cycle_id', FILTER_VALIDATE_INT);
|
||||
if ($cycleId) {
|
||||
try {
|
||||
$stmt = db()->prepare('DELETE FROM global_cycles WHERE id = ?');
|
||||
$stmt->execute([$cycleId]);
|
||||
set_flash('success', 'تم حذف الدورة بنجاح.');
|
||||
} catch (Throwable $e) {
|
||||
set_flash('error', 'لا يمكن حذف الدورة، ربما تكون مرتبطة بطلبات أخرى.');
|
||||
}
|
||||
}
|
||||
header('Location: global_cycles.php');
|
||||
exit;
|
||||
} elseif ($action === 'edit') {
|
||||
$cycleId = filter_input(INPUT_POST, 'cycle_id', FILTER_VALIDATE_INT);
|
||||
$cycleName = clean_text($_POST['cycle_name'] ?? '');
|
||||
$startDate = $_POST['start_date'] ?? '';
|
||||
$endDate = $_POST['end_date'] ?? '';
|
||||
|
||||
if ($cycleId && $cycleName && $startDate && $endDate) {
|
||||
try {
|
||||
$stmt = db()->prepare('UPDATE global_cycles SET cycle_name = ?, start_date = ?, end_date = ? WHERE id = ?');
|
||||
$stmt->execute([$cycleName, $startDate, $endDate, $cycleId]);
|
||||
set_flash('success', 'تم تعديل الدورة بنجاح.');
|
||||
} catch (Throwable $e) {
|
||||
set_flash('error', 'حدث خطأ أثناء التعديل.');
|
||||
}
|
||||
}
|
||||
header('Location: global_cycles.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch cycles
|
||||
$search = clean_text($_GET['search'] ?? '', 255);
|
||||
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT) ?: 1;
|
||||
$limit = 10;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$cycles = [];
|
||||
$totalItems = 0;
|
||||
|
||||
try {
|
||||
$query = 'SELECT * FROM global_cycles';
|
||||
$countQuery = 'SELECT COUNT(*) FROM global_cycles';
|
||||
$params = [];
|
||||
|
||||
if ($search !== '') {
|
||||
$where = ' WHERE cycle_name LIKE ?';
|
||||
$query .= $where;
|
||||
$countQuery .= $where;
|
||||
$params[] = "%$search%";
|
||||
}
|
||||
|
||||
$stmtCount = db()->prepare($countQuery);
|
||||
$stmtCount->execute($params);
|
||||
$totalItems = (int)$stmtCount->fetchColumn();
|
||||
|
||||
$query .= ' ORDER BY start_date DESC LIMIT ' . $limit . ' OFFSET ' . $offset;
|
||||
$stmt = db()->prepare($query);
|
||||
$stmt->execute($params);
|
||||
$cycles = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (Throwable $e) {}
|
||||
|
||||
render_page_start('إدارة الدورات الموسمية', 'cycles', 'إضافة وإدارة الدورات التي تتقدم لها المراكز.');
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="py-4 py-lg-5">
|
||||
<div class="container-xxl">
|
||||
<div class="admin-layout row g-4 align-items-start">
|
||||
<div class="col-lg-3 layout-sidebar-column">
|
||||
<?php require __DIR__ . '/includes/sidebar.php'; ?>
|
||||
</div>
|
||||
<div class="col-lg-9 layout-content-column">
|
||||
<div class="page-banner mb-4">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-8">
|
||||
<h1 class="page-title mb-2">الدورات الموسمية</h1>
|
||||
<p class="page-copy mb-0">يمكنك هنا تعريف الدورات الجديدة ليتاح للمراكز التقديم عليها.</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-md-end mt-3 mt-md-0">
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addCycleModal">
|
||||
إضافة دورة جديدة
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($errors['form'])): ?>
|
||||
<div class="alert alert-danger mb-4"><?= e($errors['form']) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php render_search_bar($search, "ابحث باسم الدورة...", "global_cycles.php", $_GET); ?>
|
||||
<div class="app-card table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>اسم الدورة</th>
|
||||
<th>الفترة الزمنية</th>
|
||||
<th>الحالة</th>
|
||||
<th class="text-end">الإجراءات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (empty($cycles)): ?>
|
||||
<tr>
|
||||
<td colspan="4" class="text-center py-4 text-muted">لا توجد دورات مسجلة حالياً.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($cycles as $cycle): ?>
|
||||
<tr>
|
||||
<td><strong><?= e((string)$cycle['cycle_name']) ?></strong></td>
|
||||
<td>
|
||||
<small class="text-muted d-block">من: <?= e((string)$cycle['start_date']) ?></small>
|
||||
<small class="text-muted d-block">إلى: <?= e((string)$cycle['end_date']) ?></small>
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($cycle['is_active']): ?>
|
||||
<span class="badge bg-success">متاحة للتقديم</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-secondary">مغلقة</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="d-flex justify-content-end gap-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#editCycleModal"
|
||||
data-id="<?= e((string)$cycle['id']) ?>"
|
||||
data-name="<?= e((string)$cycle['cycle_name']) ?>"
|
||||
data-start="<?= e((string)$cycle['start_date']) ?>"
|
||||
data-end="<?= e((string)$cycle['end_date']) ?>">
|
||||
تعديل
|
||||
</button>
|
||||
<form method="post" class="d-inline-block m-0" onsubmit="return confirm('هل أنت متأكد من حذف هذه الدورة؟');">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="cycle_id" value="<?= e((string)$cycle['id']) ?>">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||
حذف
|
||||
</button>
|
||||
</form>
|
||||
<form method="post" class="d-inline-block m-0">
|
||||
<input type="hidden" name="action" value="toggle">
|
||||
<input type="hidden" name="cycle_id" value="<?= e((string)$cycle['id']) ?>">
|
||||
<button type="submit" class="btn btn-sm <?= $cycle['is_active'] ? 'btn-outline-secondary' : 'btn-outline-success' ?>">
|
||||
<?= $cycle['is_active'] ? 'إغلاق' : 'تفعيل' ?>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php render_pagination($totalItems, $limit, $page, $_GET); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Add Cycle Modal -->
|
||||
<div class="modal fade" id="addCycleModal" tabindex="-1" aria-labelledby="addCycleModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="add">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="addCycleModalLabel">إضافة دورة جديدة</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="إغلاق"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="cycle_name">اسم الدورة</label>
|
||||
<input type="text" class="form-control" id="cycle_name" name="cycle_name" required placeholder="مثال: الدورة الصيفية لعام 2026">
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="start_date">تاريخ البداية</label>
|
||||
<input type="date" class="form-control" id="start_date" name="start_date" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="end_date">تاريخ النهاية</label>
|
||||
<input type="date" class="form-control" id="end_date" name="end_date" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" class="btn btn-primary">حفظ الدورة</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Cycle Modal -->
|
||||
<div class="modal fade" id="editCycleModal" tabindex="-1" aria-labelledby="editCycleModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="post">
|
||||
<input type="hidden" name="action" value="edit">
|
||||
<input type="hidden" name="cycle_id" id="edit_cycle_id">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editCycleModalLabel">تعديل الدورة</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="إغلاق"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="edit_cycle_name">اسم الدورة</label>
|
||||
<input type="text" class="form-control" id="edit_cycle_name" name="cycle_name" required>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="edit_start_date">تاريخ البداية</label>
|
||||
<input type="date" class="form-control" id="edit_start_date" name="start_date" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="edit_end_date">تاريخ النهاية</label>
|
||||
<input type="date" class="form-control" id="edit_end_date" name="end_date" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" class="btn btn-primary">حفظ التعديلات</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var editModal = document.getElementById('editCycleModal');
|
||||
if(editModal) {
|
||||
editModal.addEventListener('show.bs.modal', function (event) {
|
||||
var button = event.relatedTarget;
|
||||
var id = button.getAttribute('data-id');
|
||||
var name = button.getAttribute('data-name');
|
||||
var start = button.getAttribute('data-start');
|
||||
var end = button.getAttribute('data-end');
|
||||
|
||||
editModal.querySelector('#edit_cycle_id').value = id;
|
||||
editModal.querySelector('#edit_cycle_name').value = name;
|
||||
editModal.querySelector('#edit_start_date').value = start;
|
||||
editModal.querySelector('#edit_end_date').value = end;
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<?php render_page_end(); ?>
|
||||
22
healthz.php
Normal file
22
healthz.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
try {
|
||||
$metrics = dashboard_metrics();
|
||||
echo json_encode([
|
||||
'status' => 'ok',
|
||||
'app' => project_name(),
|
||||
'applications' => $metrics['all'],
|
||||
'timestamp' => gmdate('c'),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
} catch (Throwable $exception) {
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'status' => 'error',
|
||||
'message' => 'Database unavailable',
|
||||
'timestamp' => gmdate('c'),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
1622
includes/app.php
Normal file
1622
includes/app.php
Normal file
File diff suppressed because it is too large
Load Diff
79
includes/center_sidebar.php
Normal file
79
includes/center_sidebar.php
Normal file
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
$script = basename($_SERVER['SCRIPT_NAME'] ?? '');
|
||||
|
||||
$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' || $script === 'assessment_criteria.php') $activePage = 'assessments';
|
||||
if ($script === 'attendance.php') $activePage = 'attendance';
|
||||
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.
|
||||
if (!isset($application) || !$application) {
|
||||
// Fallback if somehow included without context
|
||||
$application = ['id' => 0, 'center_name' => 'المركز غير متوفر'];
|
||||
}
|
||||
|
||||
// Same for selected cycle
|
||||
$selectedCycleIdStr = isset($selectedCycleId) && $selectedCycleId > 0 ? '&cycle=' . $selectedCycleId : '';
|
||||
$baseQuery = '?id=' . e((string) $application['id']) . $selectedCycleIdStr;
|
||||
|
||||
?>
|
||||
<aside class="admin-sidebar sticky-top" style="top: 2rem; z-index: 10;">
|
||||
<div class="sidebar-nav">
|
||||
<div class="sidebar-label text-truncate" title="<?= e((string) $application['center_name']) ?>">
|
||||
<?= e((string) $application['center_name']) ?>
|
||||
</div>
|
||||
|
||||
<a href="approved_school.php<?= $baseQuery ?>" class="sidebar-link <?= $activePage === 'dashboard' ? 'active' : '' ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path d="M8 4a.5.5 0 0 1 .5.5V6a.5.5 0 0 1-1 0V4.5A.5.5 0 0 1 8 4zM3.732 5.732a.5.5 0 0 1 .707 0l.915.914a.5.5 0 1 1-.708.708l-.914-.915a.5.5 0 0 1 0-.707zM2 10a.5.5 0 0 1 .5-.5h1.586a.5.5 0 0 1 0 1H2.5A.5.5 0 0 1 2 10zm9.5 0a.5.5 0 0 1 .5-.5h1.5a.5.5 0 0 1 0 1H12a.5.5 0 0 1-.5-.5zm.754-4.246a.389.389 0 0 0-.527-.02L7.547 9.31a.91.91 0 1 0 1.302 1.258l3.434-4.297a.389.389 0 0 0-.029-.518z"/><path fill-rule="evenodd" d="M0 10a8 8 0 1 1 15.547 2.661c-.442 1.253-1.845 1.602-2.932 1.25C11.309 13.488 9.475 13 8 13c-1.474 0-3.31.488-4.615.911-1.087.352-2.49.003-2.932-1.25A7.988 7.988 0 0 1 0 10zm8-7a7 7 0 0 0-6.603 9.329c.203.575.923.876 1.68.63C4.397 12.533 6.358 12 8 12s3.604.532 4.923.96c.757.245 1.477-.056 1.68-.631A7 7 0 0 0 8 3z"/></svg>
|
||||
لوحة القيادة
|
||||
</a>
|
||||
|
||||
<div class="sidebar-label mt-3">الإدارة الأكاديمية</div>
|
||||
|
||||
<a href="students.php<?= $baseQuery ?>" class="sidebar-link <?= $activePage === 'students' ? 'active' : '' ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path d="M7 14s-1 0-1-1 1-4 5-4 5 3 5 4-1 1-1 1H7zm4-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/><path fill-rule="evenodd" d="M5.216 14A2.238 2.238 0 0 1 5 13c0-1.355.68-2.75 1.936-3.72A6.325 6.325 0 0 0 5 9c-4 0-5 3-5 4s1 1 1 1h4.216z"/><path d="M4.5 8a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/></svg>
|
||||
الطلاب
|
||||
</a>
|
||||
|
||||
<a href="teachers.php<?= $baseQuery ?>" class="sidebar-link <?= $activePage === 'teachers' ? 'active' : '' ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path d="M15 14s1 0 1-1-1-4-5-4-5 3-5 4 1 1 1 1h8zm-7.978-1A.261.261 0 0 1 7 12.996c.001-.264.167-1.03.76-1.72C8.312 10.629 9.282 10 11 10c1.717 0 2.687.63 3.24 1.276.593.69.758 1.457.76 1.72l-.008.002a.274.274 0 0 1-.014.002H7.022zM11 7a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm3-2a3 3 0 1 1-6 0 3 3 0 0 1 6 0zM6.936 9.28a5.88 5.88 0 0 0-1.23-.247A7.35 7.35 0 0 0 5 9c-4 0-5 3-5 4 0 .667.333 1 1 1h4.216A2.238 2.238 0 0 1 5 13c0-1.01.377-2.042 1.09-2.904.243-.294.526-.569.846-.816zM4.92 10A5.493 5.493 0 0 0 4 13H1c0-.26.164-1.03.76-1.724.545-.636 1.492-1.256 3.16-1.275zM1.5 5.5a3 3 0 1 1 6 0 3 3 0 0 1-6 0zm3-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4z"/></svg>
|
||||
المعلمون والفريق
|
||||
</a>
|
||||
|
||||
<a href="center_subjects.php<?= $baseQuery ?>" class="sidebar-link <?= $activePage === 'subjects' ? 'active' : '' ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path d="M1 2.828c.885-.37 2.154-.769 3.388-.893 1.33-.134 2.458.156 3.112.752v9.746c-.935-.53-2.12-.603-3.213-.493-1.18.12-2.37.461-3.287.811V2.828zm7.5-.141c.654-.596 1.782-.886 3.112-.752 1.234.124 2.503.523 3.388.893v9.923c-.918-.35-2.107-.692-3.287-.81-1.094-.111-2.278-.039-3.213.492V2.687zM8 1.783C7.015.936 5.587.81 4.287.94c-1.514.153-3.042.672-3.994 1.105A.5.5 0 0 0 0 2.5v11a.5.5 0 0 0 .707.455c.882-.4 2.303-.881 3.68-1.02 1.409-.142 2.59.087 3.223.877a.5.5 0 0 0 .78 0c.633-.79 1.814-1.019 3.222-.877 1.378.139 2.8.62 3.681 1.02A.5.5 0 0 0 16 13.5v-11a.5.5 0 0 0-.293-.455c-.952-.433-2.48-.952-3.994-1.105C10.413.809 8.985.936 8 1.783z"/></svg>
|
||||
المواد الدراسية
|
||||
</a>
|
||||
|
||||
<a href="assessments.php<?= $baseQuery ?>" class="sidebar-link <?= $activePage === 'assessments' ? 'active' : '' ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path d="M10.854 7.146a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7.5 9.793l2.646-2.647a.5.5 0 0 1 .708 0z"/><path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2zM9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5v2z"/></svg>
|
||||
التقييمات
|
||||
</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>
|
||||
سجلات الغياب
|
||||
</a>
|
||||
|
||||
<div class="sidebar-label mt-3">إعدادات المركز</div>
|
||||
<a href="center_profile.php<?= $baseQuery ?>" class="sidebar-link <?= $activePage === 'profile' ? 'active' : '' ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path d="M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492zM5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0z"/><path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52l-.094-.319zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.291-.16c.764-.415 1.6.42 1.184 1.185l-.159.292a1.873 1.873 0 0 0 1.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.319.094a1.873 1.873 0 0 0-1.115 2.693l.16.291c.415.764-.42 1.6-1.185 1.184l-.291-.159a1.873 1.873 0 0 0-2.693 1.116l-.094.318c-.246.835-1.428.835-1.674 0l-.094-.319a1.873 1.873 0 0 0-2.692-1.115l-.292.16c-.764.415-1.6-.42-1.184-1.185l.159-.291A1.873 1.873 0 0 0 1.945 8.93l-.319-.094c-.835-.246-.835-1.428 0-1.674l.319-.094A1.873 1.873 0 0 0 3.06 4.377l-.16-.292c-.415-.764.42-1.6 1.185-1.184l.292.159a1.873 1.873 0 0 0 2.692-1.115l.094-.319z"/></svg>
|
||||
ملف المركز
|
||||
</a>
|
||||
|
||||
<div class="sidebar-label mt-3">العودة</div>
|
||||
<a href="admin.php" class="sidebar-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8z"/></svg>
|
||||
الإدارة المركزية
|
||||
</a>
|
||||
</div>
|
||||
</aside>
|
||||
3729
includes/cycles.php
Normal file
3729
includes/cycles.php
Normal file
File diff suppressed because it is too large
Load Diff
25
includes/pexels.php
Normal file
25
includes/pexels.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
function pexels_key() {
|
||||
$k = getenv('PEXELS_KEY');
|
||||
return $k && strlen($k) > 0 ? $k : 'Vc99rnmOhHhJAbgGQoKLZtsaIVfkeownoQNbTj78VemUjKh08ZYRbf18';
|
||||
}
|
||||
function pexels_get($url) {
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPHEADER => [ 'Authorization: '. pexels_key() ],
|
||||
CURLOPT_TIMEOUT => 15,
|
||||
]);
|
||||
$resp = curl_exec($ch);
|
||||
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
if ($code >= 200 && $code < 300 && $resp) return json_decode($resp, true);
|
||||
return null;
|
||||
}
|
||||
function download_to($srcUrl, $destPath) {
|
||||
$data = file_get_contents($srcUrl);
|
||||
if ($data === false) return false;
|
||||
if (!is_dir(dirname($destPath))) mkdir(dirname($destPath), 0775, true);
|
||||
return file_put_contents($destPath, $data) !== false;
|
||||
}
|
||||
87
includes/sidebar.php
Normal file
87
includes/sidebar.php
Normal file
@ -0,0 +1,87 @@
|
||||
<?php
|
||||
$script = basename($_SERVER['SCRIPT_NAME'] ?? '');
|
||||
$statusQuery = $_GET['status'] ?? '';
|
||||
|
||||
$activePage = 'admin';
|
||||
if ($script === 'app_settings.php') $activePage = 'app_settings';
|
||||
if ($script === 'subjects.php') $activePage = 'subjects';
|
||||
if ($script === 'assessment_categories.php') $activePage = 'assessment_categories';
|
||||
if ($script === 'global_cycles.php') $activePage = 'global_cycles';
|
||||
if ($script === 'dashboard.php') $activePage = 'dashboard';
|
||||
if ($script === 'applications.php') $activePage = ($statusQuery === 'approved') ? 'approved' : 'applications';
|
||||
if (in_array($script, ['approved_school.php', 'center_profile.php', 'students.php', 'teachers.php', 'assessments.php', 'attendance.php'], true)) $activePage = 'approved';
|
||||
if (in_array($script, ['center_assessments.php', 'center_assessment_criteria.php', 'center_assessment_score_sheet.php', 'center_assessment_report.php'], true)) $activePage = 'center_assessments';
|
||||
if (in_array($script, ['global_center_assessments.php', 'global_center_assessment_criteria.php'], true)) $activePage = 'global_center_assessments';
|
||||
if ($script === 'application_detail.php') $activePage = 'applications';
|
||||
if ($script === 'modules.php') $activePage = 'modules';
|
||||
|
||||
if (!isset($recentApproved)) {
|
||||
$app_list_for_sidebar = list_applications('approved');
|
||||
$recentApproved = $app_list_for_sidebar[0] ?? null;
|
||||
}
|
||||
?>
|
||||
<aside class="admin-sidebar sticky-top" style="top: 2rem; z-index: 10;">
|
||||
<div class="sidebar-nav">
|
||||
<div class="sidebar-label">الإدارة المركزية</div>
|
||||
<a href="admin.php" class="sidebar-link <?= $activePage === 'admin' ? 'active' : '' ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path d="M6 1H1v14h5V1zm9 0h-5v5h5V1zm0 9h-5v5h5v-5z"/><path d="M0 1a1 1 0 0 1 1-1h5a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1zm9 0a1 1 0 0 1 1-1h5a1 1 0 0 1 1-1v5a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1V1zm0 9a1 1 0 0 1 1-1h5a1 1 0 0 1 1-1v5a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-5z"/></svg>
|
||||
لوحة الإدارة
|
||||
</a>
|
||||
<a href="dashboard.php" class="sidebar-link <?= $activePage === 'dashboard' ? 'active' : '' ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path d="M8 4a.5.5 0 0 1 .5.5V6a.5.5 0 0 1-1 0V4.5A.5.5 0 0 1 8 4zM3.732 5.732a.5.5 0 0 1 .707 0l.915.914a.5.5 0 1 1-.708.708l-.914-.915a.5.5 0 0 1 0-.707zM2 10a.5.5 0 0 1 .5-.5h1.586a.5.5 0 0 1 0 1H2.5A.5.5 0 0 1 2 10zm9.5 0a.5.5 0 0 1 .5-.5h1.5a.5.5 0 0 1 0 1H12a.5.5 0 0 1-.5-.5zm.754-4.246a.389.389 0 0 0-.527-.02L7.547 9.31a.91.91 0 1 0 1.302 1.258l3.434-4.297a.389.389 0 0 0-.029-.518z"/><path fill-rule="evenodd" d="M0 10a8 8 0 1 1 15.547 2.661c-.442 1.253-1.845 1.602-2.932 1.25C11.309 13.488 9.475 13 8 13c-1.474 0-3.31.488-4.615.911-1.087.352-2.49.003-2.932-1.25A7.988 7.988 0 0 1 0 10zm8-7a7 7 0 0 0-6.603 9.329c.203.575.923.876 1.68.63C4.397 12.533 6.358 12 8 12s3.604.532 4.923.96c.757.245 1.477-.056 1.68-.631A7 7 0 0 0 8 3z"/></svg>
|
||||
لوحة القيادة
|
||||
</a>
|
||||
<a href="applications.php?status=approved" class="sidebar-link <?= $activePage === 'approved' ? 'active' : '' ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M14.763.075A.5.5 0 0 1 15 .5v15a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5V14h-1v1.5a.5.5 0 0 1-.5.5h-9a.5.5 0 0 1-.5-.5V10a.5.5 0 0 1 .342-.474L6 7.64V4.5a.5.5 0 0 1 .276-.447l8-4a.5.5 0 0 1 .487.022zM6 8.694 1 10.36V15h5V8.694zM7 15h2v-1.5a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5V15h2V1.309l-7 3.5V15z"/><path d="M2 11h1v1H2v-1zm2 0h1v1H4v-1zm-2 2h1v1H2v-1zm2 0h1v1H4v-1zm4-4h1v1H8V9zm2 0h1v1h-1V9zm-2 2h1v1H8v-1zm2 0h1v1h-1v-1zm2-2h1v1h-1V9zm0 2h1v1h-1v-1zM8 7h1v1H8V7zm2 0h1v1h-1V7zm2 0h1v1h-1V7zM8 5h1v1H8V5zm2 0h1v1h-1V5zm2 0h1v1h-1V5zm0-2h1v1h-1V3z"/></svg>
|
||||
المراكز المعتمدة
|
||||
</a>
|
||||
<a href="center_assessments.php" class="sidebar-link <?= $activePage === 'center_assessments' ? 'active' : '' ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path d="M3 14s-1 0-1-1 1-4 5-4 5 3 5 4-1 1-1 1H3Zm5-5a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"/><path d="M12.5 8a.5.5 0 0 1 .5.5V10h1.5a.5.5 0 0 1 0 1H13v1.5a.5.5 0 0 1-1 0V11h-1.5a.5.5 0 0 1 0-1H12V8.5a.5.5 0 0 1 .5-.5Z"/><path d="M1 2a2 2 0 0 1 2-2h7.5a.5.5 0 0 1 0 1H3a1 1 0 0 0-1 1v7.256A4.493 4.493 0 0 1 4.528 8h.471a4 4 0 1 1 6.002 0h.471c.506 0 .992.084 1.446.238A1.999 1.999 0 0 0 15 6.5V4a.5.5 0 0 1 1 0v2.5A3 3 0 0 1 13 9.47V14a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V2Z"/></svg>
|
||||
تقييم المراكز
|
||||
</a>
|
||||
<a href="global_center_assessments.php" class="sidebar-link <?= $activePage === 'global_center_assessments' ? 'active' : '' ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path d="M8 1a3 3 0 0 0-3 3v1H4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-1V4a3 3 0 0 0-3-3Zm2 4H6V4a2 2 0 1 1 4 0v1Z"/><path d="M5.5 8a.5.5 0 0 1 .5.5V9h4v-.5a.5.5 0 0 1 1 0V9h.5a.5.5 0 0 1 0 1H11v.5a.5.5 0 0 1-1 0V10H6v.5a.5.5 0 0 1-1 0V10h-.5a.5.5 0 0 1 0-1H5v-.5a.5.5 0 0 1 .5-.5Z"/></svg>
|
||||
قوالب تقييم المراكز
|
||||
</a>
|
||||
<a href="center_application.php" class="sidebar-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path d="M8 6.5a.5.5 0 0 1 .5.5v1.5H10a.5.5 0 0 1 0 1H8.5V11a.5.5 0 0 1-1 0V9.5H6a.5.5 0 0 1 0-1h1.5V7a.5.5 0 0 1 .5-.5z"/><path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/></svg>
|
||||
طلب فتح مركز
|
||||
</a>
|
||||
|
||||
|
||||
<div class="sidebar-label mt-3">الإعدادات</div>
|
||||
<a href="global_cycles.php" class="sidebar-link <?= $activePage === 'global_cycles' ? '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>
|
||||
الدورات الموسمية
|
||||
</a>
|
||||
<a href="app_settings.php" class="sidebar-link <?= $activePage === 'app_settings' ? 'active' : '' ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path d="M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492zM5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0z"/><path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52l-.094-.319zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.291-.16c.764-.415 1.6.42 1.184 1.185l-.159.292a1.873 1.873 0 0 0 1.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.319.094a1.873 1.873 0 0 0-1.115 2.693l.16.291c.415.764-.42 1.6-1.185 1.184l-.291-.159a1.873 1.873 0 0 0-2.693 1.116l-.094.318c-.246.835-1.428.835-1.674 0l-.094-.319a1.873 1.873 0 0 0-2.692-1.115l-.292.16c-.764.415-1.6-.42-1.184-1.185l.159-.291A1.873 1.873 0 0 0 1.945 8.93l-.319-.094c-.835-.246-.835-1.428 0-1.674l.319-.094A1.873 1.873 0 0 0 3.06 4.377l-.16-.292c-.415-.764.42-1.6 1.185-1.184l.292.159a1.873 1.873 0 0 0 2.692-1.115l.094-.319z"/></svg>
|
||||
الإعدادات العامة
|
||||
</a>
|
||||
<a href="applications.php" class="sidebar-link <?= $activePage === 'applications' ? 'active' : '' ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path d="M4.98 4a.5.5 0 0 0-.39.188L1.54 8H6a.5.5 0 0 1 .5.5 1.5 1.5 0 1 0 3 0A.5.5 0 0 1 10 8h4.46l-3.05-3.812A.5.5 0 0 0 11.02 4H4.98zm-1.17-.437A1.5 1.5 0 0 1 4.98 3h6.04a1.5 1.5 0 0 1 1.17.563l3.7 4.625a.5.5 0 0 1 .106.304l-.228 6.2a1.5 1.5 0 0 1-1.5 1.464H1.74a1.5 1.5 0 0 1-1.498-1.464l-.228-6.2a.5.5 0 0 1 .106-.304l3.7-4.625zM1 8.5v6a.5.5 0 0 0 .5.5h13a.5.5 0 0 0 .5-.5v-6h-3.415a2.5 2.5 0 0 1-4.17 0H1z"/></svg>
|
||||
إدارة الطلبات
|
||||
</a>
|
||||
<a href="subjects.php" class="sidebar-link <?= $activePage === 'subjects' ? 'active' : '' ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path d="M1 2.828c.885-.37 2.154-.769 3.388-.893 1.33-.134 2.458.156 3.112.752v9.746c-.935-.53-2.12-.603-3.213-.493-1.18.12-2.37.461-3.287.811V2.828zm7.5-.141c.654-.596 1.782-.886 3.112-.752 1.234.124 2.503.523 3.388.893v9.923c-.918-.35-2.107-.692-3.287-.81-1.094-.111-2.278-.039-3.213.492V2.687zM8 1.783C7.015.936 5.587.81 4.287.94c-1.514.153-3.042.672-3.994 1.105A.5.5 0 0 0 0 2.5v11a.5.5 0 0 0 .707.455c.882-.4 2.303-.881 3.68-1.02 1.409-.142 2.59.087 3.223.877a.5.5 0 0 0 .78 0c.633-.79 1.814-1.019 3.222-.877 1.378.139 2.8.62 3.681 1.02A.5.5 0 0 0 16 13.5v-11a.5.5 0 0 0-.293-.455c-.952-.433-2.48-.952-3.994-1.105C10.413.809 8.985.936 8 1.783z"/></svg>
|
||||
المواد الدراسية
|
||||
</a>
|
||||
<a href="assessment_categories.php" class="sidebar-link <?= $activePage === 'assessment_categories' ? 'active' : '' ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/><path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/></svg>
|
||||
فئات التقييم
|
||||
</a>
|
||||
<div class="sidebar-label mt-3">هيكلية النظام</div>
|
||||
<a href="modules.php" class="sidebar-link <?= $activePage === 'modules' ? 'active' : '' ?>">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M15.817.113A.5.5 0 0 1 16 .5v14a.5.5 0 0 1-.402.49l-5 1a.502.502 0 0 1-.196 0L5.5 15.01l-4.902.98A.5.5 0 0 1 0 15.5v-14a.5.5 0 0 1 .402-.49l5-1a.5.5 0 0 1 .196 0L10.5.99l4.902-.98a.5.5 0 0 1 .415.103zM10 1.91l-4-.8v12.98l4 .8V1.91zm1 12.98 4-.8V1.11l-4 .8v12.98zm-6-.8V1.11l-4 .8v12.98l4-.8z"/></svg>
|
||||
خريطة الصفحات
|
||||
</a>
|
||||
|
||||
<?php if (isset($recentApproved)): ?>
|
||||
<div class="sidebar-label mt-3">أحدث مركز معتمد</div>
|
||||
<a href="approved_school.php?id=<?= e((string) $recentApproved["id"]) ?>" class="sidebar-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path d="M4 16s-1 0-1-1 1-4 5-4 5 3 5 4-1 1-1 1H4Zm4-5.95a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z"/><path d="M2 1a2 2 0 0 0-2 2v9.5A1.5 1.5 0 0 0 1.5 14h.653a5.373 5.373 0 0 1 1.066-2H1V3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v9h-2.219c.554.654.89 1.373 1.066 2h.653a1.5 1.5 0 0 0 1.5-1.5V3a2 2 0 0 0-2-2H2Z"/></svg>
|
||||
<?= e((string) $recentApproved["center_name"]) ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</aside>
|
||||
241
index.php
241
index.php
@ -1,150 +1,103 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
@ini_set('display_errors', '1');
|
||||
@error_reporting(E_ALL);
|
||||
@date_default_timezone_set('UTC');
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
$phpVersion = PHP_VERSION;
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$flash = consume_flash();
|
||||
$stats = dashboard_metrics();
|
||||
|
||||
// Meaningful, clean text instead of placeholder info.
|
||||
render_page_start('الرئيسية', 'home', 'لوحة تحكم منصة إدارة المراكز الصيفية.');
|
||||
render_flash($flash);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>New Style</title>
|
||||
<?php
|
||||
// Read project preview data from environment
|
||||
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
|
||||
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
|
||||
?>
|
||||
<?php if ($projectDescription): ?>
|
||||
<!-- Meta description -->
|
||||
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
|
||||
<!-- Open Graph meta tags -->
|
||||
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<!-- Twitter meta tags -->
|
||||
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
|
||||
<?php endif; ?>
|
||||
<?php if ($projectImageUrl): ?>
|
||||
<!-- Open Graph image -->
|
||||
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<!-- Twitter image -->
|
||||
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
|
||||
<?php endif; ?>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
|
||||
animation: bg-pan 20s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
@keyframes bg-pan {
|
||||
0% { background-position: 0% 0%; }
|
||||
100% { background-position: 100% 100%; }
|
||||
}
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.loader {
|
||||
margin: 1.25rem auto 1.25rem;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.hint {
|
||||
opacity: 0.9;
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px; height: 1px;
|
||||
padding: 0; margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap; border: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
margin: 0 0 1rem;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
code {
|
||||
background: rgba(0,0,0,0.2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your website…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
|
||||
<div class="container-xxl py-4">
|
||||
<div class="row align-items-center mb-5 bg-white rounded-4 shadow-sm overflow-hidden" style="border: 1px solid var(--border-color);">
|
||||
<div class="col-lg-6 p-4 p-lg-5">
|
||||
<h1 class="display-6 fw-bold mb-3 text-primary">أهلاً بك في منصة الإدارة</h1>
|
||||
<p class="lead text-muted mb-4" style="line-height: 1.8;">
|
||||
توفر لك المنصة تحكماً شاملاً في كافة عمليات المراكز التعليمية. من خلال هذه الواجهة، يمكنك متابعة الطلبات، الإشراف على الاعتمادات، وإدارة الدورات والمواد التعليمية بكفاءة عالية وبأقل مجهود.
|
||||
</p>
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
<a class="btn btn-primary px-4 py-2" href="applications.php">مراجعة الطلبات</a>
|
||||
<a class="btn btn-outline-secondary px-4 py-2" href="dashboard.php">لوحة القيادة التشغيلية</a>
|
||||
</div>
|
||||
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will update automatically as the plan is implemented.</p>
|
||||
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: <?= htmlspecialchars($now) ?> (UTC)
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
<div class="col-lg-6 p-0 d-none d-lg-block" style="background-color: var(--bg-color);">
|
||||
<img src="assets/images/pexels/hero-office.jpg" alt="الإدارة" class="img-fluid w-100 h-100 object-fit-cover" style="min-height: 350px;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center justify-content-between mb-3 mt-5">
|
||||
<h3 class="h5 mb-0 fw-bold text-dark">نظرة عامة سريعة</h3>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-5">
|
||||
<div class="col-md-3">
|
||||
<div class="app-card stat-tile p-4 h-100 d-flex flex-column justify-content-center">
|
||||
<div class="mini-stat-label text-muted mb-2">إجمالي الطلبات</div>
|
||||
<div class="mini-stat-value display-6 fw-bold text-primary mb-1"><?= e($stats['all']) ?></div>
|
||||
<div class="mini-stat-copy small text-secondary mt-auto">كافة الطلبات الواردة للمنصة</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="app-card stat-tile p-4 h-100 d-flex flex-column justify-content-center">
|
||||
<div class="mini-stat-label text-muted mb-2">المراكز المعتمدة</div>
|
||||
<div class="mini-stat-value display-6 fw-bold text-success mb-1"><?= e($stats['approved']) ?></div>
|
||||
<div class="mini-stat-copy small text-secondary mt-auto">المراكز الجاهزة للتشغيل</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="app-card stat-tile p-4 h-100 d-flex flex-column justify-content-center">
|
||||
<div class="mini-stat-label text-muted mb-2">قيد المراجعة</div>
|
||||
<div class="mini-stat-value display-6 fw-bold text-warning mb-1"><?= e($stats['under_review']) ?></div>
|
||||
<div class="mini-stat-copy small text-secondary mt-auto">الطلبات التي تنتظر القرار</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="app-card stat-tile p-4 h-100 d-flex flex-column justify-content-center">
|
||||
<div class="mini-stat-label text-muted mb-2">الطاقة الاستيعابية</div>
|
||||
<div class="mini-stat-value display-6 fw-bold text-info mb-1"><?= e($stats['expected_students']) ?></div>
|
||||
<div class="mini-stat-copy small text-secondary mt-auto">إجمالي الطلاب المتوقع</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center justify-content-between mb-3 mt-5">
|
||||
<h3 class="h5 mb-0 fw-bold text-dark">الوصول السريع</h3>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-4">
|
||||
<a href="dashboard.php" class="text-decoration-none h-100 d-block">
|
||||
<article class="app-card link-card p-4 h-100 border-top border-primary border-4" style="min-height: auto;">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<h4 class="h6 fw-bold text-dark mb-0">المتابعة التشغيلية</h4>
|
||||
</div>
|
||||
<p class="text-muted small mb-0" style="line-height: 1.6;">تابع مؤشرات الأداء بشكل أعمق واستعرض آخر النشاطات والإحصائيات الخاصة بالمراكز.</p>
|
||||
</article>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<a href="center_application.php" class="text-decoration-none h-100 d-block">
|
||||
<article class="app-card link-card p-4 h-100 border-top border-warning border-4" style="min-height: auto;">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<h4 class="h6 fw-bold text-dark mb-0">طلب مركز جديد</h4>
|
||||
</div>
|
||||
<p class="text-muted small mb-0" style="line-height: 1.6;">واجهة مخصصة ومستقلة لملء وتقديم بيانات المراكز الجديدة للمراجعة بدون أي تشتيت.</p>
|
||||
</article>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<a href="modules.php" class="text-decoration-none h-100 d-block">
|
||||
<article class="app-card link-card p-4 h-100 border-top border-success border-4" style="min-height: auto;">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<h4 class="h6 fw-bold text-dark mb-0">بنية النظام</h4>
|
||||
</div>
|
||||
<p class="text-muted small mb-0" style="line-height: 1.6;">نظرة شاملة على الوحدات المفعلة حالياً في النظام ومسارات التطوير المخطط لها مستقبلاً.</p>
|
||||
</article>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php render_page_end(); ?>
|
||||
79
modules.php
Normal file
79
modules.php
Normal file
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
$flash = consume_flash();
|
||||
|
||||
render_page_start('هيكل النظام', 'modules', 'صفحة منفصلة تعرض تنظيم وحدات تطبيق إدارة المراكز الصيفية والنطاق الحالي للنسخة الأولية.');
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="py-4 py-lg-5">
|
||||
<div class="container-xxl">
|
||||
<div class="admin-layout row g-4 align-items-start">
|
||||
<div class="col-lg-3 layout-sidebar-column">
|
||||
<?php require __DIR__ . '/includes/sidebar.php'; ?>
|
||||
</div>
|
||||
<div class="col-lg-9 layout-content-column">
|
||||
|
||||
<div class="app-card mb-4">
|
||||
<div class="section-title mb-2">هيكل الصفحات والوحدات</div>
|
||||
<p class="text-muted mb-0">تم فصل المحتوى التعريفي والتنظيمي في هذه الصفحة حتى تبقى الصفحات التشغيلية مركزة: التقديم في شاشة، المتابعة في شاشة، والتفاصيل في شاشة مستقلة.</p>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-7">
|
||||
<div class="app-card h-100">
|
||||
<div class="section-title mb-3">النطاق الحالي في هذه النسخة</div>
|
||||
<div class="module-list">
|
||||
<article class="module-item">
|
||||
<h2>1. الرئيسية</h2>
|
||||
<p>صفحة دخول مرتبة مع بطاقات وصول سريع إلى كل جزء من النظام دون خلط التفاصيل التشغيلية.</p>
|
||||
</article>
|
||||
<article class="module-item">
|
||||
<h2>2. لوحة القيادة</h2>
|
||||
<p>شاشة متابعة يومية تعرض المؤشرات وآخر الطلبات الواردة للمشرف العام.</p>
|
||||
</article>
|
||||
<article class="module-item">
|
||||
<h2>3. طلب فتح مركز</h2>
|
||||
<p>نموذج عربي كامل مع تحقق للحقول الأساسية والطاقة التشغيلية ومواعيد البرنامج.</p>
|
||||
</article>
|
||||
<article class="module-item">
|
||||
<h2>4. لوحة الطلبات</h2>
|
||||
<p>قائمة موحدة مع فرز بالحالة لتسهيل العمل الإشرافي والانتقال السريع للتفاصيل.</p>
|
||||
</article>
|
||||
<article class="module-item">
|
||||
<h2>5. صفحة المراجعة التفصيلية</h2>
|
||||
<p>عرض بيانات المركز، تسجيل الملاحظات، تحديث الحالة، وإسناد التقييم الأولي.</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<div class="app-card h-100">
|
||||
<div class="section-title mb-3">مسار الاستخدام الحالي</div>
|
||||
<ol class="workflow-list mb-4">
|
||||
<li>يرسل مدير المركز طلب الافتتاح من شاشة مستقلة.</li>
|
||||
<li>تظهر الطلبات فوراً داخل لوحة الطلبات ولوحة القيادة.</li>
|
||||
<li>يفتح المشرف العام صفحة التفاصيل لإضافة الملاحظات والتقييم.</li>
|
||||
<li>تُحدث الحالة إلى قيد الاستلام أو تحت المراجعة أو معتمد أو بحاجة إلى استكمال.</li>
|
||||
</ol>
|
||||
|
||||
<div class="section-title mb-2">ما الذي تم تحسينه؟</div>
|
||||
<p class="text-muted mb-4">أصبح التنظيم أوضح لأن كل صفحة الآن تؤدي غرضاً واحداً محدداً، وهذا أسهل للتوسع لاحقاً إلى التسجيل الطلابي، الحضور، والتقييمات التشغيلية.</p>
|
||||
|
||||
<div class="section-title mb-2">المرحلة التالية المقترحة</div>
|
||||
<p class="text-muted mb-3">بعد هذا التنظيم، الخطوة الطبيعية التالية هي بناء وحدات الطلاب والتسجيل في المراكز المعتمدة مع نفس المبدأ: صفحة مستقلة لكل وظيفة.</p>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a class="btn btn-primary btn-sm" href="admin.php">لوحة الإدارة</a>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="dashboard.php">لوحة القيادة</a>
|
||||
<a class="btn btn-outline-secondary btn-sm" href="application_detail.php?id=1">طلب نموذجي</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
<?php render_page_end(); ?>
|
||||
32
patch_admin_pages.py
Normal file
32
patch_admin_pages.py
Normal file
@ -0,0 +1,32 @@
|
||||
import re
|
||||
|
||||
files = ['subjects.php', 'global_cycles.php']
|
||||
|
||||
for file in files:
|
||||
with open(file, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
access_check = r"""
|
||||
if (!is_super_admin()) {
|
||||
http_response_code(403);
|
||||
render_page_start('صلاحيات غير كافية', '');
|
||||
?>
|
||||
<section class="py-5 text-center">
|
||||
<div class="container-xxl">
|
||||
<h1 class="mb-3">عذراً</h1>
|
||||
<p>هذه الصفحة مخصصة للمشرف العام فقط.</p>
|
||||
<a href="index.php" class="btn btn-dark mt-3">العودة للرئيسية</a>
|
||||
</div>
|
||||
</section>
|
||||
<?php
|
||||
render_page_end();
|
||||
exit;
|
||||
}
|
||||
"""
|
||||
|
||||
if 'if (!is_super_admin())' not in content:
|
||||
content = content.replace("require_once __DIR__ . '/includes/app.php';", "require_once __DIR__ . '/includes/app.php';\n" + access_check)
|
||||
|
||||
with open(file, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
93
patch_app.php
Normal file
93
patch_app.php
Normal file
@ -0,0 +1,93 @@
|
||||
<?php
|
||||
$content = file_get_contents('includes/app.php');
|
||||
|
||||
$content = str_replace(
|
||||
'\'notes\' => \'',
|
||||
'\'notes\' => \'',
|
||||
'\'subjects\' => [],
|
||||
$content
|
||||
);
|
||||
|
||||
$old_loop = <<<'EOD'
|
||||
foreach ($data as $key => $_value) {
|
||||
$data[$key] = clean_text((string) ($input[$key] ?? ''), $key === 'notes' ? 1000 : 190);
|
||||
}
|
||||
EOD;
|
||||
|
||||
$new_loop = <<<'EOD'
|
||||
foreach ($data as $key => $_value) {
|
||||
if ($key === 'subjects') {
|
||||
$data[$key] = is_array($input[$key] ?? []) ? $input[$key] : [];
|
||||
continue;
|
||||
}
|
||||
$data[$key] = clean_text((string) ($input[$key] ?? ''), $key === 'notes' ? 1000 : 190);
|
||||
}
|
||||
EOD;
|
||||
$content = str_replace($old_loop, $new_loop, $content);
|
||||
|
||||
$old_val = <<<'EOD'
|
||||
if ($startDate !== '' && $endDate !== '' && strtotime($endDate) < strtotime($startDate)) {
|
||||
$errors['end_date'] = 'يجب أن يكون تاريخ النهاية بعد البداية.';
|
||||
}
|
||||
EOD;
|
||||
|
||||
$new_val = $old_val . <<<'EOD'
|
||||
|
||||
if (empty($data['subjects'])) {
|
||||
$errors['subjects'] = 'يرجى اختيار مادة واحدة على الأقل.';
|
||||
} else {
|
||||
$data['subjects'] = array_map('intval', $data['subjects']);
|
||||
}
|
||||
EOD;
|
||||
$content = str_replace($old_val, $new_val, $content);
|
||||
|
||||
$old_insert_sql = <<<'EOD'
|
||||
'INSERT INTO center_applications (
|
||||
center_name, city, center_type, gender_scope, director_name, phone, email,
|
||||
expected_students, start_date, end_date, notes, status, submitted_at, updated_at
|
||||
) VALUES (
|
||||
:center_name, :city, :center_type, :gender_scope, :director_name, :phone, :email,
|
||||
:expected_students, :start_date, :end_date, :notes, :status, NOW(), NOW()
|
||||
)'
|
||||
EOD;
|
||||
|
||||
$new_insert_sql = <<<'EOD'
|
||||
'INSERT INTO center_applications (
|
||||
center_name, city, center_type, gender_scope, director_name, phone, email,
|
||||
expected_students, start_date, end_date, notes, subjects, status, submitted_at, updated_at
|
||||
) VALUES (
|
||||
:center_name, :city, :center_type, :gender_scope, :director_name, :phone, :email,
|
||||
:expected_students, :start_date, :end_date, :notes, :subjects, :status, NOW(), NOW()
|
||||
)'
|
||||
EOD;
|
||||
$content = str_replace($old_insert_sql, $new_insert_sql, $content);
|
||||
|
||||
$old_execute = <<<'EOD'
|
||||
':notes' => $data['notes'],
|
||||
':status' => 'submitted',
|
||||
]);
|
||||
EOD;
|
||||
|
||||
$new_execute = <<<'EOD'
|
||||
':notes' => $data['notes'],
|
||||
':subjects' => json_encode($data['subjects']),
|
||||
':status' => 'submitted',
|
||||
]);
|
||||
EOD;
|
||||
$content = str_replace($old_execute, $new_execute, $content);
|
||||
|
||||
if (strpos($content, "function get_enabled_subjects") === false) {
|
||||
$content .= <<<'EOD'
|
||||
|
||||
function get_enabled_subjects(): array
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->query("SELECT id, name, description FROM subjects WHERE status = 'enabled' ORDER BY name ASC");
|
||||
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
return $result !== false ? $result : [];
|
||||
}
|
||||
EOD;
|
||||
}
|
||||
|
||||
file_put_contents('includes/app.php', $content);
|
||||
echo "Patch applied.";
|
||||
34
patch_app2.php
Normal file
34
patch_app2.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
$content = file_get_contents('includes/app.php');
|
||||
|
||||
$content = str_replace(
|
||||
'\'notes\' => \'',
|
||||
'\'notes\' => \'',
|
||||
'\'subjects\' => [],
|
||||
$content
|
||||
);
|
||||
|
||||
$old_loop = " foreach (\$data as \$key => \$_value) {\n \$data[\$key] = clean_text((string) (\$input[\$key] ?? \'\'), \$key === 'notes' ? 1000 : 190);\n }";
|
||||
$new_loop = " foreach (\$data as \$key => \$_value) {\n if (\$key === 'subjects') {\n \$data[\$key] = is_array(\$input[\$key] ?? []) ? \$input[\$key] : [];\n continue;\n }\n \$data[\$key] = clean_text((string) (\$input[\$key] ?? \'\'), \$key === 'notes' ? 1000 : 190);\n }";
|
||||
$content = str_replace($old_loop, $new_loop, $content);
|
||||
|
||||
$old_val = " if (\$startDate !== '' && \$endDate !== '' && strtotime(\$endDate) < strtotime(\$startDate)) {\n \$errors['end_date'] = 'يجب أن يكون تاريخ النهاية بعد البداية.';\n }";
|
||||
$new_val = $old_val . "\n\n if (empty(\$data['subjects'])) {\n \$errors['subjects'] = 'يرجى اختيار مادة واحدة على الأقل.';\n } else {\n \$data['subjects'] = array_map('intval', \$data['subjects']);\n }";
|
||||
$content = str_replace($old_val, $new_val, $content);
|
||||
|
||||
$old_insert_sql = " 'INSERT INTO center_applications (\n center_name, city, center_type, gender_scope, director_name, phone, email,\n expected_students, start_date, end_date, notes, status, submitted_at, updated_at\n ) VALUES (\n :center_name, :city, :center_type, :gender_scope, :director_name, :phone, :email,\n :expected_students, :start_date, :end_date, :notes, :status, NOW(), NOW()\n )'";
|
||||
$new_insert_sql = " 'INSERT INTO center_applications (\n center_name, city, center_type, gender_scope, director_name, phone, email,\n expected_students, start_date, end_date, notes, subjects, status, submitted_at, updated_at\n ) VALUES (\n :center_name, :city, :center_type, :gender_scope, :director_name, :phone, :email,\n :expected_students, :start_date, :end_date, :notes, :subjects, :status, NOW(), NOW()\n )'";
|
||||
$content = str_replace($old_insert_sql, $new_insert_sql, $content);
|
||||
|
||||
$old_execute = " ':notes' => \$data['notes'],\n ':status' => 'submitted',\n ]);";
|
||||
$new_execute = " ':notes' => \$data['notes'],\n ':subjects' => json_encode(\$data['subjects']),
|
||||
':status' => 'submitted',\n ]);";
|
||||
$content = str_replace($old_execute, $new_execute, $content);
|
||||
|
||||
if (strpos($content, "function get_enabled_subjects") === false) {
|
||||
$content .= "\nfunction get_enabled_subjects(): array\n{\n \$pdo = db_connection();\n \$stmt = \$pdo->query(\"SELECT id, name, description FROM subjects WHERE status = 'enabled' ORDER BY name ASC\");\n \$result = \$stmt->fetchAll(PDO::FETCH_ASSOC);\n return \$result !== false ? \$result : [];\n}\n";
|
||||
}
|
||||
|
||||
file_put_contents('includes/app.php', $content);
|
||||
echo "Patch applied.\n";
|
||||
|
||||
54
patch_app3.php
Normal file
54
patch_app3.php
Normal file
@ -0,0 +1,54 @@
|
||||
<?php
|
||||
$file = 'includes/app.php';
|
||||
$content = file_get_contents($file);
|
||||
|
||||
$old_func = <<<'EOL'
|
||||
function assessment_category_options(): array
|
||||
{
|
||||
return [
|
||||
'تشخيصي',
|
||||
'واجب',
|
||||
'مشاركة',
|
||||
'مشروع',
|
||||
'أداء',
|
||||
'اختبار قصير',
|
||||
'اختبار نهائي',
|
||||
];
|
||||
}
|
||||
EOL;
|
||||
|
||||
$new_func = <<<'EOL'
|
||||
function assessment_category_options(): array
|
||||
{
|
||||
try {
|
||||
$stmt = db()->query("SELECT name FROM assessment_categories WHERE status = 'enabled' ORDER BY name ASC");
|
||||
$categories = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
return $categories ?: [];
|
||||
} catch (\PDOException $e) {
|
||||
// Fallback in case table doesn't exist yet or db error
|
||||
return [
|
||||
'تشخيصي',
|
||||
'واجب',
|
||||
'مشاركة',
|
||||
'مشروع',
|
||||
'أداء',
|
||||
'اختبار قصير',
|
||||
'اختبار نهائي',
|
||||
];
|
||||
}
|
||||
}
|
||||
EOL;
|
||||
|
||||
$new_content = str_replace($old_func, $new_func, $content);
|
||||
|
||||
if ($new_content === $content) {
|
||||
echo "Failed to replace. Let's try regex replace\n";
|
||||
$new_content = preg_replace('/function assessment_category_options\(\): array\s*\{.*?\}/s', $new_func, $content);
|
||||
if ($new_content === $content) {
|
||||
echo "Regex replace also failed.\n";
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
file_put_contents($file, $new_content);
|
||||
echo "Successfully updated assessment_category_options() in includes/app.php\n";
|
||||
36
patch_app_role.py
Normal file
36
patch_app_role.py
Normal file
@ -0,0 +1,36 @@
|
||||
import re
|
||||
|
||||
with open('includes/app.php', 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# 1. Add is_super_admin()
|
||||
role_func = """
|
||||
function is_super_admin(): bool {
|
||||
if (isset($_GET['role'])) {
|
||||
$_SESSION['role'] = $_GET['role'];
|
||||
}
|
||||
return ($_SESSION['role'] ?? 'super_admin') === 'super_admin';
|
||||
}
|
||||
"""
|
||||
if 'function is_super_admin' not in content:
|
||||
content = content.replace("function env_value", role_func + "\nfunction env_value", 1)
|
||||
|
||||
# 2. Update the header
|
||||
header_old = '<span class="header-chip">صلاحية: المشرف العام</span>'
|
||||
header_new = """<?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>"""
|
||||
content = content.replace(header_old, header_new)
|
||||
|
||||
with open('includes/app.php', 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
38
patch_approved_school.py
Normal file
38
patch_approved_school.py
Normal file
@ -0,0 +1,38 @@
|
||||
import re
|
||||
|
||||
with open('approved_school.php', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Replace the layout start
|
||||
layout_start_replacement = """<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 (!$isApproved): ?>"""
|
||||
|
||||
content = re.sub(r'<section class="py-4 py-lg-5">\s*<div class="container-xxl">\s*<\?php if \(!\$isApproved\): \?>', layout_start_replacement, content)
|
||||
|
||||
# Replace the layout end. Wait, currently it is:
|
||||
# </div>
|
||||
# </div>
|
||||
#
|
||||
# </div>
|
||||
# </section>
|
||||
# Let's just do string replacement for the end.
|
||||
end_str = """ </div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>"""
|
||||
new_end_str = """ </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>"""
|
||||
content = content.replace(end_str, new_end_str)
|
||||
|
||||
with open('approved_school.php', 'w') as f:
|
||||
f.write(content)
|
||||
28
patch_approved_school_cycles.py
Normal file
28
patch_approved_school_cycles.py
Normal file
@ -0,0 +1,28 @@
|
||||
import re
|
||||
|
||||
with open('approved_school.php', 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Hide "archive cycle" form in approved_school.php
|
||||
archive_form = r"""(<form method="post" class="d-inline">\s*<input type="hidden" name="cycle_action" value="archive_cycle">.*?<\/form>)"""
|
||||
content = re.sub(archive_form, r'<?php if (is_super_admin()): ?>\1<?php endif; ?>', content, flags=re.DOTALL)
|
||||
|
||||
# Hide col-lg-4 containing cycles switcher and start new cycle form
|
||||
col_lg_4_cycles = r"""(<div class="col-lg-4">\s*<div class="app-card sidebar-card mb-4">\s*<div class="section-title mb-3">كل دورات المركز.*?</div>\s*</div>)"""
|
||||
content = re.sub(col_lg_4_cycles, r'<?php if (is_super_admin()): ?>\n\1\n<?php endif; ?>', content, flags=re.DOTALL)
|
||||
|
||||
# Adjust col-lg-8 to be conditionally 12
|
||||
content = re.sub(r'<div class="col-lg-8">', r'<div class="<?= is_super_admin() ? \'col-lg-8\' : \'col-lg-12\' ?>">', content)
|
||||
|
||||
# Check POST handler for cycles in approved_school.php
|
||||
post_handler = r"if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['cycle_action'])) {"
|
||||
post_handler_new = r"""if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['cycle_action'])) {
|
||||
if (!is_super_admin()) {
|
||||
set_flash('error', 'عذراً، الإدارة الكاملة للدورات متاحة للمشرف العام فقط.');
|
||||
header('Location: approved_school.php?id=' . $application['id']);
|
||||
exit;
|
||||
}"""
|
||||
content = content.replace(post_handler, post_handler_new)
|
||||
|
||||
with open('approved_school.php', 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
42
patch_center_subjects.py
Normal file
42
patch_center_subjects.py
Normal file
@ -0,0 +1,42 @@
|
||||
import re
|
||||
|
||||
with open('center_subjects.php', 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Protect POST update action
|
||||
post_handler = r"if \(\$_SERVER\['REQUEST_METHOD'\] === 'POST' && isset\(\$_POST\['action'\]\) && \$_POST\['action'\] === 'update_subjects'\) \{"
|
||||
post_handler_new = r"""if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'update_subjects') {
|
||||
if (!is_super_admin()) {
|
||||
$errors['form'] = 'عذراً، تعديل المواد متاح للمشرف العام فقط.';
|
||||
} else {"""
|
||||
content = content.replace(post_handler, post_handler_new)
|
||||
|
||||
# Find the try/catch block to close the else
|
||||
try_block = r""" update_application_subjects\(\(int\) \$application\['id'\], \$selected_subjects_ids\);.*?\} catch \(Throwable \$e\) \{.*?\}"""
|
||||
try_block_new = r""" update_application_subjects((int) $application['id'], $selected_subjects_ids);
|
||||
set_flash('success', 'تم تحديث المواد الدراسية للمركز بنجاح.');
|
||||
$selectedCycleIdStr = $selectedCycleId > 0 ? '&cycle=' . $selectedCycleId : '';
|
||||
header('Location: center_subjects.php?id=' . $application['id'] . $selectedCycleIdStr);
|
||||
exit;
|
||||
} catch (Throwable $e) {
|
||||
$errors['form'] = 'تعذر حفظ البيانات. يرجى المحاولة لاحقاً.';
|
||||
}
|
||||
}"""
|
||||
content = re.sub(r" update_application_subjects\(\(int\) \$application\['id'\], \$selected_subjects_ids\);.*?\} catch \(Throwable \$e\) \{.*?\}", try_block_new, content, flags=re.DOTALL)
|
||||
|
||||
|
||||
# Disable checkboxes and hide button if not super admin
|
||||
checkbox = r'<input class="form-check-input ms-0 me-2 mt-1 float-end" type="checkbox" name="subjects\[\]"'
|
||||
checkbox_new = r'<input class="form-check-input ms-0 me-2 mt-1 float-end" type="checkbox" name="subjects[]" <?= is_super_admin() ? "" : "disabled" ?>'
|
||||
content = re.sub(checkbox, checkbox_new, content)
|
||||
|
||||
btn = r'<button type="submit" class="btn btn-dark px-4">حفظ المواد الدراسية</button>'
|
||||
btn_new = r"""<?php if (is_super_admin()): ?>
|
||||
<button type="submit" class="btn btn-dark px-4">حفظ المواد الدراسية</button>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-warning mb-0">يمكن للمشرف العام فقط تعديل هذه القائمة.</div>
|
||||
<?php endif; ?>"""
|
||||
content = content.replace(btn, btn_new)
|
||||
|
||||
with open('center_subjects.php', 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
40
patch_cycles_switcher.py
Normal file
40
patch_cycles_switcher.py
Normal file
@ -0,0 +1,40 @@
|
||||
import re
|
||||
import glob
|
||||
|
||||
files = ['students.php', 'teachers.php', 'assessments.php', 'attendance.php']
|
||||
|
||||
for file in files:
|
||||
with open(file, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Change <div class="col-lg-7"> to conditionally be 12
|
||||
# But wait, is it col-lg-7?
|
||||
content = re.sub(r'<div class="col-lg-7">', r'<div class="col-lg-<?= is_super_admin() ? \'7\' : \'12\' ?>', content)
|
||||
|
||||
# Wrap col-lg-5 in is_super_admin
|
||||
# We will find the start of col-lg-5 that contains "التبديل بين الدورات"
|
||||
# and ends at the closing div
|
||||
|
||||
# Let's find:
|
||||
# <div class="col-lg-5">
|
||||
# <div class="app-card sidebar-card h-100">
|
||||
# <div class="section-title mb-3">التبديل بين الدورات</div>
|
||||
# ...
|
||||
# </div>
|
||||
# </div>
|
||||
|
||||
pattern = r'(<div class="col-lg-5">\s*<div class="app-card sidebar-card h-100">\s*<div class="section-title mb-3">التبديل بين الدورات</div>.*?</div>\s*</div>)'
|
||||
|
||||
def replacer(match):
|
||||
return f"<?php if (is_super_admin()): ?>\n{match.group(1)}\n<?php endif; ?>"
|
||||
|
||||
content = re.sub(pattern, replacer, content, flags=re.DOTALL)
|
||||
|
||||
# Also restrict the button "إدارة الدورات الموسمية"
|
||||
# <a class="btn btn-outline-secondary" href="<?= e($approvedSchoolUrl) ?>#cycles">إدارة الدورات الموسمية</a>
|
||||
# We should hide this button for center_admin
|
||||
btn_pattern = r'(<a class="btn btn-outline-secondary" href="<\?= e\(\$approvedSchoolUrl\) \?>#cycles">إدارة الدورات الموسمية</a>)'
|
||||
content = re.sub(btn_pattern, r'<?php if (is_super_admin()): ?>\1<?php endif; ?>', content)
|
||||
|
||||
with open(file, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
21
patch_global.py
Normal file
21
patch_global.py
Normal file
@ -0,0 +1,21 @@
|
||||
import re
|
||||
|
||||
with open('global_center_assessments.php', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
replacement = """<a class="btn btn-primary btn-sm" href="global_center_assessment_criteria.php?assessment_id=<?= e((string) ($assessment['id'] ?? 0)) ?>" title="البنود">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M14.5 3a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-13a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h13zm-13-1A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2h-13z"/>
|
||||
<path d="M3 5.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zM3 8.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5zM3 11.5a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="btn btn-success btn-sm" href="execute_global_assessment.php?id=<?= e((string) ($assessment['id'] ?? 0)) ?>" title="تطبيق التقييم">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
|
||||
</svg> تطبيق
|
||||
</a>"""
|
||||
|
||||
content = re.sub(r'<a class="btn btn-primary btn-sm" href="global_center_assessment_criteria\.php.*?</svg>\s*</a>', replacement, content, flags=re.DOTALL)
|
||||
|
||||
with open('global_center_assessments.php', 'w') as f:
|
||||
f.write(content)
|
||||
7
run_migration.php
Normal file
7
run_migration.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
require 'db/config.php';
|
||||
$pdo = db();
|
||||
$sql = file_get_contents('db/migrations/20260417_add_global_template_id_to_assessment.sql');
|
||||
$pdo->exec($sql);
|
||||
echo "Migration applied.\n";
|
||||
|
||||
251
student_certificate.php
Normal file
251
student_certificate.php
Normal file
@ -0,0 +1,251 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
function certificate_date_label(string $date): string
|
||||
{
|
||||
if ($date === '' || strtotime($date) === false) {
|
||||
return '—';
|
||||
}
|
||||
|
||||
return date('Y/m/d', strtotime($date));
|
||||
}
|
||||
|
||||
function certificate_number_value(int $value): string
|
||||
{
|
||||
return number_format($value);
|
||||
}
|
||||
|
||||
function certificate_decimal_value(float $value): string
|
||||
{
|
||||
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;
|
||||
$studentId = filter_input(INPUT_GET, 'student_id', FILTER_VALIDATE_INT) ?: 0;
|
||||
$application = $applicationId > 0 ? get_application($applicationId) : null;
|
||||
$isApprovedSchool = $application && (string) $application['status'] === 'approved';
|
||||
$cycleContext = ['cycles' => [], 'selected' => null, 'active' => null, 'read_only' => false];
|
||||
$selectedCycle = null;
|
||||
$selectedCycleId = 0;
|
||||
$cycleLabel = 'لا توجد دورة بعد';
|
||||
|
||||
if ($application && $isApprovedSchool) {
|
||||
$cycleContext = resolve_school_cycle_context((int) $application['id'], $application, $requestedCycleId);
|
||||
$selectedCycle = $cycleContext['selected'];
|
||||
$selectedCycleId = $selectedCycle ? (int) ($selectedCycle['id'] ?? 0) : 0;
|
||||
$cycleLabel = $selectedCycle ? (string) $selectedCycle['cycle_name'] : $cycleLabel;
|
||||
}
|
||||
|
||||
$certificate = $isApprovedSchool && $selectedCycleId > 0 && $studentId > 0
|
||||
? school_student_certificate_summary((int) $application['id'], $selectedCycleId, $studentId)
|
||||
: [
|
||||
'student' => null,
|
||||
'assessments' => [],
|
||||
'has_results' => false,
|
||||
'completed_assessments' => 0,
|
||||
'active_assessments' => 0,
|
||||
'missing_assessments' => 0,
|
||||
'absent_assessments' => 0,
|
||||
'excused_assessments' => 0,
|
||||
'overall_percentage' => 0.0,
|
||||
'score_total' => 0.0,
|
||||
'max_score_total' => 0.0,
|
||||
'latest_assessed_on' => '',
|
||||
'performance' => student_certificate_performance_meta(0.0),
|
||||
];
|
||||
$student = is_array($certificate['student'] ?? null) ? $certificate['student'] : null;
|
||||
|
||||
if (!$application || !$isApprovedSchool || $selectedCycleId <= 0 || $studentId <= 0 || $student === null) {
|
||||
http_response_code(404);
|
||||
}
|
||||
|
||||
$pageTitle = $student
|
||||
? 'شهادة الطالب: ' . (string) $student['full_name']
|
||||
: 'شهادة الإنجاز';
|
||||
$pageDescription = 'شهادة إنجاز قابلة للطباعة تعرض أداء الطالب الإجمالي بعد انتهاء الدورة، مع تصنيف الأداء النهائي وملخص التقييمات.';
|
||||
$studentsUrl = $application ? school_page_url('students.php', (int) $application['id'], $selectedCycleId) : 'students.php';
|
||||
$approvedSchoolUrl = $application ? school_page_url('approved_school.php', (int) $application['id'], $selectedCycleId) : 'approved_school.php';
|
||||
$performance = $certificate['performance'] ?? student_certificate_performance_meta(0.0);
|
||||
$issuedOn = date('Y-m-d');
|
||||
$coverageNote = '';
|
||||
if (($certificate['missing_assessments'] ?? 0) > 0) {
|
||||
$coverageNote = 'يعتمد هذا التقدير على التقييمات المرصودة حالياً فقط.';
|
||||
} elseif (($certificate['completed_assessments'] ?? 0) > 0) {
|
||||
$coverageNote = 'تم احتساب الأداء النهائي من متوسط التقييمات المرصودة في هذه الدورة.';
|
||||
}
|
||||
|
||||
render_page_start($pageTitle, 'approved', $pageDescription);
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="student-certificate-page py-4 py-lg-5">
|
||||
<div class="container-xl">
|
||||
<div class="certificate-toolbar">
|
||||
<div>
|
||||
<div class="section-title">شهادة الإنجاز والمكافأة</div>
|
||||
<div class="section-subtle">صفحة قابلة للطباعة بعد إكمال تقييمات الطالب في الدورة الحالية.</div>
|
||||
</div>
|
||||
<div class="certificate-toolbar-actions">
|
||||
<a class="btn btn-outline-secondary" href="<?= e($studentsUrl) ?>">العودة إلى الطلاب</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($approvedSchoolUrl) ?>">صفحة المركز</a>
|
||||
<?php if ($student): ?>
|
||||
<button type="button" class="btn btn-primary" onclick="window.print()">طباعة الشهادة</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</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>
|
||||
<?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 ($selectedCycleId <= 0 || !$student): ?>
|
||||
<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($studentsUrl) ?>">العودة إلى سجل الطلاب</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<article class="student-certificate-card" aria-labelledby="certificate-title">
|
||||
<div class="student-certificate-body">
|
||||
<span class="certificate-kicker">Certificate of Completion & Performance</span>
|
||||
<h1 class="certificate-title" id="certificate-title">شهادة تكريم وإتمام الدورة</h1>
|
||||
<p class="certificate-subtitle mb-0">
|
||||
تشهد إدارة <strong><?= e((string) $application['center_name']) ?></strong> بأن الطالب/الطالبة
|
||||
<strong><?= e((string) $student['full_name']) ?></strong>
|
||||
قد أتم/أتمت متطلبات الدورة <strong><?= e($cycleLabel) ?></strong>
|
||||
وتم تقييم أدائه/أدائها وفق السجلات المرصودة في النظام.
|
||||
</p>
|
||||
|
||||
<div class="certificate-student-name"><?= e((string) $student['full_name']) ?></div>
|
||||
<div class="section-subtle">Student Code: <?= e((string) $student['student_code']) ?> · <?= e((string) $student['grade_level']) ?></div>
|
||||
|
||||
<div class="certificate-meta-grid">
|
||||
<div class="certificate-meta-item">
|
||||
<strong>المركز</strong>
|
||||
<span><?= e((string) $application['center_name']) ?></span>
|
||||
</div>
|
||||
<div class="certificate-meta-item">
|
||||
<strong>الدورة</strong>
|
||||
<span><?= e($cycleLabel) ?></span>
|
||||
</div>
|
||||
<div class="certificate-meta-item">
|
||||
<strong>مدة الدورة</strong>
|
||||
<span><?= e(certificate_date_label((string) ($selectedCycle['start_date'] ?? ''))) ?> — <?= e(certificate_date_label((string) ($selectedCycle['end_date'] ?? ''))) ?></span>
|
||||
</div>
|
||||
<div class="certificate-meta-item">
|
||||
<strong>تاريخ الإصدار</strong>
|
||||
<span><?= e(certificate_date_label($issuedOn)) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="certificate-grid">
|
||||
<div class="certificate-panel">
|
||||
<strong class="mb-2">نص الشهادة</strong>
|
||||
<p class="mb-3">بناءً على حضور الطالب ومشاركته ونتائج تقييماته خلال هذه الدورة، تم منحه/منحها هذه الشهادة التقديرية مع بيان مستوى الأداء العام أدناه.</p>
|
||||
<div class="performance-pill performance-<?= e((string) ($performance['key'] ?? 'poor')) ?> mb-3">
|
||||
<?= e((string) ($performance['label_ar'] ?? 'ضعيف')) ?> — <?= e((string) ($performance['label'] ?? 'Poor')) ?>
|
||||
</div>
|
||||
<div class="certificate-stat-grid">
|
||||
<div class="certificate-stat">
|
||||
<strong><?= e(certificate_decimal_value((float) ($certificate['overall_percentage'] ?? 0))) ?>%</strong>
|
||||
<span>متوسط الأداء النهائي</span>
|
||||
</div>
|
||||
<div class="certificate-stat">
|
||||
<strong><?= e(certificate_number_value((int) ($certificate['completed_assessments'] ?? 0))) ?></strong>
|
||||
<span>تقييمات مكتملة</span>
|
||||
</div>
|
||||
<div class="certificate-stat">
|
||||
<strong><?= e(certificate_decimal_value((float) ($certificate['score_total'] ?? 0))) ?>/<?= e(certificate_decimal_value((float) ($certificate['max_score_total'] ?? 0))) ?></strong>
|
||||
<span>إجمالي الدرجات المرصودة</span>
|
||||
</div>
|
||||
<div class="certificate-stat">
|
||||
<strong><?= e(certificate_date_label((string) ($certificate['latest_assessed_on'] ?? ''))) ?></strong>
|
||||
<span>آخر تقييم مسجل</span>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($coverageNote !== ''): ?>
|
||||
<div class="alert alert-light border mt-3 mb-0"><?= e($coverageNote) ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<aside class="certificate-panel">
|
||||
<strong class="mb-2">ملخص الأداء</strong>
|
||||
<div class="summary-stack">
|
||||
<div class="summary-row"><span>التقييمات النشطة</span><strong><?= e(certificate_number_value((int) ($certificate['active_assessments'] ?? 0))) ?></strong></div>
|
||||
<div class="summary-row"><span>المكتملة</span><strong><?= e(certificate_number_value((int) ($certificate['completed_assessments'] ?? 0))) ?></strong></div>
|
||||
<div class="summary-row"><span>الغياب</span><strong><?= e(certificate_number_value((int) ($certificate['absent_assessments'] ?? 0))) ?></strong></div>
|
||||
<div class="summary-row"><span>بعذر</span><strong><?= e(certificate_number_value((int) ($certificate['excused_assessments'] ?? 0))) ?></strong></div>
|
||||
<div class="summary-row"><span>غير المرصود بعد</span><strong><?= e(certificate_number_value((int) ($certificate['missing_assessments'] ?? 0))) ?></strong></div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($certificate['assessments'])): ?>
|
||||
<div class="certificate-panel mt-4">
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
|
||||
<strong class="mb-0">تفصيل التقييمات</strong>
|
||||
<span class="section-subtle">Overall performance is calculated from the recorded assessments in this course.</span>
|
||||
</div>
|
||||
<div class="certificate-table-wrap">
|
||||
<table class="certificate-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>التقييم</th>
|
||||
<th>الفئة</th>
|
||||
<th>الدرجة</th>
|
||||
<th>النسبة</th>
|
||||
<th>الوزن</th>
|
||||
<th>التاريخ</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($certificate['assessments'] as $assessment): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<strong><?= e((string) ($assessment['title'] ?? 'تقييم')) ?></strong>
|
||||
<?php if (!empty($assessment['notes'])): ?><small><?= e((string) $assessment['notes']) ?></small><?php endif; ?>
|
||||
</td>
|
||||
<td><?= e((string) ($assessment['category'] ?? '—')) ?></td>
|
||||
<td><?= e(certificate_decimal_value((float) ($assessment['score'] ?? 0))) ?> / <?= e(certificate_decimal_value((float) ($assessment['max_score'] ?? 0))) ?></td>
|
||||
<td><?= e(certificate_decimal_value((float) ($assessment['percentage'] ?? 0))) ?>%</td>
|
||||
<td><?= e(certificate_decimal_value((float) ($assessment['weight_percentage'] ?? 0))) ?>%</td>
|
||||
<td><?= e(certificate_date_label((string) ($assessment['assessed_on'] ?? ''))) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="certificate-panel certificate-empty-state mt-4">
|
||||
<div class="empty-title mb-2">لا توجد تقييمات مرصودة بعد</div>
|
||||
<p class="text-muted mb-3">أدخل درجات الطالب أولاً من صفحة رصد الدرجات، ثم افتح الشهادة من جديد لإظهار الأداء العام.</p>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($studentsUrl) ?>">العودة إلى الطلاب</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="certificate-signature-row">
|
||||
<div class="certificate-signature">
|
||||
<strong>مدير/ة المركز</strong>
|
||||
<span><?= e((string) ($application['director_name'] ?? '')) ?></span>
|
||||
</div>
|
||||
<div class="certificate-signature">
|
||||
<strong>اعتماد الأداء النهائي</strong>
|
||||
<span><?= e((string) ($performance['label_ar'] ?? 'ضعيف')) ?> — <?= e((string) ($performance['label'] ?? 'Poor')) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php render_page_end();
|
||||
251
student_completion_certificate.php
Normal file
251
student_completion_certificate.php
Normal file
@ -0,0 +1,251 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
function completion_certificate_date_label(string $date): string
|
||||
{
|
||||
if ($date === '' || strtotime($date) === false) {
|
||||
return date('Y/m/d');
|
||||
}
|
||||
|
||||
return date('Y/m/d', strtotime($date));
|
||||
}
|
||||
|
||||
function completion_certificate_decimal_value(float $value): string
|
||||
{
|
||||
return rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.');
|
||||
}
|
||||
|
||||
function completion_certificate_message_text(string $template, array $tokens, string $fallback): string
|
||||
{
|
||||
$message = trim($template);
|
||||
if ($message === '') {
|
||||
$message = $fallback;
|
||||
}
|
||||
|
||||
return strtr($message, $tokens);
|
||||
}
|
||||
|
||||
$flash = consume_flash();
|
||||
$settings = get_app_settings();
|
||||
$applicationId = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT) ?: 0;
|
||||
$requestedCycleId = filter_input(INPUT_GET, 'cycle', FILTER_VALIDATE_INT) ?: 0;
|
||||
$studentId = filter_input(INPUT_GET, 'student_id', FILTER_VALIDATE_INT) ?: 0;
|
||||
$application = $applicationId > 0 ? get_application($applicationId) : null;
|
||||
$isApprovedSchool = $application && (string) $application['status'] === 'approved';
|
||||
$cycleContext = ['cycles' => [], 'selected' => null, 'active' => null, 'read_only' => false];
|
||||
$selectedCycle = null;
|
||||
$selectedCycleId = 0;
|
||||
$cycleLabel = 'الدورة الحالية';
|
||||
|
||||
if ($application && $isApprovedSchool) {
|
||||
$cycleContext = resolve_school_cycle_context((int) $application['id'], $application, $requestedCycleId);
|
||||
$selectedCycle = $cycleContext['selected'];
|
||||
$selectedCycleId = $selectedCycle ? (int) ($selectedCycle['id'] ?? 0) : 0;
|
||||
$cycleLabel = $selectedCycle ? (string) ($selectedCycle['cycle_name'] ?? 'الدورة الحالية') : $cycleLabel;
|
||||
}
|
||||
|
||||
$certificate = $isApprovedSchool && $selectedCycleId > 0 && $studentId > 0
|
||||
? school_student_certificate_summary((int) $application['id'], $selectedCycleId, $studentId)
|
||||
: [
|
||||
'student' => null,
|
||||
'assessments' => [],
|
||||
'has_results' => false,
|
||||
'completed_assessments' => 0,
|
||||
'active_assessments' => 0,
|
||||
'missing_assessments' => 0,
|
||||
'absent_assessments' => 0,
|
||||
'excused_assessments' => 0,
|
||||
'overall_percentage' => 0.0,
|
||||
'score_total' => 0.0,
|
||||
'max_score_total' => 0.0,
|
||||
'latest_assessed_on' => '',
|
||||
'performance' => student_certificate_performance_meta(0.0),
|
||||
];
|
||||
$student = is_array($certificate['student'] ?? null) ? $certificate['student'] : null;
|
||||
|
||||
if (!$application || !$isApprovedSchool || $selectedCycleId <= 0 || $studentId <= 0 || $student === null) {
|
||||
http_response_code(404);
|
||||
}
|
||||
|
||||
$performance = $certificate['performance'] ?? student_certificate_performance_meta(0.0);
|
||||
$hasResults = !empty($certificate['has_results']);
|
||||
$honor = $hasResults
|
||||
? student_completion_certificate_honor_meta((float) ($certificate['overall_percentage'] ?? 0))
|
||||
: [
|
||||
'key' => 'completion',
|
||||
'title' => 'Successful Completion',
|
||||
'title_ar' => 'إتمام ناجح',
|
||||
'completion_line_ar' => 'أتمّ/أتمّت الدورة بنجاح واستفاد/استفادت من محتواها العلمي.',
|
||||
];
|
||||
$template = (string) ($settings['completion_certificate_template'] ?? 'modern');
|
||||
if (!in_array($template, ['modern', 'classic'], true)) {
|
||||
$template = 'modern';
|
||||
}
|
||||
|
||||
$centerName = trim((string) ($application['center_name'] ?? ''));
|
||||
if ($centerName === '') {
|
||||
$centerName = (string) ($settings['app_name'] ?? project_name());
|
||||
}
|
||||
$centerLogo = trim((string) ($application['logo'] ?? ''));
|
||||
if ($centerLogo === '') {
|
||||
$centerLogo = trim((string) ($settings['app_logo'] ?? ''));
|
||||
}
|
||||
$tagline = trim((string) ($settings['completion_certificate_tagline'] ?? ''));
|
||||
if ($tagline === '') {
|
||||
$tagline = 'شهادة إتمام وتكريم';
|
||||
}
|
||||
|
||||
$overallPercentage = (float) ($certificate['overall_percentage'] ?? 0);
|
||||
$percentageLabel = $hasResults ? completion_certificate_decimal_value($overallPercentage) . '%' : '—';
|
||||
$fallbackMessage = sprintf(
|
||||
'تشهد إدارة %s بأن الطالب/الطالبة %s قد أتمّ/أتمّت متطلبات %s %s.',
|
||||
$centerName,
|
||||
(string) ($student['full_name'] ?? 'الطالب/الطالبة'),
|
||||
$cycleLabel,
|
||||
$honor['title_ar']
|
||||
);
|
||||
|
||||
$bodyMessage = completion_certificate_message_text(
|
||||
(string) ($settings['completion_certificate_message'] ?? ''),
|
||||
[
|
||||
'{student}' => (string) ($student['full_name'] ?? ''),
|
||||
'{center}' => $centerName,
|
||||
'{cycle}' => $cycleLabel,
|
||||
'{performance}' => (string) ($performance['label_ar'] ?? 'مميز'),
|
||||
'{honor}' => (string) ($honor['title_ar'] ?? 'بنجاح'),
|
||||
'{percentage}' => $hasResults ? completion_certificate_decimal_value($overallPercentage) : '0',
|
||||
],
|
||||
$fallbackMessage
|
||||
);
|
||||
|
||||
$issueDateLabel = completion_certificate_date_label((string) ($certificate['latest_assessed_on'] ?? ''));
|
||||
$studentsUrl = $application ? school_page_url('students.php', (int) $application['id'], $selectedCycleId) : 'students.php';
|
||||
$detailedCertificateUrl = $application ? school_page_url('student_certificate.php', (int) $application['id'], $selectedCycleId) . '&student_id=' . urlencode((string) $studentId) : 'student_certificate.php';
|
||||
$pageTitle = $student ? 'شهادة إتمام الدورة: ' . (string) $student['full_name'] : 'شهادة إتمام الدورة';
|
||||
$pageDescription = 'شهادة إتمام وتكريم قابلة للطباعة تعرض اسم المركز وشعاره ونتيجة الطالب النهائية بتصميم جميل ومختصر.';
|
||||
|
||||
render_page_start($pageTitle, 'approved', $pageDescription);
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="student-certificate-page completion-certificate-page py-4 py-lg-5">
|
||||
<div class="container-xl">
|
||||
<div class="certificate-toolbar">
|
||||
<div>
|
||||
<div class="section-title">شهادة الإتمام والتكريم</div>
|
||||
<div class="section-subtle">نسخة مختصرة وأنيقة للطباعة، مستقلة عن كشف الدرجات التفصيلي.</div>
|
||||
</div>
|
||||
<div class="certificate-toolbar-actions">
|
||||
<a class="btn btn-outline-secondary" href="<?= e($studentsUrl) ?>">العودة إلى الطلاب</a>
|
||||
<?php if ($student): ?>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($detailedCertificateUrl) ?>" target="_blank" rel="noopener">كشف الأداء</a>
|
||||
<button type="button" class="btn btn-primary" onclick="window.print()">طباعة الشهادة</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</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>
|
||||
<?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 ($selectedCycleId <= 0 || !$student): ?>
|
||||
<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($studentsUrl) ?>">العودة إلى سجل الطلاب</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<article class="completion-certificate-card completion-template-<?= e($template) ?>" aria-labelledby="completion-certificate-title">
|
||||
<div class="completion-certificate-layer completion-certificate-layer-one" aria-hidden="true"></div>
|
||||
<div class="completion-certificate-layer completion-certificate-layer-two" aria-hidden="true"></div>
|
||||
|
||||
<div class="completion-certificate-inner">
|
||||
<header class="completion-certificate-header">
|
||||
<div class="completion-branding">
|
||||
<?php if ($centerLogo !== ''): ?>
|
||||
<img src="<?= e(asset_url($centerLogo)) ?>" alt="شعار <?= e($centerName) ?>" class="completion-brand-logo" width="96" height="96">
|
||||
<?php else: ?>
|
||||
<div class="completion-brand-logo completion-brand-badge" aria-hidden="true"><?= e(mb_substr($centerName, 0, 1)) ?></div>
|
||||
<?php endif; ?>
|
||||
<div>
|
||||
<div class="completion-overline">Certificate of Completion</div>
|
||||
<h1 id="completion-certificate-title" class="completion-certificate-title"><?= e($tagline) ?></h1>
|
||||
<p class="completion-certificate-center mb-0"><?= e($centerName) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="completion-certificate-stamps">
|
||||
<span class="completion-stamp"><?= e($cycleLabel) ?></span>
|
||||
<span class="completion-stamp completion-stamp-accent"><?= e((string) ($honor['title_ar'] ?? 'بنجاح')) ?></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="completion-certificate-content">
|
||||
<p class="completion-intro mb-2">تُمنح هذه الشهادة إلى</p>
|
||||
<div class="completion-student-name"><?= e((string) ($student['full_name'] ?? '')) ?></div>
|
||||
<?php if (!empty($student['student_code'])): ?>
|
||||
<div class="completion-student-code">رقم الطالب: <?= e((string) $student['student_code']) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<p class="completion-message"><?= e($bodyMessage) ?></p>
|
||||
<p class="completion-honor-line mb-0"><?= e((string) ($honor['completion_line_ar'] ?? '')) ?></p>
|
||||
</div>
|
||||
|
||||
<div class="completion-certificate-grid">
|
||||
<section class="completion-summary-card">
|
||||
<span class="completion-summary-label">التقدير النهائي</span>
|
||||
<div class="performance-pill performance-<?= e((string) ($performance['key'] ?? 'good')) ?> completion-performance-pill">
|
||||
<?= e((string) ($performance['label_ar'] ?? 'مميز')) ?>
|
||||
</div>
|
||||
<div class="completion-honor-title"><?= e((string) ($honor['title_ar'] ?? 'بنجاح')) ?></div>
|
||||
<div class="completion-honor-subtitle"><?= e((string) ($honor['title'] ?? 'Successful Completion')) ?></div>
|
||||
</section>
|
||||
|
||||
<section class="completion-meta-grid" aria-label="بيانات الشهادة">
|
||||
<div class="completion-meta-item">
|
||||
<strong>الدورة</strong>
|
||||
<span><?= e($cycleLabel) ?></span>
|
||||
</div>
|
||||
<div class="completion-meta-item">
|
||||
<strong>تاريخ الإصدار</strong>
|
||||
<span><?= e($issueDateLabel) ?></span>
|
||||
</div>
|
||||
<div class="completion-meta-item">
|
||||
<strong>نسبة الأداء</strong>
|
||||
<span><?= e($percentageLabel) ?></span>
|
||||
</div>
|
||||
<div class="completion-meta-item">
|
||||
<strong>التقييمات المكتملة</strong>
|
||||
<span><?= e((string) ((int) ($certificate['completed_assessments'] ?? 0))) ?></span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<?php if (!$hasResults): ?>
|
||||
<div class="completion-note">
|
||||
لا توجد تقييمات مرصودة لهذا الطالب بعد، لذلك تم إصدار شهادة إتمام مختصرة بدون مرتبة أداء رقمية.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<footer class="completion-signature-row">
|
||||
<div class="completion-signature-block">
|
||||
<strong>مدير/ة المركز</strong>
|
||||
<span><?= e((string) ($application['director_name'] ?? $centerName)) ?></span>
|
||||
</div>
|
||||
<div class="completion-signature-block">
|
||||
<strong>اعتماد الشهادة</strong>
|
||||
<span><?= e((string) ($honor['title_ar'] ?? 'إتمام ناجح')) ?></span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</article>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php render_page_end(); ?>
|
||||
299
student_completion_certificates.php
Normal file
299
student_completion_certificates.php
Normal file
@ -0,0 +1,299 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
function completion_certificate_date_label(string $date): string
|
||||
{
|
||||
if ($date === '' || strtotime($date) === false) {
|
||||
return date('Y/m/d');
|
||||
}
|
||||
|
||||
return date('Y/m/d', strtotime($date));
|
||||
}
|
||||
|
||||
function completion_certificate_decimal_value(float $value): string
|
||||
{
|
||||
return rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.');
|
||||
}
|
||||
|
||||
function completion_certificate_message_text(string $template, array $tokens, string $fallback): string
|
||||
{
|
||||
$message = trim($template);
|
||||
if ($message === '') {
|
||||
$message = $fallback;
|
||||
}
|
||||
|
||||
return strtr($message, $tokens);
|
||||
}
|
||||
|
||||
function completion_certificate_view_model(array $application, array $settings, int $cycleId, string $cycleLabel, int $studentId): ?array
|
||||
{
|
||||
$certificate = school_student_certificate_summary((int) $application['id'], $cycleId, $studentId);
|
||||
$student = is_array($certificate['student'] ?? null) ? $certificate['student'] : null;
|
||||
if ($student === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$performance = $certificate['performance'] ?? student_certificate_performance_meta(0.0);
|
||||
$hasResults = !empty($certificate['has_results']);
|
||||
$honor = $hasResults
|
||||
? student_completion_certificate_honor_meta((float) ($certificate['overall_percentage'] ?? 0))
|
||||
: [
|
||||
'key' => 'completion',
|
||||
'title' => 'Successful Completion',
|
||||
'title_ar' => 'إتمام ناجح',
|
||||
'completion_line_ar' => 'أتمّ/أتمّت الدورة بنجاح واستفاد/استفادت من محتواها العلمي.',
|
||||
];
|
||||
|
||||
$template = (string) ($settings['completion_certificate_template'] ?? 'modern');
|
||||
if (!in_array($template, ['modern', 'classic'], true)) {
|
||||
$template = 'modern';
|
||||
}
|
||||
|
||||
$centerName = trim((string) ($application['center_name'] ?? ''));
|
||||
if ($centerName === '') {
|
||||
$centerName = (string) ($settings['app_name'] ?? project_name());
|
||||
}
|
||||
|
||||
$centerLogo = trim((string) ($application['logo'] ?? ''));
|
||||
if ($centerLogo === '') {
|
||||
$centerLogo = trim((string) ($settings['app_logo'] ?? ''));
|
||||
}
|
||||
|
||||
$tagline = trim((string) ($settings['completion_certificate_tagline'] ?? ''));
|
||||
if ($tagline === '') {
|
||||
$tagline = 'شهادة إتمام وتكريم';
|
||||
}
|
||||
|
||||
$overallPercentage = (float) ($certificate['overall_percentage'] ?? 0);
|
||||
$percentageLabel = $hasResults ? completion_certificate_decimal_value($overallPercentage) . '%' : '—';
|
||||
$fallbackMessage = sprintf(
|
||||
'تشهد إدارة %s بأن الطالب/الطالبة %s قد أتمّ/أتمّت متطلبات %s %s.',
|
||||
$centerName,
|
||||
(string) ($student['full_name'] ?? 'الطالب/الطالبة'),
|
||||
$cycleLabel,
|
||||
$honor['title_ar']
|
||||
);
|
||||
|
||||
$bodyMessage = completion_certificate_message_text(
|
||||
(string) ($settings['completion_certificate_message'] ?? ''),
|
||||
[
|
||||
'{student}' => (string) ($student['full_name'] ?? ''),
|
||||
'{center}' => $centerName,
|
||||
'{cycle}' => $cycleLabel,
|
||||
'{performance}' => (string) ($performance['label_ar'] ?? 'مميز'),
|
||||
'{honor}' => (string) ($honor['title_ar'] ?? 'بنجاح'),
|
||||
'{percentage}' => $hasResults ? completion_certificate_decimal_value($overallPercentage) : '0',
|
||||
],
|
||||
$fallbackMessage
|
||||
);
|
||||
|
||||
return [
|
||||
'certificate' => $certificate,
|
||||
'student' => $student,
|
||||
'performance' => $performance,
|
||||
'has_results' => $hasResults,
|
||||
'honor' => $honor,
|
||||
'template' => $template,
|
||||
'center_name' => $centerName,
|
||||
'center_logo' => $centerLogo,
|
||||
'tagline' => $tagline,
|
||||
'percentage_label' => $percentageLabel,
|
||||
'body_message' => $bodyMessage,
|
||||
'issue_date_label' => completion_certificate_date_label((string) ($certificate['latest_assessed_on'] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
$flash = consume_flash();
|
||||
$settings = get_app_settings();
|
||||
$applicationId = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT) ?: 0;
|
||||
$requestedCycleId = filter_input(INPUT_GET, 'cycle', FILTER_VALIDATE_INT) ?: 0;
|
||||
$autoprint = filter_input(INPUT_GET, 'autoprint', FILTER_VALIDATE_INT) ?: 0;
|
||||
$search = clean_text($_GET['search'] ?? '', 255);
|
||||
$filters = [
|
||||
'gender' => clean_text($_GET['gender'] ?? '', 50),
|
||||
'grade_level' => clean_text($_GET['grade_level'] ?? '', 50),
|
||||
'enrollment_status' => clean_text($_GET['enrollment_status'] ?? '', 50),
|
||||
];
|
||||
|
||||
$application = $applicationId > 0 ? get_application($applicationId) : null;
|
||||
$isApprovedSchool = $application && (string) $application['status'] === 'approved';
|
||||
$cycleContext = ['cycles' => [], 'selected' => null, 'active' => null, 'read_only' => false];
|
||||
$selectedCycle = null;
|
||||
$selectedCycleId = 0;
|
||||
$cycleLabel = 'الدورة الحالية';
|
||||
$certificateItems = [];
|
||||
|
||||
if ($application && $isApprovedSchool) {
|
||||
$cycleContext = resolve_school_cycle_context((int) $application['id'], $application, $requestedCycleId);
|
||||
$selectedCycle = $cycleContext['selected'];
|
||||
$selectedCycleId = $selectedCycle ? (int) ($selectedCycle['id'] ?? 0) : 0;
|
||||
$cycleLabel = $selectedCycle ? (string) ($selectedCycle['cycle_name'] ?? 'الدورة الحالية') : $cycleLabel;
|
||||
|
||||
if ($selectedCycleId > 0) {
|
||||
$students = list_school_students_by_cycle((int) $application['id'], $selectedCycleId, $search, 0, 0, $filters);
|
||||
foreach ($students as $studentRow) {
|
||||
$viewModel = completion_certificate_view_model($application, $settings, $selectedCycleId, $cycleLabel, (int) ($studentRow['id'] ?? 0));
|
||||
if ($viewModel !== null) {
|
||||
$certificateItems[] = $viewModel;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$application || !$isApprovedSchool || $selectedCycleId <= 0) {
|
||||
http_response_code(404);
|
||||
}
|
||||
|
||||
$studentsUrl = $application ? school_page_url('students.php', (int) $application['id'], $selectedCycleId) : 'students.php';
|
||||
$pageTitle = $application ? 'تنزيل شهادات الإتمام: ' . (string) ($application['center_name'] ?? '') : 'تنزيل شهادات الإتمام';
|
||||
$pageDescription = 'صفحة مجمعة لطباعة أو حفظ جميع شهادات الإتمام والتكريم كملف PDF واحد للمركز والدورة المختارة.';
|
||||
|
||||
render_page_start($pageTitle, 'approved', $pageDescription);
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="student-certificate-page completion-certificate-page completion-batch-page py-4 py-lg-5">
|
||||
<div class="container-xl">
|
||||
<div class="certificate-toolbar">
|
||||
<div>
|
||||
<div class="section-title">جميع شهادات الإتمام والتكريم</div>
|
||||
<div class="section-subtle"><?= e((string) count($certificateItems)) ?> شهادة<?= $search !== '' ? ' — نتائج البحث الحالية' : '' ?>. سيتم فتح نافذة الطباعة لتتمكن من الحفظ كملف PDF واحد.</div>
|
||||
</div>
|
||||
<div class="certificate-toolbar-actions">
|
||||
<a class="btn btn-outline-secondary" href="<?= e($studentsUrl) ?>">العودة إلى الطلاب</a>
|
||||
<?php if ($certificateItems !== []): ?>
|
||||
<button type="button" class="btn btn-primary" onclick="window.print()">تنزيل PDF / طباعة</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</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>
|
||||
<?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 ($selectedCycleId <= 0): ?>
|
||||
<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($studentsUrl) ?>">العودة إلى سجل الطلاب</a>
|
||||
</div>
|
||||
<?php elseif ($certificateItems === []): ?>
|
||||
<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($studentsUrl) ?>">العودة إلى سجل الطلاب</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="batch-certificate-stack">
|
||||
<?php foreach ($certificateItems as $item): ?>
|
||||
<?php
|
||||
$student = $item['student'];
|
||||
$certificate = $item['certificate'];
|
||||
$performance = $item['performance'];
|
||||
$honor = $item['honor'];
|
||||
?>
|
||||
<article class="completion-certificate-card completion-batch-item completion-template-<?= e((string) $item['template']) ?>" aria-labelledby="completion-certificate-title-<?= e((string) ($student['id'] ?? 0)) ?>">
|
||||
<div class="completion-certificate-layer completion-certificate-layer-one" aria-hidden="true"></div>
|
||||
<div class="completion-certificate-layer completion-certificate-layer-two" aria-hidden="true"></div>
|
||||
|
||||
<div class="completion-certificate-inner">
|
||||
<header class="completion-certificate-header">
|
||||
<div class="completion-branding">
|
||||
<?php if ((string) $item['center_logo'] !== ''): ?>
|
||||
<img src="<?= e(asset_url((string) $item['center_logo'])) ?>" alt="شعار <?= e((string) $item['center_name']) ?>" class="completion-brand-logo" width="96" height="96">
|
||||
<?php else: ?>
|
||||
<div class="completion-brand-logo completion-brand-badge" aria-hidden="true"><?= e(mb_substr((string) $item['center_name'], 0, 1)) ?></div>
|
||||
<?php endif; ?>
|
||||
<div>
|
||||
<div class="completion-overline">Certificate of Completion</div>
|
||||
<h1 id="completion-certificate-title-<?= e((string) ($student['id'] ?? 0)) ?>" class="completion-certificate-title"><?= e((string) $item['tagline']) ?></h1>
|
||||
<p class="completion-certificate-center mb-0"><?= e((string) $item['center_name']) ?></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="completion-certificate-stamps">
|
||||
<span class="completion-stamp"><?= e($cycleLabel) ?></span>
|
||||
<span class="completion-stamp completion-stamp-accent"><?= e((string) ($honor['title_ar'] ?? 'بنجاح')) ?></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="completion-certificate-content">
|
||||
<p class="completion-intro mb-2">تُمنح هذه الشهادة إلى</p>
|
||||
<div class="completion-student-name"><?= e((string) ($student['full_name'] ?? '')) ?></div>
|
||||
<?php if (!empty($student['student_code'])): ?>
|
||||
<div class="completion-student-code">رقم الطالب: <?= e((string) $student['student_code']) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<p class="completion-message"><?= e((string) $item['body_message']) ?></p>
|
||||
<p class="completion-honor-line mb-0"><?= e((string) ($honor['completion_line_ar'] ?? '')) ?></p>
|
||||
</div>
|
||||
|
||||
<div class="completion-certificate-grid">
|
||||
<section class="completion-summary-card">
|
||||
<span class="completion-summary-label">التقدير النهائي</span>
|
||||
<div class="performance-pill performance-<?= e((string) ($performance['key'] ?? 'good')) ?> completion-performance-pill">
|
||||
<?= e((string) ($performance['label_ar'] ?? 'مميز')) ?>
|
||||
</div>
|
||||
<div class="completion-honor-title"><?= e((string) ($honor['title_ar'] ?? 'بنجاح')) ?></div>
|
||||
<div class="completion-honor-subtitle"><?= e((string) ($honor['title'] ?? 'Successful Completion')) ?></div>
|
||||
</section>
|
||||
|
||||
<section class="completion-meta-grid" aria-label="بيانات الشهادة">
|
||||
<div class="completion-meta-item">
|
||||
<strong>الدورة</strong>
|
||||
<span><?= e($cycleLabel) ?></span>
|
||||
</div>
|
||||
<div class="completion-meta-item">
|
||||
<strong>تاريخ الإصدار</strong>
|
||||
<span><?= e((string) $item['issue_date_label']) ?></span>
|
||||
</div>
|
||||
<div class="completion-meta-item">
|
||||
<strong>نسبة الأداء</strong>
|
||||
<span><?= e((string) $item['percentage_label']) ?></span>
|
||||
</div>
|
||||
<div class="completion-meta-item">
|
||||
<strong>التقييمات المكتملة</strong>
|
||||
<span><?= e((string) ((int) ($certificate['completed_assessments'] ?? 0))) ?></span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<?php if (empty($item['has_results'])): ?>
|
||||
<div class="completion-note">
|
||||
لا توجد تقييمات مرصودة لهذا الطالب بعد، لذلك تم إصدار شهادة إتمام مختصرة بدون مرتبة أداء رقمية.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<footer class="completion-signature-row">
|
||||
<div class="completion-signature-block">
|
||||
<strong>مدير/ة المركز</strong>
|
||||
<span><?= e((string) ($application['director_name'] ?? $item['center_name'])) ?></span>
|
||||
</div>
|
||||
<div class="completion-signature-block">
|
||||
<strong>اعتماد الشهادة</strong>
|
||||
<span><?= e((string) ($honor['title_ar'] ?? 'إتمام ناجح')) ?></span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</article>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php if ($autoprint === 1 && $certificateItems !== []): ?>
|
||||
<script>
|
||||
window.addEventListener('load', function () {
|
||||
window.setTimeout(function () {
|
||||
window.print();
|
||||
}, 250);
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
<?php render_page_end(); ?>
|
||||
521
students.php
Normal file
521
students.php
Normal file
@ -0,0 +1,521 @@
|
||||
<?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;
|
||||
$application = $applicationId > 0 ? get_application($applicationId) : null;
|
||||
$isApprovedSchool = $application && (string) $application['status'] === 'approved';
|
||||
$values = student_defaults();
|
||||
$errors = [];
|
||||
$cycleContext = ['cycles' => [], 'selected' => null, 'active' => null, 'read_only' => false];
|
||||
$selectedCycle = null;
|
||||
$selectedCycleId = 0;
|
||||
$isCycleReadOnly = false;
|
||||
$cycleLabel = 'لا توجد دورة بعد';
|
||||
$nextStudentCode = '';
|
||||
|
||||
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;
|
||||
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;
|
||||
$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'] = 'لا يمكن فتح تسجيل الطلاب قبل اعتماد المركز.';
|
||||
} elseif ($selectedCycleId <= 0) {
|
||||
$errors['form'] = 'يرجى إنشاء دورة موسمية أولاً من صفحة المركز.';
|
||||
} elseif ($isCycleReadOnly) {
|
||||
$errors['form'] = 'هذه الدورة مؤرشفة للقراءة فقط. افتح دورة جديدة أو اختر دورة نشطة لإضافة/تعديل طلاب.';
|
||||
}
|
||||
|
||||
if ($errors === []) {
|
||||
try {
|
||||
if ($action === 'edit' && $studentId > 0) {
|
||||
update_student_in_cycle((int) $application['id'], $selectedCycleId, $studentId, $values);
|
||||
set_flash('success', 'تم تحديث بيانات الطالب بنجاح.');
|
||||
} else {
|
||||
create_student_in_cycle((int) $application['id'], $selectedCycleId, $values);
|
||||
set_flash('success', 'تم تسجيل الطالب بنجاح داخل الدورة الموسمية المحددة.');
|
||||
}
|
||||
header('Location: ' . school_page_url('students.php', (int) $application['id'], $selectedCycleId));
|
||||
exit;
|
||||
} catch (PDOException $exception) {
|
||||
$duplicateCode = isset($exception->errorInfo[1]) && (int) $exception->errorInfo[1] === 1062;
|
||||
if ($duplicateCode) {
|
||||
$errors['student_code'] = 'هذا الكود مستخدم مسبقاً داخل نفس الدورة الموسمية.';
|
||||
$errors['form'] = 'تعذر الحفظ لوجود تعارض في الكود.';
|
||||
} else {
|
||||
$errors['form'] = 'تعذر حفظ بيانات الطالب حالياً. يرجى المحاولة مرة أخرى.';
|
||||
}
|
||||
} catch (Throwable $exception) {
|
||||
$errors['form'] = 'تعذر حفظ بيانات الطالب حالياً. يرجى المحاولة مرة أخرى.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$search = clean_text($_GET['search'] ?? '', 255);
|
||||
$filters = [
|
||||
'gender' => clean_text($_GET['gender'] ?? '', 50),
|
||||
'grade_level' => clean_text($_GET['grade_level'] ?? '', 50),
|
||||
'enrollment_status' => clean_text($_GET['enrollment_status'] ?? '', 50),
|
||||
];
|
||||
|
||||
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT) ?: 1;
|
||||
$limit = 15;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$students = $isApprovedSchool && $selectedCycleId > 0 ? list_school_students_by_cycle((int) $application['id'], $selectedCycleId, $search, $limit, $offset, $filters) : [];
|
||||
$totalStudents = $isApprovedSchool && $selectedCycleId > 0 ? count_school_students_by_cycle((int) $application['id'], $selectedCycleId, $search, $filters) : 0;
|
||||
|
||||
$metrics = $isApprovedSchool && $selectedCycleId > 0 ? school_student_metrics_by_cycle((int) $application['id'], $selectedCycleId) : [
|
||||
'total' => 0,
|
||||
'boys' => 0,
|
||||
'girls' => 0,
|
||||
'active' => 0,
|
||||
'waiting' => 0,
|
||||
'withdrawn' => 0,
|
||||
];
|
||||
|
||||
$expectedCapacity = $application ? (int) ($application['expected_students'] ?? 0) : 0;
|
||||
$remainingSeats = max(0, $expectedCapacity - $metrics['total']);
|
||||
$bulkCertificateParams = ['autoprint' => '1'];
|
||||
if ($search !== '') {
|
||||
$bulkCertificateParams['search'] = $search;
|
||||
}
|
||||
foreach ($filters as $filterKey => $filterValue) {
|
||||
if ($filterValue !== '') {
|
||||
$bulkCertificateParams[$filterKey] = $filterValue;
|
||||
}
|
||||
}
|
||||
$bulkCompletionCertificatesUrl = $application && $selectedCycleId > 0
|
||||
? school_page_url('student_completion_certificates.php', (int) $application['id'], $selectedCycleId) . '&' . http_build_query($bulkCertificateParams)
|
||||
: '#';
|
||||
$pageTitle = $application ? 'تسجيل الطلاب: ' . (string) $application['center_name'] . ($selectedCycle ? ' — ' . $cycleLabel : '') : 'تسجيل الطلاب';
|
||||
$pageDescription = 'صفحة مستقلة لتسجيل الطلاب وإدارة كشف المدرسة بعد الاعتماد، مع ربط كل البيانات بالدورة الموسمية النشطة أو المؤرشفة.';
|
||||
$approvedSchoolUrl = $application ? school_page_url('approved_school.php', (int) $application['id'], $selectedCycleId) : 'approved_school.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';
|
||||
$applicationDetailUrl = $application ? 'application_detail.php?id=' . urlencode((string) $application['id']) : 'application_detail.php';
|
||||
|
||||
if (!$application) {
|
||||
http_response_code(404);
|
||||
}
|
||||
|
||||
render_page_start($pageTitle, 'approved', $pageDescription, (string) ($application['favicon'] ?? ''));
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="py-4 py-lg-5">
|
||||
<div class="container-xxl">
|
||||
<div class="admin-layout row g-4 align-items-start">
|
||||
<div class="col-lg-3 layout-sidebar-column">
|
||||
<?php if ($application) { require __DIR__ . '/includes/center_sidebar.php'; } else { require __DIR__ . '/includes/sidebar.php'; } ?>
|
||||
</div>
|
||||
<div class="col-lg-9 layout-content-column">
|
||||
|
||||
<?php if (!$application): ?>
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">المدرسة غير موجودة</div>
|
||||
<p class="text-muted mb-3">تحقق من رابط المدرسة أو ارجع إلى قائمة المراكز المعتمدة.</p>
|
||||
<a class="btn btn-primary" href="applications.php?status=approved">المراكز المعتمدة</a>
|
||||
</div>
|
||||
<?php elseif (!$isApprovedSchool): ?>
|
||||
<div class="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">هذه الصفحة جاهزة، لكن فتح سجل الطلاب مرتبط بتحويل حالة المركز إلى <strong>معتمد</strong> أولاً حتى يبقى التسلسل الإداري منظمًا.</p>
|
||||
<div class="hero-meta">
|
||||
<span>الحالة الحالية: <?= e(status_meta((string) $application['status'])['label']) ?></span>
|
||||
<span>المدينة: <?= e((string) $application['city']) ?></span>
|
||||
</div>
|
||||
<div class="cta-stack mt-4">
|
||||
<a class="btn btn-primary" href="<?= e($applicationDetailUrl) ?>">العودة لملف الاعتماد</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($approvedSchoolUrl) ?>">صفحة المركز</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">هذا هو أول موديول تشغيلي بعد اعتماد المدرسة. يتم عرض الطلاب مع إمكانية التصفية، بالإضافة إلى إدارة البيانات عن طريق نموذج مدمج.</p>
|
||||
<div class="hero-meta">
|
||||
<span><?= e((string) $application['city']) ?></span>
|
||||
<span>السعة المعتمدة <?= e((string) $expectedCapacity) ?> طالب</span>
|
||||
<span>المقاعد المتبقية <?= e((string) $remainingSeats) ?></span>
|
||||
</div>
|
||||
<div class="cta-stack mt-4">
|
||||
<a class="btn btn-outline-secondary" href="<?= e($approvedSchoolUrl) ?>">صفحة المركز</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($teachersUrl) ?>">المعلمين</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="launch-metrics">
|
||||
<div class="launch-metric"><strong><?= e((string) $metrics['total']) ?></strong><span>إجمالي الطلاب</span></div>
|
||||
<div class="launch-metric"><strong><?= e((string) $metrics['active']) ?></strong><span>طلاب مؤكدون</span></div>
|
||||
<div class="launch-metric"><strong><?= e((string) $metrics['waiting']) ?></strong><span>قائمة الانتظار</span></div>
|
||||
</div>
|
||||
</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>
|
||||
</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>
|
||||
<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;
|
||||
?>
|
||||
<a class="quick-link-item <?= $isCurrentCycleLink ? 'is-current' : '' ?>" href="<?= e(school_page_url('students.php', (int) $application['id'], (int) $cycle['id'])) ?>">
|
||||
<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-12">
|
||||
|
||||
<?php if (!empty($errors['form'])): ?>
|
||||
<div class="alert alert-danger mb-4"><?= e($errors['form']) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="app-card mb-4">
|
||||
<div class="section-head mb-3">
|
||||
<div>
|
||||
<div class="section-title">كشف المدرسة</div>
|
||||
<div class="section-copy">الطلاب المسجلون حالياً في هذه المدرسة فقط.</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
<span class="header-chip"><?= e((string) $metrics['boys']) ?> طلاب / <?= e((string) $metrics['girls']) ?> طالبات</span>
|
||||
<?php if ($selectedCycleId > 0 && $totalStudents > 0): ?>
|
||||
<a class="btn btn-outline-primary btn-sm" href="<?= e($bulkCompletionCertificatesUrl) ?>" target="_blank" rel="noopener">تنزيل كل الشهادات PDF</a>
|
||||
<?php endif; ?>
|
||||
<?php if (!$isCycleReadOnly): ?>
|
||||
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#studentModal" onclick="resetStudentModal()">
|
||||
+ إضافة طالب
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="get" class="mb-4 bg-light p-3 rounded">
|
||||
<input type="hidden" name="id" value="<?= e((string) $application['id']) ?>">
|
||||
<input type="hidden" name="cycle" value="<?= e((string) $selectedCycleId) ?>">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-3">
|
||||
<input type="text" name="search" class="form-control form-control-sm" placeholder="ابحث باسم الطالب، الكود..." value="<?= e($search) ?>">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select name="gender" class="form-select form-select-sm">
|
||||
<option value="">كل الأنواع</option>
|
||||
<?php foreach (student_gender_options() as $option): ?>
|
||||
<option value="<?= e($option) ?>" <?= $filters['gender'] === $option ? 'selected' : '' ?>><?= e($option) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select name="grade_level" class="form-select form-select-sm">
|
||||
<option value="">كل الصفوف</option>
|
||||
<?php foreach (student_grade_options() as $option): ?>
|
||||
<option value="<?= e($option) ?>" <?= $filters['grade_level'] === $option ? 'selected' : '' ?>><?= e($option) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select name="enrollment_status" class="form-select form-select-sm">
|
||||
<option value="">كل الحالات</option>
|
||||
<?php foreach (student_enrollment_status_map() as $key => $meta): ?>
|
||||
<option value="<?= e($key) ?>" <?= $filters['enrollment_status'] === $key ? 'selected' : '' ?>><?= e($meta['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-outline-secondary btn-sm w-100">تصفية</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?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: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table app-table align-middle table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>الكود</th>
|
||||
<th>الطالب</th>
|
||||
<th>الصف</th>
|
||||
<th>ولي الأمر</th>
|
||||
<th>الهاتف</th>
|
||||
<th>الحالة</th>
|
||||
<th>إجراءات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($students as $student): ?>
|
||||
<tr>
|
||||
<td><strong><?= e((string) $student['student_code']) ?></strong></td>
|
||||
<td>
|
||||
<strong><?= e((string) $student['full_name']) ?></strong>
|
||||
<div class="text-muted small"><?= e((string) $student['gender']) ?></div>
|
||||
</td>
|
||||
<td><?= e((string) $student['grade_level']) ?></td>
|
||||
<td>
|
||||
<strong><?= e((string) $student['guardian_name']) ?></strong>
|
||||
<?php if (!empty($student['notes'])): ?><div class="text-muted small"><?= e((string) $student['notes']) ?></div><?php endif; ?>
|
||||
</td>
|
||||
<td><a href="tel:<?= e((string) $student['guardian_phone']) ?>" class="text-decoration-none text-primary"><?= e((string) $student['guardian_phone']) ?></a></td>
|
||||
<td><?= student_enrollment_status_badge((string) $student['enrollment_status']) ?></td>
|
||||
<?php $certificateUrl = school_page_url('student_certificate.php', (int) $application['id'], $selectedCycleId) . '&student_id=' . urlencode((string) ((int) $student['id'])); ?>
|
||||
<?php $completionCertificateUrl = school_page_url('student_completion_certificate.php', (int) $application['id'], $selectedCycleId) . '&student_id=' . urlencode((string) ((int) $student['id'])); ?>
|
||||
<td class="table-action-cell">
|
||||
<div class="table-icon-actions">
|
||||
<a
|
||||
href="<?= e($completionCertificateUrl) ?>"
|
||||
class="btn btn-sm btn-primary icon-action"
|
||||
title="شهادة الإتمام والتكريم"
|
||||
aria-label="شهادة الإتمام والتكريم"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true"><path d="M.173 5.184A1.75 1.75 0 0 1 1.87 4h12.26a1.75 1.75 0 0 1 1.697 2.184l-1.2 4.8A1.75 1.75 0 0 1 12.93 12H9.707l-1.354 2.03a.5.5 0 0 1-.706.123L6.293 12H3.07a1.75 1.75 0 0 1-1.697-1.016l-1.2-4.8ZM4.75 1.5a.75.75 0 0 1 1.5 0V3h-1.5V1.5Zm5 0a.75.75 0 0 1 1.5 0V3h-1.5V1.5ZM7 7.25a1 1 0 1 0 2 0 1 1 0 0 0-2 0Zm-2.5.5a.75.75 0 0 1 .75-.75h5.5a.75.75 0 0 1 0 1.5h-5.5a.75.75 0 0 1-.75-.75Z"/></svg>
|
||||
<span class="visually-hidden">شهادة الإتمام والتكريم</span>
|
||||
</a>
|
||||
<a
|
||||
href="<?= e($certificateUrl) ?>"
|
||||
class="btn btn-sm btn-outline-secondary icon-action"
|
||||
title="كشف الأداء التفصيلي"
|
||||
aria-label="كشف الأداء التفصيلي"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true"><path d="M9.669.864 8 0 6.331.864 4.5.5l-.864 1.669L2 4l.864 1.831L2.5 7.5l1.669.864L5 10l1.831-.864L8.5 10 11 7.5l-.364-1.669L12.5 4l-1.864-.831L9.669.864ZM8 1.153l1.214.629 1.332-.264.629 1.214 1.214.629-.264 1.332.629 1.214-1.214.629-.629 1.214-1.332-.264L8 8.847l-1.214.629-1.332.264-.629-1.214-1.214-.629.264-1.332-.629-1.214 1.214-.629.629-1.214 1.332.264L8 1.153Z"/><path d="M10 9.5v5.293l-2-1.2-2 1.2V9.5l.835-.418.754.15.411.205.411-.206.754-.15L10 9.5Z"/></svg>
|
||||
<span class="visually-hidden">كشف الأداء التفصيلي</span>
|
||||
</a>
|
||||
<?php if (!$isCycleReadOnly): ?>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-secondary icon-action"
|
||||
data-bs-toggle="modal" data-bs-target="#studentModal"
|
||||
data-student='<?= htmlspecialchars(json_encode($student, JSON_UNESCAPED_UNICODE), ENT_QUOTES, 'UTF-8') ?>'
|
||||
onclick="editStudentModal(this)"
|
||||
title="تعديل بيانات الطالب"
|
||||
aria-label="تعديل بيانات الطالب"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true"><path d="M12.854.146a.5.5 0 0 1 .707 0l2.586 2.586a.5.5 0 0 1 0 .707l-9.793 9.793a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168L12.854.146ZM11.207 2 3 10.207V13h2.793L14 4.793 11.207 2Z"/></svg>
|
||||
<span class="visually-hidden">تعديل بيانات الطالب</span>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php render_pagination($totalStudents, $limit, $page, $_GET); ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Student Modal -->
|
||||
<div class="modal fade" id="studentModal" tabindex="-1" aria-labelledby="studentModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<form method="post" id="studentForm" novalidate>
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="studentModalLabel">إضافة طالب جديد</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="إغلاق"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="action" id="formAction" value="add">
|
||||
<input type="hidden" name="student_id" id="formStudentId" value="">
|
||||
|
||||
<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'] !== '' ? $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">
|
||||
<label class="form-label" for="full_name">اسم الطالب / الطالبة</label>
|
||||
<input class="form-control <?= isset($errors['full_name']) ? 'is-invalid' : '' ?>" id="full_name" name="full_name" value="<?= e($values['full_name']) ?>" placeholder="الاسم الثلاثي" required>
|
||||
<?php if (isset($errors['full_name'])): ?><div class="invalid-feedback"><?= e($errors['full_name']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="gender">النوع</label>
|
||||
<select class="form-select <?= isset($errors['gender']) ? 'is-invalid' : '' ?>" id="gender" name="gender" required>
|
||||
<option value="">اختر</option>
|
||||
<?php foreach (student_gender_options() as $option): ?>
|
||||
<option value="<?= e($option) ?>" <?= $values['gender'] === $option ? 'selected' : '' ?>><?= e($option) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php if (isset($errors['gender'])): ?><div class="invalid-feedback"><?= e($errors['gender']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="grade_level">الصف الدراسي</label>
|
||||
<select class="form-select <?= isset($errors['grade_level']) ? 'is-invalid' : '' ?>" id="grade_level" name="grade_level" required>
|
||||
<option value="">اختر</option>
|
||||
<?php foreach (student_grade_options() as $option): ?>
|
||||
<option value="<?= e($option) ?>" <?= $values['grade_level'] === $option ? 'selected' : '' ?>><?= e($option) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php if (isset($errors['grade_level'])): ?><div class="invalid-feedback"><?= e($errors['grade_level']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="guardian_name">اسم ولي الأمر</label>
|
||||
<input class="form-control <?= isset($errors['guardian_name']) ? 'is-invalid' : '' ?>" id="guardian_name" name="guardian_name" value="<?= e($values['guardian_name']) ?>" placeholder="الاسم الكامل" required>
|
||||
<?php if (isset($errors['guardian_name'])): ?><div class="invalid-feedback"><?= e($errors['guardian_name']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="guardian_phone">هاتف ولي الأمر</label>
|
||||
<input class="form-control <?= isset($errors['guardian_phone']) ? 'is-invalid' : '' ?>" id="guardian_phone" name="guardian_phone" value="<?= e($values['guardian_phone']) ?>" placeholder="0500000000" required>
|
||||
<?php if (isset($errors['guardian_phone'])): ?><div class="invalid-feedback"><?= e($errors['guardian_phone']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="birth_date">تاريخ الميلاد</label>
|
||||
<input type="date" class="form-control <?= isset($errors['birth_date']) ? 'is-invalid' : '' ?>" id="birth_date" name="birth_date" value="<?= e($values['birth_date']) ?>">
|
||||
<?php if (isset($errors['birth_date'])): ?><div class="invalid-feedback"><?= e($errors['birth_date']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="enrollment_status">حالة التسجيل</label>
|
||||
<select class="form-select <?= isset($errors['enrollment_status']) ? 'is-invalid' : '' ?>" id="enrollment_status" name="enrollment_status" required>
|
||||
<?php foreach (student_enrollment_status_map() as $key => $meta): ?>
|
||||
<option value="<?= e($key) ?>" <?= $values['enrollment_status'] === $key ? 'selected' : '' ?>><?= e($meta['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php if (isset($errors['enrollment_status'])): ?><div class="invalid-feedback"><?= e($errors['enrollment_status']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="notes">ملاحظات</label>
|
||||
<textarea class="form-control" id="notes" name="notes" rows="2" placeholder="مثال: احتياج تعليمي، ملاحظة صحية، أو حالة انتظار."><?= e($values['notes']) ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer d-flex justify-content-between">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" class="btn btn-primary">حفظ التغييرات</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const defaultStudentCode = <?= json_encode($nextStudentCode !== '' ? $nextStudentCode : ($values['student_code'] ?? '')) ?>;
|
||||
|
||||
function resetStudentModal() {
|
||||
document.getElementById('studentModalLabel').innerText = 'إضافة طالب جديد';
|
||||
document.getElementById('formAction').value = 'add';
|
||||
document.getElementById('formStudentId').value = '';
|
||||
|
||||
// Clear inputs except defaults
|
||||
const form = document.getElementById('studentForm');
|
||||
form.reset();
|
||||
document.getElementById('student_code').value = defaultStudentCode || '';
|
||||
document.getElementById('enrollment_status').value = 'active';
|
||||
}
|
||||
|
||||
function editStudentModal(btn) {
|
||||
const student = JSON.parse(btn.getAttribute('data-student'));
|
||||
document.getElementById('studentModalLabel').innerText = 'تعديل طالب: ' + student.full_name;
|
||||
document.getElementById('formAction').value = 'edit';
|
||||
document.getElementById('formStudentId').value = student.id;
|
||||
|
||||
document.getElementById('student_code').value = student.student_code || '';
|
||||
document.getElementById('full_name').value = student.full_name || '';
|
||||
document.getElementById('gender').value = student.gender || '';
|
||||
document.getElementById('grade_level').value = student.grade_level || '';
|
||||
document.getElementById('guardian_name').value = student.guardian_name || '';
|
||||
document.getElementById('guardian_phone').value = student.guardian_phone || '';
|
||||
document.getElementById('birth_date').value = student.birth_date || '';
|
||||
document.getElementById('enrollment_status').value = student.enrollment_status || '';
|
||||
document.getElementById('notes').value = student.notes || '';
|
||||
}
|
||||
|
||||
// Show modal if there are errors (from POST)
|
||||
<?php if (!empty($errors) && $_SERVER['REQUEST_METHOD'] === 'POST'): ?>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var myModal = new bootstrap.Modal(document.getElementById('studentModal'));
|
||||
myModal.show();
|
||||
<?php if (isset($_POST['action']) && $_POST['action'] === 'edit'): ?>
|
||||
document.getElementById('studentModalLabel').innerText = 'تعديل طالب';
|
||||
document.getElementById('formAction').value = 'edit';
|
||||
document.getElementById('formStudentId').value = '<?= e($_POST['student_id'] ?? '') ?>';
|
||||
<?php endif; ?>
|
||||
});
|
||||
<?php endif; ?>
|
||||
</script>
|
||||
|
||||
<?php render_page_end(); ?>
|
||||
273
subjects.php
Normal file
273
subjects.php
Normal file
@ -0,0 +1,273 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require_once __DIR__ . '/includes/app.php';
|
||||
|
||||
if (!is_super_admin()) {
|
||||
http_response_code(403);
|
||||
render_page_start('صلاحيات غير كافية', '');
|
||||
?>
|
||||
<section class="py-5 text-center">
|
||||
<div class="container-xxl">
|
||||
<h1 class="mb-3">عذراً</h1>
|
||||
<p>هذه الصفحة مخصصة للمشرف العام فقط.</p>
|
||||
<a href="index.php" class="btn btn-primary mt-3">العودة للرئيسية</a>
|
||||
</div>
|
||||
</section>
|
||||
<?php
|
||||
render_page_end();
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Handle POST actions
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$action = $_POST['action'] ?? '';
|
||||
|
||||
if ($action === 'create') {
|
||||
$name = clean_text($_POST['name'] ?? '', 255);
|
||||
$description = clean_text($_POST['description'] ?? '', 1000);
|
||||
$status = in_array($_POST['status'] ?? '', ['enabled', 'disabled']) ? $_POST['status'] : 'enabled';
|
||||
|
||||
if ($name !== '') {
|
||||
$stmt = db()->prepare('INSERT INTO subjects (name, description, status) VALUES (?, ?, ?)');
|
||||
$stmt->execute([$name, $description, $status]);
|
||||
set_flash('success', 'تمت إضافة المادة بنجاح.');
|
||||
} else {
|
||||
set_flash('error', 'اسم المادة مطلوب.');
|
||||
}
|
||||
header('Location: subjects.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'edit') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$name = clean_text($_POST['name'] ?? '', 255);
|
||||
$description = clean_text($_POST['description'] ?? '', 1000);
|
||||
$status = in_array($_POST['status'] ?? '', ['enabled', 'disabled']) ? $_POST['status'] : 'enabled';
|
||||
|
||||
if ($id > 0 && $name !== '') {
|
||||
$stmt = db()->prepare('UPDATE subjects SET name = ?, description = ?, status = ? WHERE id = ?');
|
||||
$stmt->execute([$name, $description, $status, $id]);
|
||||
set_flash('success', 'تم تحديث المادة بنجاح.');
|
||||
} else {
|
||||
set_flash('error', 'تأكد من إدخال اسم المادة.');
|
||||
}
|
||||
header('Location: subjects.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'delete') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
if ($id > 0) {
|
||||
$stmt = db()->prepare('DELETE FROM subjects WHERE id = ?');
|
||||
$stmt->execute([$id]);
|
||||
set_flash('success', 'تم حذف المادة بنجاح.');
|
||||
}
|
||||
header('Location: subjects.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Read list
|
||||
$search = clean_text($_GET['search'] ?? '', 255);
|
||||
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT) ?: 1;
|
||||
$limit = 10;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$query = 'SELECT * FROM subjects';
|
||||
$countQuery = 'SELECT COUNT(*) FROM subjects';
|
||||
$params = [];
|
||||
if ($search !== '') {
|
||||
$where = ' WHERE name LIKE ? OR description LIKE ?';
|
||||
$query .= $where;
|
||||
$countQuery .= $where;
|
||||
$params[] = "%$search%";
|
||||
$params[] = "%$search%";
|
||||
}
|
||||
|
||||
$stmtCount = db()->prepare($countQuery);
|
||||
$stmtCount->execute($params);
|
||||
$totalItems = (int)$stmtCount->fetchColumn();
|
||||
|
||||
$query .= ' ORDER BY id DESC LIMIT ' . $limit . ' OFFSET ' . $offset;
|
||||
$stmt = db()->prepare($query);
|
||||
$stmt->execute($params);
|
||||
$subjects = $stmt->fetchAll();
|
||||
|
||||
$flash = consume_flash();
|
||||
render_page_start('المواد الدراسية', 'subjects', 'إدارة المواد الدراسية الخاصة بالمراكز');
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="py-4 py-lg-5">
|
||||
<div class="container-xxl">
|
||||
<div class="admin-layout row g-4 align-items-start">
|
||||
<div class="col-lg-3 layout-sidebar-column">
|
||||
<?php require __DIR__ . '/includes/sidebar.php'; ?>
|
||||
</div>
|
||||
<div class="col-lg-9 layout-content-column">
|
||||
<div class="app-card mb-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-3">
|
||||
<div class="section-title mb-0">إدارة المواد الدراسية</div>
|
||||
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createModal">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="me-1" viewBox="0 0 16 16">
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
|
||||
</svg>
|
||||
إضافة مادة
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Search Bar -->
|
||||
<?php render_search_bar($search, "ابحث باسم المادة أو الوصف...", "subjects.php", $_GET); ?>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table app-table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>الاسم</th>
|
||||
<th>الوصف</th>
|
||||
<th>الحالة</th>
|
||||
<th>الإجراءات</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (count($subjects) === 0): ?>
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-4 text-muted">لا توجد مواد مسجلة أو لم يتم العثور على نتائج.</td>
|
||||
</tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($subjects as $subject): ?>
|
||||
<tr>
|
||||
<td><?= e((string)$subject['id']) ?></td>
|
||||
<td class="fw-semibold"><?= e($subject['name']) ?></td>
|
||||
<td class="text-muted" style="max-width: 250px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="<?= e($subject['description']) ?>"><?= e($subject['description'] ?: '—') ?></td>
|
||||
<td>
|
||||
<?php if ($subject['status'] === 'enabled'): ?>
|
||||
<span class="badge bg-success-subtle text-success">مفعل</span>
|
||||
<?php else: ?>
|
||||
<span class="badge bg-secondary-subtle text-secondary">معطل</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex gap-2">
|
||||
<!-- Edit btn -->
|
||||
<button class="btn btn-sm btn-outline-secondary" title="تعديل"
|
||||
data-bs-toggle="modal" data-bs-target="#editModal"
|
||||
data-id="<?= e((string)$subject['id']) ?>"
|
||||
data-name="<?= e($subject['name']) ?>"
|
||||
data-description="<?= e($subject['description']) ?>"
|
||||
data-status="<?= e($subject['status']) ?>"
|
||||
onclick="fillEditModal(this)">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Delete btn -->
|
||||
<form method="POST" action="subjects.php" onsubmit="return confirm('هل أنت متأكد من حذف هذه المادة؟');">
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="hidden" name="id" value="<?= e((string)$subject['id']) ?>">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="حذف">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
||||
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php render_pagination($totalItems, $limit, $page, $_GET); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Create Modal -->
|
||||
<div class="modal fade" id="createModal" tabindex="-1" aria-labelledby="createModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="POST" action="subjects.php">
|
||||
<input type="hidden" name="action" value="create">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="createModalLabel">إضافة مادة دراسية</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">اسم المادة</label>
|
||||
<input type="text" name="name" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">الوصف</label>
|
||||
<textarea name="description" class="form-control" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">الحالة</label>
|
||||
<select name="status" class="form-select">
|
||||
<option value="enabled">مفعل</option>
|
||||
<option value="disabled">معطل</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" class="btn btn-primary">إضافة</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Modal -->
|
||||
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="POST" action="subjects.php">
|
||||
<input type="hidden" name="action" value="edit">
|
||||
<input type="hidden" name="id" id="edit_id">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editModalLabel">تعديل مادة دراسية</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">اسم المادة</label>
|
||||
<input type="text" name="name" id="edit_name" class="form-control" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">الوصف</label>
|
||||
<textarea name="description" id="edit_description" class="form-control" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">الحالة</label>
|
||||
<select name="status" id="edit_status" class="form-select">
|
||||
<option value="enabled">مفعل</option>
|
||||
<option value="disabled">معطل</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" class="btn btn-primary">حفظ التعديلات</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function fillEditModal(btn) {
|
||||
document.getElementById('edit_id').value = btn.getAttribute('data-id');
|
||||
document.getElementById('edit_name').value = btn.getAttribute('data-name');
|
||||
document.getElementById('edit_description').value = btn.getAttribute('data-description');
|
||||
document.getElementById('edit_status').value = btn.getAttribute('data-status');
|
||||
}
|
||||
</script>
|
||||
|
||||
<?php render_page_end(); ?>
|
||||
503
teachers.php
Normal file
503
teachers.php
Normal file
@ -0,0 +1,503 @@
|
||||
<?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;
|
||||
$application = $applicationId > 0 ? get_application($applicationId) : null;
|
||||
$isApprovedSchool = $application && (string) $application['status'] === 'approved';
|
||||
$values = teacher_defaults();
|
||||
$errors = [];
|
||||
$cycleContext = ['cycles' => [], 'selected' => null, 'active' => null, 'read_only' => false];
|
||||
$selectedCycle = null;
|
||||
$selectedCycleId = 0;
|
||||
$isCycleReadOnly = false;
|
||||
$cycleLabel = 'لا توجد دورة بعد';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
$allEnabledSubjectMap = get_enabled_subject_map();
|
||||
$applicationSubjectIds = $application ? normalize_id_list($application['subjects'] ?? []) : [];
|
||||
$teacherSubjectMap = $allEnabledSubjectMap;
|
||||
if ($applicationSubjectIds !== [] && $allEnabledSubjectMap !== []) {
|
||||
$applicationSubjectLookup = array_fill_keys($applicationSubjectIds, true);
|
||||
$teacherSubjectMap = array_filter(
|
||||
$allEnabledSubjectMap,
|
||||
static fn (string $_label, int|string $subjectId): bool => isset($applicationSubjectLookup[(int) $subjectId]),
|
||||
ARRAY_FILTER_USE_BOTH
|
||||
);
|
||||
}
|
||||
if ($teacherSubjectMap === []) {
|
||||
$teacherSubjectMap = $allEnabledSubjectMap;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $application) {
|
||||
$action = $_POST['action'] ?? 'add';
|
||||
$teacherId = filter_input(INPUT_POST, 'teacher_id', FILTER_VALIDATE_INT) ?: 0;
|
||||
[$values, $errors] = validate_teacher_input($_POST, array_keys($teacherSubjectMap));
|
||||
|
||||
if (!$isApprovedSchool) {
|
||||
$errors['form'] = 'لا يمكن فتح صفحة المعلمين قبل اعتماد المركز.';
|
||||
} elseif ($selectedCycleId <= 0) {
|
||||
$errors['form'] = 'يرجى إنشاء دورة موسمية أولاً من صفحة المركز.';
|
||||
} elseif ($isCycleReadOnly) {
|
||||
$errors['form'] = 'هذه الدورة مؤرشفة للقراءة فقط. افتح دورة جديدة أو اختر دورة نشطة لإضافة/تعديل أعضاء.';
|
||||
}
|
||||
|
||||
if ($errors === []) {
|
||||
try {
|
||||
if ($action === 'edit' && $teacherId > 0) {
|
||||
update_teacher_in_cycle((int) $application['id'], $selectedCycleId, $teacherId, $values);
|
||||
set_flash('success', 'تم تحديث بيانات عضو الفريق بنجاح.');
|
||||
} else {
|
||||
create_teacher_in_cycle((int) $application['id'], $selectedCycleId, $values);
|
||||
set_flash('success', 'تمت إضافة عضو الفريق داخل الدورة الموسمية المحددة بنجاح.');
|
||||
}
|
||||
header('Location: ' . school_page_url('teachers.php', (int) $application['id'], $selectedCycleId));
|
||||
exit;
|
||||
} catch (Throwable $exception) {
|
||||
$errors['form'] = 'تعذر حفظ بيانات الفريق حالياً. يرجى المحاولة مرة أخرى.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$search = clean_text($_GET['search'] ?? '', 255);
|
||||
$filters = [
|
||||
'role_title' => clean_text($_GET['role_title'] ?? '', 50),
|
||||
'employment_status' => clean_text($_GET['employment_status'] ?? '', 50),
|
||||
'search' => $search,
|
||||
];
|
||||
|
||||
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT) ?: 1;
|
||||
$limit = 15;
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$teachers = $isApprovedSchool && $selectedCycleId > 0 ? list_school_teachers_by_cycle((int) $application['id'], $selectedCycleId, $filters, $limit, $offset) : [];
|
||||
foreach ($teachers as &$teacher) {
|
||||
$teacher['subject_ids'] = normalize_id_list($teacher['subject_ids'] ?? []);
|
||||
$teacher['subject_labels'] = teacher_subject_labels($teacher, $allEnabledSubjectMap);
|
||||
}
|
||||
unset($teacher);
|
||||
$totalTeachers = $isApprovedSchool && $selectedCycleId > 0 ? count_school_teachers_by_cycle((int) $application['id'], $selectedCycleId, $filters) : 0;
|
||||
|
||||
$metrics = $isApprovedSchool && $selectedCycleId > 0 ? school_teacher_metrics_by_cycle((int) $application['id'], $selectedCycleId) : [
|
||||
'total' => 0,
|
||||
'active' => 0,
|
||||
'pending' => 0,
|
||||
'inactive' => 0,
|
||||
'teachers' => 0,
|
||||
'supervisors' => 0,
|
||||
'email_ready' => 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,
|
||||
];
|
||||
$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';
|
||||
$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';
|
||||
$applicationDetailUrl = $application ? 'application_detail.php?id=' . urlencode((string) $application['id']) : 'application_detail.php';
|
||||
|
||||
if (!$application) {
|
||||
http_response_code(404);
|
||||
}
|
||||
|
||||
render_page_start($pageTitle, 'approved', $pageDescription, (string) ($application['favicon'] ?? ''));
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="py-4 py-lg-5">
|
||||
<div class="container-xxl">
|
||||
<div class="admin-layout row g-4 align-items-start">
|
||||
<div class="col-lg-3 layout-sidebar-column">
|
||||
<?php if ($application) { require __DIR__ . '/includes/center_sidebar.php'; } else { require __DIR__ . '/includes/sidebar.php'; } ?>
|
||||
</div>
|
||||
<div class="col-lg-9 layout-content-column">
|
||||
|
||||
<?php if (!$application): ?>
|
||||
<div class="app-card text-center py-5">
|
||||
<div class="empty-title mb-2">المدرسة غير موجودة</div>
|
||||
<p class="text-muted mb-3">تحقق من رابط المدرسة أو ارجع إلى قائمة المراكز المعتمدة.</p>
|
||||
<a class="btn btn-primary" href="applications.php?status=approved">المراكز المعتمدة</a>
|
||||
</div>
|
||||
<?php elseif (!$isApprovedSchool): ?>
|
||||
<div class="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">تم تجهيز صفحة المعلمين، لكن فتح سجل الفريق مرتبط بتحويل حالة المركز إلى <strong>معتمد</strong> أولاً حتى يبدأ التشغيل وفق الترتيب الإداري الصحيح.</p>
|
||||
<div class="hero-meta">
|
||||
<span>الحالة الحالية: <?= e(status_meta((string) $application['status'])['label']) ?></span>
|
||||
<span>المدينة: <?= e((string) $application['city']) ?></span>
|
||||
</div>
|
||||
<div class="cta-stack mt-4">
|
||||
<a class="btn btn-primary" href="<?= e($applicationDetailUrl) ?>">العودة لملف الاعتماد</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($approvedSchoolUrl) ?>">صفحة المركز</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">بعد فتح سجل الطلاب، هذه هي الصفحة المنطقية التالية لبناء الكادر. يمكن هنا إضافة المعلمين والمشرفين والكوادر المساندة مع عزل واضح بين النموذج وكشف الفريق.</p>
|
||||
<div class="hero-meta">
|
||||
<span><?= e((string) $application['city']) ?></span>
|
||||
<span><?= e((string) $metrics['active']) ?> أعضاء مفعلون</span>
|
||||
<span><?= e((string) $metrics['supervisors']) ?> أدوار إشرافية</span>
|
||||
</div>
|
||||
<div class="cta-stack mt-4">
|
||||
<a class="btn btn-outline-secondary" href="<?= e($approvedSchoolUrl) ?>">العودة لصفحة المركز</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($studentsUrl) ?>">تسجيل الطلاب</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) $metrics['total']) ?> عضو</strong></div>
|
||||
<div class="summary-row"><span>جاهزون للتواصل</span><strong><?= e((string) $metrics['email_ready']) ?> ببريد موثق</strong></div>
|
||||
<div class="summary-row"><span>الطلاب النشطون</span><strong><?= e((string) $studentMetrics['active']) ?> طالب/طالبة</strong></div>
|
||||
</div>
|
||||
<p class="section-subtle mb-0">وجود سجل واضح للمعلمين يسهّل لاحقاً ربط الحصص والتقييمات بكل مدرسة.</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>
|
||||
</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>
|
||||
<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;
|
||||
?>
|
||||
<a class="quick-link-item <?= $isCurrentCycleLink ? 'is-current' : '' ?>" href="<?= e(school_page_url('teachers.php', (int) $application['id'], (int) $cycle['id'])) ?>">
|
||||
<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-12">
|
||||
|
||||
<?php if (!empty($errors['form'])): ?>
|
||||
<div class="alert alert-danger mb-4"><?= e($errors['form']) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="app-card mb-4">
|
||||
<div class="section-head mb-3">
|
||||
<div>
|
||||
<div class="section-title">كشف الفريق التعليمي</div>
|
||||
<div class="section-copy">جميع المعلمين والمشرفين المرتبطين بهذا المركز فقط.</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<span class="header-chip"><?= e((string) $metrics['email_ready']) ?> بريد جاهز / <?= e((string) $metrics['pending']) ?> بانتظار التفعيل</span>
|
||||
<?php if (!$isCycleReadOnly): ?>
|
||||
<button type="button" class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#teacherModal" onclick="resetTeacherModal()">
|
||||
+ إضافة عضو للفريق
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="get" class="mb-4 bg-light p-3 rounded">
|
||||
<input type="hidden" name="id" value="<?= e((string) $application['id']) ?>">
|
||||
<input type="hidden" name="cycle" value="<?= e((string) $selectedCycleId) ?>">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-4">
|
||||
<input type="text" name="search" class="form-control form-control-sm" placeholder="ابحث بالاسم، البريد، الهاتف، التخصص..." value="<?= e($search) ?>">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select name="role_title" class="form-select form-select-sm">
|
||||
<option value="">كل الأدوار</option>
|
||||
<?php foreach (teacher_role_options() as $option): ?>
|
||||
<option value="<?= e($option) ?>" <?= $filters['role_title'] === $option ? 'selected' : '' ?>><?= e($option) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select name="employment_status" class="form-select form-select-sm">
|
||||
<option value="">كل الحالات</option>
|
||||
<?php foreach (teacher_employment_status_map() as $key => $meta): ?>
|
||||
<option value="<?= e($key) ?>" <?= $filters['employment_status'] === $key ? 'selected' : '' ?>><?= e($meta['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-outline-secondary btn-sm w-100">تصفية</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<?php if ($teachers === []): ?>
|
||||
<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: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table app-table align-middle table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>الاسم</th>
|
||||
<th>الدور</th>
|
||||
<th>التخصص</th>
|
||||
<th>المواد</th>
|
||||
<th>التواصل</th>
|
||||
<th>الحالة</th>
|
||||
<?php if (!$isCycleReadOnly): ?><th>إجراءات</th><?php endif; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($teachers as $teacher): ?>
|
||||
<tr>
|
||||
<td>
|
||||
<strong><?= e((string) $teacher['full_name']) ?></strong>
|
||||
<?php if (!empty($teacher['notes'])): ?><div class="text-muted small"><?= e((string) $teacher['notes']) ?></div><?php endif; ?>
|
||||
</td>
|
||||
<td><?= e((string) $teacher['role_title']) ?></td>
|
||||
<td><?= e((string) ($teacher['specialization'] ?: '—')) ?></td>
|
||||
<td>
|
||||
<?php if (!empty($teacher['subject_labels'])): ?>
|
||||
<div class="teacher-subject-badges">
|
||||
<?php foreach ($teacher['subject_labels'] as $subjectLabel): ?>
|
||||
<span class="teacher-subject-badge"><?= e((string) $subjectLabel) ?></span>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<span class="text-muted">—</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<strong><?= e((string) ($teacher['phone'] ?: 'بدون هاتف')) ?></strong>
|
||||
<div class="text-muted small"><?= e((string) ($teacher['email'] ?: 'بدون بريد')) ?></div>
|
||||
</td>
|
||||
<td><?= teacher_employment_status_badge((string) $teacher['employment_status']) ?></td>
|
||||
<?php if (!$isCycleReadOnly): ?>
|
||||
<td class="table-action-cell">
|
||||
<div class="table-icon-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-outline-secondary icon-action"
|
||||
data-bs-toggle="modal" data-bs-target="#teacherModal"
|
||||
data-teacher='<?= htmlspecialchars(json_encode($teacher, JSON_UNESCAPED_UNICODE), ENT_QUOTES, 'UTF-8') ?>'
|
||||
onclick="editTeacherModal(this)"
|
||||
title="تعديل بيانات المعلم"
|
||||
aria-label="تعديل بيانات المعلم"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true"><path d="M12.854.146a.5.5 0 0 1 .707 0l2.586 2.586a.5.5 0 0 1 0 .707l-9.793 9.793a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168L12.854.146ZM11.207 2 3 10.207V13h2.793L14 4.793 11.207 2Z"/></svg>
|
||||
<span class="visually-hidden">تعديل بيانات المعلم</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php render_pagination($totalTeachers, $limit, $page, $_GET); ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Teacher Modal -->
|
||||
<div class="modal fade" id="teacherModal" tabindex="-1" aria-labelledby="teacherModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<form method="post" id="teacherForm" novalidate>
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="teacherModalLabel">إضافة عضو جديد للفريق</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="إغلاق"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="action" id="formAction" value="add">
|
||||
<input type="hidden" name="teacher_id" id="formTeacherId" value="">
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="full_name">الاسم الكامل</label>
|
||||
<input class="form-control <?= isset($errors['full_name']) ? 'is-invalid' : '' ?>" id="full_name" name="full_name" value="<?= e($values['full_name']) ?>" required>
|
||||
<?php if (isset($errors['full_name'])): ?><div class="invalid-feedback"><?= e($errors['full_name']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="role_title">الدور الوظيفي</label>
|
||||
<select class="form-select <?= isset($errors['role_title']) ? 'is-invalid' : '' ?>" id="role_title" name="role_title" required>
|
||||
<option value="">اختر الدور</option>
|
||||
<?php foreach (teacher_role_options() as $role): ?>
|
||||
<option value="<?= e($role) ?>" <?= $values['role_title'] === $role ? 'selected' : '' ?>><?= e($role) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php if (isset($errors['role_title'])): ?><div class="invalid-feedback"><?= e($errors['role_title']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="specialization">التخصص / المسار</label>
|
||||
<input class="form-control" id="specialization" name="specialization" value="<?= e($values['specialization']) ?>" placeholder="مثال: الرياضيات، القرآن، الأنشطة">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="employment_status">الحالة</label>
|
||||
<select class="form-select <?= isset($errors['employment_status']) ? 'is-invalid' : '' ?>" id="employment_status" name="employment_status">
|
||||
<?php foreach (teacher_employment_status_map() as $status => $meta): ?>
|
||||
<option value="<?= e($status) ?>" <?= $values['employment_status'] === $status ? 'selected' : '' ?>><?= e($meta['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php if (isset($errors['employment_status'])): ?><div class="invalid-feedback"><?= e($errors['employment_status']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label d-block">المواد المسندة</label>
|
||||
<?php if ($teacherSubjectMap === []): ?>
|
||||
<div class="form-text text-muted">لا توجد مواد مفعّلة حالياً، لذلك لا يمكن ربط المعلم بمادة بعد.</div>
|
||||
<?php else: ?>
|
||||
<div class="teacher-subject-picker <?= isset($errors['subject_ids']) ? 'is-invalid' : '' ?>">
|
||||
<?php foreach ($teacherSubjectMap as $subjectId => $subjectName): ?>
|
||||
<label class="teacher-subject-option" for="teacher_subject_<?= e((string) $subjectId) ?>">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
name="subject_ids[]"
|
||||
value="<?= e((string) $subjectId) ?>"
|
||||
id="teacher_subject_<?= e((string) $subjectId) ?>"
|
||||
<?= in_array($subjectId, $values['subject_ids'], true) ? 'checked' : '' ?>
|
||||
>
|
||||
<span><?= e((string) $subjectName) ?></span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php if (isset($errors['subject_ids'])): ?><div class="invalid-feedback d-block"><?= e($errors['subject_ids']) ?></div><?php endif; ?>
|
||||
<div class="form-text">يمكنك ربط المعلم بأكثر من مادة داخل هذه الدورة.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="phone">الهاتف</label>
|
||||
<input class="form-control <?= isset($errors['phone']) ? 'is-invalid' : '' ?>" id="phone" name="phone" value="<?= e($values['phone']) ?>" dir="ltr" inputmode="tel">
|
||||
<?php if (isset($errors['phone'])): ?><div class="invalid-feedback"><?= e($errors['phone']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="email">البريد الإلكتروني</label>
|
||||
<input class="form-control <?= isset($errors['email']) ? 'is-invalid' : '' ?>" id="email" name="email" type="email" value="<?= e($values['email']) ?>" dir="ltr">
|
||||
<?php if (isset($errors['email'])): ?><div class="invalid-feedback"><?= e($errors['email']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="notes">ملاحظات</label>
|
||||
<textarea class="form-control" id="notes" name="notes" rows="2" placeholder="مثال: مسؤول عن مجموعة الصفوف العليا."><?= e($values['notes']) ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer d-flex justify-content-between">
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">إلغاء</button>
|
||||
<button type="submit" class="btn btn-primary">حفظ التغييرات</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function setTeacherSubjectSelection(subjectIds) {
|
||||
const selected = new Set((subjectIds || []).map(String));
|
||||
document.querySelectorAll('input[name="subject_ids[]"]').forEach((checkbox) => {
|
||||
checkbox.checked = selected.has(checkbox.value);
|
||||
});
|
||||
}
|
||||
|
||||
function resetTeacherModal() {
|
||||
document.getElementById('teacherModalLabel').innerText = 'إضافة عضو جديد للفريق';
|
||||
document.getElementById('formAction').value = 'add';
|
||||
document.getElementById('formTeacherId').value = '';
|
||||
|
||||
const form = document.getElementById('teacherForm');
|
||||
form.reset();
|
||||
document.getElementById('employment_status').value = 'active';
|
||||
setTeacherSubjectSelection([]);
|
||||
}
|
||||
|
||||
function editTeacherModal(btn) {
|
||||
const teacher = JSON.parse(btn.getAttribute('data-teacher'));
|
||||
document.getElementById('teacherModalLabel').innerText = 'تعديل عضو: ' + teacher.full_name;
|
||||
document.getElementById('formAction').value = 'edit';
|
||||
document.getElementById('formTeacherId').value = teacher.id;
|
||||
|
||||
document.getElementById('full_name').value = teacher.full_name || '';
|
||||
document.getElementById('role_title').value = teacher.role_title || '';
|
||||
document.getElementById('specialization').value = teacher.specialization || '';
|
||||
document.getElementById('employment_status').value = teacher.employment_status || '';
|
||||
document.getElementById('phone').value = teacher.phone || '';
|
||||
document.getElementById('email').value = teacher.email || '';
|
||||
document.getElementById('notes').value = teacher.notes || '';
|
||||
setTeacherSubjectSelection(teacher.subject_ids || []);
|
||||
}
|
||||
|
||||
// Show modal if there are errors (from POST)
|
||||
<?php if (!empty($errors) && $_SERVER['REQUEST_METHOD'] === 'POST'): ?>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var myModal = new bootstrap.Modal(document.getElementById('teacherModal'));
|
||||
myModal.show();
|
||||
<?php if (isset($_POST['action']) && $_POST['action'] === 'edit'): ?>
|
||||
document.getElementById('teacherModalLabel').innerText = 'تعديل عضو الفريق';
|
||||
document.getElementById('formAction').value = 'edit';
|
||||
document.getElementById('formTeacherId').value = '<?= e($_POST['teacher_id'] ?? '') ?>';
|
||||
<?php endif; ?>
|
||||
});
|
||||
<?php endif; ?>
|
||||
</script>
|
||||
|
||||
<?php render_page_end(); ?>
|
||||
64
update.py
Normal file
64
update.py
Normal file
@ -0,0 +1,64 @@
|
||||
import json
|
||||
|
||||
with open("includes/app.php", "r") as f:
|
||||
c = f.read()
|
||||
|
||||
c = c.replace(
|
||||
"'notes' => '',",
|
||||
"'notes' => '',\n 'subjects' => [],"
|
||||
)
|
||||
|
||||
old_loop = """ foreach ($data as $key => $_value) {
|
||||
$data[$key] = clean_text((string) ($input[$key] ?? ''), $key === 'notes' ? 1000 : 190);
|
||||
}"""
|
||||
new_loop = """ foreach ($data as $key => $_value) {
|
||||
if ($key === 'subjects') {
|
||||
$data[$key] = is_array($input[$key] ?? []) ? $input[$key] : [];
|
||||
continue;
|
||||
}
|
||||
$data[$key] = clean_text((string) ($input[$key] ?? ''), $key === 'notes' ? 1000 : 190);
|
||||
}"""
|
||||
c = c.replace(old_loop, new_loop)
|
||||
|
||||
old_val = """ if ($startDate !== '' && $endDate !== '' && strtotime($endDate) < strtotime($startDate)) {
|
||||
$errors['end_date'] = 'يجب أن يكون تاريخ النهاية بعد البداية.';
|
||||
}"""
|
||||
new_val = old_val + "\n\n if (empty($data['subjects'])) {
|
||||
$errors['subjects'] = 'يرجى اختيار مادة واحدة على الأقل.';
|
||||
} else {
|
||||
$data['subjects'] = array_map('intval', $data['subjects']);
|
||||
}"
|
||||
c = c.replace(old_val, new_val)
|
||||
|
||||
old_insert_sql = " 'INSERT INTO center_applications (
|
||||
center_name, city, center_type, gender_scope, director_name, phone, email,
|
||||
expected_students, start_date, end_date, notes, status, submitted_at, updated_at
|
||||
) VALUES (
|
||||
:center_name, :city, :center_type, :gender_scope, :director_name, :phone, :email,
|
||||
:expected_students, :start_date, :end_date, :notes, :status, NOW(), NOW()
|
||||
)""
|
||||
new_insert_sql = " 'INSERT INTO center_applications (
|
||||
center_name, city, center_type, gender_scope, director_name, phone, email,
|
||||
expected_students, start_date, end_date, notes, subjects, status, submitted_at, updated_at
|
||||
) VALUES (
|
||||
:center_name, :city, :center_type, :gender_scope, :director_name, :phone, :email,
|
||||
:expected_students, :start_date, :end_date, :notes, :subjects, :status, NOW(), NOW()
|
||||
)""
|
||||
c = c.replace(old_insert_sql, new_insert_sql)
|
||||
|
||||
old_execute = """ ':notes' => $data['notes'],
|
||||
':status' => 'submitted',
|
||||
]);"""
|
||||
new_execute = """ ':notes' => $data['notes'],
|
||||
':subjects' => json_encode($data['subjects']),
|
||||
':status' => 'submitted',
|
||||
]);"""
|
||||
c = c.replace(old_execute, new_execute)
|
||||
|
||||
if "function get_enabled_subjects" not in c:
|
||||
c += "\nfunction get_enabled_subjects(): array\n{\n $pdo = db_connection();\n $stmt = $pdo->query(\"SELECT id, name, description FROM subjects WHERE status = 'enabled' ORDER BY name ASC\");\n $result = $stmt->fetchAll(PDO::FETCH_ASSOC);\n return $result !== false ? $result : [];\n}\n"
|
||||
|
||||
with open("includes/app.php", "w") as f:
|
||||
f.write(c)
|
||||
|
||||
print("Patch applied.")
|
||||
Loading…
x
Reference in New Issue
Block a user