File size: 12,311 Bytes
57a1132
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
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
};