const { google } = require('googleapis'); const db = require('../db'); const { calculateLeadScore } = require('./aiEngine'); const { generateNextStudentId } = require('./idGenerator'); require('dotenv').config(); // Initialize Google Auth let auth; if (process.env.GOOGLE_CREDS_JSON) { try { const credentials = JSON.parse(process.env.GOOGLE_CREDS_JSON); auth = new google.auth.GoogleAuth({ credentials, scopes: ['https://www.googleapis.com/auth/spreadsheets'], }); } catch (e) { console.error('Failed to parse GOOGLE_CREDS_JSON environment variable:', e); auth = new google.auth.GoogleAuth({ keyFile: 'credentials.json', scopes: ['https://www.googleapis.com/auth/spreadsheets'], }); } } else { auth = new google.auth.GoogleAuth({ keyFile: 'credentials.json', // Path to your service account key file for local dev scopes: ['https://www.googleapis.com/auth/spreadsheets'], }); } const sheets = google.sheets({ version: 'v4', auth }); const SPREADSHEET_ID = process.env.SPREADSHEET_ID; /** * Parses date strings in DD/MM/YYYY hh:mm:ss format or other fallback formats */ const parseSheetDate = (dateStr) => { if (!dateStr) return new Date().toISOString(); // Handle DD/MM/YYYY or MM/DD/YYYY format with potential time const parts = dateStr.trim().split(/[\/\s:]/); if (parts.length >= 3) { const firstVal = parseInt(parts[0], 10); const secondVal = parseInt(parts[1], 10); const year = parseInt(parts[2], 10); let hour = 0; let minute = 0; let second = 0; if (parts.length >= 6) { hour = parseInt(parts[3], 10); minute = parseInt(parts[4], 10); second = parseInt(parts[5], 10); } let month = firstVal - 1; // 0-indexed month let day = secondVal; if (firstVal > 12 && secondVal <= 12) { month = secondVal - 1; day = firstVal; } if (day > 0 && day <= 31 && month >= 0 && month < 12 && year > 1900) { const date = new Date(year, month, day, hour, minute, second); if (!isNaN(date.getTime())) { return date.toISOString(); } } } const fallback = new Date(dateStr); return !isNaN(fallback.getTime()) ? fallback.toISOString() : new Date().toISOString(); }; let isSyncingDB = false; /** * Fetch all rows from Google Sheets and sync to PostgreSQL relational tables */ const syncSheetsToDB = async (io) => { if (isSyncingDB) { console.log('Sync already in progress, skipping...'); return; } if (!SPREADSHEET_ID || SPREADSHEET_ID === 'YOUR_SPREADSHEET_ID_HERE') { console.log('Skipping sync: SPREADSHEET_ID not configured in .env'); return; } isSyncingDB = true; try { const response = await sheets.spreadsheets.values.get({ spreadsheetId: SPREADSHEET_ID, range: 'Sheet1!A2:M', }); const rows = response.data.values; if (!rows || rows.length === 0) { console.log('No data found in Google Sheets.'); return; } let newlyAdded = 0; let updated = 0; const COURSE_MAPPING = { 'DA': 'Data Analytics', 'DS': 'Data Science & AI', 'DM': 'Digital Marketing', 'TALLY': 'Tally ERP & GST', 'UI&UX-Digital Painting': 'UI/UX Design', 'Digital marketing': 'Digital Marketing' }; const COURSE_FEES = { 'Data Analytics': 65000, 'Data Science & AI': 95000, 'Tally ERP & GST': 25000, 'UI/UX Design': 48000, 'Digital Marketing': 40000, 'Full Stack Development': 85000, 'Python Programming': 35000 }; for (const row of rows) { const student_name = row[1] ? row[1].trim() : ''; const phone = row[2] ? row[2].trim() : ''; if (!phone || phone === 'Phone Number' || phone.toLowerCase().includes('contact') || student_name === 'Student Name' || student_name.toLowerCase().includes('name')) continue; const name = student_name || ('Lead - ' + phone); const email = row[3] ? row[3].trim() : ''; const course_interested_raw = row[4] ? row[4].trim() : ''; const course_interested = COURSE_MAPPING[course_interested_raw] || course_interested_raw; const sheet_fees = row[5] ? parseFloat(row[5].toString().replace(/[^0-9.]/g, '')) : 0; const fees = sheet_fees || COURSE_FEES[course_interested] || 35000; const source = row[6] ? row[6].trim() : 'Google Sheet'; let raw_status = row[7] ? row[7].trim() : 'Pending'; let followup_status = 'Pending'; if (raw_status.toLowerCase() === 'not interested' || raw_status.toLowerCase() === 'lost') { followup_status = 'Not Interested'; } else if (raw_status.toLowerCase() === 'done') { followup_status = 'Done'; } else if (raw_status.toLowerCase() === 'converted' || raw_status.toLowerCase() === 'admitted') { followup_status = 'Converted'; } else { followup_status = 'Pending'; } const admission_status = (row[8] && row[8].trim() === 'Admitted') || followup_status === 'Converted' ? 'Admitted' : 'Not Admitted'; if (admission_status === 'Admitted' && followup_status !== 'Converted') { followup_status = 'Converted'; } const counselor_name = row[9] ? row[9].trim() : ''; const last_contacted_str = row[11] || row[10] || ''; const manual_student_id = row[0] ? row[0].trim() : null; let counselorId = null; if (counselor_name) { const { data: existingCounselors, error: cErr } = await db .from('counselors') .select('id') .eq('name', counselor_name); if (cErr) { console.error('Error fetching counselor:', cErr); } else 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, error: createCErr } = await db .from('counselors') .insert([{ name: counselor_name, email: cEmail, role: 'Counselor', organization_id: '00000000-0000-0000-0000-000000000001', branch_id: '00000000-0000-0000-0000-000000000002' }]) .select('id'); if (createCErr) { console.error('Error creating counselor:', createCErr); } else if (newCounselors && newCounselors.length > 0) { counselorId = newCounselors[0].id; } } } const { data: existingLeads, error: selectError } = await db .from('leads') .select('*') .eq('phone', phone); if (selectError) { console.error('Error fetching lead from Supabase:', selectError); continue; } const created_at = parseSheetDate(last_contacted_str); if (existingLeads && existingLeads.length === 0) { const lead_score = calculateLeadScore({ lead_source: source, followup_status: followup_status, interested_course: course_interested }); const nextStudentId = manual_student_id || await generateNextStudentId(); const { data: newLeads, error: insertError } = await db .from('leads') .insert([{ name, student_id: nextStudentId, phone, email, course_interested, source, status: followup_status, lead_score, counselor_id: counselorId, created_at, updated_at: created_at, organization_id: '00000000-0000-0000-0000-000000000001', branch_id: '00000000-0000-0000-0000-000000000002' }]) .select('id'); if (insertError) { console.error('Error inserting lead into Supabase:', insertError); } else if (newLeads && newLeads.length > 0) { const leadId = newLeads[0].id; newlyAdded++; if (io && io.addNotification) { io.addNotification({ title: lead_score > 80 ? "New hot lead assigned 🔥" : "New lead assigned 👤", message: `${name} is interested in ${course_interested} (Score: ${lead_score}%).`, type: "LEAD_ALERT", priority: lead_score > 80 ? "High" : "Low", action_url: `/leads?search=${name}` }); } const { error: followUpError } = await db .from('follow_ups') .insert([{ lead_id: leadId, followup_date: created_at, followup_type: 'Call', status: followup_status === 'Pending' ? 'Pending' : 'Completed', remarks: 'Initial sync from Google Sheets', created_by: counselorId, organization_id: '00000000-0000-0000-0000-000000000001', branch_id: '00000000-0000-0000-0000-000000000002' }]); if (followUpError) { console.error('Error creating follow-up:', followUpError); } if (admission_status === 'Admitted') { const { error: admissionError } = await db .from('admissions') .insert([{ lead_id: leadId, course: course_interested, fees, payment_status: 'Pending', joined_date: created_at, organization_id: '00000000-0000-0000-0000-000000000001', branch_id: '00000000-0000-0000-0000-000000000002' }]); if (admissionError) { console.error('Error creating admission:', admissionError); } } } } else { const lead = existingLeads[0]; const hasBasicChanges = lead.name !== name || lead.course_interested !== course_interested || lead.counselor_id !== counselorId || lead.email !== email || (manual_student_id && lead.student_id !== manual_student_id); const statusChanged = lead.status !== followup_status; if (hasBasicChanges || statusChanged) { const updates = {}; if (lead.name !== name) updates.name = name; if (lead.email !== email) updates.email = email; if (lead.course_interested !== course_interested) updates.course_interested = course_interested; if (lead.counselor_id !== counselorId) updates.counselor_id = counselorId; if (manual_student_id && lead.student_id !== manual_student_id) updates.student_id = manual_student_id; if (statusChanged) updates.status = followup_status; const { error: updateError } = await db .from('leads') .update(updates) .eq('id', lead.id); if (updateError) { console.error('Error updating lead in DB:', updateError); } else { updated++; } } const { data: existingAdmissions, error: admSelectError } = await db .from('admissions') .select('id') .eq('lead_id', lead.id); if (!admSelectError) { const hasAdmissionRecord = existingAdmissions && existingAdmissions.length > 0; if (admission_status === 'Admitted' && !hasAdmissionRecord) { await db.from('admissions').insert([{ lead_id: lead.id, course: course_interested, fees, payment_status: 'Pending', joined_date: created_at, organization_id: '00000000-0000-0000-0000-000000000001', branch_id: '00000000-0000-0000-0000-000000000002' }]); } else if (admission_status !== 'Admitted' && hasAdmissionRecord) { await db.from('admissions').delete().eq('lead_id', lead.id); } } } } if (newlyAdded > 0 || updated > 0) { console.log(`Successfully synced: ${newlyAdded} new leads, ${updated} updated leads.`); if (io) { io.emit('leads_updated', { message: 'New leads synced from Google Sheets', count: newlyAdded + updated }); if (io.addNotification) { io.addNotification({ title: "Google Sheets synced successfully 🔄", message: `Synced ${newlyAdded} new leads, ${updated} updated leads from external database.`, type: "SYSTEM_ALERT", priority: "Low", action_url: "/" }); } } } } catch (error) { console.error('Error syncing Google Sheets to DB:', error); } finally { isSyncingDB = false; } }; /** * Update a specific lead in Google Sheets (DB to Sheet) */ const updateLeadInSheet = async (phone, statusData) => { if (!SPREADSHEET_ID) return; try { const response = await sheets.spreadsheets.values.get({ spreadsheetId: SPREADSHEET_ID, range: 'Sheet1!A:L', }); const rows = response.data.values; if (!rows) return; const rowIndex = rows.findIndex(row => row[2] && row[2].trim() === phone.trim()); if (rowIndex !== -1) { const sheetRowNumber = rowIndex + 1; let statusText = statusData.followup_status; if (statusData.followup_status === 'Pending') { statusText = 'Follow up'; } await sheets.spreadsheets.values.update({ spreadsheetId: SPREADSHEET_ID, range: `Sheet1!H${sheetRowNumber}:I${sheetRowNumber}`, valueInputOption: 'USER_ENTERED', resource: { values: [ [statusText, statusData.admission_status] ] } }); console.log(`Successfully updated lead ${phone} in Google Sheet.`); } } catch (error) { console.error('Error updating Google Sheet:', error); } }; /** * Append a new lead to Google Sheets (DB to Sheet) */ const appendLeadToSheet = async (leadData) => { if (!SPREADSHEET_ID || SPREADSHEET_ID === 'YOUR_SPREADSHEET_ID_HERE') return; try { const timestamp = new Date().toLocaleString(); const values = [ [ leadData.student_id || '', // A (0): STUDENT ID timestamp, // B (1): DATE ADDED leadData.name || '', // C (2): STUDENT PROFILE leadData.phone || '', // D (3): PHONE NUMBER leadData.email || '', // E (4): EMAIL leadData.course_interested || '', // F (5): TARGET PROGRAM '', // G (6): COURSE FEE leadData.source || '', // H (7): LEAD SOURCE leadData.status || 'Pending', // I (8): LEAD STATUS 'Not Admitted', // J (9): ENROLLMENT leadData.counselor_name || '', // K (10): ASSIGN leadData.followup_time || '', // L (11): TIMEFRAME timestamp, // M (12): LAST CONTACTED leadData.lead_score || '', // N (13): SCORE leadData.city || '', // O (14): CITY '' // P (15): STATE ] ]; await sheets.spreadsheets.values.append({ spreadsheetId: SPREADSHEET_ID, range: 'Sheet1!A:Q', valueInputOption: 'USER_ENTERED', insertDataOption: 'INSERT_ROWS', resource: { values } }); console.log(`Successfully appended lead ${leadData.name} to Google Sheet.`); } catch (error) { console.error('Error appending lead to Google Sheet:', error); } }; module.exports = { syncSheetsToDB, updateLeadInSheet, appendLeadToSheet };