const express = require('express'); const cors = require('cors'); const path = require('path'); const Database = require('better-sqlite3'); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); // Import controllers const usersController = require('./controllers/users.controller'); const departmentsController = require('./controllers/departments.controller'); const reportsController = require('./controllers/reports.controller'); const settingsController = require('./controllers/settings.controller'); const coursesController = require('./controllers/courses.controller'); const assignmentsController = require('./controllers/assignments.controller'); const gradesController = require('./controllers/grades.controller'); const attendanceController = require('./controllers/attendance.controller'); const messagesController = require('./controllers/messages.controller'); const calendarController = require('./controllers/calendar.controller'); const examsController = require('./controllers/exams.controller'); const paynowController = require('./controllers/paynow.controller'); const hrController = require('./controllers/hr.controller'); const financeController = require('./controllers/finance.controller'); const teacherController = require('./controllers/teacher.controller'); const socialController = require('./controllers/social.controller'); const { getSyncEngine } = require('./services/SyncEngine'); const app = express(); const PORT = process.env.PORT || 3001; const JWT_SECRET = process.env.JWT_SECRET || 'africa-alert-secret-key-2024'; // Database setup const dbPath = process.env.DB_PATH || path.join(__dirname, '../data/school.db'); const db = new Database(dbPath); db.pragma('foreign_keys = ON'); // Initialize Sync Engine getSyncEngine(); // Middleware app.use(cors()); app.use(express.json()); app.use('/uploads', express.static(path.join(__dirname, '../uploads'))); // Register controller routes app.use('/api/users', usersController); app.use('/api/departments', departmentsController); app.use('/api/reports', reportsController); app.use('/api/settings', settingsController); app.use('/api/courses', coursesController); app.use('/api/assignments', assignmentsController); app.use('/api/grades', gradesController); app.use('/api/attendance', attendanceController); app.use('/api/messages', messagesController); app.use('/api/calendar', calendarController); app.use('/api/exams', examsController); app.use('/api/paynow', paynowController); app.use('/api/hr', hrController); app.use('/api/finance', financeController); app.use('/api/teacher', teacherController); app.use('/api/social', socialController); // Auth middleware const auth = (req, res, next) => { const token = req.headers.authorization?.split(' ')[1]; if (!token) return res.status(401).json({ error: 'No token provided' }); try { req.user = jwt.verify(token, JWT_SECRET); next(); } catch (err) { res.status(401).json({ error: 'Invalid token' }); } }; // ============ AUTH ROUTES ============ app.post('/api/auth/login', (req, res) => { const { email, password } = req.body; const user = db.prepare('SELECT * FROM users WHERE email = ?').get(email); if (!user || !bcrypt.compareSync(password, user.password)) { return res.status(401).json({ error: 'Invalid credentials' }); } const token = jwt.sign({ id: user.id, email: user.email, role: user.role }, JWT_SECRET, { expiresIn: '7d' }); res.json({ token, user: { id: user.id, email: user.email, role: user.role, first_name: user.first_name, last_name: user.last_name } }); }); app.post('/api/auth/register', (req, res) => { const { email, password, role, first_name, last_name, phone } = req.body; const { v4: uuidv4 } = require('uuid'); try { const hashedPassword = bcrypt.hashSync(password, 10); const uid = uuidv4(); const result = db.prepare(` INSERT INTO users (uid, email, password, role, first_name, last_name, phone) VALUES (?, ?, ?, ?, ?, ?, ?) `).run(uid, email, hashedPassword, role || 'student', first_name, last_name, phone); res.json({ id: result.lastInsertRowid, email, role: role || 'student' }); } catch (err) { res.status(400).json({ error: 'Email already exists' }); } }); // ============ DASHBOARD STATS ============ app.get('/api/dashboard/stats', auth, (req, res) => { const stats = { totalStudents: db.prepare("SELECT COUNT(*) as count FROM users WHERE role = 'student'").get().count, totalTeachers: db.prepare("SELECT COUNT(*) as count FROM users WHERE role = 'teacher'").get().count, totalClasses: db.prepare("SELECT COUNT(*) as count FROM classes").get().count, totalRevenue: db.prepare("SELECT COALESCE(SUM(amount), 0) as total FROM payments").get().total, pendingFees: db.prepare("SELECT COALESCE(SUM(amount - paid_amount), 0) as total FROM student_fees WHERE status != 'paid'").get().total, todayAttendance: db.prepare(` SELECT COUNT(CASE WHEN status = 'present' THEN 1 END) as present, COUNT(*) as total FROM attendance WHERE date = date('now') `).get() }; res.json(stats); }); // ============ USERS CRUD ============ app.get('/api/users', auth, (req, res) => { const { role } = req.query; let query = 'SELECT id, email, role, first_name, last_name, phone, address, image_url, created_at FROM users'; const params = []; if (role) { query += ' WHERE role = ?'; params.push(role); } query += ' ORDER BY last_name, first_name'; const users = db.prepare(query).all(...params); res.json(users); }); app.get('/api/users/:id', auth, (req, res) => { const user = db.prepare('SELECT id, email, role, first_name, last_name, phone, address, image_url, created_at FROM users WHERE id = ?').get(req.params.id); if (!user) return res.status(404).json({ error: 'User not found' }); res.json(user); }); app.post('/api/users', auth, (req, res) => { if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin only' }); const { email, password, role, first_name, last_name, phone, address } = req.body; try { const hashedPassword = bcrypt.hashSync(password, 10); const result = db.prepare(` INSERT INTO users (email, password, role, first_name, last_name, phone, address) VALUES (?, ?, ?, ?, ?, ?, ?) `).run(email, hashedPassword, role, first_name, last_name, phone, address); res.json({ id: result.lastInsertRowid, email, role, first_name, last_name }); } catch (err) { res.status(400).json({ error: 'Email already exists' }); } }); app.put('/api/users/:id', auth, (req, res) => { const { first_name, last_name, phone, address } = req.body; db.prepare(` UPDATE users SET first_name = ?, last_name = ?, phone = ?, address = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `).run(first_name, last_name, phone, address, req.params.id); res.json({ success: true }); }); app.delete('/api/users/:id', auth, (req, res) => { if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin only' }); db.prepare('DELETE FROM users WHERE id = ?').run(req.params.id); res.json({ success: true }); }); // ============ CLASSES CRUD ============ app.get('/api/classes', auth, (req, res) => { const classes = db.prepare('SELECT * FROM classes ORDER BY name').all(); res.json(classes); }); app.post('/api/classes', auth, (req, res) => { if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin only' }); const { name, section, capacity } = req.body; const result = db.prepare('INSERT INTO classes (name, section, capacity) VALUES (?, ?, ?)').run(name, section, capacity); res.json({ id: result.lastInsertRowid, name, section, capacity }); }); app.put('/api/classes/:id', auth, (req, res) => { if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin only' }); const { name, section, capacity } = req.body; db.prepare('UPDATE classes SET name = ?, section = ?, capacity = ? WHERE id = ?').run(name, section, capacity, req.params.id); res.json({ success: true }); }); app.delete('/api/classes/:id', auth, (req, res) => { if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin only' }); db.prepare('DELETE FROM classes WHERE id = ?').run(req.params.id); res.json({ success: true }); }); // ============ SUBJECTS CRUD ============ app.get('/api/subjects', auth, (req, res) => { const subjects = db.prepare(` SELECT s.*, c.name as class_name, u.first_name || ' ' || u.last_name as teacher_name FROM subjects s LEFT JOIN classes c ON s.class_id = c.id LEFT JOIN users u ON s.teacher_id = u.id ORDER BY s.name `).all(); res.json(subjects); }); app.post('/api/subjects', auth, (req, res) => { if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin only' }); const { name, class_id, teacher_id } = req.body; const result = db.prepare('INSERT INTO subjects (name, class_id, teacher_id) VALUES (?, ?, ?)').run(name, class_id, teacher_id); res.json({ id: result.lastInsertRowid, name, class_id, teacher_id }); }); app.delete('/api/subjects/:id', auth, (req, res) => { if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin only' }); db.prepare('DELETE FROM subjects WHERE id = ?').run(req.params.id); res.json({ success: true }); }); // ============ ENROLLMENTS ============ app.get('/api/enrollments', auth, (req, res) => { const { class_id } = req.query; let query = ` SELECT e.*, u.first_name, u.last_name, u.email, c.name as class_name, u.id as student_id FROM enrollments e JOIN users u ON e.student_id = u.id JOIN classes c ON e.class_id = c.id `; const params = []; if (class_id) { query += ' WHERE e.class_id = ?'; params.push(class_id); } const enrollments = db.prepare(query).all(...params); res.json(enrollments); }); app.post('/api/enrollments', auth, (req, res) => { if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin only' }); const { student_id, class_id, roll_number, admission_date } = req.body; const result = db.prepare(` INSERT INTO enrollments (student_id, class_id, roll_number, admission_date) VALUES (?, ?, ?, ?) `).run(student_id, class_id, roll_number, admission_date); res.json({ id: result.lastInsertRowid, student_id, class_id, roll_number, admission_date }); }); // ============ ATTENDANCE ============ app.get('/api/attendance', auth, (req, res) => { const { date, class_id } = req.query; let query = ` SELECT a.*, u.first_name, u.last_name, u.id as student_id FROM attendance a JOIN users u ON a.student_id = u.id WHERE 1=1 `; const params = []; if (date) { query += ' AND a.date = ?'; params.push(date); } if (class_id) { query += ' AND a.class_id = ?'; params.push(class_id); } const attendance = db.prepare(query).all(...params); res.json(attendance); }); app.post('/api/attendance', auth, (req, res) => { const { student_id, class_id, date, status } = req.body; // Upsert attendance const existing = db.prepare('SELECT id FROM attendance WHERE student_id = ? AND date = ?').get(student_id, date); if (existing) { db.prepare('UPDATE attendance SET status = ? WHERE id = ?').run(status, existing.id); } else { db.prepare('INSERT INTO attendance (student_id, class_id, date, status) VALUES (?, ?, ?, ?)').run(student_id, class_id, date, status); } res.json({ success: true }); }); app.post('/api/attendance/bulk', auth, (req, res) => { const { records } = req.body; const insert = db.prepare('INSERT OR REPLACE INTO attendance (student_id, class_id, date, status) VALUES (?, ?, ?, ?)'); const insertMany = db.transaction((records) => { records.forEach(r => insert.run(r.student_id, r.class_id, r.date, r.status)); }); insertMany(records); res.json({ success: true, count: records.length }); }); // ============ FEES ============ app.get('/api/fees/groups', auth, (req, res) => { const groups = db.prepare('SELECT * FROM fee_groups ORDER BY name').all(); res.json(groups); }); app.post('/api/fees/groups', auth, (req, res) => { if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin only' }); const { name, description, amount, due_date } = req.body; const result = db.prepare('INSERT INTO fee_groups (name, description, amount, due_date) VALUES (?, ?, ?, ?)').run(name, description, amount, due_date); res.json({ id: result.lastInsertRowid }); }); app.get('/api/fees/students', auth, (req, res) => { const { student_id } = req.query; let query = ` SELECT sf.*, fg.name as fee_name, fg.amount as total_amount, u.first_name, u.last_name FROM student_fees sf JOIN fee_groups fg ON sf.fee_group_id = fg.id JOIN users u ON sf.student_id = u.id `; const params = []; if (student_id) { query += ' WHERE sf.student_id = ?'; params.push(student_id); } const fees = db.prepare(query).all(...params); res.json(fees); }); app.post('/api/fees/students', auth, (req, res) => { if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin only' }); const { student_id, fee_group_id, due_date } = req.body; const feeGroup = db.prepare('SELECT amount FROM fee_groups WHERE id = ?').get(fee_group_id); const result = db.prepare('INSERT INTO student_fees (student_id, fee_group_id, amount, due_date) VALUES (?, ?, ?, ?)').run(student_id, fee_group_id, feeGroup.amount, due_date); res.json({ id: result.lastInsertRowid }); }); app.post('/api/fees/pay', auth, (req, res) => { const { student_fee_id, amount, payment_method, reference_number, notes } = req.body; db.transaction(() => { db.prepare('INSERT INTO payments (student_fee_id, amount, payment_method, reference_number, notes) VALUES (?, ?, ?, ?, ?)').run(student_fee_id, amount, payment_method, reference_number, notes); const fee = db.prepare('SELECT * FROM student_fees WHERE id = ?').get(student_fee_id); const newPaid = fee.paid_amount + amount; const status = newPaid >= fee.amount ? 'paid' : (newPaid > 0 ? 'partial' : 'pending'); db.prepare('UPDATE student_fees SET paid_amount = ?, status = ? WHERE id = ?').run(newPaid, status, student_fee_id); })(); res.json({ success: true }); }); // ============ MESSAGES ============ app.get('/api/messages', auth, (req, res) => { const messages = db.prepare(` SELECT m.*, (SELECT first_name || ' ' || last_name FROM users WHERE id = m.sender_id) as sender_name, (SELECT first_name || ' ' || last_name FROM users WHERE id = m.recipient_id) as recipient_name FROM messages m WHERE m.recipient_id = ? OR m.sender_id = ? ORDER BY m.created_at DESC `).all(req.user.id, req.user.id); res.json(messages); }); app.post('/api/messages', auth, (req, res) => { const { recipient_id, subject, body } = req.body; const result = db.prepare('INSERT INTO messages (sender_id, recipient_id, subject, body) VALUES (?, ?, ?, ?)').run(req.user.id, recipient_id, subject, body); res.json({ id: result.lastInsertRowid }); }); // ============ EVENTS ============ app.get('/api/events', auth, (req, res) => { const events = db.prepare('SELECT * FROM events ORDER BY event_date').all(); res.json(events); }); app.post('/api/events', auth, (req, res) => { const { title, description, event_date, event_type } = req.body; const result = db.prepare('INSERT INTO events (title, description, event_date, event_type, created_by) VALUES (?, ?, ?, ?, ?)').run(title, description, event_date, event_type, req.user.id); res.json({ id: result.lastInsertRowid }); }); // ============ NOTICE BOARD ============ app.get('/api/notices', auth, (req, res) => { const notices = db.prepare(` SELECT n.*, u.first_name || ' ' || u.last_name AS creator_name FROM notices n LEFT JOIN users u ON u.id = n.created_by WHERE n.is_deleted = 0 ORDER BY n.is_pinned DESC, n.created_at DESC `).all(); res.json(notices); }); app.post('/api/notices', auth, (req, res) => { const { title, content, category, priority, audience, is_pinned, expiry_date } = req.body; const uid = require('uuid').v4(); const result = db.prepare(` INSERT INTO notices ( uid, title, content, category, priority, audience, is_pinned, created_by, expiry_date ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( uid, title, content, category, priority, audience, is_pinned ? 1 : 0, req.user.id, expiry_date ); res.json({ success: true, id: result.lastInsertRowid }); }); app.put('/api/notices/:id', auth, (req, res) => { const { id } = req.params; const { title, content, category, priority, audience, is_pinned, expiry_date } = req.body; db.prepare(` UPDATE notices SET title = ?, content = ?, category = ?, priority = ?, audience = ?, is_pinned = ?, expiry_date = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `).run( title, content, category, priority, audience, is_pinned ? 1 : 0, expiry_date, id ); res.json({ success: true }); }); app.delete('/api/notices/:id', auth, (req, res) => { const { id } = req.params; db.prepare(` UPDATE notices SET is_deleted = 1 WHERE id = ? `).run(id); res.json({ success: true }); }); // ============ LIBRARY ============ app.get('/api/library/books', auth, (req, res) => { const books = db.prepare(` SELECT * FROM library_books WHERE is_deleted = 0 ORDER BY title `).all(); res.json(books); }); app.post('/api/library/books', auth, (req, res) => { const { isbn, title, author, category, publisher, year_published, total_copies, shelf_location } = req.body; const uid = require('uuid').v4(); const result = db.prepare(` INSERT INTO library_books ( uid, isbn, title, author, category, publisher, year_published, total_copies, available_copies, shelf_location ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( uid, isbn, title, author, category, publisher, year_published, total_copies, total_copies, shelf_location ); res.json({ success: true, id: result.lastInsertRowid }); }); // FIXED: Use correct columns 'total_copies' and adjust 'available_copies' app.put('/api/library/books/:id', auth, (req, res) => { const { id } = req.params; const { title, author, isbn, category, publisher, year_published, total_copies, shelf_location } = req.body; // Get current book data const current = db.prepare('SELECT total_copies, available_copies FROM library_books WHERE id = ?').get(id); if (!current) { return res.status(404).json({ error: 'Book not found' }); } const oldTotal = current.total_copies; const newTotal = total_copies !== undefined ? total_copies : oldTotal; const delta = newTotal - oldTotal; // Update the book metadata db.prepare(` UPDATE library_books SET title = COALESCE(?, title), author = COALESCE(?, author), isbn = COALESCE(?, isbn), category = COALESCE(?, category), publisher = COALESCE(?, publisher), year_published = COALESCE(?, year_published), total_copies = ?, shelf_location = COALESCE(?, shelf_location), available_copies = available_copies + ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? `).run( title, author, isbn, category, publisher, year_published, newTotal, shelf_location, delta, // increase available copies by the same delta (cannot go negative; we rely on client input validity) id ); // Ensure available_copies does not become negative (safety) db.prepare(` UPDATE library_books SET available_copies = MAX(0, available_copies) WHERE id = ? `).run(id); res.json({ success: true }); }); app.delete('/api/library/books/:id', auth, (req, res) => { const { id } = req.params; db.prepare(` DELETE FROM library_books WHERE id = ? `).run(id); res.json({ success: true }); }); // ============ LIBRARY ISSUES ============ // Get issued books app.get('/api/library/issues', auth, (req, res) => { const issues = db.prepare(` SELECT li.*, lb.title as book_title, lb.author as book_author FROM library_issues li LEFT JOIN library_books lb ON li.book_id = lb.id WHERE li.is_deleted = 0 ORDER BY li.issue_date DESC `).all(); res.json(issues); }); // Issue a book app.post('/api/library/issues', auth, (req, res) => { const { book_id, user_id, due_date } = req.body; const book = db.prepare(` SELECT * FROM library_books WHERE id = ? `).get(book_id); if (!book) { return res.status(404).json({ error: 'Book not found' }); } if (book.available_copies <= 0) { return res.status(400).json({ error: 'No copies available' }); } const uid = require('uuid').v4(); const result = db.prepare(` INSERT INTO library_issues ( uid, book_id, user_id, issue_date, due_date, status, issued_by ) VALUES (?, ?, ?, date('now'), ?, 'issued', ?) `).run( uid, book_id, user_id, due_date, req.user.id ); db.prepare(` UPDATE library_books SET available_copies = available_copies - 1 WHERE id = ? `).run(book_id); res.json({ success: true, id: result.lastInsertRowid }); }); // Return a book app.put('/api/library/issues/:id/return', auth, (req, res) => { const { id } = req.params; const issue = db.prepare(` SELECT * FROM library_issues WHERE id = ? `).get(id); if (!issue) { return res.status(404).json({ error: 'Issue record not found' }); } db.prepare(` UPDATE library_issues SET return_date = date('now'), status = 'returned' WHERE id = ? `).run(id); db.prepare(` UPDATE library_books SET available_copies = available_copies + 1 WHERE id = ? `).run(issue.book_id); res.json({ success: true }); }); // ============ EXPENSES ============ app.get('/api/expenses', auth, (req, res) => { const expenses = db.prepare(` SELECT e.*, u.first_name || ' ' || u.last_name as created_by_name FROM expenses e LEFT JOIN users u ON e.created_by = u.id ORDER BY e.expense_date DESC `).all(); res.json(expenses); }); app.post('/api/expenses', auth, (req, res) => { if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin only' }); const { category, description, amount, expense_date } = req.body; const result = db.prepare('INSERT INTO expenses (category, description, amount, expense_date, created_by) VALUES (?, ?, ?, ?, ?)').run(category, description, amount, expense_date, req.user.id); res.json({ id: result.lastInsertRowid }); }); // Serve static frontend files in production app.use(express.static(path.join(__dirname, '../../client/dist'))); app.get('*', (req, res) => { res.sendFile(path.join(__dirname, '../../client/dist/index.html')); }); // Start server app.listen(PORT, () => { console.log(`Africa Alert API running on http://localhost:${PORT}`); }); module.exports = app;