V6-INFINITYFREE1.0
This commit is contained in:
parent
33ad70235b
commit
48f139a135
4
api/v1/.htaccess
Normal file
4
api/v1/.htaccess
Normal file
@ -0,0 +1,4 @@
|
||||
RewriteEngine On
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^(.*)$ index.php?request=/$1 [QSA,L]
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
52
api/v1/Controllers/CollaborationController.php
Normal file
52
api/v1/Controllers/CollaborationController.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Api\Controllers;
|
||||
|
||||
use Api\Core\Controller;
|
||||
use Api\Core\Response;
|
||||
use Api\Core\Auth;
|
||||
|
||||
class CollaborationController extends Controller {
|
||||
public function resources() {
|
||||
$user = Auth::getUser();
|
||||
if (!$user) return Response::error('Unauthorized', 401);
|
||||
|
||||
$db = db();
|
||||
// Show resources from same school OR public resources from other schools
|
||||
$sql = "SELECT r.*, u.email as teacher_email
|
||||
FROM resources r
|
||||
JOIN users u ON r.teacher_id = u.id
|
||||
WHERE r.school_id = :school_id OR r.is_public = 1
|
||||
ORDER BY r.created_at DESC";
|
||||
|
||||
$stmt = $db->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']);
|
||||
}
|
||||
}
|
||||
30
api/v1/Controllers/EventController.php
Normal file
30
api/v1/Controllers/EventController.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Api\Controllers;
|
||||
|
||||
use Api\Core\Controller;
|
||||
use Api\Core\Response;
|
||||
use Api\Core\Auth;
|
||||
use Api\Models\Event;
|
||||
|
||||
class EventController extends Controller {
|
||||
public function index() {
|
||||
$user = Auth::getUser();
|
||||
if (!$user) return Response::error('Unauthorized', 401);
|
||||
|
||||
$events = Event::getAllBySchool($user['school_id']);
|
||||
Response::json($events);
|
||||
}
|
||||
|
||||
public function store() {
|
||||
$user = Auth::getUser();
|
||||
if (!$user) return Response::error('Unauthorized', 401);
|
||||
|
||||
$data = $this->getRequestData();
|
||||
$data['created_by'] = $user['id'];
|
||||
$data['school_id'] = $user['school_id'];
|
||||
|
||||
$id = Event::create($data);
|
||||
Response::json(['id' => $id, 'message' => 'Event created successfully']);
|
||||
}
|
||||
}
|
||||
32
api/v1/Controllers/LeaderboardController.php
Normal file
32
api/v1/Controllers/LeaderboardController.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Api\Controllers;
|
||||
|
||||
use Api\Core\Controller;
|
||||
use Api\Core\Response;
|
||||
use Api\Core\Auth;
|
||||
|
||||
class LeaderboardController extends Controller {
|
||||
public function index() {
|
||||
$user = Auth::getUser();
|
||||
if (!$user) return Response::error('Unauthorized', 401);
|
||||
|
||||
$db = db();
|
||||
$schoolId = $user['school_id'];
|
||||
|
||||
// Get learners with their average marks
|
||||
$sql = "SELECT l.id, l.full_name, AVG(m.marks_obtained / a.total_marks * 100) as average_percent
|
||||
FROM learners l
|
||||
JOIN marks m ON l.id = m.learner_id
|
||||
JOIN assessments a ON m.assessment_id = a.id
|
||||
WHERE l.school_id = :school_id
|
||||
GROUP BY l.id
|
||||
ORDER BY average_percent DESC";
|
||||
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute(['school_id' => $schoolId]);
|
||||
$leaderboard = $stmt->fetchAll();
|
||||
|
||||
Response::json($leaderboard);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
42
api/v1/Controllers/SchoolController.php
Normal file
42
api/v1/Controllers/SchoolController.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Api\Controllers;
|
||||
|
||||
use Api\Core\Controller;
|
||||
use Api\Core\Response;
|
||||
use Api\Core\Auth;
|
||||
use Api\Models\School;
|
||||
|
||||
class SchoolController extends Controller {
|
||||
public function index() {
|
||||
$user = Auth::getUser();
|
||||
if (!$user || $user['role'] !== 'Super Admin') {
|
||||
return Response::error('Unauthorized', 403);
|
||||
}
|
||||
|
||||
$schools = School::all();
|
||||
Response::json($schools);
|
||||
}
|
||||
|
||||
public function stats() {
|
||||
$user = Auth::getUser();
|
||||
if (!$user || $user['role'] !== 'Super Admin') {
|
||||
return Response::error('Unauthorized', 403);
|
||||
}
|
||||
|
||||
$stats = School::getStats();
|
||||
Response::json($stats);
|
||||
}
|
||||
|
||||
public function store() {
|
||||
$user = Auth::getUser();
|
||||
if (!$user || $user['role'] !== 'Super Admin') {
|
||||
return Response::error('Unauthorized', 403);
|
||||
}
|
||||
|
||||
$data = $this->getRequestData();
|
||||
$id = School::create($data);
|
||||
|
||||
Response::json(['id' => $id, 'message' => 'School onboarded successfully']);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
api/v1/Models/Event.php
Normal file
16
api/v1/Models/Event.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Api\Models;
|
||||
|
||||
use Api\Core\Model;
|
||||
|
||||
class Event extends Model {
|
||||
protected static $table = 'events';
|
||||
|
||||
public static function getAllBySchool($schoolId) {
|
||||
$db = db();
|
||||
$stmt = $db->prepare("SELECT * FROM " . static::$table . " WHERE school_id = ? ORDER BY start_datetime ASC");
|
||||
$stmt->execute([$schoolId]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
api/v1/Models/School.php
Normal file
22
api/v1/Models/School.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Api\Models;
|
||||
|
||||
use Api\Core\Model;
|
||||
|
||||
class School extends Model {
|
||||
protected static $table = 'schools';
|
||||
|
||||
public static function getStats() {
|
||||
$db = db();
|
||||
$total_schools = $db->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'
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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, '/');
|
||||
|
||||
4
db/migrations/006_add_default_super_admin.sql
Normal file
4
db/migrations/006_add_default_super_admin.sql
Normal file
@ -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)
|
||||
314
public/app.js
314
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 = `
|
||||
<li class="nav-item"><a class="nav-link" href="#/">Dashboard</a></li>
|
||||
let html = `<li class="nav-item"><a class="nav-link" href="#/">Dashboard</a></li>`;
|
||||
|
||||
if (state.user.role === 'Super Admin') {
|
||||
html += `<li class="nav-item"><a class="nav-link fw-bold text-warning" href="#/super-admin">Super Admin</a></li>`;
|
||||
}
|
||||
|
||||
html += `
|
||||
<li class="nav-item"><a class="nav-link" href="#/learners">Learners</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#/assessments">Assessments</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#" id="logout-btn">Logout</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#/events">Events</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#/collaboration">Hub</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="#/leaderboard">Ranking</a></li>
|
||||
<li class="nav-item"><a class="nav-link text-danger" href="#" id="logout-btn">Logout</a></li>
|
||||
`;
|
||||
|
||||
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(`
|
||||
<div class="p-5 mb-4 bg-white rounded-3 shadow-sm">
|
||||
<h1 class="display-5 fw-bold">Welcome, ${state.user.email}</h1>
|
||||
<p class="col-md-8 fs-4">This is the new static frontend for SOMS Platform. Everything is served from an API.</p>
|
||||
<div class="p-4 mb-4 bg-white rounded-4 shadow-sm">
|
||||
<h1 class="h2 fw-bold">Welcome back, ${state.user.email}</h1>
|
||||
<p class="text-muted">You are logged in as <strong>${state.user.role}</strong>.</p>
|
||||
<hr class="my-4">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Learners</h5>
|
||||
<p class="card-text">Manage your student records.</p>
|
||||
<a href="#/learners" class="btn btn-outline-primary">View All</a>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card h-100 border-0 shadow-sm text-center p-3">
|
||||
<i class="bi bi-people fs-1 text-primary mb-2"></i>
|
||||
<h5 class="card-title">Learners</h5>
|
||||
<a href="#/learners" class="btn btn-sm btn-outline-primary mt-auto">Manage</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Assessments</h5>
|
||||
<p class="card-text">Record marks and track progress.</p>
|
||||
<a href="#/assessments" class="btn btn-outline-primary">Open Hub</a>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card h-100 border-0 shadow-sm text-center p-3">
|
||||
<i class="bi bi-journal-check fs-1 text-success mb-2"></i>
|
||||
<h5 class="card-title">Assessments</h5>
|
||||
<a href="#/assessments" class="btn btn-sm btn-outline-success mt-auto">Open Hub</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card h-100 border-0 shadow-sm text-center p-3">
|
||||
<i class="bi bi-calendar-event fs-1 text-warning mb-2"></i>
|
||||
<h5 class="card-title">Events</h5>
|
||||
<a href="#/events" class="btn btn-sm btn-outline-warning mt-auto">Calendar</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card h-100 border-0 shadow-sm text-center p-3">
|
||||
<i class="bi bi-trophy fs-1 text-info mb-2"></i>
|
||||
<h5 class="card-title">Leaderboard</h5>
|
||||
<a href="#/leaderboard" class="btn btn-sm btn-outline-info mt-auto">View Rankings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -115,20 +143,25 @@ function loginPage() {
|
||||
render(`
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-5">
|
||||
<div class="card shadow-lg border-0">
|
||||
<div class="card shadow-lg border-0 rounded-4">
|
||||
<div class="card-body p-5">
|
||||
<h2 class="fw-bold text-center mb-4">Sign In</h2>
|
||||
<h2 class="fw-bold text-center mb-4 text-primary"><i class="bi bi-mortarboard-fill me-2"></i>SOMS Platform</h2>
|
||||
<form id="login-form">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Email</label>
|
||||
<input type="email" id="email" class="form-control" value="admin@sowetohigh.edu.za" required>
|
||||
<input type="email" id="email" class="form-control" placeholder="superadmin@system.com" required>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="form-label">Password</label>
|
||||
<input type="password" id="password" class="form-control" value="password" required>
|
||||
<input type="password" id="password" class="form-control" placeholder="password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Login</button>
|
||||
<button type="submit" class="btn btn-primary w-100 py-2 fw-bold shadow">Sign In</button>
|
||||
</form>
|
||||
<div class="mt-4 p-3 bg-light rounded-3 text-center text-muted small">
|
||||
<strong>Demo Credentials</strong><br>
|
||||
Super Admin: superadmin@system.com / password<br>
|
||||
Admin: admin@sowetohigh.edu.za / password
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -160,17 +193,96 @@ function loginPage() {
|
||||
});
|
||||
}
|
||||
|
||||
async function superAdminPage() {
|
||||
render('<div class="text-center p-5"><div class="spinner-border text-primary"></div></div>');
|
||||
try {
|
||||
const stats = await apiFetch('/schools/stats');
|
||||
const schools = await apiFetch('/schools');
|
||||
|
||||
let html = `
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<h2 class="fw-bold h3">Super Admin Console</h2>
|
||||
<p class="text-muted">Global platform management</p>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button class="btn btn-primary shadow">Onboard New School</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 mb-5">
|
||||
<div class="col-md-3">
|
||||
<div class="card p-3 border-0 shadow-sm text-center">
|
||||
<div class="text-muted small mb-1">Total Schools</div>
|
||||
<div class="h2 fw-bold text-primary">${stats.total_schools}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card p-3 border-0 shadow-sm text-center">
|
||||
<div class="text-muted small mb-1">Total Learners</div>
|
||||
<div class="h2 fw-bold text-success">${stats.total_learners}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card p-3 border-0 shadow-sm text-center">
|
||||
<div class="text-muted small mb-1">System Uptime</div>
|
||||
<div class="h2 fw-bold text-info">${stats.uptime}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card p-3 border-0 shadow-sm text-center">
|
||||
<div class="text-muted small mb-1">Data Storage</div>
|
||||
<div class="h2 fw-bold text-warning">${stats.storage}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border-0 shadow-sm rounded-4">
|
||||
<div class="card-header bg-white py-3">
|
||||
<h5 class="mb-0 fw-bold">Participating Schools</h5>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="ps-4">Name</th>
|
||||
<th>Province</th>
|
||||
<th>District</th>
|
||||
<th class="text-end pe-4">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
schools.forEach(s => {
|
||||
html += `
|
||||
<tr>
|
||||
<td class="ps-4 fw-bold text-primary">${s.name}</td>
|
||||
<td>${s.province}</td>
|
||||
<td>${s.district}</td>
|
||||
<td class="text-end pe-4">
|
||||
<button class="btn btn-sm btn-outline-primary">Dashboard</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
html += '</tbody></table></div></div>';
|
||||
render(html);
|
||||
} catch (err) {
|
||||
render('<div class="alert alert-danger">Access Denied: Super Admin only</div>');
|
||||
}
|
||||
}
|
||||
|
||||
async function learnersPage() {
|
||||
render('<div class="text-center"><div class="spinner-border text-primary"></div></div>');
|
||||
render('<div class="text-center p-5"><div class="spinner-border text-primary"></div></div>');
|
||||
try {
|
||||
const learners = await apiFetch('/learners');
|
||||
let html = `
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2>Learners</h2>
|
||||
<button class="btn btn-primary">Add New</button>
|
||||
<h2 class="fw-bold">Learners</h2>
|
||||
<button class="btn btn-primary btn-sm">Add New Learner</button>
|
||||
</div>
|
||||
<div class="table-responsive bg-white p-3 rounded shadow-sm">
|
||||
<table class="table table-hover">
|
||||
<div class="table-responsive bg-white p-3 rounded-4 shadow-sm">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Full Name</th>
|
||||
@ -185,8 +297,8 @@ async function learnersPage() {
|
||||
html += `
|
||||
<tr>
|
||||
<td>${l.full_name}</td>
|
||||
<td>${l.grade}</td>
|
||||
<td>${l.student_id}</td>
|
||||
<td><span class="badge bg-light text-dark">${l.grade}</span></td>
|
||||
<td><code>${l.student_id}</code></td>
|
||||
<td><button class="btn btn-sm btn-outline-secondary">Edit</button></td>
|
||||
</tr>
|
||||
`;
|
||||
@ -199,24 +311,26 @@ async function learnersPage() {
|
||||
}
|
||||
|
||||
async function assessmentsPage() {
|
||||
render('<div class="text-center"><div class="spinner-border text-primary"></div></div>');
|
||||
render('<div class="text-center p-5"><div class="spinner-border text-primary"></div></div>');
|
||||
try {
|
||||
const assessments = await apiFetch('/assessments');
|
||||
let html = `
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2>Assessments</h2>
|
||||
<button class="btn btn-primary">Create New</button>
|
||||
<h2 class="fw-bold">Assessments</h2>
|
||||
<button class="btn btn-primary btn-sm">Create Assessment</button>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
`;
|
||||
assessments.forEach(a => {
|
||||
html += `
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm border-0">
|
||||
<div class="card shadow-sm border-0 rounded-4">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">${a.title}</h5>
|
||||
<h6 class="card-subtitle mb-2 text-muted">${a.subject} - ${a.type}</h6>
|
||||
<button class="btn btn-sm btn-primary">Record Marks</button>
|
||||
<h5 class="card-title fw-bold">${a.title || a.name}</h5>
|
||||
<h6 class="card-subtitle mb-3 text-muted">${a.subject || a.grade} — ${a.type}</h6>
|
||||
<div class="d-grid">
|
||||
<button class="btn btn-sm btn-primary">Record Marks</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -229,4 +343,116 @@ async function assessmentsPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function eventsPage() {
|
||||
render('<div class="text-center p-5"><div class="spinner-border text-primary"></div></div>');
|
||||
try {
|
||||
const events = await apiFetch('/events');
|
||||
let html = `
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="fw-bold">School Calendar</h2>
|
||||
<button class="btn btn-primary btn-sm">Add Event</button>
|
||||
</div>
|
||||
<div class="bg-white p-4 rounded-4 shadow-sm">
|
||||
`;
|
||||
if (events.length === 0) html += '<p class="text-muted text-center">No upcoming events.</p>';
|
||||
events.forEach(e => {
|
||||
html += `
|
||||
<div class="d-flex border-bottom py-3">
|
||||
<div class="text-center me-4" style="min-width: 60px;">
|
||||
<div class="h3 fw-bold mb-0">${new Date(e.start_datetime).getDate()}</div>
|
||||
<div class="small text-uppercase text-muted">${new Date(e.start_datetime).toLocaleString('default', { month: 'short' })}</div>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-1 fw-bold">${e.title}</h5>
|
||||
<p class="text-muted small mb-0"><i class="bi bi-clock"></i> ${new Date(e.start_datetime).toLocaleTimeString()} — ${e.location || 'No location'}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += '</div>';
|
||||
render(html);
|
||||
} catch (err) {
|
||||
render('<div class="alert alert-danger">Failed to load events</div>');
|
||||
}
|
||||
}
|
||||
|
||||
async function collaborationPage() {
|
||||
render('<div class="text-center p-5"><div class="spinner-border text-primary"></div></div>');
|
||||
try {
|
||||
const resources = await apiFetch('/collaboration/resources');
|
||||
let html = `
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="fw-bold">Collaboration Hub</h2>
|
||||
<button class="btn btn-primary btn-sm">Share Resource</button>
|
||||
</div>
|
||||
<div class="row g-4">
|
||||
`;
|
||||
resources.forEach(r => {
|
||||
html += `
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100 border-0 shadow-sm rounded-4">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<h5 class="fw-bold mb-1">${r.title}</h5>
|
||||
${r.is_public == 1 ? '<span class="badge bg-success">Public</span>' : '<span class="badge bg-secondary">School Only</span>'}
|
||||
</div>
|
||||
<p class="text-muted small mb-3">${r.description}</p>
|
||||
<div class="d-flex align-items-center text-muted small">
|
||||
<i class="bi bi-person-circle me-2"></i> ${r.teacher_email}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer bg-transparent border-0 text-end">
|
||||
<button class="btn btn-link btn-sm text-decoration-none">Download</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
html += '</div>';
|
||||
render(html);
|
||||
} catch (err) {
|
||||
render('<div class="alert alert-danger">Failed to load resources</div>');
|
||||
}
|
||||
}
|
||||
|
||||
async function leaderboardPage() {
|
||||
render('<div class="text-center p-5"><div class="spinner-border text-primary"></div></div>');
|
||||
try {
|
||||
const rankings = await apiFetch('/leaderboard');
|
||||
let html = `
|
||||
<h2 class="fw-bold mb-4">Student Rankings</h2>
|
||||
<div class="bg-white p-3 rounded-4 shadow-sm">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="80">Rank</th>
|
||||
<th>Learner</th>
|
||||
<th>Performance</th>
|
||||
<th class="text-end">Average</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
rankings.forEach((r, index) => {
|
||||
const color = index === 0 ? 'text-warning' : (index === 1 ? 'text-secondary' : (index === 2 ? 'text-brown' : ''));
|
||||
html += `
|
||||
<tr>
|
||||
<td class="h4 fw-bold ${color}">#${index + 1}</td>
|
||||
<td><div class="fw-bold">${r.full_name}</div></td>
|
||||
<td>
|
||||
<div class="progress" style="height: 10px;">
|
||||
<div class="progress-bar bg-primary" style="width: ${r.average_percent}%"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-end fw-bold">${parseFloat(r.average_percent).toFixed(1)}%</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
html += '</tbody></table></div>';
|
||||
render(html);
|
||||
} catch (err) {
|
||||
render('<div class="alert alert-danger">Failed to load leaderboard</div>');
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
@ -5,14 +5,39 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SOMS Platform | Modern School Management</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||
<link rel="stylesheet" href="assets/css/custom.css">
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<style>
|
||||
:root {
|
||||
--bs-primary: #4361ee;
|
||||
--bs-primary-rgb: 67, 97, 238;
|
||||
}
|
||||
body {
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
.navbar {
|
||||
background-color: var(--bs-primary) !important;
|
||||
}
|
||||
.card {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
.btn-primary {
|
||||
background-color: var(--bs-primary);
|
||||
border-color: var(--bs-primary);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<div id="app">
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-primary shadow-sm mb-4">
|
||||
<nav class="navbar navbar-expand-lg navbar-dark shadow-sm mb-4">
|
||||
<div class="container">
|
||||
<a class="navbar-brand fw-bold" href="#/">SOMS Platform</a>
|
||||
<a class="navbar-brand fw-bold" href="#/">
|
||||
<i class="bi bi-mortarboard-fill me-2"></i>SOMS Platform
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
@ -24,7 +49,7 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container">
|
||||
<main class="container mb-5">
|
||||
<div id="content">
|
||||
<div class="text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
@ -36,6 +61,6 @@
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="app.js"></script>
|
||||
<script src="app.js?v=1"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user