39669-vm/attendance.php
2026-04-16 17:50:08 +00:00

395 lines
25 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/app.php';
$flash = consume_flash();
$applicationId = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT) ?: 0;
$requestedCycleId = filter_input(INPUT_GET, 'cycle', FILTER_VALIDATE_INT) ?: 0;
$application = $applicationId > 0 ? get_application($applicationId) : null;
$isApprovedSchool = $application && (string) $application['status'] === 'approved';
$values = attendance_defaults();
$errors = [];
$cycleContext = ['cycles' => [], 'selected' => null, 'active' => null, 'read_only' => false];
$selectedCycle = null;
$selectedCycleId = 0;
$isCycleReadOnly = false;
$cycleLabel = 'لا توجد دورة بعد';
if ($application && $isApprovedSchool) {
$cycleContext = resolve_school_cycle_context((int) $application['id'], $application, $requestedCycleId);
$selectedCycle = $cycleContext['selected'];
$selectedCycleId = $selectedCycle ? (int) ($selectedCycle['id'] ?? 0) : 0;
$isCycleReadOnly = (bool) $cycleContext['read_only'];
$cycleLabel = $selectedCycle ? (string) $selectedCycle['cycle_name'] : $cycleLabel;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $application) {
if (!$isApprovedSchool) {
$errors['form'] = 'لا يمكن فتح سجل الغياب قبل اعتماد المركز.';
} elseif ($selectedCycleId <= 0) {
$errors['form'] = 'يرجى إنشاء دورة موسمية أولاً من صفحة المركز.';
} elseif ($isCycleReadOnly) {
$errors['form'] = 'هذه الدورة مؤرشفة للقراءة فقط. افتح دورة جديدة أو اختر دورة نشطة لإضافة غياب جديد.';
} else {
[$values, $errors] = validate_attendance_input_for_cycle((int) $application['id'], $selectedCycleId, $_POST);
if ($errors === []) {
try {
create_attendance_record_in_cycle((int) $application['id'], $selectedCycleId, $values);
set_flash('success', 'تم حفظ سجل الغياب داخل الدورة الموسمية المحددة.');
header('Location: ' . school_page_url('attendance.php', (int) $application['id'], $selectedCycleId));
exit;
} catch (PDOException $exception) {
$duplicateCode = isset($exception->errorInfo[1]) && (int) $exception->errorInfo[1] === 1062;
if ($duplicateCode) {
$errors['attendance_date'] = 'يوجد سجل غياب سابق لهذا الطالب في نفس التاريخ داخل هذه الدورة.';
} else {
$errors['form'] = 'تعذر حفظ سجل الغياب حالياً. يرجى المحاولة مرة أخرى.';
}
} catch (Throwable $exception) {
$errors['form'] = 'تعذر حفظ سجل الغياب حالياً. يرجى المحاولة مرة أخرى.';
}
}
}
}
$students = $isApprovedSchool && $selectedCycleId > 0 ? list_school_students_by_cycle((int) $application['id'], $selectedCycleId) : [];
$studentOptions = $isApprovedSchool && $selectedCycleId > 0 ? school_student_options_by_cycle((int) $application['id'], $selectedCycleId) : [];
$search = clean_text($_GET['search'] ?? '', 255);
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT) ?: 1;
$limit = 10;
$offset = ($page - 1) * $limit;
$records = $isApprovedSchool && $selectedCycleId > 0 ? list_school_attendance_records_by_cycle((int) $application['id'], $selectedCycleId, $search, $limit, $offset) : [];
$totalRecords = $isApprovedSchool && $selectedCycleId > 0 ? count_school_attendance_records_by_cycle((int) $application['id'], $selectedCycleId, $search) : 0;
$metrics = $isApprovedSchool && $selectedCycleId > 0 ? school_attendance_metrics_by_cycle((int) $application['id'], $selectedCycleId) : [
'total' => 0,
'absent' => 0,
'excused' => 0,
'late' => 0,
'affected_students' => 0,
'latest_date' => '',
'today_count' => 0,
];
$studentMetrics = $isApprovedSchool && $selectedCycleId > 0 ? school_student_metrics_by_cycle((int) $application['id'], $selectedCycleId) : [
'total' => 0,
'boys' => 0,
'girls' => 0,
'active' => 0,
'waiting' => 0,
'withdrawn' => 0,
];
$teacherMetrics = $isApprovedSchool && $selectedCycleId > 0 ? school_teacher_metrics_by_cycle((int) $application['id'], $selectedCycleId) : [
'total' => 0,
'active' => 0,
'pending' => 0,
'inactive' => 0,
'teachers' => 0,
'supervisors' => 0,
];
$assessmentMetrics = $isApprovedSchool && $selectedCycleId > 0 ? school_assessment_metrics_by_cycle((int) $application['id'], $selectedCycleId) : [
'total' => 0,
'active' => 0,
'inactive' => 0,
'total_weight' => 0,
'active_weight' => 0,
'average_max_score' => 0,
'percentage' => 0,
'points' => 0,
'rubric' => 0,
];
$trackedStudents = max(1, $studentMetrics['active']);
$incidentRate = $metrics['total'] > 0 ? round(($metrics['affected_students'] / $trackedStudents) * 100, 1) : 0.0;
$latestDateLabel = $metrics['latest_date'] !== '' ? $metrics['latest_date'] : 'لا يوجد';
$pageTitle = $application ? 'غياب الطلاب: ' . (string) $application['center_name'] . ($selectedCycle ? ' — ' . $cycleLabel : '') : 'غياب الطلاب';
$pageDescription = 'صفحة مستقلة لتسجيل غياب الطلاب والأعذار وحالات التأخر للمراكز المعتمدة داخل دورة موسمية محددة.';
$approvedSchoolUrl = $application ? school_page_url('approved_school.php', (int) $application['id'], $selectedCycleId) : 'approved_school.php';
$studentsUrl = $application ? school_page_url('students.php', (int) $application['id'], $selectedCycleId) : 'students.php';
$teachersUrl = $application ? school_page_url('teachers.php', (int) $application['id'], $selectedCycleId) : 'teachers.php';
$assessmentsUrl = $application ? school_page_url('assessments.php', (int) $application['id'], $selectedCycleId) : 'assessments.php';
$applicationDetailUrl = $application ? 'application_detail.php?id=' . urlencode((string) $application['id']) : 'application_detail.php';
if (!$application) {
http_response_code(404);
}
render_page_start($pageTitle, 'approved', $pageDescription, (string) ($application['favicon'] ?? ''));
render_flash($flash);
?>
<section class="py-4 py-lg-5">
<div class="container-xxl">
<div class="row g-4 align-items-start">
<div class="col-lg-3">
<?php if ($application) { require __DIR__ . '/includes/center_sidebar.php'; } else { require __DIR__ . '/includes/sidebar.php'; } ?>
</div>
<div class="col-lg-9">
<?php if (!$application): ?>
<div class="app-card text-center py-5">
<div class="empty-title mb-2">المدرسة غير موجودة</div>
<p class="text-muted mb-3">تحقق من رابط المدرسة أو ارجع إلى قائمة المراكز المعتمدة.</p>
<a class="btn btn-primary" href="applications.php?status=approved">المراكز المعتمدة</a>
</div>
<?php elseif (!$isApprovedSchool): ?>
<div class="page-banner mb-4 mb-lg-5">
<div class="row g-4 align-items-center">
<div class="col-lg-8">
<span class="eyebrow mb-3">الغياب يبدأ بعد الاعتماد</span>
<h1 class="page-title mb-3"><?= e((string) $application['center_name']) ?></h1>
<p class="page-copy mb-3">تم تجهيز صفحة غياب الطلاب، لكن استخدامها مرتبط بتحويل حالة المركز إلى <strong>معتمد</strong> أولاً حتى يبقى السجل اليومي محصوراً بالمراكز الجاهزة للتشغيل.</p>
<div class="hero-meta">
<span>الحالة الحالية: <?= e(status_meta((string) $application['status'])['label']) ?></span>
<span>المدينة: <?= e((string) $application['city']) ?></span>
</div>
<div class="cta-stack mt-4">
<a class="btn btn-primary" href="<?= e($applicationDetailUrl) ?>">العودة لملف الاعتماد</a>
<a class="btn btn-outline-secondary" href="<?= e($approvedSchoolUrl) ?>">صفحة المركز</a>
</div>
</div>
</div>
</div>
<?php else: ?>
<div class="page-banner approved-hero mb-4 mb-lg-5">
<div class="row g-4 align-items-start">
<div class="col-lg-8">
<span class="approved-kicker mb-3">صفحة مستقلة لغياب الطلاب</span>
<h1 class="page-title mb-3">سجل الغياب — <?= e((string) $application['center_name']) ?></h1>
<p class="page-copy mb-3">هنا يتم تسجيل الغياب اليومي، الأعذار، وحالات التأخر في صفحة تشغيلية منفصلة. كل سجل يرتبط مباشرة بطالب محدد داخل هذا المركز فقط.</p>
<div class="hero-meta">
<span><?= e((string) $application['city']) ?></span>
<span><?= e((string) $metrics['affected_students']) ?> طلاب عليهم ملاحظات حضور</span>
<span>آخر تحديث <?= e($latestDateLabel) ?></span>
</div>
<div class="cta-stack mt-4">
<a class="btn btn-primary" href="admin.php">لوحة الإدارة</a>
<a class="btn btn-outline-secondary" href="<?= e($approvedSchoolUrl) ?>">العودة لصفحة المركز</a>
<a class="btn btn-outline-secondary" href="<?= e($studentsUrl) ?>">تسجيل الطلاب</a>
<a class="btn btn-outline-secondary" href="<?= e($teachersUrl) ?>">فريق المعلمين</a>
<a class="btn btn-outline-secondary" href="<?= e($assessmentsUrl) ?>">التقييمات والأوزان</a>
<a class="btn btn-outline-secondary" href="<?= e($applicationDetailUrl) ?>">ملف الاعتماد</a>
</div>
</div>
<div class="col-lg-4">
<div class="app-card h-100 approved-note">
<div class="section-title mb-3">ملخص الانضباط اليومي</div>
<?php if ($studentMetrics['active'] > 0): ?>
<div class="score-display mb-3"><strong><?= e(number_format($incidentRate, 1, '.', '')) ?>%</strong><span>نسبة الطلاب المتأثرين</span></div>
<div class="score-bar mb-3" aria-label="نسبة الطلاب المتأثرين"><span style="width: <?= e((string) min(100, max(0, $incidentRate))) ?>%"></span></div>
<?php endif; ?>
<div class="summary-stack">
<div class="summary-row"><span>السجلات اليوم</span><strong><?= e((string) $metrics['today_count']) ?></strong></div>
<div class="summary-row"><span>الغياب بدون عذر</span><strong><?= e((string) $metrics['absent']) ?></strong></div>
<div class="summary-row"><span>الغياب بعذر / تأخر</span><strong><?= e((string) ($metrics['excused'] + $metrics['late'])) ?></strong></div>
</div>
</div>
</div>
</div>
</div>
<?php if ($selectedCycle): ?>
<?php $cycleStatusMap = school_cycle_status_map(); ?>
<div class="row g-4 mb-4 align-items-start">
<div class="col-lg-<?= is_super_admin() ? '7' : '12' ?>">
<div class="app-card h-100">
<div class="section-head mb-3">
<div>
<div class="section-title">الدورة الموسمية الحالية</div>
<div class="section-copy">سجلات الغياب في هذه الصفحة تخص الدورة <strong><?= e($cycleLabel) ?></strong>. عند أرشفة الدورة تبقى السجلات محفوظة للمراجعة فقط بدون خلطها بالموسم التالي.</div>
</div>
<?= school_cycle_status_badge((string) $selectedCycle['status']) ?>
</div>
<div class="row g-3">
<div class="col-md-4"><div class="school-data-item"><strong>اسم الدورة</strong><span><?= e($cycleLabel) ?></span></div></div>
<div class="col-md-4"><div class="school-data-item"><strong>الفترة</strong><span><?= e((string) $selectedCycle['start_date']) ?> → <?= e((string) $selectedCycle['end_date']) ?></span></div></div>
<div class="col-md-4"><div class="school-data-item"><strong>عدد الدورات</strong><span><?= e((string) count($cycleContext['cycles'])) ?> دورة للمركز</span></div></div>
</div>
<div class="cta-stack mt-3">
<?php if (is_super_admin()): ?><a class="btn btn-outline-secondary" href="<?= e($approvedSchoolUrl) ?>#cycles">إدارة الدورات الموسمية</a><?php endif; ?>
</div>
<?php if ($isCycleReadOnly): ?>
<div class="alert alert-warning mt-3 mb-0">هذه الدورة مؤرشفة، لذلك تبقى صفحة الغياب للقراءة فقط حالياً.</div>
<?php endif; ?>
</div>
</div>
<?php if (is_super_admin()): ?>
<div class="col-lg-5">
<div class="app-card sidebar-card h-100">
<div class="section-title mb-3">التبديل بين الدورات</div>
<p class="section-subtle mb-3">افتح سجل الغياب لنفس المركز في أي موسم سابق أو حالي مباشرة من هذه الصفحة.</p>
<div class="quick-link-stack">
<?php foreach ($cycleContext['cycles'] as $cycle): ?>
<?php
$isCurrentCycleLink = (int) $cycle['id'] === $selectedCycleId;
$isActiveCycleLink = (int) $cycle['id'] === (int) (($cycleContext['active']['id'] ?? 0));
$cycleStatusLabel = (string) ($cycleStatusMap[$cycle['status']]['label'] ?? 'غير معروف');
$cycleMetaLine = (string) $cycle['start_date'] . ' → ' . (string) $cycle['end_date'] . ' — ' . $cycleStatusLabel . ($isActiveCycleLink ? ' — النشطة حالياً' : '');
?>
<a class="quick-link-item <?= $isCurrentCycleLink ? 'is-current' : '' ?>" href="<?= e(school_page_url('attendance.php', (int) $application['id'], (int) $cycle['id'])) ?>">
<div>
<strong><?= e((string) $cycle['cycle_name']) ?><?= $isCurrentCycleLink ? ' — المعروضة الآن' : '' ?></strong>
<span><?= e($cycleMetaLine) ?></span>
</div>
</a>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<div class="row g-4 align-items-start">
<div class="col-lg-4">
<div class="app-card sidebar-card mb-4">
<div class="section-title mb-3">تسجيل غياب أو تأخر</div>
<p class="section-subtle mb-3">اختر الطالب والتاريخ والحالة، ثم أضف سبباً مختصراً وملاحظة متابعة عند الحاجة.</p>
<?php if ($isCycleReadOnly): ?>
<div class="alert alert-warning mb-0">هذه الدورة مؤرشفة. يمكنك مراجعة السجل فقط، أو فتح دورة جديدة من صفحة المركز.</div>
<?php elseif ($students === []): ?>
<div class="alert alert-warning mb-0">ابدأ أولاً من صفحة الطلاب لإضافة الطلاب قبل تسجيل أي غياب.</div>
<?php else: ?>
<?php if (isset($errors['form'])): ?>
<div class="alert alert-danger"><?= e($errors['form']) ?></div>
<?php endif; ?>
<form method="post" class="vstack gap-3" novalidate>
<div>
<label class="form-label" for="student_id">الطالب</label>
<select class="form-select <?= isset($errors['student_id']) ? 'is-invalid' : '' ?>" id="student_id" name="student_id">
<option value="">اختر الطالب</option>
<?php foreach ($studentOptions as $studentId => $studentMeta): ?>
<option value="<?= e((string) $studentId) ?>" <?= $values['student_id'] === (string) $studentId ? 'selected' : '' ?>><?= e($studentMeta['label'] . ' — ' . $studentMeta['grade_level']) ?></option>
<?php endforeach; ?>
</select>
<?php if (isset($errors['student_id'])): ?><div class="invalid-feedback"><?= e($errors['student_id']) ?></div><?php endif; ?>
</div>
<div>
<label class="form-label" for="attendance_date">التاريخ</label>
<input class="form-control <?= isset($errors['attendance_date']) ? 'is-invalid' : '' ?>" type="date" id="attendance_date" name="attendance_date" value="<?= e($values['attendance_date']) ?>">
<?php if (isset($errors['attendance_date'])): ?><div class="invalid-feedback"><?= e($errors['attendance_date']) ?></div><?php endif; ?>
</div>
<div>
<label class="form-label" for="attendance_status">الحالة</label>
<select class="form-select <?= isset($errors['attendance_status']) ? 'is-invalid' : '' ?>" id="attendance_status" name="attendance_status">
<?php foreach (attendance_status_map() as $statusKey => $statusMeta): ?>
<option value="<?= e($statusKey) ?>" <?= $values['attendance_status'] === $statusKey ? 'selected' : '' ?>><?= e($statusMeta['label']) ?></option>
<?php endforeach; ?>
</select>
<?php if (isset($errors['attendance_status'])): ?><div class="invalid-feedback"><?= e($errors['attendance_status']) ?></div><?php endif; ?>
</div>
<div>
<label class="form-label" for="absence_reason">سبب الغياب / التأخر</label>
<input class="form-control <?= isset($errors['absence_reason']) ? 'is-invalid' : '' ?>" id="absence_reason" name="absence_reason" value="<?= e($values['absence_reason']) ?>" placeholder="مثال: ظرف صحي أو تأخر في المواصلات">
<?php if (isset($errors['absence_reason'])): ?><div class="invalid-feedback"><?= e($errors['absence_reason']) ?></div><?php endif; ?>
</div>
<div>
<label class="form-label" for="notes">ملاحظات المتابعة</label>
<textarea class="form-control" id="notes" name="notes" rows="4" placeholder="مثال: تم التواصل مع ولي الأمر أو تحويل الحالة للمرشد."><?= e($values['notes']) ?></textarea>
</div>
<div class="d-grid">
<button class="btn btn-primary" type="submit">حفظ سجل الغياب</button>
</div>
</form>
<?php endif; ?>
</div>
<div class="app-card sidebar-card">
<div class="section-title mb-3">خطوات المتابعة التالية</div>
<ul class="module-roadmap-list mb-0">
<li><strong>التواصل مع أولياء الأمور</strong><span class="section-subtle">يمكن لاحقاً إضافة إشعارات أو رسائل عند تكرار الغياب لنفس الطالب.</span></li>
<li><strong>تقارير أسبوعية</strong><span class="section-subtle">المرحلة القادمة الطبيعية هي تلخيص السجلات حسب الأسبوع أو الفصل التشغيلي.</span></li>
</ul>
</div>
</div>
<div class="col-lg-8">
<div class="app-card mb-4">
<div class="section-head mb-3">
<div>
<div class="section-title">كشف الغياب اليومي</div>
<div class="section-copy">جميع سجلات الغياب والتأخر المرتبطة بهذا المركز فقط، مرتبة من الأحدث إلى الأقدم.</div>
</div>
<span class="header-chip"><?= e((string) $metrics['total']) ?> سجل / <?= e((string) $metrics['affected_students']) ?> طلاب</span>
</div>
<?php render_search_bar($search, 'ابحث باسم الطالب أو الكود...', school_page_url('attendance.php', (int)$application['id'], $selectedCycleId), $_GET); ?>
<div class="row g-3 mb-3">
<div class="col-md-3"><div class="school-data-item"><strong>الغياب</strong><span><?= e((string) $metrics['absent']) ?> حالة</span></div></div>
<div class="col-md-3"><div class="school-data-item"><strong>بعذر</strong><span><?= e((string) $metrics['excused']) ?> حالة</span></div></div>
<div class="col-md-3"><div class="school-data-item"><strong>تأخر</strong><span><?= e((string) $metrics['late']) ?> حالة</span></div></div>
<div class="col-md-3"><div class="school-data-item"><strong>السجلات اليوم</strong><span><?= e((string) $metrics['today_count']) ?> حالة</span></div></div>
</div>
<?php if ($records === []): ?>
<div class="empty-state text-center p-4">
<div class="empty-title mb-2">لا توجد سجلات غياب بعد</div>
<p class="text-muted mb-0">ابدأ من النموذج في الجانب الأيمن لإضافة أول سجل غياب أو تأخر لهذا المركز.</p>
</div>
<?php else: ?>
<div class="table-responsive">
<table class="table app-table align-middle">
<thead>
<tr>
<th>الطالب</th>
<th>التاريخ</th>
<th>الحالة</th>
<th>السبب</th>
<th>المتابعة</th>
</tr>
</thead>
<tbody>
<?php foreach ($records as $record): ?>
<tr>
<td>
<strong><?= e((string) $record['full_name']) ?></strong>
<small><?= e((string) $record['student_code']) ?> — <?= e((string) $record['grade_level']) ?></small>
</td>
<td><?= e((string) $record['attendance_date']) ?></td>
<td><?= attendance_status_badge((string) $record['attendance_status']) ?></td>
<td><?= e((string) ($record['absence_reason'] ?: '—')) ?></td>
<td>
<strong><?= e((string) ($record['guardian_phone'] ?: '—')) ?></strong>
<?php if (!empty($record['notes'])): ?><small><?= e((string) $record['notes']) ?></small><?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php render_pagination($totalRecords, $limit, $page, $_GET); ?>
<?php endif; ?>
</div>
<div class="app-card">
<div class="section-title mb-3">سياق المدرسة</div>
<div class="row g-3">
<div class="col-md-6"><div class="school-data-item"><strong>الطلاب النشطون</strong><span><?= e((string) $studentMetrics['active']) ?> طالب/طالبة</span></div></div>
<div class="col-md-6"><div class="school-data-item"><strong>الفريق التعليمي</strong><span><?= e((string) $teacherMetrics['total']) ?> عضو</span></div></div>
<div class="col-md-6"><div class="school-data-item"><strong>التقييمات المفعلة</strong><span><?= e((string) $assessmentMetrics['active']) ?> نوع</span></div></div>
<div class="col-md-6"><div class="school-data-item"><strong>آخر سجل</strong><span><?= e($latestDateLabel) ?></span></div></div>
</div>
<div class="cta-stack mt-4">
<a class="btn btn-outline-secondary" href="<?= e($assessmentsUrl) ?>">العودة إلى التقييمات</a>
<a class="btn btn-outline-secondary" href="<?= e($teachersUrl) ?>">العودة إلى المعلمين</a>
<a class="btn btn-outline-secondary" href="<?= e($studentsUrl) ?>">العودة إلى الطلاب</a>
<a class="btn btn-outline-secondary" href="<?= e($approvedSchoolUrl) ?>">صفحة المركز</a>
</div>
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
</section>
<?php render_page_end();