geocrop-platform./apps/nextgen/server/src/index.js

729 lines
24 KiB
JavaScript

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;