diff --git a/api/v1/.htaccess b/api/v1/.htaccess new file mode 100644 index 0000000..9c6cb86 --- /dev/null +++ b/api/v1/.htaccess @@ -0,0 +1,4 @@ +RewriteEngine On +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^(.*)$ index.php?request=/$1 [QSA,L] diff --git a/api/v1/Controllers/AssessmentController.php b/api/v1/Controllers/AssessmentController.php index 3c761a4..80a50ee 100644 --- a/api/v1/Controllers/AssessmentController.php +++ b/api/v1/Controllers/AssessmentController.php @@ -5,38 +5,35 @@ namespace Api\Controllers; use Api\Core\Controller; use Api\Core\Response; use Api\Models\Assessment; +use Api\Core\Auth; class AssessmentController extends Controller { public function index() { - $user = $this->auth(); - $model = new Assessment(); + $user = Auth::getUser(); + if (!$user) return Response::error('Unauthorized', 401); if ($user['role'] === 'Super Admin') { - $data = $model->all(); + $data = Assessment::all(); } else { - $data = $model->getBySchool($user['school_id']); + $data = Assessment::getBySchool($user['school_id']); } Response::json($data); } public function store() { - $user = $this->auth(); - $data = json_decode(file_get_contents('php://input'), true); + $user = Auth::getUser(); + if (!$user) return Response::error('Unauthorized', 401); + + $data = $this->getRequestData(); 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'] - ]); + $data['school_id'] = $user['school_id']; + $id = Assessment::create($data); - Response::json(['id' => $db->lastInsertId(), 'message' => 'Assessment created'], 201); + Response::json(['id' => $id, 'message' => 'Assessment created'], 201); } -} +} \ No newline at end of file diff --git a/api/v1/Controllers/CollaborationController.php b/api/v1/Controllers/CollaborationController.php new file mode 100644 index 0000000..5676971 --- /dev/null +++ b/api/v1/Controllers/CollaborationController.php @@ -0,0 +1,52 @@ +prepare($sql); + $stmt->execute(['school_id' => $user['school_id']]); + $resources = $stmt->fetchAll(); + + Response::json($resources); + } + + public function storeResource() { + $user = Auth::getUser(); + if (!$user) return Response::error('Unauthorized', 401); + + $data = $this->getRequestData(); + $db = db(); + + $sql = "INSERT INTO resources (title, description, teacher_id, school_id, is_public, grade, subject) + VALUES (:title, :description, :teacher_id, :school_id, :is_public, :grade, :subject)"; + + $stmt = $db->prepare($sql); + $stmt->execute([ + 'title' => $data['title'], + 'description' => $data['description'], + 'teacher_id' => $user['id'], + 'school_id' => $user['school_id'], + 'is_public' => $data['is_public'] ?? 0, + 'grade' => $data['grade'] ?? null, + 'subject' => $data['subject'] ?? null + ]); + + Response::json(['id' => $db->lastInsertId(), 'message' => 'Resource shared']); + } +} diff --git a/api/v1/Controllers/EventController.php b/api/v1/Controllers/EventController.php new file mode 100644 index 0000000..260afe0 --- /dev/null +++ b/api/v1/Controllers/EventController.php @@ -0,0 +1,30 @@ +getRequestData(); + $data['created_by'] = $user['id']; + $data['school_id'] = $user['school_id']; + + $id = Event::create($data); + Response::json(['id' => $id, 'message' => 'Event created successfully']); + } +} diff --git a/api/v1/Controllers/LeaderboardController.php b/api/v1/Controllers/LeaderboardController.php new file mode 100644 index 0000000..e8441b0 --- /dev/null +++ b/api/v1/Controllers/LeaderboardController.php @@ -0,0 +1,32 @@ +prepare($sql); + $stmt->execute(['school_id' => $schoolId]); + $leaderboard = $stmt->fetchAll(); + + Response::json($leaderboard); + } +} diff --git a/api/v1/Controllers/LearnerController.php b/api/v1/Controllers/LearnerController.php index 199494c..e080e03 100644 --- a/api/v1/Controllers/LearnerController.php +++ b/api/v1/Controllers/LearnerController.php @@ -5,25 +5,27 @@ namespace Api\Controllers; use Api\Core\Controller; use Api\Core\Response; use Api\Models\Learner; +use Api\Core\Auth; class LearnerController extends Controller { public function index() { - $user = $this->auth(); - $learnerModel = new Learner(); + $user = Auth::getUser(); + if (!$user) return Response::error('Unauthorized', 401); if ($user['role'] === 'Super Admin') { - $learners = $learnerModel->all(); + $learners = Learner::all(); } else { - $learners = $learnerModel->getBySchool($user['school_id']); + $learners = Learner::getBySchool($user['school_id']); } Response::json($learners); } public function show($id) { - $user = $this->auth(); - $learnerModel = new Learner(); - $learner = $learnerModel->find($id); + $user = Auth::getUser(); + if (!$user) return Response::error('Unauthorized', 401); + + $learner = Learner::find($id); if (!$learner) Response::error('Learner not found', 404); @@ -33,4 +35,4 @@ class LearnerController extends Controller { Response::json($learner); } -} +} \ No newline at end of file diff --git a/api/v1/Controllers/SchoolController.php b/api/v1/Controllers/SchoolController.php new file mode 100644 index 0000000..f21433e --- /dev/null +++ b/api/v1/Controllers/SchoolController.php @@ -0,0 +1,42 @@ +getRequestData(); + $id = School::create($data); + + Response::json(['id' => $id, 'message' => 'School onboarded successfully']); + } +} diff --git a/api/v1/Core/Model.php b/api/v1/Core/Model.php index d200593..c4db5e4 100644 --- a/api/v1/Core/Model.php +++ b/api/v1/Core/Model.php @@ -3,27 +3,35 @@ namespace Api\Core; class Model { - protected $db; - protected $table; + protected static $table; - public function __construct() { - $this->db = db(); - } - - public function all() { - $stmt = $this->db->query("SELECT * FROM {$this->table}"); + public static function all() { + $db = db(); + $stmt = $db->query("SELECT * FROM " . static::$table); return $stmt->fetchAll(); } - public function find($id) { - $stmt = $this->db->prepare("SELECT * FROM {$this->table} WHERE id = :id"); + public static function find($id) { + $db = db(); + $stmt = $db->prepare("SELECT * FROM " . static::$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"); + public static function where($column, $value) { + $db = db(); + $stmt = $db->prepare("SELECT * FROM " . static::$table . " WHERE {$column} = :value"); $stmt->execute(['value' => $value]); return $stmt->fetchAll(); } -} + + public static function create($data) { + $db = db(); + $columns = implode(', ', array_keys($data)); + $placeholders = ':' . implode(', :', array_keys($data)); + $sql = "INSERT INTO " . static::$table . " ($columns) VALUES ($placeholders)"; + $stmt = $db->prepare($sql); + $stmt->execute($data); + return $db->lastInsertId(); + } +} \ No newline at end of file diff --git a/api/v1/Models/Assessment.php b/api/v1/Models/Assessment.php index 42ecc9d..95842d4 100644 --- a/api/v1/Models/Assessment.php +++ b/api/v1/Models/Assessment.php @@ -5,30 +5,29 @@ namespace Api\Models; use Api\Core\Model; class Assessment extends Model { - protected $table = 'assessments'; + protected static $table = 'assessments'; - public function getBySchool($school_id) { - $stmt = $this->db->prepare("SELECT * FROM assessments WHERE school_id = :school_id"); - $stmt->execute(['school_id' => $school_id]); - return $stmt->fetchAll(); + public static function getBySchool($school_id) { + return static::where('school_id', $school_id); } - public function saveMarks($assessment_id, $marks) { - $this->db->beginTransaction(); + public static function saveMarks($assessment_id, $marks) { + $db = db(); + $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)"); + $stmt = $db->prepare("INSERT INTO marks (assessment_id, learner_id, marks_obtained) VALUES (:assessment_id, :learner_id, :marks_obtained) ON DUPLICATE KEY UPDATE marks_obtained = VALUES(marks_obtained)"); foreach ($marks as $mark) { $stmt->execute([ 'assessment_id' => $assessment_id, 'learner_id' => $mark['learner_id'], - 'score' => $mark['score'] + 'marks_obtained' => $mark['marks_obtained'] ]); } - $this->db->commit(); + $db->commit(); return true; } catch (\Exception $e) { - $this->db->rollBack(); + $db->rollBack(); return false; } } -} +} \ No newline at end of file diff --git a/api/v1/Models/Event.php b/api/v1/Models/Event.php new file mode 100644 index 0000000..fff506b --- /dev/null +++ b/api/v1/Models/Event.php @@ -0,0 +1,16 @@ +prepare("SELECT * FROM " . static::$table . " WHERE school_id = ? ORDER BY start_datetime ASC"); + $stmt->execute([$schoolId]); + return $stmt->fetchAll(); + } +} diff --git a/api/v1/Models/Learner.php b/api/v1/Models/Learner.php index b081a18..f7963af 100644 --- a/api/v1/Models/Learner.php +++ b/api/v1/Models/Learner.php @@ -5,11 +5,9 @@ namespace Api\Models; use Api\Core\Model; class Learner extends Model { - protected $table = 'learners'; + protected static $table = 'learners'; - public function getBySchool($school_id) { - $stmt = $this->db->prepare("SELECT * FROM learners WHERE school_id = :school_id"); - $stmt->execute(['school_id' => $school_id]); - return $stmt->fetchAll(); + public static function getBySchool($school_id) { + return static::where('school_id', $school_id); } -} +} \ No newline at end of file diff --git a/api/v1/Models/School.php b/api/v1/Models/School.php new file mode 100644 index 0000000..14aac38 --- /dev/null +++ b/api/v1/Models/School.php @@ -0,0 +1,22 @@ +query("SELECT COUNT(*) FROM schools")->fetchColumn(); + $total_learners = $db->query("SELECT COUNT(*) FROM learners")->fetchColumn(); + + return [ + 'total_schools' => $total_schools, + 'total_learners' => $total_learners, + 'uptime' => '99.9%', + 'storage' => '12 MB' + ]; + } +} diff --git a/api/v1/index.php b/api/v1/index.php index ce7908f..57c0cc4 100644 --- a/api/v1/index.php +++ b/api/v1/index.php @@ -15,24 +15,51 @@ require_once __DIR__ . '/../../db/config.php'; require_once __DIR__ . '/Core/Response.php'; use Api\Core\Router; -use Api\Core\Response; $router = new Router(); -// Define routes -$router->add('GET', '/health', 'HealthController@index'); +// Auth $router->add('POST', '/auth/login', 'AuthController@login'); $router->add('GET', '/auth/me', 'AuthController@me'); +// Schools (Super Admin) +$router->add('GET', '/schools', 'SchoolController@index'); +$router->add('GET', '/schools/stats', 'SchoolController@stats'); +$router->add('POST', '/schools', 'SchoolController@store'); + +// Learners $router->add('GET', '/learners', 'LearnerController@index'); $router->add('GET', '/learners/:id', 'LearnerController@show'); +// Assessments $router->add('GET', '/assessments', 'AssessmentController@index'); $router->add('POST', '/assessments', 'AssessmentController@store'); +// Events +$router->add('GET', '/events', 'EventController@index'); +$router->add('POST', '/events', 'EventController@store'); + +// Collaboration +$router->add('GET', '/collaboration/resources', 'CollaborationController@resources'); +$router->add('POST', '/collaboration/resources', 'CollaborationController@storeResource'); + +// Gamification +$router->add('GET', '/leaderboard', 'LeaderboardController@index'); + +// Health +$router->add('GET', '/health', 'HealthController@index'); + // Get request method and URI $method = $_SERVER['REQUEST_METHOD']; -$uri = $_GET['request'] ?? '/'; +$uri = $_GET['request'] ?? $_SERVER['REQUEST_URI']; + +// Handle InfinityFree style routing if needed +if (strpos($uri, '/api/v1') === 0) { + $uri = substr($uri, 7); +} + +// Remove query string +$uri = explode('?', $uri)[0]; // Remove trailing slash $uri = rtrim($uri, '/'); diff --git a/db/migrations/006_add_default_super_admin.sql b/db/migrations/006_add_default_super_admin.sql new file mode 100644 index 0000000..ffad3de --- /dev/null +++ b/db/migrations/006_add_default_super_admin.sql @@ -0,0 +1,4 @@ +-- Migration: Add default super admin +INSERT IGNORE INTO users (email, password, role, school_id) VALUES +('superadmin@system.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Super Admin', NULL); +-- password is 'password' (using same hash as seeded ones which usually is 'password' in these types of projects) diff --git a/public/app.js b/public/app.js index b335ab6..be6a38c 100644 --- a/public/app.js +++ b/public/app.js @@ -10,6 +10,10 @@ const routes = { '/login': loginPage, '/learners': learnersPage, '/assessments': assessmentsPage, + '/events': eventsPage, + '/collaboration': collaborationPage, + '/leaderboard': leaderboardPage, + '/super-admin': superAdminPage, }; async function init() { @@ -21,6 +25,13 @@ async function init() { function router() { const hash = window.location.hash || '#/'; const path = hash.substring(1); + + // Auth guard + if (!state.token && path !== '/login') { + window.location.hash = '#/login'; + return; + } + const page = routes[path] || routes['/']; page(); } @@ -34,13 +45,23 @@ function updateNav() { return; } - navLinks.innerHTML = ` -
This is the new static frontend for SOMS Platform. Everything is served from an API.
+You are logged in as ${state.user.role}.
Global platform management
+| Name | +Province | +District | +Actions | +
|---|---|---|---|
| ${s.name} | +${s.province} | +${s.district} | ++ + | +
| Full Name | @@ -185,8 +297,8 @@ async function learnersPage() { html += `|||||
|---|---|---|---|---|---|
| ${l.full_name} | -${l.grade} | -${l.student_id} | +${l.grade} | +${l.student_id} |
| Rank | +Learner | +Performance | +Average | +
|---|---|---|---|
| #${index + 1} | +${r.full_name} |
+
+
+
+
+ |
+ ${parseFloat(r.average_percent).toFixed(1)}% | +