Spaces:
Running
Running
| const express = require('express'); | |
| const http = require('http'); | |
| const { Server } = require('socket.io'); | |
| const cors = require('cors'); | |
| const cron = require('node-cron'); | |
| require('dotenv').config(); | |
| const { syncSheetsToDB } = require('./services/googleSheets'); | |
| const { requireAuth, getTenantDb } = require('./services/auth'); | |
| const app = express(); | |
| const server = http.createServer(app); | |
| const io = new Server(server, { | |
| cors: { | |
| origin: '*', | |
| methods: ['GET', 'POST'] | |
| } | |
| }); | |
| app.use(cors()); | |
| app.use(express.json()); | |
| // --- Cron Job --- | |
| // Schedule Google Sheets sync every 30 seconds | |
| cron.schedule('*/30 * * * * *', () => { | |
| console.log('Running scheduled sync with Google Sheets...'); | |
| syncSheetsToDB(io); | |
| }); | |
| // --- Real-time Socket.IO with Room Isolation --- | |
| io.on('connection', (socket) => { | |
| console.log('Frontend client connected via WebSocket:', socket.id); | |
| // Isolate counselor sessions by organizational room | |
| socket.on('join_organization', (orgId) => { | |
| socket.join(`org-${orgId}`); | |
| console.log(`Socket ${socket.id} joined secure channel: org-${orgId}`); | |
| }); | |
| socket.on('disconnect', () => { | |
| console.log('Frontend client disconnected:', socket.id); | |
| }); | |
| }); | |
| // --- Modular Routing Blocks --- | |
| const leadRouter = require('./services/leadService'); | |
| const admissionRouter = require('./services/admissionService'); | |
| const aiRouter = require('./services/aiService'); | |
| const { router: notificationRouter, initNotificationService } = require('./services/notificationService'); | |
| // Inject live Socket.IO hooks into the notification event controller | |
| const notificationHelper = initNotificationService(io); | |
| // Mount Scoped API Sub-routers | |
| app.use('/api/leads', leadRouter); | |
| app.use('/api/admissions', admissionRouter); | |
| app.use('/api/ai-insights', aiRouter); | |
| app.use('/api/notifications', notificationRouter); | |
| // GET /api/stats - Dashboard KPI metrics scoped strictly by active organization | |
| app.get('/api/stats', requireAuth, async (req, res) => { | |
| try { | |
| const db = getTenantDb(req); | |
| // Parallel counts enforced by database Row-Level Security automatically | |
| const [totalLeadsRes, activeLeadsRes, admissionsRes, revenueRes] = await Promise.all([ | |
| db.from('leads').select('*', { count: 'exact', head: true }), | |
| db.from('leads').select('*', { count: 'exact', head: true }).not('status', 'in', '("Not Interested","Converted")'), | |
| db.from('admissions').select('*', { count: 'exact', head: true }), | |
| db.from('admissions').select('fees') | |
| ]); | |
| if (totalLeadsRes.error) throw totalLeadsRes.error; | |
| if (activeLeadsRes.error) throw activeLeadsRes.error; | |
| if (admissionsRes.error) throw admissionsRes.error; | |
| if (revenueRes.error) throw revenueRes.error; | |
| const revenue = (revenueRes.data || []).reduce((sum, adm) => sum + parseFloat(adm.fees || 0), 0); | |
| res.json({ | |
| totalLeads: totalLeadsRes.count || 0, | |
| activeLeads: activeLeadsRes.count || 0, | |
| admissions: admissionsRes.count || 0, | |
| revenue | |
| }); | |
| } catch (error) { | |
| console.error('Error fetching KPI metrics:', error); | |
| res.status(500).json({ error: 'Internal Server Error' }); | |
| } | |
| }); | |
| // POST /api/sync-sheet - Trigger external sheets sync | |
| app.post('/api/sync-sheet', requireAuth, async (req, res) => { | |
| try { | |
| await syncSheetsToDB(io); | |
| res.json({ message: 'Sheets sync completed' }); | |
| } catch (error) { | |
| res.status(500).json({ error: 'Sheets sync failed' }); | |
| } | |
| }); | |
| const PORT = process.env.PORT || 5000; | |
| server.listen(PORT, () => { | |
| console.log(`Production API Gateway running on port ${PORT}`); | |
| // Initialize baseline sync | |
| setTimeout(() => syncSheetsToDB(io), 2000); | |
| }); | |