diff --git a/application_detail.php b/application_detail.php new file mode 100644 index 0000000..dab87de --- /dev/null +++ b/application_detail.php @@ -0,0 +1,149 @@ + 0 ? get_application($applicationId) : null; + +if (!$application) { + http_response_code(404); + render_page_start('طلب غير موجود', 'applications', 'لم يتم العثور على طلب فتح المركز المطلوب.'); + render_flash($flash); + ?> +
+
+
+
الطلب غير موجود
+

تحقق من رقم المرجع أو ارجع إلى لوحة الطلبات.

+ العودة إلى القائمة +
+
+
+ ['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); + 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']); + +render_page_start('تفاصيل الطلب #' . $applicationId, 'applications', 'عرض تفصيلي لطلب فتح المركز مع نموذج المراجعة للمشرف العام.'); +render_flash($flash); +?> +
+
+
+
+
+
+
+
مرجع الطلب #
+
+
مقدم الطلب:
+
+ +
+ +
+
نوع المركز
+
الفئة المستهدفة
+
رقم الهاتف
+
البريد الإلكتروني
+
السعة المتوقعة طالب
+
فترة البرنامج
+
+ +
+
ملخص البرنامج والاحتياجات
+

+
+
+ +
+
سجل المراجعة
+
+
+
تم استلام الطلب
+
أُرسل الطلب بتاريخ .
+
+
+
الحالة الحالية:
+
آخر تحديث بتاريخ .
+
+
+
ملاحظات المشرف العام
+
+
+
+
+
+ +
+ + + +
+
+
+
+ diff --git a/applications.php b/applications.php new file mode 100644 index 0000000..fbc7e35 --- /dev/null +++ b/applications.php @@ -0,0 +1,88 @@ + 'الكل'] + array_map(fn ($meta) => $meta['label'], status_map()); + +render_page_start('لوحة الطلبات', 'applications', 'قائمة موحدة لطلبات فتح المراكز الصيفية مع فلترة حسب حالة المراجعة.'); +render_flash($flash); +?> +
+
+
+
+
+
لوحة طلبات فتح المراكز
+
واجهة المشرف العام لمراجعة جميع الطلبات، فرزها بالحالة، ثم الانتقال إلى التفاصيل لاتخاذ القرار.
+
+ +
+
+ $label): ?> + + + + + + + +
+
+ +
+ +
+
لا توجد طلبات ضمن هذا التصنيف.
+

ابدأ بإنشاء أول طلب فتح مركز ليظهر هنا فوراً.

