updating cycle system
This commit is contained in:
parent
c9ac266bd6
commit
f43e1b9751
@ -4,7 +4,11 @@ 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);
|
||||
@ -26,6 +30,91 @@ if (!$application) {
|
||||
}
|
||||
|
||||
$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 = 'تم إنشاء الدورة الموسمية الجديدة بنجاح. يمكنك الآن العمل داخل ' . format_school_cycle_name($cycleValues['season'], (int) $cycleValues['year']) . '.';
|
||||
$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']);
|
||||
@ -34,9 +123,19 @@ if ($startTs !== false && $endTs !== false && $endTs >= $startTs) {
|
||||
$durationDays = (int) floor(($endTs - $startTs) / 86400) + 1;
|
||||
}
|
||||
|
||||
$pageTitle = $isApproved ? 'صفحة المركز المعتمد: ' . (string) $application['center_name'] : 'صفحة المركز بعد الاعتماد';
|
||||
$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);
|
||||
$approvedSchoolUrl = school_page_url('approved_school.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);
|
||||
@ -76,8 +175,10 @@ render_flash($flash);
|
||||
</div>
|
||||
<div class="cta-stack mt-4">
|
||||
<a class="btn btn-dark" href="application_detail.php?id=<?= e((string) $application['id']) ?>">فتح ملف الاعتماد</a>
|
||||
<a class="btn btn-outline-secondary" href="students.php?id=<?= e((string) $application['id']) ?>">تسجيل الطلاب</a>
|
||||
<a class="btn btn-outline-secondary" href="teachers.php?id=<?= e((string) $application['id']) ?>">فريق المعلمين</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($attendanceUrl) ?>">غياب الطلاب</a>
|
||||
<a class="btn btn-outline-secondary" href="applications.php?status=approved">كل المراكز المعتمدة</a>
|
||||
<a class="btn btn-outline-secondary" href="dashboard.php">لوحة القيادة</a>
|
||||
</div>
|
||||
@ -106,6 +207,129 @@ render_flash($flash);
|
||||
<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) $application['id']) ?></div><div class="mini-stat-copy">استخدم هذا الرقم في أي متابعة إدارية لاحقة.</div></div></div>
|
||||
</div>
|
||||
|
||||
<?php if ($selectedCycle): ?>
|
||||
<div class="row g-4 mb-4" id="cycles">
|
||||
<div class="col-lg-8">
|
||||
<div class="app-card h-100">
|
||||
<div class="section-head mb-3">
|
||||
<div>
|
||||
<div class="section-title">الدورات الموسمية والأرشفة</div>
|
||||
<div class="section-copy">كل الطلاب والمعلمين والتقييمات والغياب أصبحت الآن مرتبطة بدورة مستقلة مثل <strong>Summer 2026</strong> أو <strong>Winter 2026</strong>. عند نهاية الموسم قم بأرشفة الدورة الحالية ثم ابدأ دورة جديدة للحفاظ على التاريخ بدون خلط السجلات.</div>
|
||||
</div>
|
||||
<?= school_cycle_status_badge((string) $selectedCycle['status']) ?>
|
||||
</div>
|
||||
<div class="row g-3 mb-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) school_cycle_status_map()[$selectedCycle['status']]['label']) ?></span></div></div>
|
||||
<div class="col-md-3"><div class="school-data-item"><strong>الطلاب</strong><span><?= e((string) $studentCycleMetrics['total']) ?></span></div></div>
|
||||
<div class="col-md-3"><div class="school-data-item"><strong>الفريق</strong><span><?= e((string) $teacherCycleMetrics['total']) ?></span></div></div>
|
||||
<div class="col-md-3"><div class="school-data-item"><strong>التقييمات النشطة</strong><span><?= e((string) $assessmentCycleMetrics['active']) ?></span></div></div>
|
||||
<div class="col-md-3"><div class="school-data-item"><strong>سجلات الغياب</strong><span><?= e((string) $attendanceCycleMetrics['total']) ?></span></div></div>
|
||||
</div>
|
||||
<div class="cta-stack">
|
||||
<a class="btn btn-dark" 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($attendanceUrl) ?>">غياب هذه الدورة</a>
|
||||
<?php if (!$isCycleReadOnly): ?>
|
||||
<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-outline-danger" type="submit">أرشفة هذه الدورة</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if ($isCycleReadOnly): ?>
|
||||
<div class="alert alert-warning mt-3 mb-0">هذه الدورة مؤرشفة حالياً، لذلك كل الصفحات المرتبطة بها أصبحت للقراءة فقط. يمكنك فتح دورة جديدة من النموذج المجاور.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<div class="app-card sidebar-card mb-4">
|
||||
<div class="section-title mb-3">كل دورات المركز</div>
|
||||
<div class="quick-link-stack">
|
||||
<?php foreach ($cycleContext['cycles'] as $cycle): ?>
|
||||
<a class="quick-link-item" href="<?= e(school_page_url('approved_school.php', (int) $application['id'], (int) $cycle['id'])) ?>#cycles"><strong><?= e((string) $cycle['cycle_name']) ?></strong><span><?= e((string) $cycle['start_date']) ?> → <?= e((string) $cycle['end_date']) ?> — <?= e((string) school_cycle_status_map()[$cycle['status']]['label']) ?></span></a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="app-card sidebar-card">
|
||||
<div class="section-title mb-3">فتح دورة جديدة</div>
|
||||
<p class="section-subtle mb-3">استخدم هذا النموذج عند نهاية الصيف أو الشتاء لبدء دورة جديدة باسم تلقائي مثل Summer 2026 أو Winter 2026.</p>
|
||||
<?php if (isset($cycleErrors['form'])): ?><div class="alert alert-danger 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" for="season">الموسم</label>
|
||||
<select class="form-select <?= isset($cycleErrors['season']) ? 'is-invalid' : '' ?>" id="season" name="season">
|
||||
<?php foreach (school_cycle_season_options() as $seasonKey => $seasonMeta): ?>
|
||||
<option value="<?= e($seasonKey) ?>" <?= $cycleValues['season'] === $seasonKey ? 'selected' : '' ?>><?= e((string) $seasonMeta['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php if (isset($cycleErrors['season'])): ?><div class="invalid-feedback"><?= e($cycleErrors['season']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" for="year">السنة</label>
|
||||
<input class="form-control <?= isset($cycleErrors['year']) ? 'is-invalid' : '' ?>" id="year" name="year" value="<?= e($cycleValues['year']) ?>" inputmode="numeric" placeholder="2026">
|
||||
<?php if (isset($cycleErrors['year'])): ?><div class="invalid-feedback"><?= e($cycleErrors['year']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="start_date">تاريخ البداية</label>
|
||||
<input class="form-control <?= isset($cycleErrors['start_date']) ? 'is-invalid' : '' ?>" id="start_date" name="start_date" type="date" value="<?= e($cycleValues['start_date']) ?>">
|
||||
<?php if (isset($cycleErrors['start_date'])): ?><div class="invalid-feedback"><?= e($cycleErrors['start_date']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="end_date">تاريخ النهاية</label>
|
||||
<input class="form-control <?= isset($cycleErrors['end_date']) ? 'is-invalid' : '' ?>" id="end_date" name="end_date" type="date" value="<?= e($cycleValues['end_date']) ?>">
|
||||
<?php if (isset($cycleErrors['end_date'])): ?><div class="invalid-feedback"><?= e($cycleErrors['end_date']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" for="status">حالة البداية</label>
|
||||
<select class="form-select <?= 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">ترحيل البيانات من دورة سابقة</div>
|
||||
<p class="small text-muted mb-3">اختر الدورة التي تريد النسخ منها. الغياب لا يُنقل أبداً، والطلاب المنقولون يقتصرون على الحالات المستمرة فقط.</p>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="source_cycle_id">النسخ من دورة</label>
|
||||
<select class="form-select" 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">
|
||||
<label class="form-check">
|
||||
<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">
|
||||
<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">
|
||||
<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-dark" type="submit">إنشاء الدورة الجديدة</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-8">
|
||||
<div class="app-card mb-4">
|
||||
@ -148,8 +372,12 @@ render_flash($flash);
|
||||
<p>صفحة الفريق أصبحت جاهزة الآن لإضافة المعلمين والمشرفين وربط أدوارهم التشغيلية مباشرة بالمركز المعتمد.</p>
|
||||
</div>
|
||||
<div class="next-step-card">
|
||||
<strong>3) متابعة التشغيل</strong>
|
||||
<p>بعد الانطلاق يمكن توسيع هذه الصفحة لاحقاً بمؤشرات حضور يومية، تقييمات، وتنبيهات تشغيلية.</p>
|
||||
<strong>3) ضبط التقييمات</strong>
|
||||
<p>صفحة التقييمات أصبحت جاهزة الآن لتحديد نوع التقييم، المقياس، والوزن قبل بدء الرصد والمتابعة.</p>
|
||||
</div>
|
||||
<div class="next-step-card">
|
||||
<strong>4) متابعة الغياب</strong>
|
||||
<p>صفحة غياب الطلاب أصبحت جاهزة الآن لتسجيل الغياب اليومي، الأعذار، وحالات التأخر لكل طالب داخل المركز.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -174,8 +402,10 @@ render_flash($flash);
|
||||
<div class="app-card sidebar-card">
|
||||
<div class="section-title mb-3">روابط تشغيلية سريعة</div>
|
||||
<div class="quick-link-stack">
|
||||
<a class="quick-link-item" href="students.php?id=<?= e((string) $application['id']) ?>"><strong>تسجيل الطلاب</strong><span>فتح صفحة القيد وكشف المدرسة لهذا المركز.</span></a>
|
||||
<a class="quick-link-item" href="teachers.php?id=<?= e((string) $application['id']) ?>"><strong>فريق المعلمين</strong><span>إدارة المعلمين والمشرفين والكوادر التشغيلية للمركز.</span></a>
|
||||
<a class="quick-link-item" href="<?= e($studentsUrl) ?>"><strong>تسجيل الطلاب</strong><span>فتح صفحة القيد وكشف المدرسة لهذا المركز.</span></a>
|
||||
<a class="quick-link-item" href="<?= e($teachersUrl) ?>"><strong>فريق المعلمين</strong><span>إدارة المعلمين والمشرفين والكوادر التشغيلية للمركز.</span></a>
|
||||
<a class="quick-link-item" href="<?= e($assessmentsUrl) ?>"><strong>التقييمات والأوزان</strong><span>تعريف أنواع التقييم، المقاييس، والأوزان التشغيلية لهذا المركز.</span></a>
|
||||
<a class="quick-link-item" href="<?= e($attendanceUrl) ?>"><strong>غياب الطلاب</strong><span>تسجيل الغياب اليومي، الأعذار، وحالات التأخر للطلاب المعتمدين.</span></a>
|
||||
<a class="quick-link-item" href="application_detail.php?id=<?= e((string) $application['id']) ?>"><strong>ملف الاعتماد</strong><span>العودة إلى سجل القرار والتقييم.</span></a>
|
||||
<a class="quick-link-item" href="applications.php?status=approved"><strong>المراكز المعتمدة</strong><span>عرض بقية المراكز الجاهزة للتشغيل.</span></a>
|
||||
<a class="quick-link-item" href="dashboard.php"><strong>لوحة القيادة</strong><span>الرجوع إلى المؤشرات العامة على مستوى الولاية.</span></a>
|
||||
|
||||
370
assessments.php
Normal file
370
assessments.php
Normal file
@ -0,0 +1,370 @@
|
||||
<?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 = 'لا توجد دورة بعد';
|
||||
|
||||
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) {
|
||||
[$values, $errors] = validate_assessment_input($_POST);
|
||||
|
||||
if (!$isApprovedSchool) {
|
||||
$errors['form'] = 'لا يمكن إعداد التقييمات قبل اعتماد المركز.';
|
||||
} elseif ($selectedCycleId <= 0) {
|
||||
$errors['form'] = 'يرجى إنشاء دورة موسمية أولاً من صفحة المركز.';
|
||||
} elseif ($isCycleReadOnly) {
|
||||
$errors['form'] = 'هذه الدورة مؤرشفة للقراءة فقط. افتح دورة جديدة أو اختر دورة نشطة لإضافة تقييمات جديدة.';
|
||||
}
|
||||
|
||||
if ($errors === []) {
|
||||
try {
|
||||
create_assessment_type_in_cycle((int) $application['id'], $selectedCycleId, $values);
|
||||
set_flash('success', 'تم حفظ نوع التقييم داخل الدورة الموسمية المحددة.');
|
||||
header('Location: ' . school_page_url('assessments.php', (int) $application['id'], $selectedCycleId));
|
||||
exit;
|
||||
} catch (Throwable $exception) {
|
||||
$errors['form'] = 'تعذر حفظ نوع التقييم حالياً. يرجى المحاولة مرة أخرى.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$assessments = $isApprovedSchool && $selectedCycleId > 0 ? list_school_assessments_by_cycle((int) $application['id'], $selectedCycleId) : [];
|
||||
$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,
|
||||
];
|
||||
$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,
|
||||
'email_ready' => 0,
|
||||
'supervisors' => 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';
|
||||
$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';
|
||||
$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);
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="py-4 py-lg-5">
|
||||
<div class="container-xxl">
|
||||
<?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-dark" 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-dark" 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(number_format($activeWeight, 2, '.', '')) ?>%</span>
|
||||
</div>
|
||||
<div class="cta-stack mt-4">
|
||||
<a class="btn btn-dark" 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($attendanceUrl) ?>">غياب الطلاب</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 (abs($weightGap) < 0.01): ?>
|
||||
<div class="alert alert-success mb-3">ممتاز — الأوزان المفعلة تساوي 100% وجاهزة للاستخدام.</div>
|
||||
<?php elseif ($activeWeight > 100): ?>
|
||||
<div class="alert alert-danger mb-3">مجموع الأوزان المفعلة يتجاوز 100% بمقدار <?= e(number_format(abs($weightGap), 2, '.', '')) ?>%. يحتاج إلى إعادة موازنة.</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-warning mb-3">المجموع الحالي للأوزان المفعلة هو <?= e(number_format($activeWeight, 2, '.', '')) ?>%، والمتبقي <?= e(number_format(max(0, $weightGap), 2, '.', '')) ?>% لاستكمال الخطة.</div>
|
||||
<?php endif; ?>
|
||||
<div class="summary-stack">
|
||||
<div class="summary-row"><span>التقييمات المفعلة</span><strong><?= e((string) $metrics['active']) ?></strong></div>
|
||||
<div class="summary-row"><span>متوسط الدرجة القصوى</span><strong><?= e(number_format((float) $metrics['average_max_score'], 2, '.', '')) ?></strong></div>
|
||||
<div class="summary-row"><span>فريق التدريس</span><strong><?= e((string) $teacherMetrics['total']) ?> عضو</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-7">
|
||||
<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 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>
|
||||
<div class="cta-stack mt-3">
|
||||
<a class="btn btn-outline-secondary" href="<?= e($approvedSchoolUrl) ?>#cycles">إدارة الدورات الموسمية</a>
|
||||
</div>
|
||||
<?php if ($isCycleReadOnly): ?>
|
||||
<div class="alert alert-warning mt-3 mb-0">هذه الدورة مؤرشفة، لذلك تبقى خطة التقييم للقراءة فقط حالياً.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<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('assessments.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>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-4">
|
||||
<div class="app-card sidebar-card mb-4">
|
||||
<div class="section-title mb-3">إضافة نوع تقييم جديد</div>
|
||||
<p class="section-subtle mb-3">أدخل نوع التقييم مرة واحدة، ثم استخدم القائمة في الأسفل كمرجع للخطة الأكاديمية الخاصة بالمركز.</p>
|
||||
|
||||
<?php if (isset($errors['form'])): ?>
|
||||
<div class="alert alert-danger"><?= e($errors['form']) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($isCycleReadOnly): ?>
|
||||
<div class="alert alert-warning mb-0">هذه الدورة مؤرشفة. يمكنك مراجعة خطة التقييم فقط، أو فتح دورة جديدة من صفحة المركز.</div>
|
||||
<?php else: ?>
|
||||
<form method="post" class="vstack gap-3" novalidate>
|
||||
<div>
|
||||
<label class="form-label" for="title">اسم نوع التقييم</label>
|
||||
<input class="form-control <?= isset($errors['title']) ? 'is-invalid' : '' ?>" id="title" name="title" value="<?= e($values['title']) ?>" placeholder="مثال: اختبار دوري أول">
|
||||
<?php if (isset($errors['title'])): ?><div class="invalid-feedback"><?= e($errors['title']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="category">الفئة</label>
|
||||
<select class="form-select <?= isset($errors['category']) ? 'is-invalid' : '' ?>" id="category" name="category">
|
||||
<?php foreach (assessment_category_options() as $option): ?>
|
||||
<option value="<?= e($option) ?>" <?= $values['category'] === $option ? 'selected' : '' ?>><?= e($option) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<?php if (isset($errors['category'])): ?><div class="invalid-feedback"><?= e($errors['category']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label" for="scale_type">المقياس</label>
|
||||
<select class="form-select <?= isset($errors['scale_type']) ? 'is-invalid' : '' ?>" id="scale_type" name="scale_type">
|
||||
<?php foreach (assessment_scale_type_map() as $key => $meta): ?>
|
||||
<option value="<?= e($key) ?>" <?= $values['scale_type'] === $key ? 'selected' : '' ?>><?= e((string) $meta['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-6">
|
||||
<label class="form-label" for="max_score">الدرجة النهائية</label>
|
||||
<input type="number" step="0.01" min="0" max="1000" class="form-control <?= isset($errors['max_score']) ? 'is-invalid' : '' ?>" id="max_score" name="max_score" value="<?= e($values['max_score']) ?>">
|
||||
<?php if (isset($errors['max_score'])): ?><div class="invalid-feedback"><?= e($errors['max_score']) ?></div><?php endif; ?>
|
||||
</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 <?= isset($errors['weight_percentage']) ? 'is-invalid' : '' ?>" id="weight_percentage" name="weight_percentage" value="<?= e($values['weight_percentage']) ?>">
|
||||
<?php if (isset($errors['weight_percentage'])): ?><div class="invalid-feedback"><?= e($errors['weight_percentage']) ?></div><?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="form-label" for="is_active">حالة التفعيل</label>
|
||||
<select class="form-select" id="is_active" name="is_active">
|
||||
<option value="1" <?= $values['is_active'] === '1' ? 'selected' : '' ?>>مفعل داخل الخطة</option>
|
||||
<option value="0" <?= $values['is_active'] === '0' ? 'selected' : '' ?>>مؤرشف / تحضيري</option>
|
||||
</select>
|
||||
</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-dark" 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><div class="mt-2"><a class="btn btn-sm btn-outline-secondary" href="<?= e($attendanceUrl) ?>">فتح صفحة الغياب</a></div></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['percentage']) ?> نسبي / <?= e((string) $metrics['rubric']) ?> Rubric</span>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($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><?= 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><?= assessment_active_badge((int) $assessment['is_active']) ?></td>
|
||||
<td><?= e(substr((string) $assessment['created_at'], 0, 10)) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?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) $application['director_name']) ?></span></div></div>
|
||||
<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) $metrics['points']) ?> نوع</span></div></div>
|
||||
</div>
|
||||
<div class="cta-stack mt-4">
|
||||
<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($attendanceUrl) ?>">فتح الغياب</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($approvedSchoolUrl) ?>">صفحة المركز</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php render_page_end();
|
||||
@ -706,6 +706,16 @@ textarea.form-control {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.quick-link-item.is-current {
|
||||
border-color: rgba(15, 118, 110, 0.32);
|
||||
background: linear-gradient(135deg, #ffffff 0%, var(--accent-soft) 100%);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.quick-link-item.is-current strong {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
372
attendance.php
Normal file
372
attendance.php
Normal file
@ -0,0 +1,372 @@
|
||||
<?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) : [];
|
||||
$records = $isApprovedSchool && $selectedCycleId > 0 ? list_school_attendance_records_by_cycle((int) $application['id'], $selectedCycleId) : [];
|
||||
$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);
|
||||
render_flash($flash);
|
||||
?>
|
||||
<section class="py-4 py-lg-5">
|
||||
<div class="container-xxl">
|
||||
<?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-dark" 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-dark" 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-dark" 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-7">
|
||||
<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">
|
||||
<a class="btn btn-outline-secondary" href="<?= e($approvedSchoolUrl) ?>#cycles">إدارة الدورات الموسمية</a>
|
||||
</div>
|
||||
<?php if ($isCycleReadOnly): ?>
|
||||
<div class="alert alert-warning mt-3 mb-0">هذه الدورة مؤرشفة، لذلك تبقى صفحة الغياب للقراءة فقط حالياً.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</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-dark" 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>
|
||||
|
||||
<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 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>
|
||||
</section>
|
||||
<?php render_page_end();
|
||||
18
db/migrations/20260416_school_cycles.sql
Normal file
18
db/migrations/20260416_school_cycles.sql
Normal file
@ -0,0 +1,18 @@
|
||||
CREATE TABLE IF NOT EXISTS school_cycles (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
center_application_id INT UNSIGNED NOT NULL,
|
||||
season VARCHAR(20) NOT NULL,
|
||||
year SMALLINT UNSIGNED NOT NULL,
|
||||
cycle_name VARCHAR(80) NOT NULL,
|
||||
start_date DATE NOT NULL,
|
||||
end_date DATE NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'upcoming',
|
||||
archived_at DATETIME NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY uniq_school_cycle_center_season_year (center_application_id, season, year),
|
||||
INDEX idx_school_cycles_center (center_application_id),
|
||||
INDEX idx_school_cycles_status (status),
|
||||
INDEX idx_school_cycles_dates (start_date, end_date),
|
||||
CONSTRAINT fk_school_cycles_center_application FOREIGN KEY (center_application_id) REFERENCES center_applications(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
453
includes/app.php
453
includes/app.php
@ -6,6 +6,7 @@ if (session_status() === PHP_SESSION_NONE) {
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
require_once __DIR__ . '/cycles.php';
|
||||
|
||||
function env_value(string $key, string $default = ''): string
|
||||
{
|
||||
@ -96,8 +97,9 @@ function db_connection(): PDO
|
||||
|
||||
if (!$bootstrapped) {
|
||||
ensure_center_application_schema($pdo);
|
||||
ensure_school_module_schema($pdo);
|
||||
seed_center_application_demo_data($pdo);
|
||||
ensure_school_module_schema($pdo);
|
||||
ensure_school_cycle_schema($pdo);
|
||||
seed_school_module_demo_data($pdo);
|
||||
$bootstrapped = true;
|
||||
}
|
||||
@ -143,11 +145,19 @@ function ensure_school_module_schema(PDO $pdo): void
|
||||
|
||||
function seed_school_module_demo_data(PDO $pdo): void
|
||||
{
|
||||
$approvedId = (int) $pdo->query("SELECT id FROM center_applications WHERE status = 'approved' ORDER BY id ASC LIMIT 1")->fetchColumn();
|
||||
$approvedStmt = $pdo->query("SELECT * FROM center_applications WHERE status = 'approved' ORDER BY id ASC LIMIT 1");
|
||||
$approvedApplication = $approvedStmt ? ($approvedStmt->fetch() ?: null) : null;
|
||||
if (!$approvedApplication) {
|
||||
return;
|
||||
}
|
||||
|
||||
$approvedId = (int) ($approvedApplication['id'] ?? 0);
|
||||
if ($approvedId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$defaultCycleId = ensure_default_school_cycle_record($pdo, $approvedApplication);
|
||||
|
||||
$studentCount = (int) $pdo->query('SELECT COUNT(*) FROM school_students')->fetchColumn();
|
||||
if ($studentCount === 0) {
|
||||
$studentRows = [
|
||||
@ -158,11 +168,11 @@ function seed_school_module_demo_data(PDO $pdo): void
|
||||
|
||||
$studentStmt = $pdo->prepare(
|
||||
'INSERT INTO school_students (
|
||||
center_application_id, student_code, full_name, gender, grade_level,
|
||||
center_application_id, cycle_id, student_code, full_name, gender, grade_level,
|
||||
guardian_name, guardian_phone, birth_date, enrollment_status, notes,
|
||||
created_at, updated_at
|
||||
) VALUES (
|
||||
:center_application_id, :student_code, :full_name, :gender, :grade_level,
|
||||
:center_application_id, :cycle_id, :student_code, :full_name, :gender, :grade_level,
|
||||
:guardian_name, :guardian_phone, :birth_date, :enrollment_status, :notes,
|
||||
NOW(), NOW()
|
||||
)'
|
||||
@ -171,6 +181,7 @@ function seed_school_module_demo_data(PDO $pdo): void
|
||||
foreach ($studentRows as $row) {
|
||||
$studentStmt->execute([
|
||||
':center_application_id' => $approvedId,
|
||||
':cycle_id' => $defaultCycleId,
|
||||
':student_code' => $row[0],
|
||||
':full_name' => $row[1],
|
||||
':gender' => $row[2],
|
||||
@ -194,11 +205,11 @@ function seed_school_module_demo_data(PDO $pdo): void
|
||||
|
||||
$teacherStmt = $pdo->prepare(
|
||||
'INSERT INTO school_teachers (
|
||||
center_application_id, full_name, role_title, specialization,
|
||||
center_application_id, cycle_id, full_name, role_title, specialization,
|
||||
phone, email, employment_status, notes,
|
||||
created_at, updated_at
|
||||
) VALUES (
|
||||
:center_application_id, :full_name, :role_title, :specialization,
|
||||
:center_application_id, :cycle_id, :full_name, :role_title, :specialization,
|
||||
:phone, :email, :employment_status, :notes,
|
||||
NOW(), NOW()
|
||||
)'
|
||||
@ -207,6 +218,7 @@ function seed_school_module_demo_data(PDO $pdo): void
|
||||
foreach ($teacherRows as $row) {
|
||||
$teacherStmt->execute([
|
||||
':center_application_id' => $approvedId,
|
||||
':cycle_id' => $defaultCycleId,
|
||||
':full_name' => $row[0],
|
||||
':role_title' => $row[1],
|
||||
':specialization' => $row[2],
|
||||
@ -217,6 +229,99 @@ function seed_school_module_demo_data(PDO $pdo): void
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$assessmentCount = (int) $pdo->query('SELECT COUNT(*) FROM school_assessment_types')->fetchColumn();
|
||||
if ($assessmentCount === 0) {
|
||||
$assessmentRows = [
|
||||
['الاختبار التشخيصي', 'تشخيصي', 'percentage', 100.00, 10.00, 1, 'يقيس مستوى الانطلاق في بداية البرنامج.'],
|
||||
['المشاركة الصفية', 'مشاركة', 'points', 20.00, 15.00, 1, 'متابعة التفاعل اليومي والانضباط داخل الحلقة أو الصف.'],
|
||||
['المشروع التطبيقي', 'مشاريع', 'rubric', 4.00, 35.00, 1, 'يقيس التطبيق العملي والابتكار والعمل الجماعي.'],
|
||||
['الاختبار الختامي', 'ختامي', 'percentage', 100.00, 40.00, 1, 'يعكس المخرجات النهائية ونسبة الإتقان العامة.'],
|
||||
];
|
||||
|
||||
$assessmentStmt = $pdo->prepare(
|
||||
'INSERT INTO school_assessment_types (
|
||||
center_application_id, cycle_id, title, category, scale_type,
|
||||
max_score, weight_percentage, is_active, notes,
|
||||
created_at, updated_at
|
||||
) VALUES (
|
||||
:center_application_id, :cycle_id, :title, :category, :scale_type,
|
||||
:max_score, :weight_percentage, :is_active, :notes,
|
||||
NOW(), NOW()
|
||||
)'
|
||||
);
|
||||
|
||||
foreach ($assessmentRows as $row) {
|
||||
$assessmentStmt->execute([
|
||||
':center_application_id' => $approvedId,
|
||||
':cycle_id' => $defaultCycleId,
|
||||
':title' => $row[0],
|
||||
':category' => $row[1],
|
||||
':scale_type' => $row[2],
|
||||
':max_score' => $row[3],
|
||||
':weight_percentage' => $row[4],
|
||||
':is_active' => $row[5],
|
||||
':notes' => $row[6],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$attendanceCount = (int) $pdo->query('SELECT COUNT(*) FROM school_attendance_records')->fetchColumn();
|
||||
if ($attendanceCount === 0) {
|
||||
$studentStmt = $pdo->prepare(
|
||||
'SELECT id FROM school_students
|
||||
WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id
|
||||
ORDER BY id ASC'
|
||||
);
|
||||
$studentStmt->execute([
|
||||
':center_application_id' => $approvedId,
|
||||
':cycle_id' => $defaultCycleId,
|
||||
]);
|
||||
$studentRows = $studentStmt->fetchAll();
|
||||
|
||||
if ($studentRows !== []) {
|
||||
$studentIds = [];
|
||||
foreach ($studentRows as $studentRow) {
|
||||
$studentIds[] = (int) ($studentRow['id'] ?? 0);
|
||||
}
|
||||
|
||||
$attendanceRows = [];
|
||||
if (isset($studentIds[0])) {
|
||||
$attendanceRows[] = [$studentIds[0], '2026-04-13', 'absent', 'ظرف صحي', 'تم التواصل مع ولي الأمر والمتابعة جارية.'];
|
||||
$attendanceRows[] = [$studentIds[0], '2026-04-15', 'late', 'تأخر في المواصلات', 'دخل بعد بداية الفترة الأولى بـ 15 دقيقة.'];
|
||||
}
|
||||
if (isset($studentIds[1])) {
|
||||
$attendanceRows[] = [$studentIds[1], '2026-04-14', 'excused', 'موعد عائلي مسبق', 'غياب بعذر وتمت مشاركة الواجبات مع الطالبة.'];
|
||||
}
|
||||
if (isset($studentIds[2])) {
|
||||
$attendanceRows[] = [$studentIds[2], '2026-04-15', 'absent', 'لم يتم الرد على الاتصال', 'يحتاج إلى متابعة إضافية بسبب تكرر الغياب.'];
|
||||
}
|
||||
|
||||
if ($attendanceRows !== []) {
|
||||
$attendanceStmt = $pdo->prepare(
|
||||
'INSERT INTO school_attendance_records (
|
||||
center_application_id, cycle_id, student_id, attendance_date, attendance_status,
|
||||
absence_reason, notes, created_at, updated_at
|
||||
) VALUES (
|
||||
:center_application_id, :cycle_id, :student_id, :attendance_date, :attendance_status,
|
||||
:absence_reason, :notes, NOW(), NOW()
|
||||
)'
|
||||
);
|
||||
|
||||
foreach ($attendanceRows as $row) {
|
||||
$attendanceStmt->execute([
|
||||
':center_application_id' => $approvedId,
|
||||
':cycle_id' => $defaultCycleId,
|
||||
':student_id' => $row[0],
|
||||
':attendance_date' => $row[1],
|
||||
':attendance_status' => $row[2],
|
||||
':absence_reason' => $row[3],
|
||||
':notes' => $row[4],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function seed_center_application_demo_data(PDO $pdo): void
|
||||
@ -774,6 +879,342 @@ function school_teacher_metrics(int $centerApplicationId): array
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
function assessment_defaults(): array
|
||||
{
|
||||
return [
|
||||
'title' => '',
|
||||
'category' => 'اختبار قصير',
|
||||
'scale_type' => 'percentage',
|
||||
'max_score' => '100',
|
||||
'weight_percentage' => '10',
|
||||
'is_active' => '1',
|
||||
'notes' => '',
|
||||
];
|
||||
}
|
||||
|
||||
function assessment_category_options(): array
|
||||
{
|
||||
return [
|
||||
'تشخيصي',
|
||||
'واجب',
|
||||
'مشاركة',
|
||||
'مشروع',
|
||||
'أداء',
|
||||
'اختبار قصير',
|
||||
'اختبار نهائي',
|
||||
];
|
||||
}
|
||||
|
||||
function assessment_scale_type_map(): array
|
||||
{
|
||||
return [
|
||||
'percentage' => ['label' => 'نسبة مئوية', 'class' => 'status-approved'],
|
||||
'points' => ['label' => 'نقاط', 'class' => 'status-submitted'],
|
||||
'rubric_4' => ['label' => 'Rubric / 4', 'class' => 'status-review'],
|
||||
'rubric_5' => ['label' => 'Rubric / 5', 'class' => 'status-review'],
|
||||
];
|
||||
}
|
||||
|
||||
function assessment_scale_type_label(string $scaleType): string
|
||||
{
|
||||
$map = assessment_scale_type_map();
|
||||
return (string) ($map[$scaleType]['label'] ?? 'غير محدد');
|
||||
}
|
||||
|
||||
function assessment_scale_type_badge(string $scaleType): string
|
||||
{
|
||||
$map = assessment_scale_type_map();
|
||||
$meta = $map[$scaleType] ?? ['label' => 'غير محدد', 'class' => 'status-muted'];
|
||||
return '<span class="status-badge ' . e($meta['class']) . '">' . e($meta['label']) . '</span>';
|
||||
}
|
||||
|
||||
function assessment_active_badge(int $isActive): string
|
||||
{
|
||||
$meta = $isActive === 1
|
||||
? ['label' => 'مفعل', 'class' => 'status-approved']
|
||||
: ['label' => 'مؤرشف', 'class' => 'status-muted'];
|
||||
|
||||
return '<span class="status-badge ' . e($meta['class']) . '">' . e($meta['label']) . '</span>';
|
||||
}
|
||||
|
||||
function validate_assessment_input(array $input): array
|
||||
{
|
||||
$data = assessment_defaults();
|
||||
$data['title'] = clean_text((string) ($input['title'] ?? ''), 150);
|
||||
$data['category'] = clean_text((string) ($input['category'] ?? ''), 80);
|
||||
$data['scale_type'] = clean_text((string) ($input['scale_type'] ?? ''), 40);
|
||||
$data['notes'] = clean_text((string) ($input['notes'] ?? ''), 1000);
|
||||
$data['is_active'] = ((string) ($input['is_active'] ?? '1')) === '1' ? '1' : '0';
|
||||
|
||||
$errors = [];
|
||||
|
||||
if ($data['title'] === '') {
|
||||
$errors['title'] = 'يرجى إدخال اسم نوع التقييم.';
|
||||
}
|
||||
if (!in_array($data['category'], assessment_category_options(), true)) {
|
||||
$errors['category'] = 'يرجى اختيار فئة تقييم صحيحة.';
|
||||
}
|
||||
|
||||
$scaleMap = assessment_scale_type_map();
|
||||
if (!array_key_exists($data['scale_type'], $scaleMap)) {
|
||||
$errors['scale_type'] = 'يرجى اختيار مقياس تقييم صحيح.';
|
||||
}
|
||||
|
||||
$maxScoreRaw = str_replace(',', '.', (string) ($input['max_score'] ?? ''));
|
||||
if ($maxScoreRaw === '' || !is_numeric($maxScoreRaw)) {
|
||||
$errors['max_score'] = 'أدخل الدرجة النهائية بصيغة رقمية.';
|
||||
} else {
|
||||
$maxScore = round((float) $maxScoreRaw, 2);
|
||||
if ($maxScore <= 0 || $maxScore > 1000) {
|
||||
$errors['max_score'] = 'يجب أن تكون الدرجة النهائية بين 0.01 و1000.';
|
||||
} else {
|
||||
$data['max_score'] = number_format($maxScore, 2, '.', '');
|
||||
}
|
||||
}
|
||||
|
||||
$weightRaw = str_replace(',', '.', (string) ($input['weight_percentage'] ?? ''));
|
||||
if ($weightRaw === '' || !is_numeric($weightRaw)) {
|
||||
$errors['weight_percentage'] = 'أدخل وزن التقييم بصيغة رقمية.';
|
||||
} else {
|
||||
$weight = round((float) $weightRaw, 2);
|
||||
if ($weight < 0 || $weight > 100) {
|
||||
$errors['weight_percentage'] = 'يجب أن يكون الوزن بين 0 و100٪.';
|
||||
} else {
|
||||
$data['weight_percentage'] = number_format($weight, 2, '.', '');
|
||||
}
|
||||
}
|
||||
|
||||
return [$data, $errors];
|
||||
}
|
||||
|
||||
function create_assessment_type(int $centerApplicationId, array $data): int
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO school_assessment_types (
|
||||
center_application_id, title, category, scale_type, max_score,
|
||||
weight_percentage, is_active, notes,
|
||||
created_at, updated_at
|
||||
) VALUES (
|
||||
:center_application_id, :title, :category, :scale_type, :max_score,
|
||||
:weight_percentage, :is_active, :notes,
|
||||
NOW(), NOW()
|
||||
)'
|
||||
);
|
||||
|
||||
$stmt->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':title', $data['title'], PDO::PARAM_STR);
|
||||
$stmt->bindValue(':category', $data['category'], PDO::PARAM_STR);
|
||||
$stmt->bindValue(':scale_type', $data['scale_type'], PDO::PARAM_STR);
|
||||
$stmt->bindValue(':max_score', (float) $data['max_score']);
|
||||
$stmt->bindValue(':weight_percentage', (float) $data['weight_percentage']);
|
||||
$stmt->bindValue(':is_active', (int) $data['is_active'], PDO::PARAM_INT);
|
||||
$stmt->bindValue(':notes', $data['notes'] !== '' ? $data['notes'] : null, $data['notes'] !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL);
|
||||
$stmt->execute();
|
||||
|
||||
return (int) $pdo->lastInsertId();
|
||||
}
|
||||
|
||||
function list_school_assessments(int $centerApplicationId): array
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT *
|
||||
FROM school_assessment_types
|
||||
WHERE center_application_id = :center_application_id
|
||||
ORDER BY is_active DESC, weight_percentage DESC, created_at DESC, id DESC'
|
||||
);
|
||||
$stmt->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
function school_assessment_metrics(int $centerApplicationId): array
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT
|
||||
COUNT(*) AS total,
|
||||
COALESCE(SUM(is_active = 1), 0) AS active_count,
|
||||
COALESCE(SUM(is_active = 0), 0) AS inactive_count,
|
||||
COALESCE(SUM(weight_percentage), 0) AS total_weight,
|
||||
COALESCE(SUM(CASE WHEN is_active = 1 THEN weight_percentage ELSE 0 END), 0) AS active_weight,
|
||||
COALESCE(AVG(max_score), 0) AS average_max_score,
|
||||
COALESCE(SUM(scale_type = 'percentage'), 0) AS percentage_count,
|
||||
COALESCE(SUM(scale_type = 'points'), 0) AS points_count,
|
||||
COALESCE(SUM(scale_type IN ('rubric_4', 'rubric_5')), 0) AS rubric_count
|
||||
FROM school_assessment_types
|
||||
WHERE center_application_id = :center_application_id"
|
||||
);
|
||||
$stmt->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
$row = $stmt->fetch() ?: [];
|
||||
|
||||
return [
|
||||
'total' => (int) ($row['total'] ?? 0),
|
||||
'active' => (int) ($row['active_count'] ?? 0),
|
||||
'inactive' => (int) ($row['inactive_count'] ?? 0),
|
||||
'total_weight' => round((float) ($row['total_weight'] ?? 0), 2),
|
||||
'active_weight' => round((float) ($row['active_weight'] ?? 0), 2),
|
||||
'average_max_score' => round((float) ($row['average_max_score'] ?? 0), 2),
|
||||
'percentage' => (int) ($row['percentage_count'] ?? 0),
|
||||
'points' => (int) ($row['points_count'] ?? 0),
|
||||
'rubric' => (int) ($row['rubric_count'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
function attendance_defaults(): array
|
||||
{
|
||||
return [
|
||||
'student_id' => '',
|
||||
'attendance_date' => date('Y-m-d'),
|
||||
'attendance_status' => 'absent',
|
||||
'absence_reason' => '',
|
||||
'notes' => '',
|
||||
];
|
||||
}
|
||||
|
||||
function attendance_status_map(): array
|
||||
{
|
||||
return [
|
||||
'absent' => ['label' => 'غياب', 'class' => 'status-rejected'],
|
||||
'excused' => ['label' => 'غياب بعذر', 'class' => 'status-review'],
|
||||
'late' => ['label' => 'تأخر', 'class' => 'status-submitted'],
|
||||
];
|
||||
}
|
||||
|
||||
function attendance_status_badge(string $status): string
|
||||
{
|
||||
$map = attendance_status_map();
|
||||
$meta = $map[$status] ?? ['label' => 'غير محدد', 'class' => 'status-muted'];
|
||||
return '<span class="status-badge ' . e($meta['class']) . '">' . e($meta['label']) . '</span>';
|
||||
}
|
||||
|
||||
function school_student_options(int $centerApplicationId): array
|
||||
{
|
||||
$students = list_school_students($centerApplicationId);
|
||||
$options = [];
|
||||
foreach ($students as $student) {
|
||||
$studentId = (int) ($student['id'] ?? 0);
|
||||
if ($studentId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$options[$studentId] = [
|
||||
'label' => trim((string) ($student['full_name'] ?? '')),
|
||||
'status' => (string) ($student['enrollment_status'] ?? ''),
|
||||
'grade_level' => (string) ($student['grade_level'] ?? ''),
|
||||
'guardian_phone' => (string) ($student['guardian_phone'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
function validate_attendance_input(int $centerApplicationId, array $input): array
|
||||
{
|
||||
$data = attendance_defaults();
|
||||
$data['student_id'] = (string) ((int) ($input['student_id'] ?? 0));
|
||||
$data['attendance_status'] = clean_text((string) ($input['attendance_status'] ?? ''), 30);
|
||||
$data['absence_reason'] = clean_text((string) ($input['absence_reason'] ?? ''), 190);
|
||||
$data['notes'] = clean_text((string) ($input['notes'] ?? ''), 1000);
|
||||
$data['attendance_date'] = clean_text((string) ($input['attendance_date'] ?? ''), 20);
|
||||
|
||||
$errors = [];
|
||||
$studentId = (int) $data['student_id'];
|
||||
$studentOptions = school_student_options($centerApplicationId);
|
||||
|
||||
if ($studentId <= 0 || !array_key_exists($studentId, $studentOptions)) {
|
||||
$errors['student_id'] = 'يرجى اختيار طالب صحيح من نفس المركز.';
|
||||
}
|
||||
|
||||
$statusMap = attendance_status_map();
|
||||
if (!array_key_exists($data['attendance_status'], $statusMap)) {
|
||||
$errors['attendance_status'] = 'يرجى اختيار حالة غياب صحيحة.';
|
||||
}
|
||||
|
||||
if ($data['attendance_date'] === '' || strtotime($data['attendance_date']) === false) {
|
||||
$errors['attendance_date'] = 'يرجى إدخال تاريخ صحيح للسجل اليومي.';
|
||||
}
|
||||
|
||||
if ($data['attendance_status'] !== 'late' && $data['absence_reason'] === '') {
|
||||
$errors['absence_reason'] = 'يرجى إدخال سبب الغياب أو العذر.';
|
||||
}
|
||||
|
||||
return [$data, $errors];
|
||||
}
|
||||
|
||||
function create_attendance_record(int $centerApplicationId, array $data): int
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO school_attendance_records (
|
||||
center_application_id, student_id, attendance_date, attendance_status,
|
||||
absence_reason, notes, created_at, updated_at
|
||||
) VALUES (
|
||||
:center_application_id, :student_id, :attendance_date, :attendance_status,
|
||||
:absence_reason, :notes, NOW(), NOW()
|
||||
)'
|
||||
);
|
||||
|
||||
$stmt->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':student_id', (int) $data['student_id'], PDO::PARAM_INT);
|
||||
$stmt->bindValue(':attendance_date', $data['attendance_date'], PDO::PARAM_STR);
|
||||
$stmt->bindValue(':attendance_status', $data['attendance_status'], PDO::PARAM_STR);
|
||||
$stmt->bindValue(':absence_reason', $data['absence_reason'] !== '' ? $data['absence_reason'] : null, $data['absence_reason'] !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL);
|
||||
$stmt->bindValue(':notes', $data['notes'] !== '' ? $data['notes'] : null, $data['notes'] !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL);
|
||||
$stmt->execute();
|
||||
|
||||
return (int) $pdo->lastInsertId();
|
||||
}
|
||||
|
||||
function list_school_attendance_records(int $centerApplicationId): array
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT ar.*, s.student_code, s.full_name, s.grade_level, s.guardian_phone
|
||||
FROM school_attendance_records ar
|
||||
INNER JOIN school_students s ON s.id = ar.student_id
|
||||
WHERE ar.center_application_id = :center_application_id
|
||||
ORDER BY ar.attendance_date DESC, ar.created_at DESC, ar.id DESC'
|
||||
);
|
||||
$stmt->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
function school_attendance_metrics(int $centerApplicationId): array
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT
|
||||
COUNT(*) AS total,
|
||||
COALESCE(SUM(attendance_status = 'absent'), 0) AS absent_count,
|
||||
COALESCE(SUM(attendance_status = 'excused'), 0) AS excused_count,
|
||||
COALESCE(SUM(attendance_status = 'late'), 0) AS late_count,
|
||||
COUNT(DISTINCT student_id) AS affected_students,
|
||||
MAX(attendance_date) AS latest_date,
|
||||
COALESCE(SUM(attendance_date = CURDATE()), 0) AS today_count
|
||||
FROM school_attendance_records
|
||||
WHERE center_application_id = :center_application_id"
|
||||
);
|
||||
$stmt->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
$row = $stmt->fetch() ?: [];
|
||||
|
||||
return [
|
||||
'total' => (int) ($row['total'] ?? 0),
|
||||
'absent' => (int) ($row['absent_count'] ?? 0),
|
||||
'excused' => (int) ($row['excused_count'] ?? 0),
|
||||
'late' => (int) ($row['late_count'] ?? 0),
|
||||
'affected_students' => (int) ($row['affected_students'] ?? 0),
|
||||
'latest_date' => (string) ($row['latest_date'] ?? ''),
|
||||
'today_count' => (int) ($row['today_count'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
function render_page_start(string $pageTitle, string $active = 'home', string $pageDescription = ''): void
|
||||
{
|
||||
$projectName = project_name();
|
||||
|
||||
910
includes/cycles.php
Normal file
910
includes/cycles.php
Normal file
@ -0,0 +1,910 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
function schema_table_has_column(PDO $pdo, string $table, string $column): bool
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :table_name AND COLUMN_NAME = :column_name'
|
||||
);
|
||||
$stmt->execute([
|
||||
':table_name' => $table,
|
||||
':column_name' => $column,
|
||||
]);
|
||||
|
||||
return (int) $stmt->fetchColumn() > 0;
|
||||
}
|
||||
|
||||
function schema_table_has_index(PDO $pdo, string $table, string $index): bool
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :table_name AND INDEX_NAME = :index_name'
|
||||
);
|
||||
$stmt->execute([
|
||||
':table_name' => $table,
|
||||
':index_name' => $index,
|
||||
]);
|
||||
|
||||
return (int) $stmt->fetchColumn() > 0;
|
||||
}
|
||||
|
||||
function schema_table_has_foreign_key(PDO $pdo, string $table, string $constraint): bool
|
||||
{
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = :table_name AND CONSTRAINT_NAME = :constraint_name AND CONSTRAINT_TYPE = "FOREIGN KEY"'
|
||||
);
|
||||
$stmt->execute([
|
||||
':table_name' => $table,
|
||||
':constraint_name' => $constraint,
|
||||
]);
|
||||
|
||||
return (int) $stmt->fetchColumn() > 0;
|
||||
}
|
||||
|
||||
function ensure_school_cycle_schema(PDO $pdo): void
|
||||
{
|
||||
static $done = false;
|
||||
if ($done) {
|
||||
return;
|
||||
}
|
||||
|
||||
$migrationPath = __DIR__ . '/../db/migrations/20260416_school_cycles.sql';
|
||||
if (is_file($migrationPath)) {
|
||||
$sql = file_get_contents($migrationPath);
|
||||
if (is_string($sql) && trim($sql) !== '') {
|
||||
$pdo->exec($sql);
|
||||
}
|
||||
}
|
||||
|
||||
$tables = [
|
||||
'school_students' => [
|
||||
'index' => 'idx_school_students_cycle',
|
||||
'foreign_key' => 'fk_school_students_cycle',
|
||||
],
|
||||
'school_teachers' => [
|
||||
'index' => 'idx_school_teachers_cycle',
|
||||
'foreign_key' => 'fk_school_teachers_cycle',
|
||||
],
|
||||
'school_assessment_types' => [
|
||||
'index' => 'idx_school_assessments_cycle',
|
||||
'foreign_key' => 'fk_school_assessments_cycle',
|
||||
],
|
||||
'school_attendance_records' => [
|
||||
'index' => 'idx_school_attendance_cycle',
|
||||
'foreign_key' => 'fk_school_attendance_cycle',
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($tables as $table => $meta) {
|
||||
if (!schema_table_has_column($pdo, $table, 'cycle_id')) {
|
||||
$pdo->exec('ALTER TABLE ' . $table . ' ADD COLUMN cycle_id INT UNSIGNED NULL AFTER center_application_id');
|
||||
}
|
||||
|
||||
if (!schema_table_has_index($pdo, $table, $meta['index'])) {
|
||||
$pdo->exec('ALTER TABLE ' . $table . ' ADD INDEX ' . $meta['index'] . ' (cycle_id)');
|
||||
}
|
||||
}
|
||||
|
||||
ensure_school_cycle_backfill($pdo);
|
||||
|
||||
if (schema_table_has_index($pdo, 'school_students', 'uniq_school_student_code') && !schema_table_has_index($pdo, 'school_students', 'uniq_school_student_cycle_code')) {
|
||||
$pdo->exec('ALTER TABLE school_students DROP INDEX uniq_school_student_code');
|
||||
}
|
||||
if (!schema_table_has_index($pdo, 'school_students', 'uniq_school_student_cycle_code')) {
|
||||
$pdo->exec('ALTER TABLE school_students ADD UNIQUE KEY uniq_school_student_cycle_code (center_application_id, cycle_id, student_code)');
|
||||
}
|
||||
|
||||
foreach ($tables as $table => $meta) {
|
||||
if (!schema_table_has_foreign_key($pdo, $table, $meta['foreign_key'])) {
|
||||
$pdo->exec(
|
||||
'ALTER TABLE ' . $table . ' ADD CONSTRAINT ' . $meta['foreign_key'] . ' FOREIGN KEY (cycle_id) REFERENCES school_cycles(id) ON DELETE CASCADE'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$done = true;
|
||||
}
|
||||
|
||||
function ensure_school_cycle_backfill(PDO $pdo): void
|
||||
{
|
||||
$applicationRows = $pdo->query(
|
||||
"SELECT * FROM center_applications WHERE status = 'approved' OR id IN (
|
||||
SELECT center_application_id FROM school_students
|
||||
UNION SELECT center_application_id FROM school_teachers
|
||||
UNION SELECT center_application_id FROM school_assessment_types
|
||||
UNION SELECT center_application_id FROM school_attendance_records
|
||||
) ORDER BY id ASC"
|
||||
)->fetchAll();
|
||||
|
||||
foreach ($applicationRows as $application) {
|
||||
$applicationId = (int) ($application['id'] ?? 0);
|
||||
if ($applicationId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$cycleId = ensure_default_school_cycle_record($pdo, $application);
|
||||
foreach (['school_students', 'school_teachers', 'school_assessment_types', 'school_attendance_records'] as $table) {
|
||||
if (!schema_table_has_column($pdo, $table, 'cycle_id')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare('UPDATE ' . $table . ' SET cycle_id = :cycle_id WHERE center_application_id = :center_application_id AND cycle_id IS NULL');
|
||||
$stmt->execute([
|
||||
':cycle_id' => $cycleId,
|
||||
':center_application_id' => $applicationId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function school_cycle_season_map(): array
|
||||
{
|
||||
return [
|
||||
'summer' => ['label' => 'Summer', 'label_ar' => 'صيف'],
|
||||
'winter' => ['label' => 'Winter', 'label_ar' => 'شتاء'],
|
||||
];
|
||||
}
|
||||
|
||||
function school_cycle_status_map(): array
|
||||
{
|
||||
return [
|
||||
'active' => ['label' => 'نشطة', 'class' => 'status-approved'],
|
||||
'upcoming' => ['label' => 'قادمة', 'class' => 'status-review'],
|
||||
'archived' => ['label' => 'مؤرشفة', 'class' => 'status-muted'],
|
||||
];
|
||||
}
|
||||
|
||||
function school_cycle_status_badge(string $status): string
|
||||
{
|
||||
$map = school_cycle_status_map();
|
||||
$meta = $map[$status] ?? ['label' => 'غير محدد', 'class' => 'status-muted'];
|
||||
return '<span class="status-badge ' . e($meta['class']) . '">' . e($meta['label']) . '</span>';
|
||||
}
|
||||
|
||||
function school_cycle_season_options(): array
|
||||
{
|
||||
return school_cycle_season_map();
|
||||
}
|
||||
|
||||
function detect_school_cycle_season(?string $date = null): string
|
||||
{
|
||||
$ts = $date ? strtotime($date) : time();
|
||||
if ($ts === false) {
|
||||
$ts = time();
|
||||
}
|
||||
|
||||
$month = (int) date('n', $ts);
|
||||
return $month >= 5 && $month <= 9 ? 'summer' : 'winter';
|
||||
}
|
||||
|
||||
function format_school_cycle_name(string $season, int $year): string
|
||||
{
|
||||
$map = school_cycle_season_map();
|
||||
$label = $map[$season]['label'] ?? 'Cycle';
|
||||
return $label . ' ' . $year;
|
||||
}
|
||||
|
||||
function school_cycle_defaults(?array $application = null): array
|
||||
{
|
||||
$season = detect_school_cycle_season((string) ($application['start_date'] ?? ''));
|
||||
$year = (int) date('Y', strtotime((string) ($application['start_date'] ?? 'now')) ?: time());
|
||||
$startDate = clean_text((string) ($application['start_date'] ?? date('Y-m-d')), 20);
|
||||
$endDate = clean_text((string) ($application['end_date'] ?? date('Y-m-d', strtotime('+90 days'))), 20);
|
||||
|
||||
return [
|
||||
'season' => $season,
|
||||
'year' => (string) $year,
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
'status' => 'active',
|
||||
];
|
||||
}
|
||||
|
||||
function validate_school_cycle_input(array $input, ?array $application = null): array
|
||||
{
|
||||
$defaults = school_cycle_defaults($application);
|
||||
$data = $defaults;
|
||||
$data['season'] = clean_text((string) ($input['season'] ?? $defaults['season']), 20);
|
||||
$data['year'] = clean_text((string) ($input['year'] ?? $defaults['year']), 4);
|
||||
$data['start_date'] = clean_text((string) ($input['start_date'] ?? $defaults['start_date']), 20);
|
||||
$data['end_date'] = clean_text((string) ($input['end_date'] ?? $defaults['end_date']), 20);
|
||||
$data['status'] = clean_text((string) ($input['status'] ?? 'active'), 20);
|
||||
|
||||
$errors = [];
|
||||
if (!array_key_exists($data['season'], school_cycle_season_options())) {
|
||||
$errors['season'] = 'يرجى اختيار موسم صحيح.';
|
||||
}
|
||||
|
||||
$year = (int) $data['year'];
|
||||
if ((string) $year !== $data['year'] || $year < 2020 || $year > 2100) {
|
||||
$errors['year'] = 'يرجى إدخال سنة صحيحة مثل 2026.';
|
||||
}
|
||||
|
||||
if ($data['start_date'] === '' || strtotime($data['start_date']) === false) {
|
||||
$errors['start_date'] = 'يرجى إدخال تاريخ بداية صحيح.';
|
||||
}
|
||||
if ($data['end_date'] === '' || strtotime($data['end_date']) === false) {
|
||||
$errors['end_date'] = 'يرجى إدخال تاريخ نهاية صحيح.';
|
||||
}
|
||||
if (!isset($errors['start_date'], $errors['end_date']) && strtotime($data['end_date']) < strtotime($data['start_date'])) {
|
||||
$errors['end_date'] = 'تاريخ النهاية يجب أن يكون بعد البداية أو مساوياً لها.';
|
||||
}
|
||||
|
||||
if (!in_array($data['status'], ['active', 'upcoming'], true)) {
|
||||
$errors['status'] = 'يرجى اختيار حالة تشغيل صحيحة للدورة.';
|
||||
}
|
||||
|
||||
return [$data, $errors];
|
||||
}
|
||||
|
||||
function normalize_school_cycle_row(array $row): array
|
||||
{
|
||||
$row['id'] = (int) ($row['id'] ?? 0);
|
||||
$row['center_application_id'] = (int) ($row['center_application_id'] ?? 0);
|
||||
$row['year'] = (int) ($row['year'] ?? 0);
|
||||
$row['cycle_name'] = (string) ($row['cycle_name'] ?? format_school_cycle_name((string) ($row['season'] ?? ''), (int) ($row['year'] ?? 0)));
|
||||
$row['status'] = (string) ($row['status'] ?? 'upcoming');
|
||||
return $row;
|
||||
}
|
||||
|
||||
function ensure_default_school_cycle_record(PDO $pdo, array $application): int
|
||||
{
|
||||
$applicationId = (int) ($application['id'] ?? 0);
|
||||
if ($applicationId <= 0) {
|
||||
throw new InvalidArgumentException('Missing application id for school cycle.');
|
||||
}
|
||||
|
||||
$stmt = $pdo->prepare('SELECT * FROM school_cycles WHERE center_application_id = :center_application_id ORDER BY FIELD(status, "active", "upcoming", "archived"), start_date DESC, id DESC LIMIT 1');
|
||||
$stmt->execute([':center_application_id' => $applicationId]);
|
||||
$existing = $stmt->fetch();
|
||||
if ($existing) {
|
||||
return (int) $existing['id'];
|
||||
}
|
||||
|
||||
$season = detect_school_cycle_season((string) ($application['start_date'] ?? ''));
|
||||
$year = (int) date('Y', strtotime((string) ($application['start_date'] ?? 'now')) ?: time());
|
||||
$startDate = (string) ($application['start_date'] ?? date('Y-m-d'));
|
||||
$endDate = (string) ($application['end_date'] ?? $startDate);
|
||||
$cycleName = format_school_cycle_name($season, $year);
|
||||
$status = ((string) ($application['status'] ?? '') === 'approved') ? 'active' : 'upcoming';
|
||||
|
||||
$insert = $pdo->prepare(
|
||||
'INSERT INTO school_cycles (
|
||||
center_application_id, season, year, cycle_name, start_date, end_date,
|
||||
status, archived_at, created_at, updated_at
|
||||
) VALUES (
|
||||
:center_application_id, :season, :year, :cycle_name, :start_date, :end_date,
|
||||
:status, NULL, NOW(), NOW()
|
||||
)'
|
||||
);
|
||||
$insert->execute([
|
||||
':center_application_id' => $applicationId,
|
||||
':season' => $season,
|
||||
':year' => $year,
|
||||
':cycle_name' => $cycleName,
|
||||
':start_date' => $startDate,
|
||||
':end_date' => $endDate,
|
||||
':status' => $status,
|
||||
]);
|
||||
|
||||
return (int) $pdo->lastInsertId();
|
||||
}
|
||||
|
||||
function list_school_cycles(int $centerApplicationId): array
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->prepare('SELECT * FROM school_cycles WHERE center_application_id = :center_application_id ORDER BY start_date DESC, id DESC');
|
||||
$stmt->execute([':center_application_id' => $centerApplicationId]);
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
return array_map('normalize_school_cycle_row', $rows);
|
||||
}
|
||||
|
||||
function get_school_cycle(int $centerApplicationId, int $cycleId): ?array
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->prepare('SELECT * FROM school_cycles WHERE center_application_id = :center_application_id AND id = :id LIMIT 1');
|
||||
$stmt->execute([
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':id' => $cycleId,
|
||||
]);
|
||||
$row = $stmt->fetch();
|
||||
|
||||
return $row ? normalize_school_cycle_row($row) : null;
|
||||
}
|
||||
|
||||
|
||||
function school_cycle_rollover_defaults(?array $selectedCycle = null): array
|
||||
{
|
||||
return [
|
||||
'source_cycle_id' => $selectedCycle ? (int) ($selectedCycle['id'] ?? 0) : 0,
|
||||
'copy_teachers' => true,
|
||||
'copy_assessments' => true,
|
||||
'copy_students' => false,
|
||||
];
|
||||
}
|
||||
|
||||
function school_cycle_rollover_input(int $centerApplicationId, array $input, ?array $selectedCycle = null): array
|
||||
{
|
||||
$defaults = school_cycle_rollover_defaults($selectedCycle);
|
||||
$sourceCycleId = isset($input['source_cycle_id']) ? (int) $input['source_cycle_id'] : (int) $defaults['source_cycle_id'];
|
||||
$sourceCycle = $sourceCycleId > 0 ? get_school_cycle($centerApplicationId, $sourceCycleId) : null;
|
||||
|
||||
return [
|
||||
'source_cycle_id' => $sourceCycle ? (int) ($sourceCycle['id'] ?? 0) : 0,
|
||||
'source_cycle' => $sourceCycle,
|
||||
'copy_teachers' => isset($input['copy_teachers']) ? (bool) $input['copy_teachers'] : false,
|
||||
'copy_assessments' => isset($input['copy_assessments']) ? (bool) $input['copy_assessments'] : false,
|
||||
'copy_students' => isset($input['copy_students']) ? (bool) $input['copy_students'] : false,
|
||||
];
|
||||
}
|
||||
|
||||
function copy_school_cycle_rollover(PDO $pdo, int $centerApplicationId, int $sourceCycleId, int $targetCycleId, array $rollover): array
|
||||
{
|
||||
$summary = [
|
||||
'teachers' => 0,
|
||||
'assessments' => 0,
|
||||
'students' => 0,
|
||||
];
|
||||
|
||||
if (!empty($rollover['copy_teachers'])) {
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO school_teachers (
|
||||
center_application_id, cycle_id, full_name, role_title, specialization,
|
||||
phone, email, employment_status, notes,
|
||||
created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
center_application_id, :target_cycle_id, full_name, role_title, specialization,
|
||||
phone, email, employment_status, notes,
|
||||
NOW(), NOW()
|
||||
FROM school_teachers
|
||||
WHERE center_application_id = :center_application_id AND cycle_id = :source_cycle_id'
|
||||
);
|
||||
$stmt->execute([
|
||||
':target_cycle_id' => $targetCycleId,
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':source_cycle_id' => $sourceCycleId,
|
||||
]);
|
||||
$summary['teachers'] = $stmt->rowCount();
|
||||
}
|
||||
|
||||
if (!empty($rollover['copy_assessments'])) {
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO school_assessment_types (
|
||||
center_application_id, cycle_id, title, category, scale_type,
|
||||
max_score, weight_percentage, is_active, notes,
|
||||
created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
center_application_id, :target_cycle_id, title, category, scale_type,
|
||||
max_score, weight_percentage, is_active, notes,
|
||||
NOW(), NOW()
|
||||
FROM school_assessment_types
|
||||
WHERE center_application_id = :center_application_id AND cycle_id = :source_cycle_id'
|
||||
);
|
||||
$stmt->execute([
|
||||
':target_cycle_id' => $targetCycleId,
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':source_cycle_id' => $sourceCycleId,
|
||||
]);
|
||||
$summary['assessments'] = $stmt->rowCount();
|
||||
}
|
||||
|
||||
if (!empty($rollover['copy_students'])) {
|
||||
$stmt = $pdo->prepare(
|
||||
"INSERT INTO school_students (
|
||||
center_application_id, cycle_id, student_code, full_name, gender, grade_level,
|
||||
guardian_name, guardian_phone, birth_date, enrollment_status, notes,
|
||||
created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
center_application_id, :target_cycle_id, student_code, full_name, gender, grade_level,
|
||||
guardian_name, guardian_phone, birth_date, enrollment_status, notes,
|
||||
NOW(), NOW()
|
||||
FROM school_students
|
||||
WHERE center_application_id = :center_application_id
|
||||
AND cycle_id = :source_cycle_id
|
||||
AND enrollment_status IN ('active', 'waiting')"
|
||||
);
|
||||
$stmt->execute([
|
||||
':target_cycle_id' => $targetCycleId,
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':source_cycle_id' => $sourceCycleId,
|
||||
]);
|
||||
$summary['students'] = $stmt->rowCount();
|
||||
}
|
||||
|
||||
return $summary;
|
||||
}
|
||||
|
||||
function create_school_cycle(int $centerApplicationId, array $data, array $rollover = []): array
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$season = $data['season'];
|
||||
$year = (int) $data['year'];
|
||||
$cycleName = format_school_cycle_name($season, $year);
|
||||
$rollover = array_merge(school_cycle_rollover_defaults(), $rollover);
|
||||
$sourceCycleId = (int) ($rollover['source_cycle_id'] ?? 0);
|
||||
|
||||
$pdo->beginTransaction();
|
||||
try {
|
||||
$sourceCycle = null;
|
||||
if ($sourceCycleId > 0) {
|
||||
$sourceStmt = $pdo->prepare('SELECT * FROM school_cycles WHERE center_application_id = :center_application_id AND id = :id LIMIT 1');
|
||||
$sourceStmt->execute([
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':id' => $sourceCycleId,
|
||||
]);
|
||||
$sourceCycleRow = $sourceStmt->fetch();
|
||||
if ($sourceCycleRow) {
|
||||
$sourceCycle = normalize_school_cycle_row($sourceCycleRow);
|
||||
} elseif (!empty($rollover['copy_teachers']) || !empty($rollover['copy_assessments']) || !empty($rollover['copy_students'])) {
|
||||
throw new InvalidArgumentException('Source cycle not found for rollover.');
|
||||
}
|
||||
}
|
||||
|
||||
if ($data['status'] === 'active') {
|
||||
$archiveStmt = $pdo->prepare(
|
||||
"UPDATE school_cycles
|
||||
SET status = 'archived', archived_at = COALESCE(archived_at, NOW()), updated_at = NOW()
|
||||
WHERE center_application_id = :center_application_id AND status = 'active'"
|
||||
);
|
||||
$archiveStmt->execute([':center_application_id' => $centerApplicationId]);
|
||||
}
|
||||
|
||||
$insert = $pdo->prepare(
|
||||
'INSERT INTO school_cycles (
|
||||
center_application_id, season, year, cycle_name, start_date, end_date,
|
||||
status, archived_at, created_at, updated_at
|
||||
) VALUES (
|
||||
:center_application_id, :season, :year, :cycle_name, :start_date, :end_date,
|
||||
:status, :archived_at, NOW(), NOW()
|
||||
)'
|
||||
);
|
||||
$insert->execute([
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':season' => $season,
|
||||
':year' => $year,
|
||||
':cycle_name' => $cycleName,
|
||||
':start_date' => $data['start_date'],
|
||||
':end_date' => $data['end_date'],
|
||||
':status' => $data['status'],
|
||||
':archived_at' => $data['status'] === 'archived' ? date('Y-m-d H:i:s') : null,
|
||||
]);
|
||||
|
||||
$cycleId = (int) $pdo->lastInsertId();
|
||||
$rolloverSummary = ['teachers' => 0, 'assessments' => 0, 'students' => 0];
|
||||
|
||||
if ($sourceCycle && ($rollover['copy_teachers'] || $rollover['copy_assessments'] || $rollover['copy_students'])) {
|
||||
$rolloverSummary = copy_school_cycle_rollover($pdo, $centerApplicationId, (int) $sourceCycle['id'], $cycleId, $rollover);
|
||||
}
|
||||
|
||||
$pdo->commit();
|
||||
|
||||
return [
|
||||
'cycle_id' => $cycleId,
|
||||
'cycle_name' => $cycleName,
|
||||
'source_cycle_name' => $sourceCycle ? (string) ($sourceCycle['cycle_name'] ?? '') : '',
|
||||
'rollover' => $rolloverSummary,
|
||||
];
|
||||
} catch (Throwable $exception) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
function archive_school_cycle(int $centerApplicationId, int $cycleId): void
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->prepare(
|
||||
"UPDATE school_cycles
|
||||
SET status = 'archived', archived_at = COALESCE(archived_at, NOW()), updated_at = NOW()
|
||||
WHERE center_application_id = :center_application_id AND id = :id"
|
||||
);
|
||||
$stmt->execute([
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':id' => $cycleId,
|
||||
]);
|
||||
}
|
||||
|
||||
function resolve_school_cycle_context(int $centerApplicationId, ?array $application, int $requestedCycleId = 0): array
|
||||
{
|
||||
if ($application && (string) ($application['status'] ?? '') === 'approved') {
|
||||
$pdo = db_connection();
|
||||
ensure_default_school_cycle_record($pdo, $application);
|
||||
}
|
||||
|
||||
$cycles = list_school_cycles($centerApplicationId);
|
||||
$selected = null;
|
||||
$active = null;
|
||||
|
||||
foreach ($cycles as $cycle) {
|
||||
if ($cycle['status'] === 'active' && $active === null) {
|
||||
$active = $cycle;
|
||||
}
|
||||
if ($requestedCycleId > 0 && (int) $cycle['id'] === $requestedCycleId) {
|
||||
$selected = $cycle;
|
||||
}
|
||||
}
|
||||
|
||||
if ($selected === null) {
|
||||
$selected = $active ?? ($cycles[0] ?? null);
|
||||
}
|
||||
|
||||
return [
|
||||
'cycles' => $cycles,
|
||||
'selected' => $selected,
|
||||
'active' => $active,
|
||||
'read_only' => $selected ? ((string) ($selected['status'] ?? '') === 'archived') : false,
|
||||
];
|
||||
}
|
||||
|
||||
function school_page_url(string $page, int $applicationId, ?int $cycleId = null): string
|
||||
{
|
||||
$url = $page . '?id=' . urlencode((string) $applicationId);
|
||||
if ($cycleId !== null && $cycleId > 0) {
|
||||
$url .= '&cycle=' . urlencode((string) $cycleId);
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
function create_student_in_cycle(int $centerApplicationId, int $cycleId, array $data): int
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO school_students (
|
||||
center_application_id, cycle_id, student_code, full_name, gender, grade_level,
|
||||
guardian_name, guardian_phone, birth_date, enrollment_status, notes,
|
||||
created_at, updated_at
|
||||
) VALUES (
|
||||
:center_application_id, :cycle_id, :student_code, :full_name, :gender, :grade_level,
|
||||
:guardian_name, :guardian_phone, :birth_date, :enrollment_status, :notes,
|
||||
NOW(), NOW()
|
||||
)'
|
||||
);
|
||||
|
||||
$stmt->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':cycle_id', $cycleId, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':student_code', $data['student_code'], PDO::PARAM_STR);
|
||||
$stmt->bindValue(':full_name', $data['full_name'], PDO::PARAM_STR);
|
||||
$stmt->bindValue(':gender', $data['gender'], PDO::PARAM_STR);
|
||||
$stmt->bindValue(':grade_level', $data['grade_level'], PDO::PARAM_STR);
|
||||
$stmt->bindValue(':guardian_name', $data['guardian_name'], PDO::PARAM_STR);
|
||||
$stmt->bindValue(':guardian_phone', $data['guardian_phone'], PDO::PARAM_STR);
|
||||
$stmt->bindValue(':birth_date', $data['birth_date'] !== '' ? $data['birth_date'] : null, $data['birth_date'] !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL);
|
||||
$stmt->bindValue(':enrollment_status', $data['enrollment_status'], PDO::PARAM_STR);
|
||||
$stmt->bindValue(':notes', $data['notes'] !== '' ? $data['notes'] : null, $data['notes'] !== '' ? PDO::PARAM_STR : PDO::PARAM_NULL);
|
||||
$stmt->execute();
|
||||
|
||||
return (int) $pdo->lastInsertId();
|
||||
}
|
||||
|
||||
function list_school_students_by_cycle(int $centerApplicationId, int $cycleId): array
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT * FROM school_students
|
||||
WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id
|
||||
ORDER BY created_at DESC, id DESC'
|
||||
);
|
||||
$stmt->execute([
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':cycle_id' => $cycleId,
|
||||
]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
function school_student_metrics_by_cycle(int $centerApplicationId, int $cycleId): array
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT
|
||||
COUNT(*) AS total,
|
||||
COALESCE(SUM(gender = 'طالب'), 0) AS boys_count,
|
||||
COALESCE(SUM(gender = 'طالبة'), 0) AS girls_count,
|
||||
COALESCE(SUM(enrollment_status = 'active'), 0) AS active_count,
|
||||
COALESCE(SUM(enrollment_status = 'waiting'), 0) AS waiting_count,
|
||||
COALESCE(SUM(enrollment_status = 'withdrawn'), 0) AS withdrawn_count
|
||||
FROM school_students
|
||||
WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id"
|
||||
);
|
||||
$stmt->execute([
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':cycle_id' => $cycleId,
|
||||
]);
|
||||
$row = $stmt->fetch() ?: [];
|
||||
|
||||
return [
|
||||
'total' => (int) ($row['total'] ?? 0),
|
||||
'boys' => (int) ($row['boys_count'] ?? 0),
|
||||
'girls' => (int) ($row['girls_count'] ?? 0),
|
||||
'active' => (int) ($row['active_count'] ?? 0),
|
||||
'waiting' => (int) ($row['waiting_count'] ?? 0),
|
||||
'withdrawn' => (int) ($row['withdrawn_count'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
function create_teacher_in_cycle(int $centerApplicationId, int $cycleId, array $data): int
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO school_teachers (
|
||||
center_application_id, cycle_id, full_name, role_title, specialization,
|
||||
phone, email, employment_status, notes,
|
||||
created_at, updated_at
|
||||
) VALUES (
|
||||
:center_application_id, :cycle_id, :full_name, :role_title, :specialization,
|
||||
:phone, :email, :employment_status, :notes,
|
||||
NOW(), NOW()
|
||||
)'
|
||||
);
|
||||
$stmt->execute([
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':cycle_id' => $cycleId,
|
||||
':full_name' => $data['full_name'],
|
||||
':role_title' => $data['role_title'],
|
||||
':specialization' => $data['specialization'] !== '' ? $data['specialization'] : null,
|
||||
':phone' => $data['phone'] !== '' ? $data['phone'] : null,
|
||||
':email' => $data['email'] !== '' ? $data['email'] : null,
|
||||
':employment_status' => $data['employment_status'],
|
||||
':notes' => $data['notes'] !== '' ? $data['notes'] : null,
|
||||
]);
|
||||
return (int) $pdo->lastInsertId();
|
||||
}
|
||||
|
||||
function list_school_teachers_by_cycle(int $centerApplicationId, int $cycleId): array
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT * FROM school_teachers
|
||||
WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id
|
||||
ORDER BY created_at DESC, id DESC'
|
||||
);
|
||||
$stmt->execute([
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':cycle_id' => $cycleId,
|
||||
]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
function school_teacher_metrics_by_cycle(int $centerApplicationId, int $cycleId): array
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT
|
||||
COUNT(*) AS total,
|
||||
COALESCE(SUM(employment_status = 'active'), 0) AS active_count,
|
||||
COALESCE(SUM(employment_status = 'pending'), 0) AS pending_count,
|
||||
COALESCE(SUM(employment_status = 'inactive'), 0) AS inactive_count,
|
||||
COALESCE(SUM(role_title = 'معلم' OR role_title = 'معلمة'), 0) AS teachers_count,
|
||||
COALESCE(SUM(role_title LIKE '%مشرف%' OR role_title LIKE '%منسق%'), 0) AS supervisors_count,
|
||||
COALESCE(SUM(email IS NOT NULL AND email <> ''), 0) AS email_ready_count
|
||||
FROM school_teachers
|
||||
WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id"
|
||||
);
|
||||
$stmt->execute([
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':cycle_id' => $cycleId,
|
||||
]);
|
||||
$row = $stmt->fetch() ?: [];
|
||||
|
||||
return [
|
||||
'total' => (int) ($row['total'] ?? 0),
|
||||
'active' => (int) ($row['active_count'] ?? 0),
|
||||
'pending' => (int) ($row['pending_count'] ?? 0),
|
||||
'inactive' => (int) ($row['inactive_count'] ?? 0),
|
||||
'teachers' => (int) ($row['teachers_count'] ?? 0),
|
||||
'supervisors' => (int) ($row['supervisors_count'] ?? 0),
|
||||
'email_ready' => (int) ($row['email_ready_count'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
function create_assessment_type_in_cycle(int $centerApplicationId, int $cycleId, array $data): int
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO school_assessment_types (
|
||||
center_application_id, cycle_id, title, category, scale_type,
|
||||
max_score, weight_percentage, is_active, notes,
|
||||
created_at, updated_at
|
||||
) VALUES (
|
||||
:center_application_id, :cycle_id, :title, :category, :scale_type,
|
||||
:max_score, :weight_percentage, :is_active, :notes,
|
||||
NOW(), NOW()
|
||||
)'
|
||||
);
|
||||
$stmt->execute([
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':cycle_id' => $cycleId,
|
||||
':title' => $data['title'],
|
||||
':category' => $data['category'],
|
||||
':scale_type' => $data['scale_type'],
|
||||
':max_score' => (float) $data['max_score'],
|
||||
':weight_percentage' => (float) $data['weight_percentage'],
|
||||
':is_active' => (int) $data['is_active'],
|
||||
':notes' => $data['notes'] !== '' ? $data['notes'] : null,
|
||||
]);
|
||||
return (int) $pdo->lastInsertId();
|
||||
}
|
||||
|
||||
function list_school_assessments_by_cycle(int $centerApplicationId, int $cycleId): array
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT * FROM school_assessment_types
|
||||
WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id
|
||||
ORDER BY is_active DESC, created_at DESC, id DESC'
|
||||
);
|
||||
$stmt->execute([
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':cycle_id' => $cycleId,
|
||||
]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
function school_assessment_metrics_by_cycle(int $centerApplicationId, int $cycleId): array
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT
|
||||
COUNT(*) AS total,
|
||||
COALESCE(SUM(is_active = 1), 0) AS active_count,
|
||||
COALESCE(SUM(is_active = 0), 0) AS inactive_count,
|
||||
COALESCE(SUM(weight_percentage), 0) AS total_weight,
|
||||
COALESCE(SUM(CASE WHEN is_active = 1 THEN weight_percentage ELSE 0 END), 0) AS active_weight,
|
||||
COALESCE(AVG(max_score), 0) AS average_max_score,
|
||||
COALESCE(SUM(scale_type = 'percentage'), 0) AS percentage_count,
|
||||
COALESCE(SUM(scale_type = 'points'), 0) AS points_count,
|
||||
COALESCE(SUM(scale_type = 'rubric'), 0) AS rubric_count
|
||||
FROM school_assessment_types
|
||||
WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id"
|
||||
);
|
||||
$stmt->execute([
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':cycle_id' => $cycleId,
|
||||
]);
|
||||
$row = $stmt->fetch() ?: [];
|
||||
|
||||
return [
|
||||
'total' => (int) ($row['total'] ?? 0),
|
||||
'active' => (int) ($row['active_count'] ?? 0),
|
||||
'inactive' => (int) ($row['inactive_count'] ?? 0),
|
||||
'total_weight' => (float) ($row['total_weight'] ?? 0),
|
||||
'active_weight' => (float) ($row['active_weight'] ?? 0),
|
||||
'average_max_score' => (float) ($row['average_max_score'] ?? 0),
|
||||
'percentage' => (int) ($row['percentage_count'] ?? 0),
|
||||
'points' => (int) ($row['points_count'] ?? 0),
|
||||
'rubric' => (int) ($row['rubric_count'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
function school_student_options_by_cycle(int $centerApplicationId, int $cycleId): array
|
||||
{
|
||||
$students = list_school_students_by_cycle($centerApplicationId, $cycleId);
|
||||
$options = [];
|
||||
foreach ($students as $student) {
|
||||
$studentId = (int) ($student['id'] ?? 0);
|
||||
if ($studentId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$options[$studentId] = [
|
||||
'label' => trim((string) ($student['full_name'] ?? '')),
|
||||
'status' => (string) ($student['enrollment_status'] ?? ''),
|
||||
'grade_level' => (string) ($student['grade_level'] ?? ''),
|
||||
'guardian_phone' => (string) ($student['guardian_phone'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
function validate_attendance_input_for_cycle(int $centerApplicationId, int $cycleId, array $input): array
|
||||
{
|
||||
$data = attendance_defaults();
|
||||
$data['student_id'] = (string) ((int) ($input['student_id'] ?? 0));
|
||||
$data['attendance_status'] = clean_text((string) ($input['attendance_status'] ?? ''), 30);
|
||||
$data['absence_reason'] = clean_text((string) ($input['absence_reason'] ?? ''), 190);
|
||||
$data['notes'] = clean_text((string) ($input['notes'] ?? ''), 1000);
|
||||
$data['attendance_date'] = clean_text((string) ($input['attendance_date'] ?? ''), 20);
|
||||
|
||||
$errors = [];
|
||||
$studentId = (int) $data['student_id'];
|
||||
$studentOptions = school_student_options_by_cycle($centerApplicationId, $cycleId);
|
||||
|
||||
if ($studentId <= 0 || !array_key_exists($studentId, $studentOptions)) {
|
||||
$errors['student_id'] = 'يرجى اختيار طالب صحيح من نفس الدورة الموسمية.';
|
||||
}
|
||||
|
||||
$statusMap = attendance_status_map();
|
||||
if (!array_key_exists($data['attendance_status'], $statusMap)) {
|
||||
$errors['attendance_status'] = 'يرجى اختيار حالة غياب صحيحة.';
|
||||
}
|
||||
|
||||
if ($data['attendance_date'] === '' || strtotime($data['attendance_date']) === false) {
|
||||
$errors['attendance_date'] = 'يرجى إدخال تاريخ صحيح للسجل اليومي.';
|
||||
}
|
||||
|
||||
if ($data['attendance_status'] !== 'late' && $data['absence_reason'] === '') {
|
||||
$errors['absence_reason'] = 'يرجى إدخال سبب الغياب أو العذر.';
|
||||
}
|
||||
|
||||
return [$data, $errors];
|
||||
}
|
||||
|
||||
function create_attendance_record_in_cycle(int $centerApplicationId, int $cycleId, array $data): int
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO school_attendance_records (
|
||||
center_application_id, cycle_id, student_id, attendance_date, attendance_status,
|
||||
absence_reason, notes, created_at, updated_at
|
||||
) VALUES (
|
||||
:center_application_id, :cycle_id, :student_id, :attendance_date, :attendance_status,
|
||||
:absence_reason, :notes, NOW(), NOW()
|
||||
)'
|
||||
);
|
||||
|
||||
$stmt->execute([
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':cycle_id' => $cycleId,
|
||||
':student_id' => (int) $data['student_id'],
|
||||
':attendance_date' => $data['attendance_date'],
|
||||
':attendance_status' => $data['attendance_status'],
|
||||
':absence_reason' => $data['absence_reason'] !== '' ? $data['absence_reason'] : null,
|
||||
':notes' => $data['notes'] !== '' ? $data['notes'] : null,
|
||||
]);
|
||||
|
||||
return (int) $pdo->lastInsertId();
|
||||
}
|
||||
|
||||
function list_school_attendance_records_by_cycle(int $centerApplicationId, int $cycleId): array
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->prepare(
|
||||
'SELECT ar.*, s.student_code, s.full_name, s.grade_level, s.guardian_phone
|
||||
FROM school_attendance_records ar
|
||||
INNER JOIN school_students s ON s.id = ar.student_id
|
||||
WHERE ar.center_application_id = :center_application_id AND ar.cycle_id = :cycle_id
|
||||
ORDER BY ar.attendance_date DESC, ar.created_at DESC, ar.id DESC'
|
||||
);
|
||||
$stmt->execute([
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':cycle_id' => $cycleId,
|
||||
]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
function school_attendance_metrics_by_cycle(int $centerApplicationId, int $cycleId): array
|
||||
{
|
||||
$pdo = db_connection();
|
||||
$stmt = $pdo->prepare(
|
||||
"SELECT
|
||||
COUNT(*) AS total,
|
||||
COALESCE(SUM(attendance_status = 'absent'), 0) AS absent_count,
|
||||
COALESCE(SUM(attendance_status = 'excused'), 0) AS excused_count,
|
||||
COALESCE(SUM(attendance_status = 'late'), 0) AS late_count,
|
||||
COUNT(DISTINCT student_id) AS affected_students,
|
||||
MAX(attendance_date) AS latest_date,
|
||||
COALESCE(SUM(attendance_date = CURDATE()), 0) AS today_count
|
||||
FROM school_attendance_records
|
||||
WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id"
|
||||
);
|
||||
$stmt->execute([
|
||||
':center_application_id' => $centerApplicationId,
|
||||
':cycle_id' => $cycleId,
|
||||
]);
|
||||
$row = $stmt->fetch() ?: [];
|
||||
|
||||
return [
|
||||
'total' => (int) ($row['total'] ?? 0),
|
||||
'absent' => (int) ($row['absent_count'] ?? 0),
|
||||
'excused' => (int) ($row['excused_count'] ?? 0),
|
||||
'late' => (int) ($row['late_count'] ?? 0),
|
||||
'affected_students' => (int) ($row['affected_students'] ?? 0),
|
||||
'latest_date' => (string) ($row['latest_date'] ?? ''),
|
||||
'today_count' => (int) ($row['today_count'] ?? 0),
|
||||
];
|
||||
}
|
||||
115
students.php
115
students.php
@ -4,28 +4,46 @@ 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 = 'لا توجد دورة بعد';
|
||||
|
||||
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) {
|
||||
[$values, $errors] = validate_student_input($_POST);
|
||||
|
||||
if (!$isApprovedSchool) {
|
||||
$errors['form'] = 'لا يمكن فتح تسجيل الطلاب قبل اعتماد المركز.';
|
||||
} elseif ($selectedCycleId <= 0) {
|
||||
$errors['form'] = 'يرجى إنشاء دورة موسمية أولاً من صفحة المركز.';
|
||||
} elseif ($isCycleReadOnly) {
|
||||
$errors['form'] = 'هذه الدورة مؤرشفة للقراءة فقط. افتح دورة جديدة أو اختر دورة نشطة لإضافة طلاب جدد.';
|
||||
}
|
||||
|
||||
if ($errors === []) {
|
||||
try {
|
||||
create_student((int) $application['id'], $values);
|
||||
set_flash('success', 'تم تسجيل الطالب بنجاح وإضافته إلى كشف المدرسة.');
|
||||
header('Location: students.php?id=' . urlencode((string) $application['id']));
|
||||
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['student_code'] = 'هذا الكود مستخدم مسبقاً داخل نفس الدورة الموسمية.';
|
||||
} else {
|
||||
$errors['form'] = 'تعذر حفظ بيانات الطالب حالياً. يرجى المحاولة مرة أخرى.';
|
||||
}
|
||||
@ -35,8 +53,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && $application) {
|
||||
}
|
||||
}
|
||||
|
||||
$students = $isApprovedSchool ? list_school_students((int) $application['id']) : [];
|
||||
$metrics = $isApprovedSchool ? school_student_metrics((int) $application['id']) : [
|
||||
$students = $isApprovedSchool && $selectedCycleId > 0 ? list_school_students_by_cycle((int) $application['id'], $selectedCycleId) : [];
|
||||
$metrics = $isApprovedSchool && $selectedCycleId > 0 ? school_student_metrics_by_cycle((int) $application['id'], $selectedCycleId) : [
|
||||
'total' => 0,
|
||||
'boys' => 0,
|
||||
'girls' => 0,
|
||||
@ -47,8 +65,13 @@ $metrics = $isApprovedSchool ? school_student_metrics((int) $application['id'])
|
||||
|
||||
$expectedCapacity = $application ? (int) ($application['expected_students'] ?? 0) : 0;
|
||||
$remainingSeats = max(0, $expectedCapacity - $metrics['total']);
|
||||
$pageTitle = $application ? 'تسجيل الطلاب: ' . (string) $application['center_name'] : 'تسجيل الطلاب';
|
||||
$pageDescription = 'صفحة مستقلة لتسجيل الطلاب وإدارة كشف المدرسة بعد الاعتماد، مع فصل واضح بين النموذج والكشف والجاهزية التشغيلية.';
|
||||
$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);
|
||||
@ -77,8 +100,8 @@ render_flash($flash);
|
||||
<span>المدينة: <?= e((string) $application['city']) ?></span>
|
||||
</div>
|
||||
<div class="cta-stack mt-4">
|
||||
<a class="btn btn-dark" href="application_detail.php?id=<?= e((string) $application['id']) ?>">العودة لملف الاعتماد</a>
|
||||
<a class="btn btn-outline-secondary" href="approved_school.php?id=<?= e((string) $application['id']) ?>">صفحة المركز</a>
|
||||
<a class="btn btn-dark" href="<?= e($applicationDetailUrl) ?>">العودة لملف الاعتماد</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($approvedSchoolUrl) ?>">صفحة المركز</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -96,9 +119,11 @@ render_flash($flash);
|
||||
<span>المقاعد المتبقية <?= e((string) $remainingSeats) ?></span>
|
||||
</div>
|
||||
<div class="cta-stack mt-4">
|
||||
<a class="btn btn-dark" href="approved_school.php?id=<?= e((string) $application['id']) ?>">العودة لصفحة المركز</a>
|
||||
<a class="btn btn-outline-secondary" href="teachers.php?id=<?= e((string) $application['id']) ?>">فريق المعلمين</a>
|
||||
<a class="btn btn-outline-secondary" href="application_detail.php?id=<?= e((string) $application['id']) ?>">ملف الاعتماد</a>
|
||||
<a class="btn btn-dark" href="<?= e($approvedSchoolUrl) ?>">العودة لصفحة المركز</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($attendanceUrl) ?>">غياب الطلاب</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($applicationDetailUrl) ?>">ملف الاعتماد</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
@ -116,6 +141,56 @@ render_flash($flash);
|
||||
</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-7">
|
||||
<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">
|
||||
<a class="btn btn-outline-secondary" href="<?= e($approvedSchoolUrl) ?>#cycles">إدارة الدورات الموسمية</a>
|
||||
</div>
|
||||
<?php if ($isCycleReadOnly): ?>
|
||||
<div class="alert alert-warning mt-3 mb-0">هذه الدورة مؤرشفة حالياً، لذلك تبقى السجلات قابلة للمراجعة فقط بدون إضافة طلاب جدد.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<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('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>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row g-4 align-items-start">
|
||||
<div class="col-lg-4">
|
||||
<div class="app-card sidebar-card mb-4">
|
||||
@ -130,7 +205,10 @@ render_flash($flash);
|
||||
<div class="alert alert-danger mb-3"><?= e($errors['form']) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" novalidate>
|
||||
<?php if ($isCycleReadOnly): ?>
|
||||
<div class="alert alert-warning mb-0">هذه الدورة مؤرشفة. يمكنك مراجعة كشف الطلاب فقط، أو فتح دورة جديدة من صفحة المركز.</div>
|
||||
<?php else: ?>
|
||||
<form method="post" novalidate>
|
||||
<div class="form-section-block mb-3">
|
||||
<div class="form-section-heading">
|
||||
<div>
|
||||
@ -214,15 +292,16 @@ render_flash($flash);
|
||||
<div class="d-grid">
|
||||
<button class="btn btn-dark" type="submit">حفظ الطالب</button>
|
||||
</div>
|
||||
</form>
|
||||
</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><div class="mt-2"><a class="btn btn-sm btn-outline-secondary" href="teachers.php?id=<?= e((string) $application['id']) ?>">فتح صفحة المعلمين</a></div></li>
|
||||
<li><strong>التقييمات والأوزان</strong><span class="section-subtle">الأساس جاهز لتخزين نوع التقييم، المقياس، والوزن.</span></li>
|
||||
<li><strong>غياب الطلاب</strong><span class="section-subtle">سجل الغياب موجود في القاعدة وسيتم بناء صفحته المنفصلة لاحقاً.</span></li>
|
||||
<li><strong>المعلمين</strong><span class="section-subtle">الصفحة أصبحت جاهزة الآن لإدارة الفريق التعليمي لهذا المركز.</span><div class="mt-2"><a class="btn btn-sm btn-outline-secondary" href="<?= e($teachersUrl) ?>">فتح صفحة المعلمين</a></div></li>
|
||||
<li><strong>التقييمات والأوزان</strong><span class="section-subtle">الصفحة أصبحت جاهزة الآن لتعريف أنواع التقييم والمقاييس والأوزان.</span><div class="mt-2"><a class="btn btn-sm btn-outline-secondary" href="<?= e($assessmentsUrl) ?>">فتح صفحة التقييمات</a></div></li>
|
||||
<li><strong>غياب الطلاب</strong><span class="section-subtle">الصفحة أصبحت جاهزة الآن لتسجيل الغياب اليومي، الأعذار، وحالات التأخر.</span><div class="mt-2"><a class="btn btn-sm btn-outline-secondary" href="<?= e($attendanceUrl) ?>">فتح صفحة الغياب</a></div></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
126
teachers.php
126
teachers.php
@ -4,51 +4,73 @@ 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;
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $application) {
|
||||
[$values, $errors] = validate_teacher_input($_POST);
|
||||
|
||||
if (!$isApprovedSchool) {
|
||||
$errors['form'] = 'لا يمكن فتح صفحة المعلمين قبل اعتماد المركز.';
|
||||
} elseif ($selectedCycleId <= 0) {
|
||||
$errors['form'] = 'يرجى إنشاء دورة موسمية أولاً من صفحة المركز.';
|
||||
} elseif ($isCycleReadOnly) {
|
||||
$errors['form'] = 'هذه الدورة مؤرشفة للقراءة فقط. افتح دورة جديدة أو اختر دورة نشطة لإضافة أعضاء جدد.';
|
||||
}
|
||||
|
||||
if ($errors === []) {
|
||||
try {
|
||||
create_teacher((int) $application['id'], $values);
|
||||
set_flash('success', 'تمت إضافة عضو الفريق بنجاح إلى سجل المدرسة.');
|
||||
header('Location: teachers.php?id=' . urlencode((string) $application['id']));
|
||||
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'] = 'تعذر حفظ بيانات عضو الفريق حالياً. يرجى المحاولة مرة أخرى.';
|
||||
$errors['form'] = 'تعذر حفظ بيانات الفريق حالياً. يرجى المحاولة مرة أخرى.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$teachers = $isApprovedSchool ? list_school_teachers((int) $application['id']) : [];
|
||||
$metrics = $isApprovedSchool ? school_teacher_metrics((int) $application['id']) : [
|
||||
$teachers = $isApprovedSchool && $selectedCycleId > 0 ? list_school_teachers_by_cycle((int) $application['id'], $selectedCycleId) : [];
|
||||
$metrics = $isApprovedSchool && $selectedCycleId > 0 ? school_teacher_metrics_by_cycle((int) $application['id'], $selectedCycleId) : [
|
||||
'total' => 0,
|
||||
'active' => 0,
|
||||
'pending' => 0,
|
||||
'inactive' => 0,
|
||||
'email_ready' => 0,
|
||||
'teachers' => 0,
|
||||
'supervisors' => 0,
|
||||
'email_ready' => 0,
|
||||
];
|
||||
|
||||
$studentMetrics = $isApprovedSchool ? school_student_metrics((int) $application['id']) : [
|
||||
$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,
|
||||
'boys' => 0,
|
||||
'girls' => 0,
|
||||
];
|
||||
|
||||
$pageTitle = $application ? 'فريق المعلمين: ' . (string) $application['center_name'] : 'فريق المعلمين';
|
||||
$pageDescription = 'صفحة مستقلة لإدارة المعلمين والمشرفين بعد اعتماد المدرسة، مع فصل واضح بين إضافة الكادر وكشف الفريق ومؤشرات الجاهزية.';
|
||||
$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);
|
||||
@ -77,8 +99,8 @@ render_flash($flash);
|
||||
<span>المدينة: <?= e((string) $application['city']) ?></span>
|
||||
</div>
|
||||
<div class="cta-stack mt-4">
|
||||
<a class="btn btn-dark" href="application_detail.php?id=<?= e((string) $application['id']) ?>">العودة لملف الاعتماد</a>
|
||||
<a class="btn btn-outline-secondary" href="approved_school.php?id=<?= e((string) $application['id']) ?>">صفحة المركز</a>
|
||||
<a class="btn btn-dark" href="<?= e($applicationDetailUrl) ?>">العودة لملف الاعتماد</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($approvedSchoolUrl) ?>">صفحة المركز</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -96,9 +118,11 @@ render_flash($flash);
|
||||
<span><?= e((string) $metrics['supervisors']) ?> أدوار إشرافية</span>
|
||||
</div>
|
||||
<div class="cta-stack mt-4">
|
||||
<a class="btn btn-dark" href="approved_school.php?id=<?= e((string) $application['id']) ?>">العودة لصفحة المركز</a>
|
||||
<a class="btn btn-outline-secondary" href="students.php?id=<?= e((string) $application['id']) ?>">تسجيل الطلاب</a>
|
||||
<a class="btn btn-outline-secondary" href="application_detail.php?id=<?= e((string) $application['id']) ?>">ملف الاعتماد</a>
|
||||
<a class="btn btn-dark" href="<?= e($approvedSchoolUrl) ?>">العودة لصفحة المركز</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($studentsUrl) ?>">تسجيل الطلاب</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($assessmentsUrl) ?>">التقييمات والأوزان</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($attendanceUrl) ?>">غياب الطلاب</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($applicationDetailUrl) ?>">ملف الاعتماد</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
@ -115,6 +139,56 @@ render_flash($flash);
|
||||
</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-7">
|
||||
<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">
|
||||
<a class="btn btn-outline-secondary" href="<?= e($approvedSchoolUrl) ?>#cycles">إدارة الدورات الموسمية</a>
|
||||
</div>
|
||||
<?php if ($isCycleReadOnly): ?>
|
||||
<div class="alert alert-warning mt-3 mb-0">هذه الدورة مؤرشفة، لذلك تبقى صفحة الفريق للقراءة فقط حالياً.</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<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('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>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row g-4 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>
|
||||
@ -136,6 +210,9 @@ render_flash($flash);
|
||||
<div class="alert alert-danger"><?= e($errors['form']) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($isCycleReadOnly): ?>
|
||||
<div class="alert alert-warning mb-0">هذه الدورة مؤرشفة. يمكنك مراجعة الفريق فقط، أو فتح دورة جديدة من صفحة المركز.</div>
|
||||
<?php else: ?>
|
||||
<form method="post" class="vstack gap-3" novalidate>
|
||||
<div>
|
||||
<label class="form-label" for="full_name">الاسم الكامل</label>
|
||||
@ -191,13 +268,14 @@ render_flash($flash);
|
||||
<button class="btn btn-dark" 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>
|
||||
<li><strong>التقييمات والأوزان</strong><span class="section-subtle">الصفحة أصبحت جاهزة الآن لبناء أنواع التقييم وربط الأوزان التشغيلية بالمركز.</span><div class="mt-2"><a class="btn btn-sm btn-outline-secondary" href="<?= e($assessmentsUrl) ?>">فتح صفحة التقييمات</a></div></li>
|
||||
<li><strong>غياب الطلاب</strong><span class="section-subtle">الصفحة أصبحت جاهزة الآن لتسجيل الغياب وربطه بالطلاب المعتمدين داخل المركز.</span><div class="mt-2"><a class="btn btn-sm btn-outline-secondary" href="<?= e($attendanceUrl) ?>">فتح صفحة الغياب</a></div></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@ -268,8 +346,10 @@ render_flash($flash);
|
||||
<div class="col-md-6"><div class="school-data-item"><strong>البريد الرسمي</strong><span><?= e((string) $application['email']) ?></span></div></div>
|
||||
</div>
|
||||
<div class="cta-stack mt-4">
|
||||
<a class="btn btn-outline-secondary" href="students.php?id=<?= e((string) $application['id']) ?>">العودة إلى الطلاب</a>
|
||||
<a class="btn btn-outline-secondary" href="approved_school.php?id=<?= e((string) $application['id']) ?>">صفحة المركز</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($studentsUrl) ?>">العودة إلى الطلاب</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($assessmentsUrl) ?>">فتح التقييمات</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($attendanceUrl) ?>">فتح الغياب</a>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($approvedSchoolUrl) ?>">صفحة المركز</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user