Autosave: 20260421-074336

This commit is contained in:
Flatlogic Bot 2026-04-21 07:43:36 +00:00
parent faff5cf4a0
commit 2f04e1c8d5
9 changed files with 1322 additions and 64 deletions

273
assessment_categories.php Normal file
View File

@ -0,0 +1,273 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/app.php';
if (!is_super_admin()) {
http_response_code(403);
render_page_start('صلاحيات غير كافية', '');
?>
<section class="py-5 text-center">
<div class="container-xxl">
<h1 class="mb-3">عذراً</h1>
<p>هذه الصفحة مخصصة للمشرف العام فقط.</p>
<a href="index.php" class="btn btn-primary mt-3">العودة للرئيسية</a>
</div>
</section>
<?php
render_page_end();
exit;
}
// Handle POST actions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
if ($action === 'create') {
$name = clean_text($_POST['name'] ?? '', 255);
$description = clean_text($_POST['description'] ?? '', 1000);
$status = in_array($_POST['status'] ?? '', ['enabled', 'disabled']) ? $_POST['status'] : 'enabled';
if ($name !== '') {
$stmt = db()->prepare('INSERT INTO assessment_categories (name, description, status) VALUES (?, ?, ?)');
$stmt->execute([$name, $description, $status]);
set_flash('success', 'تمت إضافة الفئة بنجاح.');
} else {
set_flash('error', 'اسم الفئة مطلوب.');
}
header('Location: assessment_categories.php');
exit;
}
if ($action === 'edit') {
$id = (int)($_POST['id'] ?? 0);
$name = clean_text($_POST['name'] ?? '', 255);
$description = clean_text($_POST['description'] ?? '', 1000);
$status = in_array($_POST['status'] ?? '', ['enabled', 'disabled']) ? $_POST['status'] : 'enabled';
if ($id > 0 && $name !== '') {
$stmt = db()->prepare('UPDATE assessment_categories SET name = ?, description = ?, status = ? WHERE id = ?');
$stmt->execute([$name, $description, $status, $id]);
set_flash('success', 'تم تحديث الفئة بنجاح.');
} else {
set_flash('error', 'تأكد من إدخال اسم الفئة.');
}
header('Location: assessment_categories.php');
exit;
}
if ($action === 'delete') {
$id = (int)($_POST['id'] ?? 0);
if ($id > 0) {
$stmt = db()->prepare('DELETE FROM assessment_categories WHERE id = ?');
$stmt->execute([$id]);
set_flash('success', 'تم حذف الفئة بنجاح.');
}
header('Location: assessment_categories.php');
exit;
}
}
// Read list
$search = clean_text($_GET['search'] ?? '', 255);
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT) ?: 1;
$limit = 10;
$offset = ($page - 1) * $limit;
$query = 'SELECT * FROM assessment_categories';
$countQuery = 'SELECT COUNT(*) FROM assessment_categories';
$params = [];
if ($search !== '') {
$where = ' WHERE name LIKE ? OR description LIKE ?';
$query .= $where;
$countQuery .= $where;
$params[] = "%$search%";
$params[] = "%$search%";
}
$stmtCount = db()->prepare($countQuery);
$stmtCount->execute($params);
$totalItems = (int)$stmtCount->fetchColumn();
$query .= ' ORDER BY id DESC LIMIT ' . $limit . ' OFFSET ' . $offset;
$stmt = db()->prepare($query);
$stmt->execute($params);
$assessment_categories = $stmt->fetchAll();
$flash = consume_flash();
render_page_start('فئات التقييم', 'assessment_categories', 'إدارة فئات التقييم الخاصة بالمراكز');
render_flash($flash);
?>
<section class="py-4 py-lg-5">
<div class="container-xxl">
<div class="admin-layout row g-4 align-items-start">
<div class="col-lg-3 layout-sidebar-column">
<?php require __DIR__ . '/includes/sidebar.php'; ?>
</div>
<div class="col-lg-9 layout-content-column">
<div class="app-card mb-4">
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-3">
<div class="section-title mb-0">إدارة فئات التقييم</div>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createModal">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"/>
</svg>
إضافة مادة
</button>
</div>
<!-- Search Bar -->
<?php render_search_bar($search, "ابحث باسم الفئة أو الوصف...", "assessment_categories.php", $_GET); ?>
<div class="table-responsive">
<table class="table app-table align-middle">
<thead>
<tr>
<th>#</th>
<th>الاسم</th>
<th>الوصف</th>
<th>الحالة</th>
<th>الإجراءات</th>
</tr>
</thead>
<tbody>
<?php if (count($assessment_categories) === 0): ?>
<tr>
<td colspan="5" class="text-center py-4 text-muted">لا توجد مواد مسجلة أو لم يتم العثور على نتائج.</td>
</tr>
<?php else: ?>
<?php foreach ($assessment_categories as $assessment_category): ?>
<tr>
<td><?= e((string)$assessment_category['id']) ?></td>
<td class="fw-semibold"><?= e($assessment_category['name']) ?></td>
<td class="text-muted" style="max-width: 250px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="<?= e($assessment_category['description']) ?>"><?= e($assessment_category['description'] ?: '—') ?></td>
<td>
<?php if ($assessment_category['status'] === 'enabled'): ?>
<span class="badge bg-success-subtle text-success">مفعل</span>
<?php else: ?>
<span class="badge bg-secondary-subtle text-secondary">معطل</span>
<?php endif; ?>
</td>
<td>
<div class="d-flex gap-2">
<!-- Edit btn -->
<button class="btn btn-sm btn-outline-secondary" title="تعديل"
data-bs-toggle="modal" data-bs-target="#editModal"
data-id="<?= e((string)$assessment_category['id']) ?>"
data-name="<?= e($assessment_category['name']) ?>"
data-description="<?= e($assessment_category['description']) ?>"
data-status="<?= e($assessment_category['status']) ?>"
onclick="fillEditModal(this)">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
</svg>
</button>
<!-- Delete btn -->
<form method="POST" action="assessment_categories.php" onsubmit="return confirm('هل أنت متأكد من حذف هذه الفئة؟');">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id" value="<?= e((string)$assessment_category['id']) ?>">
<button type="submit" class="btn btn-sm btn-outline-danger" title="حذف">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
</svg>
</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<?php render_pagination($totalItems, $limit, $page, $_GET); ?>
</div>
</div>
</div>
</div>
</section>
<!-- Create Modal -->
<div class="modal fade" id="createModal" tabindex="-1" aria-labelledby="createModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="POST" action="assessment_categories.php">
<input type="hidden" name="action" value="create">
<div class="modal-header">
<h5 class="modal-title" id="createModalLabel">إضافة مادة دراسية</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">اسم الفئة</label>
<input type="text" name="name" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">الوصف</label>
<textarea name="description" class="form-control" rows="3"></textarea>
</div>
<div class="mb-3">
<label class="form-label">الحالة</label>
<select name="status" class="form-select">
<option value="enabled">مفعل</option>
<option value="disabled">معطل</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إلغاء</button>
<button type="submit" class="btn btn-primary">إضافة</button>
</div>
</form>
</div>
</div>
</div>
<!-- Edit Modal -->
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form method="POST" action="assessment_categories.php">
<input type="hidden" name="action" value="edit">
<input type="hidden" name="id" id="edit_id">
<div class="modal-header">
<h5 class="modal-title" id="editModalLabel">تعديل مادة دراسية</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">اسم الفئة</label>
<input type="text" name="name" id="edit_name" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">الوصف</label>
<textarea name="description" id="edit_description" class="form-control" rows="3"></textarea>
</div>
<div class="mb-3">
<label class="form-label">الحالة</label>
<select name="status" id="edit_status" class="form-select">
<option value="enabled">مفعل</option>
<option value="disabled">معطل</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إلغاء</button>
<button type="submit" class="btn btn-primary">حفظ التعديلات</button>
</div>
</form>
</div>
</div>
</div>
<script>
function fillEditModal(btn) {
document.getElementById('edit_id').value = btn.getAttribute('data-id');
document.getElementById('edit_name').value = btn.getAttribute('data-name');
document.getElementById('edit_description').value = btn.getAttribute('data-description');
document.getElementById('edit_status').value = btn.getAttribute('data-status');
}
</script>
<?php render_page_end(); ?>