+ إنشاء طلب +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
المرجعالمركزالمدينةالمديرالفترةالحالةالتقييم
# +
+
سعة متوقعة: طالب
+
+
+
+
+
+
حتى
+
+
+ +
+
+
+ diff --git a/assets/css/custom.css b/assets/css/custom.css index 789132e..8261a74 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,403 +1,571 @@ + :root { + --bg: #f3f4f6; + --surface: #ffffff; + --surface-muted: #f8fafc; + --border: #d6dbe3; + --border-strong: #c1c8d2; + --text: #0f172a; + --muted: #475569; + --primary: #111827; + --accent: #0f766e; + --accent-soft: #ecfdf5; + --success: #166534; + --warning: #92400e; + --danger: #b91c1c; + --shadow: 0 10px 30px rgba(15, 23, 42, 0.05); + --radius-sm: 10px; + --radius-md: 14px; + --radius-lg: 18px; + --font-ui: "IBM Plex Sans Arabic", "Segoe UI", Tahoma, Arial, sans-serif; +} + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + body { - background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); - background-size: 400% 400%; - animation: gradient 15s ease infinite; - color: #212529; - font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - font-size: 14px; margin: 0; + background: var(--bg); + color: var(--text); + font-family: var(--font-ui); + font-weight: 400; + line-height: 1.7; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body, +.form-control, +.form-select, +.btn, +.table { + font-size: 0.96rem; +} + +input, +select, +textarea, +button { + font: inherit; +} + +h1, +h2, +h3, +h4, +h5, +h6, +.brand-title, +.hero-title, +.stat-label, +.section-title, +.footer-title, +.timeline-title, +.empty-title { + font-family: var(--font-ui); + letter-spacing: 0; +} + +::selection { + background: rgba(15, 118, 110, 0.18); +} + +.app-shell { min-height: 100vh; } -.main-wrapper { - display: flex; +.site-header, +.site-footer { + background: rgba(255, 255, 255, 0.92); + backdrop-filter: blur(8px); +} + +.site-header { + position: sticky; + top: 0; + z-index: 1020; +} + +.navbar-brand, +.nav-link { + color: var(--text); +} + +.navbar-brand:hover, +.nav-link:hover { + color: var(--primary); +} + +.nav-link { + border-radius: 999px; + padding: 0.55rem 0.9rem; + color: var(--muted); + font-weight: 500; + transition: background-color 0.2s ease, color 0.2s ease; +} + +.nav-link.active, +.nav-link:hover { + background: #eef2f7; + color: var(--primary); +} + +.brand-mark { + text-decoration: none; +} + +.brand-badge { + width: 2.4rem; + height: 2.4rem; + display: inline-flex; align-items: center; justify-content: center; - min-height: 100vh; - width: 100%; - padding: 20px; - box-sizing: border-box; - position: relative; - z-index: 1; -} - -@keyframes gradient { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } -} - -.chat-container { - width: 100%; - max-width: 600px; - background: rgba(255, 255, 255, 0.85); - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 20px; - display: flex; - flex-direction: column; - height: 85vh; - box-shadow: 0 20px 40px rgba(0,0,0,0.2); - backdrop-filter: blur(15px); - -webkit-backdrop-filter: blur(15px); - overflow: hidden; -} - -.chat-header { - padding: 1.5rem; - border-bottom: 1px solid rgba(0, 0, 0, 0.05); - background: rgba(255, 255, 255, 0.5); + border-radius: 12px; + background: var(--primary); + color: #fff; font-weight: 700; - font-size: 1.1rem; - display: flex; - justify-content: space-between; - align-items: center; } -.chat-messages { - flex: 1; - overflow-y: auto; - padding: 1.5rem; +.brand-title { + font-weight: 700; + font-size: 1.02rem; +} + +.brand-subtitle, +.footer-meta, +.section-copy, +.hero-copy, +.text-muted { + color: var(--muted) !important; +} + +.brand-subtitle { + font-size: 0.82rem; + font-weight: 500; +} + +.header-chip, +.eyebrow, +.pill-count { + display: inline-flex; + align-items: center; + gap: 0.35rem; + border: 1px solid var(--border); + border-radius: 999px; + background: #fff; + color: var(--muted); + padding: 0.38rem 0.7rem; + font-size: 0.78rem; + font-weight: 500; +} + +.container-xxl { + max-width: 1240px; +} + +.hero-section { + padding-top: 2rem; +} + +.hero-title { + font-size: clamp(2.05rem, 2.9vw, 3.1rem); + line-height: 1.3; + font-weight: 700; + margin: 0; + max-width: 15ch; +} + +.hero-copy { + max-width: 58ch; + font-size: 1rem; +} + +.hero-meta { + display: flex; + flex-wrap: wrap; + gap: 0.7rem; + color: var(--muted); + font-size: 0.84rem; +} + +.hero-meta span { + padding: 0.45rem 0.75rem; + background: var(--surface-muted); + border: 1px solid var(--border); + border-radius: 999px; +} + +.app-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow); + padding: 1.35rem; +} + +.compact-card, +.sidebar-card { + padding: 1.1rem; +} + +.hero-card { + padding: 1.7rem; +} + +.form-card, +.detail-card { + padding: 1.4rem; +} + +.stat-card { display: flex; flex-direction: column; - gap: 1.25rem; + gap: 0.4rem; + min-height: 160px; } -/* Custom Scrollbar */ -::-webkit-scrollbar { - width: 6px; +.stat-label, +.section-title, +.footer-title, +.timeline-title, +.empty-title { + font-weight: 700; } -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.3); - border-radius: 10px; -} - -::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.5); -} - -.message { - max-width: 85%; - padding: 0.85rem 1.1rem; - border-radius: 16px; - line-height: 1.5; - font-size: 0.95rem; - box-shadow: 0 4px 15px rgba(0,0,0,0.05); - animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(20px) scale(0.95); } - to { opacity: 1; transform: translateY(0) scale(1); } -} - -.message.visitor { - align-self: flex-end; - background: linear-gradient(135deg, #212529 0%, #343a40 100%); - color: #fff; - border-bottom-right-radius: 4px; -} - -.message.bot { - align-self: flex-start; - background: #ffffff; - color: #212529; - border-bottom-left-radius: 4px; -} - -.chat-input-area { - padding: 1.25rem; - background: rgba(255, 255, 255, 0.5); - border-top: 1px solid rgba(0, 0, 0, 0.05); -} - -.chat-input-area form { +.section-head { display: flex; - gap: 0.75rem; -} - -.chat-input-area input { - flex: 1; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - padding: 0.75rem 1rem; - outline: none; - background: rgba(255, 255, 255, 0.9); - transition: all 0.3s ease; -} - -.chat-input-area input:focus { - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2); -} - -.chat-input-area button { - background: #212529; - color: #fff; - border: none; - padding: 0.75rem 1.5rem; - border-radius: 12px; - cursor: pointer; - font-weight: 600; - transition: all 0.3s ease; -} - -.chat-input-area button:hover { - background: #000; - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0,0,0,0.2); -} - -/* Background Animations */ -.bg-animations { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 0; - overflow: hidden; - pointer-events: none; -} - -.blob { - position: absolute; - width: 500px; - height: 500px; - background: rgba(255, 255, 255, 0.2); - border-radius: 50%; - filter: blur(80px); - animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1); -} - -.blob-1 { - top: -10%; - left: -10%; - background: rgba(238, 119, 82, 0.4); -} - -.blob-2 { - bottom: -10%; - right: -10%; - background: rgba(35, 166, 213, 0.4); - animation-delay: -7s; - width: 600px; - height: 600px; -} - -.blob-3 { - top: 40%; - left: 30%; - background: rgba(231, 60, 126, 0.3); - animation-delay: -14s; - width: 450px; - height: 450px; -} - -@keyframes move { - 0% { transform: translate(0, 0) rotate(0deg) scale(1); } - 33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); } - 66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); } - 100% { transform: translate(0, 0) rotate(360deg) scale(1); } -} - -.header-link { - font-size: 14px; - color: #fff; - text-decoration: none; - background: rgba(0, 0, 0, 0.2); - padding: 0.5rem 1rem; - border-radius: 8px; - transition: all 0.3s ease; -} - -.header-link:hover { - background: rgba(0, 0, 0, 0.4); - text-decoration: none; -} - -/* Admin Styles */ -.admin-container { - max-width: 900px; - margin: 3rem auto; - padding: 2.5rem; - background: rgba(255, 255, 255, 0.85); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-radius: 24px; - box-shadow: 0 20px 50px rgba(0,0,0,0.15); - border: 1px solid rgba(255, 255, 255, 0.4); - position: relative; - z-index: 1; -} - -.admin-container h1 { - margin-top: 0; - color: #212529; - font-weight: 800; -} - -.table { - width: 100%; - border-collapse: separate; - border-spacing: 0 8px; - margin-top: 1.5rem; -} - -.table th { - background: transparent; - border: none; - padding: 1rem; - color: #6c757d; - font-weight: 600; - text-transform: uppercase; - font-size: 0.75rem; - letter-spacing: 1px; -} - -.table td { - background: #fff; - padding: 1rem; - border: none; -} - -.table tr td:first-child { border-radius: 12px 0 0 12px; } -.table tr td:last-child { border-radius: 0 12px 12px 0; } - -.form-group { - margin-bottom: 1.25rem; -} - -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: 600; - font-size: 0.9rem; -} - -.form-control { - width: 100%; - padding: 0.75rem 1rem; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 12px; - background: #fff; - transition: all 0.3s ease; - box-sizing: border-box; -} - -.form-control:focus { - outline: none; - border-color: #23a6d5; - box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1); -} - -.header-container { - display: flex; - justify-content: space-between; align-items: center; -} - -.header-links { - display: flex; + justify-content: space-between; gap: 1rem; } -.admin-card { - background: rgba(255, 255, 255, 0.6); - padding: 2rem; - border-radius: 20px; - border: 1px solid rgba(255, 255, 255, 0.5); - margin-bottom: 2.5rem; - box-shadow: 0 10px 30px rgba(0,0,0,0.05); +.section-title { + font-size: 1.08rem; } -.admin-card h3 { - margin-top: 0; - margin-bottom: 1.5rem; +.small-title { + font-size: 0.96rem; +} + +.stat-value { + font-size: clamp(1.8rem, 3vw, 2.4rem); + font-weight: 700; + color: var(--primary); + letter-spacing: -0.02em; +} + +.stat-note, +.workflow-list, +.check-list, +.timeline-copy, +.summary-row span { + color: var(--muted); +} + +.module-list { + display: grid; + gap: 0.9rem; +} + +.module-item { + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--surface-muted); + padding: 1rem; +} + +.module-item h2 { + font-size: 0.98rem; + margin: 0 0 0.35rem; +} + +.module-item p, +.summary-row, +.detail-item span, +.detail-item strong, +.timeline-copy, +.notes-block p { + margin: 0; +} + +.workflow-list, +.check-list { + padding-right: 1.2rem; + margin: 0; + display: grid; + gap: 0.65rem; +} + +.filter-pills { + display: flex; + flex-wrap: wrap; + gap: 0.65rem; +} + +.filter-pill { + display: inline-flex; + align-items: center; + gap: 0.45rem; + border: 1px solid var(--border); + border-radius: 999px; + padding: 0.55rem 0.8rem; + background: #fff; + color: var(--muted); + text-decoration: none; + font-weight: 500; +} + +.filter-pill.active, +.filter-pill:hover { + color: var(--primary); + background: #eef2f7; + border-color: var(--border-strong); +} + +.pill-count { + padding: 0.2rem 0.5rem; + background: transparent; +} + +.app-table { + --bs-table-bg: transparent; + --bs-table-border-color: #e6e8ee; + margin: 0; +} + +.app-table thead th { + font-size: 0.8rem; + font-weight: 600; + letter-spacing: 0; + color: #64748b; + white-space: nowrap; + border-bottom-width: 1px; +} + +.app-table tbody td { + padding-top: 1rem; + padding-bottom: 1rem; + vertical-align: middle; +} + +.table-link { + color: var(--primary); + text-decoration: none; font-weight: 700; } -.btn-delete { - background: #dc3545; - color: white; - border: none; - padding: 0.25rem 0.5rem; - border-radius: 4px; - cursor: pointer; +.table-link:hover { + text-decoration: underline; } -.btn-add { - background: #212529; - color: white; - border: none; - padding: 0.5rem 1rem; - border-radius: 4px; - cursor: pointer; - margin-top: 1rem; -} - -.btn-save { - background: #0088cc; - color: white; - border: none; - padding: 0.8rem 1.5rem; - border-radius: 12px; - cursor: pointer; +.status-badge { + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 999px; + padding: 0.35rem 0.7rem; + font-size: 0.78rem; font-weight: 600; - width: 100%; - transition: all 0.3s ease; + border: 1px solid transparent; } -.webhook-url { - font-size: 0.85em; - color: #555; - margin-top: 0.5rem; +.status-submitted { + background: #eff6ff; + color: #1d4ed8; + border-color: #bfdbfe; } -.history-table-container { - overflow-x: auto; - background: rgba(255, 255, 255, 0.4); - padding: 1rem; +.status-review { + background: #fff7ed; + color: var(--warning); + border-color: #fed7aa; +} + +.status-approved { + background: #ecfdf5; + color: var(--success); + border-color: #bbf7d0; +} + +.status-rejected { + background: #fef2f2; + color: var(--danger); + border-color: #fecaca; +} + +.status-muted { + background: #f8fafc; + color: var(--muted); + border-color: var(--border); +} + +.form-label { + font-weight: 600; + margin-bottom: 0.45rem; +} + +.form-control, +.form-select { + min-height: 2.9rem; + border-color: var(--border-strong); border-radius: 12px; - border: 1px solid rgba(255, 255, 255, 0.3); + padding: 0.7rem 0.9rem; + background-color: #fff; } -.history-table { - width: 100%; +textarea.form-control { + min-height: 120px; } -.history-table-time { - width: 15%; - white-space: nowrap; - font-size: 0.85em; - color: #555; +.form-control:focus, +.form-select:focus, +.btn:focus, +.nav-link:focus, +.filter-pill:focus { + border-color: rgba(15, 118, 110, 0.45); + box-shadow: 0 0 0 0.18rem rgba(15, 118, 110, 0.13); } -.history-table-user { - width: 35%; - background: rgba(255, 255, 255, 0.3); - border-radius: 8px; - padding: 8px; +.btn { + border-radius: 12px; + padding: 0.68rem 1rem; + font-weight: 600; } -.history-table-ai { - width: 50%; - background: rgba(255, 255, 255, 0.5); - border-radius: 8px; - padding: 8px; +.btn-dark { + background: var(--primary); + border-color: var(--primary); } -.no-messages { - text-align: center; - color: #777; -} \ No newline at end of file +.btn-dark:hover, +.btn-dark:focus { + background: #0b1220; + border-color: #0b1220; +} + +.btn-outline-secondary { + color: var(--primary); + border-color: var(--border-strong); +} + +.btn-outline-secondary:hover { + background: #eef2f7; + border-color: var(--border-strong); + color: var(--primary); +} + +.detail-grid .detail-item, +.summary-row, +.notes-block, +.timeline-item { + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--surface-muted); + padding: 0.95rem 1rem; +} + +.detail-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + height: 100%; +} + +.detail-item span { + font-size: 0.84rem; + color: var(--muted); +} + +.detail-item strong, +.summary-row strong { + font-size: 0.94rem; + font-weight: 600; +} + +.timeline-list, +.summary-stack { + display: grid; + gap: 0.75rem; +} + +.timeline-item { + position: relative; +} + +.timeline-item::before { + content: ""; + position: absolute; + inset-inline-start: 0; + top: 0; + bottom: 0; + width: 3px; + border-radius: 999px; + background: var(--accent); + opacity: 0.16; +} + +.notes-block { + line-height: 1.85; +} + +.summary-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.empty-state { + border: 1px dashed var(--border-strong); + border-radius: var(--radius-md); + background: var(--surface-muted); +} + +.toast { + border-radius: 14px; + box-shadow: var(--shadow); +} + +.alert { + border-radius: 14px; +} + +.footer-title { + font-size: 0.98rem; +} + +@media (max-width: 991.98px) { + .site-header { + position: static; + } + + .hero-card, + .app-card, + .form-card, + .detail-card { + padding: 1rem; + } + + .header-actions { + margin-top: 1rem; + } +} + + +.link-card { + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 0.8rem; + min-height: 220px; +} + +.link-card p { + margin: 0; +} diff --git a/assets/js/main.js b/assets/js/main.js index d349598..cba6d93 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -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 }); + } }); diff --git a/center_application.php b/center_application.php new file mode 100644 index 0000000..8631a6d --- /dev/null +++ b/center_application.php @@ -0,0 +1,147 @@ + +
+
+
+
+
+
+
+
تقديم طلب فتح مركز صيفي
+
املأ الحقول التالية ليظهر الطلب مباشرة في لوحة المشرف العام ويبدأ مسار المراجعة.
+
+ نموذج أولي +
+ + +
+ + +
+
+
+ + +
+
+
+ + +
+
+ +
+ + +
+
+
+ + +
+
+ +
+ + +
+
+
+ + +
+
+
+ + +
+
+ +
+ + +
+
+
+ + +
+
+
+ + +
+
+ +
+ + +
+
+ +
+ + العودة إلى لوحة الطلبات +
+
+
+
+
+ + +
+
+
+
+ diff --git a/dashboard.php b/dashboard.php new file mode 100644 index 0000000..2fbdc42 --- /dev/null +++ b/dashboard.php @@ -0,0 +1,122 @@ + +
+
+
+
+
+
لوحة القيادة التشغيلية
+
هذه الصفحة مخصصة للمتابعة اليومية للمشرف العام: مؤشرات سريعة، آخر الطلبات، وروابط التحرك المباشر.
+
+ +
+
+ +
+
+
+
إجمالي الطلبات
+
+
جميع طلبات فتح المراكز
+
+
+
+
+
قيد الاستلام
+
+
بحاجة إلى بدء المراجعة
+
+
+
+
+
تحت المراجعة
+
+
نشطة داخل مسار التقييم
+
+
+
+
+
معتمد
+
+
جاهز لاستقبال الطلاب
+
+
+
+ +
+
+
+
+
+
آخر الطلبات الواردة
+
قائمة مختصرة للطلبات الأحدث حتى يتمكن المشرف العام من فتح التفاصيل بسرعة.
+
+ الانتقال إلى القائمة الكاملة +
+
+ + + + + + + + + + + + + + + + + + + + + +
المرجعالمركزالمدينةالحالةالسعة
# +
+
+
طالب
+
+
+
+
+
+
إجراءات سريعة
+
+
+

بدء طلب جديد

+

الانتقال مباشرة إلى شاشة التقديم دون المرور على تفاصيل أخرى.

+ فتح النموذج +
+
+

مراجعة شاملة

+

فتح لوحة الطلبات مع جميع السجلات وحالاتها الحالية.

+ فتح القائمة +
+
+

هيكل النظام

+

مراجعة النطاق الحالي والصفحات المفصولة ضمن النسخة الأولية.

+ فتح الصفحة +
+
+
+
+
+
+
+ diff --git a/db/migrations/20260416_center_applications.sql b/db/migrations/20260416_center_applications.sql new file mode 100644 index 0000000..2dea565 --- /dev/null +++ b/db/migrations/20260416_center_applications.sql @@ -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; diff --git a/healthz.php b/healthz.php new file mode 100644 index 0000000..0897555 --- /dev/null +++ b/healthz.php @@ -0,0 +1,22 @@ + '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); +} diff --git a/includes/app.php b/includes/app.php new file mode 100644 index 0000000..a5700d3 --- /dev/null +++ b/includes/app.php @@ -0,0 +1,493 @@ + $type, + 'message' => $message, + ]; +} + +function consume_flash(): ?array +{ + if (empty($_SESSION['flash']) || !is_array($_SESSION['flash'])) { + return null; + } + + $flash = $_SESSION['flash']; + unset($_SESSION['flash']); + + return $flash; +} + +function status_map(): array +{ + return [ + 'submitted' => ['label' => 'قيد الاستلام', 'class' => 'status-submitted'], + 'under_review' => ['label' => 'تحت المراجعة', 'class' => 'status-review'], + 'approved' => ['label' => 'معتمد', 'class' => 'status-approved'], + 'rejected' => ['label' => 'بحاجة إلى استكمال', 'class' => 'status-rejected'], + ]; +} + +function status_meta(string $status): array +{ + $map = status_map(); + return $map[$status] ?? ['label' => 'غير معروف', 'class' => 'status-muted']; +} + +function status_badge(string $status): string +{ + $meta = status_meta($status); + return '' . e($meta['label']) . ''; +} + +function db_connection(): PDO +{ + static $pdo = null; + static $bootstrapped = false; + + if (!$pdo instanceof PDO) { + $pdo = db(); + } + + if (!$bootstrapped) { + ensure_center_application_schema($pdo); + seed_center_application_demo_data($pdo); + $bootstrapped = true; + } + + return $pdo; +} + +function ensure_center_application_schema(PDO $pdo): void +{ + static $done = false; + if ($done) { + return; + } + + $migrationPath = __DIR__ . '/../db/migrations/20260416_center_applications.sql'; + if (is_file($migrationPath)) { + $sql = file_get_contents($migrationPath); + if (is_string($sql) && trim($sql) !== '') { + $pdo->exec($sql); + } + } + + $done = true; +} + +function seed_center_application_demo_data(PDO $pdo): void +{ + $count = (int) $pdo->query('SELECT COUNT(*) FROM center_applications')->fetchColumn(); + if ($count > 0) { + return; + } + + $rows = [ + [ + 'مركز النور الصيفي', 'العاصمة', 'بنين', 'طلاب', 'أ. خالد السالمي', '0501234567', 'alnoor@example.com', + 180, '2026-06-15', '2026-08-15', 'يركز على القرآن والمهارات الرقمية والأنشطة الرياضية المسائية.', + 'submitted', 'بانتظار زيارة ميدانية أولية.', null, + ], + [ + 'مركز الواحة للفتيات', 'الزور', 'بنات', 'طالبات', 'أ. نورة الشيبانية', '0507654321', 'alwaha@example.com', + 140, '2026-06-20', '2026-08-10', 'طلب تجهيز معمل حاسب وقاعة أنشطة فنية.', + 'under_review', 'تمت مراجعة الوثائق والمطلوب استكمال خطة الأمن والسلامة.', 82, + ], + [ + 'مركز الريادة المجتمعي', 'الساحل', 'مختلط', 'طلاب وطالبات', 'أ. سيف الحارثي', '0509988776', 'riyadah@example.com', + 220, '2026-06-18', '2026-08-20', 'يشمل برنامجاً علمياً ومساراً للابتكار ومتابعة أسرية.', + 'approved', 'المركز مستوفٍ للاشتراطات ويُنصح ببدء التسجيل.', 94, + ], + ]; + + $stmt = $pdo->prepare( + 'INSERT INTO center_applications ( + center_name, city, center_type, gender_scope, director_name, phone, email, + expected_students, start_date, end_date, notes, status, admin_notes, evaluation_score, + submitted_at, updated_at + ) VALUES ( + :center_name, :city, :center_type, :gender_scope, :director_name, :phone, :email, + :expected_students, :start_date, :end_date, :notes, :status, :admin_notes, :evaluation_score, + NOW(), NOW() + )' + ); + + foreach ($rows as $row) { + $stmt->execute([ + ':center_name' => $row[0], + ':city' => $row[1], + ':center_type' => $row[2], + ':gender_scope' => $row[3], + ':director_name' => $row[4], + ':phone' => $row[5], + ':email' => $row[6], + ':expected_students' => $row[7], + ':start_date' => $row[8], + ':end_date' => $row[9], + ':notes' => $row[10], + ':status' => $row[11], + ':admin_notes' => $row[12], + ':evaluation_score' => $row[13], + ]); + } +} + +function clean_text(string $value, int $limit = 255): string +{ + $normalized = preg_replace('/\s+/u', ' ', trim($value)); + if (!is_string($normalized)) { + $normalized = trim($value); + } + + if (function_exists('mb_substr')) { + return mb_substr($normalized, 0, $limit); + } + + return substr($normalized, 0, $limit); +} + +function application_defaults(): array +{ + return [ + 'center_name' => '', + 'city' => '', + 'center_type' => '', + 'gender_scope' => '', + 'director_name' => '', + 'phone' => '', + 'email' => '', + 'expected_students' => '', + 'start_date' => '', + 'end_date' => '', + 'notes' => '', + ]; +} + +function validate_application_input(array $input): array +{ + $data = application_defaults(); + foreach ($data as $key => $_value) { + $data[$key] = clean_text((string) ($input[$key] ?? ''), $key === 'notes' ? 1000 : 190); + } + + $errors = []; + + if ($data['center_name'] === '') { + $errors['center_name'] = 'يرجى إدخال اسم المركز.'; + } + if ($data['city'] === '') { + $errors['city'] = 'يرجى اختيار المدينة أو الولاية الفرعية.'; + } + if ($data['center_type'] === '') { + $errors['center_type'] = 'يرجى تحديد نوع المركز.'; + } + if ($data['gender_scope'] === '') { + $errors['gender_scope'] = 'يرجى تحديد الفئة المستهدفة.'; + } + if ($data['director_name'] === '') { + $errors['director_name'] = 'يرجى إدخال اسم مدير أو مديرة المركز.'; + } + if ($data['phone'] === '') { + $errors['phone'] = 'يرجى إدخال رقم الهاتف.'; + } + if ($data['email'] === '' || !filter_var($data['email'], FILTER_VALIDATE_EMAIL)) { + $errors['email'] = 'يرجى إدخال بريد إلكتروني صحيح.'; + } + + $expectedStudents = filter_var($input['expected_students'] ?? null, FILTER_VALIDATE_INT, [ + 'options' => ['min_range' => 10, 'max_range' => 2000], + ]); + if ($expectedStudents === false) { + $errors['expected_students'] = 'أدخل عدداً صحيحاً بين 10 و2000.'; + } else { + $data['expected_students'] = (string) $expectedStudents; + } + + $startDate = clean_text((string) ($input['start_date'] ?? ''), 20); + $endDate = clean_text((string) ($input['end_date'] ?? ''), 20); + $data['start_date'] = $startDate; + $data['end_date'] = $endDate; + + if ($startDate === '') { + $errors['start_date'] = 'حدد تاريخ بداية البرنامج.'; + } + if ($endDate === '') { + $errors['end_date'] = 'حدد تاريخ نهاية البرنامج.'; + } + if ($startDate !== '' && $endDate !== '' && strtotime($endDate) < strtotime($startDate)) { + $errors['end_date'] = 'يجب أن يكون تاريخ النهاية بعد البداية.'; + } + + return [$data, $errors]; +} + +function create_application(array $data): int +{ + $pdo = db_connection(); + $stmt = $pdo->prepare( + '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() + )' + ); + + $stmt->execute([ + ':center_name' => $data['center_name'], + ':city' => $data['city'], + ':center_type' => $data['center_type'], + ':gender_scope' => $data['gender_scope'], + ':director_name' => $data['director_name'], + ':phone' => $data['phone'], + ':email' => $data['email'], + ':expected_students' => (int) $data['expected_students'], + ':start_date' => $data['start_date'], + ':end_date' => $data['end_date'], + ':notes' => $data['notes'], + ':status' => 'submitted', + ]); + + return (int) $pdo->lastInsertId(); +} + +function list_applications(string $status = 'all'): array +{ + $pdo = db_connection(); + if ($status === 'all' || !array_key_exists($status, status_map())) { + $stmt = $pdo->query('SELECT * FROM center_applications ORDER BY submitted_at DESC, id DESC'); + return $stmt->fetchAll(); + } + + $stmt = $pdo->prepare('SELECT * FROM center_applications WHERE status = :status ORDER BY submitted_at DESC, id DESC'); + $stmt->execute([':status' => $status]); + return $stmt->fetchAll(); +} + +function get_application(int $id): ?array +{ + $pdo = db_connection(); + $stmt = $pdo->prepare('SELECT * FROM center_applications WHERE id = :id LIMIT 1'); + $stmt->execute([':id' => $id]); + $row = $stmt->fetch(); + return $row ?: null; +} + +function update_application_review(int $id, string $status, string $adminNotes, ?int $evaluationScore): void +{ + $allowed = array_keys(status_map()); + if (!in_array($status, $allowed, true)) { + throw new InvalidArgumentException('Invalid status value.'); + } + + $pdo = db_connection(); + $stmt = $pdo->prepare( + 'UPDATE center_applications + SET status = :status, admin_notes = :admin_notes, evaluation_score = :evaluation_score, updated_at = NOW() + WHERE id = :id' + ); + $stmt->bindValue(':status', $status, PDO::PARAM_STR); + $stmt->bindValue(':admin_notes', $adminNotes !== '' ? $adminNotes : null, $adminNotes !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL); + $stmt->bindValue(':evaluation_score', $evaluationScore, $evaluationScore !== null ? PDO::PARAM_INT : PDO::PARAM_NULL); + $stmt->bindValue(':id', $id, PDO::PARAM_INT); + $stmt->execute(); +} + +function dashboard_metrics(): array +{ + $pdo = db_connection(); + $totals = [ + 'all' => 0, + 'submitted' => 0, + 'under_review' => 0, + 'approved' => 0, + 'rejected' => 0, + 'expected_students' => 0, + ]; + + $summary = $pdo->query('SELECT status, COUNT(*) AS total FROM center_applications GROUP BY status')->fetchAll(); + foreach ($summary as $row) { + $status = (string) ($row['status'] ?? ''); + $count = (int) ($row['total'] ?? 0); + if (array_key_exists($status, $totals)) { + $totals[$status] = $count; + $totals['all'] += $count; + } + } + + $totals['expected_students'] = (int) $pdo->query('SELECT COALESCE(SUM(expected_students), 0) FROM center_applications')->fetchColumn(); + + return $totals; +} + +function latest_applications(int $limit = 5): array +{ + $pdo = db_connection(); + $stmt = $pdo->prepare('SELECT * FROM center_applications ORDER BY submitted_at DESC, id DESC LIMIT :limit'); + $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); + $stmt->execute(); + return $stmt->fetchAll(); +} + +function render_page_start(string $pageTitle, string $active = 'home', string $pageDescription = ''): void +{ + $projectName = project_name(); + $description = $pageDescription !== '' ? $pageDescription : project_description(); + $projectImageUrl = env_value('PROJECT_IMAGE_URL'); + ?> + + + + + + <?= e($pageTitle) ?> | <?= e($projectName) ?> + + + + + + + + + + + + + + + + + +
+ +
+ 'text-bg-success', + 'error' => 'text-bg-danger', + default => 'text-bg-dark', + }; + ?> +
+
+
+
+ +
+
+
+ +
+ +
+ + + + + - - - - - - New Style - - - - - - - - - - - - - - - - - - - - - -
-
-

