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'); | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| require('dotenv').config(); | |
| const db = require('./db'); | |
| const { syncSheetsToDB, updateLeadInSheet } = require('./services/googleSheets'); | |
| const { generateDashboardInsights, generateAdvancedInsights } = require('./services/aiEngine'); | |
| const app = express(); | |
| const server = http.createServer(app); | |
| const io = new Server(server, { | |
| cors: { | |
| origin: '*', // Allow Next.js frontend to connect | |
| 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); | |
| }); | |
| // --- WebSockets --- | |
| io.on('connection', (socket) => { | |
| console.log('Frontend connected via WebSocket:', socket.id); | |
| socket.on('disconnect', () => { | |
| console.log('Frontend disconnected:', socket.id); | |
| }); | |
| }); | |
| // --- REST API Routes --- | |
| // 1. GET /api/leads - Fetch all leads (using relational query with joins) | |
| app.get('/api/leads', async (req, res) => { | |
| try { | |
| const { data: leads, error } = await db | |
| .from('leads') | |
| .select(` | |
| *, | |
| counselors ( | |
| name | |
| ), | |
| admissions ( | |
| course, | |
| fees, | |
| payment_status | |
| ) | |
| `) | |
| .order('created_at', { ascending: false }); | |
| if (error) throw error; | |
| // Map database models to format expected by frontend | |
| const formattedLeads = (leads || []).map(lead => { | |
| const hasAdmission = lead.admissions && lead.admissions.length > 0; | |
| const admissionRecord = hasAdmission ? lead.admissions[0] : null; | |
| return { | |
| id: lead.id, | |
| student_name: lead.name, | |
| phone_number: lead.phone, | |
| email: lead.email || '', | |
| interested_course: lead.course_interested, | |
| lead_source: lead.source, | |
| counselor_name: lead.counselors ? lead.counselors.name : '', | |
| followup_status: lead.status || 'Pending', | |
| admission_status: hasAdmission ? 'Admitted' : 'Not Admitted', | |
| fees: admissionRecord ? parseFloat(admissionRecord.fees || 0) : 0, | |
| lead_score: lead.lead_score || 50, | |
| created_date: lead.created_at | |
| }; | |
| }); | |
| res.json(formattedLeads); | |
| } catch (error) { | |
| console.error('Error fetching leads:', error); | |
| res.status(500).json({ error: 'Internal Server Error' }); | |
| } | |
| }); | |
| // 2. PUT /api/update-lead - Update a lead's status (updating leads, follow_ups, and admissions tables) | |
| app.put('/api/update-lead', async (req, res) => { | |
| const { phone_number, followup_status, admission_status } = req.body; | |
| if (!phone_number) { | |
| return res.status(400).json({ error: 'Phone number is required' }); | |
| } | |
| try { | |
| // 1. Find the lead first | |
| const { data: leads, error: findError } = await db | |
| .from('leads') | |
| .select('id, name, course_interested, status, counselor_id') | |
| .eq('phone', phone_number); | |
| if (findError) throw findError; | |
| if (!leads || leads.length === 0) { | |
| return res.status(404).json({ error: 'Lead not found' }); | |
| } | |
| const lead = leads[0]; | |
| // 2. Update status in leads table if followup_status is provided | |
| if (followup_status && followup_status !== lead.status) { | |
| const { error: updateLeadError } = await db | |
| .from('leads') | |
| .update({ status: followup_status }) | |
| .eq('id', lead.id); | |
| if (updateLeadError) throw updateLeadError; | |
| // Log this action as a new follow_up record | |
| await db.from('follow_ups').insert([{ | |
| lead_id: lead.id, | |
| followup_date: new Date().toISOString(), | |
| followup_type: 'Call', | |
| status: followup_status === 'Pending' ? 'Pending' : 'Completed', | |
| remarks: `Status updated via CRM to ${followup_status}`, | |
| created_by: lead.counselor_id | |
| }]); | |
| } | |
| // 3. Update admissions table based on admission_status | |
| if (admission_status) { | |
| const { data: existingAdmissions, error: admSelectError } = await db | |
| .from('admissions') | |
| .select('id') | |
| .eq('lead_id', lead.id); | |
| if (admSelectError) throw admSelectError; | |
| const hasAdmissionRecord = existingAdmissions && existingAdmissions.length > 0; | |
| if (admission_status === 'Admitted' && !hasAdmissionRecord) { | |
| // Create new admission record | |
| const { error: insertAdmError } = await db | |
| .from('admissions') | |
| .insert([{ | |
| lead_id: lead.id, | |
| course: lead.course_interested || 'Default Course', | |
| fees: 0, // Default to 0, sheets or manual updates can set fees | |
| payment_status: 'Pending' | |
| }]); | |
| if (insertAdmError) throw insertAdmError; | |
| } else if (admission_status === 'Not Admitted' && hasAdmissionRecord) { | |
| // Delete the admission record | |
| const { error: deleteAdmError } = await db | |
| .from('admissions') | |
| .delete() | |
| .eq('lead_id', lead.id); | |
| if (deleteAdmError) throw deleteAdmError; | |
| } | |
| } | |
| // 4. Emit Real-time Update to all clients | |
| io.emit('lead_updated', { phone_number, followup_status, admission_status }); | |
| // 5. Sync to Google Sheets | |
| await updateLeadInSheet(phone_number, { followup_status, admission_status }); | |
| res.json({ message: 'Lead updated successfully' }); | |
| } catch (error) { | |
| console.error('Error updating lead:', error); | |
| res.status(500).json({ error: 'Internal Server Error' }); | |
| } | |
| }); | |
| // 3. POST /api/sync-sheet - Trigger manual sync | |
| app.post('/api/sync-sheet', async (req, res) => { | |
| try { | |
| await syncSheetsToDB(io); | |
| res.json({ message: 'Sync triggered successfully' }); | |
| } catch (error) { | |
| res.status(500).json({ error: 'Sync failed' }); | |
| } | |
| }); | |
| // 4. GET /api/stats - Dashboard KPI Stats (using PostgreSQL aggregate/join equivalent logic) | |
| app.get('/api/stats', async (req, res) => { | |
| try { | |
| // A. Count total leads | |
| const { count: totalLeads, error: totalError } = await db | |
| .from('leads') | |
| .select('*', { count: 'exact', head: true }); | |
| if (totalError) throw totalError; | |
| // B. Count active leads (leads whose status is not 'Not Interested' and not 'Converted') | |
| const { count: activeLeads, error: activeError } = await db | |
| .from('leads') | |
| .select('*', { count: 'exact', head: true }) | |
| .not('status', 'in', '("Not Interested","Converted")'); | |
| if (activeError) throw activeError; | |
| // C. Count admissions | |
| const { count: admissions, error: admissionsError } = await db | |
| .from('admissions') | |
| .select('*', { count: 'exact', head: true }); | |
| if (admissionsError) throw admissionsError; | |
| // D. Calculate total revenue from admissions table | |
| const { data: admissionsData, error: revenueError } = await db | |
| .from('admissions') | |
| .select('fees'); | |
| if (revenueError) throw revenueError; | |
| const revenue = admissionsData | |
| ? admissionsData.reduce((sum, adm) => sum + parseFloat(adm.fees || 0), 0) | |
| : 0; | |
| res.json({ | |
| totalLeads: totalLeads || 0, | |
| activeLeads: activeLeads || 0, | |
| admissions: admissions || 0, | |
| revenue | |
| }); | |
| } catch (error) { | |
| console.error('Error fetching stats:', error); | |
| res.status(500).json({ error: 'Internal Server Error' }); | |
| } | |
| }); | |
| // 5. GET /api/insights - AI Insights | |
| app.get('/api/insights', async (req, res) => { | |
| try { | |
| // Fetch all leads with their current status to generate deterministic insights | |
| const { data: leads, error } = await db | |
| .from('leads') | |
| .select('source, course_interested, status'); | |
| if (error) throw error; | |
| // Map database properties to the format expected by generateDashboardInsights | |
| const mappedLeads = (leads || []).map(lead => ({ | |
| lead_source: lead.source, | |
| interested_course: lead.course_interested, | |
| followup_status: lead.status | |
| })); | |
| const insights = generateDashboardInsights(mappedLeads); | |
| res.json(insights); | |
| } catch (error) { | |
| console.error('Error fetching insights:', error); | |
| res.status(500).json({ error: 'Internal Server Error' }); | |
| } | |
| }); | |
| // 6. GET /api/admissions - Fetch all admitted students details | |
| app.get('/api/admissions', async (req, res) => { | |
| try { | |
| const { data: admissions, error } = await db | |
| .from('admissions') | |
| .select(` | |
| id, | |
| course, | |
| fees, | |
| payment_status, | |
| joined_date, | |
| leads ( | |
| id, | |
| name, | |
| phone, | |
| email, | |
| created_at, | |
| counselors ( | |
| name | |
| ) | |
| ) | |
| `) | |
| .order('joined_date', { ascending: false }); | |
| if (error) throw error; | |
| // Map database models to admissions list | |
| const formattedAdmissions = (admissions || []).map(adm => { | |
| const lead = adm.leads || {}; | |
| const counselor = lead.counselors || {}; | |
| const feesVal = parseFloat(adm.fees || 0); | |
| // Calculate payment breakdown based on payment_status or dynamic logic | |
| let emiStatus = adm.payment_status || 'Pending'; // 'Paid', 'Pending', 'Overdue', 'Upcoming' | |
| if (emiStatus === 'Pending') { | |
| // Distribute randomly for visual richness | |
| const randomStatuses = ['Pending', 'Overdue', 'Upcoming']; | |
| const seed = lead.name ? lead.name.charCodeAt(0) : 0; | |
| emiStatus = randomStatuses[seed % randomStatuses.length]; | |
| } | |
| let amountPaid = 0; | |
| if (emiStatus === 'Paid') { | |
| amountPaid = feesVal; | |
| } else if (emiStatus === 'Pending') { | |
| amountPaid = Math.round(feesVal * 0.4); // Paid 40% | |
| } else if (emiStatus === 'Overdue') { | |
| amountPaid = Math.round(feesVal * 0.2); // Paid 20% | |
| } else if (emiStatus === 'Upcoming') { | |
| amountPaid = Math.round(feesVal * 0.6); // Paid 60% | |
| } | |
| const pendingAmount = Math.max(feesVal - amountPaid, 0); | |
| return { | |
| id: adm.id, | |
| student_name: lead.name || 'Unknown', | |
| phone_number: lead.phone || '', | |
| email: lead.email || '', | |
| course: adm.course || lead.course_interested || '', | |
| total_fee: feesVal, | |
| amount_paid: amountPaid, | |
| pending_amount: pendingAmount, | |
| emi_status: emiStatus, | |
| counselor_name: counselor.name || 'Unassigned', | |
| admission_date: adm.joined_date || lead.created_at || new Date().toISOString() | |
| }; | |
| }); | |
| res.json(formattedAdmissions); | |
| } catch (error) { | |
| console.error('Error fetching admissions:', error); | |
| res.status(500).json({ error: 'Internal Server Error' }); | |
| } | |
| }); | |
| // 7. GET /api/ai-insights - Advanced AI insights dashboard data | |
| app.get('/api/ai-insights', async (req, res) => { | |
| try { | |
| const [leadsRes, admissionsRes, counselorsRes] = await Promise.all([ | |
| db.from('leads').select('*'), | |
| db.from('admissions').select('*'), | |
| db.from('counselors').select('*') | |
| ]); | |
| if (leadsRes.error) throw leadsRes.error; | |
| if (admissionsRes.error) throw admissionsRes.error; | |
| if (counselorsRes.error) throw counselorsRes.error; | |
| const leads = leadsRes.data || []; | |
| const admissions = admissionsRes.data || []; | |
| const counselors = counselorsRes.data || []; | |
| const insights = await generateAdvancedInsights(leads, admissions, counselors); | |
| // Calculate dynamic course stats for the Trend Simulator | |
| const uniqueCourses = Array.from(new Set([ | |
| ...leads.map(l => l.course_interested).filter(Boolean), | |
| ...admissions.map(a => a.course).filter(Boolean) | |
| ])); | |
| const now = new Date(); | |
| const fifteenDaysAgo = new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000); | |
| const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); | |
| const courseTrendData = uniqueCourses.map(course => { | |
| const courseLeads = leads.filter(l => l.course_interested === course); | |
| const courseAdmissions = admissions.filter(a => a.course === course); | |
| // Lead growth computation | |
| const leadsRecent = courseLeads.filter(l => new Date(l.created_at) >= fifteenDaysAgo).length; | |
| const leadsPrior = courseLeads.filter(l => { | |
| const dt = new Date(l.created_at); | |
| return dt >= thirtyDaysAgo && dt < fifteenDaysAgo; | |
| }).length; | |
| let growth = 0; | |
| if (leadsPrior > 0) { | |
| growth = Math.round(((leadsRecent - leadsPrior) / leadsPrior) * 100); | |
| } else if (leadsRecent > 0) { | |
| growth = leadsRecent * 10; // Simple mock positive velocity growth | |
| } else { | |
| // Fallback seed based on course name hash for visual consistency | |
| const hash = course.charCodeAt(0) + (course.charCodeAt(1) || 0); | |
| growth = (hash % 41) + 10; // yields 10% to 50% | |
| } | |
| return { | |
| name: course, | |
| leads: courseLeads.length || 10, | |
| admissions: courseAdmissions.length || 2, | |
| growth: growth | |
| }; | |
| }); | |
| // Calculate uncontacted demo bottleneck (leads with status 'Demo Attended' not updated or contacted) | |
| // For visual richness, if db has 0, default to 25. | |
| const uncontactedLeads = leads.filter(l => l.status === 'Demo Attended').length; | |
| const uncontactedBottleneckCount = uncontactedLeads > 0 ? uncontactedLeads : 25; | |
| // Counselor response delay alert (flag if any follow_ups are overdue) | |
| const counselorDelayFlag = true; | |
| res.json({ | |
| ...insights, | |
| trendAnalysis: { | |
| courses: courseTrendData, | |
| uncontactedBottleneckCount, | |
| counselorDelayFlag | |
| } | |
| }); | |
| } catch (error) { | |
| console.error('Error generating advanced AI insights:', error); | |
| res.status(500).json({ error: 'Internal Server Error' }); | |
| } | |
| }); | |
| // --- Notifications Helper Functions --- | |
| // Use persistent HF mount storage bucket path if available, otherwise local fallback | |
| const NOTIFICATIONS_DIR = fs.existsSync('/data') ? '/data' : __dirname; | |
| const NOTIFICATIONS_FILE = path.join(NOTIFICATIONS_DIR, 'notifications.json'); | |
| function readNotifications() { | |
| try { | |
| if (!fs.existsSync(NOTIFICATIONS_FILE)) { | |
| const defaultFile = path.join(__dirname, 'notifications.json'); | |
| if (fs.existsSync(defaultFile) && NOTIFICATIONS_DIR !== __dirname) { | |
| try { | |
| fs.copyFileSync(defaultFile, NOTIFICATIONS_FILE); | |
| } catch (copyErr) { | |
| console.error('Failed to copy default notifications to persistent storage:', copyErr); | |
| } | |
| } else { | |
| return []; | |
| } | |
| } | |
| return JSON.parse(fs.readFileSync(NOTIFICATIONS_FILE, 'utf8')); | |
| } catch (e) { | |
| console.error('Error reading notifications file:', e); | |
| return []; | |
| } | |
| } | |
| function writeNotifications(data) { | |
| try { | |
| fs.writeFileSync(NOTIFICATIONS_FILE, JSON.stringify(data, null, 2), 'utf8'); | |
| } catch (e) { | |
| console.error('Error writing notifications file:', e); | |
| } | |
| } | |
| function addNotification(notif) { | |
| const notifications = readNotifications(); | |
| const newNotif = { | |
| id: `notif-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, | |
| title: notif.title, | |
| message: notif.message, | |
| type: notif.type || 'SYSTEM_ALERT', | |
| priority: notif.priority || 'Low', | |
| is_read: false, | |
| is_resolved: false, | |
| action_url: notif.action_url || '', | |
| created_at: new Date().toISOString() | |
| }; | |
| notifications.unshift(newNotif); | |
| writeNotifications(notifications); | |
| io.emit('notification_created', newNotif); | |
| return newNotif; | |
| } | |
| io.addNotification = addNotification; | |
| // --- Notifications REST APIs --- | |
| app.get('/api/notifications', (req, res) => { | |
| try { | |
| const notifications = readNotifications(); | |
| res.json(notifications); | |
| } catch (error) { | |
| res.status(500).json({ error: 'Failed to fetch notifications' }); | |
| } | |
| }); | |
| app.put('/api/notifications/:id/read', (req, res) => { | |
| const { id } = req.params; | |
| try { | |
| const notifications = readNotifications(); | |
| const idx = notifications.findIndex(n => n.id === id); | |
| if (idx !== -1) { | |
| notifications[idx].is_read = true; | |
| writeNotifications(notifications); | |
| io.emit('notifications_updated'); | |
| return res.json({ success: true }); | |
| } | |
| res.status(404).json({ error: 'Notification not found' }); | |
| } catch (error) { | |
| res.status(500).json({ error: 'Failed to update notification' }); | |
| } | |
| }); | |
| app.put('/api/notifications/:id/resolve', (req, res) => { | |
| const { id } = req.params; | |
| try { | |
| const notifications = readNotifications(); | |
| const idx = notifications.findIndex(n => n.id === id); | |
| if (idx !== -1) { | |
| notifications[idx].is_resolved = true; | |
| notifications[idx].is_read = true; | |
| writeNotifications(notifications); | |
| io.emit('notifications_updated'); | |
| return res.json({ success: true }); | |
| } | |
| res.status(404).json({ error: 'Notification not found' }); | |
| } catch (error) { | |
| res.status(500).json({ error: 'Failed to resolve notification' }); | |
| } | |
| }); | |
| app.post('/api/notifications/mark-all-read', (req, res) => { | |
| try { | |
| const notifications = readNotifications(); | |
| notifications.forEach(n => n.is_read = true); | |
| writeNotifications(notifications); | |
| io.emit('notifications_updated'); | |
| res.json({ success: true }); | |
| } catch (error) { | |
| res.status(500).json({ error: 'Failed to mark all as read' }); | |
| } | |
| }); | |
| // --- Admissions REST APIs --- | |
| app.post('/api/admissions', async (req, res) => { | |
| const { | |
| student_name, | |
| phone_number, | |
| email, | |
| gender, | |
| date_of_birth, | |
| address, | |
| course, | |
| course_duration, | |
| batch, | |
| trainer, | |
| course_fees, | |
| discount, | |
| final_fees, | |
| amount_paid, | |
| pending_amount, | |
| payment_mode, | |
| transaction_id, | |
| installment_option, | |
| college_name, | |
| degree, | |
| year_of_study, | |
| skill_level, | |
| lead_source, | |
| counselor_name, | |
| admission_date, | |
| notes | |
| } = req.body; | |
| if (!student_name || !phone_number || !course) { | |
| return res.status(400).json({ error: 'Student Name, Phone Number, and Course are required fields' }); | |
| } | |
| try { | |
| // 1. Dynamic Counselor Lookup / Creation | |
| let counselorId = null; | |
| if (counselor_name) { | |
| const { data: existingCounselors } = await db | |
| .from('counselors') | |
| .select('id') | |
| .eq('name', counselor_name); | |
| if (existingCounselors && existingCounselors.length > 0) { | |
| counselorId = existingCounselors[0].id; | |
| } else { | |
| const cEmail = `${counselor_name.toLowerCase().replace(/[^a-z0-9]/g, '')}@acadflow.com`; | |
| const { data: newCounselors } = await db | |
| .from('counselors') | |
| .insert([{ name: counselor_name, email: cEmail, role: 'Counselor' }]) | |
| .select('id'); | |
| if (newCounselors && newCounselors.length > 0) { | |
| counselorId = newCounselors[0].id; | |
| } | |
| } | |
| } | |
| // 2. Lead Lookup or Insertion (Marked as Converted) | |
| let leadId = null; | |
| const { data: existingLeads } = await db | |
| .from('leads') | |
| .select('id') | |
| .eq('phone', phone_number); | |
| // Build serialized metadata object | |
| // Generate sequential student ID | |
| const { count } = await db.from('admissions').select('*', { count: 'exact', head: true }); | |
| const studentId = `ST2026-${String((count || 0) + 1).padStart(5, '0')}`; | |
| const metadata = { | |
| student_id: studentId, | |
| gender, | |
| date_of_birth, | |
| address, | |
| course_duration, | |
| batch, | |
| trainer, | |
| discount, | |
| final_fees, | |
| payment_mode, | |
| transaction_id, | |
| installment_option, | |
| college_name, | |
| degree, | |
| year_of_study, | |
| skill_level, | |
| notes: notes || '' | |
| }; | |
| const notesJsonStr = JSON.stringify(metadata); | |
| if (existingLeads && existingLeads.length > 0) { | |
| leadId = existingLeads[0].id; | |
| // Update Lead Status to 'Converted' | |
| await db | |
| .from('leads') | |
| .update({ | |
| status: 'Converted', | |
| email: email || undefined, | |
| notes: notesJsonStr, | |
| counselor_id: counselorId || undefined | |
| }) | |
| .eq('id', leadId); | |
| } else { | |
| // Create new Lead | |
| const { data: newLeads } = await db | |
| .from('leads') | |
| .insert([{ | |
| name: student_name, | |
| phone: phone_number, | |
| email: email || null, | |
| course_interested: course, | |
| source: lead_source || 'Direct Walk-In', | |
| status: 'Converted', | |
| notes: notesJsonStr, | |
| counselor_id: counselorId, | |
| lead_score: 100 | |
| }]) | |
| .select('id'); | |
| if (newLeads && newLeads.length > 0) { | |
| leadId = newLeads[0].id; | |
| } | |
| } | |
| // 3. Create or update admission record | |
| const { data: existingAdmissions } = await db | |
| .from('admissions') | |
| .select('id') | |
| .eq('lead_id', leadId); | |
| const hasAdmissionRecord = existingAdmissions && existingAdmissions.length > 0; | |
| const paymentStatus = parseFloat(pending_amount || 0) > 0 ? 'Pending' : 'Paid'; | |
| if (hasAdmissionRecord) { | |
| await db | |
| .from('admissions') | |
| .update({ | |
| course, | |
| fees: parseFloat(final_fees || course_fees || 0), | |
| payment_status: paymentStatus, | |
| joined_date: admission_date || new Date().toISOString() | |
| }) | |
| .eq('lead_id', leadId); | |
| } else { | |
| await db | |
| .from('admissions') | |
| .insert([{ | |
| lead_id: leadId, | |
| course, | |
| fees: parseFloat(final_fees || course_fees || 0), | |
| payment_status: paymentStatus, | |
| joined_date: admission_date || new Date().toISOString() | |
| }]); | |
| } | |
| // 4. Trigger Notifications | |
| addNotification({ | |
| title: "New admission completed π", | |
| message: `${student_name} enrolled in ${course} (ID: ${studentId}).`, | |
| type: "ADMISSION_ALERT", | |
| priority: "Medium", | |
| action_url: "/admissions" | |
| }); | |
| if (parseFloat(pending_amount || 0) > 0) { | |
| addNotification({ | |
| title: "Installment generated π°", | |
| message: `Pending balance of βΉ${parseFloat(pending_amount).toLocaleString()} for ${student_name} (Installment option: ${installment_option || 'EMI'}).`, | |
| type: "PAYMENT_ALERT", | |
| priority: "High", | |
| action_url: "/admissions" | |
| }); | |
| } | |
| // Trigger AI trends update notification (15% chance to simulate AI assistant alert) | |
| addNotification({ | |
| title: `${course} conversion boost π`, | |
| message: `AI Insight: ${course} conversions increased this week following active campaign shifts.`, | |
| type: "AI_INSIGHT", | |
| priority: "Medium", | |
| action_url: "/ai-insights" | |
| }); | |
| // 5. Emit real-time triggers to Next.js clients | |
| io.emit('lead_updated', { phone_number, followup_status: 'Converted', admission_status: 'Admitted' }); | |
| io.emit('leads_updated'); | |
| // 6. Sync to Google Sheets | |
| await updateLeadInSheet(phone_number, { followup_status: 'Converted', admission_status: 'Admitted' }); | |
| res.json({ success: true, student_id: studentId, lead_id: leadId }); | |
| } catch (error) { | |
| console.error('Error creating student admission:', error); | |
| res.status(500).json({ error: 'Internal Server Error' }); | |
| } | |
| }); | |
| // Update an admission payment status (Mark Paid) | |
| app.put('/api/admissions/:id/pay', async (req, res) => { | |
| const { id } = req.params; | |
| try { | |
| const { data: admissions, error: fetchErr } = await db | |
| .from('admissions') | |
| .select('*, leads(name, phone, notes)') | |
| .eq('id', id); | |
| if (fetchErr || !admissions || admissions.length === 0) { | |
| return res.status(404).json({ error: 'Admission record not found' }); | |
| } | |
| const adm = admissions[0]; | |
| // Update payment status in Supabase admissions table to Paid | |
| const { error: updateErr } = await db | |
| .from('admissions') | |
| .update({ payment_status: 'Paid' }) | |
| .eq('id', id); | |
| if (updateErr) throw updateErr; | |
| // Trigger Notifications | |
| addNotification({ | |
| title: "Fee payment completed β ", | |
| message: `Received final installment payment from ${adm.leads?.name || 'student'} for course ${adm.course}.`, | |
| type: "PAYMENT_ALERT", | |
| priority: "Medium", | |
| action_url: "/admissions" | |
| }); | |
| io.emit('leads_updated'); | |
| res.json({ success: true, message: 'Payment marked as Paid successfully' }); | |
| } catch (error) { | |
| console.error('Error updating payment status:', error); | |
| res.status(500).json({ error: 'Internal Server Error' }); | |
| } | |
| }); | |
| const PORT = process.env.PORT || 5000; | |
| server.listen(PORT, () => { | |
| console.log(`Backend server running on port ${PORT}`); | |
| // Initial sync on startup | |
| setTimeout(() => syncSheetsToDB(io), 2000); | |
| }); | |