From 33ad70235b4159db8da96494f80dc2b7f7c82105 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Fri, 30 Jan 2026 15:36:51 +0000 Subject: [PATCH] v5 --- admin.php | 8 +- api/v1/Controllers/AssessmentController.php | 42 ++++ api/v1/Controllers/AuthController.php | 54 ++++ api/v1/Controllers/HealthController.php | 15 ++ api/v1/Controllers/LearnerController.php | 36 +++ api/v1/Core/Auth.php | 54 ++++ api/v1/Core/Controller.php | 27 ++ api/v1/Core/Model.php | 29 +++ api/v1/Core/Response.php | 16 ++ api/v1/Core/Router.php | 40 +++ api/v1/Models/Assessment.php | 34 +++ api/v1/Models/Learner.php | 15 ++ api/v1/index.php | 41 ++++ collaboration.php | 174 ++++++++----- .../005_gamification_and_school_collab.sql | 55 +++++ includes/header.php | 11 +- leaderboard.php | 206 ++++++++++++++++ public/app.js | 232 ++++++++++++++++++ public/assets/css/custom.css | 100 ++++++++ public/assets/js/main.js | 87 +++++++ public/index.html | 41 ++++ public/manifest.json | 21 ++ public/sw.js | 39 +++ sw.js | 5 +- 24 files changed, 1316 insertions(+), 66 deletions(-) create mode 100644 api/v1/Controllers/AssessmentController.php create mode 100644 api/v1/Controllers/AuthController.php create mode 100644 api/v1/Controllers/HealthController.php create mode 100644 api/v1/Controllers/LearnerController.php create mode 100644 api/v1/Core/Auth.php create mode 100644 api/v1/Core/Controller.php create mode 100644 api/v1/Core/Model.php create mode 100644 api/v1/Core/Response.php create mode 100644 api/v1/Core/Router.php create mode 100644 api/v1/Models/Assessment.php create mode 100644 api/v1/Models/Learner.php create mode 100644 api/v1/index.php create mode 100644 db/migrations/005_gamification_and_school_collab.sql create mode 100644 leaderboard.php create mode 100644 public/app.js create mode 100644 public/assets/css/custom.css create mode 100644 public/assets/js/main.js create mode 100644 public/index.html create mode 100644 public/manifest.json create mode 100644 public/sw.js diff --git a/admin.php b/admin.php index c26c2e6..569ea28 100644 --- a/admin.php +++ b/admin.php @@ -135,6 +135,12 @@ include 'includes/header.php'; Manage Learners + + Academic Leaderboard + + + Collaboration Hub + Bulk Upload Learners @@ -154,4 +160,4 @@ include 'includes/header.php'; - + \ No newline at end of file diff --git a/api/v1/Controllers/AssessmentController.php b/api/v1/Controllers/AssessmentController.php new file mode 100644 index 0000000..3c761a4 --- /dev/null +++ b/api/v1/Controllers/AssessmentController.php @@ -0,0 +1,42 @@ +auth(); + $model = new Assessment(); + + if ($user['role'] === 'Super Admin') { + $data = $model->all(); + } else { + $data = $model->getBySchool($user['school_id']); + } + + Response::json($data); + } + + public function store() { + $user = $this->auth(); + $data = json_decode(file_get_contents('php://input'), true); + + if ($user['role'] !== 'Admin' && $user['role'] !== 'Teacher' && $user['role'] !== 'Super Admin') { + Response::error('Unauthorized', 403); + } + + $db = db(); + $stmt = $db->prepare("INSERT INTO assessments (title, subject, type, school_id) VALUES (:title, :subject, :type, :school_id)"); + $stmt->execute([ + 'title' => $data['title'], + 'subject' => $data['subject'], + 'type' => $data['type'], + 'school_id' => $user['school_id'] + ]); + + Response::json(['id' => $db->lastInsertId(), 'message' => 'Assessment created'], 201); + } +} diff --git a/api/v1/Controllers/AuthController.php b/api/v1/Controllers/AuthController.php new file mode 100644 index 0000000..839593f --- /dev/null +++ b/api/v1/Controllers/AuthController.php @@ -0,0 +1,54 @@ +prepare("SELECT * FROM users WHERE email = :email"); + $stmt->execute(['email' => $email]); + $user = $stmt->fetch(); + + if ($user && password_verify($password, $user['password'])) { + $token = Auth::generateToken([ + 'user_id' => $user['id'], + 'role' => $user['role'], + 'school_id' => $user['school_id'], + 'email' => $user['email'] + ]); + + Response::json([ + 'token' => $token, + 'user' => [ + 'id' => $user['id'], + 'email' => $user['email'], + 'role' => $user['role'], + 'name' => $user['name'] ?? '' + ] + ]); + } else { + Response::error('Invalid credentials', 401); + } + } + + public function me() { + $token = Auth::getBearerToken(); + if (!$token) Response::error('No token provided', 401); + + $payload = Auth::verifyToken($token); + if (!$payload) Response::error('Invalid or expired token', 401); + + Response::json(['user' => $payload]); + } +} diff --git a/api/v1/Controllers/HealthController.php b/api/v1/Controllers/HealthController.php new file mode 100644 index 0000000..3286bf6 --- /dev/null +++ b/api/v1/Controllers/HealthController.php @@ -0,0 +1,15 @@ + 'ok', + 'timestamp' => date('c'), + 'php_version' => PHP_VERSION + ]); + } +} diff --git a/api/v1/Controllers/LearnerController.php b/api/v1/Controllers/LearnerController.php new file mode 100644 index 0000000..199494c --- /dev/null +++ b/api/v1/Controllers/LearnerController.php @@ -0,0 +1,36 @@ +auth(); + $learnerModel = new Learner(); + + if ($user['role'] === 'Super Admin') { + $learners = $learnerModel->all(); + } else { + $learners = $learnerModel->getBySchool($user['school_id']); + } + + Response::json($learners); + } + + public function show($id) { + $user = $this->auth(); + $learnerModel = new Learner(); + $learner = $learnerModel->find($id); + + if (!$learner) Response::error('Learner not found', 404); + + if ($user['role'] !== 'Super Admin' && $learner['school_id'] != $user['school_id']) { + Response::error('Unauthorized', 403); + } + + Response::json($learner); + } +} diff --git a/api/v1/Core/Auth.php b/api/v1/Core/Auth.php new file mode 100644 index 0000000..4b41003 --- /dev/null +++ b/api/v1/Core/Auth.php @@ -0,0 +1,54 @@ + 'JWT', 'alg' => 'HS256']); + $payload['exp'] = time() + (60 * 60 * 24); // 24 hours + $payload = json_encode($payload); + + $base64UrlHeader = self::base64UrlEncode($header); + $base64UrlPayload = self::base64UrlEncode($payload); + + $signature = hash_hmac('sha256', $base64UrlHeader . "." . $base64UrlPayload, self::$secret, true); + $base64UrlSignature = self::base64UrlEncode($signature); + + return $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature; + } + + public static function verifyToken($token) { + $parts = explode('.', $token); + if (count($parts) !== 3) return false; + + list($header, $payload, $signature) = $parts; + + $validSignature = hash_hmac('sha256', $header . "." . $payload, self::$secret, true); + if (self::base64UrlEncode($validSignature) !== $signature) return false; + + $payloadData = json_decode(self::base64UrlDecode($payload), true); + if (isset($payloadData['exp']) && $payloadData['exp'] < time()) return false; + + return $payloadData; + } + + public static function getBearerToken() { + $headers = getallheaders(); + if (isset($headers['Authorization'])) { + if (preg_match('/Bearer\s(\S+)/', $headers['Authorization'], $matches)) { + return $matches[1]; + } + } + return null; + } + + private static function base64UrlEncode($data) { + return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($data)); + } + + private static function base64UrlDecode($data) { + return base64_decode(str_replace(['-', '_'], ['+', '/'], $data)); + } +} diff --git a/api/v1/Core/Controller.php b/api/v1/Core/Controller.php new file mode 100644 index 0000000..891dff2 --- /dev/null +++ b/api/v1/Core/Controller.php @@ -0,0 +1,27 @@ + $rule) { + if ($rule === 'required' && (!isset($data[$field]) || empty($data[$field]))) { + $errors[] = "Field '{$field}' is required"; + } + } + if (!empty($errors)) { + Response::json(['errors' => $errors], 422); + } + } +} \ No newline at end of file diff --git a/api/v1/Core/Model.php b/api/v1/Core/Model.php new file mode 100644 index 0000000..d200593 --- /dev/null +++ b/api/v1/Core/Model.php @@ -0,0 +1,29 @@ +db = db(); + } + + public function all() { + $stmt = $this->db->query("SELECT * FROM {$this->table}"); + return $stmt->fetchAll(); + } + + public function find($id) { + $stmt = $this->db->prepare("SELECT * FROM {$this->table} WHERE id = :id"); + $stmt->execute(['id' => $id]); + return $stmt->fetch(); + } + + public function where($column, $value) { + $stmt = $this->db->prepare("SELECT * FROM {$this->table} WHERE {$column} = :value"); + $stmt->execute(['value' => $value]); + return $stmt->fetchAll(); + } +} diff --git a/api/v1/Core/Response.php b/api/v1/Core/Response.php new file mode 100644 index 0000000..4558f67 --- /dev/null +++ b/api/v1/Core/Response.php @@ -0,0 +1,16 @@ + $message], $status); + } +} diff --git a/api/v1/Core/Router.php b/api/v1/Core/Router.php new file mode 100644 index 0000000..b81f8fe --- /dev/null +++ b/api/v1/Core/Router.php @@ -0,0 +1,40 @@ +routes[] = [ + 'method' => $method, + 'path' => $path, + 'handler' => $handler + ]; + } + + public function handle($method, $uri) { + // Simple router logic: match path exactly or with :id + foreach ($this->routes as $route) { + $pattern = preg_replace('/\/:\w+/', '/(\d+)', $route['path']); + if ($route['method'] === $method && preg_match('#^' . $pattern . '$#', $uri, $matches)) { + array_shift($matches); // remove the full match + return $this->executeHandler($route['handler'], $matches); + } + } + Response::error('Route not found', 404); + } + + private function executeHandler($handler, $params) { + list($controllerName, $methodName) = explode('@', $handler); + $controllerClass = "Api\\Controllers\\" . $controllerName; + + if (class_exists($controllerClass)) { + $controller = new $controllerClass(); + if (method_exists($controller, $methodName)) { + return call_user_func_array([$controller, $methodName], $params); + } + } + Response::error('Controller or method not found', 500); + } +} \ No newline at end of file diff --git a/api/v1/Models/Assessment.php b/api/v1/Models/Assessment.php new file mode 100644 index 0000000..42ecc9d --- /dev/null +++ b/api/v1/Models/Assessment.php @@ -0,0 +1,34 @@ +db->prepare("SELECT * FROM assessments WHERE school_id = :school_id"); + $stmt->execute(['school_id' => $school_id]); + return $stmt->fetchAll(); + } + + public function saveMarks($assessment_id, $marks) { + $this->db->beginTransaction(); + try { + $stmt = $this->db->prepare("INSERT INTO marks (assessment_id, learner_id, score) VALUES (:assessment_id, :learner_id, :score) ON DUPLICATE KEY UPDATE score = VALUES(score)"); + foreach ($marks as $mark) { + $stmt->execute([ + 'assessment_id' => $assessment_id, + 'learner_id' => $mark['learner_id'], + 'score' => $mark['score'] + ]); + } + $this->db->commit(); + return true; + } catch (\Exception $e) { + $this->db->rollBack(); + return false; + } + } +} diff --git a/api/v1/Models/Learner.php b/api/v1/Models/Learner.php new file mode 100644 index 0000000..b081a18 --- /dev/null +++ b/api/v1/Models/Learner.php @@ -0,0 +1,15 @@ +db->prepare("SELECT * FROM learners WHERE school_id = :school_id"); + $stmt->execute(['school_id' => $school_id]); + return $stmt->fetchAll(); + } +} diff --git a/api/v1/index.php b/api/v1/index.php new file mode 100644 index 0000000..ce7908f --- /dev/null +++ b/api/v1/index.php @@ -0,0 +1,41 @@ +add('GET', '/health', 'HealthController@index'); +$router->add('POST', '/auth/login', 'AuthController@login'); +$router->add('GET', '/auth/me', 'AuthController@me'); + +$router->add('GET', '/learners', 'LearnerController@index'); +$router->add('GET', '/learners/:id', 'LearnerController@show'); + +$router->add('GET', '/assessments', 'AssessmentController@index'); +$router->add('POST', '/assessments', 'AssessmentController@store'); + +// Get request method and URI +$method = $_SERVER['REQUEST_METHOD']; +$uri = $_GET['request'] ?? '/'; + +// Remove trailing slash +$uri = rtrim($uri, '/'); +if (empty($uri)) $uri = '/'; + +$router->handle($method, $uri); \ No newline at end of file diff --git a/collaboration.php b/collaboration.php index 8cec1e9..7fec240 100644 --- a/collaboration.php +++ b/collaboration.php @@ -18,11 +18,11 @@ if (isset($_POST['upload_resource'])) { $description = $_POST['description']; $grade = $_POST['grade']; $subject = $_POST['subject']; + $is_public = isset($_POST['is_public']) ? 1 : 0; - // In a real app, we'd handle file uploads. For this demo, we'll just store the metadata. try { - $stmt = db()->prepare("INSERT INTO resources (title, description, teacher_id, school_id, grade, subject) VALUES (?, ?, ?, ?, ?, ?)"); - $stmt->execute([$title, $description, $user_id, $school_id, $grade, $subject]); + $stmt = db()->prepare("INSERT INTO resources (title, description, teacher_id, school_id, grade, subject, is_public) VALUES (?, ?, ?, ?, ?, ?, ?)"); + $stmt->execute([$title, $description, $user_id, $school_id, $grade, $subject, $is_public]); $success = "Resource shared successfully!"; } catch (PDOException $e) { $error = "Failed to share resource: " . $e->getMessage(); @@ -33,34 +33,55 @@ if (isset($_POST['upload_resource'])) { if (isset($_POST['create_post'])) { $title = $_POST['title']; $content = $_POST['content']; + $is_public = isset($_POST['is_public']) ? 1 : 0; try { - $stmt = db()->prepare("INSERT INTO forum_posts (title, content, author_id, school_id) VALUES (?, ?, ?, ?)"); - $stmt->execute([$title, $content, $user_id, $school_id]); + $stmt = db()->prepare("INSERT INTO forum_posts (title, content, author_id, school_id, is_public) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$title, $content, $user_id, $school_id, $is_public]); $success = "Topic posted successfully!"; } catch (PDOException $e) { $error = "Failed to post topic: " . $e->getMessage(); } } -// Fetch Resources -$resources = db()->prepare("SELECT r.*, u.email as teacher_email FROM resources r JOIN users u ON r.teacher_id = u.id WHERE r.school_id = ? ORDER BY r.created_at DESC"); -$resources->execute([$school_id]); -$resources = $resources->fetchAll(); +// Fetch Resources (Own school or Public from other schools) +$resources_query = " + SELECT r.*, u.email as teacher_email, s.name as school_name + FROM resources r + JOIN users u ON r.teacher_id = u.id + JOIN schools s ON r.school_id = s.id + WHERE r.school_id = :school_id OR r.is_public = 1 + ORDER BY r.created_at DESC +"; +$resources_stmt = db()->prepare($resources_query); +$resources_stmt->execute(['school_id' => $school_id]); +$resources = $resources_stmt->fetchAll(); -// Fetch Forum Posts -$posts = db()->prepare("SELECT p.*, u.email as author_email, (SELECT COUNT(*) FROM forum_comments WHERE post_id = p.id) as comment_count FROM forum_posts p JOIN users u ON p.author_id = u.id WHERE p.school_id = ? ORDER BY p.created_at DESC"); -$posts->execute([$school_id]); -$posts = $posts->fetchAll(); +// Fetch Forum Posts (Own school or Public from other schools) +$posts_query = " + SELECT p.*, u.email as author_email, s.name as school_name, + (SELECT COUNT(*) FROM forum_comments WHERE post_id = p.id) as comment_count + FROM forum_posts p + JOIN users u ON p.author_id = u.id + JOIN schools s ON p.school_id = s.id + WHERE p.school_id = :school_id OR p.is_public = 1 + ORDER BY p.created_at DESC +"; +$posts_stmt = db()->prepare($posts_query); +$posts_stmt->execute(['school_id' => $school_id]); +$posts = $posts_stmt->fetchAll(); -$pageTitle = "Teacher Collaboration Hub"; +$pageTitle = "School Collaboration Hub"; include 'includes/header.php'; ?>
-

Collaboration Hub

-
+
+

Collaboration Hub

+

Share resources and ideas with your school and the wider community.

+
+
@@ -71,18 +92,18 @@ include 'includes/header.php';
-