Analyzing your requirements and generating your website…

-
- Loading… +
+
+
+
+
+
واجهة رئيسية منظمة للمشرف العام
+

كل مهمة في صفحة مستقلة وواضحة.

+

تم تنظيم النسخة الحالية بحيث تكون الصفحة الرئيسية نقطة دخول فقط، بينما توجد صفحة منفصلة للمتابعة التشغيلية، وصفحة مستقلة للطلبات، وصفحة خاصة ببنية النظام والنطاق الحالي.

+ +
+ تنظيم أفضل للشاشات + واجهة عربية RTL + جاهزة للتوسع إلى التسجيل والحضور +
+
+
+
+
+
+
+
إجمالي الطلبات
+
+
إجمالي الطلبات المسجلة
+
+
+
+
+
المعتمد
+
+
مراكز جاهزة للتشغيل
+
+
+
+
+
تحت المراجعة
+
+
طلبات تحتاج متابعة
+
+
+
+
+
الطاقة المقترحة
+
+
عدد الطلاب المتوقع
+
+
+
-

AI is collecting your requirements and applying the first changes.

-

This page will update automatically as the plan is implemented.

-

Runtime: PHP — UTC

-
- - - + + + +
+
+
+
+
الوصول السريع حسب المهمة
+
بدلاً من خلط كل شيء في صفحة واحدة، كل جزء أصبح له شاشة مستقلة ليسهل على المستخدم الوصول والتنقل.
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ diff --git a/modules.php b/modules.php new file mode 100644 index 0000000..cfb2f78 --- /dev/null +++ b/modules.php @@ -0,0 +1,69 @@ + +
+
+
+
هيكل الصفحات والوحدات
+

