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 = ` - + let html = ``; + + if (state.user.role === 'Super Admin') { + html += ``; + } + + html += ` - + + + + `; + navLinks.innerHTML = html; + document.getElementById('logout-btn').addEventListener('click', (e) => { e.preventDefault(); logout(); @@ -65,11 +86,12 @@ async function apiFetch(endpoint, options = {}) { } }; const response = await fetch(url, { ...defaultOptions, ...options }); - const data = await response.json(); if (response.status === 401) { logout(); throw new Error('Unauthorized'); } + const data = await response.json(); + if (!response.ok) throw new Error(data.error || 'API Error'); return data; } @@ -78,32 +100,38 @@ function render(html) { } async function homePage() { - if (!state.token) { - window.location.hash = '#/login'; - return; - } render(` -
-

Welcome, ${state.user.email}

-

This is the new static frontend for SOMS Platform. Everything is served from an API.

+
+

Welcome back, ${state.user.email}

+

You are logged in as ${state.user.role}.


-
-
-
-
Learners
-

Manage your student records.

- View All -
+
+
+ +
Learners
+ Manage
-
-
-
-
Assessments
-

Record marks and track progress.

- Open Hub -
+
+
+ +
Assessments
+ Open Hub +
+
+
+
+ +
Events
+ Calendar +
+
+
+
+ +
Leaderboard
+ View Rankings
@@ -115,20 +143,25 @@ function loginPage() { render(`
-
+
-

Sign In

+

SOMS Platform

- +
- +
- +
+
+ Demo Credentials
+ Super Admin: superadmin@system.com / password
+ Admin: admin@sowetohigh.edu.za / password +
@@ -160,17 +193,96 @@ function loginPage() { }); } +async function superAdminPage() { + render('
'); + try { + const stats = await apiFetch('/schools/stats'); + const schools = await apiFetch('/schools'); + + let html = ` +
+
+

Super Admin Console

+

Global platform management

+
+
+ +
+
+ +
+
+
+
Total Schools
+
${stats.total_schools}
+
+
+
+
+
Total Learners
+
${stats.total_learners}
+
+
+
+
+
System Uptime
+
${stats.uptime}
+
+
+
+
+
Data Storage
+
${stats.storage}
+
+
+
+ +
+
+
Participating Schools
+
+
+ + + + + + + + + + + `; + schools.forEach(s => { + html += ` + + + + + + + `; + }); + html += '
NameProvinceDistrictActions
${s.name}${s.province}${s.district} + +
'; + render(html); + } catch (err) { + render('
Access Denied: Super Admin only
'); + } +} + async function learnersPage() { - render('
'); + render('
'); try { const learners = await apiFetch('/learners'); let html = `
-

Learners

- +

Learners

+
-
- +
+
@@ -185,8 +297,8 @@ async function learnersPage() { html += ` - - + + `; @@ -199,24 +311,26 @@ async function learnersPage() { } async function assessmentsPage() { - render('
'); + render('
'); try { const assessments = await apiFetch('/assessments'); let html = `
-

Assessments

- +

Assessments

+
`; assessments.forEach(a => { html += `
-
+
-
${a.title}
-
${a.subject} - ${a.type}
- +
${a.title || a.name}
+
${a.subject || a.grade} — ${a.type}
+
+ +
@@ -229,4 +343,116 @@ async function assessmentsPage() { } } +async function eventsPage() { + render('
'); + try { + const events = await apiFetch('/events'); + let html = ` +
+

School Calendar

+ +
+
+ `; + if (events.length === 0) html += '

No upcoming events.

'; + events.forEach(e => { + html += ` +
+
+
${new Date(e.start_datetime).getDate()}
+
${new Date(e.start_datetime).toLocaleString('default', { month: 'short' })}
+
+
+
${e.title}
+

${new Date(e.start_datetime).toLocaleTimeString()} — ${e.location || 'No location'}

+
+
+ `; + }); + html += '
'; + render(html); + } catch (err) { + render('
Failed to load events
'); + } +} + +async function collaborationPage() { + render('
'); + try { + const resources = await apiFetch('/collaboration/resources'); + let html = ` +
+

Collaboration Hub

+ +
+
+ `; + resources.forEach(r => { + html += ` +
+
+
+
+
${r.title}
+ ${r.is_public == 1 ? 'Public' : 'School Only'} +
+

${r.description}

+
+ ${r.teacher_email} +
+
+ +
+
+ `; + }); + html += '
'; + render(html); + } catch (err) { + render('
Failed to load resources
'); + } +} + +async function leaderboardPage() { + render('
'); + try { + const rankings = await apiFetch('/leaderboard'); + let html = ` +

Student Rankings

+
+
Full Name
${l.full_name}${l.grade}${l.student_id}${l.grade}${l.student_id}
+ + + + + + + + + + `; + rankings.forEach((r, index) => { + const color = index === 0 ? 'text-warning' : (index === 1 ? 'text-secondary' : (index === 2 ? 'text-brown' : '')); + html += ` + + + + + + + `; + }); + html += '
RankLearnerPerformanceAverage
#${index + 1}
${r.full_name}
+
+
+
+
${parseFloat(r.average_percent).toFixed(1)}%
'; + render(html); + } catch (err) { + render('
Failed to load leaderboard
'); + } +} + init(); diff --git a/public/index.html b/public/index.html index 6aee20c..12f0c25 100644 --- a/public/index.html +++ b/public/index.html @@ -5,14 +5,39 @@ SOMS Platform | Modern School Management + +
-