const express = require('express'); const router = express.Router(); const { getTenantDb, requireAuth } = require('./auth'); const eventBus = require('./eventBus'); const { appendLeadToSheet, updateLeadInSheet } = require('./googleSheets'); // 1. GET /api/leads - Fetch active leads scoped by tenant organization router.get('/', requireAuth, async (req, res) => { try { const db = getTenantDb(req); const { data: leads, error } = await db .from('active_leads') .select(` *, student_id, counselors ( name ), admissions ( course, fees, payment_status ) `) .order('created_at', { ascending: false }); if (error) throw error; 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 : '', counselor_id: lead.counselor_id, followup_status: lead.status || 'Pending', followup_time: lead.followup_time || 'One Day', admission_status: hasAdmission ? 'Admitted' : 'Not Admitted', fees: admissionRecord ? parseFloat(admissionRecord.fees || 0) : 0, lead_score: lead.lead_score || 50, created_date: lead.created_at, gender: lead.gender || '', city: lead.city || '', fees_discussed: lead.fees_discussed ? parseFloat(lead.fees_discussed) : 0, remarks: lead.remarks || '', student_id: lead.student_id || '', referred_by_student_name: lead.referred_by_student_name || '' }; }); res.json(formattedLeads); } catch (error) { console.error('Error fetching leads:', error); res.status(500).json({ error: 'Internal Server Error' }); } }); router.get('/test', async (req, res) => { try { const db = require('../db'); const { data: leads, error } = await db .from('active_leads') .select(` *, student_id `) .order('created_at', { ascending: false }); if (error) throw error; res.json(leads); } catch (error) { console.error('Error fetching leads test:', error); res.status(500).json({ error: 'Internal Server Error' }); } }); // 2. POST /api/leads - Create a new lead directly in Supabase router.post('/', requireAuth, async (req, res) => { const { student_name, phone_number, email, interested_course, lead_source, counselor_id, followup_status, followup_time, gender, city, fees_discussed, remarks, referred_by_student_name } = req.body; if (!student_name || !phone_number || !email) { return res.status(400).json({ error: 'Student name, phone number, and email are required' }); } try { const db = getTenantDb(req); const orgId = req.user?.organization_id || '00000000-0000-0000-0000-000000000001'; const branchId = req.user?.branch_id || '00000000-0000-0000-0000-000000000002'; console.log('--- LEAD CREATION DEBUG ---'); console.log('Incoming Payload:', req.body); console.log('Phone Number Received:', phone_number); console.log('Organization ID Received:', orgId); // Auto-generate student ID always const { generateNextStudentId } = require('./idGenerator'); const nextId = await generateNextStudentId(); const newLead = { name: student_name, student_id: nextId, phone: phone_number, email: email, course_interested: interested_course || '', source: lead_source || 'Direct Walk-in', counselor_id: counselor_id || null, status: followup_status || 'Pending', followup_time: followup_time || 'One Day', gender: gender || '', city: city || '', fees_discussed: fees_discussed ? parseFloat(fees_discussed) : null, remarks: remarks || '', referred_by_student_name: lead_source === 'Student Referral' ? referred_by_student_name : null, organization_id: orgId, branch_id: branchId, lead_score: 50 }; const { data: createdData, error } = await db .from('leads') .insert([newLead]) .select(); if (error) throw error; const createdLead = createdData[0]; // Log in audit logs await db.from('audit_logs').insert([{ organization_id: orgId, branch_id: branchId, user_id: req.user?.id || null, action: 'CREATE_LEAD', entity_type: 'leads', entity_id: createdLead.id, new_values: newLead }]); // Publish event to decoupled Event Bus eventBus.publish('lead.status_changed', { lead_id: createdLead.id, student_name: createdLead.name, phone: createdLead.phone, old_status: null, new_status: createdLead.status, followup_time: createdLead.followup_time, organization_id: orgId, branch_id: branchId }); // Fetch counselor name for GSheet let counselorName = ''; if (counselor_id) { const { data: cData } = await db.from('counselors').select('name').eq('id', counselor_id); if (cData && cData.length > 0) counselorName = cData[0].name; } // Automatically add to Google Sheet appendLeadToSheet({ name: newLead.name, phone: newLead.phone, email: newLead.email, course_interested: newLead.course_interested, source: newLead.source, status: newLead.status, counselor_name: counselorName, followup_time: newLead.followup_time, remarks: newLead.remarks, student_id: newLead.student_id }).catch(err => console.error('Failed to append to GSheet:', err)); res.status(201).json({ message: 'Lead created successfully', lead: createdLead }); } catch (error) { console.error('Error creating lead:', error, JSON.stringify(error)); return res.status(500).json({ success: false, error: error.message || 'Internal Server Error', details: error.details || null }); } }); // Legacy / compatibility route for update-lead (MUST be registered before dynamic param /:id) router.put('/update-lead', requireAuth, async (req, res) => { const { phone_number, followup_status, admission_status, followup_time } = req.body; if (!phone_number) { return res.status(400).json({ error: 'Phone number is required' }); } try { const db = getTenantDb(req); const { data: leads, error: findError } = await db .from('leads') .select('id, name, course_interested, status, counselor_id, organization_id, branch_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]; // Map legacy update call to our PUT /:id updates const updates = {}; if (followup_status) updates.status = followup_status; if (followup_time) updates.followup_time = followup_time; if (Object.keys(updates).length > 0) { const { error: updateLeadError } = await db .from('leads') .update(updates) .eq('id', lead.id); if (updateLeadError) throw updateLeadError; // Log followup details if status changed if (followup_status && followup_status !== lead.status) { await db.from('follow_ups').insert([{ lead_id: lead.id, organization_id: lead.organization_id, branch_id: lead.branch_id, followup_date: new Date().toISOString(), followup_type: 'Call', status: followup_status === 'Pending' ? 'Pending' : 'Completed', remarks: `Status updated via legacy API to ${followup_status}`, created_by: lead.counselor_id || req.user?.id || null }]); // Publish event to decoupled Event Bus eventBus.publish('lead.status_changed', { lead_id: lead.id, student_name: lead.name, phone: phone_number, old_status: lead.status, new_status: followup_status, followup_time: followup_time || 'One Day', organization_id: lead.organization_id, branch_id: lead.branch_id }); } } // Update admissions 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) { // Trigger Lead Converted Event Bus sequence eventBus.publish('lead.converted', { lead_id: lead.id, student_name: lead.name, phone: phone_number, course: lead.course_interested || 'Default Course', counselor_id: lead.counselor_id, organization_id: lead.organization_id, branch_id: lead.branch_id }); } else if (admission_status === 'Not Admitted' && hasAdmissionRecord) { // Remove admission details const { error: deleteAdmError } = await db .from('admissions') .delete() .eq('lead_id', lead.id); if (deleteAdmError) throw deleteAdmError; } } res.json({ message: 'Lead updated successfully' }); } catch (error) { console.error('Error updating lead:', error); return res.status(500).json({ success: false, error: error.message || 'Internal Server Error', details: error.details || null }); } }); // 3. PUT /api/leads/:id - Update an existing lead record router.put('/:id', requireAuth, async (req, res) => { const { id } = req.params; const { student_id, student_name, phone_number, email, interested_course, lead_source, counselor_id, followup_status, followup_time, gender, city, fees_discussed, remarks, referred_by_student_name } = req.body; if (email === "") { return res.status(400).json({ error: 'Email is required' }); } try { const db = getTenantDb(req); const { data: currentLeads, error: getError } = await db .from('leads') .select('*') .eq('id', id); if (getError) throw getError; if (!currentLeads || currentLeads.length === 0) { return res.status(404).json({ error: 'Lead not found' }); } const currentLead = currentLeads[0]; const updates = {}; if (student_id !== undefined) updates.student_id = student_id === "" ? null : student_id; if (student_name !== undefined) updates.name = student_name; if (phone_number !== undefined) updates.phone = phone_number; if (email !== undefined) updates.email = email; if (interested_course !== undefined) updates.course_interested = interested_course; if (lead_source !== undefined) updates.source = lead_source; if (counselor_id !== undefined) updates.counselor_id = (counselor_id === "" || counselor_id === "null" || counselor_id === null) ? null : counselor_id; if (followup_status !== undefined) updates.status = followup_status; if (followup_time !== undefined) updates.followup_time = followup_time; if (gender !== undefined) updates.gender = gender; if (city !== undefined) updates.city = city; if (fees_discussed !== undefined) { if (fees_discussed === "" || fees_discussed === null || fees_discussed === "null") { updates.fees_discussed = null; } else { const parsed = parseFloat(fees_discussed); updates.fees_discussed = isNaN(parsed) ? null : parsed; } } if (remarks !== undefined) updates.remarks = remarks; if (referred_by_student_name !== undefined) updates.referred_by_student_name = referred_by_student_name; const { error: updateError } = await db .from('leads') .update(updates) .eq('id', id); if (updateError) throw updateError; // Log audit log await db.from('audit_logs').insert([{ organization_id: currentLead.organization_id, branch_id: currentLead.branch_id, user_id: req.user?.id || null, action: 'UPDATE_LEAD', entity_type: 'leads', entity_id: id, old_values: currentLead, new_values: { ...currentLead, ...updates } }]); // Check status change or details change if (followup_status && followup_status !== currentLead.status) { // Log follow up history await db.from('follow_ups').insert([{ lead_id: id, organization_id: currentLead.organization_id, branch_id: currentLead.branch_id, followup_date: new Date().toISOString(), followup_type: 'Call', status: followup_status === 'Pending' ? 'Pending' : 'Completed', remarks: remarks || `Status updated via CRM edit to ${followup_status}`, created_by: counselor_id || currentLead.counselor_id || req.user?.id || null }]); // Publish event eventBus.publish('lead.status_changed', { lead_id: id, student_name: student_name || currentLead.name, phone: phone_number || currentLead.phone, old_status: currentLead.status, new_status: followup_status, followup_time: followup_time || currentLead.followup_time, organization_id: currentLead.organization_id, branch_id: currentLead.branch_id }); } res.json({ message: 'Lead updated successfully' }); } catch (error) { console.error('Error updating lead:', error); return res.status(500).json({ success: false, error: error.message || 'Internal Server Error', details: error.details || null }); } }); // 4. DELETE /api/leads/:id - Soft delete a lead router.delete('/:id', requireAuth, async (req, res) => { const { id } = req.params; try { const db = getTenantDb(req); const { data: currentLeads, error: getError } = await db .from('leads') .select('*') .eq('id', id); if (getError) throw getError; if (!currentLeads || currentLeads.length === 0) { return res.status(404).json({ error: 'Lead not found' }); } const currentLead = currentLeads[0]; // Soft delete: update deleted_at const { error: deleteError } = await db .from('leads') .update({ deleted_at: new Date().toISOString() }) .eq('id', id); if (deleteError) throw deleteError; // Log audit log await db.from('audit_logs').insert([{ organization_id: currentLead.organization_id, branch_id: currentLead.branch_id, user_id: req.user?.id || null, action: 'DELETE_LEAD', entity_type: 'leads', entity_id: id, old_values: currentLead, new_values: { deleted_at: new Date().toISOString() } }]); res.json({ message: 'Lead soft-deleted successfully' }); } catch (error) { console.error('Error deleting lead:', error); res.status(500).json({ error: 'Internal Server Error' }); } }); // 5. POST /api/leads/follow-up - Create a new follow-up and update lead status router.post('/follow-up', requireAuth, async (req, res) => { const { lead_id, followup_type, status, remarks, next_followup_time } = req.body; if (!lead_id) { return res.status(400).json({ error: 'Lead ID is required' }); } try { const db = getTenantDb(req); const { data: leads, error: getError } = await db .from('leads') .select('*') .eq('id', lead_id); if (getError) throw getError; if (!leads || leads.length === 0) { return res.status(404).json({ error: 'Lead not found' }); } const lead = leads[0]; const orgId = lead.organization_id; const branchId = lead.branch_id; // Insert follow-up record const { data: followUpData, error: followUpError } = await db .from('follow_ups') .insert([{ lead_id, organization_id: orgId, branch_id: branchId, followup_date: new Date().toISOString(), followup_type: followup_type || 'Call', status: status || 'Completed', remarks: remarks || '', created_by: req.user?.id || lead.counselor_id || null }]) .select(); if (followUpError) throw followUpError; // Update lead notes / followup_time / status const updates = {}; if (status && status !== lead.status) { updates.status = status; } if (next_followup_time) { updates.followup_time = next_followup_time; } if (remarks) { updates.remarks = remarks; } if (Object.keys(updates).length > 0) { const { error: updateError } = await db .from('leads') .update(updates) .eq('id', lead_id); if (updateError) throw updateError; } // Log audit log await db.from('audit_logs').insert([{ organization_id: orgId, branch_id: branchId, user_id: req.user?.id || null, action: 'ADD_FOLLOW_UP', entity_type: 'follow_ups', entity_id: followUpData[0].id, new_values: { lead_id, status, remarks, next_followup_time } }]); // Publish event eventBus.publish('lead.status_changed', { lead_id, student_name: lead.name, phone: lead.phone, old_status: lead.status, new_status: status || lead.status, followup_time: next_followup_time || lead.followup_time, organization_id: orgId, branch_id: branchId }); res.status(201).json({ message: 'Follow-up logged successfully', followUp: followUpData[0] }); } catch (error) { console.error('Error logging follow-up:', error); res.status(500).json({ error: 'Internal Server Error' }); } }); // 6. GET /api/leads/:id/timeline - Get timeline of events/followups for a lead router.get('/:id/timeline', requireAuth, async (req, res) => { try { const db = getTenantDb(req); // Fetch follow ups for this lead const { data: followups, error: followupsError } = await db .from('active_follow_ups') .select('*') .eq('lead_id', req.params.id) .order('followup_date', { ascending: false }); if (followupsError) throw followupsError; // Fetch audit logs for this lead const { data: auditLogs, error: auditError } = await db .from('audit_logs') .select('*') .eq('entity_id', req.params.id) .eq('entity_type', 'leads') .order('created_at', { ascending: false }); if (auditError) throw auditError; // Fetch admission details to show if converted const { data: admissions, error: admissionError } = await db .from('active_admissions') .select('*') .eq('lead_id', req.params.id); if (admissionError) throw admissionError; // Merge and format into timeline events const timeline = []; // Add follow ups (followups || []).forEach(f => { timeline.push({ id: f.id, type: 'followup', date: f.followup_date, title: `Follow-up (${f.followup_type})`, description: f.remarks, status: f.status }); }); // Add audit logs (auditLogs || []).forEach(a => { let title = 'Lead Modified'; if (a.action === 'CREATE_LEAD') title = 'Lead Created'; if (a.action === 'DELETE_LEAD') title = 'Lead Deleted'; if (a.action === 'UPDATE_LEAD') title = 'Lead Updated'; timeline.push({ id: a.id, type: 'audit', date: a.created_at, title: title, description: `Action: ${a.action}` }); }); // Add admission if exists if (admissions && admissions.length > 0) { const adm = admissions[0]; timeline.push({ id: adm.id, type: 'admission', date: adm.joined_date || adm.created_at || new Date().toISOString(), title: 'Lead Converted to Admission 🎓', description: `Enrolled in ${adm.course} | Fees: ₹${parseFloat(adm.fees || 0).toLocaleString()} | Status: ${adm.payment_status}` }); } // Sort descending by date timeline.sort((a, b) => new Date(b.date) - new Date(a.date)); res.json(timeline); } catch (error) { console.error('Error fetching lead timeline:', error); res.status(500).json({ error: 'Internal Server Error' }); } }); module.exports = router;