Compare commits

...

19 Commits

Author SHA1 Message Date
Flatlogic Bot
2f04e1c8d5 Autosave: 20260421-074336 2026-04-21 07:43:36 +00:00
Flatlogic Bot
faff5cf4a0 Autosave: 20260417-142208 2026-04-17 14:22:06 +00:00
Flatlogic Bot
7937c54a84 Autosave: 20260417-073012 2026-04-17 07:30:11 +00:00
Flatlogic Bot
b80640c59c adding centers assessment 2026-04-17 06:59:52 +00:00
Flatlogic Bot
7fc46facca Autosave: 20260417-035000 2026-04-17 03:49:59 +00:00
Flatlogic Bot
11de03c65b update students reward 2026-04-17 03:38:41 +00:00
Flatlogic Bot
5849af849c Autosave: 20260417-025303 2026-04-17 02:53:02 +00:00
Flatlogic Bot
a83ac160ed add students assessments 2026-04-17 02:12:13 +00:00
Flatlogic Bot
6d8ede20a0 Autosave: 20260416-175011 2026-04-16 17:50:08 +00:00
Flatlogic Bot
61e8c6788b update center page 2026-04-16 17:02:27 +00:00
Flatlogic Bot
a55b4a5394 Autosave: 20260416-163907 2026-04-16 16:39:04 +00:00
Flatlogic Bot
371b07bffa update center page 2026-04-16 14:19:43 +00:00
Flatlogic Bot
e5366bdd58 Autosave: 20260416-131924 2026-04-16 13:19:21 +00:00
Flatlogic Bot
d70f2ad2ca Autosave: 20260416-093953 2026-04-16 09:39:51 +00:00
Flatlogic Bot
b7b7995909 update admin page 2026-04-16 09:11:38 +00:00
Flatlogic Bot
f43e1b9751 updating cycle system 2026-04-16 08:13:52 +00:00
Flatlogic Bot
c9ac266bd6 Autosave: 20260416-065856 2026-04-16 06:58:53 +00:00
Flatlogic Bot
c7366417c4 splitting pages 2026-04-16 06:26:16 +00:00
Flatlogic Bot
18026059c0 first update 2026-04-16 06:19:26 +00:00
79 changed files with 18965 additions and 534 deletions

15
add_logo_to_hero.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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']) ?> &rarr; <?= 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

View File

@ -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
View 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
View 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(); ?>

View 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();

View 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>

View 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();

View 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();

View 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
View 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
View 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
View 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
View 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">عرض الكل &larr;</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(); ?>

View File

@ -0,0 +1 @@
ALTER TABLE app_settings ADD COLUMN app_slogan VARCHAR(255);

View 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;

View File

@ -0,0 +1 @@
ALTER TABLE center_applications ADD COLUMN subjects JSON NULL AFTER notes;

View 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;

View 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');

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,2 @@
ALTER TABLE school_teachers
ADD COLUMN subject_ids TEXT NULL AFTER specialization;

View 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');

View 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;

View 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;

View 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;

View 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;

View 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
View 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
View 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
View 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
View 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)

View 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(); ?>

View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

View 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

File diff suppressed because it is too large Load Diff

25
includes/pexels.php Normal file
View 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
View 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
View File

@ -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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)

View 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
View 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
View 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
View 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
View 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
View 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();

View 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(); ?>

View 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
View 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
View 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
View 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
View 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.")