تم فصل المحتوى التعريفي والتنظيمي في هذه الصفحة حتى تبقى الصفحات التشغيلية مركزة: التقديم في شاشة، المتابعة في شاشة، والتفاصيل في شاشة مستقلة.

+
+ +
+
+
+
النطاق الحالي في هذه النسخة
+
+
+

1. الرئيسية

+

صفحة دخول مرتبة مع بطاقات وصول سريع إلى كل جزء من النظام دون خلط التفاصيل التشغيلية.

+
+
+

2. لوحة القيادة

+

شاشة متابعة يومية تعرض المؤشرات وآخر الطلبات الواردة للمشرف العام.

+
+
+

3. طلب فتح مركز

+

نموذج عربي كامل مع تحقق للحقول الأساسية والطاقة التشغيلية ومواعيد البرنامج.

+
+
+

4. لوحة الطلبات

+

قائمة موحدة مع فرز بالحالة لتسهيل العمل الإشرافي والانتقال السريع للتفاصيل.

+
+
+

5. صفحة المراجعة التفصيلية

+

عرض بيانات المركز، تسجيل الملاحظات، تحديث الحالة، وإسناد التقييم الأولي.

+
+
+
+
+
+
+
مسار الاستخدام الحالي
+
    +
  1. يرسل مدير المركز طلب الافتتاح من شاشة مستقلة.
  2. +
  3. تظهر الطلبات فوراً داخل لوحة الطلبات ولوحة القيادة.
  4. +
  5. يفتح المشرف العام صفحة التفاصيل لإضافة الملاحظات والتقييم.
  6. +
  7. تُحدث الحالة إلى قيد الاستلام أو تحت المراجعة أو معتمد أو بحاجة إلى استكمال.
  8. +
+ +
ما الذي تم تحسينه؟
+

أصبح التنظيم أوضح لأن كل صفحة الآن تؤدي غرضاً واحداً محدداً، وهذا أسهل للتوسع لاحقاً إلى التسجيل الطلابي، الحضور، والتقييمات التشغيلية.

+ +
المرحلة التالية المقترحة
+

بعد هذا التنظيم، الخطوة الطبيعية التالية هي بناء وحدات الطلاب والتسجيل في المراكز المعتمدة مع نفس المبدأ: صفحة مستقلة لكل وظيفة.

+ +
+
+
+
+
+