1129 lines
44 KiB
PHP
1129 lines
44 KiB
PHP
<?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['status'] = clean_text((string) ($input['status'] ?? 'active'), 20);
|
|
$data['global_cycle_id'] = filter_var($input['global_cycle_id'] ?? null, FILTER_VALIDATE_INT) ?: null;
|
|
|
|
$errors = [];
|
|
if (empty($data['global_cycle_id'])) {
|
|
$errors['global_cycle_id'] = 'يرجى اختيار الدورة.';
|
|
} else {
|
|
try {
|
|
$stmt = db_connection()->prepare('SELECT * FROM global_cycles WHERE id = ?');
|
|
$stmt->execute([$data['global_cycle_id']]);
|
|
if ($cycle = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
|
$data['cycle_name'] = $cycle['cycle_name'];
|
|
$data['start_date'] = $cycle['start_date'];
|
|
$data['end_date'] = $cycle['end_date'];
|
|
$data['season'] = null; // No longer used
|
|
$data['year'] = null; // No longer used
|
|
} else {
|
|
$errors['global_cycle_id'] = 'الدورة غير صالحة';
|
|
}
|
|
} catch (Throwable $e) {}
|
|
}
|
|
|
|
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 = null;
|
|
$year = null;
|
|
$startDate = (string) ($application['start_date'] ?? date('Y-m-d'));
|
|
$endDate = (string) ($application['end_date'] ?? $startDate);
|
|
$cycleName = 'الدورة الأساسية';
|
|
$globalCycleId = $application['global_cycle_id'] ?? null;
|
|
|
|
if ($globalCycleId) {
|
|
$gcStmt = $pdo->prepare('SELECT * FROM global_cycles WHERE id = ?');
|
|
$gcStmt->execute([$globalCycleId]);
|
|
if ($gc = $gcStmt->fetch()) {
|
|
$cycleName = $gc['cycle_name'];
|
|
$startDate = $gc['start_date'];
|
|
$endDate = $gc['end_date'];
|
|
}
|
|
}
|
|
|
|
$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, global_cycle_id
|
|
) VALUES (
|
|
:center_application_id, :season, :year, :cycle_name, :start_date, :end_date,
|
|
:status, NULL, NOW(), NOW(), :global_cycle_id
|
|
)'
|
|
);
|
|
$insert->execute([
|
|
':center_application_id' => $applicationId,
|
|
':season' => $season,
|
|
':year' => $year,
|
|
':cycle_name' => $cycleName,
|
|
':start_date' => $startDate,
|
|
':end_date' => $endDate,
|
|
':status' => $status,
|
|
':global_cycle_id' => $globalCycleId
|
|
]);
|
|
|
|
return (int) $pdo->lastInsertId();
|
|
}
|
|
|
|
|
|
function update_teacher_in_cycle(int $id, int $centerApplicationId, int $cycleId, array $data): void
|
|
{
|
|
$pdo = db_connection();
|
|
$stmt = $pdo->prepare(
|
|
'UPDATE school_teachers SET
|
|
full_name = :full_name, role_title = :role_title, specialization = :specialization,
|
|
phone = :phone, email = :email, employment_status = :employment_status, notes = :notes,
|
|
updated_at = NOW()
|
|
WHERE id = :id AND center_application_id = :center_application_id AND cycle_id = :cycle_id'
|
|
);
|
|
$stmt->execute([
|
|
':id' => $id,
|
|
':center_application_id' => $centerApplicationId,
|
|
':cycle_id' => $cycleId,
|
|
':full_name' => $data['full_name'],
|
|
':role_title' => $data['role_title'],
|
|
':specialization' => $data['specialization'],
|
|
':phone' => $data['phone'],
|
|
':email' => $data['email'],
|
|
':employment_status' => $data['employment_status'],
|
|
':notes' => $data['notes'],
|
|
]);
|
|
}
|
|
|
|
|
|
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'] ?? null;
|
|
$year = isset($data['year']) ? (int) $data['year'] : null;
|
|
$cycleName = $data['cycle_name'] ?? format_school_cycle_name($season ?? 'summer', $year ?? (int)date('Y'));
|
|
$globalCycleId = $data['global_cycle_id'] ?? null;
|
|
$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, global_cycle_id
|
|
) VALUES (
|
|
:center_application_id, :season, :year, :cycle_name, :start_date, :end_date,
|
|
:status, :archived_at, NOW(), NOW(), :global_cycle_id
|
|
)'
|
|
);
|
|
$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,
|
|
':global_cycle_id' => $globalCycleId,
|
|
]);
|
|
|
|
$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, string $search = '', int $limit = 0, int $offset = 0, array $filters = []): array
|
|
{
|
|
$pdo = db_connection();
|
|
$query = 'SELECT * FROM school_students WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id';
|
|
$params = [
|
|
':center_application_id' => $centerApplicationId,
|
|
':cycle_id' => $cycleId,
|
|
];
|
|
|
|
if ($search !== '') {
|
|
$query .= ' AND (student_code LIKE :search1 OR full_name LIKE :search2 OR guardian_phone LIKE :search3)';
|
|
$params[':search1'] = "%$search%";
|
|
$params[':search2'] = "%$search%";
|
|
$params[':search3'] = "%$search%";
|
|
}
|
|
|
|
if (!empty($filters["gender"])) { $query .= " AND gender = :gender"; $params[":gender"] = $filters["gender"]; } if (!empty($filters["grade_level"])) { $query .= " AND grade_level = :grade_level"; $params[":grade_level"] = $filters["grade_level"]; } if (!empty($filters["enrollment_status"])) { $query .= " AND enrollment_status = :enrollment_status"; $params[":enrollment_status"] = $filters["enrollment_status"]; }
|
|
$query .= ' ORDER BY created_at DESC, id DESC';
|
|
|
|
if ($limit > 0) {
|
|
$query .= ' LIMIT ' . (int)$limit . ' OFFSET ' . (int)$offset;
|
|
}
|
|
|
|
$stmt = $pdo->prepare($query);
|
|
$stmt->execute($params);
|
|
return $stmt->fetchAll();
|
|
}
|
|
|
|
function count_school_students_by_cycle(int $centerApplicationId, int $cycleId, string $search = '', array $filters = []): int
|
|
{
|
|
$pdo = db_connection();
|
|
$query = 'SELECT COUNT(*) FROM school_students WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id';
|
|
$params = [
|
|
':center_application_id' => $centerApplicationId,
|
|
':cycle_id' => $cycleId,
|
|
];
|
|
|
|
if ($search !== '') {
|
|
$query .= ' AND (student_code LIKE :search1 OR full_name LIKE :search2 OR guardian_phone LIKE :search3)';
|
|
$params[':search1'] = "%$search%";
|
|
$params[':search2'] = "%$search%";
|
|
$params[':search3'] = "%$search%";
|
|
}
|
|
|
|
if (!empty($filters["gender"])) { $query .= " AND gender = :gender"; $params[":gender"] = $filters["gender"]; } if (!empty($filters["grade_level"])) { $query .= " AND grade_level = :grade_level"; $params[":grade_level"] = $filters["grade_level"]; } if (!empty($filters["enrollment_status"])) { $query .= " AND enrollment_status = :enrollment_status"; $params[":enrollment_status"] = $filters["enrollment_status"]; }
|
|
$stmt = $pdo->prepare($query);
|
|
$stmt->execute($params);
|
|
return (int)$stmt->fetchColumn();
|
|
}
|
|
|
|
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 $filters = [], int $limit = 0, int $offset = 0): array
|
|
{
|
|
$pdo = db_connection();
|
|
$query = 'SELECT * FROM school_teachers WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id';
|
|
$params = [
|
|
':center_application_id' => $centerApplicationId,
|
|
':cycle_id' => $cycleId,
|
|
];
|
|
|
|
$search = $filters['search'] ?? '';
|
|
if ($search !== '') {
|
|
$query .= ' AND (full_name LIKE :search1 OR email LIKE :search2 OR phone LIKE :search3)';
|
|
$params[':search1'] = "%$search%";
|
|
$params[':search2'] = "%$search%";
|
|
$params[':search3'] = "%$search%";
|
|
}
|
|
|
|
if (!empty($filters['role_title'])) {
|
|
$query .= ' AND role_title = :role';
|
|
$params[':role'] = $filters['role_title'];
|
|
}
|
|
|
|
if (!empty($filters['employment_status'])) {
|
|
$query .= ' AND employment_status = :status';
|
|
$params[':status'] = $filters['employment_status'];
|
|
}
|
|
|
|
$query .= ' ORDER BY created_at DESC, id DESC';
|
|
|
|
if ($limit > 0) {
|
|
$query .= ' LIMIT ' . (int)$limit . ' OFFSET ' . (int)$offset;
|
|
}
|
|
|
|
$stmt = $pdo->prepare($query);
|
|
$stmt->execute($params);
|
|
return $stmt->fetchAll();
|
|
}
|
|
|
|
function count_school_teachers_by_cycle(int $centerApplicationId, int $cycleId, array $filters = []): int
|
|
{
|
|
$pdo = db_connection();
|
|
$query = 'SELECT COUNT(*) FROM school_teachers WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id';
|
|
$params = [
|
|
':center_application_id' => $centerApplicationId,
|
|
':cycle_id' => $cycleId,
|
|
];
|
|
|
|
$search = $filters['search'] ?? '';
|
|
if ($search !== '') {
|
|
$query .= ' AND (full_name LIKE :search1 OR email LIKE :search2 OR phone LIKE :search3)';
|
|
$params[':search1'] = "%$search%";
|
|
$params[':search2'] = "%$search%";
|
|
$params[':search3'] = "%$search%";
|
|
}
|
|
|
|
if (!empty($filters['role_title'])) {
|
|
$query .= ' AND role_title = :role';
|
|
$params[':role'] = $filters['role_title'];
|
|
}
|
|
|
|
if (!empty($filters['employment_status'])) {
|
|
$query .= ' AND employment_status = :status';
|
|
$params[':status'] = $filters['employment_status'];
|
|
}
|
|
|
|
$stmt = $pdo->prepare($query);
|
|
$stmt->execute($params);
|
|
return (int)$stmt->fetchColumn();
|
|
}
|
|
|
|
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, string $search = '', int $limit = 0, int $offset = 0): array
|
|
{
|
|
$pdo = db_connection();
|
|
$query = 'SELECT * FROM school_assessment_types WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id';
|
|
$params = [
|
|
':center_application_id' => $centerApplicationId,
|
|
':cycle_id' => $cycleId,
|
|
];
|
|
|
|
if ($search !== '') {
|
|
$query .= ' AND (assessment_name LIKE :search1 OR assessment_category LIKE :search2)';
|
|
$params[':search1'] = "%$search%";
|
|
$params[':search2'] = "%$search%";
|
|
}
|
|
|
|
$query .= ' ORDER BY is_active DESC, created_at DESC, id DESC';
|
|
|
|
if ($limit > 0) {
|
|
$query .= ' LIMIT ' . (int)$limit . ' OFFSET ' . (int)$offset;
|
|
}
|
|
|
|
$stmt = $pdo->prepare($query);
|
|
$stmt->execute($params);
|
|
return $stmt->fetchAll();
|
|
}
|
|
|
|
function count_school_assessments_by_cycle(int $centerApplicationId, int $cycleId, string $search = ''): int
|
|
{
|
|
$pdo = db_connection();
|
|
$query = 'SELECT COUNT(*) FROM school_assessment_types WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id';
|
|
$params = [
|
|
':center_application_id' => $centerApplicationId,
|
|
':cycle_id' => $cycleId,
|
|
];
|
|
|
|
if ($search !== '') {
|
|
$query .= ' AND (assessment_name LIKE :search1 OR assessment_category LIKE :search2)';
|
|
$params[':search1'] = "%$search%";
|
|
$params[':search2'] = "%$search%";
|
|
}
|
|
|
|
$stmt = $pdo->prepare($query);
|
|
$stmt->execute($params);
|
|
return (int)$stmt->fetchColumn();
|
|
}
|
|
|
|
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, string $search = '', int $limit = 0, int $offset = 0): array
|
|
{
|
|
$pdo = db_connection();
|
|
$query = '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';
|
|
$params = [
|
|
':center_application_id' => $centerApplicationId,
|
|
':cycle_id' => $cycleId,
|
|
];
|
|
|
|
if ($search !== '') {
|
|
$query .= ' AND (s.full_name LIKE :search1 OR s.student_code LIKE :search2)';
|
|
$params[':search1'] = "%$search%";
|
|
$params[':search2'] = "%$search%";
|
|
}
|
|
|
|
$query .= ' ORDER BY ar.attendance_date DESC, ar.created_at DESC, ar.id DESC';
|
|
|
|
if ($limit > 0) {
|
|
$query .= ' LIMIT ' . (int)$limit . ' OFFSET ' . (int)$offset;
|
|
}
|
|
|
|
$stmt = $pdo->prepare($query);
|
|
$stmt->execute($params);
|
|
return $stmt->fetchAll();
|
|
}
|
|
|
|
function count_school_attendance_records_by_cycle(int $centerApplicationId, int $cycleId, string $search = ''): int
|
|
{
|
|
$pdo = db_connection();
|
|
$query = 'SELECT COUNT(*)
|
|
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';
|
|
$params = [
|
|
':center_application_id' => $centerApplicationId,
|
|
':cycle_id' => $cycleId,
|
|
];
|
|
|
|
if ($search !== '') {
|
|
$query .= ' AND (s.full_name LIKE :search1 OR s.student_code LIKE :search2)';
|
|
$params[':search1'] = "%$search%";
|
|
$params[':search2'] = "%$search%";
|
|
}
|
|
|
|
$stmt = $pdo->prepare($query);
|
|
$stmt->execute($params);
|
|
return (int)$stmt->fetchColumn();
|
|
}
|
|
|
|
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),
|
|
];
|
|
}
|
|
|
|
function update_student_in_cycle(int $centerApplicationId, int $cycleId, int $studentId, array $data): void
|
|
{
|
|
$pdo = db_connection();
|
|
$stmt = $pdo->prepare(
|
|
'UPDATE school_students SET
|
|
student_code = :student_code, full_name = :full_name, gender = :gender, grade_level = :grade_level,
|
|
guardian_name = :guardian_name, guardian_phone = :guardian_phone, birth_date = :birth_date,
|
|
enrollment_status = :enrollment_status, notes = :notes, updated_at = NOW()
|
|
WHERE center_application_id = :center_application_id AND cycle_id = :cycle_id AND id = :id'
|
|
);
|
|
$stmt->bindValue(':center_application_id', $centerApplicationId, PDO::PARAM_INT);
|
|
$stmt->bindValue(':cycle_id', $cycleId, PDO::PARAM_INT);
|
|
$stmt->bindValue(':id', $studentId, 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();
|
|
}
|