const { google } = require('googleapis'); const db = require('../db'); const { calculateLeadScore } = require('./aiEngine'); 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 format with potential time const parts = dateStr.trim().split(/[\/\s:]/); if (parts.length >= 3) { const day = parseInt(parts[0], 10); const month = parseInt(parts[1], 10) - 1; // 0-indexed 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); } // Ensure numbers are valid 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(); }; /** * Fetch all rows from Google Sheets and sync to PostgreSQL relational tables */ const syncSheetsToDB = async (io) => { if (!SPREADSHEET_ID || SPREADSHEET_ID === 'YOUR_SPREADSHEET_ID_HERE') { console.log('Skipping sync: SPREADSHEET_ID not configured in .env'); return; } try { // Read from Sheet1!A2:L to get all columns (ID, Student Name, Phone, Email, Course, Fee, Source, Status, Admission, Counselor, Date, Contacted) const response = await sheets.spreadsheets.values.get({ spreadsheetId: SPREADSHEET_ID, range: 'Sheet1!A2:L', // Fetch up to L }); 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; for (const row of rows) { // Corrected indices based on actual spreadsheet layout: // 0: ID (Serial No) // 1: Student Name // 2: Phone Number // 3: Email // 4: Interested Course // 5: Course Fee // 6: Lead Source // 7: Lead Status (followup_status) // 8: Admission Status // 9: Counselor Name // 10: Follow-up Date // 11: Last Contacted (created_date) const student_name = row[1] || ''; const phone = row[2] || ''; const email = row[3] || ''; const course_interested = row[4] || ''; const fees = row[5] ? parseFloat(row[5].toString().replace(/[^0-9.]/g, '')) : 0; const source = row[6] || ''; const followup_status = row[7] || 'Pending'; const admission_status = row[8] || 'Pending'; // 'Pending' or 'Admitted' const counselor_name = row[9] || ''; const last_contacted_str = row[11] || row[10] || ''; // Fallback to Follow-up Date if Last Contacted is empty if (!phone || phone === 'Phone Number' || student_name === 'Student Name') continue; // Skip header/invalid rows // 1. Dynamic Counselor Lookup / Creation 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 { // Dynamically create counselor with a generated unique email address 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' }]) .select('id'); if (createCErr) { console.error('Error creating counselor:', createCErr); } else if (newCounselors && newCounselors.length > 0) { counselorId = newCounselors[0].id; } } } // 2. Check if lead already exists based on phone const { data: existingLeads, error: selectError } = await db .from('leads') .select('*') .eq('phone', phone); if (selectError) { console.error('Error fetching lead from Supabase:', selectError); continue; } // Parse dates safely const created_at = parseSheetDate(last_contacted_str); if (existingLeads && existingLeads.length === 0) { // Calculate AI Lead Score const lead_score = calculateLeadScore({ lead_source: source, followup_status: followup_status, interested_course: course_interested }); // Insert new lead const { data: newLeads, error: insertError } = await db .from('leads') .insert([{ name: student_name, phone, email, course_interested, source, status: followup_status, lead_score, counselor_id: counselorId, created_at, updated_at: created_at }]) .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: `${student_name} is interested in ${course_interested} (Score: ${lead_score}%).`, type: "LEAD_ALERT", priority: lead_score > 80 ? "High" : "Low", action_url: `/leads?search=${student_name}` }); } // Create dynamic linked follow-up record 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 }]); if (followUpError) { console.error('Error creating follow-up:', followUpError); } // Create dynamic linked admission record if Admitted 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 }]); if (admissionError) { console.error('Error creating admission:', admissionError); } } } } else { // True Two-Way Sync logic: // Update DB if Sheet has new basic info const lead = existingLeads[0]; const hasBasicChanges = lead.name !== student_name || lead.course_interested !== course_interested || lead.counselor_id !== counselorId || lead.email !== email; const statusChanged = lead.status !== followup_status; if (hasBasicChanges || statusChanged) { const updates = {}; if (lead.name !== student_name) updates.name = student_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 (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++; } } // Sync linked admissions record based on admission status 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) { // Create admissions record dynamically await db.from('admissions').insert([{ lead_id: lead.id, course: course_interested, fees, payment_status: 'Pending', joined_date: created_at }]); } else if (admission_status !== 'Admitted' && hasAdmissionRecord) { // Remove admissions record 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); } }; /** * Update a specific lead in Google Sheets (DB to Sheet) */ const updateLeadInSheet = async (phone, statusData) => { if (!SPREADSHEET_ID) return; try { // Fetch all rows to find the exact row index const response = await sheets.spreadsheets.values.get({ spreadsheetId: SPREADSHEET_ID, range: 'Sheet1!A:L', }); const rows = response.data.values; if (!rows) return; // Find row index (Adding 1 because array is 0-indexed and sheet is 1-indexed) const rowIndex = rows.findIndex(row => row[2] === phone); // Phone is row[2] if (rowIndex !== -1) { const sheetRowNumber = rowIndex + 1; // Column H is Lead Status (row[7]) and Column I is Admission Status (row[8]) // We update just those cells to avoid overwriting other data await sheets.spreadsheets.values.update({ spreadsheetId: SPREADSHEET_ID, range: `Sheet1!H${sheetRowNumber}:I${sheetRowNumber}`, valueInputOption: 'USER_ENTERED', resource: { values: [ [statusData.followup_status, statusData.admission_status] ] } }); console.log(`Successfully updated lead ${phone} in Google Sheet.`); } } catch (error) { console.error('Error updating Google Sheet:', error); } }; module.exports = { syncSheetsToDB, updateLeadInSheet };