729 lines
24 KiB
JavaScript
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;
|