diff --git a/admin.php b/admin.php index feab5a5..7ebd78b 100644 --- a/admin.php +++ b/admin.php @@ -16,11 +16,11 @@ render_flash($flash);
-
-
+
+
-
+
diff --git a/app_settings.php b/app_settings.php index 20ebb9e..7d2d5ed 100644 --- a/app_settings.php +++ b/app_settings.php @@ -11,6 +11,9 @@ $values = [ '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') { @@ -18,8 +21,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $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'] ?? ''; @@ -56,14 +68,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { if (empty($errors)) { try { - $stmt = db()->prepare('UPDATE app_settings SET app_name = ?, app_slogan = ?, app_email = ?, app_telephone = ?, app_logo = ?, app_favicon = ?, updated_at = NOW() WHERE id = 1'); + $stmt = 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 + $faviconPath, + $values['completion_certificate_template'], + $values['completion_certificate_tagline'], + $values['completion_certificate_message'] ]); set_flash('success', 'تم تحديث الإعدادات العامة بنجاح.'); header('Location: app_settings.php'); @@ -79,11 +94,11 @@ render_flash($flash); ?>
-
-
+
+
-
+

الإعدادات العامة للنظام

تعديل اسم النظام، الشعار (Logo)، الأيقونة (Favicon)، وبيانات التواصل.

@@ -132,6 +147,26 @@ render_flash($flash);
+ +

+
+ + +
يغيّر شكل شهادة الإتمام والتكريم فقط، بدون التأثير على كشف الدرجات.
+
+
+ + +
مثال: شهادة إتمام وتكريم أو شهادة تفوق.
+
+
+ + +
يمكنك استخدام المتغيرات: {student} {center} {cycle} {performance} {honor} {percentage}
+
@@ -144,4 +179,4 @@ render_flash($flash);
- \ No newline at end of file + diff --git a/application_detail.php b/application_detail.php index 9a03571..e610821 100644 --- a/application_detail.php +++ b/application_detail.php @@ -76,11 +76,11 @@ render_flash($flash); ?>
-
-
+
+
-
+
diff --git a/applications.php b/applications.php index 780214a..1af258e 100644 --- a/applications.php +++ b/applications.php @@ -51,11 +51,11 @@ render_flash($flash); ?>
-
-
+
+
-
+
إدارة طلبات فتح المراكز
@@ -149,4 +149,4 @@ render_flash($flash);
- \ No newline at end of file + diff --git a/approved_school.php b/approved_school.php index 3be3fe4..c6dda90 100644 --- a/approved_school.php +++ b/approved_school.php @@ -17,11 +17,11 @@ if (!$application) { ?>
-
-
+
+
-
+
المركز غير موجود
@@ -151,11 +151,11 @@ render_flash($flash); ?>
-
-
+
+
-
+
diff --git a/assessment_criteria.php b/assessment_criteria.php index 8ad7fc6..412b761 100644 --- a/assessment_criteria.php +++ b/assessment_criteria.php @@ -101,11 +101,11 @@ render_flash($flash); ?>
-
-
+
+
-
+
المدرسة غير موجودة
@@ -213,7 +213,10 @@ render_flash($flash); أوقف التفعيل لإخفائه - + @@ -255,7 +258,10 @@ render_flash($flash); - + diff --git a/assessment_score_sheet.php b/assessment_score_sheet.php index 0c0a4c5..8a569d8 100644 --- a/assessment_score_sheet.php +++ b/assessment_score_sheet.php @@ -149,11 +149,11 @@ render_flash($flash); ?>
-
-
+
+
-
+
المدرسة غير موجودة
diff --git a/assessment_scores.php b/assessment_scores.php index 17871d9..bf2bab8 100644 --- a/assessment_scores.php +++ b/assessment_scores.php @@ -55,11 +55,11 @@ render_flash($flash); ?>
-
-
+
+
-
+
المدرسة غير موجودة
diff --git a/assessments.php b/assessments.php index 98d22c7..1783f8a 100644 --- a/assessments.php +++ b/assessments.php @@ -99,11 +99,11 @@ render_flash($flash); ?>
-
-
+
+
-
+
@@ -237,12 +237,35 @@ render_flash($flash); - diff --git a/assets/css/custom.css b/assets/css/custom.css index 33e451e..9eaa204 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -172,7 +172,42 @@ h6, } .container-xxl { - max-width: 1240px; + max-width: min(1540px, calc(100vw - 2rem)); +} + +@media (min-width: 992px) { + .admin-layout.row { + --bs-gutter-x: 0; + --bs-gutter-y: 0; + display: flex; + flex-wrap: nowrap; + align-items: flex-start; + gap: 1.5rem; + margin-left: 0; + margin-right: 0; + } + + .admin-layout.row > .layout-sidebar-column, + .admin-layout.row > .layout-content-column { + width: auto; + max-width: none; + padding-left: 0; + padding-right: 0; + flex: 0 1 auto; + } + + .admin-layout.row > .layout-sidebar-column { + order: 1; + flex: 0 0 clamp(280px, 22vw, 320px); + max-width: clamp(280px, 22vw, 320px); + align-self: flex-start; + } + + .admin-layout.row > .layout-content-column { + order: 2; + flex: 1 1 auto; + min-width: 0; + } } .hero-section { @@ -915,14 +950,96 @@ textarea.form-control { color: var(--muted); } +.table-action-cell { + white-space: nowrap; +} + +.table-icon-actions { + display: inline-flex; + align-items: center; + justify-content: flex-end; + gap: 0.45rem; + flex-wrap: nowrap; +} + +.icon-action { + width: 2rem; + height: 2rem; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 0.8rem; + line-height: 1; +} + +.icon-action svg { + width: 0.95rem; + height: 0.95rem; + flex-shrink: 0; +} + +.teacher-subject-picker { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 0.65rem; +} + +.teacher-subject-option { + display: flex; + align-items: center; + gap: 0.55rem; + border: 1px solid var(--border); + border-radius: 0.95rem; + background: #fff; + padding: 0.7rem 0.85rem; + min-height: 100%; + cursor: pointer; + transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease; +} + +.teacher-subject-option:hover { + border-color: rgba(14, 165, 233, 0.35); + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.06); + transform: translateY(-1px); +} + +.teacher-subject-option .form-check-input { + margin: 0; + flex-shrink: 0; +} + +.teacher-subject-badges { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; +} + +.teacher-subject-badge { + display: inline-flex; + align-items: center; + border: 1px solid rgba(14, 165, 233, 0.18); + border-radius: 999px; + background: #f8fbff; + color: var(--primary); + padding: 0.18rem 0.55rem; + font-size: 0.78rem; + font-weight: 600; + line-height: 1.4; +} + /* Admin Sidebar */ .admin-sidebar { + position: sticky; + top: 2rem; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow); padding: 1.25rem; + max-height: calc(100vh - 2.5rem); + overflow: auto; } .sidebar-nav { display: flex; @@ -957,3 +1074,578 @@ textarea.form-control { margin-bottom: 0.5rem; padding: 0 1rem; } + + +.student-certificate-page { + padding-top: 2rem; +} + +.certificate-toolbar { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + gap: 1rem; + margin-bottom: 1.5rem; +} + +.certificate-toolbar-actions { + display: inline-flex; + flex-wrap: wrap; + gap: 0.75rem; +} + +.student-certificate-card { + position: relative; + overflow: hidden; + border-radius: 1.8rem; + border: 1px solid rgba(15, 23, 42, 0.08); + background: + radial-gradient(circle at top left, rgba(14, 165, 233, 0.16), transparent 34%), + radial-gradient(circle at top right, rgba(245, 158, 11, 0.18), transparent 32%), + linear-gradient(135deg, #ffffff 0%, #f8fbff 55%, #fdf7eb 100%); + box-shadow: 0 28px 60px rgba(15, 23, 42, 0.12); +} + +.student-certificate-card::before, +.student-certificate-card::after { + content: ''; + position: absolute; + border-radius: 999px; + pointer-events: none; +} + +.student-certificate-card::before { + width: 220px; + height: 220px; + top: -90px; + left: -70px; + background: rgba(14, 165, 233, 0.12); +} + +.student-certificate-card::after { + width: 180px; + height: 180px; + bottom: -80px; + right: -55px; + background: rgba(245, 158, 11, 0.14); +} + +.student-certificate-body { + position: relative; + z-index: 1; + padding: 2rem; +} + +.certificate-kicker { + display: inline-flex; + align-items: center; + gap: 0.45rem; + padding: 0.5rem 0.9rem; + border-radius: 999px; + background: rgba(255, 255, 255, 0.8); + border: 1px solid rgba(15, 23, 42, 0.08); + color: var(--muted); + font-size: 0.92rem; +} + +.certificate-title { + font-size: clamp(2rem, 4vw, 3rem); + font-weight: 700; + margin: 1rem 0 0.75rem; +} + +.certificate-subtitle { + max-width: 58rem; + color: var(--muted); + font-size: 1.02rem; +} + +.certificate-student-name { + font-size: clamp(1.8rem, 3vw, 2.5rem); + font-weight: 700; + margin: 1.25rem 0 0.35rem; +} + +.certificate-grid { + display: grid; + grid-template-columns: minmax(0, 1.5fr) minmax(280px, 0.9fr); + gap: 1.35rem; + margin-top: 1.75rem; +} + +.certificate-panel { + border-radius: 1.35rem; + border: 1px solid rgba(15, 23, 42, 0.08); + background: rgba(255, 255, 255, 0.82); + padding: 1.2rem 1.25rem; + backdrop-filter: blur(8px); +} + +.certificate-meta-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 0.9rem; + margin-top: 1.25rem; +} + +.certificate-meta-item { + border-radius: 1rem; + background: rgba(248, 250, 252, 0.95); + padding: 0.9rem 1rem; + border: 1px solid rgba(148, 163, 184, 0.2); +} + +.certificate-meta-item strong, +.certificate-signature strong, +.certificate-panel strong { + display: block; + margin-bottom: 0.25rem; +} + +.performance-pill { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.7rem 1.15rem; + border-radius: 999px; + font-weight: 700; + font-size: 1rem; + background: rgba(14, 165, 233, 0.12); + color: #075985; + border: 1px solid rgba(14, 165, 233, 0.18); +} + +.performance-pill.performance-excellent { + background: rgba(22, 163, 74, 0.14); + color: #166534; + border-color: rgba(22, 163, 74, 0.2); +} + +.performance-pill.performance-very_good { + background: rgba(14, 165, 233, 0.14); + color: #075985; + border-color: rgba(14, 165, 233, 0.18); +} + +.performance-pill.performance-good { + background: rgba(245, 158, 11, 0.16); + color: #92400e; + border-color: rgba(245, 158, 11, 0.18); +} + +.performance-pill.performance-poor { + background: rgba(239, 68, 68, 0.14); + color: #991b1b; + border-color: rgba(239, 68, 68, 0.18); +} + +.certificate-stat-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.9rem; + margin-top: 1rem; +} + +.certificate-stat { + border-radius: 1rem; + background: rgba(255, 255, 255, 0.9); + border: 1px solid rgba(148, 163, 184, 0.2); + padding: 0.9rem 1rem; +} + +.certificate-stat strong { + font-size: 1.4rem; + margin-bottom: 0.15rem; +} + +.certificate-table-wrap { + overflow-x: auto; +} + +.certificate-table { + width: 100%; + border-collapse: collapse; +} + +.certificate-table th, +.certificate-table td { + padding: 0.85rem 0.9rem; + border-bottom: 1px solid rgba(148, 163, 184, 0.18); + text-align: right; + vertical-align: top; +} + +.certificate-table thead th { + color: var(--muted); + font-size: 0.92rem; + font-weight: 600; + background: rgba(248, 250, 252, 0.9); +} + +.certificate-table tbody tr:last-child td { + border-bottom: none; +} + +.certificate-signature-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 1rem; + margin-top: 1.5rem; +} + +.certificate-signature { + border-top: 1px dashed rgba(100, 116, 139, 0.55); + padding-top: 0.85rem; +} + +.certificate-empty-state { + text-align: center; + padding: 3rem 1.25rem; +} + +@media (max-width: 991.98px) { + .certificate-grid { + grid-template-columns: 1fr; + } +} + +@media print { + body { + background: #fff; + } + + .site-header, + .site-footer, + .certificate-toolbar, + .toast-container { + display: none !important; + } + + .student-certificate-page, + .student-certificate-body { + padding: 0; + } + + .student-certificate-card, + .certificate-panel, + .certificate-meta-item, + .certificate-stat { + box-shadow: none !important; + background: #fff !important; + backdrop-filter: none !important; + } + + .student-certificate-card { + border: 2px solid rgba(148, 116, 18, 0.65); + border-radius: 1rem; + } + + .student-certificate-card::before, + .student-certificate-card::after { + display: none; + } +} + +.completion-certificate-page { + padding-top: 2rem; +} + +.completion-certificate-card { + position: relative; + overflow: hidden; + border-radius: 2rem; + border: 1px solid rgba(148, 163, 184, 0.24); + box-shadow: 0 28px 60px rgba(15, 23, 42, 0.12); +} + +.completion-template-modern { + background: + radial-gradient(circle at top left, rgba(14, 165, 233, 0.18), transparent 30%), + radial-gradient(circle at bottom right, rgba(251, 191, 36, 0.18), transparent 28%), + linear-gradient(145deg, #fffdf7 0%, #ffffff 38%, #f5fbff 100%); +} + +.completion-template-classic { + background: + linear-gradient(180deg, rgba(255, 251, 235, 0.9) 0%, rgba(255, 255, 255, 1) 24%, rgba(255, 251, 235, 0.82) 100%); +} + +.completion-template-classic::before { + content: ''; + position: absolute; + inset: 1.1rem; + border: 2px solid rgba(180, 146, 42, 0.42); + border-radius: 1.5rem; + pointer-events: none; +} + +.completion-certificate-layer { + position: absolute; + border-radius: 999px; + pointer-events: none; +} + +.completion-certificate-layer-one { + width: 260px; + height: 260px; + top: -90px; + right: -80px; + background: rgba(14, 165, 233, 0.14); +} + +.completion-certificate-layer-two { + width: 220px; + height: 220px; + bottom: -110px; + left: -70px; + background: rgba(245, 158, 11, 0.15); +} + +.completion-template-classic .completion-certificate-layer-one, +.completion-template-classic .completion-certificate-layer-two { + background: rgba(180, 146, 42, 0.08); +} + +.completion-certificate-inner { + position: relative; + z-index: 1; + padding: 2.2rem; +} + +.completion-certificate-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 1.25rem; + margin-bottom: 2rem; +} + +.completion-branding { + display: flex; + align-items: center; + gap: 1rem; +} + +.completion-brand-logo { + width: 96px; + height: 96px; + object-fit: cover; + border-radius: 1.35rem; + border: 1px solid rgba(148, 163, 184, 0.24); + background: rgba(255, 255, 255, 0.92); + box-shadow: 0 16px 35px rgba(15, 23, 42, 0.08); +} + +.completion-brand-badge { + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 2rem; + font-weight: 700; + color: #075985; +} + +.completion-overline { + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.08em; + font-size: 0.85rem; + margin-bottom: 0.4rem; +} + +.completion-certificate-title { + font-size: clamp(2.1rem, 4vw, 3.4rem); + font-weight: 700; + margin: 0; +} + +.completion-certificate-center { + color: var(--muted); + font-size: 1.05rem; + margin-top: 0.4rem; +} + +.completion-certificate-stamps { + display: inline-flex; + flex-direction: column; + align-items: flex-end; + gap: 0.7rem; +} + +.completion-stamp { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 2.75rem; + padding: 0.7rem 1rem; + border-radius: 999px; + background: rgba(255, 255, 255, 0.86); + border: 1px solid rgba(148, 163, 184, 0.2); + font-weight: 600; +} + +.completion-stamp-accent { + background: rgba(250, 204, 21, 0.18); + color: #854d0e; + border-color: rgba(202, 138, 4, 0.22); +} + +.completion-certificate-content { + text-align: center; + max-width: 54rem; + margin: 0 auto; +} + +.completion-intro { + color: var(--muted); + font-size: 1rem; +} + +.completion-student-name { + font-size: clamp(2.35rem, 5vw, 4.35rem); + font-weight: 700; + line-height: 1.1; + margin-bottom: 0.5rem; +} + +.completion-student-code { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 2.2rem; + padding: 0.45rem 0.95rem; + border-radius: 999px; + background: rgba(255, 255, 255, 0.72); + border: 1px solid rgba(148, 163, 184, 0.2); + color: var(--muted); + margin-bottom: 1.2rem; +} + +.completion-message { + font-size: 1.2rem; + line-height: 2; + margin: 0 auto 0.8rem; + max-width: 48rem; +} + +.completion-honor-line { + color: #92400e; + font-weight: 600; + font-size: 1.05rem; +} + +.completion-certificate-grid { + display: grid; + grid-template-columns: minmax(260px, 0.8fr) minmax(0, 1.3fr); + gap: 1.1rem; + margin-top: 2rem; + align-items: stretch; +} + +.completion-summary-card, +.completion-meta-item, +.completion-note, +.completion-signature-block { + border-radius: 1.25rem; + background: rgba(255, 255, 255, 0.82); + border: 1px solid rgba(148, 163, 184, 0.2); + backdrop-filter: blur(8px); +} + +.completion-summary-card { + padding: 1.25rem; + text-align: center; + display: flex; + flex-direction: column; + justify-content: center; + gap: 0.6rem; +} + +.completion-summary-label { + color: var(--muted); + font-size: 0.92rem; +} + +.completion-performance-pill { + margin: 0 auto; +} + +.completion-honor-title { + font-size: 1.5rem; + font-weight: 700; +} + +.completion-honor-subtitle { + color: var(--muted); +} + +.completion-meta-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; +} + +.completion-meta-item { + padding: 1rem 1.05rem; +} + +.completion-meta-item strong, +.completion-signature-block strong { + display: block; + margin-bottom: 0.35rem; +} + +.completion-note { + padding: 1rem 1.1rem; + margin-top: 1rem; + text-align: center; + color: var(--muted); +} + +.completion-signature-row { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; + margin-top: 1.5rem; +} + +.completion-signature-block { + padding: 1rem 1.1rem; + text-align: center; +} + +@media (max-width: 991.98px) { + .completion-certificate-header, + .completion-branding { + flex-direction: column; + align-items: center; + text-align: center; + } + + .completion-certificate-stamps { + align-items: center; + } + + .completion-certificate-grid, + .completion-signature-row, + .completion-meta-grid { + grid-template-columns: 1fr; + } +} + +@media print { + .completion-certificate-card, + .completion-summary-card, + .completion-meta-item, + .completion-note, + .completion-signature-block { + box-shadow: none !important; + backdrop-filter: none !important; + } + + .completion-certificate-card { + border: 2px solid rgba(180, 146, 42, 0.62); + border-radius: 1rem; + } +} diff --git a/attendance.php b/attendance.php index ecd7bd5..3e12a97 100644 --- a/attendance.php +++ b/attendance.php @@ -119,11 +119,11 @@ render_flash($flash); ?>
-
-
+
+
-
+
diff --git a/center_profile.php b/center_profile.php index 4ad0488..7723986 100644 --- a/center_profile.php +++ b/center_profile.php @@ -13,11 +13,11 @@ if (!$application) { ?>
-
-
+
+
-
+
المركز غير موجود
العودة @@ -109,11 +109,11 @@ render_flash($flash); ?>
-
-
+
+
-
+

إعدادات وهوية المركز

تعديل اسم المركز، الشعار (Logo)، الأيقونة (Favicon)، وبيانات التواصل.

diff --git a/center_subjects.php b/center_subjects.php index 2ab1c16..e2d7906 100644 --- a/center_subjects.php +++ b/center_subjects.php @@ -22,11 +22,11 @@ if (!$application || !$isApproved) { ?>
-
-
+
+
-
+
المركز غير موجود
العودة @@ -73,11 +73,11 @@ render_flash($flash); ?>
-
-
+
+
-
+

المواد الدراسية للمركز

تحديد وتحديث المواد الدراسية التي يتم تقديمها في هذا المركز. سيتم توفير هذه المواد لاختيارها في التقييمات والجداول.

@@ -125,4 +125,4 @@ render_flash($flash);
-
-
-
+
+
-
+
@@ -137,4 +137,4 @@ render_flash($flash);
- \ No newline at end of file + diff --git a/db/migrations/20260417_alter_app_settings_completion_certificate.sql b/db/migrations/20260417_alter_app_settings_completion_certificate.sql new file mode 100644 index 0000000..2e9fbdf --- /dev/null +++ b/db/migrations/20260417_alter_app_settings_completion_certificate.sql @@ -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; diff --git a/db/migrations/20260417_alter_school_teachers_subject_ids.sql b/db/migrations/20260417_alter_school_teachers_subject_ids.sql new file mode 100644 index 0000000..2d02d7f --- /dev/null +++ b/db/migrations/20260417_alter_school_teachers_subject_ids.sql @@ -0,0 +1,2 @@ +ALTER TABLE school_teachers + ADD COLUMN subject_ids TEXT NULL AFTER specialization; diff --git a/global_cycles.php b/global_cycles.php index b529e40..3f4b48d 100644 --- a/global_cycles.php +++ b/global_cycles.php @@ -127,11 +127,11 @@ render_flash($flash); ?>
-
-
+
+
-
+
diff --git a/includes/app.php b/includes/app.php index caeb087..5a2e44c 100644 --- a/includes/app.php +++ b/includes/app.php @@ -131,6 +131,23 @@ function ensure_app_settings_schema(PDO $pdo): void updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; "); + + $sloganMigrationPath = __DIR__ . '/../db/migrations/20260416_alter_app_settings_slogan.sql'; + if (!schema_table_has_column($pdo, 'app_settings', 'app_slogan') && is_file($sloganMigrationPath)) { + $sql = file_get_contents($sloganMigrationPath); + if (is_string($sql) && trim($sql) !== '') { + $pdo->exec($sql); + } + } + + $completionMigrationPath = __DIR__ . '/../db/migrations/20260417_alter_app_settings_completion_certificate.sql'; + if (!schema_table_has_column($pdo, 'app_settings', 'completion_certificate_template') && is_file($completionMigrationPath)) { + $sql = file_get_contents($completionMigrationPath); + if (is_string($sql) && trim($sql) !== '') { + $pdo->exec($sql); + } + } + $pdo->exec("INSERT IGNORE INTO app_settings (id, app_name) VALUES (1, 'Central Admin')"); } @@ -146,9 +163,18 @@ function get_app_settings(): array 'app_telephone' => '', 'app_logo' => '', 'app_favicon' => '', - 'app_slogan' => '' + 'app_slogan' => '', + 'completion_certificate_template' => 'modern', + 'completion_certificate_tagline' => 'شهادة إتمام وتكريم', + 'completion_certificate_message' => 'تشهد إدارة {center} بأن الطالب/الطالبة {student} قد أتمّ/أتمّت متطلبات {cycle} بنجاح، وحقق/حققت مستوى أداء {performance} {honor}.', ]; } + + $res['app_slogan'] = (string) ($res['app_slogan'] ?? ''); + $res['completion_certificate_template'] = (string) ($res['completion_certificate_template'] ?? 'modern'); + $res['completion_certificate_tagline'] = (string) ($res['completion_certificate_tagline'] ?? 'شهادة إتمام وتكريم'); + $res['completion_certificate_message'] = (string) ($res['completion_certificate_message'] ?? 'تشهد إدارة {center} بأن الطالب/الطالبة {student} قد أتمّ/أتمّت متطلبات {cycle} بنجاح، وحقق/حققت مستوى أداء {performance} {honor}.'); + return $res; } @@ -185,6 +211,14 @@ function ensure_school_module_schema(PDO $pdo): void } } + $teacherSubjectsMigrationPath = __DIR__ . '/../db/migrations/20260417_alter_school_teachers_subject_ids.sql'; + if (!schema_table_has_column($pdo, 'school_teachers', 'subject_ids') && is_file($teacherSubjectsMigrationPath)) { + $sql = file_get_contents($teacherSubjectsMigrationPath); + if (is_string($sql) && trim($sql) !== '') { + $pdo->exec($sql); + } + } + $done = true; } @@ -804,6 +838,7 @@ function teacher_defaults(): array 'full_name' => '', 'role_title' => '', 'specialization' => '', + 'subject_ids' => [], 'phone' => '', 'email' => '', 'employment_status' => 'active', @@ -840,14 +875,72 @@ function teacher_employment_status_badge(string $status): string return '' . e($meta['label']) . ''; } -function validate_teacher_input(array $input): array +function normalize_id_list(mixed $value): array +{ + if (is_array($value)) { + $items = $value; + } elseif (is_string($value) && $value !== '') { + $decoded = json_decode($value, true); + $items = is_array($decoded) ? $decoded : []; + } else { + $items = []; + } + + $normalized = []; + foreach ($items as $item) { + $id = (int) $item; + if ($id > 0) { + $normalized[$id] = $id; + } + } + + return array_values($normalized); +} + +function get_enabled_subject_map(): array +{ + $map = []; + foreach (get_enabled_subjects() as $subject) { + $subjectId = (int) ($subject['id'] ?? 0); + if ($subjectId <= 0) { + continue; + } + + $map[$subjectId] = (string) ($subject['name'] ?? ''); + } + + return $map; +} + +function teacher_subject_labels(array $teacher, ?array $subjectMap = null): array +{ + $subjectMap = $subjectMap ?? get_enabled_subject_map(); + $labels = []; + + foreach (normalize_id_list($teacher['subject_ids'] ?? []) as $subjectId) { + if (!isset($subjectMap[$subjectId]) || $subjectMap[$subjectId] === '') { + continue; + } + + $labels[] = $subjectMap[$subjectId]; + } + + return $labels; +} + +function validate_teacher_input(array $input, array $allowedSubjectIds = []): array { $data = teacher_defaults(); foreach ($data as $key => $_value) { + if ($key === 'subject_ids') { + continue; + } + $limit = $key === 'notes' ? 1000 : 190; $data[$key] = clean_text((string) ($input[$key] ?? ''), $limit); } + $data['subject_ids'] = normalize_id_list($input['subject_ids'] ?? []); $errors = []; if ($data['full_name'] === '') { @@ -868,6 +961,23 @@ function validate_teacher_input(array $input): array $errors['employment_status'] = 'يرجى اختيار حالة توظيف صحيحة.'; } + $validSubjectIds = $allowedSubjectIds !== [] ? normalize_id_list($allowedSubjectIds) : array_keys(get_enabled_subject_map()); + if ($validSubjectIds !== []) { + $validLookup = array_fill_keys($validSubjectIds, true); + $filteredSubjectIds = array_values(array_filter( + $data['subject_ids'], + static fn (int $subjectId): bool => isset($validLookup[$subjectId]) + )); + + if (count($filteredSubjectIds) !== count($data['subject_ids'])) { + $errors['subject_ids'] = 'اختر مواداً صحيحة من قائمة المركز.'; + } + + $data['subject_ids'] = $filteredSubjectIds; + } else { + $data['subject_ids'] = []; + } + return [$data, $errors]; } @@ -876,20 +986,23 @@ function create_teacher(int $centerApplicationId, array $data): int $pdo = db_connection(); $stmt = $pdo->prepare( 'INSERT INTO school_teachers ( - center_application_id, full_name, role_title, specialization, + center_application_id, full_name, role_title, specialization, subject_ids, phone, email, employment_status, notes, created_at, updated_at ) VALUES ( - :center_application_id, :full_name, :role_title, :specialization, + :center_application_id, :full_name, :role_title, :specialization, :subject_ids, :phone, :email, :employment_status, :notes, NOW(), NOW() )' ); + $subjectIdsJson = !empty($data['subject_ids']) ? json_encode(array_values($data['subject_ids'])) : null; + $stmt->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT); $stmt->bindValue(':full_name', $data['full_name'], PDO::PARAM_STR); $stmt->bindValue(':role_title', $data['role_title'], PDO::PARAM_STR); $stmt->bindValue(':specialization', $data['specialization'] !== '' ? $data['specialization'] : null, $data['specialization'] !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL); + $stmt->bindValue(':subject_ids', $subjectIdsJson, $subjectIdsJson !== null ? PDO::PARAM_STR : PDO::PARAM_NULL); $stmt->bindValue(':phone', $data['phone'] !== '' ? $data['phone'] : null, $data['phone'] !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL); $stmt->bindValue(':email', $data['email'] !== '' ? $data['email'] : null, $data['email'] !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL); $stmt->bindValue(':employment_status', $data['employment_status'], PDO::PARAM_STR); diff --git a/includes/cycles.php b/includes/cycles.php index 5d6dc69..d8f5247 100644 --- a/includes/cycles.php +++ b/includes/cycles.php @@ -335,12 +335,12 @@ function ensure_default_school_cycle_record(PDO $pdo, array $application): int } -function update_teacher_in_cycle(int $id, int $centerApplicationId, int $cycleId, array $data): void +function update_teacher_in_cycle(int $centerApplicationId, int $cycleId, int $id, array $data): void { $pdo = db_connection(); $stmt = $pdo->prepare( 'UPDATE school_teachers SET - full_name = :full_name, role_title = :role_title, specialization = :specialization, + full_name = :full_name, role_title = :role_title, specialization = :specialization, subject_ids = :subject_ids, phone = :phone, email = :email, employment_status = :employment_status, notes = :notes, updated_at = NOW() WHERE id = :id AND center_application_id = :center_application_id AND cycle_id = :cycle_id' @@ -351,11 +351,12 @@ function update_teacher_in_cycle(int $id, int $centerApplicationId, int $cycleId ':cycle_id' => $cycleId, ':full_name' => $data['full_name'], ':role_title' => $data['role_title'], - ':specialization' => $data['specialization'], - ':phone' => $data['phone'], - ':email' => $data['email'], + ':specialization' => $data['specialization'] !== '' ? $data['specialization'] : null, + ':subject_ids' => !empty($data['subject_ids']) ? json_encode(array_values($data['subject_ids'])) : null, + ':phone' => $data['phone'] !== '' ? $data['phone'] : null, + ':email' => $data['email'] !== '' ? $data['email'] : null, ':employment_status' => $data['employment_status'], - ':notes' => $data['notes'], + ':notes' => $data['notes'] !== '' ? $data['notes'] : null, ]); } @@ -420,12 +421,12 @@ function copy_school_cycle_rollover(PDO $pdo, int $centerApplicationId, int $sou if (!empty($rollover['copy_teachers'])) { $stmt = $pdo->prepare( 'INSERT INTO school_teachers ( - center_application_id, cycle_id, full_name, role_title, specialization, + center_application_id, cycle_id, full_name, role_title, specialization, subject_ids, phone, email, employment_status, notes, created_at, updated_at ) SELECT - center_application_id, :target_cycle_id, full_name, role_title, specialization, + center_application_id, :target_cycle_id, full_name, role_title, specialization, subject_ids, phone, email, employment_status, notes, NOW(), NOW() FROM school_teachers @@ -816,11 +817,11 @@ function create_teacher_in_cycle(int $centerApplicationId, int $cycleId, array $ $pdo = db_connection(); $stmt = $pdo->prepare( 'INSERT INTO school_teachers ( - center_application_id, cycle_id, full_name, role_title, specialization, + center_application_id, cycle_id, full_name, role_title, specialization, subject_ids, phone, email, employment_status, notes, created_at, updated_at ) VALUES ( - :center_application_id, :cycle_id, :full_name, :role_title, :specialization, + :center_application_id, :cycle_id, :full_name, :role_title, :specialization, :subject_ids, :phone, :email, :employment_status, :notes, NOW(), NOW() )' @@ -831,6 +832,7 @@ function create_teacher_in_cycle(int $centerApplicationId, int $cycleId, array $ ':full_name' => $data['full_name'], ':role_title' => $data['role_title'], ':specialization' => $data['specialization'] !== '' ? $data['specialization'] : null, + ':subject_ids' => !empty($data['subject_ids']) ? json_encode(array_values($data['subject_ids'])) : null, ':phone' => $data['phone'] !== '' ? $data['phone'] : null, ':email' => $data['email'] !== '' ? $data['email'] : null, ':employment_status' => $data['employment_status'], @@ -1843,6 +1845,218 @@ function school_student_options_by_cycle(int $centerApplicationId, int $cycleId) return $options; } +function school_student_record_by_cycle(int $centerApplicationId, int $cycleId, int $studentId): ?array +{ + if ($studentId <= 0) { + return null; + } + + $pdo = db_connection(); + $stmt = $pdo->prepare( + 'SELECT * + FROM school_students + WHERE center_application_id = :center_application_id + AND cycle_id = :cycle_id + AND id = :student_id + LIMIT 1' + ); + $stmt->execute([ + ':center_application_id' => $centerApplicationId, + ':cycle_id' => $cycleId, + ':student_id' => $studentId, + ]); + $row = $stmt->fetch(); + + return $row ?: null; +} + +function student_certificate_performance_meta(float $overallPercentage): array +{ + if ($overallPercentage >= 90) { + return ['key' => 'excellent', 'label' => 'Excellent', 'label_ar' => 'ممتاز']; + } + + if ($overallPercentage >= 80) { + return ['key' => 'very_good', 'label' => 'Very Good', 'label_ar' => 'جيد جداً']; + } + + if ($overallPercentage >= 65) { + return ['key' => 'good', 'label' => 'Good', 'label_ar' => 'جيد']; + } + + return ['key' => 'poor', 'label' => 'Poor', 'label_ar' => 'ضعيف']; +} + +function student_completion_certificate_honor_meta(float $overallPercentage): array +{ + $performance = student_certificate_performance_meta($overallPercentage); + + return match ($performance['key']) { + 'excellent' => [ + 'key' => 'highest_honors', + 'title' => 'With Highest Honors', + 'title_ar' => 'بمرتبة الشرف العليا', + 'completion_line_ar' => 'أتمّ/أتمّت الدورة بتميز استثنائي واستحقاق رفيع.', + ], + 'very_good' => [ + 'key' => 'honors', + 'title' => 'With Honors', + 'title_ar' => 'بمرتبة الشرف', + 'completion_line_ar' => 'أتمّ/أتمّت الدورة بمستوى قوي يبعث على الفخر.', + ], + 'good' => [ + 'key' => 'merit', + 'title' => 'With Merit', + 'title_ar' => 'بتقدير جيد', + 'completion_line_ar' => 'أتمّ/أتمّت الدورة بنجاح وأظهر/أظهرت التزاماً جيداً.', + ], + default => [ + 'key' => 'completion', + 'title' => 'Successful Completion', + 'title_ar' => 'بإتمام ناجح', + 'completion_line_ar' => 'أتمّ/أتمّت الدورة واستوفى/استوفت متطلباتها الأساسية.', + ], + }; +} + +function school_student_certificate_summary(int $centerApplicationId, int $cycleId, int $studentId): array +{ + $student = school_student_record_by_cycle($centerApplicationId, $cycleId, $studentId); + $summary = [ + 'student' => $student, + '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), + ]; + + if ($student === null) { + return $summary; + } + + $pdo = db_connection(); + $activeAssessmentsStmt = $pdo->prepare( + 'SELECT COUNT(*) + FROM school_assessment_types + WHERE center_application_id = :center_application_id + AND cycle_id = :cycle_id + AND is_active = 1' + ); + $activeAssessmentsStmt->execute([ + ':center_application_id' => $centerApplicationId, + ':cycle_id' => $cycleId, + ]); + $summary['active_assessments'] = (int) $activeAssessmentsStmt->fetchColumn(); + + $stmt = $pdo->prepare( + 'SELECT + scores.assessment_type_id, + scores.score, + scores.max_score, + scores.status, + scores.notes, + scores.assessed_on, + assessments.title, + assessments.category, + assessments.weight_percentage, + assessments.is_active, + assessments.subject_id + FROM school_assessment_scores scores + INNER JOIN school_assessment_types assessments ON assessments.id = scores.assessment_type_id + WHERE scores.center_application_id = :center_application_id + AND scores.cycle_id = :cycle_id + AND scores.student_id = :student_id + AND assessments.is_active = 1 + ORDER BY scores.assessed_on ASC, assessments.weight_percentage DESC, assessments.id ASC' + ); + $stmt->execute([ + ':center_application_id' => $centerApplicationId, + ':cycle_id' => $cycleId, + ':student_id' => $studentId, + ]); + $rows = $stmt->fetchAll(); + + $weightedScore = 0.0; + $weightedTotal = 0.0; + $plainPercentageSum = 0.0; + $plainPercentageCount = 0; + $scoreTotal = 0.0; + $maxScoreTotal = 0.0; + $latestAssessedOn = ''; + + foreach ($rows as $row) { + $status = (string) ($row['status'] ?? ''); + if ($status === 'absent') { + $summary['absent_assessments']++; + continue; + } + if ($status === 'excused') { + $summary['excused_assessments']++; + continue; + } + + $score = isset($row['score']) ? (float) $row['score'] : null; + $maxScore = (float) ($row['max_score'] ?? 0); + if ($status !== 'present' || $score === null || $maxScore <= 0) { + continue; + } + + $percentage = round(($score / $maxScore) * 100, 2); + $weight = max(0.0, (float) ($row['weight_percentage'] ?? 0)); + if ($weight > 0) { + $weightedScore += $percentage * $weight; + $weightedTotal += $weight; + } + $plainPercentageSum += $percentage; + $plainPercentageCount++; + $scoreTotal += $score; + $maxScoreTotal += $maxScore; + + $assessedOn = (string) ($row['assessed_on'] ?? ''); + if ($assessedOn !== '' && ($latestAssessedOn === '' || strtotime($assessedOn) > strtotime($latestAssessedOn))) { + $latestAssessedOn = $assessedOn; + } + + $summary['assessments'][] = [ + 'assessment_type_id' => (int) ($row['assessment_type_id'] ?? 0), + 'title' => (string) ($row['title'] ?? 'تقييم'), + 'category' => (string) ($row['category'] ?? ''), + 'weight_percentage' => $weight, + 'score' => round($score, 2), + 'max_score' => round($maxScore, 2), + 'percentage' => $percentage, + 'assessed_on' => $assessedOn, + 'notes' => (string) ($row['notes'] ?? ''), + ]; + } + + $overallPercentage = 0.0; + if ($weightedTotal > 0) { + $overallPercentage = round($weightedScore / $weightedTotal, 2); + } elseif ($plainPercentageCount > 0) { + $overallPercentage = round($plainPercentageSum / $plainPercentageCount, 2); + } + + $summary['completed_assessments'] = $plainPercentageCount; + $summary['missing_assessments'] = max(0, $summary['active_assessments'] - $summary['completed_assessments'] - $summary['absent_assessments'] - $summary['excused_assessments']); + $summary['has_results'] = $plainPercentageCount > 0; + $summary['overall_percentage'] = $overallPercentage; + $summary['score_total'] = round($scoreTotal, 2); + $summary['max_score_total'] = round($maxScoreTotal, 2); + $summary['latest_assessed_on'] = $latestAssessedOn; + $summary['performance'] = student_certificate_performance_meta($overallPercentage); + + return $summary; +} + function validate_attendance_input_for_cycle(int $centerApplicationId, int $cycleId, array $input): array { $data = attendance_defaults(); diff --git a/modules.php b/modules.php index d055a30..b681f47 100644 --- a/modules.php +++ b/modules.php @@ -9,11 +9,11 @@ render_flash($flash); ?>
-
-
+
+
-
+
هيكل الصفحات والوحدات
diff --git a/student_certificate.php b/student_certificate.php new file mode 100644 index 0000000..66dbd84 --- /dev/null +++ b/student_certificate.php @@ -0,0 +1,251 @@ + 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); +?> +
+
+
+
+
شهادة الإنجاز والمكافأة
+
صفحة قابلة للطباعة بعد إكمال تقييمات الطالب في الدورة الحالية.
+
+
+ العودة إلى الطلاب + صفحة المركز + + + +
+
+ + +
+
المركز غير موجود
+

تحقق من الرابط ثم حاول فتح الشهادة من جديد من صفحة الطلاب.

+ المراكز المعتمدة +
+ +
+
الشهادة غير متاحة بعد
+

يمكن إصدار شهادات الإنجاز فقط للمراكز المعتمدة.

+
+ +
+
تعذر العثور على الطالب
+

ربما تم تغيير الدورة أو أن الطالب لا يتبع هذا المركز.

+ العودة إلى سجل الطلاب +
+ +
+
+ Certificate of Completion & Performance +

شهادة تكريم وإتمام الدورة

+

+ تشهد إدارة بأن الطالب/الطالبة + + قد أتم/أتمت متطلبات الدورة + وتم تقييم أدائه/أدائها وفق السجلات المرصودة في النظام. +

+ +
+
Student Code: ·
+ +
+
+ المركز + +
+
+ الدورة + +
+
+ مدة الدورة + +
+
+ تاريخ الإصدار + +
+
+ +
+
+ نص الشهادة +

بناءً على حضور الطالب ومشاركته ونتائج تقييماته خلال هذه الدورة، تم منحه/منحها هذه الشهادة التقديرية مع بيان مستوى الأداء العام أدناه.

+
+ +
+
+
+ % + متوسط الأداء النهائي +
+
+ + تقييمات مكتملة +
+
+ / + إجمالي الدرجات المرصودة +
+
+ + آخر تقييم مسجل +
+
+ +
+ +
+ + +
+ + +
+
+ تفصيل التقييمات + Overall performance is calculated from the recorded assessments in this course. +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
التقييمالفئةالدرجةالنسبةالوزنالتاريخ
+ + + / %%
+
+
+ +
+
لا توجد تقييمات مرصودة بعد
+

أدخل درجات الطالب أولاً من صفحة رصد الدرجات، ثم افتح الشهادة من جديد لإظهار الأداء العام.

+ العودة إلى الطلاب +
+ + +
+
+ مدير/ة المركز + +
+
+ اعتماد الأداء النهائي + +
+
+
+
+ +
+
+ 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); +?> +
+
+
+
+
شهادة الإتمام والتكريم
+
نسخة مختصرة وأنيقة للطباعة، مستقلة عن كشف الدرجات التفصيلي.
+
+
+ العودة إلى الطلاب + + كشف الأداء + + +
+
+ + +
+
المركز غير موجود
+

تحقق من الرابط ثم أعد فتح الشهادة من صفحة الطلاب.

+ المراكز المعتمدة +
+ +
+
الشهادة غير متاحة حالياً
+

يمكن إصدار شهادة الإتمام للمراكز المعتمدة فقط.

+
+ +
+
تعذر العثور على الطالب
+

ربما تم تغيير الدورة أو أن الطالب لا يتبع هذا المركز.

+ العودة إلى سجل الطلاب +
+ +
+ + + +
+
+
+ + + + + +
+
Certificate of Completion
+

+

+
+
+
+ + +
+
+ +
+

تُمنح هذه الشهادة إلى

+
+ +
رقم الطالب:
+ + +

+

+
+ +
+
+ التقدير النهائي +
+ +
+
+
+
+ +
+
+ الدورة + +
+
+ تاريخ الإصدار + +
+
+ نسبة الأداء + +
+
+ التقييمات المكتملة + +
+
+
+ + +
+ لا توجد تقييمات مرصودة لهذا الطالب بعد، لذلك تم إصدار شهادة إتمام مختصرة بدون مرتبة أداء رقمية. +
+ + +
+
+ مدير/ة المركز + +
+
+ اعتماد الشهادة + +
+
+
+
+ +
+
+ diff --git a/students.php b/students.php index f3f5579..e31067f 100644 --- a/students.php +++ b/students.php @@ -113,11 +113,11 @@ render_flash($flash); ?>
-
-
+
+
-
+
@@ -294,7 +294,7 @@ render_flash($flash); ولي الأمر الهاتف الحالة - إجراءات + إجراءات @@ -312,14 +312,48 @@ render_flash($flash); - - - + + + +
+ + + شهادة الإتمام والتكريم + + + + كشف الأداء التفصيلي + + + + +
- @@ -469,4 +503,4 @@ document.addEventListener('DOMContentLoaded', function() { - \ No newline at end of file + diff --git a/subjects.php b/subjects.php index f3807c8..a5ccf98 100644 --- a/subjects.php +++ b/subjects.php @@ -100,11 +100,11 @@ render_flash($flash); ?>
-
-
+
+
-
+
إدارة المواد الدراسية
diff --git a/teachers.php b/teachers.php index 9cdda8d..5c3f475 100644 --- a/teachers.php +++ b/teachers.php @@ -23,10 +23,25 @@ if ($application && $isApprovedSchool) { $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); + [$values, $errors] = validate_teacher_input($_POST, array_keys($teacherSubjectMap)); if (!$isApprovedSchool) { $errors['form'] = 'لا يمكن فتح صفحة المعلمين قبل اعتماد المركز.'; @@ -65,6 +80,11 @@ $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) : [ @@ -101,11 +121,11 @@ render_flash($flash); ?>
-
-
+
+
-
+
@@ -272,6 +292,7 @@ render_flash($flash); الاسم الدور التخصص + المواد التواصل الحالة إجراءات @@ -286,17 +307,38 @@ render_flash($flash); + + +
+ + + +
+ + + +
- - + +
+ +
@@ -359,6 +401,30 @@ render_flash($flash);
+
+ + +
لا توجد مواد مفعّلة حالياً، لذلك لا يمكن ربط المعلم بمادة بعد.
+ +
+ $subjectName): ?> + + +
+
+
يمكنك ربط المعلم بأكثر من مادة داخل هذه الدورة.
+ +
@@ -386,6 +452,13 @@ render_flash($flash);
- \ No newline at end of file +