View File

@ -0,0 +1,785 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/app.php';
function print_sheet_score_display(?float $value): string
{
if ($value === null) {
return '—';
}
return rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.');
}
$applicationId = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT) ?: 0;
$requestedCycleId = filter_input(INPUT_GET, 'cycle', FILTER_VALIDATE_INT) ?: 0;
$requestedAssessmentId = filter_input(INPUT_GET, 'assessment_id', FILTER_VALIDATE_INT) ?: 0;
$application = $applicationId > 0 ? get_application($applicationId) : null;
$isApprovedCenter = $application && (string) ($application['status'] ?? '') === 'approved';
$cycleContext = ['cycles' => [], 'selected' => null, 'active' => null, 'read_only' => false];
$selectedCycle = null;
$selectedCycleId = 0;
$cycleLabel = 'لا توجد دورة بعد';
$buildCenterAssessmentsUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0): string {
$params = [];
if ($targetApplicationId > 0) {
$params['id'] = $targetApplicationId;
}
if ($targetCycleId > 0) {
$params['cycle'] = $targetCycleId;
}
return 'center_assessments.php' . ($params !== [] ? '?' . http_build_query($params) : '');
};
$buildCenterAssessmentScoreUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0, int $targetAssessmentId = 0): string {
$params = [];
if ($targetApplicationId > 0) {
$params['id'] = $targetApplicationId;
}
if ($targetCycleId > 0) {
$params['cycle'] = $targetCycleId;
}
if ($targetAssessmentId > 0) {
$params['assessment_id'] = $targetAssessmentId;
}
return 'center_assessment_score_sheet.php' . ($params !== [] ? '?' . http_build_query($params) : '');
};
if (!$application || !$isApprovedCenter || $requestedCycleId <= 0 || $requestedAssessmentId <= 0) {
set_flash('error', 'اختر مركزاً ودورة وتقييماً صحيحاً لفتح ورقة الطباعة.');
header('Location: ' . $buildCenterAssessmentsUrl($applicationId, $requestedCycleId));
exit;
}
$cycleContext = resolve_school_cycle_context((int) $application['id'], $application, $requestedCycleId);
$selectedCycle = $cycleContext['selected'];
$selectedCycleId = $selectedCycle ? (int) ($selectedCycle['id'] ?? 0) : 0;
$cycleLabel = $selectedCycle ? (string) ($selectedCycle['cycle_name'] ?? $cycleLabel) : $cycleLabel;
if ($selectedCycleId <= 0) {
set_flash('error', 'تعذر تحديد الدورة المطلوبة لورقة الطباعة.');
header('Location: ' . $buildCenterAssessmentsUrl((int) $application['id'], $requestedCycleId));
exit;
}
$selectedAssessment = null;
foreach (list_center_assessments_by_cycle((int) $application['id'], $selectedCycleId) as $assessment) {
if ((int) ($assessment['id'] ?? 0) === $requestedAssessmentId) {
$selectedAssessment = $assessment;
break;
}
}
if (!$selectedAssessment) {
set_flash('error', 'تعذر العثور على التقييم المطلوب لطباعة النموذج.');
header('Location: ' . $buildCenterAssessmentsUrl((int) $application['id'], $selectedCycleId));
exit;
}
$criteria = list_center_assessment_criteria_by_assessment((int) $application['id'], $selectedCycleId, $requestedAssessmentId, true);
$scoreBundle = center_assessment_score_bundle_by_assessment((int) $application['id'], $selectedCycleId, $requestedAssessmentId);
$scoreRow = $scoreBundle['score'] ?? null;
$criteriaScores = $scoreBundle['criteria_scores'] ?? [];
$statusKey = center_assessment_normalize_status((string) ($scoreRow['status'] ?? 'pending'));
$statusMeta = center_assessment_status_map()[$statusKey] ?? ['label' => 'غير محدد'];
$totalScore = isset($scoreRow['score']) && $scoreRow['score'] !== null ? (float) $scoreRow['score'] : null;
$maxScore = isset($scoreRow['max_score']) && $scoreRow['max_score'] !== null
? (float) $scoreRow['max_score']
: (float) ($selectedAssessment['max_score'] ?? 0);
if ($criteria !== []) {
$criteriaMax = 0.0;
foreach ($criteria as $criterion) {
$criteriaMax += (float) ($criterion['max_score'] ?? 0);
}
$maxScore = $criteriaMax > 0 ? $criteriaMax : $maxScore;
}
$percentage = $totalScore !== null && $maxScore > 0 ? round(($totalScore / $maxScore) * 100, 2) : null;
$notes = trim((string) ($scoreRow['notes'] ?? ''));
$assessmentDate = (string) ($scoreRow['assessed_on'] ?? '');
$settings = get_app_settings();
$projectName = (string) (!empty($settings['app_name']) ? $settings['app_name'] : project_name());
$projectLogo = (string) ($settings['app_logo'] ?? '');
$pageTitle = 'ورقة متابعة تقييم المركز — ' . (string) ($selectedAssessment['title'] ?? '');
$pageDescription = 'نموذج رسمي قابل للطباعة لتوثيق تقييم المركز والتوقيعات وخطة المتابعة الإدارية.';
$generatedAt = date('Y-m-d H:i');
$scoreSheetUrl = $buildCenterAssessmentScoreUrl((int) $application['id'], $selectedCycleId, $requestedAssessmentId);
?>
<!doctype html>
<html lang="ar" dir="rtl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= e($pageTitle) ?> | <?= e($projectName) ?></title>
<meta name="description" content="<?= e($pageDescription) ?>">
<meta name="robots" content="noindex, nofollow">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700;800&display=swap" rel="stylesheet">
<style>
:root {
--ink: #17324d;
--ink-soft: #48637d;
--line: #cfd9e5;
--line-strong: #8da6be;
--panel: #ffffff;
--panel-soft: #f4f7fb;
--accent: #0f4c81;
--accent-soft: #e9f2fb;
--success-soft: #edf7ef;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: #eef3f8;
color: var(--ink);
font-family: 'Cairo', system-ui, sans-serif;
line-height: 1.65;
}
.screen-toolbar {
max-width: 1100px;
margin: 0 auto;
padding: 1.25rem 1rem 0;
display: flex;
justify-content: space-between;
gap: 0.75rem;
flex-wrap: wrap;
}
.toolbar-actions {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.toolbar-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.45rem;
padding: 0.8rem 1.15rem;
border-radius: 999px;
text-decoration: none;
border: 1px solid rgba(15, 76, 129, 0.18);
background: rgba(255,255,255,0.92);
color: var(--ink);
font-weight: 700;
box-shadow: 0 14px 35px rgba(15, 76, 129, 0.08);
cursor: pointer;
}
.toolbar-btn.primary {
background: linear-gradient(135deg, #0f4c81, #2f7bc4);
color: #fff;
border-color: transparent;
}
.sheet-wrap {
max-width: 1100px;
margin: 0 auto;
padding: 1rem;
}
.sheet {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 28px;
box-shadow: 0 25px 60px rgba(17, 43, 70, 0.08);
padding: 2rem;
}
.sheet-header {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 1.5rem;
align-items: start;
padding-bottom: 1.5rem;
border-bottom: 2px solid var(--line);
}
.sheet-brand {
display: flex;
align-items: center;
gap: 1rem;
}
.sheet-brand img {
width: 76px;
height: 76px;
object-fit: contain;
border-radius: 18px;
border: 1px solid var(--line);
padding: 0.5rem;
background: #fff;
}
.brand-mark {
width: 76px;
height: 76px;
border-radius: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #0f4c81, #2f7bc4);
color: #fff;
font-size: 1.8rem;
font-weight: 800;
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.3rem 0.75rem;
border-radius: 999px;
background: var(--accent-soft);
color: var(--accent);
font-weight: 700;
font-size: 0.85rem;
margin-bottom: 0.75rem;
}
h1 {
margin: 0 0 0.35rem;
font-size: 2rem;
line-height: 1.25;
}
.subhead {
margin: 0;
color: var(--ink-soft);
font-size: 1rem;
}
.document-meta {
min-width: 250px;
border: 1px solid var(--line);
border-radius: 20px;
background: var(--panel-soft);
padding: 1rem 1.15rem;
}
.document-meta .meta-row,
.info-grid .info-item,
.summary-grid .summary-card {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 1rem;
}
.meta-row + .meta-row { margin-top: 0.45rem; }
.meta-label,
.info-label,
.summary-label {
color: var(--ink-soft);
font-size: 0.92rem;
}
.meta-value,
.info-value,
.summary-value {
font-weight: 700;
}
.section {
margin-top: 1.35rem;
border: 1px solid var(--line);
border-radius: 22px;
overflow: hidden;
background: #fff;
}
.section-head {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: center;
padding: 0.95rem 1.2rem;
background: linear-gradient(180deg, #f8fbff, #edf4fb);
border-bottom: 1px solid var(--line);
}
.section-title {
margin: 0;
font-size: 1.05rem;
font-weight: 800;
}
.section-note {
color: var(--ink-soft);
font-size: 0.9rem;
margin: 0;
}
.section-body { padding: 1.15rem 1.2rem 1.3rem; }
.info-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.85rem 1rem;
}
.info-item {
padding: 0.75rem 0.9rem;
background: var(--panel-soft);
border-radius: 16px;
min-height: 56px;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.85rem;
}
.print-top-grid,
.print-bottom-grid {
display: grid;
gap: 1rem;
}
.summary-card {
flex-direction: column;
align-items: flex-start;
justify-content: center;
padding: 1rem;
border-radius: 18px;
border: 1px solid var(--line);
background: linear-gradient(180deg, #fff, #f8fbff);
min-height: 108px;
}
.summary-card.status-card { background: var(--success-soft); }
.summary-value.big {
font-size: 1.45rem;
font-weight: 800;
margin-top: 0.2rem;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid var(--line);
padding: 0.8rem 0.7rem;
vertical-align: top;
text-align: right;
font-size: 0.95rem;
}
thead th {
background: #f3f7fb;
font-weight: 800;
}
td.center, th.center { text-align: center; }
.muted { color: var(--ink-soft); }
.notes-box,
.lines-box {
min-height: 110px;
border: 1px dashed var(--line-strong);
border-radius: 18px;
padding: 0.95rem 1rem;
background: linear-gradient(180deg, #fff, #fbfdff);
}
.notes-box.empty::after {
content: '................................................................................................................................................................................';
color: var(--line-strong);
word-break: break-word;
line-height: 2;
}
.follow-up-table td {
height: 54px;
}
.signature-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 1rem;
}
.signature-card {
border: 1px solid var(--line);
border-radius: 18px;
padding: 1rem;
background: #fff;
min-height: 155px;
}
.signature-role {
font-weight: 800;
margin-bottom: 1.5rem;
font-size: 1rem;
}
.signature-line {
border-bottom: 1px solid var(--line-strong);
margin: 1rem 0 0.45rem;
height: 1.25rem;
}
.print-footnote {
margin-top: 1.25rem;
color: var(--ink-soft);
font-size: 0.86rem;
display: flex;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
@media (max-width: 900px) {
.sheet-header,
.info-grid,
.summary-grid,
.signature-grid {
grid-template-columns: 1fr;
}
.document-meta { min-width: 0; }
}
@media print {
@page { size: A4; margin: 8mm; }
body {
background: #fff;
color: #000;
font-size: 11px;
line-height: 1.3;
}
.screen-toolbar { display: none !important; }
.sheet-wrap { max-width: none; padding: 0; }
.sheet {
box-shadow: none;
border: none;
border-radius: 0;
padding: 0;
}
.sheet-header {
gap: 0.75rem;
padding-bottom: 0.5rem;
margin-bottom: 0.4rem;
}
.sheet-brand { gap: 0.7rem; }
.sheet-brand img,
.brand-mark {
width: 52px;
height: 52px;
border-radius: 12px;
font-size: 1.25rem;
}
.eyebrow {
margin-bottom: 0.3rem;
padding: 0.12rem 0.5rem;
font-size: 0.66rem;
}
h1 {
font-size: 1.15rem;
margin-bottom: 0.1rem;
}
.subhead {
font-size: 0.74rem;
line-height: 1.25;
}
.document-meta {
min-width: 205px;
padding: 0.5rem 0.65rem;
border-radius: 12px;
}
.meta-row + .meta-row { margin-top: 0.18rem; }
.meta-label,
.info-label,
.summary-label,
.section-note,
.muted,
.print-footnote {
font-size: 0.68rem;
line-height: 1.2;
}
.print-top-grid {
grid-template-columns: 1.25fr 0.95fr;
gap: 0.45rem;
margin-top: 0.45rem;
}
.print-bottom-grid {
grid-template-columns: 0.95fr 1.05fr;
gap: 0.45rem;
margin-top: 0.45rem;
}
.section {
margin-top: 0.45rem;
border-radius: 12px;
}
.section-head {
padding: 0.45rem 0.65rem;
gap: 0.45rem;
}
.section-title {
font-size: 0.82rem;
line-height: 1.2;
}
.section-body {
padding: 0.5rem 0.65rem 0.6rem;
}
.info-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.35rem 0.45rem;
}
.info-item {
min-height: 0;
padding: 0.35rem 0.45rem;
border-radius: 10px;
}
.summary-grid {
gap: 0.35rem;
}
.summary-card {
min-height: 0;
padding: 0.45rem 0.5rem;
border-radius: 10px;
}
.summary-value.big {
font-size: 1rem;
margin-top: 0.05rem;
}
table { table-layout: fixed; }
th, td {
padding: 0.28rem 0.32rem;
font-size: 0.68rem;
line-height: 1.2;
}
td > div + div { margin-top: 0.08rem; }
.notes-box,
.lines-box {
min-height: 54px;
padding: 0.45rem 0.55rem;
border-radius: 10px;
}
.notes-box.empty::after {
line-height: 1.55;
font-size: 0.66rem;
}
.follow-up-table td {
height: 28px;
}
.follow-up-table tbody tr:nth-child(n+3) {
display: none;
}
.criterion-desc {
display: none;
}
.signature-grid {
gap: 0.35rem;
}
.signature-card {
min-height: 82px;
padding: 0.45rem 0.5rem;
border-radius: 10px;
}
.signature-role {
font-size: 0.74rem;
margin-bottom: 0.5rem;
}
.signature-line {
margin: 0.5rem 0 0.2rem;
height: 0.65rem;
}
.print-footnote {
margin-top: 0.45rem;
}
.section,
.document-meta,
.summary-card,
.signature-card,
.info-item,
tr,
td,
th {
break-inside: avoid;
}
a { color: inherit; text-decoration: none; }
}
</style>
</head>
<body>
<div class="screen-toolbar">
<div>
<div style="font-weight:800; font-size:1.05rem; color:#17324d;">ورقة تقييم قابلة للطباعة</div>
<div style="color:#48637d; font-size:0.92rem;">نموذج رسمي للتوقيعات وخطة المتابعة الإدارية.</div>
</div>
<div class="toolbar-actions">
<a class="toolbar-btn" href="<?= e($scoreSheetUrl) ?>">العودة إلى صفحة الرصد</a>
<a class="toolbar-btn" href="<?= e($buildCenterAssessmentsUrl((int) $application['id'], $selectedCycleId)) ?>">كل تقييمات المركز</a>
<button class="toolbar-btn primary" type="button" onclick="window.print()">طباعة / حفظ PDF</button>
</div>
</div>
<div class="sheet-wrap">
<article class="sheet">
<header class="sheet-header">
<div>
<div class="sheet-brand">
<?php if ($projectLogo !== ''): ?>
<img src="<?= e(asset_url($projectLogo)) ?>" alt="<?= e($projectName) ?>">
<?php else: ?>
<span class="brand-mark"><?= e(mb_substr($projectName, 0, 1)) ?></span>
<?php endif; ?>
<div>
<div class="eyebrow">نموذج رسمي معتمد للمتابعة</div>
<h1>ورقة تقييم ومتابعة إدارية للمركز</h1>
<p class="subhead"><?= e($projectName) ?> — توثيق نتيجة التقييم وخطوات المتابعة والتوقيعات.</p>
</div>
</div>
</div>
<div class="document-meta">
<div class="meta-row"><span class="meta-label">رقم المركز</span><span class="meta-value">#<?= e((string) $application['id']) ?></span></div>
<div class="meta-row"><span class="meta-label">الدورة</span><span class="meta-value"><?= e($cycleLabel) ?></span></div>
<div class="meta-row"><span class="meta-label">تاريخ التقييم</span><span class="meta-value"><?= e($assessmentDate !== '' ? $assessmentDate : '................') ?></span></div>
<div class="meta-row"><span class="meta-label">تاريخ الطباعة</span><span class="meta-value"><?= e($generatedAt) ?></span></div>
<div class="meta-row"><span class="meta-label">الحالة</span><span class="meta-value"><?= e((string) ($statusMeta['label'] ?? 'غير محدد')) ?></span></div>
</div>
</header>
<div class="print-top-grid">
<section class="section">
<div class="section-head">
<h2 class="section-title">بيانات المركز والتقييم</h2>
<p class="section-note">يمكن اعتماد هذه الصفحة في الملف الورقي أو حفظها بصيغة PDF.</p>
</div>
<div class="section-body">
<div class="info-grid">
<div class="info-item"><span class="info-label">اسم المركز</span><span class="info-value"><?= e((string) ($application['center_name'] ?? '')) ?></span></div>
<div class="info-item"><span class="info-label">المدينة</span><span class="info-value"><?= e((string) (($application['city'] ?? '') !== '' ? $application['city'] : '—')) ?></span></div>
<div class="info-item"><span class="info-label">مدير المركز</span><span class="info-value"><?= e((string) (($application['director_name'] ?? '') !== '' ? $application['director_name'] : '—')) ?></span></div>
<div class="info-item"><span class="info-label">التقييم</span><span class="info-value"><?= e((string) ($selectedAssessment['title'] ?? '')) ?></span></div>
<div class="info-item"><span class="info-label">الفئة</span><span class="info-value"><?= e((string) (($selectedAssessment['category'] ?? '') !== '' ? $selectedAssessment['category'] : '—')) ?></span></div>
<div class="info-item"><span class="info-label">نوع المقياس</span><span class="info-value"><?= e((string) assessment_scale_type_label((string) ($selectedAssessment['scale_type'] ?? ''))) ?></span></div>
</div>
</div>
</section>
<section class="section">
<div class="section-head">
<h2 class="section-title">ملخص النتيجة</h2>
<p class="section-note">يعرض هذا القسم النتيجة الحالية إن كانت مرصودة، أو يترك مساحة للتعبئة اليدوية.</p>
</div>
<div class="section-body">
<div class="summary-grid">
<div class="summary-card">
<div class="summary-label">الدرجة المحققة</div>
<div class="summary-value big"><?= e(print_sheet_score_display($totalScore)) ?></div>
</div>
<div class="summary-card">
<div class="summary-label">الدرجة القصوى</div>
<div class="summary-value big"><?= e(print_sheet_score_display($maxScore)) ?></div>
</div>
<div class="summary-card">
<div class="summary-label">النسبة المئوية</div>
<div class="summary-value big"><?= e($percentage !== null ? print_sheet_score_display($percentage) . '%' : '—') ?></div>
</div>
<div class="summary-card status-card">
<div class="summary-label">وزن التقييم</div>
<div class="summary-value big"><?= e(print_sheet_score_display((float) ($selectedAssessment['weight_percentage'] ?? 0))) ?>%</div>
</div>
</div>
</div>
</section>
</div>
<section class="section">
<div class="section-head">
<h2 class="section-title">تفاصيل البنود</h2>
<p class="section-note"><?= $criteria !== [] ? 'تفصيل البنود النشطة لهذا التقييم.' : 'لا توجد بنود معرفة حالياً، لذا يظهر سطر موحد للتقييم العام.' ?></p>
</div>
<div class="section-body">
<table>
<thead>
<tr>
<th class="center" style="width:64px;">#</th>
<th>البند / المعيار</th>
<th class="center" style="width:120px;">الدرجة القصوى</th>
<th class="center" style="width:140px;">الدرجة المرصودة</th>
<th>ملاحظات مختصرة</th>
</tr>
</thead>
<tbody>
<?php if ($criteria !== []): ?>
<?php foreach ($criteria as $index => $criterion): ?>
<?php
$criterionId = (int) ($criterion['id'] ?? 0);
$criterionScore = $criteriaScores[$criterionId] ?? null;
$criterionNote = trim((string) ($criterionScore['notes'] ?? ''));
?>
<tr>
<td class="center"><?= e((string) ($index + 1)) ?></td>
<td>
<div style="font-weight:700;"><?= e((string) ($criterion['title'] ?? '')) ?></div>
<?php if (!empty($criterion['description'])): ?><div class="muted criterion-desc"><?= e((string) $criterion['description']) ?></div><?php endif; ?>
</td>
<td class="center"><?= e(print_sheet_score_display((float) ($criterion['max_score'] ?? 0))) ?></td>
<td class="center"><?= e(isset($criterionScore['score']) && $criterionScore['score'] !== null ? print_sheet_score_display((float) $criterionScore['score']) : '................') ?></td>
<td><?= e($criterionNote !== '' ? $criterionNote : '................................................................') ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
<tr>
<td class="center">1</td>
<td><?= e((string) ($selectedAssessment['title'] ?? 'التقييم العام')) ?></td>
<td class="center"><?= e(print_sheet_score_display($maxScore)) ?></td>
<td class="center"><?= e($totalScore !== null ? print_sheet_score_display($totalScore) : '................') ?></td>
<td>................................................................</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
</section>
<div class="print-bottom-grid">
<section class="section">
<div class="section-head">
<h2 class="section-title">ملاحظات المقيم</h2>
<p class="section-note">أبرز الملاحظات أو أسباب القرار أو التوصية العامة.</p>
</div>
<div class="section-body">
<div class="notes-box<?= $notes === '' ? ' empty' : '' ?>"><?= $notes !== '' ? nl2br(e($notes)) : '' ?></div>
</div>
</section>
<section class="section">
<div class="section-head">
<h2 class="section-title">خطة المتابعة الإدارية</h2>
<p class="section-note">تُعبّأ عند وجود ملاحظات تتطلب معالجة أو متابعة لاحقة.</p>
</div>
<div class="section-body">
<table class="follow-up-table">
<thead>
<tr>
<th style="width:48%;">الإجراء المطلوب</th>
<th style="width:18%;">المسؤول</th>
<th style="width:18%;">الموعد المستهدف</th>
<th style="width:16%;">حالة المتابعة</th>
</tr>
</thead>
<tbody>
<tr><td></td><td></td><td></td><td></td></tr>
<tr><td></td><td></td><td></td><td></td></tr>
<tr><td></td><td></td><td></td><td></td></tr>
<tr><td></td><td></td><td></td><td></td></tr>
</tbody>
</table>
</div>
</section>
</div>
<section class="section">
<div class="section-head">
<h2 class="section-title">التوقيعات والاعتماد</h2>
<p class="section-note">للاستخدام الرسمي بين المقيم وإدارة المركز والإدارة المشرفة.</p>
</div>
<div class="section-body">
<div class="signature-grid">
<div class="signature-card">
<div class="signature-role">المقيم / المشرف</div>
<div class="signature-line"></div>
<div class="muted">الاسم والتوقيع</div>
<div class="signature-line"></div>
<div class="muted">التاريخ</div>
</div>
<div class="signature-card">
<div class="signature-role">مدير المركز</div>
<div class="signature-line"></div>
<div class="muted">الاسم والتوقيع</div>
<div class="signature-line"></div>
<div class="muted">التاريخ</div>
</div>
<div class="signature-card">
<div class="signature-role">اعتماد الإدارة</div>
<div class="signature-line"></div>
<div class="muted">الاسم والتوقيع</div>
<div class="signature-line"></div>
<div class="muted">التاريخ</div>
</div>
<div class="signature-card">
<div class="signature-role">متابعة الإغلاق</div>
<div class="signature-line"></div>
<div class="muted">تمت المراجعة / أغلقت الملاحظات</div>
<div class="signature-line"></div>
<div class="muted">التاريخ</div>
</div>
</div>
</div>
</section>
<div class="print-footnote">
<span>هذه الورقة ناتجة من نظام إدارة المراكز ويمكن أرشفتها ضمن ملف المركز.</span>
<span>مرجع التقييم: #<?= e((string) ($selectedAssessment['id'] ?? 0)) ?> — <?= e((string) ($selectedAssessment['title'] ?? '')) ?></span>
</div>
</article>
</div>
</body>
</html>

View File

@ -87,6 +87,20 @@ $buildCenterAssessmentReportUrl = static function (int $targetApplicationId = 0,
return 'center_assessment_report.php' . ($params !== [] ? '?' . http_build_query($params) : '');
};
$buildCenterAssessmentPrintUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0, int $targetAssessmentId = 0): string {
$params = [];
if ($targetApplicationId > 0) {
$params['id'] = $targetApplicationId;
}
if ($targetCycleId > 0) {
$params['cycle'] = $targetCycleId;
}
if ($targetAssessmentId > 0) {
$params['assessment_id'] = $targetAssessmentId;
}
return 'center_assessment_print_sheet.php' . ($params !== [] ? '?' . http_build_query($params) : '');
};
if ($applicationId <= 0 || $requestedCycleId <= 0) {
set_flash('error', 'اختر المركز والدورة أولاً ثم افتح صفحة رصد التقييم من شاشة تقييم المراكز.');
header('Location: ' . $buildCenterAssessmentsUrl($applicationId, $requestedCycleId));
@ -260,6 +274,7 @@ render_flash($flash);
<a class="btn btn-outline-secondary" href="<?= e($assessmentsUrl) ?>">كل التقييمات</a>
<a class="btn btn-outline-secondary" href="<?= e($reportUrl) ?>">تقرير الدورة</a>
<?php if ($selectedAssessment): ?>
<a class="btn btn-outline-secondary" href="<?= e($buildCenterAssessmentPrintUrl((int) $application['id'], $selectedCycleId, (int) ($selectedAssessment['id'] ?? 0))) ?>" target="_blank" rel="noopener">طباعة النموذج الرسمي</a>
<a class="btn btn-outline-secondary" href="<?= e($criteriaUrl) ?>">إدارة البنود</a>
<?php endif; ?>
</div>

View File

@ -59,6 +59,20 @@ $buildCenterAssessmentReportUrl = static function (int $targetApplicationId = 0,
return 'center_assessment_report.php' . ($params !== [] ? '?' . http_build_query($params) : '');
};
$buildCenterAssessmentPrintUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0, int $targetAssessmentId = 0): string {
$params = [];
if ($targetApplicationId > 0) {
$params['id'] = $targetApplicationId;
}
if ($targetCycleId > 0) {
$params['cycle'] = $targetCycleId;
}
if ($targetAssessmentId > 0) {
$params['assessment_id'] = $targetAssessmentId;
}
return 'center_assessment_print_sheet.php' . ($params !== [] ? '?' . http_build_query($params) : '');
};
$buildCenterAssessmentCriteriaUrl = static function (int $targetApplicationId = 0, int $targetCycleId = 0, int $targetAssessmentId = 0): string {
$params = [];
if ($targetApplicationId > 0) {
@ -166,6 +180,49 @@ $pageDescription = 'إدارة تقييمات إشرافية مستقلة للم
render_page_start($pageTitle, 'admin', $pageDescription);
render_flash($flash);
?>
<style>
.icon-actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.icon-action {
width: 2.25rem;
height: 2.25rem;
display: inline-flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(15, 23, 42, 0.12);
border-radius: 0.85rem;
color: #0f4c81;
background: #ffffff;
padding: 0;
line-height: 1;
cursor: pointer;
text-decoration: none;
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease, background-color 0.18s ease;
}
.icon-action:hover,
.icon-action:focus-visible {
color: #0a2f57;
border-color: rgba(15, 76, 129, 0.28);
background: #f4f9ff;
box-shadow: 0 10px 24px rgba(15, 76, 129, 0.12);
transform: translateY(-1px);
}
.icon-action svg {
width: 1rem;
height: 1rem;
}
.table-meta {
font-size: 0.85rem;
color: var(--bs-secondary-color, #6c757d);
}
</style>
<section class="py-4 py-lg-5">
<div class="container-xxl">
<div class="admin-layout row g-4 align-items-start">
@ -177,7 +234,6 @@ render_flash($flash);
<div class="page-banner mb-4 mb-lg-5">
<div class="row g-4 align-items-start">
<div class="col-lg-8">
<span class="eyebrow mb-3">مرحلة 2 التقييم + البنود</span>
<h1 class="page-title mb-3">إدارة تقييمات المراكز</h1>
<p class="page-copy mb-3">هذه الصفحة تضيف طبقة مستقلة لتقييم <strong>المراكز المعتمدة</strong> حسب <strong>الدورة الموسمية</strong>، بدون خلطها مع تقييمات الطلاب. يمكنك الآن تعريف <strong>أنواع تقييم المراكز وأوزانها</strong> ثم فتح <strong>بنود كل تقييم</strong> بنفس النمط المستخدم في تقييم الطلاب، تمهيداً لصفحة الرصد الفعلي.</p>
<div class="hero-meta">
@ -212,12 +268,6 @@ render_flash($flash);
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-md-6 col-xl-3"><div class="app-card stat-tile"><div class="mini-stat-label">إجمالي التقييمات</div><div class="mini-stat-value"><?= e((string) $metrics['total']) ?></div><div class="mini-stat-copy">كل الأنواع المعرفة للمركز المحدد.</div></div></div>
<div class="col-md-6 col-xl-3"><div class="app-card stat-tile"><div class="mini-stat-label">تقييمات نشطة</div><div class="mini-stat-value"><?= e((string) $metrics['active']) ?></div><div class="mini-stat-copy">جاهزة لربط البنود ثم الرصد.</div></div></div>
<div class="col-md-6 col-xl-3"><div class="app-card stat-tile"><div class="mini-stat-label">إجمالي الوزن النشط</div><div class="mini-stat-value"><?= e((string) round((float) $metrics['active_weight'], 2)) ?>٪</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) round((float) $metrics['average_max_score'], 2)) ?></div><div class="mini-stat-copy">متوسط السقف لكل تقييم مركز.</div></div></div>
</div>
<div class="row g-4 align-items-start">
<div class="col-lg-8">
@ -300,8 +350,6 @@ render_flash($flash);
</div>
</form>
<div class="alert alert-light border mb-4">أصبحت دورة تقييم المراكز مكتملة: <strong>تعريف التقييم</strong> ثم <strong>البنود</strong> ثم <strong>الرصد</strong> ثم <strong>تقرير ملخّص للدورة</strong>.</div>
<?php if ($assessments === []): ?>
<div class="empty-state text-center py-5">
<div class="empty-title mb-2">لا توجد تقييمات مراكز بعد</div>
@ -317,9 +365,7 @@ render_flash($flash);
<tr>
<th>التقييم</th>
<th>الفئة</th>
<th>المقياس</th>
<th>الدرجة</th>
<th>الوزن</th>
<th>الدرجة والوزن</th>
<th>البنود</th>
<th>الحالة</th>
<th>الإجراء</th>
@ -330,29 +376,75 @@ render_flash($flash);
<tr>
<td>
<div class="fw-semibold"><?= e((string) ($assessment['title'] ?? '')) ?></div>
<?php if (!empty($assessment['notes'])): ?>
<div class="text-muted small"><?= e((string) $assessment['notes']) ?></div>
<?php endif; ?>
</td>
<td><?= e((string) ($assessment['category'] ?? '')) ?></td>
<td><?= assessment_scale_type_badge((string) ($assessment['scale_type'] ?? '')) ?></td>
<td><?= e((string) round((float) ($assessment['max_score'] ?? 0), 2)) ?></td>
<td><?= e((string) round((float) ($assessment['weight_percentage'] ?? 0), 2)) ?>٪</td>
<td>
<div class="fw-semibold"><?= e((string) ((int) ($assessment['criteria_count'] ?? 0))) ?></div>
<div class="text-muted small">المجموع <?= e((string) round((float) ($assessment['criteria_total_max_score'] ?? 0), 2)) ?></div>
<div><?= e((string) ($assessment['category'] ?? '')) ?></div>
<div class="table-meta mt-1"><?= assessment_scale_type_badge((string) ($assessment['scale_type'] ?? '')) ?></div>
</td>
<td>
<div class="fw-semibold"><?= e((string) round((float) ($assessment['max_score'] ?? 0), 2)) ?></div>
<div class="table-meta mt-1">وزن <?= e((string) round((float) ($assessment['weight_percentage'] ?? 0), 2)) ?>٪</div>
</td>
<td>
<span class="fw-semibold"><?= e((string) ((int) ($assessment['criteria_count'] ?? 0))) ?></span>
</td>
<td><?= assessment_active_badge((int) ($assessment['is_active'] ?? 0)) ?></td>
<td>
<div class="d-flex flex-wrap gap-2">
<a class="btn btn-outline-primary btn-sm" href="<?= e($buildCenterAssessmentCriteriaUrl((int) $application['id'], $selectedCycleId, (int) ($assessment['id'] ?? 0))) ?>">البنود</a>
<a class="btn btn-primary btn-sm" href="<?= e($buildCenterAssessmentScoreUrl((int) $application['id'], $selectedCycleId, (int) ($assessment['id'] ?? 0))) ?>">رصد</a>
<div class="icon-actions">
<a
class="icon-action"
href="<?= e($buildCenterAssessmentCriteriaUrl((int) $application['id'], $selectedCycleId, (int) ($assessment['id'] ?? 0))) ?>"
data-bs-toggle="tooltip"
data-bs-placement="top"
data-bs-title="إدارة البنود"
title="إدارة البنود"
aria-label="إدارة البنود"
>
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path d="M2.5 3A1.5 1.5 0 0 1 4 1.5h8A1.5 1.5 0 0 1 13.5 3v10A1.5 1.5 0 0 1 12 14.5H4A1.5 1.5 0 0 1 2.5 13V3Zm1.5-.5a.5.5 0 0 0-.5.5v10a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5V3a.5.5 0 0 0-.5-.5H4Zm1.25 2.25a.75.75 0 0 1 .75-.75h4a.75.75 0 0 1 0 1.5h-4a.75.75 0 0 1-.75-.75Zm0 3a.75.75 0 0 1 .75-.75h4a.75.75 0 0 1 0 1.5h-4a.75.75 0 0 1-.75-.75Zm0 3a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"/>
</svg>
<span class="visually-hidden">إدارة البنود</span>
</a>
<a
class="icon-action"
href="<?= e($buildCenterAssessmentScoreUrl((int) $application['id'], $selectedCycleId, (int) ($assessment['id'] ?? 0))) ?>"
data-bs-toggle="tooltip"
data-bs-placement="top"
data-bs-title="رصد الدرجات"
title="رصد الدرجات"
aria-label="رصد الدرجات"
>
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path d="M11 1.5a1.5 1.5 0 0 1 1.5 1.5v9.293l-2-2V3a.5.5 0 0 0-.5-.5H4A.5.5 0 0 0 3.5 3v10a.5.5 0 0 0 .5.5h4.5l2 2H4A1.5 1.5 0 0 1 2.5 14V3A1.5 1.5 0 0 1 4 1.5h7Zm2.354 9.146a.5.5 0 0 1 .11.54l-1 3a.5.5 0 0 1-.316.316l-3 1a.5.5 0 0 1-.65-.65l1-3a.5.5 0 0 1 .12-.195l2.5-2.5a.5.5 0 0 1 .707 0l.53.53Zm-6.104-4.396a.75.75 0 0 1 .75-.75h1.5a.75.75 0 0 1 0 1.5H8a.75.75 0 0 1-.75-.75Zm0 3a.75.75 0 0 1 .75-.75h1.5a.75.75 0 0 1 0 1.5H8a.75.75 0 0 1-.75-.75Z"/>
</svg>
<span class="visually-hidden">رصد الدرجات</span>
</a>
<a
class="icon-action"
href="<?= e($buildCenterAssessmentPrintUrl((int) $application['id'], $selectedCycleId, (int) ($assessment['id'] ?? 0))) ?>"
target="_blank"
rel="noopener"
data-bs-toggle="tooltip"
data-bs-placement="top"
data-bs-title="طباعة النموذج الرسمي"
title="طباعة النموذج الرسمي"
aria-label="طباعة النموذج الرسمي"
>
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path d="M4 1.5A1.5 1.5 0 0 0 2.5 3v2.5a.5.5 0 0 0 1 0V3a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v2.5a.5.5 0 0 0 1 0V3A1.5 1.5 0 0 0 12 1.5H4ZM3 6a2 2 0 0 0-2 2v3a2 2 0 0 0 2 2h1v1.5A1.5 1.5 0 0 0 5.5 16h5A1.5 1.5 0 0 0 12 14.5V13h1a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2H3Zm2 7v1.5a.5.5 0 0 0 .5.5h5a.5.5 0 0 0 .5-.5V13H5Zm6-1H5V8h6v4Zm1-3.25a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Z"/>
</svg>
<span class="visually-hidden">طباعة النموذج الرسمي</span>
</a>
<?php if (!$isCycleReadOnly): ?>
<button
type="button"
class="btn btn-outline-secondary btn-sm"
class="icon-action"
data-bs-toggle="modal"
data-bs-target="#centerAssessmentModal"
data-bs-placement="top"
data-bs-title="تعديل التقييم"
title="تعديل التقييم"
aria-label="تعديل التقييم"
data-action="edit"
data-id="<?= e((string) ($assessment['id'] ?? 0)) ?>"
data-title="<?= e((string) ($assessment['title'] ?? '')) ?>"
@ -362,9 +454,14 @@ render_flash($flash);
data-weight-percentage="<?= e((string) round((float) ($assessment['weight_percentage'] ?? 0), 2)) ?>"
data-is-active="<?= e((string) ($assessment['is_active'] ?? 0)) ?>"
data-notes="<?= e((string) ($assessment['notes'] ?? '')) ?>"
>تعديل</button>
>
<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path d="M12.854.146a.5.5 0 0 1 .707 0l1.586 1.586a.5.5 0 0 1 0 .707l-9.5 9.5a.5.5 0 0 1-.168.11l-3.5 1.25a.5.5 0 0 1-.64-.64l1.25-3.5a.5.5 0 0 1 .11-.168l9.5-9.5ZM11.207 2 3.354 9.854l-.793 2.22 2.22-.793L12.646 3.414 11.207 2Z"/>
</svg>
<span class="visually-hidden">تعديل التقييم</span>
</button>
<?php else: ?>
<span class="text-muted small align-self-center">قراءة فقط</span>
<span class="table-meta align-self-center">قراءة فقط</span>
<?php endif; ?>
</div>
</td>
@ -522,6 +619,10 @@ render_flash($flash);
<script>
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(function (element) {
new bootstrap.Tooltip(element);
});
const modal = document.getElementById('centerAssessmentModal');
if (!modal) return;

View File

@ -0,0 +1,22 @@
CREATE TABLE IF NOT EXISTS `assessment_categories` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(255) NOT NULL,
`description` TEXT,
`status` ENUM('enabled', 'disabled') NOT NULL DEFAULT 'enabled',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT IGNORE INTO `assessment_categories` (`name`, `status`) VALUES
('تشخيصي', 'enabled'),
('واجب', 'enabled'),
('مشاركة', 'enabled'),
('تقييم', 'enabled'),
('تلاوة', 'enabled'),
('حفظ', 'enabled'),
('مراجعة', 'enabled'),
('اختبار قصير', 'enabled'),
('اختبار دوري', 'enabled'),
('اختبار نهائي', 'enabled'),
('أخرى', 'enabled');

View File

@ -16,30 +16,23 @@ if (!$template) {
exit;
}
$requestedCycleId = filter_input(INPUT_GET, 'cycle', FILTER_VALIDATE_INT) ?: 0;
$requestedCycleName = $_GET['cycle'] ?? '';
// Fetch active cycles
// Fetch distinct cycle names from active cycles
$pdo = db();
$cyclesStmt = $pdo->query("SELECT id, cycle_name, status FROM school_cycles ORDER BY (status = 'active') DESC, start_date DESC");
$cycles = $cyclesStmt->fetchAll() ?: [];
$cyclesStmt = $pdo->query("SELECT DISTINCT cycle_name FROM school_cycles WHERE status != 'archived' ORDER BY cycle_name ASC");
$cycles = $cyclesStmt->fetchAll(PDO::FETCH_COLUMN) ?: [];
// Find default cycle if not selected
if ($requestedCycleId <= 0 && count($cycles) > 0) {
foreach ($cycles as $c) {
if ($c['status'] === 'active') {
$requestedCycleId = (int)$c['id'];
break;
}
}
if ($requestedCycleId <= 0) {
$requestedCycleId = (int)$cycles[0]['id'];
}
if (empty($requestedCycleName) && count($cycles) > 0) {
$requestedCycleName = $cycles[0];
}
// Action: assess a specific center
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'assess') {
$centerId = filter_input(INPUT_POST, 'center_id', FILTER_VALIDATE_INT) ?: 0;
$cycleId = filter_input(INPUT_POST, 'cycle_id', FILTER_VALIDATE_INT) ?: 0;
$cycleName = $_POST['cycle_name'] ?? '';
if ($centerId > 0 && $cycleId > 0) {
// Check if already imported
@ -59,12 +52,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
$assessmentId = import_global_center_assessment_to_center($templateId, $centerId, $cycleId);
} catch (Exception $e) {
set_flash('error', 'فشل استيراد التقييم للمركز.');
header("Location: execute_global_assessment.php?id={$templateId}&cycle={$cycleId}");
header("Location: execute_global_assessment.php?id={$templateId}&cycle=" . urlencode($cycleName));
exit;
}
}
$returnUrl = urlencode("execute_global_assessment.php?id={$templateId}&cycle={$cycleId}");
$returnUrl = urlencode("execute_global_assessment.php?id={$templateId}&cycle=" . urlencode($cycleName));
header("Location: center_assessment_score_sheet.php?id={$centerId}&cycle={$cycleId}&assessment_id={$assessmentId}&return_url={$returnUrl}");
exit;
}
@ -72,18 +65,20 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['
// Fetch all approved centers and their assessment status for this template
$centers = [];
if ($requestedCycleId > 0) {
// Left join with center_assessment_types and center_assessment_scores
if (!empty($requestedCycleName)) {
// Join with school_cycles by cycle_name, then left join with center_assessment_types and center_assessment_scores
$query = "
SELECT
c.id, c.center_name, c.region,
c.id, c.center_name, c.city,
sc.id AS center_cycle_id,
cat.id AS assessment_type_id,
cas.status AS score_status,
cas.score,
cas.max_score
FROM center_applications c
JOIN school_cycles sc ON sc.center_application_id = c.id AND sc.cycle_name = :cycle_name
LEFT JOIN center_assessment_types cat ON cat.center_application_id = c.id
AND cat.cycle_id = :cycle_id
AND cat.cycle_id = sc.id
AND cat.global_template_id = :template_id
LEFT JOIN center_assessment_scores cas ON cas.assessment_type_id = cat.id
WHERE c.status = 'approved'
@ -91,7 +86,7 @@ if ($requestedCycleId > 0) {
";
$stmt = $pdo->prepare($query);
$stmt->execute([
':cycle_id' => $requestedCycleId,
':cycle_name' => $requestedCycleName,
':template_id' => $templateId
]);
$centers = $stmt->fetchAll() ?: [];
@ -137,8 +132,8 @@ render_flash($flash);
<option value="">لا توجد دورات</option>
<?php else: ?>
<?php foreach ($cycles as $c): ?>
<option value="<?= e((string)$c['id']) ?>" <?= $c['id'] == $requestedCycleId ? 'selected' : '' ?>>
<?= e((string)$c['cycle_name']) ?> <?= $c['status'] === 'active' ? '(نشطة)' : '' ?>
<option value="<?= e((string)$c) ?>" <?= $c === $requestedCycleName ? 'selected' : '' ?>>
<?= e((string)$c) ?>
</option>
<?php endforeach; ?>
<?php endif; ?>
@ -150,7 +145,7 @@ render_flash($flash);
</form>
</div>
<?php if ($requestedCycleId > 0 && count($centers) > 0): ?>
<?php if (!empty($requestedCycleName) && count($centers) > 0): ?>
<div class="app-card">
<div class="table-responsive">
<table class="table app-table align-middle mb-0">
@ -179,7 +174,7 @@ render_flash($flash);
<td>
<div class="fw-semibold"><?= e((string) ($center['center_name'] ?? '')) ?></div>
</td>
<td><?= e((string) ($center['region'] ?? '—')) ?></td>
<td><?= e((string) ($center['city'] ?? '—')) ?></td>
<td><?= $statusBadge ?></td>
<td>
<?php if ($hasScore): ?>
@ -192,7 +187,8 @@ render_flash($flash);
<form method="post" style="display:inline-block;">
<input type="hidden" name="action" value="assess">
<input type="hidden" name="center_id" value="<?= e((string)$center['id']) ?>">
<input type="hidden" name="cycle_id" value="<?= e((string)$requestedCycleId) ?>">
<input type="hidden" name="cycle_id" value="<?= e((string)$center['center_cycle_id']) ?>">
<input type="hidden" name="cycle_name" value="<?= e((string)$requestedCycleName) ?>">
<?php if ($status === 'completed'): ?>
<button type="submit" class="btn btn-sm btn-outline-primary">تعديل الرصد</button>
<?php else: ?>
@ -206,7 +202,7 @@ render_flash($flash);
</table>
</div>
</div>
<?php elseif ($requestedCycleId > 0): ?>
<?php elseif (!empty($requestedCycleName)): ?>
<div class="app-card text-center py-5">
<div class="empty-title mb-2">لا توجد مراكز معتمدة</div>
<p class="text-muted">لم يتم العثور على مراكز معتمدة لتقييمها في هذه الدورة.</p>
@ -217,4 +213,4 @@ render_flash($flash);
</div>
</div>
</section>
<?php render_page_end(); ?>
<?php render_page_end(); ?>

View File

@ -1067,15 +1067,22 @@ function assessment_defaults(): array
function assessment_category_options(): array
{
return [
'تشخيصي',
'واجب',
'مشاركة',
'مشروع',
'أداء',
'اختبار قصير',
'اختبار نهائي',
];
try {
$stmt = db()->query("SELECT name FROM assessment_categories WHERE status = 'enabled' ORDER BY name ASC");
$categories = $stmt->fetchAll(PDO::FETCH_COLUMN);
return $categories ?: [];
} catch (\PDOException $e) {
// Fallback in case table doesn't exist yet or db error
return [
'تشخيصي',
'واجب',
'مشاركة',
'مشروع',
'أداء',
'اختبار قصير',
'اختبار نهائي',
];
}
}
function assessment_scale_type_map(): array

View File

@ -5,6 +5,7 @@ $statusQuery = $_GET['status'] ?? '';
$activePage = 'admin';
if ($script === 'app_settings.php') $activePage = 'app_settings';
if ($script === 'subjects.php') $activePage = 'subjects';
if ($script === 'assessment_categories.php') $activePage = 'assessment_categories';
if ($script === 'global_cycles.php') $activePage = 'global_cycles';
if ($script === 'dashboard.php') $activePage = 'dashboard';
if ($script === 'applications.php') $activePage = ($statusQuery === 'approved') ? 'approved' : 'applications';
@ -65,6 +66,10 @@ if (!isset($recentApproved)) {
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path d="M1 2.828c.885-.37 2.154-.769 3.388-.893 1.33-.134 2.458.156 3.112.752v9.746c-.935-.53-2.12-.603-3.213-.493-1.18.12-2.37.461-3.287.811V2.828zm7.5-.141c.654-.596 1.782-.886 3.112-.752 1.234.124 2.503.523 3.388.893v9.923c-.918-.35-2.107-.692-3.287-.81-1.094-.111-2.278-.039-3.213.492V2.687zM8 1.783C7.015.936 5.587.81 4.287.94c-1.514.153-3.042.672-3.994 1.105A.5.5 0 0 0 0 2.5v11a.5.5 0 0 0 .707.455c.882-.4 2.303-.881 3.68-1.02 1.409-.142 2.59.087 3.223.877a.5.5 0 0 0 .78 0c.633-.79 1.814-1.019 3.222-.877 1.378.139 2.8.62 3.681 1.02A.5.5 0 0 0 16 13.5v-11a.5.5 0 0 0-.293-.455c-.952-.433-2.48-.952-3.994-1.105C10.413.809 8.985.936 8 1.783z"/></svg>
المواد الدراسية
</a>
<a href="assessment_categories.php" class="sidebar-link <?= $activePage === 'assessment_categories' ? 'active' : '' ?>">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"/><path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"/></svg>
فئات التقييم
</a>
<div class="sidebar-label mt-3">هيكلية النظام</div>
<a href="modules.php" class="sidebar-link <?= $activePage === 'modules' ? 'active' : '' ?>">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M15.817.113A.5.5 0 0 1 16 .5v14a.5.5 0 0 1-.402.49l-5 1a.502.502 0 0 1-.196 0L5.5 15.01l-4.902.98A.5.5 0 0 1 0 15.5v-14a.5.5 0 0 1 .402-.49l5-1a.5.5 0 0 1 .196 0L10.5.99l4.902-.98a.5.5 0 0 1 .415.103zM10 1.91l-4-.8v12.98l4 .8V1.91zm1 12.98 4-.8V1.11l-4 .8v12.98zm-6-.8V1.11l-4 .8v12.98l4-.8z"/></svg>

54
patch_app3.php Normal file
View File

@ -0,0 +1,54 @@
<?php
$file = 'includes/app.php';
$content = file_get_contents($file);
$old_func = <<<'EOL'
function assessment_category_options(): array
{
return [
'تشخيصي',
'واجب',
'مشاركة',
'مشروع',
'أداء',
'اختبار قصير',
'اختبار نهائي',
];
}
EOL;
$new_func = <<<'EOL'
function assessment_category_options(): array
{
try {
$stmt = db()->query("SELECT name FROM assessment_categories WHERE status = 'enabled' ORDER BY name ASC");
$categories = $stmt->fetchAll(PDO::FETCH_COLUMN);
return $categories ?: [];
} catch (\PDOException $e) {
// Fallback in case table doesn't exist yet or db error
return [
'تشخيصي',
'واجب',
'مشاركة',
'مشروع',
'أداء',
'اختبار قصير',
'اختبار نهائي',
];
}
}
EOL;
$new_content = str_replace($old_func, $new_func, $content);
if ($new_content === $content) {
echo "Failed to replace. Let's try regex replace\n";
$new_content = preg_replace('/function assessment_category_options\(\): array\s*\{.*?\}/s', $new_func, $content);
if ($new_content === $content) {
echo "Regex replace also failed.\n";
exit(1);
}
}
file_put_contents($file, $new_content);
echo "Successfully updated assessment_category_options() in includes/app.php\n";